Categorygithub.com/fops9311/mbserver
modulepackage
0.0.15
Repository: https://github.com/fops9311/mbserver.git
Documentation: pkg.go.dev

# README

Build Status Coverage Status GoDoc Software License

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

# Packages

No description provided by the author

# Functions

BytesToUint16 converts a big endian array of bytes to an array of unit16s.
GetException retunrns the Modbus exception or Success (indicating not exception).
NewRTUFrame converts a packet to a Modbus TCP frame.
NewServer creates a new Modbus server (slave).
NewTCPFrame converts a packet to a Modbus TCP frame.
ReadCoils function 1, reads coils from internal memory.
ReadDiscreteInputs function 2, reads discrete inputs from internal memory.
ReadHoldingRegisters function 3, reads holding registers from internal memory.
ReadInputRegisters function 4, reads input registers from internal memory.
SetDataWithRegisterAndNumber sets the RTUFrame Data byte field to hold a register and number of registers.
SetDataWithRegisterAndNumberAndBytes sets the TCPFrame Data byte field to hold a register and number of registers and coil bytes.
SetDataWithRegisterAndNumberAndValues sets the TCPFrame Data byte field to hold a register and number of registers and values.
Uint16ToBytes converts an array of uint16s to a big endian array of bytes.
WriteHoldingRegister function 6, write a holding register to internal memory.
WriteHoldingRegisters function 16, writes holding registers to internal memory.
WriteMultipleCoils function 15, writes holding registers to internal memory.
WriteSingleCoil function 5, write a coil to internal memory.

# Constants

No description provided by the author

# Variables

AcknowledgeSlave has accepted request and is processing it, but a long duration of time is required.
GatewayPathUnavailable Specialized for Modbus gateways.
GatewayTargetDeviceFailedtoRespond Specialized for Modbus gateways.
IllegalDataAddress data address of some or all the required entities are not allowed or do not exist in slave.
IllegalDataValue value is not accepted by slave.
IllegalFunction function code received in the query is not recognized or allowed by slave.
MemoryParityError Slave detected a parity error in memory.
NegativeAcknowledge Slave cannot perform the programming functions.
SlaveDeviceBusy is engaged in processing a long-duration command.
SlaveDeviceFailure Unrecoverable error occurred while slave was attempting to perform requested action.
Success operation successful.

# Structs

Request contains the connection and Modbus frame.
RTUFrame is the Modbus TCP frame.
Server is a Modbus slave with allocated memory for discrete inputs, coils, etc.
TCPFrame is the Modbus TCP frame.

# Interfaces

Framer is the interface that wraps Modbus frames.

# Type aliases

Exception codes.