commit 29d76388e0d0991ebf9bae9d700211f708855236 Author: Thomas Klaehn Date: Thu Dec 15 11:10:01 2022 +0100 Initial commit diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6deb9a6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go" + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7643d05 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module modbusbridge + +go 1.19 + +require github.com/simonvetter/modbus v1.5.1 + +require github.com/goburrow/serial v0.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..155d37a --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= +github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= +github.com/simonvetter/modbus v1.5.1 h1:OmCf8+ErlkDeDa0AcxoOs17rr9p9RVusCvJ4rEOsRCQ= +github.com/simonvetter/modbus v1.5.1/go.mod h1:Dj4SBrfEUBg+qCRH6C7bCsZYEvQWTfAVy1Xx+WN3IA4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7ece6f4 --- /dev/null +++ b/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "log" + "strconv" + "time" + + "github.com/simonvetter/modbus" +) + +var ( + tcp_server *modbus.ModbusServer + rtu_client *modbus.ModbusClient + Logger log.Logger = *log.Default() +) + +const ( + SERVER = "[::]" + PORT = 502 + SERIAL_INTERFACE = "/dev/ttyUSB0" + BAUD = 9600 +) + +func init() { + Logger.SetPrefix("modbus_bridge: ") + Logger.Println("Starting") + + // create modbus tcp server + timeout := 30 * time.Second + var server_cfg = modbus.ServerConfiguration{ + URL: "tcp://" + SERVER + ":" + strconv.Itoa(PORT), + Timeout: timeout, + MaxClients: 5, + } + var err error + handler := &modbusTcpHandler{} + tcp_server, err = modbus.NewServer(&server_cfg, handler) + if err != nil { + Logger.Printf("failed to create server: %v\n", err) + return + } + + // create modbus rtu client + url := "rtu://" + SERIAL_INTERFACE + rtu_client, err = modbus.NewClient(&modbus.ClientConfiguration{ + URL: url, + Speed: BAUD, + DataBits: 8, + Parity: modbus.PARITY_NONE, + StopBits: 1, + Timeout: 300 * time.Millisecond, + }) + if err != nil { + Logger.Printf("Could not create new modbus rtu client on %s (%s).", url, err.Error()) + err = modbus.ErrConfigurationError + return + } + err = rtu_client.Open() + if err != nil { + Logger.Printf("Could not open new modbus rtu client on %s (%s).", url, err.Error()) + err = modbus.ErrConfigurationError + return + } +} + +func main() { + if tcp_server != nil { + Logger.Print("Starting modbus tcp server") + err := tcp_server.Start() + if err != nil { + Logger.Printf("Unable to start modbus tcp server: %s", err.Error()) + } + } + for { + time.Sleep(time.Millisecond * 500) + } +} diff --git a/modbusbridge.service b/modbusbridge.service new file mode 100644 index 0000000..1d93b5f --- /dev/null +++ b/modbusbridge.service @@ -0,0 +1,10 @@ +[Unit] +Description=Relay rpc service +After=multi-user.target + +[Service] +Type=idle +ExecStart=/usr/bin/modbusbridge + +[Install] +WantedBy=multi-user.target diff --git a/tcphandler.go b/tcphandler.go new file mode 100644 index 0000000..79486a2 --- /dev/null +++ b/tcphandler.go @@ -0,0 +1,127 @@ +package main + +import ( + "sync" + + "github.com/simonvetter/modbus" +) + +// Handler object, passed to the NewServer() +type modbusTcpHandler struct { + // this lock is used to avoid concurrency issues between goroutines, as + // handler methods are called from different requests + lock sync.RWMutex +} + +// Coil handler method. +// This method gets called whenever a valid modbus request asking for a coil operation is +// received by the server. +func (handler *modbusTcpHandler) HandleCoils(req *modbus.CoilsRequest) (res []bool, err error) { + if rtu_client == nil { + Logger.Print("Rtu client not running.") + return + } + err = rtu_client.SetUnitId(req.UnitId) + if err != nil { + Logger.Printf("Could not set unit id (%s).", err.Error()) + err = modbus.ErrConfigurationError + return + } + + handler.lock.Lock() + defer handler.lock.Unlock() + + if req.IsWrite { + err = rtu_client.WriteCoils(req.Addr, req.Args) + if err != nil { + Logger.Printf("Could not write coil(s) (%s).", err.Error()) + err = modbus.ErrServerDeviceFailure + return + } + } else { + res, err = rtu_client.ReadCoils(req.Addr, req.Quantity) + if err != nil { + Logger.Printf("Could not read coil(s) (%s).", err.Error()) + err = modbus.ErrServerDeviceFailure + return + } + } + err = nil + return +} + +// Discrete input handler method. +// Note that we're returning ErrIllegalFunction unconditionally. +func (handler *modbusTcpHandler) HandleDiscreteInputs(req *modbus.DiscreteInputsRequest) (res []bool, err error) { + // this is the equivalent of saying + // "discrete inputs are not supported by this device" + err = modbus.ErrIllegalFunction + + return +} + +// Holding register handler method. +// This method gets called whenever a valid modbus request asking for a holding register +// operation (either read or write) received by the server. +func (handler *modbusTcpHandler) HandleHoldingRegisters(req *modbus.HoldingRegistersRequest) (res []uint16, err error) { + if rtu_client == nil { + Logger.Print("Rtu client not running.") + return + } + err = rtu_client.SetUnitId(req.UnitId) + if err != nil { + Logger.Printf("Could not set unit id (%s).", err.Error()) + err = modbus.ErrConfigurationError + return + } + + handler.lock.Lock() + defer handler.lock.Unlock() + + if req.IsWrite { + err = rtu_client.WriteRegisters(req.Addr, req.Args) + if err != nil { + Logger.Printf("Could not write holding register(s) (%s).", err.Error()) + err = modbus.ErrServerDeviceFailure + return + } + } else { + res, err = rtu_client.ReadRegisters(req.Addr, req.Quantity, modbus.HOLDING_REGISTER) + if err != nil { + Logger.Printf("Could not read holding register(s) (%s).", err.Error()) + err = modbus.ErrServerDeviceFailure + return + } + } + err = nil + return +} + +// Input register handler method. +// This method gets called whenever a valid modbus request asking for an input register +// operation is received by the server. +// Note that input registers are always read-only as per the modbus spec. +func (handler *modbusTcpHandler) HandleInputRegisters(req *modbus.InputRegistersRequest) (res []uint16, err error) { + if rtu_client == nil { + Logger.Print("Rtu client not running.") + return + } + err = rtu_client.SetUnitId(req.UnitId) + if err != nil { + Logger.Printf("Could not set unit id (%s).", err.Error()) + err = modbus.ErrConfigurationError + return + } + + handler.lock.Lock() + defer handler.lock.Unlock() + + res, err = rtu_client.ReadRegisters(req.Addr, req.Quantity, modbus.INPUT_REGISTER) + if err != nil { + Logger.Printf("Could not read input register(s) (%s).", err.Error()) + err = modbus.ErrServerDeviceFailure + return + } + err = nil + return +}