Modbus TCP and RTU protocol client

Modbus client (TCP/RTU) over TCP/UDP/Serial for Golang.
For questions use Github Discussions
Installation
go get github.com/aldas/go-modbus-client
Supported functions
- FC1 - Read Coils (req/resp)
- FC2 - Read Discrete Inputs (req/resp)
- FC3 - Read Holding Registers (req/resp)
- FC4 - Read Input Registers (req/resp)
- FC5 - Write Single Coil (req/resp)
- FC6 - Write Single Register (req/resp)
- FC15 - Write Multiple Coils (req/resp)
- FC16 - Write Multiple Registers (req/resp)
- FC17 - Read Server ID (req/resp)
- FC23 - Read / Write Multiple Registers (req/resp)
Goals
- Packets separate from Client implementation
- Client (TCP/UDP +RTU) separated from Modbus packets
- Convenience methods to convert register data to/from different data types (with endianess/word order)
- Builders to group multiple fields into request batches
- Poller to request batches request and parse response to field values with long-running process.
Examples
Readings from Modbus TCP/IP
Higher level API allows you to compose register requests out of arbitrary number of fields and extract those
field values from response registers with convenience methods
Addresses without scheme (i.e. localhost:5020) are considered as TCP addresses. For UDP unicast use udp://localhost:5020.
b := modbus.NewRequestBuilder("tcp://localhost:5020", 1)
requests, _ := b.
AddField(modbus.Field{Name: "test_do", Type: modbus.FieldTypeUint16, Address: 18}).
AddField(modbus.Field{Name: "alarm_do_1", Type: modbus.FieldTypeInt64, Address: 19}).
ReadHoldingRegistersTCP() // split added fields into multiple requests with suitable quantity size
client := modbus.NewTCPClient()
if err := client.Connect(context.Background(), "tcp://localhost:5020"); err != nil {
return err
}
for _, req := range requests {
resp, err := client.Do(context.Background(), req)
if err != nil {
return err
}
// extract response as packet.Registers instance to have access to convenience methods to
// extracting registers as different data types
registers, _ := resp.(*packet.ReadHoldingRegistersResponseTCP).AsRegisters(req.StartAddress)
alarmDo1, _ := registers.Int64(19)
fmt.Printf("int64 @ address 19: %v", alarmDo1)
// or extract values to FieldValue struct
fields, _ := req.ExtractFields(resp, true)
assert.Equal(t, uint16(1), fields[0].Value)
assert.Equal(t, "alarm_do_1", fields[1].Field.Name)
}
Write multiple registers to Modbus TCP/IP
// Registers is a helper to compose payload for write multiple registers request
registers, _ := packet.NewRegisters(make([]byte, 4), 101) // 4 bytes is 2 registers payload, 101 is start address
_ = registers.WriteValue(packet.WriteValueCmd{ // set first register (101) to uint8 value of 1
Value: uint8(1),
RegisterAddress: 101,
})
_ = registers.WriteValue(packet.WriteValueCmd{ // set second register (102) to int16 value of 258
Value: int16(258),
RegisterAddress: 102,
})
req, _ := packet.NewWriteMultipleRegistersRequestTCP(1, 101, registers.Data())
client := modbus.NewTCPClient()
if err := client.Connect(context.Background(), "tcp://localhost:5020"); err != nil {
return
}
defer client.Close()
resp, _ := client.Do(context.Background(), req)
Polling values with long-running process
See simple poller implementation cmd/modbus-poller/main.go.
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
b := modbus.NewRequestBuilderWithConfig(modbus.BuilderDefaults{
ServerAddress: "tcp://127.0.0.1:5022?invalid_addr=1000,12000-12100&read_timeout=2s",
FunctionCode: packet.FunctionReadHoldingRegisters, // fc3
UnitID: 1,
Protocol: modbus.ProtocolTCP,
Interval: modbus.Duration(1 * time.Second), // send a request every 1 second
Splitter: nil, // allows custom splitter logic to be used
})
batches, _ := b.
AddField(modbus.Field{Name: "test_do", Type: modbus.FieldTypeUint16, Address: 18}).
AddField(modbus.Field{Name: "alarm_do_1", Type: modbus.FieldTypeInt64, Address: 19}).
Split() // split added fields into multiple requests with suitable quantity size
p := poller.NewPoller(batches)
go func() {
for {
select {
case result := <-p.ResultChan:
slog.Info("polled values", "values", result)
case <-ctx.Done():
return
}
}
}()
if err := p.Poll(ctx); err != nil {
slog.Error("polling ended with failure", "err", err)
return
}
}
RTU over serial port
RTU examples to interact with serial port can be found from serial.md
Addresses without scheme (i.e. localhost:5020) are considered as TCP addresses. For UDP unicast use udp://localhost:5020.
Low level packets
client := modbus.NewTCPClientWithConfig(modbus.ClientConfig{
WriteTimeout: 2 * time.Second,
ReadTimeout: 2 * time.Second,
})
if err := client.Connect(context.Background(), "localhost:5020"); err != nil {
return err
}
defer client.Close()
startAddress := uint16(10)
req, err := packet.NewReadHoldingRegistersRequestTCP(0, startAddress, 9)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.Do(ctx, req)
if err != nil {
return err
}
registers, err := resp.(*packet.ReadHoldingRegistersResponseTCP).AsRegisters(startAddress)
if err != nil {
return err
}
uint32Var, err := registers.Uint32(17) // extract uint32 value from register 17
To create single TCP packet use following methods. Use RTU suffix to create RTU packets.
import "github.com/aldas/go-modbus-client/packet"
req, err := packet.NewReadCoilsRequestTCP(0, 10, 9)
req, err := packet.NewReadDiscreteInputsRequestTCP(0, 10, 9)
req, err := packet.NewReadHoldingRegistersRequestTCP(0, 10, 9)
req, err := packet.NewReadInputRegistersRequestTCP(0, 10, 9)
req, err := packet.NewWriteSingleCoilRequestTCP(0, 10, true)
req, err := packet.NewWriteSingleRegisterRequestTCP(0, 10, []byte{0xCA, 0xFE})
req, err := packet.NewWriteMultipleCoilsRequestTCP(0, 10, []bool{true, false, true})
req, err := packet.NewReadServerIDRequestTCP(0)
req, err := packet.NewWriteMultipleRegistersRequestTCP(0, 10, []byte{0xCA, 0xFE, 0xBA, 0xBE})
Builder to group fields to packets
b := modbus.NewRequestBuilder("localhost:5020", 1)
requests, _ := b.
AddField(modbus.Field{Name: "test_do", Type: modbus.FieldTypeUint16, Address: 18}).
AddField(modbus.Field{Name: "alarm_do_1", Type: modbus.FieldTypeInt64, Address: 19}).
ReadHoldingRegistersTCP() // split added fields into multiple requests with suitable quantity size
Changelog
See CHANGELOG.md
Tests
make check
评论