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 wg sync.WaitGroup } func New(Address string, Unit uint8, KeepAlive time.Duration) (*Mbclient, error) { c := new(Mbclient) c.address = Address c.unit = Unit c.t = time.NewTimer(0) <-c.t.C c.keepAliveDuration = KeepAlive 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() } func (m *Mbclient) ReadRegisters(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, 5*time.Second) 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) binary.BigEndian.PutUint16(req[4:6], 6) // Length req[6] = m.unit req[7] = 3 //FunctionCode binary.BigEndian.PutUint16(req[8:10], first-1) binary.BigEndian.PutUint16(req[10:12], numRegs) m.conn.SetDeadline(time.Now().Add(5 * time.Second)) fmt.Printf("%x", req) 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(5 * time.Second)) _, 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) 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 != 3 { 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 }