diff --git a/cmd/confighandler.go b/cmd/confighandler.go index 058bd3a8..f7c124eb 100644 --- a/cmd/confighandler.go +++ b/cmd/confighandler.go @@ -77,6 +77,7 @@ type InfluxConfig struct { // AdapterConfig describes device communication parameters type AdapterConfig struct { Device string + RTU bool Baudrate int Comset string } @@ -104,12 +105,18 @@ func NewDeviceConfigHandler() *DeviceConfigHandler { } // createConnection parses adapter string to create TCP or RTU connection -func createConnection(device string, baudrate int, comset string) (res meters.Connection) { +func createConnection(device string, rtu bool, baudrate int, comset string) (res meters.Connection) { if device == "mock" { res = meters.NewMock(device) // mocked connection } else if tcp, _ := regexp.MatchString(":[0-9]+$", device); tcp { - log.Printf("config: creating TCP connection for %s", device) - res = meters.NewTCP(device) // tcp connection + if rtu { + // special case: RTU over TCP + log.Printf("config: creating RTU over TCP connection for %s", device) + res = meters.NewRTUOverTCP(device) // tcp connection + } else { + log.Printf("config: creating TCP connection for %s", device) + res = meters.NewTCP(device) // tcp connection + } } else { log.Printf("config: creating RTU connection for %s (%dbaud, %s)", device, baudrate, comset) if baudrate == 0 || comset == "" { @@ -123,23 +130,10 @@ func createConnection(device string, baudrate int, comset string) (res meters.Co return res } -// CreateAdapter creates a connection handler for given adapter configuration. -// While connectionManager does the same it is not able to configure the connection. -func (conf *DeviceConfigHandler) CreateAdapter(connSpec string, baudrate int, comset string) meters.Manager { - manager, ok := conf.Managers[connSpec] - if !ok { - conn := createConnection(connSpec, baudrate, comset) - manager = meters.NewManager(conn) - conf.Managers[connSpec] = manager - } - - return manager -} - -func (conf *DeviceConfigHandler) connectionManager(connSpec string) meters.Manager { +func (conf *DeviceConfigHandler) ConnectionManager(connSpec string, rtu bool, baudrate int, comset string) meters.Manager { manager, ok := conf.Managers[connSpec] if !ok { - conn := createConnection(connSpec, 0, "") + conn := createConnection(connSpec, rtu, baudrate, comset) manager = meters.NewManager(conn) conf.Managers[connSpec] = manager } @@ -177,6 +171,12 @@ func (conf *DeviceConfigHandler) createDeviceForManager( return meter } +// isRTUDevice checks if type is a known RTU device type +func (conf *DeviceConfigHandler) isRTUDevice(meterType string) bool { + _, ok := rs485.Producers[strings.ToUpper(meterType)] + return ok +} + // CreateDevice creates new device and adds it to the connection manager func (conf *DeviceConfigHandler) CreateDevice(devConf DeviceConfig) { if devConf.Adapter == "" { @@ -191,7 +191,10 @@ func (conf *DeviceConfigHandler) CreateDevice(devConf DeviceConfig) { } } - manager := conf.connectionManager(devConf.Adapter) + manager, ok := conf.Managers[devConf.Adapter] + if !ok { + log.Fatalf("Missing adapter configuration for device %v", devConf) + } meter := conf.createDeviceForManager(manager, devConf.Type) if err := manager.Add(devConf.ID, meter); err != nil { @@ -217,7 +220,6 @@ func (conf *DeviceConfigHandler) CreateDeviceFromSpec(deviceDef string) { log.Fatalf("Cannot parse connect string- missing physical device or connection for %s. See -h for help.", deviceDef) } - manager := conf.connectionManager(connSpec) meterSplit := strings.Split(meterDef, ":") if len(meterSplit) != 2 { log.Fatalf("Cannot parse device definition: %s. See -h for help.", meterDef) @@ -233,6 +235,11 @@ func (conf *DeviceConfigHandler) CreateDeviceFromSpec(deviceDef string) { log.Fatalf("Error parsing device id %s: %v. See -h for help.", devID, err) } + // RTU flag is inferred if the device is of RTU type. + // It is used to implicitly create an RTUOverTCP instead of a TCP connection + rtu := conf.isRTUDevice(meterType) + manager := conf.ConnectionManager(connSpec, rtu, 0, "") + meter := conf.createDeviceForManager(manager, meterType) if err := manager.Add(uint8(id), meter); err != nil { log.Fatalf("Error adding device %s: %v. See -h for help.", meterDef, err) diff --git a/cmd/inspect.go b/cmd/inspect.go index 236865e1..20796b36 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -128,13 +128,17 @@ func printModel(m *smdx.ModelElement) { } func inspect(cmd *cobra.Command, args []string) { + if len(args) > 0 { + log.Fatalf("excess arguments, aborting: %v", args) + } + confHandler := NewDeviceConfigHandler() // create default adapter from configuration defaultDevice := viper.GetString("adapter") if defaultDevice != "" { confHandler.DefaultDevice = defaultDevice - confHandler.CreateAdapter(defaultDevice, viper.GetInt("baudrate"), viper.GetString("comset")) + confHandler.ConnectionManager(defaultDevice, viper.GetBool("rtu"), viper.GetInt("baudrate"), viper.GetString("comset")) } // create devices from command line diff --git a/cmd/root.go b/cmd/root.go index dca15f8c..51b7e392 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -52,6 +52,13 @@ The default adapter can be overridden per device`, "8N1", `Communication parameters for default adapter, either 8N1 or 8E1. Only applicable if the default adapter is an RTU device`, + ) + rootCmd.PersistentFlags().Bool( + "rtu", + false, + `Use RTU over TCP for default adapter. +Typically used with RS485 to Ethernet adapters that don't perform protocol conversion (e.g. USR-TCP232). +Only applicable if the default adapter is a TCP connection`, ) rootCmd.PersistentFlags().BoolP( "help", "h", diff --git a/cmd/run.go b/cmd/run.go index c3ca8935..02b1ab74 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -181,6 +181,9 @@ func checkVersion() { func run(cmd *cobra.Command, args []string) { log.Printf("mbmd %s (%s)", server.Version, server.Commit) + if len(args) > 0 { + log.Fatalf("excess arguments, aborting: %v", args) + } go checkVersion() confHandler := NewDeviceConfigHandler() @@ -189,7 +192,7 @@ func run(cmd *cobra.Command, args []string) { defaultDevice := viper.GetString("adapter") if defaultDevice != "" { confHandler.DefaultDevice = defaultDevice - confHandler.CreateAdapter(defaultDevice, viper.GetInt("baudrate"), viper.GetString("comset")) + confHandler.ConnectionManager(defaultDevice, viper.GetBool("rtu"), viper.GetInt("baudrate"), viper.GetString("comset")) } // create devices from command line @@ -213,7 +216,7 @@ func run(cmd *cobra.Command, args []string) { if len(devices) == 0 { // add adapters from configuration for _, a := range conf.Adapters { - confHandler.CreateAdapter(a.Device, a.Baudrate, a.Comset) + confHandler.ConnectionManager(a.Device, a.RTU, a.Baudrate, a.Comset) } // add devices from configuration diff --git a/cmd/scan.go b/cmd/scan.go index ddb01353..0decade2 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -57,13 +57,17 @@ func (v validator) check(f float64) bool { } func scan(cmd *cobra.Command, args []string) { + if len(args) > 0 { + log.Fatalf("excess arguments, aborting: %v", args) + } + // create connection adapter := viper.GetString("adapter") if adapter == "" { - log.Fatal("Missing adapter configuration") + log.Fatal("missing adapter configuration") } - conn := createConnection(adapter, viper.GetInt("baudrate"), viper.GetString("comset")) + conn := createConnection(adapter, viper.GetBool("rtu"), viper.GetInt("baudrate"), viper.GetString("comset")) client := conn.ModbusClient() // raw log diff --git a/go.mod b/go.mod index 4478d566..a4befdac 100644 --- a/go.mod +++ b/go.mod @@ -29,4 +29,6 @@ require ( gopkg.in/yaml.v2 v2.2.7 // indirect ) +replace github.com/grid-x/modbus v0.0.0-20191105145357-867898f52408 => github.com/andig/gridx-modbus v0.0.0-20191223181555-ff0a69a1ad97 + go 1.13 diff --git a/go.sum b/go.sum index c397652f..508281c6 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andig/gosunspec v0.0.0-20191120114235-802a74abaa2d h1:IX1Jqj7owJlsaDtsxPlRjI/JdHQJ8BIM/rXL2j1ILyA= github.com/andig/gosunspec v0.0.0-20191120114235-802a74abaa2d/go.mod h1:YkshK8WMzYn1iXAZzHUO75gIqhMSan2ctgBVtBkRIyA= +github.com/andig/gridx-modbus v0.0.0-20191223181555-ff0a69a1ad97 h1:3jXowxWJbymtgs7uDmsPTy2rsOr4IvpOLQ4rog6nAaQ= +github.com/andig/gridx-modbus v0.0.0-20191223181555-ff0a69a1ad97/go.mod h1:9mi0a7/tbYsUWwsC32q/R0r2p1lkNt+8li81Zaf/yTE= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= diff --git a/mbmd.dist.yaml b/mbmd.dist.yaml index 5c57efa3..207b7963 100644 --- a/mbmd.dist.yaml +++ b/mbmd.dist.yaml @@ -25,12 +25,19 @@ adapters: - device: /dev/ttyUSB0 baudrate: 9600 comset: 8N1 # "8E1" needs be quoted as string or will error +- device: 192.168.0.7:23 + rtu: true # Modbus RS485 to Ethernet converter uses RTU over TCP # list of devices devices: -- type: sdm +- name: sdm1 + type: sdm id: 1 adapter: /dev/ttyUSB0 +- name: sdm2 + type: sdm + id: 1 + adapter: 192.168.0.7:23 - name: sma1 type: sunspec id: 126 diff --git a/meters/rtuovertcp.go b/meters/rtuovertcp.go new file mode 100644 index 00000000..c3af3f08 --- /dev/null +++ b/meters/rtuovertcp.go @@ -0,0 +1,73 @@ +package meters + +import ( + "time" + + "github.com/grid-x/modbus" +) + +// RTUOverTCP is an RTU encoder over a TCP modbus connection +type RTUOverTCP struct { + address string + Client modbus.Client + Handler *modbus.RTUOverTCPClientHandler +} + +// NewRTUOverTCPClientHandler creates a TCP modbus handler +func NewRTUOverTCPClientHandler(device string) *modbus.RTUOverTCPClientHandler { + handler := modbus.NewRTUOverTCPClientHandler(device) + + // set default timings + handler.Timeout = 1 * time.Second + handler.ProtocolRecoveryTimeout = 10 * time.Second // not used + handler.LinkRecoveryTimeout = 15 * time.Second // not used + + return handler +} + +// NewRTUOverTCP creates a TCP modbus client +func NewRTUOverTCP(address string) Connection { + handler := NewRTUOverTCPClientHandler(address) + client := modbus.NewClient(handler) + + b := &RTUOverTCP{ + address: address, + Client: client, + Handler: handler, + } + + return b +} + +// String returns the bus connection address (TCP) +func (b *RTUOverTCP) String() string { + return b.address +} + +// ModbusClient returns the TCP modbus client +func (b *RTUOverTCP) ModbusClient() modbus.Client { + return b.Client +} + +// Logger sets a logging instance for physical bus operations +func (b *RTUOverTCP) Logger(l Logger) { + b.Handler.Logger = l +} + +// Slave sets the modbus device id for the following operations +func (b *RTUOverTCP) Slave(deviceID uint8) { + b.Handler.SetSlave(deviceID) +} + +// Timeout sets the modbus timeout +func (b *RTUOverTCP) Timeout(timeout time.Duration) time.Duration { + t := b.Handler.Timeout + b.Handler.Timeout = timeout + return t +} + +// Close closes the modbus connection. +// This forces the modbus client to reopen the connection before the next bus operations. +func (b *RTUOverTCP) Close() { + b.Handler.Close() +}