modbusone

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2022 License: BSD-3-Clause Imports: 15 Imported by: 4

README

ModbusOne Go Reference

A Modbus library for Go, with unified client and server APIs. One implementation to rule them all.

Example
// handlerGenerator returns ProtocolHandlers that interact with our application.
// In this example, we are only using Holding Registers.
func handlerGenerator(name string) modbusone.ProtocolHandler {
    return &modbusone.SimpleHandler{
        ReadHoldingRegisters: func(address, quantity uint16) ([]uint16, error) {
            fmt.Printf("%v ReadHoldingRegisters from %v, quantity %v\n",
                name, address, quantity)
            r := make([]uint16, quantity)
            // Application code that fills in r here.
            return r, nil
        },
        WriteHoldingRegisters: func(address uint16, values []uint16) error {
            fmt.Printf("%v WriteHoldingRegisters from %v, quantity %v\n",
                name, address, len(values))
            // Application code here.
            return nil
        },
        OnErrorImp: func(req modbusone.PDU, errRep modbusone.PDU) {
            fmt.Printf("%v received error:%x in request:%x", name, errRep, req)
        },
    }
}

// serial is a fake serial port.
type serial struct {
    io.ReadCloser
    io.WriteCloser
}

func newInternalSerial() (io.ReadWriteCloser, io.ReadWriteCloser) {
    r1, w1 := io.Pipe()
    r2, w2 := io.Pipe()
    return &serial{ReadCloser: r1, WriteCloser: w2}, &serial{ReadCloser: r2, WriteCloser: w1}
}

func (s *serial) Close() error {
    s.ReadCloser.Close()
    return s.WriteCloser.Close()
}

func Example_serialPort() {
    // Server id and baudRate, for Modbus over serial port.
    id := byte(1)
    baudRate := int64(19200)

    // Open serial connections:
    clientSerial, serverSerial := newInternalSerial()
    // Normally we want to open a serial connection from serial.OpenPort
    // such as github.com/tarm/serial. modbusone can take any io.ReadWriteCloser,
    // so we created two that talks to each other for demonstration here.

    // SerialContext adds baudRate information to calculate
    // the duration that data transfers should takes.
    // It also records Stats of read and dropped packets.
    clientSerialContext := modbusone.NewSerialContext(clientSerial, baudRate)
    serverSerialContext := modbusone.NewSerialContext(serverSerial, baudRate)

    // You can create either a client or a server from a SerialContext and an id.
    client := modbusone.NewRTUClient(clientSerialContext, id)
    server := modbusone.NewRTUServer(serverSerialContext, id)

    useClientAndServer(client, server, id) // follow the next function

    // Output:
    // reqs count: 2
    // reqs count: 3
    // server ReadHoldingRegisters from 0, quantity 125
    // client WriteHoldingRegisters from 0, quantity 125
    // server ReadHoldingRegisters from 125, quantity 75
    // client WriteHoldingRegisters from 125, quantity 75
    // client ReadHoldingRegisters from 1000, quantity 100
    // server WriteHoldingRegisters from 1000, quantity 100
    // server ReadHoldingRegisters from 0, quantity 125
    // client WriteHoldingRegisters from 0, quantity 125
    // server ReadHoldingRegisters from 125, quantity 75
    // client WriteHoldingRegisters from 125, quantity 75
    // client ReadHoldingRegisters from 1000, quantity 100
    // server WriteHoldingRegisters from 1000, quantity 100
    // serve terminated: io: read/write on closed pipe
}

func useClientAndServer(client modbusone.Client, server modbusone.ServerCloser, id byte) {
    termChan := make(chan error)

    // Serve is blocking until the serial connection has io errors or is closed.
    // So we use a goroutine to start it and continue setting up our demo.
    go client.Serve(handlerGenerator("client"))
    go func() {
        // A server is Started to same way as a client
        err := server.Serve(handlerGenerator("server"))
        // Do something with the err here.
        // For a command line app, you probably want to terminate.
        // For a service, you probably want to wait until you can open the serial port again.
        termChan <- err
    }()
    defer client.Close()
    defer server.Close()

    // If you only need to support server side, then you are done.
    // If you need to support client side, then you need to make requests.
    clientDoTransactions(client, id) // see following function

    // Clean up
    server.Close()
    fmt.Println("serve terminated:", <-termChan)
}

func clientDoTransactions(client modbusone.Client, id byte) {
    // start by building some requests
    startAddress := uint16(0)
    quantity := uint16(200)
    reqs, err := modbusone.MakePDURequestHeaders(modbusone.FcReadHoldingRegisters,
        startAddress, quantity, nil)
    if err != nil {
        fmt.Println(err) // if what you asked for is not possible.
    }
    // Larger than allowed requests are split to many packets.
    fmt.Println("reqs count:", len(reqs))

    // We can add more requests, even of different types.
    // The last nil is replaced by the reqs to append to.
    startAddress = uint16(1000)
    quantity = uint16(100)
    reqs, err = modbusone.MakePDURequestHeaders(modbusone.FcWriteMultipleRegisters,
        startAddress, quantity, reqs)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("reqs count:", len(reqs))

    // Range over the requests to handle each individually,
    for _, r := range reqs {
        err = client.DoTransaction(r)
        if err != nil {
            fmt.Println(err, "on", r) // The server timed out, or the connection was closed.
        }
    }
    // or just do them all at once. Notice that reqs can be reused.
    n, err := modbusone.DoTransactions(client, id, reqs)
    if err != nil {
        fmt.Println(err, "on", reqs[n])
    }
}

