Skip to content

feat(modbus_server): add modbus_server io plugins #16669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ following works:
- github.com/go-sql-driver/mysql [Mozilla Public License 2.0](https://github.com/go-sql-driver/mysql/blob/master/LICENSE)
- github.com/go-stack/stack [MIT License](https://github.com/go-stack/stack/blob/master/LICENSE.md)
- github.com/go-stomp/stomp [Apache License 2.0](https://github.com/go-stomp/stomp/blob/master/LICENSE.txt)
- github.com/goburrow/serial [MIT License](https://github.com/goburrow/serial?tab=MIT-1-ov-file#readme)
- github.com/gobwas/glob [MIT License](https://github.com/gobwas/glob/blob/master/LICENSE)
- github.com/goccy/go-json [MIT License](https://github.com/goccy/go-json/blob/master/LICENSE)
- github.com/godbus/dbus [BSD 2-Clause "Simplified" License](https://github.com/godbus/dbus/blob/master/LICENSE)
Expand Down Expand Up @@ -361,6 +362,7 @@ following works:
- github.com/signalfx/golib [Apache License 2.0](https://github.com/signalfx/golib/blob/master/LICENSE)
- github.com/signalfx/sapm-proto [Apache License 2.0](https://github.com/signalfx/sapm-proto/blob/master/LICENSE)
- github.com/sijms/go-ora [MIT License](https://github.com/sijms/go-ora/blob/master/LICENSE)
- github.com/simonvetter/modbus [MIT License](https://github.com/simonvetter/modbus/blob/master/LICENSE.txt)
- github.com/sirupsen/logrus [MIT License](https://github.com/sirupsen/logrus/blob/master/LICENSE)
- github.com/sleepinggenius2/gosmi [MIT License](https://github.com/sleepinggenius2/gosmi/blob/master/LICENSE)
- github.com/snowflakedb/gosnowflake [Apache License 2.0](https://github.com/snowflakedb/gosnowflake/blob/master/LICENSE)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ require (
github.com/showwin/speedtest-go v1.7.10
github.com/signalfx/golib/v3 v3.3.54
github.com/sijms/go-ora/v2 v2.8.22
github.com/simonvetter/modbus v1.6.3
github.com/sirupsen/logrus v1.9.3
github.com/sleepinggenius2/gosmi v0.4.4
github.com/snowflakedb/gosnowflake v1.11.2
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,7 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro=
github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg=
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd h1:qJthTC7IG7e/QYR4i2QHxcDmDdB72FXsaGo4CUQvsPo=
github.com/goburrow/serial v0.1.1-0.20211022031912-bfb69110f8dd/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
Expand Down Expand Up @@ -2212,6 +2213,8 @@ github.com/signalfx/sapm-proto v0.12.0 h1:OtOe+Jm8L61Ml8K6X8a89zc8/RlaaMRElCImeG
github.com/signalfx/sapm-proto v0.12.0/go.mod h1:wQEki8RNCYjkv19jw5aWDcmDMTQru0ckfUbgHI69U2E=
github.com/sijms/go-ora/v2 v2.8.22 h1:3ABgRzVKxS439cEgSLjFKutIwOyhnyi4oOSBywEdOlU=
github.com/sijms/go-ora/v2 v2.8.22/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
github.com/simonvetter/modbus v1.6.3 h1:kDzwVfIPczsM4Iz09il/Dij/bqlT4XiJVa0GYaOVA9w=
github.com/simonvetter/modbus v1.6.3/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd8q+GVRObPw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
Expand Down
251 changes: 251 additions & 0 deletions plugins/common/modbus_server/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package modbus_server

import (
"sync"
"time"

"github.com/simonvetter/modbus"

"github.com/influxdata/telegraf"
)

// Handler object, passed to the NewServer() constructor above.
type Handler struct {
// this lock is used to avoid concurrency issues between goroutines, as
// Handler methods are called from different goroutines
// (1 goroutine per client)
lock sync.RWMutex

// these are here to hold client-provided (written) values, for both coils and
// holding registers

coils []bool
coilOffset uint16
holdingRegisters []uint16
registerOffset uint16
LastEdit chan time.Time
logger telegraf.Logger
}

func NewRequestHandler(coilsLen, coilOffset, registersLen, registerOffset uint16, logger telegraf.Logger) (*Handler, error) {
if coilsLen == 0 && registersLen == 0 {
return nil, modbus.ErrConfigurationError
}

return &Handler{
coils: make([]bool, coilsLen),
coilOffset: coilOffset,
holdingRegisters: make([]uint16, registersLen),
registerOffset: registerOffset,
LastEdit: make(chan time.Time, 1),
logger: logger,
}, nil
}

func (h *Handler) updateLastEdit() {
// Check if the channel is empty. If empty write the current time to the channel, otherwise update the time.
select {
case <-h.LastEdit:
h.LastEdit <- time.Now()
default:
h.LastEdit <- time.Now()
}
}

func (h *Handler) GetCoilsAndOffset() ([]bool, uint16) {
h.lock.Lock()
defer h.lock.Unlock()

coils := make([]bool, len(h.coils))
registers := make([]uint16, len(h.holdingRegisters))

copy(coils, h.coils)
copy(registers, h.holdingRegisters)

return coils, h.coilOffset
}

func (h *Handler) GetRegistersAndOffset() ([]uint16, uint16) {
h.lock.Lock()
defer h.lock.Unlock()

coils := make([]bool, len(h.coils))
registers := make([]uint16, len(h.holdingRegisters))

copy(coils, h.coils)
copy(registers, h.holdingRegisters)

return registers, h.registerOffset
}

func (h *Handler) getRegisters(address, quantity uint16) ([]uint16, error) {
if address < h.registerOffset || address+quantity > h.registerOffset+uint16(len(h.holdingRegisters)) {
h.logger.Errorf("Reading address out of range: %v, %v, %v", address, quantity, h.registerOffset)
return nil, modbus.ErrIllegalDataAddress
}

res := make([]uint16, quantity)
copy(res, h.holdingRegisters[address-h.registerOffset:address-h.registerOffset+quantity])

return res, nil
}

func (h *Handler) setRegisters(address uint16, values []uint16) []uint16 {
res := make([]uint16, 0)
for i, value := range values {
// check if the address is within the range of the holding registers, if not skip the value
if address+uint16(i) >= h.registerOffset+uint16(len(h.holdingRegisters)) || address+uint16(i) < h.registerOffset {
continue
}
h.holdingRegisters[address-h.registerOffset+uint16(i)] = value
res = append(res, value)
}
return res
}

func (h *Handler) WriteBitToHoldingRegister(address uint16, bitValue bool, bitIndex uint8) (register uint16, err error) {
h.lock.Lock()
defer h.lock.Unlock()

registers, err := h.getRegisters(address, 1)
if err != nil {
return 0, err
}

currentValue := registers[0]
if bitValue {
// Set the bit (use OR to ensure the bit is 1)
currentValue |= 1 << bitIndex
} else {
// Clear the bit (use AND with NOT to ensure the bit is 0)
currentValue &^= 1 << bitIndex
}

registers = h.setRegisters(address, []uint16{currentValue})
if len(registers) == 0 {
return 0, nil
}

h.updateLastEdit()
return registers[0], nil
}

func (h *Handler) WriteCoils(address uint16, values []bool) ([]bool, error) {
h.lock.Lock()
defer h.lock.Unlock()

res := make([]bool, 0)
for i, value := range values {
// check if the address is within the range of the coils, if not skip the value
if address+uint16(i) >= h.coilOffset+uint16(len(h.coils)) || address+uint16(i) < h.coilOffset {
continue
}
h.coils[address-h.coilOffset+uint16(i)] = value
res = append(res, value)
}

h.updateLastEdit()
return res, nil
}

func (h *Handler) ReadCoils(address, quantity uint16) ([]bool, error) {
h.lock.Lock()
defer h.lock.Unlock()

// check if the address is within the range of the coils
if address < h.coilOffset || address+quantity > h.coilOffset+uint16(len(h.coils)) {
h.logger.Errorf("Reading address out of range: %v, %v, %v", address, quantity, h.coilOffset)
return nil, modbus.ErrIllegalDataAddress
}

res := make([]bool, quantity)
copy(res, h.coils[address-h.coilOffset:address-h.coilOffset+quantity])
return res, nil
}

func (h *Handler) WriteHoldingRegisters(address uint16, values []uint16) ([]uint16, error) {
h.lock.Lock()
defer h.lock.Unlock()

res := h.setRegisters(address, values)

if len(res) > 0 {
h.updateLastEdit()
}

return res, nil
}

func (h *Handler) ReadHoldingRegisters(address, quantity uint16) ([]uint16, error) {
h.lock.Lock()
defer h.lock.Unlock()

if address < h.registerOffset || address+quantity > h.registerOffset+uint16(len(h.holdingRegisters)) {
h.logger.Errorf("Reading address out of range: %v, %v, %v", address, quantity, h.registerOffset)
return nil, modbus.ErrIllegalDataAddress
}

res := make([]uint16, quantity)
copy(res, h.holdingRegisters[address-h.registerOffset:address-h.registerOffset+quantity])
return res, nil
}

// HandleCoils handler method.
// This method gets called whenever a valid modbus request asking for a coil operation is
// received by the server.
func (h *Handler) HandleCoils(req *modbus.CoilsRequest) (res []bool, err error) {
h.logger.Debugf("Handling coils request: %+v", req)
if req.IsWrite {
h.logger.Debugf("Writing coils: %+v, args: %+v", req.Addr, req.Args)
res, err = h.WriteCoils(req.Addr, req.Args)
h.logger.Debugf("Write coils: %+v", res)
// Check if the channel is empty. If empty write the current time to the channel, otherwise update the time.
} else {
h.logger.Debugf("Reading coils: %+v, quantity %+v", req.Addr, req.Quantity)
res, err = h.ReadCoils(req.Addr, req.Quantity)
h.logger.Debugf("Read coils: %+v", res)
}
return res, err
}

// HandleDiscreteInputs handler method.
// Note that we're returning ErrIllegalFunction unconditionally.
// This will cause the client to receive "illegal function", which is the modbus way of
// reporting that this server does not support/implement the discrete input type.
func (h *Handler) HandleDiscreteInputs(_ *modbus.DiscreteInputsRequest) (res []bool, err error) {
// this is the equivalent of saying
// "discrete inputs are not supported by this device"
// (try it with modbus-cli --target tcp://localhost:5502 rdi:1)
h.logger.Error("Discrete inputs are not supported by this device")
err = modbus.ErrIllegalFunction

return res, err
}

// HandleHoldingRegisters 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 (h *Handler) HandleHoldingRegisters(req *modbus.HoldingRegistersRequest) (res []uint16, err error) {
h.logger.Debugf("Handling register reguest: %+v", req)
if req.IsWrite {
h.logger.Debugf("Writing registers: %+v, args: %+v", req.Addr, req.Args)
res, err = h.WriteHoldingRegisters(req.Addr, req.Args)
h.logger.Debugf("Write registers: %+v", res)
} else {
h.logger.Debugf("Reading registers: %+v, quantity: %+v ", req.Addr, req.Quantity)
res, err = h.ReadHoldingRegisters(req.Addr, req.Quantity)
h.logger.Debugf("Read registers: %+v", res)
}

return res, err
}

// HandleInputRegisters 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 (h *Handler) HandleInputRegisters(_ *modbus.InputRegistersRequest) (res []uint16, err error) {
h.logger.Error("Input registers are not supported by this device")
err = modbus.ErrIllegalFunction
return res, err
}
Loading