Modbus TCP client library written in Gengo.
A real-world use of Gengo's module system, module-qualified types, bitwise
operators, structural interfaces, and the cap:net TCP capability.
- Gengo v0.5.0-pre6 or later (
std.bytes, module-qualified types,--modulesflag) cap:netcapability enabled (passed via--cap net)
gengo --cap net --modules ./modbus demo.gengoThe demo connects to 127.0.0.1:5020, reads holding registers, writes a
register and a coil, reads coils, and writes/reads back multiple registers.
Edit the constants at the top of demo.gengo:
host := "192.168.1.10"
port := 502
unit_id := 1
modbus/
constants.gengo Function codes and exception codes
frame.gengo MBAP frame build + parse; exports Response struct
client.gengo Request helpers; exports Client struct and Connection interface
codec.gengo IEEE 754 float / integer register codec; exports Codec struct
demo.gengo Demo script
Import the library from a script in the same directory:
mb := import("./client")
Or with --modules ./modbus:
mb := import("./client")
pub type Client struct {
unit_id int,
tx_id int,
}
Any value with read(int) string, write(string) int, close(), and
set_deadline(int) satisfies Connection. cap:net.Conn does.
mb.read_holding_registers(conn Connection, c Client, address int, count int) []int
mb.read_input_registers(conn Connection, c Client, address int, count int) []int
mb.read_coils(conn Connection, c Client, address int, count int) []bool
mb.read_discrete_inputs(conn Connection, c Client, address int, count int) []bool
mb.write_single_register(conn Connection, c Client, address int, value int) bool
mb.write_single_coil(conn Connection, c Client, address int, on bool) bool
mb.write_multiple_registers(conn Connection, c Client, address int, values []int) bool
mb.write_multiple_coils(conn Connection, c Client, address int, values []bool) bool
All functions return an error value (checkable with std.core.is_error) on
exception response or connection failure.
cdc := import("./modbus/codec")
// Create a codec for a specific device's byte order.
decoder := cdc.Codec { byte_order: cdc.CDAB } // most common Modbus convention
// Decode a 32-bit float from two consecutive registers.
regs := mb.read_holding_registers(conn, client, 100, 2)
value := decoder.f32(regs, 0)
// Encode a float back to register values for writing.
out := decoder.f32_regs(value)
mb.write_multiple_registers(conn, client, 100, out)
Byte orders (ABCD naming convention, A = most significant byte):
| Constant | Description | Used by |
|---|---|---|
ABCD |
Big-endian (IEEE 754 native) | Siemens S7, Allen-Bradley BE mode |
CDAB |
Word-swapped (most common) | Schneider/Modicon, Emerson, Yaskawa |
BADC |
Byte-swapped within each word | Some Danfoss devices |
DCBA |
Fully little-endian | Some x86-based devices |
Methods on Codec:
// 32-bit float (2 registers)
cdc.f32(regs []int, i int) float // decode at register index i
cdc.f32_regs(v float) []int // encode → 2 register values
// 64-bit double (4 registers)
cdc.f64(regs []int, i int) float // decode at register index i
cdc.f64_regs(v float) []int // encode → 4 register values
// 32-bit unsigned integer (2 registers)
cdc.u32(regs []int, i int) int // decode at register index i
cdc.u32_regs(v int) []int // encode → 2 register values
Client holds only unit_id and a transaction counter. The TCP connection is
passed to each operation so that deadline management, reconnect logic, and
multiplexing stay in the caller:
conn := net.dial("tcp", "127.0.0.1:502")
defer conn.close()
conn.set_deadline(5000)
client := mb.Client { unit_id: 1, tx_id: 0 }
regs := mb.read_holding_registers(conn, client, 100, 10)
Frames are assembled and parsed using std.bytes — the raw-byte string
primitive introduced in v0.5.0-pre6. std.bytes.u8(n) produces a single raw
byte (unlike string(rune(n)) which produces UTF-8), and std.bytes.u16be/
u32be write big-endian integers directly into binary strings:
header := std.bytes.u16be(tx_id) + std.bytes.u16be(0) +
std.bytes.u16be(pdu_len) + std.bytes.u8(unit_id)
fr.Response as a return type and c Client as a parameter annotation use
Gengo's module-qualified type feature (v0.5.0-pre5+):
fr := import("./frame")
func recv(conn Connection) fr.Response { ... }
pub func read_holding_registers(conn Connection, c Client, ...) []int { ... }