func Example_tcp() {
    // TCP address of the host
    host := "127.2.9.1:12345"

    // Default server id
    id := byte(1)

    // Open server tcp listener:
    listener, err := net.Listen("tcp", host)
    if err != nil {
        fmt.Println(err)
        return
    }

    // Connect to server:
    conn, err := net.Dial("tcp", host)
    if err != nil {
        fmt.Println(err)
        return
    }

    // You can create either a client or a server
    client := modbusone.NewTCPClient(conn, 0)
    server := modbusone.NewTCPServer(listener)

    // shared example code with serial port
    useClientAndServer(client, server, id)

    // Output:
    // reqs count: 2
    // reqs count: 3
    // server ReadHoldingRegisters from 0, quantity 125
    // client WriteHoldingRegisters from 0, quantity 125
    // server ReadHoldingRegisters from 125, quantity 75
    // client WriteHoldingRegisters from 125, quantity 75
    // client ReadHoldingRegisters from 1000, quantity 100
    // server WriteHoldingRegisters from 1000, quantity 100
    // server ReadHoldingRegisters from 0, quantity 125
    // client WriteHoldingRegisters from 0, quantity 125
    // server ReadHoldingRegisters from 125, quantity 75
    // client WriteHoldingRegisters from 125, quantity 75
    // client ReadHoldingRegisters from 1000, quantity 100
    // server WriteHoldingRegisters from 1000, quantity 100
    // serve terminated: accept tcp 127.2.9.1:12345: use of closed network connection
}

// end readme example
For more usage examples, see examples/memory, which is a command line application that can be used as either a server or a client.

Architecture

modbusone architecture

Why

There exist Modbus libraries for Go, such as goburrow/modbus and flosse/go-modbus. However they do not include any server APIs. Even if server function is implemented, user code will have to be written separately to support running both as client and server.

In my use case, client/server should be interchangeable. User code should worry about how to handle the translation of MODBUS data model to application logic. The only difference is the client also initiate requests.

This means that a remote function call like API, which is effective as a client-side API, is insufficient.

Instead, a callback based API (like http server handler) is used for both server and client.

Implemented

  • Serial RTU
  • Modbus over TCP
  • Function Codes 1-6,15,16
  • Server and Client API
  • Server and Client Tester (examples/memory)

Development

This project and API is stable, and I am using it in production.

My primary usage is RTU (over RS-485). TCP is also supported. Others may or may not be implemented in the future.

Contribution to new or existing functionally, or just changing a private identifier public are welcome, as well as documentation, test, example code or any other improvements.

Development tools:

  • go generate runs embedmd to copy parts of the examples_test.go to this readme file
  • golangci-lint run for improvement hints. Ideally there are no warnings.
  • Use go test -race -count=5 ./... pre release.

Breaking Changes

2022-09-09 v1.0.0

V1 release has the following depreciated identifiers removed:

  • The Server interface is removed. Use ServerCloser instead.
  • Public global variable DebugOut is changed to private. Use SetDebugOut(w io.Writer) instead for thread safety.
  • Type alias type ProtocalHandler = ProtocolHandler removed.
  • Function redirect from GetRTUBidirectionSizeFromHeader to GetRTUBidirectionalSizeFromHeader removed.
  • Function redirect from NewRTUCLient to NewRTUClient removed.

2018-09-27 v0.2.0

  • NewRTUPacketReader returns PacketReader interface instead of io.Reader. When a new RTU server or client receives a SerialContext, it will test if it is also a PacketReader, and only create a new NewRTUPacketReader if not.
  • (client/server).Serve() now also closes themselves when returned. This avoids some potentially bad usages. Before, the behavior was undefined.

2017-06-13 pre-v0.1.0

  • Removed dependency on goburrow/serial. All serial connections should be created with NewSerialContext, which can accept any ReadWriteCloser

Challenges

Compatibility with a wide range of serial hardware/drivers. (good)

Compatibility with existing Modbus environments, including non-compliance and extensions. (good)

Recover from transmission errors and timeouts, to work continuously unattended. (good)

Better test coverage that also tests error conditions. (todo)

Fuzz testing. (todo)

Failover mode

TLDR: do not use.

Failover has landed in v0.2.0, but it should be considered less stable than the other parts.

In mission-critical applications, or anywhere hardware redundancy is cheaper than downtime, having a standby system taking over in case of the failure of the primary system is desirable.

Ideally, failover is implemented in the application level, which speaks over two serial ports simultaneously, only acting on the values from one of the ports at a time. However, this may not always be possible. A "foreign" application, which you have no control over, might not have this feature. As such, failover mode attempts to addresses this by allowing two separate hardware devices sharing a single serial bus to appear as a single device. This failover mode is outside the design of the original Modbus protocol.

The basic operation of failover mode is to stay quiet on the port until the primary fails. While staying quiet, it relays all reads and writes to the application side as if it is the primary. This allows the application to stay in sync for a hot switch over when the primary fails. While on standby and in Client (Master) mode, writes may be received by the handler that is not initiated by that Client.

Definitions

Client/Server
Also called Master/Slave in the context of serial communication.
PDU
Protocol data unit, MODBUS application protocol, include function code and data. The same format no matter what the lower level protocol is.
ADU
Application data unit, PDU prepended with Server addresses and postpended with error check, as needed.
RTU
Remote terminal unit, in the context of Modbus, it is a raw wire protocol delimited by a delay. RTU is an example of ADU.

License

This library is distributed under the BSD-style license found in the LICENSE file.

See also licenses folder for origins of large blocks of source code.

Documentation

Overview

Package modbusone provides a Modbus library to implement both server and client using one set of APIs.

For sample code, see examples/memory, and handler2serial_test.go

Example (SerialPort)
package main

import (
	"fmt"
	"io"

	"github.com/xiegeo/modbusone"
)

