| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- // 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"
- "log"
- "net"
- "sync"
- "sync/atomic"
- "time"
- )
- const (
- tcpProtocolIdentifier uint16 = 0x0000
- // Modbus Application Protocol
- tcpHeaderSize = 7
- tcpMaxLength = 260
- // Default TCP timeout is not set
- tcpTimeout = 10 * time.Second
- tcpIdleTimeout = 60 * time.Second
- )
- // TCPClientHandler implements Packager and Transporter interface.
- type TCPClientHandler struct {
- tcpPackager
- tcpTransporter
- }
- // NewTCPClientHandler allocates a new TCPClientHandler.
- func NewTCPClientHandler(address string) *TCPClientHandler {
- h := &TCPClientHandler{}
- h.Address = address
- h.Timeout = tcpTimeout
- h.IdleTimeout = tcpIdleTimeout
- return h
- }
- // TCPClient creates TCP client with default handler and given connect string.
- func TCPClient(address string) Client {
- handler := NewTCPClientHandler(address)
- return NewClient(handler)
- }
- // tcpPackager implements Packager interface.
- type tcpPackager struct {
- // For synchronization between messages of server & client
- transactionId uint32
- // Broadcast address is 0
- SlaveId byte
- }
- // Encode adds modbus application protocol header:
- //
- // Transaction identifier: 2 bytes
- // Protocol identifier: 2 bytes
- // Length: 2 bytes
- // Unit identifier: 1 byte
- // Function code: 1 byte
- // Data: n bytes
- func (mb *tcpPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) {
- adu = make([]byte, tcpHeaderSize+1+len(pdu.Data))
- // Transaction identifier
- transactionId := atomic.AddUint32(&mb.transactionId, 1)
- binary.BigEndian.PutUint16(adu, uint16(transactionId))
- // Protocol identifier
- binary.BigEndian.PutUint16(adu[2:], tcpProtocolIdentifier)
- // Length = sizeof(SlaveId) + sizeof(FunctionCode) + Data
- length := uint16(1 + 1 + len(pdu.Data))
- binary.BigEndian.PutUint16(adu[4:], length)
- // Unit identifier
- adu[6] = mb.SlaveId
- // PDU
- adu[tcpHeaderSize] = pdu.FunctionCode
- copy(adu[tcpHeaderSize+1:], pdu.Data)
- return
- }
- // Verify confirms transaction, protocol and unit id.
- func (mb *tcpPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) {
- // Transaction id
- responseVal := binary.BigEndian.Uint16(aduResponse)
- requestVal := binary.BigEndian.Uint16(aduRequest)
- if responseVal != requestVal {
- err = fmt.Errorf("modbus: response transaction id '%v' does not match request '%v'", responseVal, requestVal)
- return
- }
- // Protocol id
- responseVal = binary.BigEndian.Uint16(aduResponse[2:])
- requestVal = binary.BigEndian.Uint16(aduRequest[2:])
- if responseVal != requestVal {
- err = fmt.Errorf("modbus: response protocol id '%v' does not match request '%v'", responseVal, requestVal)
- return
- }
- // Unit id (1 byte)
- if aduResponse[6] != aduRequest[6] {
- err = fmt.Errorf("modbus: response unit id '%v' does not match request '%v'", aduResponse[6], aduRequest[6])
- return
- }
- return
- }
- // Decode extracts PDU from TCP frame:
- //
- // Transaction identifier: 2 bytes
- // Protocol identifier: 2 bytes
- // Length: 2 bytes
- // Unit identifier: 1 byte
- func (mb *tcpPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) {
- // Read length value in the header
- length := binary.BigEndian.Uint16(adu[4:])
- pduLength := len(adu) - tcpHeaderSize
- if pduLength <= 0 || pduLength != int(length-1) {
- err = fmt.Errorf("modbus: length in response '%v' does not match pdu data length '%v'", length-1, pduLength)
- return
- }
- pdu = &ProtocolDataUnit{}
- // The first byte after header is function code
- pdu.FunctionCode = adu[tcpHeaderSize]
- pdu.Data = adu[tcpHeaderSize+1:]
- return
- }
- // tcpTransporter implements Transporter interface.
- type tcpTransporter struct {
- // Connect string
- Address string
- // Connect & Read timeout
- Timeout time.Duration
- // Idle timeout to close the connection
- IdleTimeout time.Duration
- // Transmission logger
- Logger *log.Logger
- // TCP connection
- mu sync.Mutex
- conn net.Conn
- closeTimer *time.Timer
- lastActivity time.Time
- }
- // Send sends data to server and ensures response length is greater than header length.
- func (mb *tcpTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) {
- mb.mu.Lock()
- defer mb.mu.Unlock()
- // Establish a new connection if not connected
- if err = mb.connect(); err != nil {
- return
- }
- // Set timer to close when idle
- mb.lastActivity = time.Now()
- mb.startCloseTimer()
- // Set write and read timeout
- var timeout time.Time
- if mb.Timeout > 0 {
- timeout = mb.lastActivity.Add(mb.Timeout)
- }
- if err = mb.conn.SetDeadline(timeout); err != nil {
- return
- }
- // Send data
- mb.logf("modbus: sending % x", aduRequest)
- if _, err = mb.conn.Write(aduRequest); err != nil {
- return
- }
- // Read header first
- var data [tcpMaxLength]byte
- if _, err = io.ReadFull(mb.conn, data[:tcpHeaderSize]); err != nil {
- return
- }
- // Read length, ignore transaction & protocol id (4 bytes)
- length := int(binary.BigEndian.Uint16(data[4:]))
- if length <= 0 {
- mb.flush(data[:])
- err = fmt.Errorf("modbus: length in response header '%v' must not be zero", length)
- return
- }
- if length > (tcpMaxLength - (tcpHeaderSize - 1)) {
- mb.flush(data[:])
- err = fmt.Errorf("modbus: length in response header '%v' must not greater than '%v'", length, tcpMaxLength-tcpHeaderSize+1)
- return
- }
- // Skip unit id
- length += tcpHeaderSize - 1
- if _, err = io.ReadFull(mb.conn, data[tcpHeaderSize:length]); err != nil {
- return
- }
- aduResponse = data[:length]
- mb.logf("modbus: received % x\n", aduResponse)
- return
- }
- // Connect establishes a new connection to the address in Address.
- // Connect and Close are exported so that multiple requests can be done with one session
- func (mb *tcpTransporter) Connect() error {
- mb.mu.Lock()
- defer mb.mu.Unlock()
- return mb.connect()
- }
- func (mb *tcpTransporter) connect() error {
- if mb.conn == nil {
- dialer := net.Dialer{Timeout: mb.Timeout}
- conn, err := dialer.Dial("tcp", mb.Address)
- if err != nil {
- return err
- }
- mb.conn = conn
- }
- return nil
- }
- func (mb *tcpTransporter) startCloseTimer() {
- if mb.IdleTimeout <= 0 {
- return
- }
- if mb.closeTimer == nil {
- mb.closeTimer = time.AfterFunc(mb.IdleTimeout, mb.closeIdle)
- } else {
- mb.closeTimer.Reset(mb.IdleTimeout)
- }
- }
- // Close closes current connection.
- func (mb *tcpTransporter) Close() error {
- mb.mu.Lock()
- defer mb.mu.Unlock()
- return mb.close()
- }
- // flush flushes pending data in the connection,
- // returns io.EOF if connection is closed.
- func (mb *tcpTransporter) flush(b []byte) (err error) {
- if err = mb.conn.SetReadDeadline(time.Now()); err != nil {
- return
- }
- // Timeout setting will be reset when reading
- if _, err = mb.conn.Read(b); err != nil {
- // Ignore timeout error
- if netError, ok := err.(net.Error); ok && netError.Timeout() {
- err = nil
- }
- }
- return
- }
- func (mb *tcpTransporter) logf(format string, v ...interface{}) {
- if mb.Logger != nil {
- mb.Logger.Printf(format, v...)
- }
- }
- // closeLocked closes current connection. Caller must hold the mutex before calling this method.
- func (mb *tcpTransporter) close() (err error) {
- if mb.conn != nil {
- err = mb.conn.Close()
- mb.conn = nil
- }
- return
- }
- // closeIdle closes the connection if last activity is passed behind IdleTimeout.
- func (mb *tcpTransporter) closeIdle() {
- mb.mu.Lock()
- defer mb.mu.Unlock()
- if mb.IdleTimeout <= 0 {
- return
- }
- idle := time.Now().Sub(mb.lastActivity)
- if idle >= mb.IdleTimeout {
- mb.logf("modbus: closing connection due to idle timeout: %v", idle)
- mb.close()
- }
- }
|