sungrow/modbus/client.go

528 lines
15 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"
)
// ClientHandler is the interface that groups the Packager and Transporter methods.
type ClientHandler interface {
Packager
Transporter
}
type client struct {
packager Packager
transporter Transporter
}
// NewClient creates a new modbus client with given backend handler.
func NewClient(handler ClientHandler) Client {
return &client{packager: handler, transporter: handler}
}
// NewClient2 creates a new modbus client with given backend packager and transporter.
func NewClient2(packager Packager, transporter Transporter) Client {
return &client{packager: packager, transporter: transporter}
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x01)
// Starting address : 2 bytes
// Quantity of coils : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x01)
// Byte count : 1 byte
// Coil status : N* bytes (=N or N+1)
2021-01-10 14:43:46 +11:00
func (mb *client) ReadCoils(address, quantity uint16) (results []byte, err error) {
if quantity < 1 || quantity > 2000 {
err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 2000)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeReadCoils,
Data: dataBlock(address, quantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
count := int(response.Data[0])
length := len(response.Data) - 1
if count != length {
err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count)
return
}
results = response.Data[1:]
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x02)
// Starting address : 2 bytes
// Quantity of inputs : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x02)
// Byte count : 1 byte
// Input status : N* bytes (=N or N+1)
2021-01-10 14:43:46 +11:00
func (mb *client) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) {
if quantity < 1 || quantity > 2000 {
err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 2000)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeReadDiscreteInputs,
Data: dataBlock(address, quantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
count := int(response.Data[0])
length := len(response.Data) - 1
if count != length {
err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count)
return
}
results = response.Data[1:]
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x03)
// Starting address : 2 bytes
// Quantity of registers : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x03)
// Byte count : 1 byte
// Register value : Nx2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) ReadHoldingRegisters(address, quantity uint16) (results []byte, err error) {
if quantity < 1 || quantity > 125 {
err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 125)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeReadHoldingRegisters,
Data: dataBlock(address, quantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
count := int(response.Data[0])
length := len(response.Data) - 1
if count != length {
err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count)
return
}
results = response.Data[1:]
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x04)
// Starting address : 2 bytes
// Quantity of registers : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x04)
// Byte count : 1 byte
// Input registers : N bytes
2021-01-10 14:43:46 +11:00
func (mb *client) ReadInputRegisters(address, quantity uint16) (results []byte, err error) {
if quantity < 1 || quantity > 125 {
err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 125)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeReadInputRegisters,
Data: dataBlock(address, quantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
count := int(response.Data[0])
length := len(response.Data) - 1
if count != length {
err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count)
return
}
results = response.Data[1:]
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x05)
// Output address : 2 bytes
// Output value : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x05)
// Output address : 2 bytes
// Output value : 2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) WriteSingleCoil(address, value uint16) (results []byte, err error) {
// The requested ON/OFF state can only be 0xFF00 and 0x0000
if value != 0xFF00 && value != 0x0000 {
err = fmt.Errorf("modbus: state '%v' must be either 0xFF00 (ON) or 0x0000 (OFF)", value)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeWriteSingleCoil,
Data: dataBlock(address, value),
}
response, err := mb.send(&request)
if err != nil {
return
}
// Fixed response length
if len(response.Data) != 4 {
err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4)
return
}
respValue := binary.BigEndian.Uint16(response.Data)
if address != respValue {
err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address)
return
}
results = response.Data[2:]
respValue = binary.BigEndian.Uint16(results)
if value != respValue {
err = fmt.Errorf("modbus: response value '%v' does not match request '%v'", respValue, value)
return
}
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x06)
// Register address : 2 bytes
// Register value : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x06)
// Register address : 2 bytes
// Register value : 2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) WriteSingleRegister(address, value uint16) (results []byte, err error) {
request := ProtocolDataUnit{
FunctionCode: FuncCodeWriteSingleRegister,
Data: dataBlock(address, value),
}
response, err := mb.send(&request)
if err != nil {
return
}
// Fixed response length
if len(response.Data) != 4 {
err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4)
return
}
respValue := binary.BigEndian.Uint16(response.Data)
if address != respValue {
err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address)
return
}
results = response.Data[2:]
respValue = binary.BigEndian.Uint16(results)
if value != respValue {
err = fmt.Errorf("modbus: response value '%v' does not match request '%v'", respValue, value)
return
}
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x0F)
// Starting address : 2 bytes
// Quantity of outputs : 2 bytes
// Byte count : 1 byte
// Outputs value : N* bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x0F)
// Starting address : 2 bytes
// Quantity of outputs : 2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) {
if quantity < 1 || quantity > 1968 {
err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 1968)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeWriteMultipleCoils,
Data: dataBlockSuffix(value, address, quantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
// Fixed response length
if len(response.Data) != 4 {
err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4)
return
}
respValue := binary.BigEndian.Uint16(response.Data)
if address != respValue {
err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address)
return
}
results = response.Data[2:]
respValue = binary.BigEndian.Uint16(results)
if quantity != respValue {
err = fmt.Errorf("modbus: response quantity '%v' does not match request '%v'", respValue, quantity)
return
}
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x10)
// Starting address : 2 bytes
// Quantity of outputs : 2 bytes
// Byte count : 1 byte
// Registers value : N* bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x10)
// Starting address : 2 bytes
// Quantity of registers : 2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) WriteMultipleRegisters(address, quantity uint16, value []byte) (results []byte, err error) {
if quantity < 1 || quantity > 123 {
err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 123)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeWriteMultipleRegisters,
Data: dataBlockSuffix(value, address, quantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
// Fixed response length
if len(response.Data) != 4 {
err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4)
return
}
respValue := binary.BigEndian.Uint16(response.Data)
if address != respValue {
err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address)
return
}
results = response.Data[2:]
respValue = binary.BigEndian.Uint16(results)
if quantity != respValue {
err = fmt.Errorf("modbus: response quantity '%v' does not match request '%v'", respValue, quantity)
return
}
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x16)
// Reference address : 2 bytes
// AND-mask : 2 bytes
// OR-mask : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x16)
// Reference address : 2 bytes
// AND-mask : 2 bytes
// OR-mask : 2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) {
request := ProtocolDataUnit{
FunctionCode: FuncCodeMaskWriteRegister,
Data: dataBlock(address, andMask, orMask),
}
response, err := mb.send(&request)
if err != nil {
return
}
// Fixed response length
if len(response.Data) != 6 {
err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 6)
return
}
respValue := binary.BigEndian.Uint16(response.Data)
if address != respValue {
err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address)
return
}
respValue = binary.BigEndian.Uint16(response.Data[2:])
if andMask != respValue {
err = fmt.Errorf("modbus: response AND-mask '%v' does not match request '%v'", respValue, andMask)
return
}
respValue = binary.BigEndian.Uint16(response.Data[4:])
if orMask != respValue {
err = fmt.Errorf("modbus: response OR-mask '%v' does not match request '%v'", respValue, orMask)
return
}
results = response.Data[2:]
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x17)
// Read starting address : 2 bytes
// Quantity to read : 2 bytes
// Write starting address: 2 bytes
// Quantity to write : 2 bytes
// Write byte count : 1 byte
// Write registers value : N* bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x17)
// Byte count : 1 byte
// Read registers value : Nx2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
if readQuantity < 1 || readQuantity > 125 {
err = fmt.Errorf("modbus: quantity to read '%v' must be between '%v' and '%v',", readQuantity, 1, 125)
return
}
if writeQuantity < 1 || writeQuantity > 121 {
err = fmt.Errorf("modbus: quantity to write '%v' must be between '%v' and '%v',", writeQuantity, 1, 121)
return
}
request := ProtocolDataUnit{
FunctionCode: FuncCodeReadWriteMultipleRegisters,
Data: dataBlockSuffix(value, readAddress, readQuantity, writeAddress, writeQuantity),
}
response, err := mb.send(&request)
if err != nil {
return
}
count := int(response.Data[0])
if count != (len(response.Data) - 1) {
err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", len(response.Data)-1, count)
return
}
results = response.Data[1:]
return
}
// Request:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x18)
// FIFO pointer address : 2 bytes
//
2021-01-10 14:43:46 +11:00
// Response:
2023-05-19 17:50:26 +10:00
//
// Function code : 1 byte (0x18)
// Byte count : 2 bytes
// FIFO count : 2 bytes
// FIFO count : 2 bytes (<=31)
// FIFO value register : Nx2 bytes
2021-01-10 14:43:46 +11:00
func (mb *client) ReadFIFOQueue(address uint16) (results []byte, err error) {
request := ProtocolDataUnit{
FunctionCode: FuncCodeReadFIFOQueue,
Data: dataBlock(address),
}
response, err := mb.send(&request)
if err != nil {
return
}
if len(response.Data) < 4 {
err = fmt.Errorf("modbus: response data size '%v' is less than expected '%v'", len(response.Data), 4)
return
}
count := int(binary.BigEndian.Uint16(response.Data))
if count != (len(response.Data) - 1) {
err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", len(response.Data)-1, count)
return
}
count = int(binary.BigEndian.Uint16(response.Data[2:]))
if count > 31 {
err = fmt.Errorf("modbus: fifo count '%v' is greater than expected '%v'", count, 31)
return
}
results = response.Data[4:]
return
}
// Helpers
// send sends request and checks possible exception in the response.
func (mb *client) send(request *ProtocolDataUnit) (response *ProtocolDataUnit, err error) {
aduRequest, err := mb.packager.Encode(request)
if err != nil {
return
}
aduResponse, err := mb.transporter.Send(aduRequest)
if err != nil {
return
}
if err = mb.packager.Verify(aduRequest, aduResponse); err != nil {
return
}
response, err = mb.packager.Decode(aduResponse)
if err != nil {
return
}
// Check correct function code returned (exception)
if response.FunctionCode != request.FunctionCode {
err = responseError(response)
return
}
if response.Data == nil || len(response.Data) == 0 {
// Empty response
err = fmt.Errorf("modbus: response data is empty")
return
}
return
}
// dataBlock creates a sequence of uint16 data.
func dataBlock(value ...uint16) []byte {
data := make([]byte, 2*len(value))
for i, v := range value {
binary.BigEndian.PutUint16(data[i*2:], v)
}
return data
}
// dataBlockSuffix creates a sequence of uint16 data and append the suffix plus its length.
func dataBlockSuffix(suffix []byte, value ...uint16) []byte {
length := 2 * len(value)
data := make([]byte, length+1+len(suffix))
for i, v := range value {
binary.BigEndian.PutUint16(data[i*2:], v)
}
data[length] = uint8(len(suffix))
copy(data[length+1:], suffix)
return data
}
func responseError(response *ProtocolDataUnit) error {
mbError := &ModbusError{FunctionCode: response.FunctionCode}
if response.Data != nil && len(response.Data) > 0 {
mbError.ExceptionCode = response.Data[0]
}
return mbError
}