// handlerGenerator returns ProtocolHandlers that interact with our application.
// In this example, we are only using Holding Registers.
func handlerGenerator(name string) modbusone.ProtocolHandler {
	return &modbusone.SimpleHandler{
		ReadHoldingRegisters: func(address, quantity uint16) ([]uint16, error) {
			fmt.Printf("%v ReadHoldingRegisters from %v, quantity %v\n",
				name, address, quantity)
			r := make([]uint16, quantity)

			return r, nil
		},
		WriteHoldingRegisters: func(address uint16, values []uint16) error {
			fmt.Printf("%v WriteHoldingRegisters from %v, quantity %v\n",
				name, address, len(values))

			return nil
		},
		OnErrorImp: func(req modbusone.PDU, errRep modbusone.PDU) {
			fmt.Printf("%v received error:%x in request:%x", name, errRep, req)
		},
	}
}

// serial is a fake serial port.
type serial struct {
	io.ReadCloser
	io.WriteCloser
}

func newInternalSerial() (io.ReadWriteCloser, io.ReadWriteCloser) {
	r1, w1 := io.Pipe()
	r2, w2 := io.Pipe()
	return &serial{ReadCloser: r1, WriteCloser: w2}, &serial{ReadCloser: r2, WriteCloser: w1}
}

func (s *serial) Close() error {
	s.ReadCloser.Close()
	return s.WriteCloser.Close()
}

func main() {
	// Server id and baudRate, for Modbus over serial port.
	id := byte(1)
	baudRate := int64(19200)

	// Open serial connections:
	clientSerial, serverSerial := newInternalSerial()
	// Normally we want to open a serial connection from serial.OpenPort
	// such as github.com/tarm/serial. modbusone can take any io.ReadWriteCloser,
	// so we created two that talks to each other for demonstration here.

	// SerialContext adds baudRate information to calculate
	// the duration that data transfers should takes.
	// It also records Stats of read and dropped packets.
	clientSerialContext := modbusone.NewSerialContext(clientSerial, baudRate)
	serverSerialContext := modbusone.NewSerialContext(serverSerial, baudRate)

	// You can create either a client or a server from a SerialContext and an id.
	client := modbusone.NewRTUClient(clientSerialContext, id)
	server := modbusone.NewRTUServer(serverSerialContext, id)

	useClientAndServer(client, server, id) // follow the next function

}

func useClientAndServer(client modbusone.Client, server modbusone.ServerCloser, id byte) {
	termChan := make(chan error)

	go client.Serve(handlerGenerator("client"))
	go func() {

		err := server.Serve(handlerGenerator("server"))

		termChan <- err
	}()
	defer client.Close()
	defer server.Close()

	clientDoTransactions(client, id)

	server.Close()
	fmt.Println("serve terminated:", <-termChan)
}

func clientDoTransactions(client modbusone.Client, id byte) {

	startAddress := uint16(0)
	quantity := uint16(200)
	reqs, err := modbusone.MakePDURequestHeaders(modbusone.FcReadHoldingRegisters,
		startAddress, quantity, nil)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println("reqs count:", len(reqs))

	startAddress = uint16(1000)
	quantity = uint16(100)
	reqs, err = modbusone.MakePDURequestHeaders(modbusone.FcWriteMultipleRegisters,
		startAddress, quantity, reqs)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("reqs count:", len(reqs))

	for _, r := range reqs {
		err = client.DoTransaction(r)
		if err != nil {
			fmt.Println(err, "on", r)
		}
	}

	n, err := modbusone.DoTransactions(client, id, reqs)
	if err != nil {
		fmt.Println(err, "on", reqs[n])
	}
}
Output:

reqs count: 2
reqs count: 3
server ReadHoldingRegisters from 0, quantity 125
client WriteHoldingRegisters from 0, quantity 125
server ReadHoldingRegisters from 125, quantity 75
client WriteHoldingRegisters from 125, quantity 75
client ReadHoldingRegisters from 1000, quantity 100
server WriteHoldingRegisters from 1000, quantity 100
server ReadHoldingRegisters from 0, quantity 125
client WriteHoldingRegisters from 0, quantity 125
server ReadHoldingRegisters from 125, quantity 75
client WriteHoldingRegisters from 125, quantity 75
client ReadHoldingRegisters from 1000, quantity 100
server WriteHoldingRegisters from 1000, quantity 100
serve terminated: io: read/write on closed pipe
Example (Tcp)
package main

import (
	"fmt"
	"net"

	"github.com/xiegeo/modbusone"
)

// handlerGenerator returns ProtocolHandlers that interact with our application.
// In this example, we are only using Holding Registers.
func handlerGenerator(name string) modbusone.ProtocolHandler {
	return &modbusone.SimpleHandler{
		ReadHoldingRegisters: func(address, quantity uint16) ([]uint16, error) {
			fmt.Printf("%v ReadHoldingRegisters from %v, quantity %v\n",
				name, address, quantity)
			r := make([]uint16, quantity)

			return r, nil
		},
		WriteHoldingRegisters: func(address uint16, values []uint16) error {
			fmt.Printf("%v WriteHoldingRegisters from %v, quantity %v\n",
				name, address, len(values))

			return nil
		},
		OnErrorImp: func(req modbusone.PDU, errRep modbusone.PDU) {
			fmt.Printf("%v received error:%x in request:%x", name, errRep, req)
		},
	}
}

func useClientAndServer(client modbusone.Client, server modbusone.ServerCloser, id byte) {
	termChan := make(chan error)

	go client.Serve(handlerGenerator("client"))
	go func() {

		err := server.Serve(handlerGenerator("server"))

		termChan <- err
	}()
	defer client.Close()
	defer server.Close()

	clientDoTransactions(client, id)

	server.Close()
	fmt.Println("serve terminated:", <-termChan)
}

