sungrow/modbus/rtuclient.go

211 lines
5.5 KiB
Go
Raw Normal View History

2021-01-10 14:43:46 +11:00
// Copyright 2014 Quoc-Viet Nguyen. All rights reserved.
// This software may be modified and distributed under the terms
// of the BSD license. See the LICENSE file for details.
package modbus
import (
"encoding/binary"
"fmt"
"io"
"time"
)
const (
rtuMinSize = 4
rtuMaxSize = 256
rtuExceptionSize = 5
)
// RTUClientHandler implements Packager and Transporter interface.
type RTUClientHandler struct {
rtuPackager
rtuSerialTransporter
}
// NewRTUClientHandler allocates and initializes a RTUClientHandler.
func NewRTUClientHandler(address string) *RTUClientHandler {
handler := &RTUClientHandler{}
handler.Address = address
handler.Timeout = serialTimeout
handler.IdleTimeout = serialIdleTimeout
return handler
}
// RTUClient creates RTU client with default handler and given connect string.
func RTUClient(address string) Client {
handler := NewRTUClientHandler(address)
return NewClient(handler)
}
// rtuPackager implements Packager interface.
type rtuPackager struct {
SlaveId byte
}
// Encode encodes PDU in a RTU frame:
// Slave Address : 1 byte
// Function : 1 byte
// Data : 0 up to 252 bytes
// CRC : 2 byte
func (mb *rtuPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) {
length := len(pdu.Data) + 4
if length > rtuMaxSize {
err = fmt.Errorf("modbus: length of data '%v' must not be bigger than '%v'", length, rtuMaxSize)
return
}
adu = make([]byte, length)
adu[0] = mb.SlaveId
adu[1] = pdu.FunctionCode
copy(adu[2:], pdu.Data)
// Append crc
var crc crc
crc.reset().pushBytes(adu[0 : length-2])
checksum := crc.value()
adu[length-1] = byte(checksum >> 8)
adu[length-2] = byte(checksum)
return
}
// Verify verifies response length and slave id.
func (mb *rtuPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) {
length := len(aduResponse)
// Minimum size (including address, function and CRC)
if length < rtuMinSize {
err = fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", length, rtuMinSize)
return
}
// Slave address must match
if aduResponse[0] != aduRequest[0] {
err = fmt.Errorf("modbus: response slave id '%v' does not match request '%v'", aduResponse[0], aduRequest[0])
return
}
return
}
// Decode extracts PDU from RTU frame and verify CRC.
func (mb *rtuPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) {
length := len(adu)
// Calculate checksum
var crc crc
crc.reset().pushBytes(adu[0 : length-2])
checksum := uint16(adu[length-1])<<8 | uint16(adu[length-2])
if checksum != crc.value() {
err = fmt.Errorf("modbus: response crc '%v' does not match expected '%v'", checksum, crc.value())
return
}
// Function code & data
pdu = &ProtocolDataUnit{}
pdu.FunctionCode = adu[1]
pdu.Data = adu[2 : length-2]
return
}
// rtuSerialTransporter implements Transporter interface.
type rtuSerialTransporter struct {
serialPort
}
func (mb *rtuSerialTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) {
// Make sure port is connected
if err = mb.serialPort.connect(); err != nil {
return
}
// Start the timer to close when idle
mb.serialPort.lastActivity = time.Now()
mb.serialPort.startCloseTimer()
// Send the request
mb.serialPort.logf("modbus: sending % x\n", aduRequest)
if _, err = mb.port.Write(aduRequest); err != nil {
return
}
function := aduRequest[1]
functionFail := aduRequest[1] & 0x80
bytesToRead := calculateResponseLength(aduRequest)
time.Sleep(mb.calculateDelay(len(aduRequest) + bytesToRead))
var n int
var n1 int
var data [rtuMaxSize]byte
//We first read the minimum length and then read either the full package
//or the error package, depending on the error status (byte 2 of the response)
n, err = io.ReadAtLeast(mb.port, data[:], rtuMinSize)
if err != nil {
return
}
//if the function is correct
if data[1] == function {
//we read the rest of the bytes
if n < bytesToRead {
if bytesToRead > rtuMinSize && bytesToRead <= rtuMaxSize {
if bytesToRead > n {
n1, err = io.ReadFull(mb.port, data[n:bytesToRead])
n += n1
}
}
}
} else if data[1] == functionFail {
//for error we need to read 5 bytes
if n < rtuExceptionSize {
n1, err = io.ReadFull(mb.port, data[n:rtuExceptionSize])
}
n += n1
}
if err != nil {
return
}
aduResponse = data[:n]
mb.serialPort.logf("modbus: received % x\n", aduResponse)
return
}
// calculateDelay roughly calculates time needed for the next frame.
// See MODBUS over Serial Line - Specification and Implementation Guide (page 13).
func (mb *rtuSerialTransporter) calculateDelay(chars int) time.Duration {
var characterDelay, frameDelay int // us
if mb.BaudRate <= 0 || mb.BaudRate > 19200 {
characterDelay = 750
frameDelay = 1750
} else {
characterDelay = 15000000 / mb.BaudRate
frameDelay = 35000000 / mb.BaudRate
}
return time.Duration(characterDelay*chars+frameDelay) * time.Microsecond
}
func calculateResponseLength(adu []byte) int {
length := rtuMinSize
switch adu[1] {
case FuncCodeReadDiscreteInputs,
FuncCodeReadCoils:
count := int(binary.BigEndian.Uint16(adu[4:]))
length += 1 + count/8
if count%8 != 0 {
length++
}
case FuncCodeReadInputRegisters,
FuncCodeReadHoldingRegisters,
FuncCodeReadWriteMultipleRegisters:
count := int(binary.BigEndian.Uint16(adu[4:]))
length += 1 + count*2
case FuncCodeWriteSingleCoil,
FuncCodeWriteMultipleCoils,
FuncCodeWriteSingleRegister,
FuncCodeWriteMultipleRegisters:
length += 4
case FuncCodeMaskWriteRegister:
length += 6
case FuncCodeReadFIFOQueue:
// undetermined
default:
}
return length
}