modbusone

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 15, 2018 License: BSD-3-Clause Imports: 8 Imported by: 0

README

ModbusOne GoDoc

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

Architecture

modbusone architecture

For usage examples, see examples/memory and handler2serial_test.go

Why

There exists Modbus libraries for Go, such as goburrow/modbus and flosse/go-modbus. But 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
  • Function Codes 1-6,15,16
  • Server and Client API
  • Server and Client Tester (examples/memory)

Development

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

API stability is best effort. This means:

  • Changes should not break users code, unless there is a compelling reason.

  • Code broken by API changes should not compile, new errors to user code should not be introduced silently.

  • API changes will be documented to help someone losing their mind when working code stopped compiling.

My primary usage is RTU (over RS-485). 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.

Breaking Changes

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

Challenges

Packet separation uses a mix of length and timing indications. Length is used if CRC is valid, otherwise timing indications is used to find where the next packet starts.

Compatibility with wide range of serial hardware/drivers.

Compatibility with different existing Modbus environments. (needs more testing)

Recover from transmission errors and timeouts, to work continuously unattended. (needs more testing)

Fuzze testing against crashes.

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 context of Modbus, it is a raw wire protocol delimited by 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

Index

Constants

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 DebugOut io.Writer

DebugOut sets where to print debug messages

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")
View Source
var OverSizeMaxRTU = MaxRTUSize

OverSizeMaxRTU overides 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 requestes. 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 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 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 minum Delay of 3.5 bytes between packets or 1750 mircos

func NewRTUPacketReader

func NewRTUPacketReader(r SerialContext, isClient bool) io.Reader

NewRTUPacketReader create a Reader that attempt to read full packets.

func RegistersToData

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

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

func Uint64ToSlaveID

func Uint64ToSlaveID(n uint64) (byte, error)

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

Types

type ExceptionCode

type ExceptionCode byte

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.

func (ExceptionCode) Error

func (e ExceptionCode) Error() string

Error implements error for ExceptionCode

type FunctionCode

type FunctionCode byte

FunctionCode Modebus 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 write. 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 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 spliting 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 spliting 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 stating 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

GetRequestCount returns the number of values requested, If PDU is invalid (too short), behavior is undefined (can panic).

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 ProtocalHandler

type ProtocalHandler interface {
	//OnInput 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

	//OnOutput is called on the server for a read request,
	//or on the client before write requst.
	//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)
}

ProtocalHandler 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 ProtocalHandler

func NewRTUCLient

func NewRTUCLient(com SerialContext, slaveID byte) *RTUClient

NewRTUCLient create a new client communicating over SerialContext with the give slaveID as default.

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 ProtocalHandler) error

Serve serves RTUClient side handlers, must close SerialContext after error is returned, to clean up.

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 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 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 ProtocalHandler

func NewRTUServer

func NewRTUServer(com SerialContext, slaveID byte) *RTUServer

NewRTUServer creates a RTU server on SerialContext listening on slaveID

func (*RTUServer) Serve

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

Serve runs the server and only returns after unrecoverable error, such as SerialContext is closed. Read is assumed to only read full packets, as per RTU delay based spec.

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 is 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

type Server

type Server interface {
	Serve(handler ProtocalHandler) error
}

Server is the common interface for all Clients and Servers that use ProtocalHandlers

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 ProtocalHandler, 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
}

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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