func clientDoTransactions(client modbusone.Client, id byte) {

	startAddress := uint16(0)
	quantity := uint16(200)
	reqs, err := modbusone.MakePDURequestHeaders(modbusone.FcReadHoldingRegisters,
		startAddress, quantity, nil)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println("reqs count:", len(reqs))

	startAddress = uint16(1000)
	quantity = uint16(100)
	reqs, err = modbusone.MakePDURequestHeaders(modbusone.FcWriteMultipleRegisters,
		startAddress, quantity, reqs)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("reqs count:", len(reqs))

	for _, r := range reqs {
		err = client.DoTransaction(r)
		if err != nil {
			fmt.Println(err, "on", r)
		}
	}

	n, err := modbusone.DoTransactions(client, id, reqs)
	if err != nil {
		fmt.Println(err, "on", reqs[n])
	}
}

func main() {
	// TCP address of the host
	host := "127.2.9.1:12345"

	// Default server id
	id := byte(1)

	// Open server tcp listener:
	listener, err := net.Listen("tcp", host)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Connect to server:
	conn, err := net.Dial("tcp", host)
	if err != nil {
		fmt.Println(err)
		return
	}

	// You can create either a client or a server
	client := modbusone.NewTCPClient(conn, 0)
	server := modbusone.NewTCPServer(listener)

	// shared example code with serial port
	useClientAndServer(client, server, id)

}
Output:

reqs count: 2
reqs count: 3
server ReadHoldingRegisters from 0, quantity 125
client WriteHoldingRegisters from 0, quantity 125
server ReadHoldingRegisters from 125, quantity 75
client WriteHoldingRegisters from 125, quantity 75
client ReadHoldingRegisters from 1000, quantity 100
server WriteHoldingRegisters from 1000, quantity 100
server ReadHoldingRegisters from 0, quantity 125
client WriteHoldingRegisters from 0, quantity 125
server ReadHoldingRegisters from 125, quantity 75
client WriteHoldingRegisters from 125, quantity 75
client ReadHoldingRegisters from 1000, quantity 100
server WriteHoldingRegisters from 1000, quantity 100
serve terminated: accept tcp 127.2.9.1:12345: use of closed network connection

Index

Examples

Constants

View Source
const (
	TCPHeaderLength  = 6
	MBAPHeaderLength = TCPHeaderLength + 1
)
View Source
const MaxPDUSize = 253

MaxPDUSize is the max possible size of a PDU packet.

View Source
const MaxRTUSize = 256

MaxRTUSize is the max possible size of a RTU packet.

Variables

View Source
var DefaultCPUHiccup = time.Second / 10

DefaultCPUHiccup is the max amount of time the local host is allowed to freeze before we break packets appear and throw away old unused partial packet data.

View Source
var ErrFcNotSupported = errors.New("this FunctionCode is not supported")

ErrFcNotSupported is another version of EcIllegalFunction, encountering of this error shows the error is locally generated, not a remote ExceptionCode.

View Source
var ErrServerTimeOut = errors.New("server timed out")

ErrServerTimeOut is the time out error for StartTransaction.

View Source
var ErrorCrc = fmt.Errorf("RTU data crc not valid")

ErrorCrc indicates data corruption detected by checking the CRC.

View Source
var OverSizeMaxRTU = MaxRTUSize

OverSizeMaxRTU overrides MaxRTUSize when OverSizeSupport is true.

View Source
var OverSizeSupport = false

OverSizeSupport ignores max packet size and encoded number of bytes to support over sided implementations encountered in the wild. This setting only applies to the server end, since client is always reserved in what it requests. Also change OverSizeMaxRTU properly.

Functions

func BoolsToData

func BoolsToData(values []bool, fc FunctionCode) ([]byte, error)

BoolsToData translates []bool to the data part of PDU dependent on FunctionCode.

func BytesDelay

func BytesDelay(baudRate int64, n int) time.Duration

BytesDelay returns the time it takes to send n bytes in baudRate.

func DataToBools

func DataToBools(data []byte, count uint16, fc FunctionCode) ([]bool, error)

DataToBools translates the data part of PDU to []bool dependent on FunctionCode.

func DataToRegisters

func DataToRegisters(data []byte) ([]uint16, error)

DataToRegisters translates the data part of PDU to []uint16.

func DoTransactions

func DoTransactions(c RTUTransactionStarter, slaveID byte, reqs []PDU) (int, error)

DoTransactions runs the reqs transactions in order. If any error is encountered, it returns early and reports the index number and error message.

func GetPDUSizeFromHeader

func GetPDUSizeFromHeader(header []byte, isClient bool) int

GetPDUSizeFromHeader returns the expected sized of a PDU packet with the given PDU header, if not enough info is in the header, then it returns the shortest possible. isClient is true if a client/master is reading the packet.

func GetPacketCutoffDurationFromSerialContext added in v0.9.0

func GetPacketCutoffDurationFromSerialContext(s SerialContext, n int) time.Duration

func GetRTUBidirectionalSizeFromHeader added in v0.2.0

func GetRTUBidirectionalSizeFromHeader(header []byte) int

GetRTUBidirectionalSizeFromHeader is like GetRTUSizeFromHeader, except for any direction by checking the CRC for disambiguation of length.

func GetRTUSizeFromHeader

func GetRTUSizeFromHeader(header []byte, isClient bool) int

GetRTUSizeFromHeader returns the expected sized of a RTU packet with the given RTU header, if not enough info is in the header, then it returns the shortest possible. isClient is true if a client/master is reading the packet.

func IsRequestReply added in v0.2.0

func IsRequestReply(r, a PDU) bool

IsRequestReply test if PDUs are a request reply pair, useful for listening to transactions passively.

func MatchPDU

func MatchPDU(ask PDU, ans PDU) bool

MatchPDU returns true if ans is a valid reply to ask, including normal and error code replies.

func MinDelay

func MinDelay(baudRate int64) time.Duration

MinDelay returns the minimum Delay of 3.5 bytes between packets or 1750 micros.

func PacketCutoffDuration added in v0.9.0

func PacketCutoffDuration(baudRate int64, n int, cpuHiccup time.Duration) time.Duration

func RegistersToData

func RegistersToData(values []uint16) ([]byte, error)

