sungrow/modbus/asciiclient.go
2021-01-10 15:14:38 +11:00

227 lines
5.6 KiB
Go

// 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 (
"bytes"
"encoding/hex"
"fmt"
"time"
)
const (
asciiStart = ":"
asciiEnd = "\r\n"
asciiMinSize = 3
asciiMaxSize = 513
hexTable = "0123456789ABCDEF"
)
// ASCIIClientHandler implements Packager and Transporter interface.
type ASCIIClientHandler struct {
asciiPackager
asciiSerialTransporter
}
// NewASCIIClientHandler allocates and initializes a ASCIIClientHandler.
func NewASCIIClientHandler(address string) *ASCIIClientHandler {
handler := &ASCIIClientHandler{}
handler.Address = address
handler.Timeout = serialTimeout
handler.IdleTimeout = serialIdleTimeout
return handler
}
// ASCIIClient creates ASCII client with default handler and given connect string.
func ASCIIClient(address string) Client {
handler := NewASCIIClientHandler(address)
return NewClient(handler)
}
// asciiPackager implements Packager interface.
type asciiPackager struct {
SlaveId byte
}
// Encode encodes PDU in a ASCII frame:
// Start : 1 char
// Address : 2 chars
// Function : 2 chars
// Data : 0 up to 2x252 chars
// LRC : 2 chars
// End : 2 chars
func (mb *asciiPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) {
var buf bytes.Buffer
if _, err = buf.WriteString(asciiStart); err != nil {
return
}
if err = writeHex(&buf, []byte{mb.SlaveId, pdu.FunctionCode}); err != nil {
return
}
if err = writeHex(&buf, pdu.Data); err != nil {
return
}
// Exclude the beginning colon and terminating CRLF pair characters
var lrc lrc
lrc.reset()
lrc.pushByte(mb.SlaveId).pushByte(pdu.FunctionCode).pushBytes(pdu.Data)
if err = writeHex(&buf, []byte{lrc.value()}); err != nil {
return
}
if _, err = buf.WriteString(asciiEnd); err != nil {
return
}
adu = buf.Bytes()
return
}
// Verify verifies response length, frame boundary and slave id.
func (mb *asciiPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) {
length := len(aduResponse)
// Minimum size (including address, function and LRC)
if length < asciiMinSize+6 {
err = fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", length, 9)
return
}
// Length excluding colon must be an even number
if length%2 != 1 {
err = fmt.Errorf("modbus: response length '%v' is not an even number", length-1)
return
}
// First char must be a colon
str := string(aduResponse[0:len(asciiStart)])
if str != asciiStart {
err = fmt.Errorf("modbus: response frame '%v'... is not started with '%v'", str, asciiStart)
return
}
// 2 last chars must be \r\n
str = string(aduResponse[len(aduResponse)-len(asciiEnd):])
if str != asciiEnd {
err = fmt.Errorf("modbus: response frame ...'%v' is not ended with '%v'", str, asciiEnd)
return
}
// Slave id
responseVal, err := readHex(aduResponse[1:])
if err != nil {
return
}
requestVal, err := readHex(aduRequest[1:])
if err != nil {
return
}
if responseVal != requestVal {
err = fmt.Errorf("modbus: response slave id '%v' does not match request '%v'", responseVal, requestVal)
return
}
return
}
// Decode extracts PDU from ASCII frame and verify LRC.
func (mb *asciiPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) {
pdu = &ProtocolDataUnit{}
// Slave address
address, err := readHex(adu[1:])
if err != nil {
return
}
// Function code
if pdu.FunctionCode, err = readHex(adu[3:]); err != nil {
return
}
// Data
dataEnd := len(adu) - 4
data := adu[5:dataEnd]
pdu.Data = make([]byte, hex.DecodedLen(len(data)))
if _, err = hex.Decode(pdu.Data, data); err != nil {
return
}
// LRC
lrcVal, err := readHex(adu[dataEnd:])
if err != nil {
return
}
// Calculate checksum
var lrc lrc
lrc.reset()
lrc.pushByte(address).pushByte(pdu.FunctionCode).pushBytes(pdu.Data)
if lrcVal != lrc.value() {
err = fmt.Errorf("modbus: response lrc '%v' does not match expected '%v'", lrcVal, lrc.value())
return
}
return
}
// asciiSerialTransporter implements Transporter interface.
type asciiSerialTransporter struct {
serialPort
}
func (mb *asciiSerialTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) {
mb.serialPort.mu.Lock()
defer mb.serialPort.mu.Unlock()
// 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 %q\n", aduRequest)
if _, err = mb.port.Write(aduRequest); err != nil {
return
}
// Get the response
var n int
var data [asciiMaxSize]byte
length := 0
for {
if n, err = mb.port.Read(data[length:]); err != nil {
return
}
length += n
if length >= asciiMaxSize || n == 0 {
break
}
// Expect end of frame in the data received
if length > asciiMinSize {
if string(data[length-len(asciiEnd):length]) == asciiEnd {
break
}
}
}
aduResponse = data[:length]
mb.serialPort.logf("modbus: received %q\n", aduResponse)
return
}
// writeHex encodes byte to string in hexadecimal, e.g. 0xA5 => "A5"
// (encoding/hex only supports lowercase string).
func writeHex(buf *bytes.Buffer, value []byte) (err error) {
var str [2]byte
for _, v := range value {
str[0] = hexTable[v>>4]
str[1] = hexTable[v&0x0F]
if _, err = buf.Write(str[:]); err != nil {
return
}
}
return
}
// readHex decodes hexa string to byte, e.g. "8C" => 0x8C.
func readHex(data []byte) (value byte, err error) {
var dst [1]byte
if _, err = hex.Decode(dst[:], data[0:2]); err != nil {
return
}
value = dst[0]
return
}