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(), } }