RegistersToData translates []uint16 to the data part of PDU.

func SetDebugOut added in v0.2.0

func SetDebugOut(w io.Writer)

SetDebugOut to print debug messages, set to nil to turn off debug output.

func Uint64ToSlaveID

func Uint64ToSlaveID(n uint64) (byte, error)

Uint64ToSlaveID is a helper function for reading configuration data to SlaveID. See also flag.Uint64 and strconv.ParseUint.

Types

type Client added in v0.9.0

type Client interface {
	ServerCloser
	RTUTransactionStarter
	DoTransaction(req PDU) error
}

Client interface can both start and serve transactions.

type ExceptionCode

type ExceptionCode byte //nolint:errname

ExceptionCode Modbus exception codes.

const (
	// EcOK is invented for no error.
	EcOK ExceptionCode = 0
	// EcInternal is invented for error reading ExceptionCode.
	EcInternal                           ExceptionCode = 255
	EcIllegalFunction                    ExceptionCode = 1
	EcIllegalDataAddress                 ExceptionCode = 2
	EcIllegalDataValue                   ExceptionCode = 3
	EcServerDeviceFailure                ExceptionCode = 4
	EcAcknowledge                        ExceptionCode = 5
	EcServerDeviceBusy                   ExceptionCode = 6
	EcMemoryParityError                  ExceptionCode = 8
	EcGatewayPathUnavailable             ExceptionCode = 10
	EcGatewayTargetDeviceFailedToRespond ExceptionCode = 11
)

Defined exception codes, 5 to 11 are not used.

func ToExceptionCode

func ToExceptionCode(err error) ExceptionCode

ToExceptionCode turns an error into an ExceptionCode (to send in PDU). Best effort with EcServerDeviceFailure as fail back.

- If the error is a ExceptionCode or warped ExceptionCode, the original ExceptionCode is returned. - IF the error is ErrFcNotSupported or warped ErrFcNotSupported, EcIllegalFunction is returned. - For all other cases, EcServerDeviceFailure is returned.

func (ExceptionCode) Error

func (e ExceptionCode) Error() string

Error implements error for ExceptionCode.

type FailoverRTUClient added in v0.2.0

type FailoverRTUClient struct {
	SlaveID byte
	// contains filtered or unexported fields
}

FailoverRTUClient implements Client/Master side logic for RTU over a SerialContext to be used by a ProtocolHandler with failover function.

func NewFailoverRTUClient added in v0.2.0

func NewFailoverRTUClient(com SerialContext, isFailover bool, slaveID byte) *FailoverRTUClient

NewFailoverRTUClient create a new client with failover function communicating over SerialContext with the give slaveID as default.

If isFailover is true, it is the secondary.

func (*FailoverRTUClient) Close added in v0.2.0

func (c *FailoverRTUClient) Close() error

Close closes the client and closes the connect.

func (*FailoverRTUClient) DoTransaction added in v0.2.0

func (c *FailoverRTUClient) DoTransaction(req PDU) error

DoTransaction starts a transaction, and returns a channel that returns an error or nil, with the default slaveID.

DoTransaction is blocking.

For read from server, the PDU is sent as is (after been warped up in RTU) For write to server, the data part given will be ignored, and filled in by data from handler.

func (*FailoverRTUClient) GetTransactionTimeOut added in v0.2.0

func (c *FailoverRTUClient) GetTransactionTimeOut(reqLen, ansLen int) time.Duration

GetTransactionTimeOut returns the total time to wait for a transaction (server response) to time out, given the expected length of RTU packets. This function is also used internally to calculate timeout.

func (*FailoverRTUClient) Serve added in v0.2.0

func (c *FailoverRTUClient) Serve(handler ProtocolHandler) error

Serve serves FailoverRTUClient side handlers,

A FailoverRTUClient expects a lot of "unexpected" read packets and "lost" writes so it is does not do the error checking that a normal client does, but instead try to guess the best interpretation.

func (*FailoverRTUClient) SetServerProcessingTime added in v0.2.0

func (c *FailoverRTUClient) SetServerProcessingTime(t time.Duration)

SetServerProcessingTime sets the time to wait for a server response, the total wait time also includes the time needed for data transmission.

func (*FailoverRTUClient) StartTransactionToServer added in v0.2.0

func (c *FailoverRTUClient) StartTransactionToServer(slaveID byte, req PDU, errChan chan error)

StartTransactionToServer starts a transaction, with a custom slaveID. errChan is required and usable, an error is set is the transaction failed, or nil for success.

StartTransactionToServer is not blocking.

For read from server, the PDU is sent as is (after been warped up in RTU) For write to server, the data part given will be ignored, and filled in by data from handler.

type FailoverSerialConn added in v0.2.0

type FailoverSerialConn struct {
	SerialContext // base SerialContext
	PacketReader

	// if primary has not received data for this long, it thinks it's disconnected
	// and go passive, just like at restart.
	// This potentially allows a long running passive with better data to take over
	// until primary has gained enough historical data.
	// default 10 seconds
	PrimaryDisconnectDelay time.Duration

	// when a failover is running,
	// the time it waits to take over again.
	// default 10 mins
	PrimaryForceBackDelay time.Duration

	// SecondaryDelay is the delay to use on a secondary to give time for the primary to reply first.
	// Default 0.1 seconds.
	SecondaryDelay time.Duration
	// MissDelay is the delay to use by the primary when passive to detect missed packets by secondary.
	// It must be bigger than SecondaryDelay for primary to detect an active failover.
	// Default 0.2 seconds.
	MissDelay time.Duration

	// number of misses until the other is detected as down
	// default 2 for primary, 4 for failover
	MissesMax int32
	// contains filtered or unexported fields
}

FailoverSerialConn manages a failover connection, which does failover using shared serial bus and shared slaveID. Slaves using other ids on the same bus is not supported. If the other side supports multiple slave ids, then it is better to implement failover on the other side by call different slaveIDs, or using separated serial ports.

