commit a774387776198b06871566a4ee103bbefcc633cf Author: walker Date: Wed Dec 6 17:07:52 2023 +0800 Modbus客户端及常用功能接口及实现 Modbus客户端管理实现 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6106fad --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +.idea/ +.vscode +.air.toml +.DS_Store +output/ + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +/pom.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..441788c --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# iot-gateway(Internet of Things)物联网网关模块 + +用于提供各种硬件协议实现及应用抽象,将各种协议最终转换为 MQTT 协议实现应用功能 + +# 硬件协议 + +## Modbus(实现中) + +功能路线: + +- Modbus TCP 客户端接口及实现,支持自动重连 --- 已完成 +- Modbus 客户端管理实现 --- 已完成 +- Modbus TCP 常用功能接口实现 --- 已完成 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..88146fe --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module joylink.club/iot + +go 1.21 + +require github.com/simonvetter/modbus v1.6.0 + +require github.com/goburrow/serial v0.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a15954 --- /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.6.0 h1:RDHJevtc7LDIVoHAbhDun8fy+QwnGe+ZU+sLm9ZZzjc= +github.com/simonvetter/modbus v1.6.0/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd8q+GVRObPw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3bb32f9 --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "time" + + "joylink.club/iot/modbus" +) + +func main() { + cli, err := modbus.NewClient("tcp://localhost:502") + if err != nil { + panic(err) + } + cli.Start() + // vb := false + for i := 0; i < 10; i++ { + time.Sleep(time.Second * 2) + // read a single 16-bit holding register at address 100 + if i%2 == 0 { + // vb = !vb + // data := []bool{vb, !vb, vb, !vb, vb, !vb, vb, !vb} + // err := cli.WriteCoils(0, data) + // if err != nil { + // // error out + // fmt.Printf("第%d次写错误: %s\n", i, err) + // } else { + // fmt.Printf("第%d次写成功, value: %v\n", i, vb) + // } + data := []uint16{uint16(i + 1), uint16(i + 2), uint16(i + 3), uint16(i + 4), uint16(i + 5), uint16(i + 6), uint16(i + 7), uint16(i + 8)} + err := cli.WriteRegisters(0, data) + if err != nil { + // error out + fmt.Printf("第%d次写错误: %s\n", i, err) + } else { + fmt.Printf("第%d次写成功, value: %v\n", i, data) + } + } else { + // bs, err := cli.ReadCoil(0, 10) + // if err != nil { + // // error out + // fmt.Printf("第%d次读错误: %s\n", i, err) + // } else { + // fmt.Printf("第%d次读成功, value: %v\n", i, bs) + // } + bs, err := cli.ReadHoldingRegister(0, 10) + if err != nil { + // error out + fmt.Printf("第%d次读错误: %s\n", i, err) + } else { + fmt.Printf("第%d次读成功, value: %v\n", i, bs) + } + } + } + +} diff --git a/modbus/api.go b/modbus/api.go new file mode 100644 index 0000000..650daf4 --- /dev/null +++ b/modbus/api.go @@ -0,0 +1,33 @@ +package modbus + +// 在modbus协议中,主机正常是客户端 +type MasterClient interface { + // 启动 + Start() error + // 停止 + Stop() error + // 关闭,销毁 + Close() error + // 是否连接 + IsConnected() bool + // 读线圈,位操作,功能码为0x01 + ReadCoil(addr uint16, quantity uint16) ([]bool, error) + // 读一个线圈 + ReadCoilBit(addr uint16) (bool, error) + // 读离散输入,位操作,功能码为0x02 + ReadDiscreteInput(addr uint16, quantity uint16) ([]bool, error) + // 读保持寄存器,字节操作,功能码为0x03 + ReadHoldingRegister(addr uint16, quantity uint16) ([]uint16, error) + // 读一个保持寄存器 + ReadHoldingRegisterUint16(addr uint16) (uint16, error) + // 读输入寄存器,字节操作,功能码为0x04 + ReadInputRegister(addr uint16, quantity uint16) ([]uint16, error) + // 写单个线圈,功能码为0x05 + WriteCoil(addr uint16, value bool) error + // 写多个线圈,功能码为0x0F + WriteCoils(addr uint16, values []bool) error + // 写单个保持寄存器,功能码为0x06 + WriteRegister(addr uint16, value uint16) error + // 写多个保持寄存器,功能码为0x10 + WriteRegisters(addr uint16, values []uint16) error +} diff --git a/modbus/client.go b/modbus/client.go new file mode 100644 index 0000000..54958b1 --- /dev/null +++ b/modbus/client.go @@ -0,0 +1,194 @@ +package modbus + +import ( + "context" + "fmt" + "log/slog" + "net" + "os" + "time" + + "github.com/simonvetter/modbus" +) + +type ConnectState int + +const () + +type client struct { + url string + cli *modbus.ModbusClient + started bool // 是否启动 + connected bool // 是否连接 + cancel context.CancelFunc +} + +func newClient(url string) (MasterClient, error) { + cli, err := modbus.NewClient(&modbus.ClientConfiguration{ + URL: url, + Timeout: 1 * time.Second, + }) + if err != nil { + return nil, err + } + c := &client{ + url: url, + cli: cli, + connected: false, + } + ctx, cancel := context.WithCancel(context.Background()) + go c.connManage(ctx) + c.cancel = cancel + return c, err +} + +func (c *client) Start() error { + c.started = true + return nil +} + +func (c *client) Stop() error { + c.started = false + return nil +} + +// 客户端连接管理 +func (c *client) connManage(ctx context.Context) { + for { + select { + case <-ctx.Done(): + slog.Info("modbus客户端连接管理线程退出", "url", c.url) + return + default: + } + if c.started && !c.connected { // 已经启动, 尝试重连 + err := c.cli.Open() + if err != nil { + slog.Error("modbus客户端尝试连接失败", "url", c.url, "err", err) + } else { + c.connected = true + slog.Info("modbus客户端连接成功", "url", c.url) + } + } else if !c.started && c.connected { // 未启动,尝试关闭 + err := c.cli.Close() + if err != nil { + slog.Error("modbus客户端关闭错误", "url", c.url, "err", err) + } + c.connected = false + slog.Info("modbus客户端关闭", "url", c.url) + } + time.Sleep(time.Second) + } +} +func (c *client) IsConnected() bool { + return c.connected +} + +// Close implements MasterClient. +func (c *client) Close() error { + c.cancel() + if c.connected { + return c.cli.Close() + } + return nil +} + +// 读请求执行 +func readExecute[T any](c *client, req func() (T, error)) (T, error) { + if !c.started { + return *new(T), fmt.Errorf("modbus客户端未启动") + } + if !c.connected { + return *new(T), fmt.Errorf("modbus客户端未连接或连接断开") + } + res, err := req() + if err != nil { + if newErr, ok := err.(*net.OpError); ok { + if se, ok := newErr.Err.(*os.SyscallError); ok { + c.connected = false + return res, se + } + } + } + return res, err +} + +// 写请求执行 +func writeExecute(c *client, req func() error) error { + if !c.started { + return fmt.Errorf("modbus客户端未启动") + } + if !c.connected { + return fmt.Errorf("modbus客户端未连接或连接断开") + } + return req() +} + +// ReadCoil implements MasterClient. +func (c *client) ReadCoil(addr uint16, quantity uint16) ([]bool, error) { + return readExecute[[]bool](c, func() ([]bool, error) { + return c.cli.ReadCoils(addr, quantity) + }) +} + +func (c *client) ReadCoilBit(addr uint16) (bool, error) { + return readExecute[bool](c, func() (bool, error) { + return c.cli.ReadCoil(addr) + }) +} + +// ReadDiscreteInput implements MasterClient. +func (c *client) ReadDiscreteInput(addr uint16, quantity uint16) ([]bool, error) { + return readExecute[[]bool](c, func() ([]bool, error) { + return c.cli.ReadDiscreteInputs(addr, quantity) + }) +} + +// ReadHoldingRegister implements MasterClient. +func (c *client) ReadHoldingRegister(addr uint16, quantity uint16) ([]uint16, error) { + return readExecute[[]uint16](c, func() ([]uint16, error) { + return c.cli.ReadRegisters(addr, quantity, modbus.HOLDING_REGISTER) + }) +} + +// ReadHoldingRegisterUint16 implements MasterClient. +func (c *client) ReadHoldingRegisterUint16(addr uint16) (uint16, error) { + return readExecute[uint16](c, func() (uint16, error) { + return c.cli.ReadRegister(addr, modbus.HOLDING_REGISTER) + }) +} + +// ReadInputRegister implements MasterClient. +func (c *client) ReadInputRegister(addr uint16, quantity uint16) ([]uint16, error) { + return readExecute[[]uint16](c, func() ([]uint16, error) { + return c.cli.ReadRegisters(addr, quantity, modbus.INPUT_REGISTER) + }) +} + +// WriteCoil implements MasterClient. +func (c *client) WriteCoil(addr uint16, value bool) error { + return writeExecute(c, func() error { + return c.cli.WriteCoil(addr, value) + }) +} + +// WriteCoils implements MasterClient. +func (c *client) WriteCoils(addr uint16, values []bool) error { + return writeExecute(c, func() error { + return c.cli.WriteCoils(addr, values) + }) +} + +// WriteRegister implements MasterClient. +func (c *client) WriteRegister(addr uint16, value uint16) error { + return writeExecute(c, func() error { + return c.cli.WriteRegister(addr, value) + }) +} + +// WriteRegisters implements MasterClient. +func (c *client) WriteRegisters(addr uint16, values []uint16) error { + return writeExecute(c, func() error { + return c.cli.WriteRegisters(addr, values) + }) +} diff --git a/modbus/function_code.go b/modbus/function_code.go new file mode 100644 index 0000000..9fe7cb3 --- /dev/null +++ b/modbus/function_code.go @@ -0,0 +1,23 @@ +package modbus + +// 功能码 +type FunctionCode int + +const ( + // 读线圈 + FCReadCoil FunctionCode = 0x01 + // 读离散输入 + FCReadDiscreteInput FunctionCode = 0x02 + // 读多个寄存器 + FCReadHoldingRegister FunctionCode = 0x03 + // 读输入寄存器 + FCReadInputRegister FunctionCode = 0x04 + // 写单个线圈 + FCWriteSingleCoil FunctionCode = 0x05 + // 写单个寄存器 + FCWriteSingleRegister FunctionCode = 0x06 + // 写多个线圈 + FCWriteMultipleCoil FunctionCode = 0x0F + // 写多个寄存器 + FCWriteMultipleRegister FunctionCode = 0x10 +) diff --git a/modbus/manage.go b/modbus/manage.go new file mode 100644 index 0000000..1b0bbe6 --- /dev/null +++ b/modbus/manage.go @@ -0,0 +1,53 @@ +package modbus + +import "sync" + +// modbus客户端管理 +type modbusManager struct { + lock sync.Mutex + clientMap map[string]MasterClient +} + +var manager = modbusManager{ + clientMap: make(map[string]MasterClient), +} + +// 获取modbus客户端 +func GetClient(url string) (MasterClient, bool) { + manager.lock.Lock() + defer manager.lock.Unlock() + client, ok := manager.clientMap[url] + return client, ok +} + +// 新建客户端 +func NewClient(url string) (MasterClient, error) { + manager.lock.Lock() + defer manager.lock.Unlock() + client, err := newClient(url) + if err != nil { + return nil, err + } + manager.clientMap[url] = client + return client, err +} + +// 获取或新建客户端 +func GetOrInitClient(url string) (MasterClient, error) { + client, ok := GetClient(url) + if ok { + return client, nil + } + return NewClient(url) +} + +// 删除客户端 +func DeleteClient(url string) { + manager.lock.Lock() + defer manager.lock.Unlock() + c := manager.clientMap[url] + if c != nil { + c.Close() + delete(manager.clientMap, url) + } +}