Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc011e3643 | |||
| f16b9dd009 | |||
| 8fbead4274 | |||
| 8c33fd2a89 | |||
| ebdbc92693 | |||
| 7b1533b3e6 | |||
| ce537e1373 | |||
| 9a92e47837 | |||
| a5f1936632 | |||
| 07ad6f4b24 | |||
| e450b2f3c8 |
112
client.go
112
client.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,23 +14,67 @@ type Mbclient struct {
|
|||||||
address string
|
address string
|
||||||
header [7]byte
|
header [7]byte
|
||||||
unit uint8
|
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) (*Mbclient, error) {
|
func New(Address string, Unit uint8, KeepAlive, TimeOut time.Duration, registerOffset uint16) (*Mbclient, error) {
|
||||||
|
|
||||||
c := new(Mbclient)
|
c := new(Mbclient)
|
||||||
c.address = address
|
c.address = Address
|
||||||
c.unit = unit
|
c.unit = Unit
|
||||||
|
c.t = time.NewTimer(0)
|
||||||
|
<-c.t.C
|
||||||
|
c.keepAliveDuration = KeepAlive
|
||||||
|
c.timeOut = TimeOut
|
||||||
|
c.registerOffset = registerOffset
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mbclient) ReadRegisters(first uint16, numRegs uint16) ([]uint16, error) {
|
func (m *Mbclient) closer() {
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", m.address)
|
<-m.t.C
|
||||||
if err != nil {
|
m.conn.Close()
|
||||||
return nil, err
|
//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()
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
const requestLength = 12
|
const requestLength = 12
|
||||||
m.transactionCounter++
|
m.transactionCounter++
|
||||||
@@ -38,36 +83,59 @@ func (m *Mbclient) ReadRegisters(first uint16, numRegs uint16) ([]uint16, error)
|
|||||||
var mbpayload mbPDU
|
var mbpayload mbPDU
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(req[0:2], m.transactionCounter)
|
binary.BigEndian.PutUint16(req[0:2], m.transactionCounter)
|
||||||
binary.BigEndian.PutUint16(req[2:4], 0)
|
binary.BigEndian.PutUint16(req[2:4], 0) // Protocol identifier
|
||||||
binary.BigEndian.PutUint16(req[4:6], 6) // Length
|
binary.BigEndian.PutUint16(req[4:6], 6) // Length
|
||||||
req[6] = m.unit
|
req[6] = m.unit
|
||||||
req[7] = 3 //FunctionCode
|
req[7] = functionCode
|
||||||
binary.BigEndian.PutUint16(req[8:10], first-1)
|
binary.BigEndian.PutUint16(req[8:10], first-m.registerOffset)
|
||||||
binary.BigEndian.PutUint16(req[10:12], numRegs)
|
binary.BigEndian.PutUint16(req[10:12], numRegs)
|
||||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
m.conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
byteswritten, err := conn.Write(req)
|
|
||||||
|
byteswritten, err := m.conn.Write(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
m.closeConn()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if byteswritten != requestLength {
|
if byteswritten != requestLength {
|
||||||
return nil, fmt.Errorf("Failed to send request")
|
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
|
||||||
}
|
}
|
||||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
_, err = io.ReadFull(conn, m.header[:])
|
|
||||||
responseHeader.unMarshal(m.header)
|
responseHeader.unMarshal(m.header)
|
||||||
expectedDataLength := responseHeader.length - 1
|
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)
|
response := make([]byte, expectedDataLength)
|
||||||
_, err = conn.Read(response)
|
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)
|
err = mbpayload.unMarshal(response)
|
||||||
if mbpayload.functionCode != 3 {
|
if mbpayload.functionCode != functionCode {
|
||||||
return nil, fmt.Errorf("modbus exception %v", mbpayload.functionCode&0x7F)
|
m.t.Reset(m.keepAliveDuration)
|
||||||
|
return nil, fmt.Errorf("modbus exception %v req: %x", mbpayload.functionCode&0x7F, req)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
m.t.Reset(m.keepAliveDuration)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.t.Reset(m.keepAliveDuration)
|
||||||
return mbpayload.registers, nil
|
return mbpayload.registers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +146,6 @@ type mbapHeader struct {
|
|||||||
unitIdentifier byte
|
unitIdentifier byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (m *mbapHeader) marshalBinary() ([]byte, error) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (m *mbapHeader) unMarshal(data [7]byte) error {
|
func (m *mbapHeader) unMarshal(data [7]byte) error {
|
||||||
m.transactionID = binary.BigEndian.Uint16(data[0:2])
|
m.transactionID = binary.BigEndian.Uint16(data[0:2])
|
||||||
m.protocolIdentifier = binary.BigEndian.Uint16(data[2:4])
|
m.protocolIdentifier = binary.BigEndian.Uint16(data[2:4])
|
||||||
@@ -101,7 +165,7 @@ func (d *mbPDU) unMarshal(data []byte) error {
|
|||||||
d.functionCode = data[0]
|
d.functionCode = data[0]
|
||||||
d.length = data[1]
|
d.length = data[1]
|
||||||
if d.length+2 != uint8(len(data)) {
|
if d.length+2 != uint8(len(data)) {
|
||||||
return fmt.Errorf("Lenght mismatch in modbus payload")
|
return fmt.Errorf("length mismatch in modbus payload")
|
||||||
}
|
}
|
||||||
d.registers = make([]uint16, d.length/2)
|
d.registers = make([]uint16, d.length/2)
|
||||||
var n uint8
|
var n uint8
|
||||||
|
|||||||
142
client_test.go
142
client_test.go
@@ -2,33 +2,161 @@ package modbustcpclient
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadOneRegister(t *testing.T) {
|
const testNibeHost = "NIBE-06543922346009.solver.nu:502"
|
||||||
c, err := New("192.168.0.154:502", 1)
|
const testIAMhost = "IAM_248000012514.solver.nu:502"
|
||||||
|
|
||||||
|
func TestNibeInputRegsters(t *testing.T) {
|
||||||
|
inputRegs := []uint16{1, 5, 7 /*8,*/, 9, 10, 11, 12, 13, 14, 16 /*26,*/ /*39,*/, 86, 1046, 1083, 1100, 1102, 1104, // S1155 specific
|
||||||
|
40 /*46, 48, 50*/ /* 396, 398,*/, 1017 /*1567,*/, 1025, 1028, 1029, 1083, 1087, 1575, 1577 /*1581,*/, 1583, 1585, 1975} // common registers
|
||||||
|
c, err := New(testNibeHost, 1, 100*time.Millisecond, 5*time.Second, 0)
|
||||||
|
t.Log("Connect")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, reg := range inputRegs {
|
||||||
|
|
||||||
|
res, err := c.ReadInputRegisters(uint16(reg), 1)
|
||||||
|
assert.NoError(t, err, "Failed to read reg %v", reg)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("reg: %v res: %v \n", reg, res)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNibeHoldingRegsters(t *testing.T) {
|
||||||
|
holdingRegs := []uint16{18, 20, 22, 26, 30, 34, 38, 39, 40, 41, 45, 43, 44, 45, 56 /*97, 159,*/, 196, 197, 237}
|
||||||
|
c, err := New(testNibeHost, 1, 100*time.Millisecond, 5*time.Second, 0)
|
||||||
|
t.Log("Connect")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, reg := range holdingRegs {
|
||||||
|
|
||||||
|
res, err := c.ReadHoldingRegisters(uint16(reg), 1)
|
||||||
|
assert.NoError(t, err, "Failed to read reg %v", reg)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("reg: %v res: %v \n", reg, res)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNibeReads32Register(t *testing.T) {
|
||||||
|
holdingRegs := []uint16{1083}
|
||||||
|
c, err := New(testNibeHost, 1, 100*time.Millisecond, 5*time.Second, 0)
|
||||||
|
t.Log("Connect")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, reg := range holdingRegs {
|
||||||
|
|
||||||
|
res, err := c.ReadInputRegisters(uint16(reg), 2)
|
||||||
|
assert.NoError(t, err, "Failed to read reg %v", reg)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("reg: %v res: %v \n", reg, res)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNibeReadu32ScaledRegister(t *testing.T) {
|
||||||
|
holdingRegs := []uint16{1583}
|
||||||
|
c, err := New(testNibeHost, 1, 100*time.Millisecond, 5*time.Second, 0)
|
||||||
|
t.Log("Connect")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, reg := range holdingRegs {
|
||||||
|
|
||||||
|
res, err := c.ReadInputRegisters(uint16(reg), 2)
|
||||||
|
assert.NoError(t, err, "Failed to read reg %v", reg)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("reg: %v res: %v \n", reg, res)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOneRegisterKeepAlive(t *testing.T) {
|
||||||
|
c, err := New(testIAMhost, 1, 100*time.Millisecond, 5*time.Second, 1)
|
||||||
|
t.Log("Connect")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
for n := 0; n < 5; n++ {
|
for n := 0; n < 5; n++ {
|
||||||
res, err := c.ReadRegisters(12401, 2)
|
res, err := c.ReadHoldingRegisters(12401, 2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, res, 2)
|
assert.Len(t, res, 2)
|
||||||
t.Log(res)
|
t.Log(res)
|
||||||
|
|
||||||
res, err = c.ReadRegisters(12102, 2)
|
res, err = c.ReadHoldingRegisters(12102, 2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, res, 2)
|
assert.Len(t, res, 2)
|
||||||
t.Log(res)
|
t.Log(res)
|
||||||
|
|
||||||
res, err = c.ReadRegisters(12544, 1)
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOneRegisterShortKeepAlive(t *testing.T) {
|
||||||
|
c, err := New(testIAMhost, 1, 10*time.Nanosecond, 5*time.Second, 1)
|
||||||
|
t.Log("Connect")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for n := 0; n < 5; n++ {
|
||||||
|
res, err := c.ReadHoldingRegisters(12401, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, res, 2)
|
||||||
|
t.Log(res)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
res, err = c.ReadHoldingRegisters(12102, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, res, 2)
|
||||||
|
t.Log(res)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
res, err = c.ReadHoldingRegisters(12544, 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, res, 1)
|
assert.Len(t, res, 1)
|
||||||
t.Log(float32(res[0]) / 10)
|
t.Log(float32(res[0]) / 10)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
res, err = c.ReadRegisters(12136, 1)
|
res, err = c.ReadHoldingRegisters(12136, 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, res, 1)
|
assert.Len(t, res, 1)
|
||||||
t.Log(res)
|
t.Log(res)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadALot(t *testing.T) {
|
||||||
|
c, err := New(testIAMhost, 1, 100*time.Millisecond, 5*time.Second, 1)
|
||||||
|
t.Log("Connect")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for n := 0; n < 500; n++ {
|
||||||
|
t.Log(n)
|
||||||
|
_, err := c.ReadHoldingRegisters(12401, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
_, err = c.ReadHoldingRegisters(12102, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.ReadHoldingRegisters(12544, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
_, err = c.ReadHoldingRegisters(12136, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user