func NewFailoverConn added in v0.2.0

func NewFailoverConn(sc SerialContext, isFailover, isClient bool) *FailoverSerialConn

NewFailoverConn adds failover function to a SerialContext.

func (*FailoverSerialConn) BytesDelay added in v0.2.0

func (s *FailoverSerialConn) BytesDelay(n int) time.Duration

BytesDelay implements BytesDelay for SerialContext.

func (*FailoverSerialConn) IsActive added in v0.2.0

func (s *FailoverSerialConn) IsActive() bool

IsActive returns if the connetion is in the active state. This state can change asynchronous so it is not useful for logic other than status gethering or testing in a controlled envernment.

func (*FailoverSerialConn) Read added in v0.2.0

func (s *FailoverSerialConn) Read(b []byte) (int, error)

Read reads the serial port.

func (*FailoverSerialConn) Write added in v0.2.0

func (s *FailoverSerialConn) Write(b []byte) (int, error)

type FunctionCode

type FunctionCode byte

FunctionCode Modbus function codes.

const (
	FcReadCoils              FunctionCode = 1
	FcReadDiscreteInputs     FunctionCode = 2
	FcReadHoldingRegisters   FunctionCode = 3
	FcReadInputRegisters     FunctionCode = 4
	FcWriteSingleCoil        FunctionCode = 5
	FcWriteSingleRegister    FunctionCode = 6
	FcWriteMultipleCoils     FunctionCode = 15
	FcWriteMultipleRegisters FunctionCode = 16
)

Implemented FunctionCodes.

func (FunctionCode) IsBool

func (f FunctionCode) IsBool() bool

IsBool returns true if the FunctionCode concerns boolean values.

func (FunctionCode) IsReadToServer

func (f FunctionCode) IsReadToServer() bool

IsReadToServer returns true if the FunctionCode is a read. FunctionCode 23 is both a read and write.

func (FunctionCode) IsSingle

func (f FunctionCode) IsSingle() bool

IsSingle returns true if the FunctionCode can transmit only one value.

func (FunctionCode) IsUint16

func (f FunctionCode) IsUint16() bool

IsUint16 returns true if the FunctionCode concerns 16bit values.

func (FunctionCode) IsWriteToServer

func (f FunctionCode) IsWriteToServer() bool

IsWriteToServer returns true if the FunctionCode is a write. FunctionCode 23 is both a read and write.

func (FunctionCode) MakeRequestHeader

func (f FunctionCode) MakeRequestHeader(address, quantity uint16) (PDU, error)

MakeRequestHeader makes a particular PDU without any data, to be used for client side StartTransaction. The inverse functions are PDU.GetFunctionCode() .GetAddress() and .GetRequestCount().

func (FunctionCode) MaxPerPacket

func (f FunctionCode) MaxPerPacket() uint16

MaxPerPacket returns the max number of values a FunctionCode can carry.

func (FunctionCode) MaxPerPacketSized

func (f FunctionCode) MaxPerPacketSized(size int) uint16

MaxPerPacketSized returns the max number of values a FunctionCode can carry, if we are to further limit PDU packet size from MaxRTUSize. At least 1 (8 for bools) is returned if size is too small.

func (FunctionCode) MaxRange

func (f FunctionCode) MaxRange() uint16

MaxRange is the largest address in the Modbus protocol.

func (FunctionCode) SeparateError

func (f FunctionCode) SeparateError() (bool, FunctionCode)

SeparateError test if FunctionCode is an error response, and also return the version without error flag set.

func (FunctionCode) Valid

func (f FunctionCode) Valid() bool

Valid test if FunctionCode is a supported function, and not an error response.

func (FunctionCode) WithError

func (f FunctionCode) WithError() FunctionCode

WithError return a copy of FunctionCode with the error flag set.

type Option added in v0.9.0

type Option struct {
	CPUHiccup          time.Duration
	ReturnShortPackets bool
}

type PDU

type PDU []byte

PDU is the Modbus Protocol Data Unit.

func ExceptionReplyPacket

func ExceptionReplyPacket(req PDU, e ExceptionCode) PDU

ExceptionReplyPacket make a PDU packet to reply to request req with ExceptionCode e.

func MakePDURequestHeaders

func MakePDURequestHeaders(fc FunctionCode, address, quantity uint16, appendTO []PDU) ([]PDU, error)

MakePDURequestHeaders generates the list of PDU request headers by splitting quantity into allowed sizes. Returns an error if quantity is out of range.

func MakePDURequestHeadersSized

func MakePDURequestHeadersSized(fc FunctionCode, address, quantity uint16, maxPerPacket uint16, appendTO []PDU) ([]PDU, error)

MakePDURequestHeadersSized generates the list of PDU request headers by splitting quantity into sizes of maxPerPacket or less. Returns an error if quantity is out of range.

You can use FunctionCode.MaxPerPacketSized to calculate one with the wanted byte length.

func (PDU) GetAddress

func (p PDU) GetAddress() uint16

GetAddress returns the starting address, If PDU is invalid, behavior is undefined (can panic).

func (PDU) GetFunctionCode

func (p PDU) GetFunctionCode() FunctionCode

GetFunctionCode returns the function code.

func (PDU) GetReplyValues

func (p PDU) GetReplyValues() ([]byte, error)

GetReplyValues returns the values in a read reply.

func (PDU) GetRequestCount

func (p PDU) GetRequestCount() (uint16, error)

GetRequestCount returns the number of values requested, If PDU is invalid (too short), return 0 with error.

func (PDU) GetRequestValues

func (p PDU) GetRequestValues() ([]byte, error)

GetRequestValues returns the values in a write request.

func (PDU) MakeReadReply

func (p PDU) MakeReadReply(data []byte) PDU

MakeReadReply produces the reply PDU based on the request PDU and read data.

