Implemented new datasources (modbus,scpi)
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
)
|
||||
|
||||
// Config is the [datasource.modbus] section. Devices share no connection state;
|
||||
// each is polled independently over its own TCP socket.
|
||||
type Config struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
// PollIntervalMs is the default polling period for every register that does
|
||||
// not override it. Zero → 1000 ms.
|
||||
PollIntervalMs int `toml:"poll_interval_ms"`
|
||||
Devices []Device `toml:"devices"`
|
||||
}
|
||||
|
||||
// Device is one Modbus TCP slave. Address is "host:port" (default port 502 is
|
||||
// appended if absent). UnitID is the Modbus unit/slave identifier (0–255).
|
||||
type Device struct {
|
||||
Name string `toml:"name"`
|
||||
Address string `toml:"address"`
|
||||
UnitID uint8 `toml:"unit_id"`
|
||||
TimeoutMs int `toml:"timeout_ms"`
|
||||
Registers []Register `toml:"registers"`
|
||||
}
|
||||
|
||||
// Register describes one logical signal mapped onto a Modbus address.
|
||||
//
|
||||
// - Kind selects the address space / function code:
|
||||
// "holding" (FC03/06/10), "input" (FC04, read-only),
|
||||
// "coil" (FC01/05, bool), "discrete" (FC02, read-only bool).
|
||||
// - Encoding selects how holding/input words are decoded:
|
||||
// "uint16", "int16", "uint32", "int32", "float32", "float64".
|
||||
// Ignored for coil/discrete (always bool).
|
||||
// - WordOrder is "big" (default, high word first) or "little" for the
|
||||
// multi-word encodings.
|
||||
// - Scale/Offset transform the raw numeric value: value*Scale + Offset.
|
||||
// Scale 0 is treated as 1.
|
||||
type Register struct {
|
||||
Name string `toml:"name"`
|
||||
Kind string `toml:"kind"`
|
||||
Address uint16 `toml:"address"`
|
||||
Encoding string `toml:"encoding"`
|
||||
WordOrder string `toml:"word_order"`
|
||||
Unit string `toml:"unit"`
|
||||
Scale float64 `toml:"scale"`
|
||||
Offset float64 `toml:"offset"`
|
||||
Min float64 `toml:"min"`
|
||||
Max float64 `toml:"max"`
|
||||
Writable bool `toml:"writable"`
|
||||
Description string `toml:"description"`
|
||||
}
|
||||
|
||||
// register kinds.
|
||||
const (
|
||||
kindHolding = "holding"
|
||||
kindInput = "input"
|
||||
kindCoil = "coil"
|
||||
kindDiscrete = "discrete"
|
||||
)
|
||||
|
||||
// isBool reports whether the register addresses a single-bit space.
|
||||
func (r Register) isBool() bool {
|
||||
return r.kind() == kindCoil || r.kind() == kindDiscrete
|
||||
}
|
||||
|
||||
func (r Register) kind() string {
|
||||
if r.Kind == "" {
|
||||
return kindHolding
|
||||
}
|
||||
return strings.ToLower(r.Kind)
|
||||
}
|
||||
|
||||
func (r Register) encoding() string {
|
||||
if r.Encoding == "" {
|
||||
return "uint16"
|
||||
}
|
||||
return strings.ToLower(r.Encoding)
|
||||
}
|
||||
|
||||
func (r Register) littleWordOrder() bool {
|
||||
return strings.ToLower(r.WordOrder) == "little"
|
||||
}
|
||||
|
||||
func (r Register) scale() float64 {
|
||||
if r.Scale == 0 {
|
||||
return 1
|
||||
}
|
||||
return r.Scale
|
||||
}
|
||||
|
||||
// wordCount returns the number of 16-bit registers the encoding occupies.
|
||||
func (r Register) wordCount() (int, error) {
|
||||
switch r.encoding() {
|
||||
case "uint16", "int16":
|
||||
return 1, nil
|
||||
case "uint32", "int32", "float32":
|
||||
return 2, nil
|
||||
case "float64":
|
||||
return 4, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("modbus: unknown encoding %q", r.Encoding)
|
||||
}
|
||||
}
|
||||
|
||||
// dataType maps the register to a datasource value type.
|
||||
func (r Register) dataType() datasource.DataType {
|
||||
if r.isBool() {
|
||||
return datasource.TypeBool
|
||||
}
|
||||
// Integer encodings with no fractional scaling stay integers; anything
|
||||
// scaled, offset, or float-encoded becomes a float64.
|
||||
switch r.encoding() {
|
||||
case "float32", "float64":
|
||||
return datasource.TypeFloat64
|
||||
default:
|
||||
if r.scale() == 1 && r.Offset == 0 {
|
||||
return datasource.TypeInt64
|
||||
}
|
||||
return datasource.TypeFloat64
|
||||
}
|
||||
}
|
||||
|
||||
// writable reports whether writes are permitted. Input registers and discrete
|
||||
// inputs are read-only regardless of the Writable flag.
|
||||
func (r Register) writable() bool {
|
||||
switch r.kind() {
|
||||
case kindInput, kindDiscrete:
|
||||
return false
|
||||
default:
|
||||
return r.Writable
|
||||
}
|
||||
}
|
||||
|
||||
// metadata builds the datasource.Metadata for this register under signal name
|
||||
// "device:register".
|
||||
func (r Register) metadata(device string) datasource.Metadata {
|
||||
return datasource.Metadata{
|
||||
Name: device + ":" + r.Name,
|
||||
Type: r.dataType(),
|
||||
Unit: r.Unit,
|
||||
Description: r.Description,
|
||||
DisplayLow: r.Min,
|
||||
DisplayHigh: r.Max,
|
||||
DriveLow: r.Min,
|
||||
DriveHigh: r.Max,
|
||||
Writable: r.writable(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user