Add Modbus proxy (#4981)

This commit is contained in:
andig 2022-11-03 21:05:34 +01:00 • committed by GitHub
parent 3e4b50a3bc
commit a5b611a83e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 387 additions and 26 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

1
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

141
server/modbus/handler.go Normal file
View file

@ -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)
}

45
server/modbus/proxy.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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