func (PDU) MakeWriteReply

func (p PDU) MakeWriteReply() PDU

MakeWriteReply assumes the request is a successful write, and make the associated response.

func (PDU) MakeWriteRequest

func (p PDU) MakeWriteRequest(data []byte) PDU

MakeWriteRequest produces the request PDU based on the request PDU header and (locally) read data.

func (PDU) ValidateRequest

func (p PDU) ValidateRequest() error

ValidateRequest tests for errors in a received Request PDU packet. Use ToExceptionCode to get the ExceptionCode for error. Checks for errors 2 and 3 are done in GetRequestValues.

type PacketReader added in v0.2.0

type PacketReader interface {
	io.Reader
	PacketReaderFace()
}

PacketReader signals that this reader returns full ADU packets.

func NewRTUBidirectionalPacketReader added in v0.2.0

func NewRTUBidirectionalPacketReader(r SerialContext) PacketReader

NewRTUBidirectionalPacketReader create a Reader that attempt to read full packets that comes from either server or client.

func NewRTUPacketReader

func NewRTUPacketReader(r SerialContext, isClient bool) PacketReader

NewRTUPacketReader create a Reader that attempt to read full packets.

type ProtocolHandler added in v0.2.0

type ProtocolHandler interface {
	// OnWrite is called on the server for a write request,
	// or on the client for read reply.
	// For write to server on server side, data is part of req.
	// For read from server on client side, req is the req from client, and
	// data is part of reply.
	OnWrite(req PDU, data []byte) error

	// OnRead is called on the server for a read request,
	// or on the client before write request.
	// For read from server on the server side, req is from client and data is
	// part of reply.
	// For write to server on the client side, req is from local action
	// (such as RTUClient.StartTransaction), and data will be added to req to send
	// to server.
	OnRead(req PDU) (data []byte, err error)

	// OnError is called on the client when it receive a well formed
	// error from server
	OnError(req PDU, errRep PDU)
}

ProtocolHandler handles PDUs based on if it is a write or read from the local perspective.

type RTU

type RTU []byte

RTU is the Modbus RTU Application Data Unit.

func MakeRTU

func MakeRTU(slaveID byte, p PDU) RTU

MakeRTU makes a RTU with slaveID and PDU.

func (RTU) GetPDU

func (r RTU) GetPDU() (PDU, error)

GetPDU returns the PDU inside, CRC is checked.

func (RTU) IsMulticast

func (r RTU) IsMulticast() bool

IsMulticast returns true if slaveID is the multicast address 0.

type RTUClient

type RTUClient struct {
	SlaveID byte
	// contains filtered or unexported fields
}

RTUClient implements Client/Master side logic for RTU over a SerialContext to be used by a ProtocolHandler.

func NewRTUClient added in v0.2.0

func NewRTUClient(com SerialContext, slaveID byte) *RTUClient

NewRTUClient create a new client communicating over SerialContext with the given slaveID as default.

func (*RTUClient) Close added in v0.2.0

func (c *RTUClient) Close() error

Close closes the client and closes the connection.

func (*RTUClient) DoTransaction

func (c *RTUClient) DoTransaction(req PDU) error

DoTransaction starts a transaction, and returns a channel that returns an error or nil, with the default slaveID.

DoTransaction is blocking.

For read from server, the PDU is sent as is (after been warped up in RTU) For write to server, the data part given will be ignored, and filled in by data from handler.

func (*RTUClient) GetTransactionTimeOut

func (c *RTUClient) GetTransactionTimeOut(reqLen, ansLen int) time.Duration

GetTransactionTimeOut returns the total time to wait for a transaction (server response) to time out, given the expected length of RTU packets. This function is also used internally to calculate timeout.

func (*RTUClient) Serve

func (c *RTUClient) Serve(handler ProtocolHandler) error

Serve serves RTUClient handlers.

func (*RTUClient) SetServerProcessingTime

func (c *RTUClient) SetServerProcessingTime(t time.Duration)

SetServerProcessingTime sets the time to wait for a server response, the total wait time also includes the time needed for data transmission.

func (*RTUClient) StartTransactionToServer

func (c *RTUClient) StartTransactionToServer(slaveID byte, req PDU, errChan chan error)

StartTransactionToServer starts a transaction, with a custom slaveID. errChan is required, an error is set is the transaction failed, or nil for success.

StartTransactionToServer is not blocking.

For read from server, the PDU is sent as is (after been warped up in RTU) For write to server, the data part given will be ignored, and filled in by data from handler.

type RTUServer

type RTUServer struct {
	SlaveID byte
	// contains filtered or unexported fields
}

RTUServer implements Server/Slave side logic for RTU over a SerialContext to be used by a ProtocolHandler.

func NewRTUServer

func NewRTUServer(com SerialContext, slaveID byte) *RTUServer

NewRTUServer creates a RTU server on SerialContext listening on slaveID.

func (*RTUServer) Close added in v0.2.0

func (s *RTUServer) Close() error

Close closes the server and closes the connect.

func (*RTUServer) Serve

func (s *RTUServer) Serve(handler ProtocolHandler) error

Serve runs the server and only returns after unrecoverable error, such as SerialContext is closed.

type RTUTransactionStarter

type RTUTransactionStarter interface {
	StartTransactionToServer(slaveID byte, req PDU, errChan chan error)
}

RTUTransactionStarter is an interface implemented by RTUClient.

type SerialContext

type SerialContext interface {
	io.ReadWriteCloser
	// RTUMinDelay returns the minimum required delay between packets for framing.
	MinDelay() time.Duration
	// RTUBytesDelay returns the duration it takes to send n bytes.
	BytesDelay(n int) time.Duration
	// Stats reporting.
	Stats() *Stats
}

SerialContext is an interface implemented by SerialPort, can also be mocked for testing.

func NewSerialContext

