├── .travis.yml ├── LICENSE ├── README.md ├── api.go ├── asciiclient.go ├── asciiclient_test.go ├── client.go ├── crc.go ├── crc_test.go ├── lrc.go ├── lrc_test.go ├── modbus.go ├── rtuclient.go ├── rtuclient_test.go ├── serial.go ├── serial_test.go ├── tcpclient.go ├── tcpclient_test.go └── test ├── README.md ├── asciiclient_test.go ├── client.go ├── common.go ├── commw32 ├── commw32.c └── commw32.go ├── rtuclient_test.go └── tcpclient_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | - 1.3 6 | - 1.4 7 | - tip 8 | 9 | script: 10 | - go test -v -bench . -benchmem 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Quoc-Viet Nguyen 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. Neither the names of the copyright holders nor the names of any 13 | contributors may be used to endorse or promote products derived from this 14 | software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go modbus [![Build Status](https://travis-ci.org/goburrow/modbus.svg?branch=master)](https://travis-ci.org/goburrow/modbus) [![GoDoc](https://godoc.org/github.com/goburrow/modbus?status.svg)](https://godoc.org/github.com/goburrow/modbus) 2 | ========= 3 | Fault-tolerant, fail-fast implementation of Modbus protocol in Go. 4 | 5 | Supported functions 6 | ------------------- 7 | Bit access: 8 | * Read Discrete Inputs 9 | * Read Coils 10 | * Write Single Coil 11 | * Write Multiple Coils 12 | 13 | 16-bit access: 14 | * Read Input Registers 15 | * Read Holding Registers 16 | * Write Single Register 17 | * Write Multiple Registers 18 | * Read/Write Multiple Registers 19 | * Mask Write Register 20 | * Read FIFO Queue 21 | 22 | Supported formats 23 | ----------------- 24 | * TCP 25 | * Serial (RTU, ASCII) 26 | 27 | Usage 28 | ----- 29 | Basic usage: 30 | ```go 31 | // Modbus TCP 32 | client := modbus.TCPClient("localhost:502") 33 | // Read input register 9 34 | results, err := client.ReadInputRegisters(8, 1) 35 | 36 | // Modbus RTU/ASCII 37 | // Default configuration is 19200, 8, 1, even 38 | client = modbus.RTUClient("/dev/ttyS0") 39 | results, err = client.ReadCoils(2, 1) 40 | ``` 41 | 42 | Advanced usage: 43 | ```go 44 | // Modbus TCP 45 | handler := modbus.NewTCPClientHandler("localhost:502") 46 | handler.Timeout = 10 * time.Second 47 | handler.SlaveId = 0xFF 48 | handler.Logger = log.New(os.Stdout, "test: ", log.LstdFlags) 49 | // Connect manually so that multiple requests are handled in one connection session 50 | err := handler.Connect() 51 | defer handler.Close() 52 | 53 | client := modbus.NewClient(handler) 54 | results, err := client.ReadDiscreteInputs(15, 2) 55 | results, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4}) 56 | results, err = client.WriteMultipleCoils(5, 10, []byte{4, 3}) 57 | ``` 58 | 59 | ```go 60 | // Modbus RTU/ASCII 61 | handler := modbus.NewRTUClientHandler("/dev/ttyUSB0") 62 | handler.BaudRate = 115200 63 | handler.DataBits = 8 64 | handler.Parity = "N" 65 | handler.StopBits = 1 66 | handler.SlaveId = 1 67 | handler.Timeout = 5 * time.Second 68 | 69 | err := handler.Connect() 70 | defer handler.Close() 71 | 72 | client := modbus.NewClient(handler) 73 | results, err := client.ReadDiscreteInputs(15, 2) 74 | ``` 75 | 76 | References 77 | ---------- 78 | - [Modbus Specifications and Implementation Guides](http://www.modbus.org/specs.php) 79 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | type Client interface { 8 | // Bit access 9 | 10 | // ReadCoils reads from 1 to 2000 contiguous status of coils in a 11 | // remote device and returns coil status. 12 | ReadCoils(address, quantity uint16) (results []byte, err error) 13 | // ReadDiscreteInputs reads from 1 to 2000 contiguous status of 14 | // discrete inputs in a remote device and returns input status. 15 | ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) 16 | // WriteSingleCoil write a single output to either ON or OFF in a 17 | // remote device and returns output value. 18 | WriteSingleCoil(address, value uint16) (results []byte, err error) 19 | // WriteMultipleCoils forces each coil in a sequence of coils to either 20 | // ON or OFF in a remote device and returns quantity of outputs. 21 | WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) 22 | 23 | // 16-bit access 24 | 25 | // ReadInputRegisters reads from 1 to 125 contiguous input registers in 26 | // a remote device and returns input registers. 27 | ReadInputRegisters(address, quantity uint16) (results []byte, err error) 28 | // ReadHoldingRegisters reads the contents of a contiguous block of 29 | // holding registers in a remote device and returns register value. 30 | ReadHoldingRegisters(address, quantity uint16) (results []byte, err error) 31 | // WriteSingleRegister writes a single holding register in a remote 32 | // device and returns register value. 33 | WriteSingleRegister(address, value uint16) (results []byte, err error) 34 | // WriteMultipleRegisters writes a block of contiguous registers 35 | // (1 to 123 registers) in a remote device and returns quantity of 36 | // registers. 37 | WriteMultipleRegisters(address, quantity uint16, value []byte) (results []byte, err error) 38 | // ReadWriteMultipleRegisters performs a combination of one read 39 | // operation and one write operation. It returns read registers value. 40 | ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) 41 | // MaskWriteRegister modify the contents of a specified holding 42 | // register using a combination of an AND mask, an OR mask, and the 43 | // register's current contents. The function returns 44 | // AND-mask and OR-mask. 45 | MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) 46 | //ReadFIFOQueue reads the contents of a First-In-First-Out (FIFO) queue 47 | // of register in a remote device and returns FIFO value register. 48 | ReadFIFOQueue(address uint16) (results []byte, err error) 49 | } 50 | -------------------------------------------------------------------------------- /asciiclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "bytes" 9 | "encoding/hex" 10 | "fmt" 11 | "time" 12 | ) 13 | 14 | const ( 15 | asciiStart = ":" 16 | asciiEnd = "\r\n" 17 | asciiMinSize = 3 18 | asciiMaxSize = 513 19 | 20 | hexTable = "0123456789ABCDEF" 21 | ) 22 | 23 | // ASCIIClientHandler implements Packager and Transporter interface. 24 | type ASCIIClientHandler struct { 25 | asciiPackager 26 | asciiSerialTransporter 27 | } 28 | 29 | // NewASCIIClientHandler allocates and initializes a ASCIIClientHandler. 30 | func NewASCIIClientHandler(address string) *ASCIIClientHandler { 31 | handler := &ASCIIClientHandler{} 32 | handler.Address = address 33 | handler.Timeout = serialTimeout 34 | handler.IdleTimeout = serialIdleTimeout 35 | return handler 36 | } 37 | 38 | // ASCIIClient creates ASCII client with default handler and given connect string. 39 | func ASCIIClient(address string) Client { 40 | handler := NewASCIIClientHandler(address) 41 | return NewClient(handler) 42 | } 43 | 44 | // asciiPackager implements Packager interface. 45 | type asciiPackager struct { 46 | SlaveId byte 47 | } 48 | 49 | // Encode encodes PDU in a ASCII frame: 50 | // Start : 1 char 51 | // Address : 2 chars 52 | // Function : 2 chars 53 | // Data : 0 up to 2x252 chars 54 | // LRC : 2 chars 55 | // End : 2 chars 56 | func (mb *asciiPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) { 57 | var buf bytes.Buffer 58 | 59 | if _, err = buf.WriteString(asciiStart); err != nil { 60 | return 61 | } 62 | if err = writeHex(&buf, []byte{mb.SlaveId, pdu.FunctionCode}); err != nil { 63 | return 64 | } 65 | if err = writeHex(&buf, pdu.Data); err != nil { 66 | return 67 | } 68 | // Exclude the beginning colon and terminating CRLF pair characters 69 | var lrc lrc 70 | lrc.reset() 71 | lrc.pushByte(mb.SlaveId).pushByte(pdu.FunctionCode).pushBytes(pdu.Data) 72 | if err = writeHex(&buf, []byte{lrc.value()}); err != nil { 73 | return 74 | } 75 | if _, err = buf.WriteString(asciiEnd); err != nil { 76 | return 77 | } 78 | adu = buf.Bytes() 79 | return 80 | } 81 | 82 | // Verify verifies response length, frame boundary and slave id. 83 | func (mb *asciiPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) { 84 | length := len(aduResponse) 85 | // Minimum size (including address, function and LRC) 86 | if length < asciiMinSize+6 { 87 | err = fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", length, 9) 88 | return 89 | } 90 | // Length excluding colon must be an even number 91 | if length%2 != 1 { 92 | err = fmt.Errorf("modbus: response length '%v' is not an even number", length-1) 93 | return 94 | } 95 | // First char must be a colon 96 | str := string(aduResponse[0:len(asciiStart)]) 97 | if str != asciiStart { 98 | err = fmt.Errorf("modbus: response frame '%v'... is not started with '%v'", str, asciiStart) 99 | return 100 | } 101 | // 2 last chars must be \r\n 102 | str = string(aduResponse[len(aduResponse)-len(asciiEnd):]) 103 | if str != asciiEnd { 104 | err = fmt.Errorf("modbus: response frame ...'%v' is not ended with '%v'", str, asciiEnd) 105 | return 106 | } 107 | // Slave id 108 | responseVal, err := readHex(aduResponse[1:]) 109 | if err != nil { 110 | return 111 | } 112 | requestVal, err := readHex(aduRequest[1:]) 113 | if err != nil { 114 | return 115 | } 116 | if responseVal != requestVal { 117 | err = fmt.Errorf("modbus: response slave id '%v' does not match request '%v'", responseVal, requestVal) 118 | return 119 | } 120 | return 121 | } 122 | 123 | // Decode extracts PDU from ASCII frame and verify LRC. 124 | func (mb *asciiPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) { 125 | pdu = &ProtocolDataUnit{} 126 | // Slave address 127 | address, err := readHex(adu[1:]) 128 | if err != nil { 129 | return 130 | } 131 | // Function code 132 | if pdu.FunctionCode, err = readHex(adu[3:]); err != nil { 133 | return 134 | } 135 | // Data 136 | dataEnd := len(adu) - 4 137 | data := adu[5:dataEnd] 138 | pdu.Data = make([]byte, hex.DecodedLen(len(data))) 139 | if _, err = hex.Decode(pdu.Data, data); err != nil { 140 | return 141 | } 142 | // LRC 143 | lrcVal, err := readHex(adu[dataEnd:]) 144 | if err != nil { 145 | return 146 | } 147 | // Calculate checksum 148 | var lrc lrc 149 | lrc.reset() 150 | lrc.pushByte(address).pushByte(pdu.FunctionCode).pushBytes(pdu.Data) 151 | if lrcVal != lrc.value() { 152 | err = fmt.Errorf("modbus: response lrc '%v' does not match expected '%v'", lrcVal, lrc.value()) 153 | return 154 | } 155 | return 156 | } 157 | 158 | // asciiSerialTransporter implements Transporter interface. 159 | type asciiSerialTransporter struct { 160 | serialPort 161 | } 162 | 163 | func (mb *asciiSerialTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) { 164 | mb.serialPort.mu.Lock() 165 | defer mb.serialPort.mu.Unlock() 166 | 167 | // Make sure port is connected 168 | if err = mb.serialPort.connect(); err != nil { 169 | return 170 | } 171 | // Start the timer to close when idle 172 | mb.serialPort.lastActivity = time.Now() 173 | mb.serialPort.startCloseTimer() 174 | 175 | // Send the request 176 | mb.serialPort.logf("modbus: sending %q\n", aduRequest) 177 | if _, err = mb.port.Write(aduRequest); err != nil { 178 | return 179 | } 180 | // Get the response 181 | var n int 182 | var data [asciiMaxSize]byte 183 | length := 0 184 | for { 185 | if n, err = mb.port.Read(data[length:]); err != nil { 186 | return 187 | } 188 | length += n 189 | if length >= asciiMaxSize || n == 0 { 190 | break 191 | } 192 | // Expect end of frame in the data received 193 | if length > asciiMinSize { 194 | if string(data[length-len(asciiEnd):length]) == asciiEnd { 195 | break 196 | } 197 | } 198 | } 199 | aduResponse = data[:length] 200 | mb.serialPort.logf("modbus: received %q\n", aduResponse) 201 | return 202 | } 203 | 204 | // writeHex encodes byte to string in hexadecimal, e.g. 0xA5 => "A5" 205 | // (encoding/hex only supports lowercase string). 206 | func writeHex(buf *bytes.Buffer, value []byte) (err error) { 207 | var str [2]byte 208 | for _, v := range value { 209 | str[0] = hexTable[v>>4] 210 | str[1] = hexTable[v&0x0F] 211 | 212 | if _, err = buf.Write(str[:]); err != nil { 213 | return 214 | } 215 | } 216 | return 217 | } 218 | 219 | // readHex decodes hexa string to byte, e.g. "8C" => 0x8C. 220 | func readHex(data []byte) (value byte, err error) { 221 | var dst [1]byte 222 | if _, err = hex.Decode(dst[:], data[0:2]); err != nil { 223 | return 224 | } 225 | value = dst[0] 226 | return 227 | } 228 | -------------------------------------------------------------------------------- /asciiclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func TestASCIIEncoding(t *testing.T) { 13 | encoder := asciiPackager{} 14 | encoder.SlaveId = 17 15 | 16 | pdu := ProtocolDataUnit{} 17 | pdu.FunctionCode = 3 18 | pdu.Data = []byte{0, 107, 0, 3} 19 | 20 | adu, err := encoder.Encode(&pdu) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | expected := []byte(":1103006B00037E\r\n") 25 | if !bytes.Equal(expected, adu) { 26 | t.Fatalf("adu actual: %v, expected %v", adu, expected) 27 | } 28 | } 29 | 30 | func TestASCIIDecoding(t *testing.T) { 31 | decoder := asciiPackager{} 32 | decoder.SlaveId = 247 33 | adu := []byte(":F7031389000A60\r\n") 34 | 35 | pdu, err := decoder.Decode(adu) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if 3 != pdu.FunctionCode { 41 | t.Fatalf("Function code: expected %v, actual %v", 15, pdu.FunctionCode) 42 | } 43 | expected := []byte{0x13, 0x89, 0, 0x0A} 44 | if !bytes.Equal(expected, pdu.Data) { 45 | t.Fatalf("Data: expected %v, actual %v", expected, pdu.Data) 46 | } 47 | } 48 | 49 | func BenchmarkASCIIEncoder(b *testing.B) { 50 | encoder := asciiPackager{ 51 | SlaveId: 10, 52 | } 53 | pdu := ProtocolDataUnit{ 54 | FunctionCode: 1, 55 | Data: []byte{2, 3, 4, 5, 6, 7, 8, 9}, 56 | } 57 | for i := 0; i < b.N; i++ { 58 | _, err := encoder.Encode(&pdu) 59 | if err != nil { 60 | b.Fatal(err) 61 | } 62 | } 63 | } 64 | 65 | func BenchmarkASCIIDecoder(b *testing.B) { 66 | decoder := asciiPackager{ 67 | SlaveId: 10, 68 | } 69 | adu := []byte(":F7031389000A60\r\n") 70 | for i := 0; i < b.N; i++ { 71 | _, err := decoder.Decode(adu) 72 | if err != nil { 73 | b.Fatal(err) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "encoding/binary" 9 | "fmt" 10 | ) 11 | 12 | // ClientHandler is the interface that groups the Packager and Transporter methods. 13 | type ClientHandler interface { 14 | Packager 15 | Transporter 16 | } 17 | 18 | type client struct { 19 | packager Packager 20 | transporter Transporter 21 | } 22 | 23 | // NewClient creates a new modbus client with given backend handler. 24 | func NewClient(handler ClientHandler) Client { 25 | return &client{packager: handler, transporter: handler} 26 | } 27 | 28 | // NewClient2 creates a new modbus client with given backend packager and transporter. 29 | func NewClient2(packager Packager, transporter Transporter) Client { 30 | return &client{packager: packager, transporter: transporter} 31 | } 32 | 33 | // Request: 34 | // Function code : 1 byte (0x01) 35 | // Starting address : 2 bytes 36 | // Quantity of coils : 2 bytes 37 | // Response: 38 | // Function code : 1 byte (0x01) 39 | // Byte count : 1 byte 40 | // Coil status : N* bytes (=N or N+1) 41 | func (mb *client) ReadCoils(address, quantity uint16) (results []byte, err error) { 42 | if quantity < 1 || quantity > 2000 { 43 | err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 2000) 44 | return 45 | } 46 | request := ProtocolDataUnit{ 47 | FunctionCode: FuncCodeReadCoils, 48 | Data: dataBlock(address, quantity), 49 | } 50 | response, err := mb.send(&request) 51 | if err != nil { 52 | return 53 | } 54 | count := int(response.Data[0]) 55 | length := len(response.Data) - 1 56 | if count != length { 57 | err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count) 58 | return 59 | } 60 | results = response.Data[1:] 61 | return 62 | } 63 | 64 | // Request: 65 | // Function code : 1 byte (0x02) 66 | // Starting address : 2 bytes 67 | // Quantity of inputs : 2 bytes 68 | // Response: 69 | // Function code : 1 byte (0x02) 70 | // Byte count : 1 byte 71 | // Input status : N* bytes (=N or N+1) 72 | func (mb *client) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) { 73 | if quantity < 1 || quantity > 2000 { 74 | err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 2000) 75 | return 76 | } 77 | request := ProtocolDataUnit{ 78 | FunctionCode: FuncCodeReadDiscreteInputs, 79 | Data: dataBlock(address, quantity), 80 | } 81 | response, err := mb.send(&request) 82 | if err != nil { 83 | return 84 | } 85 | count := int(response.Data[0]) 86 | length := len(response.Data) - 1 87 | if count != length { 88 | err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count) 89 | return 90 | } 91 | results = response.Data[1:] 92 | return 93 | } 94 | 95 | // Request: 96 | // Function code : 1 byte (0x03) 97 | // Starting address : 2 bytes 98 | // Quantity of registers : 2 bytes 99 | // Response: 100 | // Function code : 1 byte (0x03) 101 | // Byte count : 1 byte 102 | // Register value : Nx2 bytes 103 | func (mb *client) ReadHoldingRegisters(address, quantity uint16) (results []byte, err error) { 104 | if quantity < 1 || quantity > 125 { 105 | err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 125) 106 | return 107 | } 108 | request := ProtocolDataUnit{ 109 | FunctionCode: FuncCodeReadHoldingRegisters, 110 | Data: dataBlock(address, quantity), 111 | } 112 | response, err := mb.send(&request) 113 | if err != nil { 114 | return 115 | } 116 | count := int(response.Data[0]) 117 | length := len(response.Data) - 1 118 | if count != length { 119 | err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count) 120 | return 121 | } 122 | results = response.Data[1:] 123 | return 124 | } 125 | 126 | // Request: 127 | // Function code : 1 byte (0x04) 128 | // Starting address : 2 bytes 129 | // Quantity of registers : 2 bytes 130 | // Response: 131 | // Function code : 1 byte (0x04) 132 | // Byte count : 1 byte 133 | // Input registers : N bytes 134 | func (mb *client) ReadInputRegisters(address, quantity uint16) (results []byte, err error) { 135 | if quantity < 1 || quantity > 125 { 136 | err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 125) 137 | return 138 | } 139 | request := ProtocolDataUnit{ 140 | FunctionCode: FuncCodeReadInputRegisters, 141 | Data: dataBlock(address, quantity), 142 | } 143 | response, err := mb.send(&request) 144 | if err != nil { 145 | return 146 | } 147 | count := int(response.Data[0]) 148 | length := len(response.Data) - 1 149 | if count != length { 150 | err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", length, count) 151 | return 152 | } 153 | results = response.Data[1:] 154 | return 155 | } 156 | 157 | // Request: 158 | // Function code : 1 byte (0x05) 159 | // Output address : 2 bytes 160 | // Output value : 2 bytes 161 | // Response: 162 | // Function code : 1 byte (0x05) 163 | // Output address : 2 bytes 164 | // Output value : 2 bytes 165 | func (mb *client) WriteSingleCoil(address, value uint16) (results []byte, err error) { 166 | // The requested ON/OFF state can only be 0xFF00 and 0x0000 167 | if value != 0xFF00 && value != 0x0000 { 168 | err = fmt.Errorf("modbus: state '%v' must be either 0xFF00 (ON) or 0x0000 (OFF)", value) 169 | return 170 | } 171 | request := ProtocolDataUnit{ 172 | FunctionCode: FuncCodeWriteSingleCoil, 173 | Data: dataBlock(address, value), 174 | } 175 | response, err := mb.send(&request) 176 | if err != nil { 177 | return 178 | } 179 | // Fixed response length 180 | if len(response.Data) != 4 { 181 | err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4) 182 | return 183 | } 184 | respValue := binary.BigEndian.Uint16(response.Data) 185 | if address != respValue { 186 | err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address) 187 | return 188 | } 189 | results = response.Data[2:] 190 | respValue = binary.BigEndian.Uint16(results) 191 | if value != respValue { 192 | err = fmt.Errorf("modbus: response value '%v' does not match request '%v'", respValue, value) 193 | return 194 | } 195 | return 196 | } 197 | 198 | // Request: 199 | // Function code : 1 byte (0x06) 200 | // Register address : 2 bytes 201 | // Register value : 2 bytes 202 | // Response: 203 | // Function code : 1 byte (0x06) 204 | // Register address : 2 bytes 205 | // Register value : 2 bytes 206 | func (mb *client) WriteSingleRegister(address, value uint16) (results []byte, err error) { 207 | request := ProtocolDataUnit{ 208 | FunctionCode: FuncCodeWriteSingleRegister, 209 | Data: dataBlock(address, value), 210 | } 211 | response, err := mb.send(&request) 212 | if err != nil { 213 | return 214 | } 215 | // Fixed response length 216 | if len(response.Data) != 4 { 217 | err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4) 218 | return 219 | } 220 | respValue := binary.BigEndian.Uint16(response.Data) 221 | if address != respValue { 222 | err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address) 223 | return 224 | } 225 | results = response.Data[2:] 226 | respValue = binary.BigEndian.Uint16(results) 227 | if value != respValue { 228 | err = fmt.Errorf("modbus: response value '%v' does not match request '%v'", respValue, value) 229 | return 230 | } 231 | return 232 | } 233 | 234 | // Request: 235 | // Function code : 1 byte (0x0F) 236 | // Starting address : 2 bytes 237 | // Quantity of outputs : 2 bytes 238 | // Byte count : 1 byte 239 | // Outputs value : N* bytes 240 | // Response: 241 | // Function code : 1 byte (0x0F) 242 | // Starting address : 2 bytes 243 | // Quantity of outputs : 2 bytes 244 | func (mb *client) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) { 245 | if quantity < 1 || quantity > 1968 { 246 | err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 1968) 247 | return 248 | } 249 | request := ProtocolDataUnit{ 250 | FunctionCode: FuncCodeWriteMultipleCoils, 251 | Data: dataBlockSuffix(value, address, quantity), 252 | } 253 | response, err := mb.send(&request) 254 | if err != nil { 255 | return 256 | } 257 | // Fixed response length 258 | if len(response.Data) != 4 { 259 | err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4) 260 | return 261 | } 262 | respValue := binary.BigEndian.Uint16(response.Data) 263 | if address != respValue { 264 | err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address) 265 | return 266 | } 267 | results = response.Data[2:] 268 | respValue = binary.BigEndian.Uint16(results) 269 | if quantity != respValue { 270 | err = fmt.Errorf("modbus: response quantity '%v' does not match request '%v'", respValue, quantity) 271 | return 272 | } 273 | return 274 | } 275 | 276 | // Request: 277 | // Function code : 1 byte (0x10) 278 | // Starting address : 2 bytes 279 | // Quantity of outputs : 2 bytes 280 | // Byte count : 1 byte 281 | // Registers value : N* bytes 282 | // Response: 283 | // Function code : 1 byte (0x10) 284 | // Starting address : 2 bytes 285 | // Quantity of registers : 2 bytes 286 | func (mb *client) WriteMultipleRegisters(address, quantity uint16, value []byte) (results []byte, err error) { 287 | if quantity < 1 || quantity > 123 { 288 | err = fmt.Errorf("modbus: quantity '%v' must be between '%v' and '%v',", quantity, 1, 123) 289 | return 290 | } 291 | request := ProtocolDataUnit{ 292 | FunctionCode: FuncCodeWriteMultipleRegisters, 293 | Data: dataBlockSuffix(value, address, quantity), 294 | } 295 | response, err := mb.send(&request) 296 | if err != nil { 297 | return 298 | } 299 | // Fixed response length 300 | if len(response.Data) != 4 { 301 | err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 4) 302 | return 303 | } 304 | respValue := binary.BigEndian.Uint16(response.Data) 305 | if address != respValue { 306 | err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address) 307 | return 308 | } 309 | results = response.Data[2:] 310 | respValue = binary.BigEndian.Uint16(results) 311 | if quantity != respValue { 312 | err = fmt.Errorf("modbus: response quantity '%v' does not match request '%v'", respValue, quantity) 313 | return 314 | } 315 | return 316 | } 317 | 318 | // Request: 319 | // Function code : 1 byte (0x16) 320 | // Reference address : 2 bytes 321 | // AND-mask : 2 bytes 322 | // OR-mask : 2 bytes 323 | // Response: 324 | // Function code : 1 byte (0x16) 325 | // Reference address : 2 bytes 326 | // AND-mask : 2 bytes 327 | // OR-mask : 2 bytes 328 | func (mb *client) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) { 329 | request := ProtocolDataUnit{ 330 | FunctionCode: FuncCodeMaskWriteRegister, 331 | Data: dataBlock(address, andMask, orMask), 332 | } 333 | response, err := mb.send(&request) 334 | if err != nil { 335 | return 336 | } 337 | // Fixed response length 338 | if len(response.Data) != 6 { 339 | err = fmt.Errorf("modbus: response data size '%v' does not match expected '%v'", len(response.Data), 6) 340 | return 341 | } 342 | respValue := binary.BigEndian.Uint16(response.Data) 343 | if address != respValue { 344 | err = fmt.Errorf("modbus: response address '%v' does not match request '%v'", respValue, address) 345 | return 346 | } 347 | respValue = binary.BigEndian.Uint16(response.Data[2:]) 348 | if andMask != respValue { 349 | err = fmt.Errorf("modbus: response AND-mask '%v' does not match request '%v'", respValue, andMask) 350 | return 351 | } 352 | respValue = binary.BigEndian.Uint16(response.Data[4:]) 353 | if orMask != respValue { 354 | err = fmt.Errorf("modbus: response OR-mask '%v' does not match request '%v'", respValue, orMask) 355 | return 356 | } 357 | results = response.Data[2:] 358 | return 359 | } 360 | 361 | // Request: 362 | // Function code : 1 byte (0x17) 363 | // Read starting address : 2 bytes 364 | // Quantity to read : 2 bytes 365 | // Write starting address: 2 bytes 366 | // Quantity to write : 2 bytes 367 | // Write byte count : 1 byte 368 | // Write registers value : N* bytes 369 | // Response: 370 | // Function code : 1 byte (0x17) 371 | // Byte count : 1 byte 372 | // Read registers value : Nx2 bytes 373 | func (mb *client) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) { 374 | if readQuantity < 1 || readQuantity > 125 { 375 | err = fmt.Errorf("modbus: quantity to read '%v' must be between '%v' and '%v',", readQuantity, 1, 125) 376 | return 377 | } 378 | if writeQuantity < 1 || writeQuantity > 121 { 379 | err = fmt.Errorf("modbus: quantity to write '%v' must be between '%v' and '%v',", writeQuantity, 1, 121) 380 | return 381 | } 382 | request := ProtocolDataUnit{ 383 | FunctionCode: FuncCodeReadWriteMultipleRegisters, 384 | Data: dataBlockSuffix(value, readAddress, readQuantity, writeAddress, writeQuantity), 385 | } 386 | response, err := mb.send(&request) 387 | if err != nil { 388 | return 389 | } 390 | count := int(response.Data[0]) 391 | if count != (len(response.Data) - 1) { 392 | err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", len(response.Data)-1, count) 393 | return 394 | } 395 | results = response.Data[1:] 396 | return 397 | } 398 | 399 | // Request: 400 | // Function code : 1 byte (0x18) 401 | // FIFO pointer address : 2 bytes 402 | // Response: 403 | // Function code : 1 byte (0x18) 404 | // Byte count : 2 bytes 405 | // FIFO count : 2 bytes 406 | // FIFO count : 2 bytes (<=31) 407 | // FIFO value register : Nx2 bytes 408 | func (mb *client) ReadFIFOQueue(address uint16) (results []byte, err error) { 409 | request := ProtocolDataUnit{ 410 | FunctionCode: FuncCodeReadFIFOQueue, 411 | Data: dataBlock(address), 412 | } 413 | response, err := mb.send(&request) 414 | if err != nil { 415 | return 416 | } 417 | if len(response.Data) < 4 { 418 | err = fmt.Errorf("modbus: response data size '%v' is less than expected '%v'", len(response.Data), 4) 419 | return 420 | } 421 | count := int(binary.BigEndian.Uint16(response.Data)) 422 | if count != (len(response.Data) - 1) { 423 | err = fmt.Errorf("modbus: response data size '%v' does not match count '%v'", len(response.Data)-1, count) 424 | return 425 | } 426 | count = int(binary.BigEndian.Uint16(response.Data[2:])) 427 | if count > 31 { 428 | err = fmt.Errorf("modbus: fifo count '%v' is greater than expected '%v'", count, 31) 429 | return 430 | } 431 | results = response.Data[4:] 432 | return 433 | } 434 | 435 | // Helpers 436 | 437 | // send sends request and checks possible exception in the response. 438 | func (mb *client) send(request *ProtocolDataUnit) (response *ProtocolDataUnit, err error) { 439 | aduRequest, err := mb.packager.Encode(request) 440 | if err != nil { 441 | return 442 | } 443 | aduResponse, err := mb.transporter.Send(aduRequest) 444 | if err != nil { 445 | return 446 | } 447 | if err = mb.packager.Verify(aduRequest, aduResponse); err != nil { 448 | return 449 | } 450 | response, err = mb.packager.Decode(aduResponse) 451 | if err != nil { 452 | return 453 | } 454 | // Check correct function code returned (exception) 455 | if response.FunctionCode != request.FunctionCode { 456 | err = responseError(response) 457 | return 458 | } 459 | if response.Data == nil || len(response.Data) == 0 { 460 | // Empty response 461 | err = fmt.Errorf("modbus: response data is empty") 462 | return 463 | } 464 | return 465 | } 466 | 467 | // dataBlock creates a sequence of uint16 data. 468 | func dataBlock(value ...uint16) []byte { 469 | data := make([]byte, 2*len(value)) 470 | for i, v := range value { 471 | binary.BigEndian.PutUint16(data[i*2:], v) 472 | } 473 | return data 474 | } 475 | 476 | // dataBlockSuffix creates a sequence of uint16 data and append the suffix plus its length. 477 | func dataBlockSuffix(suffix []byte, value ...uint16) []byte { 478 | length := 2 * len(value) 479 | data := make([]byte, length+1+len(suffix)) 480 | for i, v := range value { 481 | binary.BigEndian.PutUint16(data[i*2:], v) 482 | } 483 | data[length] = uint8(len(suffix)) 484 | copy(data[length+1:], suffix) 485 | return data 486 | } 487 | 488 | func responseError(response *ProtocolDataUnit) error { 489 | mbError := &ModbusError{FunctionCode: response.FunctionCode} 490 | if response.Data != nil && len(response.Data) > 0 { 491 | mbError.ExceptionCode = response.Data[0] 492 | } 493 | return mbError 494 | } 495 | -------------------------------------------------------------------------------- /crc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | // Table of CRC values for high–order byte 8 | var crcHighBytes = []byte{ 9 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 10 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 11 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 12 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 13 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 14 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 15 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 16 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 17 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 18 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 19 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 20 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 21 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 22 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 23 | 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 24 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 25 | } 26 | 27 | // Table of CRC values for low-order byte 28 | var crcLowBytes = []byte{ 29 | 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 30 | 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 31 | 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 32 | 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 33 | 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 34 | 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 35 | 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 36 | 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 37 | 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 38 | 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 39 | 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 40 | 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 41 | 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 42 | 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 43 | 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 44 | 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40, 45 | } 46 | 47 | // Cyclical Redundancy Checking 48 | type crc struct { 49 | high byte 50 | low byte 51 | } 52 | 53 | func (crc *crc) reset() *crc { 54 | crc.high = 0xFF 55 | crc.low = 0xFF 56 | return crc 57 | } 58 | 59 | func (crc *crc) pushBytes(bs []byte) *crc { 60 | var idx, b byte 61 | 62 | for _, b = range bs { 63 | idx = crc.low ^ b 64 | crc.low = crc.high ^ crcHighBytes[idx] 65 | crc.high = crcLowBytes[idx] 66 | } 67 | return crc 68 | } 69 | 70 | func (crc *crc) value() uint16 { 71 | return uint16(crc.high)<<8 | uint16(crc.low) 72 | } 73 | -------------------------------------------------------------------------------- /crc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestCRC(t *testing.T) { 12 | var crc crc 13 | crc.reset() 14 | crc.pushBytes([]byte{0x02, 0x07}) 15 | 16 | if 0x1241 != crc.value() { 17 | t.Fatalf("crc expected %v, actual %v", 0x1241, crc.value()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lrc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | // Longitudinal Redundancy Checking 8 | type lrc struct { 9 | sum uint8 10 | } 11 | 12 | func (lrc *lrc) reset() *lrc { 13 | lrc.sum = 0 14 | return lrc 15 | } 16 | 17 | func (lrc *lrc) pushByte(b byte) *lrc { 18 | lrc.sum += b 19 | return lrc 20 | } 21 | 22 | func (lrc *lrc) pushBytes(data []byte) *lrc { 23 | var b byte 24 | for _, b = range data { 25 | lrc.sum += b 26 | } 27 | return lrc 28 | } 29 | 30 | func (lrc *lrc) value() byte { 31 | // Return twos complement 32 | return uint8(-int8(lrc.sum)) 33 | } 34 | -------------------------------------------------------------------------------- /lrc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestLRC(t *testing.T) { 12 | var lrc lrc 13 | lrc.reset().pushByte(0x01).pushByte(0x03) 14 | lrc.pushBytes([]byte{0x01, 0x0A}) 15 | 16 | if 0xF1 != lrc.value() { 17 | t.Fatalf("lrc expected %v, actual %v", 0xF1, lrc.value()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modbus.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | /* 6 | Package modbus provides a client for MODBUS TCP and RTU/ASCII. 7 | */ 8 | package modbus 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | const ( 15 | // Bit access 16 | FuncCodeReadDiscreteInputs = 2 17 | FuncCodeReadCoils = 1 18 | FuncCodeWriteSingleCoil = 5 19 | FuncCodeWriteMultipleCoils = 15 20 | 21 | // 16-bit access 22 | FuncCodeReadInputRegisters = 4 23 | FuncCodeReadHoldingRegisters = 3 24 | FuncCodeWriteSingleRegister = 6 25 | FuncCodeWriteMultipleRegisters = 16 26 | FuncCodeReadWriteMultipleRegisters = 23 27 | FuncCodeMaskWriteRegister = 22 28 | FuncCodeReadFIFOQueue = 24 29 | ) 30 | 31 | const ( 32 | ExceptionCodeIllegalFunction = 1 33 | ExceptionCodeIllegalDataAddress = 2 34 | ExceptionCodeIllegalDataValue = 3 35 | ExceptionCodeServerDeviceFailure = 4 36 | ExceptionCodeAcknowledge = 5 37 | ExceptionCodeServerDeviceBusy = 6 38 | ExceptionCodeMemoryParityError = 8 39 | ExceptionCodeGatewayPathUnavailable = 10 40 | ExceptionCodeGatewayTargetDeviceFailedToRespond = 11 41 | ) 42 | 43 | // ModbusError implements error interface. 44 | type ModbusError struct { 45 | FunctionCode byte 46 | ExceptionCode byte 47 | } 48 | 49 | // Error converts known modbus exception code to error message. 50 | func (e *ModbusError) Error() string { 51 | var name string 52 | switch e.ExceptionCode { 53 | case ExceptionCodeIllegalFunction: 54 | name = "illegal function" 55 | case ExceptionCodeIllegalDataAddress: 56 | name = "illegal data address" 57 | case ExceptionCodeIllegalDataValue: 58 | name = "illegal data value" 59 | case ExceptionCodeServerDeviceFailure: 60 | name = "server device failure" 61 | case ExceptionCodeAcknowledge: 62 | name = "acknowledge" 63 | case ExceptionCodeServerDeviceBusy: 64 | name = "server device busy" 65 | case ExceptionCodeMemoryParityError: 66 | name = "memory parity error" 67 | case ExceptionCodeGatewayPathUnavailable: 68 | name = "gateway path unavailable" 69 | case ExceptionCodeGatewayTargetDeviceFailedToRespond: 70 | name = "gateway target device failed to respond" 71 | default: 72 | name = "unknown" 73 | } 74 | return fmt.Sprintf("modbus: exception '%v' (%s), function '%v'", e.ExceptionCode, name, e.FunctionCode) 75 | } 76 | 77 | // ProtocolDataUnit (PDU) is independent of underlying communication layers. 78 | type ProtocolDataUnit struct { 79 | FunctionCode byte 80 | Data []byte 81 | } 82 | 83 | // Packager specifies the communication layer. 84 | type Packager interface { 85 | Encode(pdu *ProtocolDataUnit) (adu []byte, err error) 86 | Decode(adu []byte) (pdu *ProtocolDataUnit, err error) 87 | Verify(aduRequest []byte, aduResponse []byte) (err error) 88 | } 89 | 90 | // Transporter specifies the transport layer. 91 | type Transporter interface { 92 | Send(aduRequest []byte) (aduResponse []byte, err error) 93 | } 94 | -------------------------------------------------------------------------------- /rtuclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "encoding/binary" 9 | "fmt" 10 | "io" 11 | "time" 12 | ) 13 | 14 | const ( 15 | rtuMinSize = 4 16 | rtuMaxSize = 256 17 | 18 | rtuExceptionSize = 5 19 | ) 20 | 21 | // RTUClientHandler implements Packager and Transporter interface. 22 | type RTUClientHandler struct { 23 | rtuPackager 24 | rtuSerialTransporter 25 | } 26 | 27 | // NewRTUClientHandler allocates and initializes a RTUClientHandler. 28 | func NewRTUClientHandler(address string) *RTUClientHandler { 29 | handler := &RTUClientHandler{} 30 | handler.Address = address 31 | handler.Timeout = serialTimeout 32 | handler.IdleTimeout = serialIdleTimeout 33 | return handler 34 | } 35 | 36 | // RTUClient creates RTU client with default handler and given connect string. 37 | func RTUClient(address string) Client { 38 | handler := NewRTUClientHandler(address) 39 | return NewClient(handler) 40 | } 41 | 42 | // rtuPackager implements Packager interface. 43 | type rtuPackager struct { 44 | SlaveId byte 45 | } 46 | 47 | // Encode encodes PDU in a RTU frame: 48 | // Slave Address : 1 byte 49 | // Function : 1 byte 50 | // Data : 0 up to 252 bytes 51 | // CRC : 2 byte 52 | func (mb *rtuPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) { 53 | length := len(pdu.Data) + 4 54 | if length > rtuMaxSize { 55 | err = fmt.Errorf("modbus: length of data '%v' must not be bigger than '%v'", length, rtuMaxSize) 56 | return 57 | } 58 | adu = make([]byte, length) 59 | 60 | adu[0] = mb.SlaveId 61 | adu[1] = pdu.FunctionCode 62 | copy(adu[2:], pdu.Data) 63 | 64 | // Append crc 65 | var crc crc 66 | crc.reset().pushBytes(adu[0 : length-2]) 67 | checksum := crc.value() 68 | 69 | adu[length-1] = byte(checksum >> 8) 70 | adu[length-2] = byte(checksum) 71 | return 72 | } 73 | 74 | // Verify verifies response length and slave id. 75 | func (mb *rtuPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) { 76 | length := len(aduResponse) 77 | // Minimum size (including address, function and CRC) 78 | if length < rtuMinSize { 79 | err = fmt.Errorf("modbus: response length '%v' does not meet minimum '%v'", length, rtuMinSize) 80 | return 81 | } 82 | // Slave address must match 83 | if aduResponse[0] != aduRequest[0] { 84 | err = fmt.Errorf("modbus: response slave id '%v' does not match request '%v'", aduResponse[0], aduRequest[0]) 85 | return 86 | } 87 | return 88 | } 89 | 90 | // Decode extracts PDU from RTU frame and verify CRC. 91 | func (mb *rtuPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) { 92 | length := len(adu) 93 | // Calculate checksum 94 | var crc crc 95 | crc.reset().pushBytes(adu[0 : length-2]) 96 | checksum := uint16(adu[length-1])<<8 | uint16(adu[length-2]) 97 | if checksum != crc.value() { 98 | err = fmt.Errorf("modbus: response crc '%v' does not match expected '%v'", checksum, crc.value()) 99 | return 100 | } 101 | // Function code & data 102 | pdu = &ProtocolDataUnit{} 103 | pdu.FunctionCode = adu[1] 104 | pdu.Data = adu[2 : length-2] 105 | return 106 | } 107 | 108 | // rtuSerialTransporter implements Transporter interface. 109 | type rtuSerialTransporter struct { 110 | serialPort 111 | } 112 | 113 | func (mb *rtuSerialTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) { 114 | // Make sure port is connected 115 | if err = mb.serialPort.connect(); err != nil { 116 | return 117 | } 118 | // Start the timer to close when idle 119 | mb.serialPort.lastActivity = time.Now() 120 | mb.serialPort.startCloseTimer() 121 | 122 | // Send the request 123 | mb.serialPort.logf("modbus: sending % x\n", aduRequest) 124 | if _, err = mb.port.Write(aduRequest); err != nil { 125 | return 126 | } 127 | function := aduRequest[1] 128 | functionFail := aduRequest[1] & 0x80 129 | bytesToRead := calculateResponseLength(aduRequest) 130 | time.Sleep(mb.calculateDelay(len(aduRequest) + bytesToRead)) 131 | 132 | var n int 133 | var n1 int 134 | var data [rtuMaxSize]byte 135 | //We first read the minimum length and then read either the full package 136 | //or the error package, depending on the error status (byte 2 of the response) 137 | n, err = io.ReadAtLeast(mb.port, data[:], rtuMinSize) 138 | if err != nil { 139 | return 140 | } 141 | //if the function is correct 142 | if data[1] == function { 143 | //we read the rest of the bytes 144 | if n < bytesToRead { 145 | if bytesToRead > rtuMinSize && bytesToRead <= rtuMaxSize { 146 | if bytesToRead > n { 147 | n1, err = io.ReadFull(mb.port, data[n:bytesToRead]) 148 | n += n1 149 | } 150 | } 151 | } 152 | } else if data[1] == functionFail { 153 | //for error we need to read 5 bytes 154 | if n < rtuExceptionSize { 155 | n1, err = io.ReadFull(mb.port, data[n:rtuExceptionSize]) 156 | } 157 | n += n1 158 | } 159 | 160 | if err != nil { 161 | return 162 | } 163 | aduResponse = data[:n] 164 | mb.serialPort.logf("modbus: received % x\n", aduResponse) 165 | return 166 | } 167 | 168 | // calculateDelay roughly calculates time needed for the next frame. 169 | // See MODBUS over Serial Line - Specification and Implementation Guide (page 13). 170 | func (mb *rtuSerialTransporter) calculateDelay(chars int) time.Duration { 171 | var characterDelay, frameDelay int // us 172 | 173 | if mb.BaudRate <= 0 || mb.BaudRate > 19200 { 174 | characterDelay = 750 175 | frameDelay = 1750 176 | } else { 177 | characterDelay = 15000000 / mb.BaudRate 178 | frameDelay = 35000000 / mb.BaudRate 179 | } 180 | return time.Duration(characterDelay*chars+frameDelay) * time.Microsecond 181 | } 182 | 183 | func calculateResponseLength(adu []byte) int { 184 | length := rtuMinSize 185 | switch adu[1] { 186 | case FuncCodeReadDiscreteInputs, 187 | FuncCodeReadCoils: 188 | count := int(binary.BigEndian.Uint16(adu[4:])) 189 | length += 1 + count/8 190 | if count%8 != 0 { 191 | length++ 192 | } 193 | case FuncCodeReadInputRegisters, 194 | FuncCodeReadHoldingRegisters, 195 | FuncCodeReadWriteMultipleRegisters: 196 | count := int(binary.BigEndian.Uint16(adu[4:])) 197 | length += 1 + count*2 198 | case FuncCodeWriteSingleCoil, 199 | FuncCodeWriteMultipleCoils, 200 | FuncCodeWriteSingleRegister, 201 | FuncCodeWriteMultipleRegisters: 202 | length += 4 203 | case FuncCodeMaskWriteRegister: 204 | length += 6 205 | case FuncCodeReadFIFOQueue: 206 | // undetermined 207 | default: 208 | } 209 | return length 210 | } 211 | -------------------------------------------------------------------------------- /rtuclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func TestRTUEncoding(t *testing.T) { 13 | encoder := rtuPackager{} 14 | encoder.SlaveId = 0x01 15 | 16 | pdu := ProtocolDataUnit{} 17 | pdu.FunctionCode = 0x03 18 | pdu.Data = []byte{0x50, 0x00, 0x00, 0x18} 19 | 20 | adu, err := encoder.Encode(&pdu) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | expected := []byte{0x01, 0x03, 0x50, 0x00, 0x00, 0x18, 0x54, 0xC0} 25 | if !bytes.Equal(expected, adu) { 26 | t.Fatalf("adu: expected %v, actual %v", expected, adu) 27 | } 28 | } 29 | 30 | func TestRTUDecoding(t *testing.T) { 31 | decoder := rtuPackager{} 32 | adu := []byte{0x01, 0x10, 0x8A, 0x00, 0x00, 0x03, 0xAA, 0x10} 33 | 34 | pdu, err := decoder.Decode(adu) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | if 16 != pdu.FunctionCode { 40 | t.Fatalf("Function code: expected %v, actual %v", 16, pdu.FunctionCode) 41 | } 42 | expected := []byte{0x8A, 0x00, 0x00, 0x03} 43 | if !bytes.Equal(expected, pdu.Data) { 44 | t.Fatalf("Data: expected %v, actual %v", expected, pdu.Data) 45 | } 46 | } 47 | 48 | var responseLengthTests = []struct { 49 | adu []byte 50 | length int 51 | }{ 52 | {[]byte{4, 1, 0, 0xA, 0, 0xD, 0xDD, 0x98}, 7}, 53 | {[]byte{4, 2, 0, 0xA, 0, 0xD, 0x99, 0x98}, 7}, 54 | {[]byte{1, 3, 0, 0, 0, 2, 0xC4, 0xB}, 9}, 55 | {[]byte{0x11, 5, 0, 0xAC, 0xFF, 0, 0x4E, 0x8B}, 8}, 56 | {[]byte{0x11, 6, 0, 1, 0, 3, 0x9A, 0x9B}, 8}, 57 | {[]byte{0x11, 0xF, 0, 0x13, 0, 0xA, 2, 0xCD, 1, 0xBF, 0xB}, 8}, 58 | {[]byte{0x11, 0x10, 0, 1, 0, 2, 4, 0, 0xA, 1, 2, 0xC6, 0xF0}, 8}, 59 | } 60 | 61 | func TestCalculateResponseLength(t *testing.T) { 62 | for _, input := range responseLengthTests { 63 | output := calculateResponseLength(input.adu) 64 | if output != input.length { 65 | t.Errorf("Response length of %x: expected %v, actual: %v", 66 | input.adu, input.length, output) 67 | } 68 | } 69 | } 70 | 71 | func BenchmarkRTUEncoder(b *testing.B) { 72 | encoder := rtuPackager{ 73 | SlaveId: 10, 74 | } 75 | pdu := ProtocolDataUnit{ 76 | FunctionCode: 1, 77 | Data: []byte{2, 3, 4, 5, 6, 7, 8, 9}, 78 | } 79 | for i := 0; i < b.N; i++ { 80 | _, err := encoder.Encode(&pdu) 81 | if err != nil { 82 | b.Fatal(err) 83 | } 84 | } 85 | } 86 | 87 | func BenchmarkRTUDecoder(b *testing.B) { 88 | decoder := rtuPackager{ 89 | SlaveId: 10, 90 | } 91 | adu := []byte{0x01, 0x10, 0x8A, 0x00, 0x00, 0x03, 0xAA, 0x10} 92 | for i := 0; i < b.N; i++ { 93 | _, err := decoder.Decode(adu) 94 | if err != nil { 95 | b.Fatal(err) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /serial.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "io" 9 | "log" 10 | "sync" 11 | "time" 12 | 13 | "github.com/goburrow/serial" 14 | ) 15 | 16 | const ( 17 | // Default timeout 18 | serialTimeout = 5 * time.Second 19 | serialIdleTimeout = 60 * time.Second 20 | ) 21 | 22 | // serialPort has configuration and I/O controller. 23 | type serialPort struct { 24 | // Serial port configuration. 25 | serial.Config 26 | 27 | Logger *log.Logger 28 | IdleTimeout time.Duration 29 | 30 | mu sync.Mutex 31 | // port is platform-dependent data structure for serial port. 32 | port io.ReadWriteCloser 33 | lastActivity time.Time 34 | closeTimer *time.Timer 35 | } 36 | 37 | func (mb *serialPort) Connect() (err error) { 38 | mb.mu.Lock() 39 | defer mb.mu.Unlock() 40 | 41 | return mb.connect() 42 | } 43 | 44 | // connect connects to the serial port if it is not connected. Caller must hold the mutex. 45 | func (mb *serialPort) connect() error { 46 | if mb.port == nil { 47 | port, err := serial.Open(&mb.Config) 48 | if err != nil { 49 | return err 50 | } 51 | mb.port = port 52 | } 53 | return nil 54 | } 55 | 56 | func (mb *serialPort) Close() (err error) { 57 | mb.mu.Lock() 58 | defer mb.mu.Unlock() 59 | 60 | return mb.close() 61 | } 62 | 63 | // close closes the serial port if it is connected. Caller must hold the mutex. 64 | func (mb *serialPort) close() (err error) { 65 | if mb.port != nil { 66 | err = mb.port.Close() 67 | mb.port = nil 68 | } 69 | return 70 | } 71 | 72 | func (mb *serialPort) logf(format string, v ...interface{}) { 73 | if mb.Logger != nil { 74 | mb.Logger.Printf(format, v...) 75 | } 76 | } 77 | 78 | func (mb *serialPort) startCloseTimer() { 79 | if mb.IdleTimeout <= 0 { 80 | return 81 | } 82 | if mb.closeTimer == nil { 83 | mb.closeTimer = time.AfterFunc(mb.IdleTimeout, mb.closeIdle) 84 | } else { 85 | mb.closeTimer.Reset(mb.IdleTimeout) 86 | } 87 | } 88 | 89 | // closeIdle closes the connection if last activity is passed behind IdleTimeout. 90 | func (mb *serialPort) closeIdle() { 91 | mb.mu.Lock() 92 | defer mb.mu.Unlock() 93 | 94 | if mb.IdleTimeout <= 0 { 95 | return 96 | } 97 | idle := time.Now().Sub(mb.lastActivity) 98 | if idle >= mb.IdleTimeout { 99 | mb.logf("modbus: closing connection due to idle timeout: %v", idle) 100 | mb.close() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /serial_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type nopCloser struct { 11 | io.ReadWriter 12 | 13 | closed bool 14 | } 15 | 16 | func (n *nopCloser) Close() error { 17 | n.closed = true 18 | return nil 19 | } 20 | 21 | func TestSerialCloseIdle(t *testing.T) { 22 | port := &nopCloser{ 23 | ReadWriter: &bytes.Buffer{}, 24 | } 25 | s := serialPort{ 26 | port: port, 27 | IdleTimeout: 100 * time.Millisecond, 28 | } 29 | s.lastActivity = time.Now() 30 | s.startCloseTimer() 31 | 32 | time.Sleep(150 * time.Millisecond) 33 | if !port.closed || s.port != nil { 34 | t.Fatalf("serial port is not closed when inactivity: %+v", port) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tcpclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "encoding/binary" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | ) 17 | 18 | const ( 19 | tcpProtocolIdentifier uint16 = 0x0000 20 | 21 | // Modbus Application Protocol 22 | tcpHeaderSize = 7 23 | tcpMaxLength = 260 24 | // Default TCP timeout is not set 25 | tcpTimeout = 10 * time.Second 26 | tcpIdleTimeout = 60 * time.Second 27 | ) 28 | 29 | // TCPClientHandler implements Packager and Transporter interface. 30 | type TCPClientHandler struct { 31 | tcpPackager 32 | tcpTransporter 33 | } 34 | 35 | // NewTCPClientHandler allocates a new TCPClientHandler. 36 | func NewTCPClientHandler(address string) *TCPClientHandler { 37 | h := &TCPClientHandler{} 38 | h.Address = address 39 | h.Timeout = tcpTimeout 40 | h.IdleTimeout = tcpIdleTimeout 41 | return h 42 | } 43 | 44 | // TCPClient creates TCP client with default handler and given connect string. 45 | func TCPClient(address string) Client { 46 | handler := NewTCPClientHandler(address) 47 | return NewClient(handler) 48 | } 49 | 50 | // tcpPackager implements Packager interface. 51 | type tcpPackager struct { 52 | // For synchronization between messages of server & client 53 | transactionId uint32 54 | // Broadcast address is 0 55 | SlaveId byte 56 | } 57 | 58 | // Encode adds modbus application protocol header: 59 | // Transaction identifier: 2 bytes 60 | // Protocol identifier: 2 bytes 61 | // Length: 2 bytes 62 | // Unit identifier: 1 byte 63 | // Function code: 1 byte 64 | // Data: n bytes 65 | func (mb *tcpPackager) Encode(pdu *ProtocolDataUnit) (adu []byte, err error) { 66 | adu = make([]byte, tcpHeaderSize+1+len(pdu.Data)) 67 | 68 | // Transaction identifier 69 | transactionId := atomic.AddUint32(&mb.transactionId, 1) 70 | binary.BigEndian.PutUint16(adu, uint16(transactionId)) 71 | // Protocol identifier 72 | binary.BigEndian.PutUint16(adu[2:], tcpProtocolIdentifier) 73 | // Length = sizeof(SlaveId) + sizeof(FunctionCode) + Data 74 | length := uint16(1 + 1 + len(pdu.Data)) 75 | binary.BigEndian.PutUint16(adu[4:], length) 76 | // Unit identifier 77 | adu[6] = mb.SlaveId 78 | 79 | // PDU 80 | adu[tcpHeaderSize] = pdu.FunctionCode 81 | copy(adu[tcpHeaderSize+1:], pdu.Data) 82 | return 83 | } 84 | 85 | // Verify confirms transaction, protocol and unit id. 86 | func (mb *tcpPackager) Verify(aduRequest []byte, aduResponse []byte) (err error) { 87 | // Transaction id 88 | responseVal := binary.BigEndian.Uint16(aduResponse) 89 | requestVal := binary.BigEndian.Uint16(aduRequest) 90 | if responseVal != requestVal { 91 | err = fmt.Errorf("modbus: response transaction id '%v' does not match request '%v'", responseVal, requestVal) 92 | return 93 | } 94 | // Protocol id 95 | responseVal = binary.BigEndian.Uint16(aduResponse[2:]) 96 | requestVal = binary.BigEndian.Uint16(aduRequest[2:]) 97 | if responseVal != requestVal { 98 | err = fmt.Errorf("modbus: response protocol id '%v' does not match request '%v'", responseVal, requestVal) 99 | return 100 | } 101 | // Unit id (1 byte) 102 | if aduResponse[6] != aduRequest[6] { 103 | err = fmt.Errorf("modbus: response unit id '%v' does not match request '%v'", aduResponse[6], aduRequest[6]) 104 | return 105 | } 106 | return 107 | } 108 | 109 | // Decode extracts PDU from TCP frame: 110 | // Transaction identifier: 2 bytes 111 | // Protocol identifier: 2 bytes 112 | // Length: 2 bytes 113 | // Unit identifier: 1 byte 114 | func (mb *tcpPackager) Decode(adu []byte) (pdu *ProtocolDataUnit, err error) { 115 | // Read length value in the header 116 | length := binary.BigEndian.Uint16(adu[4:]) 117 | pduLength := len(adu) - tcpHeaderSize 118 | if pduLength <= 0 || pduLength != int(length-1) { 119 | err = fmt.Errorf("modbus: length in response '%v' does not match pdu data length '%v'", length-1, pduLength) 120 | return 121 | } 122 | pdu = &ProtocolDataUnit{} 123 | // The first byte after header is function code 124 | pdu.FunctionCode = adu[tcpHeaderSize] 125 | pdu.Data = adu[tcpHeaderSize+1:] 126 | return 127 | } 128 | 129 | // tcpTransporter implements Transporter interface. 130 | type tcpTransporter struct { 131 | // Connect string 132 | Address string 133 | // Connect & Read timeout 134 | Timeout time.Duration 135 | // Idle timeout to close the connection 136 | IdleTimeout time.Duration 137 | // Transmission logger 138 | Logger *log.Logger 139 | 140 | // TCP connection 141 | mu sync.Mutex 142 | conn net.Conn 143 | closeTimer *time.Timer 144 | lastActivity time.Time 145 | } 146 | 147 | // Send sends data to server and ensures response length is greater than header length. 148 | func (mb *tcpTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) { 149 | mb.mu.Lock() 150 | defer mb.mu.Unlock() 151 | 152 | // Establish a new connection if not connected 153 | if err = mb.connect(); err != nil { 154 | return 155 | } 156 | // Set timer to close when idle 157 | mb.lastActivity = time.Now() 158 | mb.startCloseTimer() 159 | // Set write and read timeout 160 | var timeout time.Time 161 | if mb.Timeout > 0 { 162 | timeout = mb.lastActivity.Add(mb.Timeout) 163 | } 164 | if err = mb.conn.SetDeadline(timeout); err != nil { 165 | return 166 | } 167 | // Send data 168 | mb.logf("modbus: sending % x", aduRequest) 169 | if _, err = mb.conn.Write(aduRequest); err != nil { 170 | return 171 | } 172 | // Read header first 173 | var data [tcpMaxLength]byte 174 | if _, err = io.ReadFull(mb.conn, data[:tcpHeaderSize]); err != nil { 175 | return 176 | } 177 | // Read length, ignore transaction & protocol id (4 bytes) 178 | length := int(binary.BigEndian.Uint16(data[4:])) 179 | if length <= 0 { 180 | mb.flush(data[:]) 181 | err = fmt.Errorf("modbus: length in response header '%v' must not be zero", length) 182 | return 183 | } 184 | if length > (tcpMaxLength - (tcpHeaderSize - 1)) { 185 | mb.flush(data[:]) 186 | err = fmt.Errorf("modbus: length in response header '%v' must not greater than '%v'", length, tcpMaxLength-tcpHeaderSize+1) 187 | return 188 | } 189 | // Skip unit id 190 | length += tcpHeaderSize - 1 191 | if _, err = io.ReadFull(mb.conn, data[tcpHeaderSize:length]); err != nil { 192 | return 193 | } 194 | aduResponse = data[:length] 195 | mb.logf("modbus: received % x\n", aduResponse) 196 | return 197 | } 198 | 199 | // Connect establishes a new connection to the address in Address. 200 | // Connect and Close are exported so that multiple requests can be done with one session 201 | func (mb *tcpTransporter) Connect() error { 202 | mb.mu.Lock() 203 | defer mb.mu.Unlock() 204 | 205 | return mb.connect() 206 | } 207 | 208 | func (mb *tcpTransporter) connect() error { 209 | if mb.conn == nil { 210 | dialer := net.Dialer{Timeout: mb.Timeout} 211 | conn, err := dialer.Dial("tcp", mb.Address) 212 | if err != nil { 213 | return err 214 | } 215 | mb.conn = conn 216 | } 217 | return nil 218 | } 219 | 220 | func (mb *tcpTransporter) startCloseTimer() { 221 | if mb.IdleTimeout <= 0 { 222 | return 223 | } 224 | if mb.closeTimer == nil { 225 | mb.closeTimer = time.AfterFunc(mb.IdleTimeout, mb.closeIdle) 226 | } else { 227 | mb.closeTimer.Reset(mb.IdleTimeout) 228 | } 229 | } 230 | 231 | // Close closes current connection. 232 | func (mb *tcpTransporter) Close() error { 233 | mb.mu.Lock() 234 | defer mb.mu.Unlock() 235 | 236 | return mb.close() 237 | } 238 | 239 | // flush flushes pending data in the connection, 240 | // returns io.EOF if connection is closed. 241 | func (mb *tcpTransporter) flush(b []byte) (err error) { 242 | if err = mb.conn.SetReadDeadline(time.Now()); err != nil { 243 | return 244 | } 245 | // Timeout setting will be reset when reading 246 | if _, err = mb.conn.Read(b); err != nil { 247 | // Ignore timeout error 248 | if netError, ok := err.(net.Error); ok && netError.Timeout() { 249 | err = nil 250 | } 251 | } 252 | return 253 | } 254 | 255 | func (mb *tcpTransporter) logf(format string, v ...interface{}) { 256 | if mb.Logger != nil { 257 | mb.Logger.Printf(format, v...) 258 | } 259 | } 260 | 261 | // closeLocked closes current connection. Caller must hold the mutex before calling this method. 262 | func (mb *tcpTransporter) close() (err error) { 263 | if mb.conn != nil { 264 | err = mb.conn.Close() 265 | mb.conn = nil 266 | } 267 | return 268 | } 269 | 270 | // closeIdle closes the connection if last activity is passed behind IdleTimeout. 271 | func (mb *tcpTransporter) closeIdle() { 272 | mb.mu.Lock() 273 | defer mb.mu.Unlock() 274 | 275 | if mb.IdleTimeout <= 0 { 276 | return 277 | } 278 | idle := time.Now().Sub(mb.lastActivity) 279 | if idle >= mb.IdleTimeout { 280 | mb.logf("modbus: closing connection due to idle timeout: %v", idle) 281 | mb.close() 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /tcpclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package modbus 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "net" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestTCPEncoding(t *testing.T) { 16 | packager := tcpPackager{} 17 | pdu := ProtocolDataUnit{} 18 | pdu.FunctionCode = 3 19 | pdu.Data = []byte{0, 4, 0, 3} 20 | 21 | adu, err := packager.Encode(&pdu) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | expected := []byte{0, 1, 0, 0, 0, 6, 0, 3, 0, 4, 0, 3} 27 | if !bytes.Equal(expected, adu) { 28 | t.Fatalf("Expected %v, actual %v", expected, adu) 29 | } 30 | } 31 | 32 | func TestTCPDecoding(t *testing.T) { 33 | packager := tcpPackager{} 34 | packager.transactionId = 1 35 | packager.SlaveId = 17 36 | adu := []byte{0, 1, 0, 0, 0, 6, 17, 3, 0, 120, 0, 3} 37 | 38 | pdu, err := packager.Decode(adu) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if 3 != pdu.FunctionCode { 44 | t.Fatalf("Function code: expected %v, actual %v", 3, pdu.FunctionCode) 45 | } 46 | expected := []byte{0, 120, 0, 3} 47 | if !bytes.Equal(expected, pdu.Data) { 48 | t.Fatalf("Data: expected %v, actual %v", expected, adu) 49 | } 50 | } 51 | 52 | func TestTCPTransporter(t *testing.T) { 53 | ln, err := net.Listen("tcp", "127.0.0.1:0") 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | defer ln.Close() 58 | 59 | go func() { 60 | conn, err := ln.Accept() 61 | if err != nil { 62 | t.Error(err) 63 | return 64 | } 65 | defer conn.Close() 66 | _, err = io.Copy(conn, conn) 67 | if err != nil { 68 | t.Error(err) 69 | return 70 | } 71 | }() 72 | client := &tcpTransporter{ 73 | Address: ln.Addr().String(), 74 | Timeout: 1 * time.Second, 75 | IdleTimeout: 100 * time.Millisecond, 76 | } 77 | req := []byte{0, 1, 0, 2, 0, 2, 1, 2} 78 | rsp, err := client.Send(req) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | if !bytes.Equal(req, rsp) { 83 | t.Fatalf("unexpected response: %x", rsp) 84 | } 85 | time.Sleep(150 * time.Millisecond) 86 | if client.conn != nil { 87 | t.Fatalf("connection is not closed: %+v", client.conn) 88 | } 89 | } 90 | 91 | func BenchmarkTCPEncoder(b *testing.B) { 92 | encoder := tcpPackager{ 93 | SlaveId: 10, 94 | } 95 | pdu := ProtocolDataUnit{ 96 | FunctionCode: 1, 97 | Data: []byte{2, 3, 4, 5, 6, 7, 8, 9}, 98 | } 99 | for i := 0; i < b.N; i++ { 100 | _, err := encoder.Encode(&pdu) 101 | if err != nil { 102 | b.Fatal(err) 103 | } 104 | } 105 | } 106 | 107 | func BenchmarkTCPDecoder(b *testing.B) { 108 | decoder := tcpPackager{ 109 | SlaveId: 10, 110 | } 111 | adu := []byte{0, 1, 0, 0, 0, 6, 17, 3, 0, 120, 0, 3} 112 | for i := 0; i < b.N; i++ { 113 | _, err := decoder.Decode(adu) 114 | if err != nil { 115 | b.Fatal(err) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | System testing for [modbus library](https://github.com/goburrow/modbus) 2 | 3 | Modbus simulator 4 | ---------------- 5 | * [Diagslave](http://www.modbusdriver.com/diagslave.html) 6 | * [socat](http://www.dest-unreach.org/socat/) 7 | 8 | ```bash 9 | # TCP 10 | $ diagslave -m tcp -p 5020 11 | 12 | # RTU/ASCII 13 | $ socat -d -d pty,raw,echo=0 pty,raw,echo=0 14 | 2015/04/03 12:34:56 socat[2342] N PTY is /dev/pts/6 15 | 2015/04/03 12:34:56 socat[2342] N PTY is /dev/pts/7 16 | $ diagslave -m ascii /dev/pts/7 17 | 18 | # Or 19 | $ diagslave -m rtu /dev/pts/7 20 | 21 | $ go test -v -run TCP 22 | $ go test -v -run RTU 23 | $ go test -v -run ASCII 24 | ``` 25 | -------------------------------------------------------------------------------- /test/asciiclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package test 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "testing" 11 | 12 | "github.com/goburrow/modbus" 13 | ) 14 | 15 | const ( 16 | asciiDevice = "/dev/pts/6" 17 | ) 18 | 19 | func TestASCIIClient(t *testing.T) { 20 | // Diagslave does not support broadcast id. 21 | handler := modbus.NewASCIIClientHandler(asciiDevice) 22 | handler.SlaveId = 17 23 | ClientTestAll(t, modbus.NewClient(handler)) 24 | } 25 | 26 | func TestASCIIClientAdvancedUsage(t *testing.T) { 27 | handler := modbus.NewASCIIClientHandler(asciiDevice) 28 | handler.BaudRate = 19200 29 | handler.DataBits = 8 30 | handler.Parity = "E" 31 | handler.StopBits = 1 32 | handler.SlaveId = 12 33 | handler.Logger = log.New(os.Stdout, "ascii: ", log.LstdFlags) 34 | err := handler.Connect() 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | defer handler.Close() 39 | 40 | client := modbus.NewClient(handler) 41 | results, err := client.ReadDiscreteInputs(15, 2) 42 | if err != nil || results == nil { 43 | t.Fatal(err, results) 44 | } 45 | results, err = client.ReadWriteMultipleRegisters(0, 2, 2, 2, []byte{1, 2, 3, 4}) 46 | if err != nil || results == nil { 47 | t.Fatal(err, results) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/goburrow/modbus" 11 | ) 12 | 13 | func ClientTestReadCoils(t *testing.T, client modbus.Client) { 14 | // Read discrete outputs 20-38: 15 | address := uint16(0x0013) 16 | quantity := uint16(0x0013) 17 | results, err := client.ReadCoils(address, quantity) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | AssertEquals(t, 3, len(results)) 22 | } 23 | 24 | func ClientTestReadDiscreteInputs(t *testing.T, client modbus.Client) { 25 | // Read discrete inputs 197-218 26 | address := uint16(0x00C4) 27 | quantity := uint16(0x0016) 28 | results, err := client.ReadDiscreteInputs(address, quantity) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | AssertEquals(t, 3, len(results)) 33 | } 34 | 35 | func ClientTestReadHoldingRegisters(t *testing.T, client modbus.Client) { 36 | // Read registers 108-110 37 | address := uint16(0x006B) 38 | quantity := uint16(0x0003) 39 | results, err := client.ReadHoldingRegisters(address, quantity) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | AssertEquals(t, 6, len(results)) 44 | } 45 | 46 | func ClientTestReadInputRegisters(t *testing.T, client modbus.Client) { 47 | // Read input register 9 48 | address := uint16(0x0008) 49 | quantity := uint16(0x0001) 50 | results, err := client.ReadInputRegisters(address, quantity) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | AssertEquals(t, 2, len(results)) 55 | } 56 | 57 | func ClientTestWriteSingleCoil(t *testing.T, client modbus.Client) { 58 | // Write coil 173 ON 59 | address := uint16(0x00AC) 60 | value := uint16(0xFF00) 61 | results, err := client.WriteSingleCoil(address, value) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | AssertEquals(t, 2, len(results)) 66 | } 67 | 68 | func ClientTestWriteSingleRegister(t *testing.T, client modbus.Client) { 69 | // Write register 2 to 00 03 hex 70 | address := uint16(0x0001) 71 | value := uint16(0x0003) 72 | results, err := client.WriteSingleRegister(address, value) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | AssertEquals(t, 2, len(results)) 77 | } 78 | 79 | func ClientTestWriteMultipleCoils(t *testing.T, client modbus.Client) { 80 | // Write a series of 10 coils starting at coil 20 81 | address := uint16(0x0013) 82 | quantity := uint16(0x000A) 83 | values := []byte{0xCD, 0x01} 84 | results, err := client.WriteMultipleCoils(address, quantity, values) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | AssertEquals(t, 2, len(results)) 89 | } 90 | 91 | func ClientTestWriteMultipleRegisters(t *testing.T, client modbus.Client) { 92 | // Write two registers starting at 2 to 00 0A and 01 02 hex 93 | address := uint16(0x0001) 94 | quantity := uint16(0x0002) 95 | values := []byte{0x00, 0x0A, 0x01, 0x02} 96 | results, err := client.WriteMultipleRegisters(address, quantity, values) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | AssertEquals(t, 2, len(results)) 101 | } 102 | 103 | func ClientTestMaskWriteRegisters(t *testing.T, client modbus.Client) { 104 | // Mask write to register 5 105 | address := uint16(0x0004) 106 | andMask := uint16(0x00F2) 107 | orMask := uint16(0x0025) 108 | results, err := client.MaskWriteRegister(address, andMask, orMask) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | AssertEquals(t, 4, len(results)) 113 | } 114 | 115 | func ClientTestReadWriteMultipleRegisters(t *testing.T, client modbus.Client) { 116 | // read six registers starting at register 4, and to write three registers starting at register 15 117 | address := uint16(0x0003) 118 | quantity := uint16(0x0006) 119 | writeAddress := uint16(0x000E) 120 | writeQuantity := uint16(0x0003) 121 | values := []byte{0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF} 122 | results, err := client.ReadWriteMultipleRegisters(address, quantity, writeAddress, writeQuantity, values) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | AssertEquals(t, 12, len(results)) 127 | } 128 | 129 | func ClientTestReadFIFOQueue(t *testing.T, client modbus.Client) { 130 | // Read queue starting at the pointer register 1246 131 | address := uint16(0x04DE) 132 | results, err := client.ReadFIFOQueue(address) 133 | // Server not implemented 134 | if err != nil { 135 | AssertEquals(t, "modbus: exception '1' (illegal function), function '152'", err.Error()) 136 | } else { 137 | AssertEquals(t, 0, len(results)) 138 | } 139 | } 140 | 141 | func ClientTestAll(t *testing.T, client modbus.Client) { 142 | ClientTestReadCoils(t, client) 143 | ClientTestReadDiscreteInputs(t, client) 144 | ClientTestReadHoldingRegisters(t, client) 145 | ClientTestReadInputRegisters(t, client) 146 | ClientTestWriteSingleCoil(t, client) 147 | ClientTestWriteSingleRegister(t, client) 148 | ClientTestWriteMultipleCoils(t, client) 149 | ClientTestWriteMultipleRegisters(t, client) 150 | ClientTestMaskWriteRegisters(t, client) 151 | ClientTestReadWriteMultipleRegisters(t, client) 152 | ClientTestReadFIFOQueue(t, client) 153 | } 154 | -------------------------------------------------------------------------------- /test/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package test 6 | 7 | import ( 8 | "runtime" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func AssertEquals(t *testing.T, expected, actual interface{}) { 14 | _, file, line, ok := runtime.Caller(1) 15 | if !ok { 16 | file = "???" 17 | line = 0 18 | } else { 19 | // Get file name only 20 | idx := strings.LastIndex(file, "/") 21 | if idx >= 0 { 22 | file = file[idx+1:] 23 | } 24 | } 25 | 26 | if expected != actual { 27 | t.Logf("%s:%d: Expected: %+v (%T), actual: %+v (%T)", file, line, 28 | expected, expected, actual, actual) 29 | t.FailNow() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/commw32/commw32.c: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | // Test serial communication in Win32 6 | // gcc commw32.c 7 | #include 8 | #include 9 | #include 10 | 11 | static const char* port = "COM4"; 12 | 13 | static printLastError() { 14 | char lpBuffer[256] = "?"; 15 | FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 16 | NULL, 17 | GetLastError(), 18 | MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT), 19 | lpBuffer, 20 | sizeof(lpBuffer)-1, 21 | NULL); 22 | printf("%s\n", lpBuffer); 23 | } 24 | 25 | int main(int argc, char** argv) { 26 | HANDLE handle; 27 | DCB dcb = {0}; 28 | COMMTIMEOUTS timeouts; 29 | DWORD n = 0; 30 | char data[512]; 31 | int i = 0; 32 | 33 | handle = CreateFile(port, 34 | GENERIC_READ | GENERIC_WRITE, 35 | 0, 36 | 0, 37 | OPEN_EXISTING, 38 | 0, 39 | 0); 40 | if (handle == INVALID_HANDLE_VALUE) { 41 | printf("invalid handle %d\n", GetLastError()); 42 | printLastError(); 43 | return 1; 44 | } 45 | printf("handle created %d\n", handle); 46 | 47 | dcb.BaudRate = CBR_9600; 48 | dcb.ByteSize = 8; 49 | dcb.StopBits = ONESTOPBIT; 50 | dcb.Parity = NOPARITY; 51 | // No software handshaking 52 | dcb.fTXContinueOnXoff = 1; 53 | dcb.fOutX = 0; 54 | dcb.fInX = 0; 55 | // Binary mode 56 | dcb.fBinary = 1; 57 | // No blocking on errors 58 | dcb.fAbortOnError = 0; 59 | 60 | if (!SetCommState(handle, &dcb)) { 61 | printf("set comm state error %d\n", GetLastError()); 62 | printLastError(); 63 | CloseHandle(handle); 64 | return 1; 65 | } 66 | printf("set comm state succeed\n"); 67 | 68 | // time-out between charactor for receiving (ms) 69 | timeouts.ReadIntervalTimeout = 1000; 70 | timeouts.ReadTotalTimeoutMultiplier = 0; 71 | timeouts.ReadTotalTimeoutConstant = 1000; 72 | timeouts.WriteTotalTimeoutMultiplier = 0; 73 | timeouts.WriteTotalTimeoutConstant = 1000; 74 | if (!SetCommTimeouts(handle, &timeouts)) { 75 | printf("set comm timeouts error %d\n", GetLastError()); 76 | printLastError(); 77 | CloseHandle(handle); 78 | return 1; 79 | } 80 | printf("set comm timeouts succeed\n"); 81 | 82 | if (!WriteFile(handle, "abc", 3, &n, NULL)) { 83 | printf("write file error %d\n", GetLastError()); 84 | printLastError(); 85 | CloseHandle(handle); 86 | return 1; 87 | } 88 | printf("write file succeed\n"); 89 | printf("Press Enter when ready for reading..."); 90 | getchar(); 91 | 92 | if (!ReadFile(handle, data, sizeof(data), &n, NULL)) { 93 | printf("read file error %d\n", GetLastError()); 94 | printLastError(); 95 | CloseHandle(handle); 96 | return 1; 97 | } 98 | printf("received data %d:\n", n); 99 | for (i = 0; i < n; ++i) { 100 | printf("%02x", data[i]); 101 | } 102 | printf("\n"); 103 | 104 | CloseHandle(handle); 105 | printf("closed\n"); 106 | return 0; 107 | } 108 | -------------------------------------------------------------------------------- /test/commw32/commw32.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | // +build windows,cgo 5 | 6 | // Port of commw32.c 7 | // To generate go types: go tool cgo commw32.go 8 | package main 9 | 10 | // #include 11 | import "C" 12 | 13 | import ( 14 | "bufio" 15 | "fmt" 16 | "os" 17 | "syscall" 18 | ) 19 | 20 | const port = "COM4" 21 | 22 | func main() { 23 | handle, err := syscall.CreateFile(syscall.StringToUTF16Ptr(port), 24 | syscall.GENERIC_READ|syscall.GENERIC_WRITE, 25 | 0, // mode 26 | nil, // security 27 | syscall.OPEN_EXISTING, // no creating new 28 | 0, 29 | 0) 30 | if err != nil { 31 | fmt.Print(err) 32 | return 33 | } 34 | fmt.Printf("handle created %d\n", handle) 35 | defer syscall.CloseHandle(handle) 36 | 37 | var dcb C.DCB 38 | dcb.BaudRate = 9600 39 | dcb.ByteSize = 8 40 | dcb.StopBits = C.ONESTOPBIT 41 | dcb.Parity = C.NOPARITY 42 | if C.SetCommState(C.HANDLE(handle), &dcb) == 0 { 43 | fmt.Printf("set comm state error %v\n", syscall.GetLastError()) 44 | return 45 | } 46 | fmt.Printf("set comm state succeed\n") 47 | 48 | var timeouts C.COMMTIMEOUTS 49 | // time-out between charactor for receiving (ms) 50 | timeouts.ReadIntervalTimeout = 1000 51 | timeouts.ReadTotalTimeoutMultiplier = 0 52 | timeouts.ReadTotalTimeoutConstant = 1000 53 | timeouts.WriteTotalTimeoutMultiplier = 0 54 | timeouts.WriteTotalTimeoutConstant = 1000 55 | if C.SetCommTimeouts(C.HANDLE(handle), &timeouts) == 0 { 56 | fmt.Printf("set comm timeouts error %v\n", syscall.GetLastError()) 57 | return 58 | } 59 | fmt.Printf("set comm timeouts succeed\n") 60 | 61 | var n uint32 62 | data := []byte("abc") 63 | err = syscall.WriteFile(handle, data, &n, nil) 64 | if err != nil { 65 | fmt.Println(err) 66 | return 67 | } 68 | fmt.Printf("write file succeed\n") 69 | fmt.Printf("Press Enter when ready for reading...") 70 | reader := bufio.NewReader(os.Stdin) 71 | _, _ = reader.ReadString('\n') 72 | 73 | data = make([]byte, 512) 74 | err = syscall.ReadFile(handle, data, &n, nil) 75 | if err != nil { 76 | fmt.Println(err) 77 | return 78 | } 79 | fmt.Printf("received data %v:\n", n) 80 | fmt.Printf("%x\n", data[:n]) 81 | fmt.Printf("closed\n") 82 | } 83 | -------------------------------------------------------------------------------- /test/rtuclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package test 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "testing" 11 | 12 | "github.com/goburrow/modbus" 13 | ) 14 | 15 | const ( 16 | rtuDevice = "/dev/pts/6" 17 | ) 18 | 19 | func TestRTUClient(t *testing.T) { 20 | // Diagslave does not support broadcast id. 21 | handler := modbus.NewRTUClientHandler(rtuDevice) 22 | handler.SlaveId = 17 23 | ClientTestAll(t, modbus.NewClient(handler)) 24 | } 25 | 26 | func TestRTUClientAdvancedUsage(t *testing.T) { 27 | handler := modbus.NewRTUClientHandler(rtuDevice) 28 | handler.BaudRate = 19200 29 | handler.DataBits = 8 30 | handler.Parity = "E" 31 | handler.StopBits = 1 32 | handler.SlaveId = 11 33 | handler.Logger = log.New(os.Stdout, "rtu: ", log.LstdFlags) 34 | err := handler.Connect() 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | defer handler.Close() 39 | 40 | client := modbus.NewClient(handler) 41 | results, err := client.ReadDiscreteInputs(15, 2) 42 | if err != nil || results == nil { 43 | t.Fatal(err, results) 44 | } 45 | results, err = client.ReadWriteMultipleRegisters(0, 2, 2, 2, []byte{1, 2, 3, 4}) 46 | if err != nil || results == nil { 47 | t.Fatal(err, results) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/tcpclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Quoc-Viet Nguyen. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | package test 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "testing" 11 | "time" 12 | 13 | "github.com/goburrow/modbus" 14 | ) 15 | 16 | const ( 17 | tcpDevice = "localhost:5020" 18 | ) 19 | 20 | func TestTCPClient(t *testing.T) { 21 | client := modbus.TCPClient(tcpDevice) 22 | ClientTestAll(t, client) 23 | } 24 | 25 | func TestTCPClientAdvancedUsage(t *testing.T) { 26 | handler := modbus.NewTCPClientHandler(tcpDevice) 27 | handler.Timeout = 5 * time.Second 28 | handler.SlaveId = 1 29 | handler.Logger = log.New(os.Stdout, "tcp: ", log.LstdFlags) 30 | handler.Connect() 31 | defer handler.Close() 32 | 33 | client := modbus.NewClient(handler) 34 | results, err := client.ReadDiscreteInputs(15, 2) 35 | if err != nil || results == nil { 36 | t.Fatal(err, results) 37 | } 38 | results, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4}) 39 | if err != nil || results == nil { 40 | t.Fatal(err, results) 41 | } 42 | results, err = client.WriteMultipleCoils(5, 10, []byte{4, 3}) 43 | if err != nil || results == nil { 44 | t.Fatal(err, results) 45 | } 46 | } 47 | --------------------------------------------------------------------------------