Files
uopi/internal/datasource/modbus/config.go
T
2026-06-24 06:12:52 +02:00

153 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (0255).
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(),
}
}