func NewSerialContext(conn io.ReadWriteCloser, baudRate int64) SerialContext

NewSerialContext creates a SerialContext from any io.ReadWriteCloser.

func NewSerialContextWithOption added in v0.9.0

func NewSerialContextWithOption(conn io.ReadWriteCloser, baudRate int64, option Option) SerialContext

type SerialContextV2 added in v0.9.0

type SerialContextV2 interface {
	SerialContext
	// PacketCutoffDuration returns the duration to force packet breaks,
	// with the duration it took to read current data considered.
	PacketCutoffDuration(n int) time.Duration
}

SerialContextV2 is an superset interface of SerialContext, to support customizing CPU hiccup time.

type ServerCloser added in v0.9.0

type ServerCloser interface {
	Serve(handler ProtocolHandler) error
	io.Closer
}

ServerCloser is the common interface for all Clients and Servers that use ProtocolHandlers.

type SimpleHandler

type SimpleHandler struct {
	// ReadDiscreteInputs handles server side FC=2
	ReadDiscreteInputs func(address, quantity uint16) ([]bool, error)
	// ReadDiscreteInputs handles client side FC=2
	WriteDiscreteInputs func(address uint16, values []bool) error

	// ReadCoils handles client side FC=5&15, server side FC=1
	ReadCoils func(address, quantity uint16) ([]bool, error)
	// WriteCoils handles client side FC=1, server side FC=5&15
	WriteCoils func(address uint16, values []bool) error

	// ReadInputRegisters handles server side FC=4
	ReadInputRegisters func(address, quantity uint16) ([]uint16, error)
	// ReadDiscreteInputs handles client side FC=4
	WriteInputRegisters func(address uint16, values []uint16) error

	// ReadHoldingRegisters handles client side FC=6&16, server side FC=3
	ReadHoldingRegisters func(address, quantity uint16) ([]uint16, error)
	// WriteHoldingRegisters handles client side FC=3, server side FC=6&16
	WriteHoldingRegisters func(address uint16, values []uint16) error

	// OnErrorImp handles OnError
	OnErrorImp func(req PDU, errRep PDU)
}

SimpleHandler implements ProtocolHandler, any nil function returns ErrFcNotSupported.

func (*SimpleHandler) OnError

func (h *SimpleHandler) OnError(req PDU, errRep PDU)

OnError is called by a Server, set OnErrorImp to catch the calls.

func (*SimpleHandler) OnRead

func (h *SimpleHandler) OnRead(req PDU) ([]byte, error)

OnRead is called by a Server, set Read... to catch the calls.

func (*SimpleHandler) OnWrite

func (h *SimpleHandler) OnWrite(req PDU, data []byte) error

OnWrite is called by a Server, set Write... to catch the calls.

type Stats

type Stats struct {
	ReadPackets      int64
	CrcErrors        int64
	RemoteErrors     int64
	OtherErrors      int64
	LongReadWarnings int64
	FormateWarnings  int64
	IDDrops          int64
	OtherDrops       int64
}

Stats records statics on a SerialContext, must be aligned to 64 bits on 32 bit systems.

func (*Stats) Reset added in v0.2.0

func (s *Stats) Reset()

Reset the stats to zero.

func (*Stats) String added in v0.2.0

func (s *Stats) String() string

func (*Stats) TotalDrops added in v0.2.0

func (s *Stats) TotalDrops() int64

TotalDrops adds up all the errors for the total number of read packets dropped.

type TCPClient added in v0.9.0

type TCPClient struct {
	SlaveID byte
	// contains filtered or unexported fields
}

TCPClient implements Client/Master side logic for Modbus over a TCP connection to be used by a ProtocolHandler.

func NewTCPClient added in v0.9.0

func NewTCPClient(conn io.ReadWriteCloser, slaveID byte) *TCPClient

NewTCPClient create a new client communicating over a TCP connection with the given slaveID as default.

func (*TCPClient) Close added in v0.9.0

func (c *TCPClient) Close() error

Close closes the client and closes the TCP connection.

func (*TCPClient) DoTransaction added in v0.9.0

func (c *TCPClient) DoTransaction(req PDU) error

DoTransaction starts a transaction, and returns a channel that returns an error or nil, with the default slaveID.

DoTransaction is blocking.

For read from server, the PDU is sent as is (after been warped up in RTU) For write to server, the data part given will be ignored, and filled in by data from handler.

func (*TCPClient) DoTransaction2 added in v0.9.0

func (c *TCPClient) DoTransaction2(slaveID byte, req PDU) error

DoTransaction2 is DoTransaction with a settable slaveID.

func (*TCPClient) Serve added in v0.9.0

func (c *TCPClient) Serve(handler ProtocolHandler) error

Serve serves TCPClient handlers.

func (*TCPClient) StartTransactionToServer added in v0.9.0

func (c *TCPClient) StartTransactionToServer(slaveID byte, req PDU, errChan chan error)

StartTransactionToServer starts a transaction, with a custom slaveID. errChan is required, an error is set if the transaction failed, or nil for success.

StartTransactionToServer is not blocking.

For read from server, the PDU is sent as is (after been warped up in RTU). For write to server, the data part given will be ignored, and filled in by data from handler.

type TCPServer added in v0.9.0

type TCPServer struct {
	// contains filtered or unexported fields
}

TCPServer implements Server/Slave side logic for Modbus over TCP to be used by a ProtocolHandler.

func NewTCPServer added in v0.9.0

func NewTCPServer(listener net.Listener) *TCPServer

NewTCPServer runs TCP server.

func (*TCPServer) Close added in v0.9.0

func (s *TCPServer) Close() error

Close closes the server and closes the listener.

func (*TCPServer) Serve added in v0.9.0

func (s *TCPServer) Serve(handler ProtocolHandler) error

Serve runs the server and only returns after a connection or data error occurred. The underling connection is always closed before this function returns.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL