// 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: // Function code : 1 byte (0x01) // Starting address : 2 bytes // Quantity of coils : 2 bytes // Response: // Function code : 1 byte (0x01) // Byte count : 1 byte // Coil status : N* bytes (=N or N+1) 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: // Function code : 1 byte (0x02) // Starting address : 2 bytes // Quantity of inputs : 2 bytes // Response: // Function code : 1 byte (0x02) // Byte count : 1 byte // Input status : N* bytes (=N or N+1) 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: // Function code : 1 byte (0x03) // Starting address : 2 bytes // Quantity of registers : 2 bytes // Response: // Function code : 1 byte (0x03) // Byte count : 1 byte // Register value : Nx2 bytes 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: // Function code : 1 byte (0x04) // Starting address : 2 bytes // Quantity of registers : 2 bytes // Response: // Function code : 1 byte (0x04) // Byte count : 1 byte // Input registers : N bytes 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: // Function code : 1 byte (0x05) // Output address : 2 bytes // Output value : 2 bytes // Response: // Function code : 1 byte (0x05) // Output address : 2 bytes // Output value : 2 bytes 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: // Function code : 1 byte (0x06) // Register address : 2 bytes // Register value : 2 bytes // Response: // Function code : 1 byte (0x06) // Register address : 2 bytes // Register value : 2 bytes 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: // Function code : 1 byte (0x0F) // Starting address : 2 bytes // Quantity of outputs : 2 bytes // Byte count : 1 byte // Outputs value : N* bytes // Response: // Function code : 1 byte (0x0F) // Starting address : 2 bytes // Quantity of outputs : 2 bytes 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: // Function code : 1 byte (0x10) // Starting address : 2 bytes // Quantity of outputs : 2 bytes // Byte count : 1 byte // Registers value : N* bytes // Response: // Function code : 1 byte (0x10) // Starting address : 2 bytes // Quantity of registers : 2 bytes 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: // Function code : 1 byte (0x16) // Reference address : 2 bytes // AND-mask : 2 bytes // OR-mask : 2 bytes // Response: // Function code : 1 byte (0x16) // Reference address : 2 bytes // AND-mask : 2 bytes // OR-mask : 2 bytes 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: // 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 // Response: // Function code : 1 byte (0x17) // Byte count : 1 byte // Read registers value : Nx2 bytes 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: // Function code : 1 byte (0x18) // FIFO pointer address : 2 bytes // Response: // Function code : 1 byte (0x18) // Byte count : 2 bytes // FIFO count : 2 bytes // FIFO count : 2 bytes (<=31) // FIFO value register : Nx2 bytes 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 }