// 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 }