From 58e1c2ff9af780c946a19d8887baeeb336380e85 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 25 Oct 2022 17:06:36 +0200 Subject: [PATCH] Add Modbus proxy --- cmd/config.go | 10 ++- cmd/setup.go | 10 +++ evcc.dist.yaml | 8 ++ go.mod | 1 + go.sum | 4 + server/modbus/handler.go | 141 ++++++++++++++++++++++++++++++++++++ server/modbus/proxy.go | 45 ++++++++++++ server/modbus/proxy_test.go | 70 ++++++++++++++++++ util/modbus/modbus.go | 124 ++++++++++++++++++++++++------- 9 files changed, 387 insertions(+), 26 deletions(-) create mode 100644 server/modbus/handler.go create mode 100644 server/modbus/proxy.go create mode 100644 server/modbus/proxy_test.go diff --git a/cmd/config.go b/cmd/config.go index 963ad47983..ab31d5b7ed 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -18,6 +18,7 @@ import ( "github.com/evcc-io/evcc/server" autoauth "github.com/evcc-io/evcc/server/auth" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" "github.com/evcc-io/evcc/vehicle" "github.com/evcc-io/evcc/vehicle/wrapper" "github.com/gorilla/handlers" @@ -47,13 +48,14 @@ type config struct { Network networkConfig Log string SponsorToken string - Telemetry bool Plant string // telemetry plant id + Telemetry bool Metrics bool Profile bool Levels map[string]string Interval time.Duration Mqtt mqttConfig + ModbusProxy []proxyConfig Database dbConfig Javascript map[string]interface{} Influx server.InfluxConfig @@ -73,6 +75,12 @@ type mqttConfig struct { Topic string } +type proxyConfig struct { + Port int + ReadOnly bool + modbus.Settings `mapstructure:",squash"` +} + type dbConfig struct { Type string Dsn string diff --git a/cmd/setup.go b/cmd/setup.go index b51e430357..38260311ca 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -20,6 +20,7 @@ import ( "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/server/db" "github.com/evcc-io/evcc/server/db/settings" + "github.com/evcc-io/evcc/server/modbus" "github.com/evcc-io/evcc/tariff" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/locale" @@ -106,6 +107,15 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) { err = configureMQTT(conf.Mqtt) } + // setup modbus proxy listeners + if err == nil { + for _, cfg := range conf.ModbusProxy { + if err = modbus.StartProxy(cfg.Port, cfg.Settings, cfg.ReadOnly); err != nil { + break + } + } + } + // setup javascript VMs if err == nil { err = configureJavascript(conf.Javascript) diff --git a/evcc.dist.yaml b/evcc.dist.yaml index f0e7611aab..4067c749f6 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -37,6 +37,14 @@ levels: cache: error db: error +# modbus proxy for allowing external programs to reuse the evcc modbus connection +# each entry will start a proxy instance at the given port speaking Modbus TCP and +# relaying to the given modbus downstream device (either TCP or RTU, RS485 or TCP) +modbusproxy: + # - port: 5200 + # uri: localhost:502 + # readonly: true + # meter definitions # name can be freely chosen and is used as reference when assigning meters to site and loadpoints # for documentation see https://docs.evcc.io/docs/devices/meters diff --git a/go.mod b/go.mod index bf0e8f4c13..8e123350b7 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/PuerkitoBio/goquery v1.8.0 github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71 + github.com/andig/mbserver v0.0.0-20221101145037-fcaa2dfef9fb github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef github.com/avast/retry-go/v3 v3.1.1 github.com/aws/aws-sdk-go v1.44.127 diff --git a/go.sum b/go.sum index eb9d5c2d76..5ec4c0b0b7 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/andig/go-powerwall v0.2.1-0.20220205120646-e5220ad9a9a0 h1:9fQ/afZNwq github.com/andig/go-powerwall v0.2.1-0.20220205120646-e5220ad9a9a0/go.mod h1:NA12RXBKXFQFx3Nb9xMTQjHIphu1Fl64i/gQMuRdMZc= github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71 h1:tnjVNZjuz+CK6fdc7ohJpMHjcEGFI5APp0l5T5Ocr/Y= github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k= +github.com/andig/mbserver v0.0.0-20221101145037-fcaa2dfef9fb h1:cw8kFzV0kbG4E4Bv4UOmbgNkhCEZ0p7frNCFO4LKzm0= +github.com/andig/mbserver v0.0.0-20221101145037-fcaa2dfef9fb/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM= github.com/andig/rct v0.0.0-20221101081802-96d01efdc68c h1:iXNLsesR2rTRPmr+QbjGgRTAOue8QpIkibakicNR7Qg= github.com/andig/rct v0.0.0-20221101081802-96d01efdc68c/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI= github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= @@ -292,6 +294,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -774,6 +777,7 @@ github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc= github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/simonvetter/modbus v1.6.0 h1:RDHJevtc7LDIVoHAbhDun8fy+QwnGe+ZU+sLm9ZZzjc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= diff --git a/server/modbus/handler.go b/server/modbus/handler.go new file mode 100644 index 0000000000..6c2ac1ad99 --- /dev/null +++ b/server/modbus/handler.go @@ -0,0 +1,141 @@ +package modbus + +import ( + "encoding/binary" + "errors" + + "github.com/andig/mbserver" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + gridx "github.com/grid-x/modbus" +) + +type handler struct { + log *util.Logger + readOnly bool + mbserver.RequestHandler + conn *modbus.Connection +} + +func bytesAsUint16(b []byte) []uint16 { + u := make([]uint16, 0, len(b)/2) + for i := 0; i < len(b)/2; i++ { + u = append(u, binary.BigEndian.Uint16(b[2*i:])) + } + return u +} + +func asBytes(u []uint16) []byte { + b := make([]byte, 2*len(u)) + for i, u := range u { + binary.BigEndian.PutUint16(b[2*i:], u) + } + return b +} + +func bytesAsBool(b []byte) []bool { + var res []bool + for _, c := range bytesAsUint16(b) { + if c != 0 { + res = append(res, true) + continue + } + res = append(res, false) + } + return res +} + +func boolAsBytes(b []bool) []byte { + res := make([]byte, 2*len(b)) + for i, bb := range b { + if bb { + binary.BigEndian.PutUint16(res[2*i:], 0xFF00) + } + } + return res +} + +func (h *handler) logResult(op string, b []byte, err error) { + if err == nil { + h.log.TRACE.Printf(op+" response: %0x", b) + } else { + h.log.TRACE.Printf(op+" response: %v", err) + } +} + +func (h *handler) exceptionToUint16AndError(op string, b []byte, err error) ([]uint16, error) { + h.logResult(op, b, err) + + var modbusError *gridx.Error + if errors.As(err, &modbusError) { + err = mbserver.MapExceptionCodeToError(modbusError.ExceptionCode) + } + + return bytesAsUint16(b), err +} + +func (h *handler) exceptionToBoolAndError(op string, b []byte, err error) ([]bool, error) { + h.logResult(op, b, err) + + var modbusError *gridx.Error + if errors.As(err, &modbusError) { + err = mbserver.MapExceptionCodeToError(modbusError.ExceptionCode) + } + + return bytesAsBool(b), err +} + +func (h *handler) HandleCoils(req *mbserver.CoilsRequest) ([]bool, error) { + if req.IsWrite { + if h.readOnly { + return nil, mbserver.ErrIllegalFunction + } + + if req.Quantity == 1 { + h.log.TRACE.Printf("write coil: id: %d addr: %d val: %t", req.UnitId, req.Addr, req.Args[0]) + var u uint16 + if req.Args[0] { + u = 0xFF00 + } + + b, err := h.conn.WriteSingleCoilWithSlave(req.UnitId, req.Addr, u) + return h.exceptionToBoolAndError("write coil", b, err) + } + + h.log.TRACE.Printf("write multiple coils: id: %d addr: %d qty: %d val: %v", req.UnitId, req.Addr, req.Quantity, req.Args) + b, err := h.conn.WriteMultipleCoilsWithSlave(req.UnitId, req.Addr, req.Quantity, boolAsBytes(req.Args)) + return h.exceptionToBoolAndError("write multiple coils", b, err) + } + + h.log.TRACE.Printf("read coil: id: %d addr: %d qty: %d", req.UnitId, req.Addr, req.Quantity) + b, err := h.conn.ReadCoilsWithSlave(req.UnitId, req.Addr, req.Quantity) + return h.exceptionToBoolAndError("read coil", b, err) +} + +func (h *handler) HandleInputRegisters(req *mbserver.InputRegistersRequest) (res []uint16, err error) { + h.log.TRACE.Printf("read input: id: %d addr: %d qty: %d", req.UnitId, req.Addr, req.Quantity) + b, err := h.conn.ReadInputRegistersWithSlave(req.UnitId, req.Addr, req.Quantity) + return h.exceptionToUint16AndError("read input", b, err) +} + +func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) (res []uint16, err error) { + if req.IsWrite { + if h.readOnly { + return nil, mbserver.ErrIllegalFunction + } + + if req.Quantity == 1 { + h.log.TRACE.Printf("write holding: id: %d addr: %d val: %0x", req.UnitId, req.Addr, req.Args[0]) + b, err := h.conn.WriteSingleRegisterWithSlave(req.UnitId, req.Addr, req.Args[0]) + return h.exceptionToUint16AndError("write holding", b, err) + } + + h.log.TRACE.Printf("write multiple holding: id: %d addr: %d qty: %d val: %0x", req.UnitId, req.Addr, req.Quantity, asBytes(req.Args)) + b, err := h.conn.WriteMultipleRegistersWithSlave(req.UnitId, req.Addr, req.Quantity, asBytes(req.Args)) + return h.exceptionToUint16AndError("write multiple holding", b, err) + } + + h.log.TRACE.Printf("read holding: id: %d addr: %d qty: %d", req.UnitId, req.Addr, req.Quantity) + b, err := h.conn.ReadHoldingRegistersWithSlave(req.UnitId, req.Addr, req.Quantity) + return h.exceptionToUint16AndError("read holding", b, err) +} diff --git a/server/modbus/proxy.go b/server/modbus/proxy.go new file mode 100644 index 0000000000..7a4593b1f0 --- /dev/null +++ b/server/modbus/proxy.go @@ -0,0 +1,45 @@ +package modbus + +import ( + "fmt" + "net" + + "github.com/andig/mbserver" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + "github.com/evcc-io/evcc/util/sponsor" +) + +func StartProxy(port int, config modbus.Settings, readOnly bool) error { + conn, err := modbus.NewConnection(config.URI, config.Device, config.Comset, config.Baudrate, modbus.ProtocolFromRTU(config.RTU), config.ID) + if err != nil { + return err + } + + if !sponsor.IsAuthorized() { + return api.ErrSponsorRequired + } + + h := &handler{ + log: util.NewLogger(fmt.Sprintf("proxy-%d", port)), + readOnly: readOnly, + RequestHandler: new(mbserver.DummyHandler), + conn: conn, + } + + l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return err + } + + h.log.DEBUG.Printf("modbus proxy for %s listening at :%d", config.String(), port) + + srv, err := mbserver.New(h) + + if err == nil { + err = srv.Start(l) + } + + return err +} diff --git a/server/modbus/proxy_test.go b/server/modbus/proxy_test.go new file mode 100644 index 0000000000..c325cf82d5 --- /dev/null +++ b/server/modbus/proxy_test.go @@ -0,0 +1,70 @@ +package modbus + +import ( + "encoding/binary" + "math/rand" + "net" + "sync" + "testing" + "time" + + "github.com/andig/mbserver" + "github.com/evcc-io/evcc/util/modbus" + "github.com/stretchr/testify/assert" +) + +func TestProxyRead(t *testing.T) { + l, err := net.Listen("tcp", ":0") + assert.NoError(t, err) + defer l.Close() + + t.Log(l.Addr().String()) + + conn, err := modbus.NewConnection(l.Addr().String(), "", "", 0, modbus.Tcp, 1) + assert.NoError(t, err) + + h := &echoHandler{ + id: 0, + RequestHandler: new(mbserver.DummyHandler), + conn: conn, + } + + srv, _ := mbserver.New(h) + assert.NoError(t, srv.Start(l)) + defer func() { _ = srv.Stop() }() + + var wg sync.WaitGroup + + for i := 1; i <= 10; i++ { + wg.Add(1) + + go func(id int) { + for i := 0; i < 50; i++ { + addr := uint16(rand.Int31n(200) + 1) + + b, err := conn.ReadInputRegistersWithSlave(uint8(id), addr, 1) + assert.NoError(t, err) + + if err == nil { + assert.Equal(t, addr^uint16(id), binary.BigEndian.Uint16(b)) + } + + time.Sleep(time.Duration(rand.Int31n(1000)) * time.Microsecond) + } + + wg.Done() + }(i) + } + + wg.Wait() +} + +type echoHandler struct { + id int + mbserver.RequestHandler + conn *modbus.Connection +} + +func (h *echoHandler) HandleInputRegisters(req *mbserver.InputRegistersRequest) (res []uint16, err error) { + return []uint16{req.Addr ^ uint16(req.UnitId)}, err +} diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go index a715f756dd..382803ac8c 100644 --- a/util/modbus/modbus.go +++ b/util/modbus/modbus.go @@ -46,15 +46,23 @@ type Settings struct { RTU *bool // indicates RTU over TCP if true } +func (s *Settings) String() string { + if s.URI != "" { + return s.URI + } + return s.Device +} + // Connection decorates a meters.Connection with transparent slave id and error handling type Connection struct { slaveID uint8 + mu sync.Mutex conn meters.Connection delay time.Duration } -func (mb *Connection) prepare() { - mb.conn.Slave(mb.slaveID) +func (mb *Connection) prepare(slaveID uint8) { + mb.conn.Slave(slaveID) if mb.delay > 0 { time.Sleep(mb.delay) } @@ -88,71 +96,137 @@ func (mb *Connection) Timeout(timeout time.Duration) { } // ReadCoils wraps the underlying implementation -func (mb *Connection) ReadCoils(address, quantity uint16) ([]byte, error) { - mb.prepare() +func (mb *Connection) ReadCoilsWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().ReadCoils(address, quantity)) } // WriteSingleCoil wraps the underlying implementation -func (mb *Connection) WriteSingleCoil(address, quantity uint16) ([]byte, error) { - mb.prepare() - return mb.handle(mb.conn.ModbusClient().WriteSingleCoil(address, quantity)) +func (mb *Connection) WriteSingleCoilWithSlave(slaveID uint8, address, value uint16) ([]byte, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) + return mb.handle(mb.conn.ModbusClient().WriteSingleCoil(address, value)) } // ReadInputRegisters wraps the underlying implementation -func (mb *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) { - mb.prepare() +func (mb *Connection) ReadInputRegistersWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().ReadInputRegisters(address, quantity)) } // ReadHoldingRegisters wraps the underlying implementation -func (mb *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) { - mb.prepare() +func (mb *Connection) ReadHoldingRegistersWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().ReadHoldingRegisters(address, quantity)) } // WriteSingleRegister wraps the underlying implementation -func (mb *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) { - mb.prepare() +func (mb *Connection) WriteSingleRegisterWithSlave(slaveID uint8, address, value uint16) ([]byte, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().WriteSingleRegister(address, value)) } // WriteMultipleRegisters wraps the underlying implementation -func (mb *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) { - mb.prepare() +func (mb *Connection) WriteMultipleRegistersWithSlave(slaveID uint8, address, quantity uint16, value []byte) ([]byte, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().WriteMultipleRegisters(address, quantity, value)) } // ReadDiscreteInputs wraps the underlying implementation -func (mb *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) { - mb.prepare() +func (mb *Connection) ReadDiscreteInputsWithSlave(slaveID uint8, address, quantity uint16) (results []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().ReadDiscreteInputs(address, quantity)) } // WriteMultipleCoils wraps the underlying implementation -func (mb *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) { - mb.prepare() +func (mb *Connection) WriteMultipleCoilsWithSlave(slaveID uint8, address, quantity uint16, value []byte) (results []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().WriteMultipleCoils(address, quantity, value)) } // ReadWriteMultipleRegisters wraps the underlying implementation -func (mb *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) { - mb.prepare() +func (mb *Connection) ReadWriteMultipleRegistersWithSlave(slaveID uint8, readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value)) } // MaskWriteRegister wraps the underlying implementation -func (mb *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) { - mb.prepare() +func (mb *Connection) MaskWriteRegisterWithSlave(slaveID uint8, address, andMask, orMask uint16) (results []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().MaskWriteRegister(address, andMask, orMask)) } // ReadFIFOQueue wraps the underlying implementation -func (mb *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) { - mb.prepare() +func (mb *Connection) ReadFIFOQueueWithSlave(slaveID uint8, address uint16) (results []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.prepare(slaveID) return mb.handle(mb.conn.ModbusClient().ReadFIFOQueue(address)) } +func (mb *Connection) ReadCoils(address, quantity uint16) ([]byte, error) { + return mb.ReadCoilsWithSlave(mb.slaveID, address, quantity) +} + +func (mb *Connection) WriteSingleCoil(address, quantity uint16) ([]byte, error) { + return mb.WriteSingleCoilWithSlave(mb.slaveID, address, quantity) +} + +func (mb *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) { + return mb.ReadInputRegistersWithSlave(mb.slaveID, address, quantity) +} + +func (mb *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) { + return mb.ReadHoldingRegistersWithSlave(mb.slaveID, address, quantity) +} + +func (mb *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) { + return mb.WriteSingleRegisterWithSlave(mb.slaveID, address, value) +} + +func (mb *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) { + return mb.WriteMultipleRegistersWithSlave(mb.slaveID, address, quantity, value) +} + +func (mb *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) { + return mb.ReadDiscreteInputsWithSlave(mb.slaveID, address, quantity) +} + +func (mb *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) { + return mb.WriteMultipleCoilsWithSlave(mb.slaveID, address, quantity, value) +} + +func (mb *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) { + return mb.ReadWriteMultipleRegistersWithSlave(mb.slaveID, readAddress, readQuantity, writeAddress, writeQuantity, value) +} + +func (mb *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) { + return mb.MaskWriteRegisterWithSlave(mb.slaveID, address, andMask, orMask) +} + +func (mb *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) { + return mb.ReadFIFOQueueWithSlave(mb.slaveID, address) +} + var ( connections = make(map[string]meters.Connection) mu sync.Mutex