Skip to content

gengoscript/gengo-modbus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 

Repository files navigation

gengo-modbus

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.

Requirements

  • Gengo v0.5.0-pre6 or later (std.bytes, module-qualified types, --modules flag)
  • cap:net capability enabled (passed via --cap net)

Usage

gengo --cap net --modules ./modbus demo.gengo

The 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.

Changing the target

Edit the constants at the top of demo.gengo:

host    := "192.168.1.10"
port    := 502
unit_id := 1

Module layout

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

API

Import the library from a script in the same directory:

mb := import("./client")

Or with --modules ./modbus:

mb := import("./client")

Client struct

pub type Client struct {
    unit_id int,
    tx_id   int,
}

Connection interface

Any value with read(int) string, write(string) int, close(), and set_deadline(int) satisfies Connection. cap:net.Conn does.

Functions

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.

Codec struct — float and integer register decoding / encoding

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

Design notes

Connection ownership

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)

Binary framing with std.bytes

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)

Module-qualified types

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 { ... }

Releases

No releases published

Packages

 
 
 

Contributors