178 lines
4.5 KiB
Go
178 lines
4.5 KiB
Go
package modbustcpclient
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Mbclient struct {
|
|
transactionCounter uint16
|
|
address string
|
|
header [7]byte
|
|
unit uint8
|
|
conn net.Conn
|
|
t *time.Timer
|
|
keepAliveDuration time.Duration
|
|
timeOut time.Duration
|
|
wg sync.WaitGroup
|
|
registerOffset uint16
|
|
}
|
|
|
|
func New(Address string, Unit uint8, KeepAlive, TimeOut time.Duration, registerOffset uint16) (*Mbclient, error) {
|
|
|
|
c := new(Mbclient)
|
|
c.address = Address
|
|
c.unit = Unit
|
|
c.t = time.NewTimer(0)
|
|
<-c.t.C
|
|
c.keepAliveDuration = KeepAlive
|
|
c.timeOut = TimeOut
|
|
c.registerOffset = registerOffset
|
|
return c, nil
|
|
}
|
|
|
|
func (m *Mbclient) closer() {
|
|
|
|
<-m.t.C
|
|
m.conn.Close()
|
|
//m.transactionCounter = 0
|
|
m.wg.Done()
|
|
}
|
|
|
|
// closeConn can be called when a tcp communication failure require the current
|
|
// connection to be closed, it should only be used when closer routine is running
|
|
// and the timer is stopped.
|
|
func (m *Mbclient) closeConn() {
|
|
m.t.Reset(0)
|
|
m.wg.Wait()
|
|
}
|
|
|
|
// Reads one or many holding registers (Function code 03)
|
|
func (m *Mbclient) ReadHoldingRegisters(first uint16, numRegs uint16) ([]uint16, error) {
|
|
return m.readRegisters(3, first, numRegs)
|
|
}
|
|
|
|
// Reads one or many input registers (Function code 04)
|
|
func (m *Mbclient) ReadInputRegisters(first uint16, numRegs uint16) ([]uint16, error) {
|
|
return m.readRegisters(4, first, numRegs)
|
|
}
|
|
|
|
func (m *Mbclient) readRegisters(functionCode uint8, first uint16, numRegs uint16) ([]uint16, error) {
|
|
var err error
|
|
// If The timer is expired, conn is closed and needs to be reopened
|
|
if !m.t.Stop() {
|
|
// Wait for closer to exit to mitigate race condiion
|
|
// between closer routine and this code path
|
|
m.wg.Wait()
|
|
m.conn, err = net.DialTimeout("tcp", m.address, m.timeOut)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.wg.Add(1)
|
|
go m.closer()
|
|
}
|
|
|
|
const requestLength = 12
|
|
m.transactionCounter++
|
|
req := make([]byte, requestLength)
|
|
var responseHeader mbapHeader
|
|
var mbpayload mbPDU
|
|
|
|
binary.BigEndian.PutUint16(req[0:2], m.transactionCounter)
|
|
binary.BigEndian.PutUint16(req[2:4], 0) // Protocol identifier
|
|
binary.BigEndian.PutUint16(req[4:6], 6) // Length
|
|
req[6] = m.unit
|
|
req[7] = functionCode
|
|
binary.BigEndian.PutUint16(req[8:10], first-m.registerOffset)
|
|
binary.BigEndian.PutUint16(req[10:12], numRegs)
|
|
m.conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
|
|
byteswritten, err := m.conn.Write(req)
|
|
if err != nil {
|
|
m.closeConn()
|
|
return nil, err
|
|
}
|
|
if byteswritten != requestLength {
|
|
m.closeConn()
|
|
return nil, fmt.Errorf("failed to send request")
|
|
}
|
|
m.conn.SetDeadline(time.Now().Add(m.timeOut))
|
|
_, err = io.ReadFull(m.conn, m.header[:])
|
|
if err != nil {
|
|
m.closeConn()
|
|
return nil, err
|
|
}
|
|
responseHeader.unMarshal(m.header)
|
|
expectedDataLength := responseHeader.length - 1
|
|
|
|
if m.transactionCounter != responseHeader.transactionID {
|
|
m.closeConn()
|
|
return nil, fmt.Errorf("modbus transaction mismatch %v != %v", m.transactionCounter, responseHeader.transactionID)
|
|
}
|
|
response := make([]byte, expectedDataLength)
|
|
m.conn.SetDeadline(time.Now().Add(m.timeOut))
|
|
bytesRead, err := io.ReadFull(m.conn, response)
|
|
if err != nil {
|
|
m.closeConn()
|
|
return nil, err
|
|
}
|
|
if uint16(bytesRead) != expectedDataLength {
|
|
m.t.Reset(m.keepAliveDuration)
|
|
return nil, fmt.Errorf("failed to read complete package")
|
|
}
|
|
|
|
err = mbpayload.unMarshal(response)
|
|
if mbpayload.functionCode != functionCode {
|
|
m.t.Reset(m.keepAliveDuration)
|
|
return nil, fmt.Errorf("modbus exception %v req: %x", mbpayload.functionCode&0x7F, req)
|
|
}
|
|
if err != nil {
|
|
m.t.Reset(m.keepAliveDuration)
|
|
return nil, err
|
|
}
|
|
|
|
m.t.Reset(m.keepAliveDuration)
|
|
return mbpayload.registers, nil
|
|
}
|
|
|
|
type mbapHeader struct {
|
|
transactionID uint16
|
|
protocolIdentifier uint16
|
|
length uint16
|
|
unitIdentifier byte
|
|
}
|
|
|
|
func (m *mbapHeader) unMarshal(data [7]byte) error {
|
|
m.transactionID = binary.BigEndian.Uint16(data[0:2])
|
|
m.protocolIdentifier = binary.BigEndian.Uint16(data[2:4])
|
|
m.length = binary.BigEndian.Uint16(data[4:6])
|
|
m.unitIdentifier = data[6]
|
|
|
|
return nil
|
|
}
|
|
|
|
type mbPDU struct {
|
|
functionCode uint8
|
|
length uint8
|
|
registers []uint16
|
|
}
|
|
|
|
func (d *mbPDU) unMarshal(data []byte) error {
|
|
d.functionCode = data[0]
|
|
d.length = data[1]
|
|
if d.length+2 != uint8(len(data)) {
|
|
return fmt.Errorf("length mismatch in modbus payload")
|
|
}
|
|
d.registers = make([]uint16, d.length/2)
|
|
var n uint8
|
|
for n < d.length/2 {
|
|
d.registers[n] = binary.BigEndian.Uint16(data[n*2+2 : n*2+4])
|
|
n++
|
|
}
|
|
return nil
|
|
}
|