README
¶
Golang Modbus Server (Slave)
The Golang Modbus Server (Slave) responds to the following Modbus function requests:
Bit access:
- Read Discrete Inputs
- Read Coils
- Write Single Coil
- Write Multiple Coils
16-bit acess:
- Read Input Registers
- Read Multiple Holding Registers
- Write Single Holding Register
- Write Multiple Holding Registers
TCP and serial RTU access is supported.
The server internally allocates memory for 65536 coils, 65536 discrete inputs, 653356 holding registers and 65536 input registers. On start, all values are initialzied to zero. Modbus requests are processed in the order they are received and will not overlap/interfere with each other.
The golang mbserver documentation.
Example Modbus TCP Server
Create a Modbus TCP Server (Slave):
package main
import (
"log"
"time"
"github.com/tbrandon/mbserver"
)
func main() {
serv := mbserver.NewServer()
err := serv.ListenTCP("127.0.0.1:1502")
if err != nil {
log.Printf("%v\n", err)
}
defer serv.Close()
// Wait forever
for {
time.Sleep(1 * time.Second)
}
}
The server will continue to listen until killed (<ctrl>-c). Modbus typically uses port 502 (standard users require special permissions to listen on port 502). Change the port number as required. Change the address to 0.0.0.0 to listen on all network interfaces.
An example of a client writing and reading holding regsiters:
package main
import (
"fmt"
"github.com/goburrow/modbus"
)
func main() {
handler := modbus.NewTCPClientHandler("localhost:1502")
// Connect manually so that multiple requests are handled in one session
err := handler.Connect()
defer handler.Close()
client := modbus.NewClient(handler)
_, err = client.WriteMultipleRegisters(0, 3, []byte{0, 3, 0, 4, 0, 5})
if err != nil {
fmt.Printf("%v\n", err)
}
results, err := client.ReadHoldingRegisters(0, 3)
if err != nil {
fmt.Printf("%v\n", err)
}
fmt.Printf("results %v\n", results)
}
Outputs:
results [0 3 0 4 0 5]
Example Listening on Multiple TCP Ports and Serial Devices
The Golang Modbus Server can listen on multiple TCP ports and serial devices. In the following example, the Modbus server will be configured to listen on 127.0.0.1:1502, 0.0.0.0:3502, /dev/ttyUSB0 and /dev/ttyACM0
serv := mbserver.NewServer()
err := serv.ListenTCP("127.0.0.1:1502")
if err != nil {
log.Printf("%v\n", err)
}
err := serv.ListenTCP("0.0.0.0:3502")
if err != nil {
log.Printf("%v\n", err)
}
err := s.ListenRTU(&serial.Config{
Address: "/dev/ttyUSB0",
BaudRate: 115200,
DataBits: 8,
StopBits: 1,
Parity: "N",
Timeout: 10 * time.Second})
if err != nil {
t.Fatalf("failed to listen, got %v\n", err)
}
err := s.ListenRTU(&serial.Config{
Address: "/dev/ttyACM0",
BaudRate: 9600,
DataBits: 8,
StopBits: 1,
Parity: "N",
Timeout: 10 * time.Second,
RS485: serial.RS485Config{
Enabled: true,
DelayRtsBeforeSend: 2 * time.Millisecond
DelayRtsAfterSend: 3 * time.Millisecond
RtsHighDuringSend: false,
RtsHighAfterSend: false,
RxDuringTx: false
})
if err != nil {
t.Fatalf("failed to listen, got %v\n", err)
}
defer serv.Close()
Information on serial port settings.
Server Customization
RegisterFunctionHandler allows the default server functionality to be overridden for a Modbus function code.
func (s *Server) RegisterFunctionHandler(funcCode uint8, function func(*Server, Framer) ([]byte, *Exception))
Example of overriding the default ReadDiscreteInputs funtion:
serv := NewServer()
// Override ReadDiscreteInputs function.
serv.RegisterFunctionHandler(2,
func(s *Server, frame Framer) ([]byte, *Exception) {
register, numRegs, endRegister := frame.registerAddressAndNumber()
// Check the request is within the allocated memory
if endRegister > 65535 {
return []byte{}, &IllegalDataAddress
}
dataSize := numRegs / 8
if (numRegs % 8) != 0 {
dataSize++
}
data := make([]byte, 1+dataSize)
data[0] = byte(dataSize)
for i := range s.DiscreteInputs[register:endRegister] {
// Return all 1s, regardless of the value in the DiscreteInputs array.
shift := uint(i) % 8
data[1+i/8] |= byte(1 << shift)
}
return data, &Success
})
// Start the server.
err := serv.ListenTCP("localhost:4321")
if err != nil {
log.Printf("%v\n", err)
return
}
defer serv.Close()
// Wait for the server to start
time.Sleep(1 * time.Millisecond)
// Example of a client reading from the server started above.
// Connect a client.
handler := modbus.NewTCPClientHandler("localhost:4321")
err = handler.Connect()
if err != nil {
log.Printf("%v\n", err)
return
}
defer handler.Close()
client := modbus.NewClient(handler)
// Read discrete inputs.
results, err := client.ReadDiscreteInputs(0, 16)
if err != nil {
log.Printf("%v\n", err)
}
fmt.Printf("results %v\n", results)
Output:
results [255 255]
Benchmarks
Quanitify server read/write performance. Benchmarks are for Modbus TCP operations.
Run benchmarks:
$ go test -bench=.
BenchmarkModbusWrite1968MultipleCoils-8 50000 30912 ns/op
BenchmarkModbusRead2000Coils-8 50000 27875 ns/op
BenchmarkModbusRead2000DiscreteInputs-8 50000 27335 ns/op
BenchmarkModbusWrite123MultipleRegisters-8 100000 22655 ns/op
BenchmarkModbusRead125HoldingRegisters-8 100000 21117 ns/op
PASS
Operations per second are higher when requests are not forced to be synchronously processed. In the case of simultaneous client access, synchronous Modbus request processing prevents data corruption.
To understand performanc limitations, create a CPU profile graph for the WriteMultipleCoils benchmark:
go test -bench=.MultipleCoils -cpuprofile=cpu.out
go tool pprof modbus-server.test cpu.out
(pprof) web
Race Conditions
There is a known race condition in the code relating to calling Serial Read() and Close() functions in different go routines.
To check for race conditions, run:
go test --race
Documentation
¶
Overview ¶
Package mbserver implments a Modbus server (slave).
Index ¶
- func BytesToUint16(bytes []byte) []uint16
- func SetDataWithRegisterAndNumber(frame Framer, register uint16, number uint16)
- func SetDataWithRegisterAndNumberAndBytes(frame Framer, register uint16, number uint16, bytes []byte)
- func SetDataWithRegisterAndNumberAndValues(frame Framer, register uint16, number uint16, values []uint16)
- func Uint16ToBytes(values []uint16) []byte
- type Exception
- func GetException(frame Framer) (exception Exception)
- func ReadCoils(s *Server, frame Framer) ([]byte, *Exception)
- func ReadDiscreteInputs(s *Server, frame Framer) ([]byte, *Exception)
- func ReadHoldingRegisters(s *Server, frame Framer) ([]byte, *Exception)
- func ReadInputRegisters(s *Server, frame Framer) ([]byte, *Exception)
- func WriteHoldingRegister(s *Server, frame Framer) ([]byte, *Exception)
- func WriteHoldingRegisters(s *Server, frame Framer) ([]byte, *Exception)
- func WriteMultipleCoils(s *Server, frame Framer) ([]byte, *Exception)
- func WriteSingleCoil(s *Server, frame Framer) ([]byte, *Exception)
- type Framer
- type RTUFrame
- type Request
- type Server
- type TCPFrame
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BytesToUint16 ¶
BytesToUint16 converts a big endian array of bytes to an array of unit16s
func SetDataWithRegisterAndNumber ¶
SetDataWithRegisterAndNumber sets the RTUFrame Data byte field to hold a register and number of registers
func SetDataWithRegisterAndNumberAndBytes ¶
func SetDataWithRegisterAndNumberAndBytes(frame Framer, register uint16, number uint16, bytes []byte)
SetDataWithRegisterAndNumberAndBytes sets the TCPFrame Data byte field to hold a register and number of registers and coil bytes
func SetDataWithRegisterAndNumberAndValues ¶
func SetDataWithRegisterAndNumberAndValues(frame Framer, register uint16, number uint16, values []uint16)
SetDataWithRegisterAndNumberAndValues sets the TCPFrame Data byte field to hold a register and number of registers and values
func Uint16ToBytes ¶
Uint16ToBytes converts an array of uint16s to a big endian array of bytes
Types ¶
type Exception ¶
type Exception uint8
Exception codes.
var ( // Success operation successful. Success Exception // IllegalFunction function code received in the query is not recognized or allowed by slave. IllegalFunction Exception = 1 // IllegalDataAddress data address of some or all the required entities are not allowed or do not exist in slave. IllegalDataAddress Exception = 2 // IllegalDataValue value is not accepted by slave. IllegalDataValue Exception = 3 // SlaveDeviceFailure Unrecoverable error occurred while slave was attempting to perform requested action. SlaveDeviceFailure Exception = 4 // AcknowledgeSlave has accepted request and is processing it, but a long duration of time is required. This response is returned to prevent a timeout error from occurring in the master. Master can next issue a Poll Program Complete message to determine whether processing is completed. AcknowledgeSlave Exception = 5 // SlaveDeviceBusy is engaged in processing a long-duration command. Master should retry later. SlaveDeviceBusy Exception = 6 // NegativeAcknowledge Slave cannot perform the programming functions. Master should request diagnostic or error information from slave. NegativeAcknowledge Exception = 7 // MemoryParityError Slave detected a parity error in memory. Master can retry the request, but service may be required on the slave device. MemoryParityError Exception = 8 GatewayPathUnavailable Exception = 10 // GatewayTargetDeviceFailedtoRespond Specialized for Modbus gateways. Sent when slave fails to respond. GatewayTargetDeviceFailedtoRespond Exception = 11 )
func GetException ¶
GetException retunrns the Modbus exception or Success (indicating not exception).
func ReadDiscreteInputs ¶
ReadDiscreteInputs function 2, reads discrete inputs from internal memory.
func ReadHoldingRegisters ¶
ReadHoldingRegisters function 3, reads holding registers from internal memory.
func ReadInputRegisters ¶
ReadInputRegisters function 4, reads input registers from internal memory.
func WriteHoldingRegister ¶
WriteHoldingRegister function 6, write a holding register to internal memory.
func WriteHoldingRegisters ¶
WriteHoldingRegisters function 16, writes holding registers to internal memory.
func WriteMultipleCoils ¶
WriteMultipleCoils function 15, writes holding registers to internal memory.
func WriteSingleCoil ¶
WriteSingleCoil function 5, write a coil to internal memory.
type Framer ¶
type Framer interface { Bytes() []byte Copy() Framer GetID() uint8 GetData() []byte GetFunction() uint8 SetException(exception *Exception) SetData(data []byte) }
Framer is the interface that wraps Modbus frames.
type RTUFrame ¶
RTUFrame is the Modbus TCP frame.
func NewRTUFrame ¶
NewRTUFrame converts a packet to a Modbus TCP frame.
func (*RTUFrame) GetFunction ¶
GetFunction returns the Modbus function code.
func (*RTUFrame) SetData ¶
SetData sets the RTUFrame Data byte field and updates the frame length accordingly.
func (*RTUFrame) SetException ¶
SetException sets the Modbus exception code in the frame.
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request contains the connection and Modbus frame.
type Server ¶
type Server struct { // Debug enables more verbose messaging. ID uint8 Debug bool DiscreteInputs []byte Coils []byte HoldingRegisters []uint16 InputRegisters []uint16 // contains filtered or unexported fields }
Server is a Modbus slave with allocated memory for discrete inputs, coils, etc.
func (*Server) Close ¶
func (s *Server) Close()
Close stops listening to TCP/IP ports and closes serial ports.
func (*Server) ListenSerial ¶
func (s *Server) ListenSerial(port io.ReadWriteCloser) (err error)
ListenRTU starts the Modbus server listening to a serial device. For example: err := s.ListenRTU(&serial.Config{Address: "/dev/ttyUSB0"})
type TCPFrame ¶
type TCPFrame struct { TransactionIdentifier uint16 ProtocolIdentifier uint16 Length uint16 Device uint8 Function uint8 Data []byte }
TCPFrame is the Modbus TCP frame.
func NewTCPFrame ¶
NewTCPFrame converts a packet to a Modbus TCP frame.
func (*TCPFrame) GetFunction ¶
GetFunction returns the Modbus function code.
func (*TCPFrame) SetData ¶
SetData sets the TCPFrame Data byte field and updates the frame length accordingly.
func (*TCPFrame) SetException ¶
SetException sets the Modbus exception code in the frame.