diff --git a/api.go b/api.go index 821c280..f9fb092 100644 --- a/api.go +++ b/api.go @@ -28,6 +28,7 @@ type Client interface { // ReadHoldingRegisters reads the contents of a contiguous block of // holding registers in a remote device and returns register value. ReadHoldingRegisters(address, quantity uint16) (results []byte, err error) + ReadCustomRegisters(command string) (results []byte, err error) // WriteSingleRegister writes a single holding register in a remote // device and returns register value. WriteSingleRegister(address, value uint16) (results []byte, err error) @@ -48,5 +49,6 @@ type Client interface { ReadFIFOQueue(address uint16) (results []byte, err error) Connect() (err error) + IsConnect() (err error) Close() (err error) } diff --git a/client.go b/client.go index 208a036..71e8824 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ package modbus import ( "encoding/binary" "fmt" + "k8s.io/klog/v2" ) // ClientHandler is the interface that groups the Packager and Transporter methods. @@ -123,6 +124,39 @@ func (mb *client) ReadHoldingRegisters(address, quantity uint16) (results []byte return } +func (mb *client) ReadCustomRegisters(command string) (results []byte, err error) { + + request := ProtocolDataUnit{ + Data: dataBlock(1, 1), + CustomCode: command, + } + aduRequest, err := mb.packager.Encode(&request) + if err != nil { + return + } + aduResponse, err := mb.transporter.Send(aduRequest) + if err != nil { + return + } + if err = mb.packager.Verify(aduRequest, aduResponse); err != nil { + return + } + response, err := mb.packager.Decode(aduResponse) + if err != nil { + return + } + if response.Data == nil || len(response.Data) == 0 { + // Empty response + err = fmt.Errorf("modbus: response data is empty") + return + } + if err != nil { + return + } + results = response.Data + return +} + // Request: // Function code : 1 byte (0x04) // Starting address : 2 bytes @@ -435,6 +469,9 @@ func (mb *client) ReadFIFOQueue(address uint16) (results []byte, err error) { func (mb *client) Connect() (err error) { return mb.transporter.Connect() } +func (mb *client) IsConnect() (err error) { + return mb.transporter.IsConnect() +} func (mb *client) Close() (err error) { return mb.transporter.Close() @@ -448,10 +485,12 @@ func (mb *client) send(request *ProtocolDataUnit) (response *ProtocolDataUnit, e if err != nil { return } + klog.V(1).Info("Send request:", aduRequest) aduResponse, err := mb.transporter.Send(aduRequest) if err != nil { return } + klog.V(1).Info("Send response:", aduResponse) if err = mb.packager.Verify(aduRequest, aduResponse); err != nil { return } diff --git a/customclient.go b/customclient.go new file mode 100644 index 0000000..8eb4fd9 --- /dev/null +++ b/customclient.go @@ -0,0 +1,318 @@ +// 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/hex" + "errors" + //"github.com/dop251/goja" + "github.com/robertkrimen/otto" + "io" + "log" + "net" + "sync" + "time" +) + +const ( + // Default TCP timeout is not set + customMaxLength = 10240 + customTimeout = 10 * time.Second + customIdleTimeout = 60 * time.Second +) + +// CustomClientHandler implements Packager and Transporter interface. +type CustomClientHandler struct { + customPackager + customTransporter +} + +// NewCustomClientHandler allocates a new CustomClientHandler. +func NewCustomClientHandler(address string) *CustomClientHandler { + h := &CustomClientHandler{} + h.Address = address + h.Timeout = tcpTimeout + h.IdleTimeout = customIdleTimeout + return h +} + +// CustomClient creates TCP client with default handler and given connect string. +func CustomClient(address string) Client { + handler := NewCustomClientHandler(address) + return NewClient(handler) +} + +// customPackager implements Packager interface. +type customPackager struct { + VerifyFunc string + DecodeFunc string + EncodeFunc string +} + +// 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 *customPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) { + // goja + //vm := goja.New() + //_, err = vm.RunString(mb.EncodeFunc) + //if err!=nil { + // panic(err) + //} + //var fn func(string) string + //err = vm.ExportTo(vm.Get("encode"), &fn) + //if err != nil { + // panic(err) + //} + // + //ss := fn(pdu.CustomCode) + //adu, _ = hex.DecodeString(ss) + + //otto + vm := otto.New() + _, err = vm.Run(mb.EncodeFunc) + if err!=nil { + panic(err) + } + value, err := vm.Call("encode", nil, pdu.CustomCode) + if err != nil { + panic(err) + } + adu, _ = hex.DecodeString(value.String()) + + return +} + +// Verify confirms transaction, protocol and unit id. +func (mb *customPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) { + aduReq := hex.EncodeToString(aduRequest) + aduRep := hex.EncodeToString(aduResponse) + //goja + //vm := goja.New() + //_, err = vm.RunString(mb.VerifyFunc) + //if err!=nil { + // panic(err) + //} + //var fn func(string, string) bool + //err = vm.ExportTo(vm.Get("verify"), &fn) + //if err != nil { + // panic(err) + //} + // + //ret := fn(aduReq, aduRep) + //if !ret { + // return errors.New("Verify error") + //} + + //otto + vm := otto.New() + _, err = vm.Run(mb.VerifyFunc) + if err!=nil { + panic(err) + } + value, err := vm.Call("verify", nil, aduReq, aduRep) + if err != nil { + panic(err) + } + if ret,_ := value.ToBoolean();!ret { + return errors.New("Verify error") + } + + return +} + +// Decode extracts PDU from TCP frame: +// Transaction identifier: 2 bytes +// Protocol identifier: 2 bytes +// Length: 2 bytes +// Unit identifier: 1 byte +func (mb *customPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) { + //goja + //vm := goja.New() + //_, err = vm.RunString(mb.DecodeFunc) + //if err!=nil { + // panic(err) + //} + //var fn func(string) string + //err = vm.ExportTo(vm.Get("decode"), &fn) + //if err != nil { + // panic(err) + //} + //s := hex.EncodeToString(adu) + //ss := fn(s) + //res, _ := hex.DecodeString(ss) + //pdu = &ProtocolDataUnit{} + //pdu.Data = res + + //otto + vm := otto.New() + _, err = vm.Run(mb.DecodeFunc) + if err!=nil { + panic(err) + } + aduStr := hex.EncodeToString(adu) + ss, err := vm.Call("decode", nil, aduStr) + if err != nil { + panic(err) + } + res, _ := hex.DecodeString(ss.String()) + pdu = &ProtocolDataUnit{} + pdu.Data = res + + return +} + +// customTransporter implements Transporter interface. +type customTransporter 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 + HeaderLength uint8 + DataLength uint8 + CrcLength uint8 +} + +// Send sends data to server and ensures response length is greater than header length. +func (mb *customTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + + // 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 [customMaxLength]byte + readLength := mb.HeaderLength+mb.DataLength+mb.CrcLength + if _, err = io.ReadFull(mb.Conn, data[:readLength]); err != nil { + return + } + aduResponse = data[:mb.HeaderLength+mb.DataLength+mb.CrcLength] + 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 *customTransporter) Connect() error { + mb.mu.Lock() + defer mb.mu.Unlock() + + return mb.connect() +} + +func (mb *customTransporter) IsConnect() error { + mb.mu.Lock() + defer mb.mu.Unlock() + + if mb.Conn == nil { + return errors.New("Bad Connection") + } + return nil +} + +func (mb *customTransporter) 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 *customTransporter) 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 *customTransporter) 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 *customTransporter) 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 *customTransporter) 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 *customTransporter) 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 *customTransporter) 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() + } +} diff --git a/modbus.go b/modbus.go index edc8290..35232a0 100644 --- a/modbus.go +++ b/modbus.go @@ -78,6 +78,7 @@ func (e *ModbusError) Error() string { type ProtocolDataUnit struct { FunctionCode byte Data []byte + CustomCode string } // Packager specifies the communication layer. @@ -91,5 +92,6 @@ type Packager interface { type Transporter interface { Send(aduRequest []byte) (aduResponse []byte, err error) Connect() (err error) + IsConnect() (err error) Close() (err error) } diff --git a/serial.go b/serial.go index 7d14c67..eaa2f23 100644 --- a/serial.go +++ b/serial.go @@ -5,6 +5,7 @@ package modbus import ( + "errors" "io" "log" "sync" @@ -40,7 +41,15 @@ func (mb *serialPort) Connect() (err error) { return mb.connect() } +func (mb *serialPort) IsConnect() (err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + if mb.port == nil { + return errors.New("Bad Connection") + } + return nil +} // connect connects to the serial port if it is not connected. Caller must hold the mutex. func (mb *serialPort) connect() error { if mb.port == nil { diff --git a/tcpclient.go b/tcpclient.go index 5287a9c..88f2cec 100644 --- a/tcpclient.go +++ b/tcpclient.go @@ -6,6 +6,7 @@ package modbus import ( "encoding/binary" + "errors" "fmt" "io" "log" @@ -142,6 +143,7 @@ type tcpTransporter struct { conn net.Conn closeTimer *time.Timer lastActivity time.Time + Conn net.Conn } // Send sends data to server and ensures response length is greater than header length. @@ -155,7 +157,7 @@ func (mb *tcpTransporter) Send(aduRequest []byte) (aduResponse []byte, err error } // Set timer to close when idle mb.lastActivity = time.Now() - mb.startCloseTimer() + //mb.startCloseTimer() // Set write and read timeout var timeout time.Time if mb.Timeout > 0 { @@ -167,9 +169,6 @@ func (mb *tcpTransporter) Send(aduRequest []byte) (aduResponse []byte, err error // Send data mb.logf("modbus: sending % x", aduRequest) if _, err = mb.conn.Write(aduRequest); err != nil { - if netError, ok := err.(net.Error); ok && netError.Timeout() == false { - mb.close() - } return } // Read header first @@ -207,6 +206,15 @@ func (mb *tcpTransporter) Connect() error { return mb.connect() } +func (mb *tcpTransporter) IsConnect() error { + mb.mu.Lock() + defer mb.mu.Unlock() + + if mb.conn == nil { + return errors.New("Bad Connection") + } + return nil +} func (mb *tcpTransporter) connect() error { if mb.conn == nil {