├── .gitignore ├── .travis.yml ├── README.md ├── api.go ├── client.go ├── client_test.go ├── error.go ├── error_test.go ├── pdu.go ├── pdu_test.go ├── tcp.go ├── tcp_test.go ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | - 1.3 6 | - 1.4 7 | 8 | before_install: 9 | - go get github.com/axw/gocov/gocov 10 | - go get github.com/mattn/goveralls 11 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 12 | 13 | script: 14 | - go test -v && $HOME/gopath/bin/goveralls -service=travis-ci 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-modbus 2 | 3 | [![Build Status](https://travis-ci.org/flosse/go-modbus.svg?branch=master)](https://travis-ci.org/flosse/go-modbus) 4 | [![GoDoc](https://godoc.org/github.com/flosse/go-modbus?status.svg)](https://godoc.org/github.com/flosse/go-modbus) 5 | [![Coverage Status](https://coveralls.io/repos/flosse/go-modbus/badge.svg?branch=master)](https://coveralls.io/r/flosse/go-modbus?branch=master) 6 | 7 | **DONT USE IT**: This was a proof-of-concept! 8 | 9 | a free [Modbus](http://en.wikipedia.org/wiki/Modbus) library 10 | for [Go](http://golang.org/). 11 | 12 | ## Usage 13 | 14 | ### Modbus Master (Client) 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/flosse/go-modbus" 22 | ) 23 | 24 | func main(){ 25 | master := modbus.NewTcpClient("127.0.0.1", 502) 26 | } 27 | ``` 28 | 29 | #### High Level API 30 | 31 | ```go 32 | /* read-only */ 33 | di := master.DiscreteInput(7) 34 | state, err := di.Test() 35 | 36 | /* read-write */ 37 | coil := master.Coil(2) 38 | state, err = coil.Test() 39 | err = coil.Set() 40 | err = coil.Clear() 41 | err = coil.Toggle() 42 | 43 | /* read-only */ 44 | roRegister := master.InputRegister(0x2000) 45 | value, err := roRegister.Read() 46 | 47 | /* multiple ro registers */ 48 | multRoRegister := master.InputRegisters(0x1000,7) 49 | values, err := roRegister.Read() 50 | myString, err := multRoRegister.ReadString() 51 | 52 | /* read-write */ 53 | register := master.HoldingRegister(0x0900) 54 | value, err := register.Read() 55 | err = register.Write(0x435) 56 | 57 | /* multiple rw registers */ 58 | multRwRegisters := master.HoldingRegisters(0x9000, 3) 59 | values, err := multRwRegisters.Read() 60 | aString, err := multRwRegisters.ReadString() 61 | err := multRwRegisters.Write(uint16{3,2,1}) 62 | err = multRwRegisters.WriteString("foo") 63 | ``` 64 | 65 | #### Low Level API 66 | 67 | ```go 68 | /* Bit access */ 69 | 70 | // read three read-only bits 71 | res, err := master.ReadDiscreteInputs(0x0800, 3) 72 | // res could be [true, false, false] 73 | 74 | // read 5 read-write bits 75 | res, err = master.ReadCoils(0x02, 2) 76 | // res could be [false, true] 77 | 78 | // set the coil at address 0x0734 79 | err = master.WriteSingleCoil(0x734, true) 80 | 81 | // set/clear multiple coils at address 0x0002 82 | err = master.WriteMultipleCoils(2, []bool{false, true, true}) 83 | 84 | /* 16 bits access */ 85 | 86 | // read three read-only registers 87 | res, err = master.ReadInputRegisters(0x12, 3) 88 | // res could be [334, 912, 0] 89 | 90 | // read two read-write registers 91 | res, err = master.ReadHoldingRegisters(0x00, 2) 92 | // res could be [9, 42] 93 | 94 | // write a value to a single register 95 | err = master.WriteSingleRegister(0x07, 9923) 96 | 97 | // write values to multiple registers 98 | err = master.WriteMultipleRegisters(0x03, []uint16{9,0,66}) 99 | 100 | // read two and write three values within one transaction 101 | res, err = master.ReadWriteMultipleRegisters(0x0065, 2, 0x0800, []uint16{0,7,33}) 102 | // res could be [0, 88] 103 | ``` 104 | 105 | ## Run Tests 106 | 107 | go get github.com/smartystreets/goconvey 108 | go test 109 | 110 | or run 111 | 112 | $GOPATH/bin/goconvey 113 | 114 | and open `http://localhost:8080` in your browser 115 | 116 | ## License 117 | 118 | This library is licensed under the MIT license 119 | 120 | ## Credits 121 | 122 | This library is inspired by [this modbus library](https://github.com/goburrow/modbus). 123 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 - 2015, Markus Kohlhase 3 | * 4 | * Modbus API 5 | */ 6 | 7 | package modbus 8 | 9 | type Transporter interface { 10 | Connect() error 11 | Close() error 12 | Send(pdu *Pdu) (*Pdu, error) 13 | } 14 | 15 | type Client interface { 16 | 17 | /* Access to transporter layer */ 18 | 19 | Transporter() Transporter 20 | 21 | /************** 22 | * Bit access * 23 | **************/ 24 | 25 | /* Physical Discrete Inputs */ 26 | 27 | // Function Code 2 28 | ReadDiscreteInputs(address, quantity uint16) (inputs []bool, err error) 29 | 30 | /* Internal Bits Or Physical Coils */ 31 | 32 | // Function Code 1 33 | ReadCoils(address, quantity uint16) (coils []bool, err error) 34 | 35 | // Function Code 5 36 | WriteSingleCoil(address uint16, coil bool) error 37 | 38 | // Function Code 15 39 | WriteMultipleCoils(address uint16, coils []bool) error 40 | 41 | /****************** 42 | * 16 bits access * 43 | ******************/ 44 | 45 | /* Physical Input Registers */ 46 | 47 | // Function Code 4 48 | ReadInputRegisters(address, quantity uint16) (readRegisters []uint16, err error) 49 | 50 | // Function Code 3 51 | ReadHoldingRegisters(address, quantity uint16) (readRegisters []uint16, err error) 52 | 53 | // Function Code 6 54 | WriteSingleRegister(address, value uint16) error 55 | 56 | // Function Code 16 57 | WriteMultipleRegisters(address uint16, values []uint16) error 58 | 59 | // Function Code 23 60 | ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress uint16, values []uint16) (readRegisters []uint16, err error) 61 | 62 | // Function Code 22 63 | MaskWriteRegister(address, andMask, orMask uint16) error 64 | 65 | // Function Code 24 66 | ReadFifoQueue(address uint16) (values []uint16, err error) 67 | 68 | /********************** 69 | * File record access * 70 | **********************/ 71 | 72 | // TODO: specify methods 73 | 74 | } 75 | 76 | type SerialClient interface { 77 | 78 | // Embed general client API 79 | Client 80 | 81 | /*************** 82 | * Diagnostics * 83 | **************/ 84 | 85 | // Function Code 7 86 | ReadExceptionStatus() (states []bool, err error) 87 | 88 | // Function Code 8 89 | Diagnostics(subfunction uint16, data []uint16) (response []uint16, err error) 90 | 91 | // Function Code 11 92 | GetCommEventCounter() (status bool, count uint16, err error) 93 | 94 | // TODO: specify method 95 | // Function Code 12 96 | // GetCommEventLog 97 | 98 | // Function Code 17 99 | ReportServerId() (response []byte, err error) 100 | 101 | // TODO: specify method 102 | // Function Code 43 103 | // ReadDeviceIdentification 104 | 105 | } 106 | 107 | type IoClient interface { 108 | 109 | /******************** 110 | * Abstract Objects * 111 | ********************/ 112 | 113 | // Embed general client API 114 | Client 115 | 116 | // Discrete input 117 | DiscreteInput(address uint16) DiscreteInput 118 | 119 | // Coil 120 | Coil(address uint16) Coil 121 | 122 | // Input Register 123 | InputRegister(address uint16) InputRegister 124 | 125 | // Input Registers 126 | InputRegisters(address, count uint16) InputRegisters 127 | 128 | // Holding Register 129 | HoldingRegister(address uint16) HoldingRegister 130 | 131 | // Holding Registers 132 | HoldingRegisters(address, count uint16) HoldingRegisters 133 | } 134 | 135 | type DiscreteInput interface { 136 | Test() (bool, error) 137 | } 138 | 139 | type Coil interface { 140 | DiscreteInput 141 | Set() error 142 | Clear() error 143 | Toggle() error 144 | } 145 | 146 | type InputRegister interface { 147 | Read() (uint16, error) 148 | } 149 | 150 | type InputRegisters interface { 151 | Read() ([]uint16, error) 152 | ReadString() (string, error) 153 | } 154 | 155 | type HoldingRegister interface { 156 | InputRegister 157 | Write(uint16) error 158 | } 159 | 160 | type HoldingRegisters interface { 161 | InputRegisters 162 | Write([]uint16) error 163 | WriteString(s string) error 164 | } 165 | 166 | type Handler interface { 167 | Handle(req *Pdu) (res *Pdu) 168 | } 169 | 170 | type Server interface { 171 | SetHandler(h *Handler) 172 | Start() error 173 | Stop() error 174 | } 175 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 - 2015, Markus Kohlhase 3 | * 4 | * Modbus Master (Client) implementation 5 | */ 6 | 7 | package modbus 8 | 9 | import ( 10 | "encoding/binary" 11 | "fmt" 12 | "math" 13 | ) 14 | 15 | type mbClient struct { 16 | transport Transporter 17 | } 18 | 19 | type io struct { 20 | master Client 21 | address uint16 22 | count uint16 23 | } 24 | 25 | type roBit io 26 | type rwBit io 27 | 28 | type roRegister io 29 | type roRegisters io 30 | type rwRegister io 31 | type rwRegisters io 32 | 33 | func (c *mbClient) request(f uint8, addr uint16, data []byte) (pdu *Pdu, err error) { 34 | pdu, err = c.transport.Send(&Pdu{f, append(wordsToByteArray(addr), data...)}) 35 | if err != nil { 36 | return 37 | } 38 | if errFn := f + 0x80; errFn == pdu.Function { 39 | return nil, Error{errFn, pdu.Data[0]} 40 | } 41 | return 42 | } 43 | 44 | func (c *mbClient) readRegisters(fn uint8, addr, count uint16) (values []uint16, err error) { 45 | res, err := c.request(fn, addr, wordsToByteArray(count)) 46 | if err != nil { 47 | return 48 | } 49 | values = bytesToWordArray(res.Data[1:]...) 50 | return 51 | } 52 | 53 | func (c *mbClient) ReadDiscreteInputs(addr, count uint16) (result []bool, err error) { 54 | resp, err := c.request(2, addr, wordsToByteArray(count)) 55 | if err != nil { 56 | return 57 | } 58 | result = make([]bool, count) 59 | 60 | inputs := resp.Data[1:] 61 | byteNr := uint(0) 62 | bitNr := uint(0) 63 | 64 | for i := 0; i < int(count); i++ { 65 | result[i] = inputs[byteNr]&(1< 0) 91 | } 92 | } 93 | return r[0:count], nil 94 | } 95 | 96 | func (c *mbClient) WriteSingleCoil(addr uint16, value bool) (err error) { 97 | var set uint8 98 | if value { 99 | set = 0xff 100 | } 101 | _, err = c.request(5, addr, []byte{set, uint8(0)}) 102 | return 103 | } 104 | 105 | func (c *mbClient) WriteMultipleCoils(addr uint16, values []bool) (err error) { 106 | count := len(values) 107 | byteCount := uint(math.Ceil(float64(count) / 8)) 108 | data := make([]byte, 3+byteCount) 109 | 110 | binary.BigEndian.PutUint16(data, uint16(count)) 111 | data[2] = uint8(byteCount) 112 | 113 | byteNr := uint(0) 114 | bitNr := uint8(0) 115 | byteVal := uint8(0) 116 | 117 | for v := 0; v < count; v++ { 118 | if v == count-1 { 119 | data[byteNr+3] = byteVal 120 | break 121 | } 122 | if values[v] { 123 | byteVal |= 1 << bitNr 124 | } 125 | if bitNr > 6 { 126 | data[byteNr+3] = byteVal 127 | byteVal = 0 128 | bitNr = 0 129 | byteNr++ 130 | } else { 131 | bitNr++ 132 | } 133 | } 134 | res, err := c.request(15, addr, data) 135 | if err != nil { 136 | return 137 | } 138 | if c := binary.BigEndian.Uint16(res.Data[2:]); c != uint16(count) { 139 | return fmt.Errorf("%d coils were forced instead of %d", c, count) 140 | } 141 | return 142 | } 143 | 144 | func (c *mbClient) ReadInputRegisters(addr, count uint16) ([]uint16, error) { 145 | return c.readRegisters(4, addr, count) 146 | } 147 | 148 | func (c *mbClient) ReadHoldingRegisters(addr, count uint16) ([]uint16, error) { 149 | return c.readRegisters(3, addr, count) 150 | } 151 | 152 | func (c *mbClient) WriteMultipleRegisters(addr uint16, values []uint16) (err error) { 153 | 154 | regCount := len(values) 155 | byteCount := regCount * 2 156 | data := make([]byte, byteCount+3) 157 | data[0] = uint8(regCount >> 8) 158 | data[1] = uint8(regCount & 0xff) 159 | data[2] = uint8(byteCount) 160 | 161 | for i := 0; i < regCount; i++ { 162 | data[i*2+3] = uint8(values[i] >> 8) 163 | data[i*2+4] = uint8(values[i] & 0xff) 164 | } 165 | _, err = c.request(16, addr, data) 166 | return 167 | } 168 | 169 | func (c *mbClient) WriteSingleRegister(addr uint16, value uint16) (err error) { 170 | _, err = c.request(6, addr, []byte{uint8(value >> 8), uint8(value & 0xff)}) 171 | return 172 | } 173 | 174 | func (c *mbClient) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress uint16, vals []uint16) (values []uint16, err error) { 175 | writeQuantity := len(vals) 176 | data := wordsToByteArray(readQuantity, writeAddress, uint16(writeQuantity)) 177 | data = append(data, uint8(writeQuantity*2)) 178 | data = append(data, wordsToByteArray(vals...)...) 179 | resp, err := c.request(23, readAddress, data) 180 | if err != nil { 181 | return 182 | } 183 | byteCount := resp.Data[0] 184 | if byteCount > 0 { 185 | values = bytesToWordArray(resp.Data[1:]...) 186 | } else { 187 | values = []uint16{} 188 | } 189 | return 190 | } 191 | 192 | func (c *mbClient) MaskWriteRegister(addr, and, or uint16) (err error) { 193 | _, err = c.request(22, addr, wordsToByteArray(and, or)) 194 | return 195 | } 196 | 197 | func (c *mbClient) ReadFifoQueue(addr uint16) (fifoValues []uint16, err error) { 198 | resp, err := c.request(24, addr, nil) 199 | fifoValues = bytesToWordArray(resp.Data[4:]...) 200 | return 201 | } 202 | 203 | func (c *mbClient) ReadExceptionStatus() (states []bool, err error) { 204 | pdu, err := c.transport.Send(&Pdu{7, nil}) 205 | if err != nil { 206 | return 207 | } 208 | 209 | states = make([]bool, 8) 210 | 211 | for bit := 0; bit < 8; bit++ { 212 | states[bit] = bool((pdu.Data[0] & (1 << uint(bit))) > 0) 213 | } 214 | return 215 | } 216 | 217 | func (c *mbClient) Diagnostics(subfunction uint16, data []uint16) (result []uint16, err error) { 218 | resp, err := c.request(8, subfunction, wordsToByteArray(data...)) 219 | if err != nil { 220 | return 221 | } 222 | return bytesToWordArray(resp.Data[2:]...), nil 223 | } 224 | 225 | func (c *mbClient) GetCommEventCounter() (status bool, count uint16, err error) { 226 | pdu, err := c.transport.Send(&Pdu{11, nil}) 227 | if err != nil { 228 | return 229 | } 230 | res := bytesToWordArray(pdu.Data...) 231 | return (res[0] > 0), res[1], err 232 | } 233 | 234 | func (c *mbClient) ReportServerId() (response []byte, err error) { 235 | pdu, err := c.transport.Send(&Pdu{17, nil}) 236 | if err != nil { 237 | return 238 | } 239 | return pdu.Data, nil 240 | } 241 | 242 | func (c *mbClient) DiscreteInput(addr uint16) DiscreteInput { 243 | return &roBit{master: c, address: addr} 244 | } 245 | 246 | func (c *mbClient) Coil(addr uint16) Coil { 247 | return &rwBit{master: c, address: addr} 248 | } 249 | 250 | func (c *mbClient) InputRegister(addr uint16) InputRegister { 251 | return &roRegister{master: c, address: addr} 252 | } 253 | 254 | func (c *mbClient) InputRegisters(addr, count uint16) InputRegisters { 255 | return &roRegisters{c, addr, count} 256 | } 257 | 258 | func (c *mbClient) HoldingRegister(addr uint16) HoldingRegister { 259 | return &rwRegister{master: c, address: addr} 260 | } 261 | 262 | func (c *mbClient) HoldingRegisters(addr, count uint16) HoldingRegisters { 263 | return &rwRegisters{c, addr, count} 264 | } 265 | 266 | func (io *roBit) Test() (result bool, err error) { 267 | res, err := io.master.ReadDiscreteInputs(io.address, 1) 268 | if err != nil { 269 | return 270 | } 271 | return res[0], nil 272 | } 273 | 274 | func (io *rwBit) Test() (result bool, err error) { 275 | res, err := io.master.ReadCoils(io.address, 1) 276 | if err != nil { 277 | return 278 | } 279 | return res[0], err 280 | } 281 | 282 | func (io *rwBit) Set() (err error) { 283 | return io.master.WriteSingleCoil(io.address, true) 284 | } 285 | 286 | func (io *rwBit) Clear() (err error) { 287 | return io.master.WriteSingleCoil(io.address, false) 288 | } 289 | 290 | func (io *rwBit) Toggle() (err error) { 291 | state, err := io.Test() 292 | if err != nil { 293 | return 294 | } 295 | if state { 296 | return io.Clear() 297 | } 298 | return io.Set() 299 | } 300 | 301 | func (io *roRegister) Read() (value uint16, err error) { 302 | if res, err := io.master.ReadInputRegisters(io.address, 1); err == nil { 303 | value = res[0] 304 | } 305 | return 306 | } 307 | 308 | func (io *rwRegister) Read() (value uint16, err error) { 309 | if res, err := io.master.ReadHoldingRegisters(io.address, 1); err == nil { 310 | value = res[0] 311 | } 312 | return 313 | } 314 | 315 | func (io *rwRegister) Write(value uint16) (err error) { 316 | return io.master.WriteSingleRegister(io.address, value) 317 | } 318 | 319 | func (io *roRegisters) Read() (values []uint16, err error) { 320 | return io.master.ReadInputRegisters(io.address, io.count) 321 | } 322 | 323 | func (io *roRegisters) ReadString() (s string, err error) { 324 | words, err := io.Read() 325 | if err != nil { 326 | return 327 | } 328 | s = string(filterNullChar(wordsToByteArray(words...))) 329 | return 330 | } 331 | 332 | func (io *rwRegisters) Read() (values []uint16, err error) { 333 | return io.master.ReadHoldingRegisters(io.address, io.count) 334 | } 335 | 336 | func (io *rwRegisters) Write(values []uint16) (err error) { 337 | if l := len(values); l > int(io.count) { 338 | return fmt.Errorf("Invalid length of words %d", l) 339 | } 340 | return io.master.WriteMultipleRegisters(io.address, values) 341 | } 342 | 343 | func (io *rwRegisters) ReadString() (s string, err error) { 344 | words, err := io.Read() 345 | if err != nil { 346 | return 347 | } 348 | s = string(filterNullChar(wordsToByteArray(words...))) 349 | return 350 | } 351 | 352 | func (io *rwRegisters) WriteString(s string) (err error) { 353 | return io.Write(bytesToWordArray([]byte(s)...)) 354 | } 355 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | type dummyTransporter struct { 9 | 10 | // define a dummy response data 11 | resData []byte 12 | 13 | // cache current request 14 | req *Pdu 15 | 16 | // define a dummy response method 17 | send func(pdu *Pdu) (*Pdu, error) 18 | } 19 | 20 | func (t *dummyTransporter) Connect() error { 21 | return nil 22 | } 23 | 24 | func (t *dummyTransporter) Close() error { 25 | return nil 26 | } 27 | 28 | func (t *dummyTransporter) Send(pdu *Pdu) (resp *Pdu, err error) { 29 | 30 | t.req = pdu 31 | 32 | if t.send != nil { 33 | resp, err = t.send(pdu) 34 | t.resData = resp.Data 35 | return 36 | } 37 | 38 | return &Pdu{pdu.Function, t.resData}, nil 39 | 40 | } 41 | 42 | func getClient(respData []byte, send func(pdu *Pdu) (*Pdu, error)) (Client, *dummyTransporter) { 43 | t := &dummyTransporter{respData, nil, send} 44 | return &mbClient{t}, t 45 | } 46 | 47 | func getSerialClient(respData []byte, send func(pdu *Pdu) (*Pdu, error)) (SerialClient, *dummyTransporter) { 48 | t := &dummyTransporter{respData, nil, send} 49 | return &mbClient{t}, t 50 | } 51 | 52 | func getIoClient(respData []byte, send func(pdu *Pdu) (*Pdu, error)) (IoClient, *dummyTransporter) { 53 | t := &dummyTransporter{respData, nil, send} 54 | return &mbClient{t}, t 55 | } 56 | 57 | func Test_Client(t *testing.T) { 58 | 59 | Convey("Given a client", t, func() { 60 | c, d := getClient(nil, nil) 61 | 62 | // FUNCTION NR 1 63 | Convey("when reading coils", func() { 64 | 65 | c, d := getClient([]byte{0x03, 0xcd, 0x6b, 0x05}, nil) 66 | 67 | result, _ := c.ReadCoils(19, 19) 68 | 69 | Convey("the function nr should be 1", func() { 70 | So(d.req.Function, ShouldEqual, 1) 71 | }) 72 | 73 | Convey("the first two data byte should encode the address", func() { 74 | So(d.req.Data[0], ShouldEqual, 0x00) // Hi 75 | So(d.req.Data[1], ShouldEqual, 0x13) // Lo 76 | }) 77 | 78 | Convey("the following two data byte should encode the quantity", func() { 79 | So(d.req.Data[2], ShouldEqual, 0x00) // Hi 80 | So(d.req.Data[3], ShouldEqual, 0x13) // Lo 81 | }) 82 | 83 | Convey("the response should be an array of coil states", func() { 84 | So(len(result), ShouldEqual, 19) 85 | So(result[11], ShouldEqual, true) // register 36 86 | So(result[12], ShouldEqual, false) // register 37 87 | So(result[13], ShouldEqual, true) // register 38 88 | }) 89 | 90 | }) 91 | 92 | // FUNCTION NR 2 93 | Convey("when reading discrete inputs", func() { 94 | 95 | c, d = getClient([]byte{0x03, 0xac, 0xdb, 0x35}, nil) 96 | 97 | result, _ := c.ReadDiscreteInputs(196, 22) 98 | 99 | Convey("the function nr should be 2", func() { 100 | So(d.req.Function, ShouldEqual, 2) 101 | }) 102 | 103 | Convey("the response should be an array of input states", func() { 104 | So(len(result), ShouldEqual, 22) 105 | So(result[16], ShouldEqual, true) 106 | So(result[17], ShouldEqual, false) 107 | So(result[18], ShouldEqual, true) 108 | So(result[19], ShouldEqual, false) 109 | So(result[20], ShouldEqual, true) 110 | So(result[21], ShouldEqual, true) 111 | }) 112 | 113 | }) 114 | 115 | // FUNCTION NR 3 116 | Convey("when reading holding registers", func() { 117 | 118 | c, d = getClient([]byte{0x06, 0x02, 0x2b, 0x00, 0x00, 0x00, 0x64}, nil) 119 | 120 | result, _ := c.ReadHoldingRegisters(107, 3) 121 | 122 | Convey("the function nr should be 3", func() { 123 | So(d.req.Function, ShouldEqual, 3) 124 | }) 125 | 126 | Convey("the response should be an array of register values", func() { 127 | So(len(result), ShouldEqual, 3) 128 | So(result[0], ShouldEqual, 555) 129 | So(result[1], ShouldEqual, 0) 130 | So(result[2], ShouldEqual, 100) 131 | }) 132 | 133 | }) 134 | 135 | // FUNCTION NR 4 136 | Convey("when reading input registers", func() { 137 | 138 | c, d = getClient([]byte{0x02, 0x00, 0x0a}, nil) 139 | 140 | result, _ := c.ReadInputRegisters(8, 1) 141 | 142 | Convey("the function nr should be 4", func() { 143 | So(d.req.Function, ShouldEqual, 4) 144 | }) 145 | 146 | Convey("the response should be an array of register values", func() { 147 | So(len(result), ShouldEqual, 1) 148 | So(result[0], ShouldEqual, 10) 149 | }) 150 | 151 | }) 152 | 153 | // FUNCTION NR 5 154 | Convey("when writing a single coil", func() { 155 | 156 | c, d = getClient([]byte{0x00, 0xac, 0xff, 0x00}, nil) 157 | 158 | c.WriteSingleCoil(172, true) 159 | 160 | Convey("the function nr should be 5", func() { 161 | So(d.req.Function, ShouldEqual, 5) 162 | }) 163 | 164 | Convey("the last word of the request should be 0xff00", func() { 165 | So(d.req.Data[2], ShouldEqual, 0xff) // Hi 166 | So(d.req.Data[3], ShouldEqual, 0x00) // Lo 167 | }) 168 | 169 | }) 170 | 171 | // FUNCTION NR 6 172 | Convey("when writing a single register", func() { 173 | 174 | c, d = getClient([]byte{0x00, 0x01, 0x00, 0x03}, nil) 175 | 176 | c.WriteSingleRegister(1, 3) 177 | 178 | Convey("the function nr should be 6", func() { 179 | So(d.req.Function, ShouldEqual, 6) 180 | }) 181 | 182 | Convey("the register values should be encoded as big endian binary", func() { 183 | So(d.req.Data[2], ShouldEqual, 0x00) // Hi 184 | So(d.req.Data[3], ShouldEqual, 0x03) // Lo 185 | }) 186 | }) 187 | }) 188 | 189 | Convey("Given a serial client", t, func() { 190 | 191 | // FUNCTION NR 7 (serial line only) 192 | Convey("when reading the exception status", func() { 193 | 194 | c, d := getSerialClient([]byte{0x6d}, nil) 195 | 196 | result, _ := c.ReadExceptionStatus() 197 | 198 | Convey("the function nr should be 7", func() { 199 | So(d.req.Function, ShouldEqual, 7) 200 | }) 201 | 202 | Convey("the request data should be nil", func() { 203 | So(d.req.Data, ShouldBeNil) 204 | }) 205 | 206 | Convey("response should be an array of boolean states", func() { 207 | So(result[0], ShouldEqual, true) 208 | So(result[1], ShouldEqual, false) 209 | So(result[2], ShouldEqual, true) 210 | So(result[3], ShouldEqual, true) 211 | So(result[4], ShouldEqual, false) 212 | So(result[5], ShouldEqual, true) 213 | So(result[6], ShouldEqual, true) 214 | So(result[7], ShouldEqual, false) 215 | }) 216 | 217 | }) 218 | 219 | // FUNCTION NR 8 (serial line only) 220 | Convey("when reading the diagnostics", func() { 221 | 222 | c, d := getSerialClient(nil, func(pdu *Pdu) (*Pdu, error) { 223 | return pdu, nil 224 | }) 225 | 226 | result, _ := c.Diagnostics(0, []uint16{0xa537}) 227 | 228 | Convey("the function nr should be 8", func() { 229 | So(d.req.Function, ShouldEqual, 8) 230 | }) 231 | 232 | Convey("the request data of sub-function 0 should exist", func() { 233 | So(d.req.Data[0], ShouldEqual, 0) 234 | So(d.req.Data[2], ShouldEqual, 0xa5) 235 | So(d.req.Data[3], ShouldEqual, 0x37) 236 | }) 237 | 238 | Convey("response of sub-function 0 should echo the request data", func() { 239 | So(result[0], ShouldEqual, 42295) 240 | }) 241 | 242 | }) 243 | 244 | // FUNCTION NR 11 (serial line only) 245 | Convey("when receiving the comm event counter", func() { 246 | 247 | c, d := getSerialClient([]byte{0xff, 0xff, 0x01, 0x08}, nil) 248 | 249 | state, count, _ := c.GetCommEventCounter() 250 | 251 | Convey("the function nr should be 11", func() { 252 | So(d.req.Function, ShouldEqual, 11) 253 | }) 254 | 255 | Convey("the state and count values should be decoded", func() { 256 | So(state, ShouldEqual, true) 257 | So(count, ShouldEqual, 264) 258 | }) 259 | 260 | }) 261 | }) 262 | 263 | Convey("Given an io client", t, func() { 264 | 265 | Convey("when creating a coil", func() { 266 | io, d := getIoClient([]byte{0x01, 0x01}, nil) 267 | coil := io.Coil(3) 268 | 269 | Convey("the test method should use function nr 1", func() { 270 | _, err := coil.Test() 271 | So(d.req.Function, ShouldEqual, 1) 272 | So(err, ShouldBeNil) 273 | }) 274 | 275 | Convey("the test method should read the coil state", func() { 276 | x, _ := coil.Test() 277 | So(d.req.Data[1], ShouldEqual, 0x03) 278 | So(d.req.Data[2], ShouldEqual, 0x00) 279 | So(d.req.Data[3], ShouldEqual, 0x01) 280 | So(x, ShouldEqual, true) 281 | }) 282 | 283 | Convey("the set method should write 0xff00", func() { 284 | coil.Set() 285 | So(d.req.Function, ShouldEqual, 5) 286 | So(d.req.Data[1], ShouldEqual, 0x03) 287 | So(d.req.Data[2], ShouldEqual, 0xff) 288 | So(d.req.Data[3], ShouldEqual, 0x00) 289 | }) 290 | 291 | Convey("the clear method should write 0x0000", func() { 292 | coil.Clear() 293 | So(d.req.Function, ShouldEqual, 5) 294 | So(d.req.Data[2], ShouldEqual, 0x00) 295 | }) 296 | 297 | Convey("the toggle method should invert the state", func() { 298 | io, d = getIoClient([]byte{0x01, 0x01}, nil) 299 | coil = io.Coil(0) 300 | coil.Toggle() 301 | So(d.req.Function, ShouldEqual, 5) 302 | So(d.req.Data[2], ShouldEqual, 0x00) 303 | io, d = getIoClient([]byte{0x01, 0x00}, nil) 304 | coil = io.Coil(0) 305 | coil.Toggle() 306 | So(d.req.Data[2], ShouldEqual, 0xff) 307 | }) 308 | 309 | }) 310 | 311 | Convey("when creating a discrete input", func() { 312 | io, d := getIoClient([]byte{0x02, 0xdf}, nil) 313 | di := io.DiscreteInput(3) 314 | 315 | Convey("the test method should use function nr 2", func() { 316 | _, err := di.Test() 317 | So(d.req.Function, ShouldEqual, 2) 318 | So(err, ShouldBeNil) 319 | }) 320 | 321 | Convey("the test method should read the coil state", func() { 322 | x, _ := di.Test() 323 | So(x, ShouldEqual, true) 324 | }) 325 | 326 | }) 327 | 328 | Convey("when creating a holding register", func() { 329 | io, d := getIoClient([]byte{0x02, 0xda, 0x45}, nil) 330 | reg := io.HoldingRegister(0) 331 | 332 | Convey("the read method should use function nr 3", func() { 333 | _, err := reg.Read() 334 | So(d.req.Function, ShouldEqual, 3) 335 | So(err, ShouldBeNil) 336 | }) 337 | 338 | Convey("the read method should read the value", func() { 339 | v, _ := reg.Read() 340 | So(d.req.Function, ShouldEqual, 3) 341 | So(v, ShouldEqual, 55877) 342 | }) 343 | 344 | Convey("the write method should use function nr 6", func() { 345 | err := reg.Write(0) 346 | So(d.req.Function, ShouldEqual, 6) 347 | So(err, ShouldBeNil) 348 | }) 349 | 350 | Convey("the write method should write the value", func() { 351 | reg.Write(1234) 352 | So(d.req.Data[2], ShouldEqual, 0x04) 353 | So(d.req.Data[3], ShouldEqual, 0xd2) 354 | }) 355 | }) 356 | 357 | Convey("when creating an input register", func() { 358 | io, d := getIoClient([]byte{0x02, 0xda, 0x45}, nil) 359 | reg := io.InputRegister(7) 360 | 361 | Convey("the read method should use function nr 4", func() { 362 | x, err := reg.Read() 363 | So(d.req.Function, ShouldEqual, 4) 364 | So(err, ShouldBeNil) 365 | So(x, ShouldEqual, 55877) 366 | }) 367 | }) 368 | 369 | Convey("when creating a multi input registers", func() { 370 | io, d := getIoClient([]byte{0x02, 0x66, 0x6f, 0x6f}, nil) 371 | reg := io.InputRegisters(0x1000, 2) 372 | 373 | Convey("the read method should use function nr 4", func() { 374 | x, err := reg.Read() 375 | So(d.req.Function, ShouldEqual, 4) 376 | So(err, ShouldBeNil) 377 | So(x[0], ShouldEqual, 0x666f) 378 | So(x[1], ShouldEqual, 0x6f) 379 | }) 380 | 381 | Convey("the registers can be read as string", func() { 382 | x, _ := reg.ReadString() 383 | So(x, ShouldResemble, "foo") 384 | }) 385 | }) 386 | 387 | Convey("when creating a multi holting registers", func() { 388 | io, d := getIoClient([]byte{0x02, 0x00, 0x05, 0x00, 0x03}, nil) 389 | reg := io.HoldingRegisters(0x1000, 2) 390 | 391 | Convey("the write method should use function nr x", func() { 392 | err := reg.Write([]uint16{3, 5}) 393 | So(d.req.Function, ShouldEqual, 16) 394 | So(err, ShouldBeNil) 395 | }) 396 | 397 | Convey("the write method should check the word length", func() { 398 | err := reg.Write([]uint16{3, 5, 6}) 399 | So(err, ShouldNotBeNil) 400 | }) 401 | }) 402 | }) 403 | } 404 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 - 2015, Markus Kohlhase 3 | */ 4 | 5 | package modbus 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | /* Modbus Error */ 12 | 13 | type Error struct { 14 | 15 | // Error Code 16 | Code uint8 17 | 18 | // Exception Code 19 | Exception uint8 20 | } 21 | 22 | func getExceptionMessage(nr uint8) string { 23 | switch nr { 24 | case 0x01: 25 | return "ILLEGAL FUNCTION" 26 | case 0x02: 27 | return "ILLEGAL DATA ADDRESS" 28 | case 0x03: 29 | return "ILLEGAL DATA VALUE" 30 | case 0x04: 31 | return "SERVER DEVICE FAILURE" 32 | case 0x05: 33 | return "ACKNOWLEDGE" 34 | case 0x06: 35 | return "SERVER DEVICE BUSY" 36 | case 0x08: 37 | return "MEMORY PARITY ERROR" 38 | case 0x0A: 39 | return "GATEWAY PATH UNAVAILABLE" 40 | case 0x0B: 41 | return "GATEWAY TARGET DEVICE FAILED TO RESPOND" 42 | 43 | default: 44 | return "UNKNOWN EXCEPTION" 45 | } 46 | } 47 | 48 | func (e Error) Error() string { 49 | return fmt.Sprintf("Error %d (Function %d); Exception %d ('%s')", e.Code, (e.Code - 128), e.Exception, getExceptionMessage(e.Exception)) 50 | } 51 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func Test_Error(t *testing.T) { 9 | 10 | Convey("Given an modbus error struct", t, func() { 11 | err := &Error{129, 0x01} 12 | 13 | Convey("it should print a human readable message", func() { 14 | So(err.Error(), ShouldEqual, "Error 129 (Function 1); Exception 1 ('ILLEGAL FUNCTION')") 15 | }) 16 | }) 17 | 18 | Convey("Given an exception nr", t, func() { 19 | Convey("it should print the exception message", func() { 20 | So(getExceptionMessage(1), ShouldEqual, "ILLEGAL FUNCTION") 21 | So(getExceptionMessage(2), ShouldEqual, "ILLEGAL DATA ADDRESS") 22 | So(getExceptionMessage(3), ShouldEqual, "ILLEGAL DATA VALUE") 23 | So(getExceptionMessage(4), ShouldEqual, "SERVER DEVICE FAILURE") 24 | So(getExceptionMessage(5), ShouldEqual, "ACKNOWLEDGE") 25 | So(getExceptionMessage(6), ShouldEqual, "SERVER DEVICE BUSY") 26 | So(getExceptionMessage(8), ShouldEqual, "MEMORY PARITY ERROR") 27 | So(getExceptionMessage(10), ShouldEqual, "GATEWAY PATH UNAVAILABLE") 28 | So(getExceptionMessage(11), ShouldEqual, "GATEWAY TARGET DEVICE FAILED TO RESPOND") 29 | }) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /pdu.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 - 2015, Markus Kohlhase 3 | */ 4 | 5 | package modbus 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | type Pdu struct { 12 | 13 | // Function Code 14 | Function uint8 15 | 16 | // PDU data 17 | Data []byte 18 | } 19 | 20 | const pduLength = 253 21 | 22 | func (pdu *Pdu) pack() (bin []byte, err error) { 23 | if pdu.Function < 1 { 24 | return nil, fmt.Errorf("Invalid function code %d", pdu.Function) 25 | } 26 | if l := len(pdu.Data); l > pduLength-1 { 27 | return nil, fmt.Errorf("Invalid length of data (%d instead of max. %d bytes)", l, pduLength-1) 28 | } 29 | return append([]byte{pdu.Function}, pdu.Data...), nil 30 | } 31 | 32 | func unpackPdu(data []byte) (*Pdu, error) { 33 | if l := len(data); l < 1 { 34 | return nil, fmt.Errorf("Invalid PDU length (%d bytes)", l) 35 | } 36 | return &Pdu{data[0], data[1:]}, nil 37 | } 38 | -------------------------------------------------------------------------------- /pdu_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func Test_Pdu(t *testing.T) { 9 | 10 | Convey("Given a pdu struct", t, func() { 11 | data := []byte{7, 8} 12 | pdu := &Pdu{4, data} 13 | 14 | Convey("When we pack it", func() { 15 | bin, _ := pdu.pack() 16 | 17 | Convey("the length of the binary array should be correct", func() { 18 | So(len(bin), ShouldEqual, 3) 19 | }) 20 | 21 | Convey("the function code should be checked", func() { 22 | _, err := (&Pdu{0, data}).pack() 23 | So(err, ShouldNotBeNil) 24 | 25 | _, err = (&Pdu{1, data}).pack() 26 | So(err, ShouldBeNil) 27 | }) 28 | 29 | Convey("the function code should be encoded", func() { 30 | So(bin[0], ShouldEqual, 4) 31 | }) 32 | 33 | Convey("the data should be added", func() { 34 | So(bin[2], ShouldEqual, 8) 35 | }) 36 | 37 | Convey("the data field can be nil", func() { 38 | b, err := (&Pdu{1, nil}).pack() 39 | So(err, ShouldBeNil) 40 | So(len(b), ShouldEqual, 1) 41 | }) 42 | 43 | Convey("the data length has to be less than 252", func() { 44 | _, err := (&Pdu{1, make([]byte, 253)}).pack() 45 | So(err, ShouldNotBeNil) 46 | 47 | _, err = (&Pdu{1, make([]byte, 252)}).pack() 48 | So(err, ShouldBeNil) 49 | }) 50 | 51 | }) 52 | }) 53 | 54 | Convey("Given a valid binary pdu", t, func() { 55 | bin := []byte{3, 7, 8} 56 | 57 | Convey("When we unpack it", func() { 58 | pdu, err := unpackPdu(bin) 59 | 60 | Convey("we should not get an error", func() { 61 | So(err, ShouldBeNil) 62 | }) 63 | 64 | Convey("the function code should be decoded", func() { 65 | So(pdu.Function, ShouldEqual, 3) 66 | }) 67 | 68 | Convey("the data field should be corret", func() { 69 | So(len(pdu.Data), ShouldEqual, 2) 70 | So(pdu.Data[0], ShouldEqual, 7) 71 | }) 72 | 73 | Convey("the data field can be empty", func() { 74 | pdu, _ := unpackPdu([]byte{1}) 75 | So(pdu.Data, ShouldHaveSameTypeAs, []byte{}) 76 | So(len(pdu.Data), ShouldEqual, 0) 77 | }) 78 | }) 79 | }) 80 | 81 | Convey("Given an invalid binary pdu", t, func() { 82 | 83 | Convey("When we unpack it", func() { 84 | _, err := unpackPdu([]byte{}) 85 | 86 | Convey("we should get an error", func() { 87 | So(err, ShouldNotBeNil) 88 | }) 89 | }) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /tcp.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 - 2015, Markus Kohlhase 3 | */ 4 | 5 | package modbus 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | "net" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | const ( 18 | aduLength = 260 19 | headerLength = 7 20 | tcpProtocolId = 0 21 | ) 22 | 23 | type header struct { 24 | 25 | // Transaction Identifier 26 | transaction uint16 27 | 28 | // Protocol Identifier 29 | protocol uint16 30 | 31 | // PDU Length 32 | length uint16 33 | 34 | // Unit Identifier 35 | unit uint8 36 | } 37 | 38 | type adu struct { 39 | header *header 40 | pdu *Pdu 41 | } 42 | 43 | func (adu *adu) pack() (bin []byte, err error) { 44 | binPdu, err := adu.pdu.pack() 45 | if err != nil { 46 | return 47 | } 48 | bin = append(adu.header.pack(), binPdu...) 49 | return 50 | } 51 | 52 | func (h *header) pack() []byte { 53 | buff := bytes.NewBuffer([]byte{}) 54 | binary.Write(buff, binary.BigEndian, h) 55 | return buff.Bytes() 56 | } 57 | 58 | func unpackHeader(data []byte) (*header, error) { 59 | if l := len(data); l < headerLength { 60 | return nil, fmt.Errorf("Invalid header length: %d byte", l) 61 | } 62 | return &header{ 63 | binary.BigEndian.Uint16(data[0:2]), 64 | binary.BigEndian.Uint16(data[2:4]), 65 | binary.BigEndian.Uint16(data[4:6]), 66 | data[6], 67 | }, nil 68 | } 69 | 70 | func unpackAdu(data []byte) (*adu, error) { 71 | if l := len(data); l < 8 { 72 | return nil, fmt.Errorf("Invalid ADU length: %d byte", l) 73 | } 74 | pdu, err := unpackPdu(data[headerLength:]) 75 | if err != nil { 76 | return nil, err 77 | } 78 | header, err := unpackHeader(data[0:headerLength]) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return &adu{header, pdu}, nil 83 | } 84 | 85 | type tcpTransporter struct { 86 | host string 87 | port uint 88 | conn net.Conn 89 | transaction uint16 90 | id uint8 91 | timeout time.Duration 92 | } 93 | 94 | func (t *tcpTransporter) Connect() error { 95 | address := t.host + ":" + strconv.Itoa(int(t.port)) 96 | if t.timeout > 0 { 97 | conn, err := net.DialTimeout("tcp", address, t.timeout) 98 | t.conn = conn 99 | return err 100 | } else { 101 | conn, err := net.Dial("tcp", address) 102 | t.conn = conn 103 | return err 104 | } 105 | } 106 | 107 | func (t *tcpTransporter) Close() (err error) { 108 | if t.conn != nil { 109 | if err = t.conn.Close(); err != nil { 110 | return 111 | } 112 | t.conn = nil 113 | return 114 | } 115 | return errors.New("Not connected") 116 | } 117 | 118 | func (t *tcpTransporter) Send(pdu *Pdu) (*Pdu, error) { 119 | if t.conn == nil { 120 | if err := t.Connect(); err != nil { 121 | return nil, err 122 | } 123 | } 124 | t.transaction++ 125 | header := &header{t.transaction, tcpProtocolId, uint16(len(pdu.Data) + 2), t.id} 126 | binAdu, err := (&adu{header, pdu}).pack() 127 | if err != nil { 128 | return nil, err 129 | } 130 | if _, err := t.conn.Write(binAdu); err != nil { 131 | return nil, fmt.Errorf("Could not write data: %s", err) 132 | } 133 | buff := make([]byte, aduLength) 134 | l, err := t.conn.Read(buff) 135 | if err != nil { 136 | return nil, fmt.Errorf("Could not receive data: %s", err) 137 | } 138 | res, err := unpackAdu(buff[:l]) 139 | if err != nil { 140 | return nil, fmt.Errorf("Could not read PDU: %s", err) 141 | } 142 | if i := res.header.transaction; i != t.transaction { 143 | return nil, fmt.Errorf("Invalid transaction id: %d instead of %d", i, t.transaction) 144 | } 145 | return res.pdu, nil 146 | } 147 | 148 | func NewTcpClient(host string, port uint) IoClient { 149 | return &mbClient{&tcpTransporter{host: host, port: port}} 150 | } 151 | 152 | func NewTcpClientTimeout(host string, port uint, timeout time.Duration) IoClient { 153 | return &mbClient{&tcpTransporter{host: host, port: port, timeout: timeout}} 154 | } 155 | -------------------------------------------------------------------------------- /tcp_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/binary" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "net" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type dummyAddr struct{} 12 | 13 | func (a *dummyAddr) Network() string { 14 | return "" 15 | } 16 | 17 | func (a *dummyAddr) String() string { 18 | return "" 19 | } 20 | 21 | type dummyConn struct { 22 | respond func([]byte) (int, error) 23 | } 24 | 25 | func (c *dummyConn) Read(b []byte) (n int, err error) { 26 | if c.respond != nil { 27 | return c.respond(b) 28 | } 29 | return 0, nil 30 | } 31 | 32 | func (c *dummyConn) Write(b []byte) (n int, err error) { 33 | return 0, nil 34 | 35 | } 36 | 37 | func (c *dummyConn) Close() error { 38 | return nil 39 | } 40 | 41 | func (c *dummyConn) LocalAddr() net.Addr { 42 | return &dummyAddr{} 43 | } 44 | 45 | func (c *dummyConn) RemoteAddr() net.Addr { 46 | return &dummyAddr{} 47 | } 48 | 49 | func (c *dummyConn) SetDeadline(t time.Time) error { 50 | return nil 51 | } 52 | 53 | func (c *dummyConn) SetReadDeadline(t time.Time) error { 54 | return nil 55 | } 56 | 57 | func (c *dummyConn) SetWriteDeadline(t time.Time) error { 58 | return nil 59 | } 60 | 61 | func Test_Tcp(t *testing.T) { 62 | Convey("Given a header struct", t, func() { 63 | header := &header{1234, 99, 42, 9} 64 | 65 | Convey("When we pack it", func() { 66 | bin := header.pack() 67 | 68 | Convey("The length of the binary array should be 7", func() { 69 | So(len(bin), ShouldEqual, 7) 70 | }) 71 | 72 | Convey("The transaction number should be encoded as BigEndian uint16", func() { 73 | So(binary.BigEndian.Uint16(bin[0:2]), ShouldEqual, 1234) 74 | }) 75 | 76 | Convey("The protocol id should be encoded as BigEndian uint16", func() { 77 | So(binary.BigEndian.Uint16(bin[2:4]), ShouldEqual, 99) 78 | }) 79 | 80 | Convey("The pdu length should be encoded as BigEndian uint16", func() { 81 | So(binary.BigEndian.Uint16(bin[4:6]), ShouldEqual, 42) 82 | }) 83 | 84 | Convey("The uni id should be the last byte", func() { 85 | So(bin[6], ShouldEqual, 9) 86 | }) 87 | }) 88 | }) 89 | 90 | Convey("Given a invalid binary header", t, func() { 91 | header := []byte{0, 0, 0, 0, 0, 0} 92 | 93 | Convey("When we unpack it", func() { 94 | _, err := unpackHeader(header) 95 | 96 | Convey("we should get an error", func() { 97 | So(err, ShouldNotBeNil) 98 | }) 99 | }) 100 | }) 101 | 102 | Convey("Given a valid binary header", t, func() { 103 | header := []byte{0xff, 0xff, 0, 5, 0, 3, 9} 104 | 105 | Convey("When we unpack it", func() { 106 | h, err := unpackHeader(header) 107 | 108 | Convey("we should not get an error", func() { 109 | So(err, ShouldBeNil) 110 | }) 111 | 112 | Convey("the transaction id should be decoded", func() { 113 | So(h.transaction, ShouldEqual, 65535) 114 | }) 115 | 116 | Convey("the protocol id should be decoded", func() { 117 | So(h.protocol, ShouldEqual, 5) 118 | }) 119 | 120 | Convey("the pdu length should be correct", func() { 121 | So(h.length, ShouldEqual, 3) 122 | }) 123 | 124 | Convey("the unit id should be decoded", func() { 125 | So(h.unit, ShouldEqual, 9) 126 | }) 127 | }) 128 | }) 129 | 130 | Convey("Given an adu struct", t, func() { 131 | adu := &adu{&header{1, 2, 3, 4}, &Pdu{6, []byte{2, 4}}} 132 | 133 | Convey("When we pack it", func() { 134 | bin, _ := adu.pack() 135 | 136 | Convey("the byte array length should correct", func() { 137 | So(len(bin), ShouldEqual, 10) 138 | }) 139 | 140 | Convey("the header should be encoded correctly", func() { 141 | So(bin[6], ShouldEqual, 4) 142 | So(bin[3], ShouldEqual, 2) 143 | }) 144 | 145 | Convey("the function code should be correct", func() { 146 | So(bin[7], ShouldEqual, 6) 147 | }) 148 | 149 | Convey("the data code should be included", func() { 150 | So(bin[9], ShouldEqual, 4) 151 | }) 152 | }) 153 | }) 154 | 155 | Convey("Given an invalid binary adu", t, func() { 156 | bin := []byte{0, 0, 0, 0, 0, 0, 0} 157 | 158 | Convey("When we unpack it", func() { 159 | _, err := unpackAdu(bin) 160 | 161 | Convey("we should get an error", func() { 162 | So(err, ShouldNotBeNil) 163 | }) 164 | }) 165 | }) 166 | 167 | Convey("Given a valid binary adu", t, func() { 168 | bin := []byte{0, 0x0f, 0, 5, 0, 3, 9, 4, 2} 169 | 170 | Convey("When we unpack it", func() { 171 | adu, err := unpackAdu(bin) 172 | 173 | Convey("we should not get an error", func() { 174 | So(err, ShouldBeNil) 175 | }) 176 | 177 | Convey("the header should be unpacked", func() { 178 | So(adu.header.transaction, ShouldEqual, 15) 179 | }) 180 | 181 | Convey("the pdu should be unpacked", func() { 182 | So(adu.pdu.Function, ShouldEqual, 4) 183 | So(adu.pdu.Data[0], ShouldEqual, 2) 184 | }) 185 | }) 186 | }) 187 | 188 | Convey("Given a tcpTransporter", t, func() { 189 | tr := &tcpTransporter{host: "foo", port: 502} 190 | tr.conn = &dummyConn{} 191 | 192 | Convey("when sending a pdu", func() { 193 | req := &Pdu{3, nil} 194 | 195 | Convey("the transaction id should be incremented", func() { 196 | tr.Send(req) 197 | So(tr.transaction, ShouldEqual, 1) 198 | }) 199 | 200 | Convey("the received transaction id should be checked", func() { 201 | tr.conn = &dummyConn{respond: func(b []byte) (int, error) { 202 | adu := &adu{ 203 | header: &header{transaction: 2, length: 8}, 204 | pdu: &Pdu{2, nil}, 205 | } 206 | bin, _ := adu.pack() 207 | for idx, v := range bin { 208 | b[idx] = v 209 | } 210 | return 8, nil 211 | }} 212 | _, err := tr.Send(req) 213 | So(tr.transaction, ShouldEqual, 1) 214 | So(err, ShouldNotEqual, nil) 215 | }) 216 | }) 217 | }) 218 | } 219 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | func wordsToByteArray(words ...uint16) []byte { 9 | array := make([]byte, 2*len(words)) 10 | for i, v := range words { 11 | binary.BigEndian.PutUint16(array[i*2:], v) 12 | } 13 | return array 14 | } 15 | 16 | func bytesToWordArray(bytes ...byte) []uint16 { 17 | l := len(bytes) 18 | n := int(math.Ceil(float64(l) / 2)) 19 | array := make([]uint16, n) 20 | for i := 0; i < n; i++ { 21 | j := i * 2 22 | if j+2 > l { 23 | array[i] = uint16(bytes[j]) 24 | } else { 25 | array[i] = binary.BigEndian.Uint16(bytes[j : j+2]) 26 | } 27 | } 28 | return array 29 | } 30 | 31 | func filterNullChar(a []byte) []byte { 32 | x := filter(a, func(c byte) bool { 33 | return c != 0 34 | }) 35 | return x 36 | } 37 | 38 | func filter(s []byte, fn func(byte) bool) []byte { 39 | var p []byte // == nil 40 | for _, v := range s { 41 | if fn(v) { 42 | p = append(p, v) 43 | } 44 | } 45 | return p 46 | } 47 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func Test_Util(t *testing.T) { 9 | 10 | Convey("Given some uint16 words", t, func() { 11 | words := []uint16{5, 2, 3} 12 | 13 | Convey("when converting them into an array of bytes", func() { 14 | bytes := wordsToByteArray(words...) 15 | 16 | Convey("the array length should be twice the amount of words", func() { 17 | So(len(bytes), ShouldEqual, len(words)*2) 18 | }) 19 | 20 | Convey("the words should be encoded as big endian binary", func() { 21 | So(bytes[0], ShouldEqual, 0) 22 | So(bytes[1], ShouldEqual, 5) 23 | }) 24 | }) 25 | }) 26 | 27 | Convey("Given an even amount of bytes", t, func() { 28 | evenBytes := []byte{0x04, 0xd2, 0, 0xf} 29 | 30 | Convey("when converting them into an array of uint16 words", func() { 31 | evenWords := bytesToWordArray(evenBytes...) 32 | 33 | Convey("the array length should be halve the amount of words", func() { 34 | So(len(evenWords), ShouldEqual, len(evenBytes)/2) 35 | }) 36 | 37 | Convey("the words should be decoded as big endian binary", func() { 38 | So(evenWords[0], ShouldEqual, 1234) 39 | So(evenWords[1], ShouldEqual, 15) 40 | }) 41 | }) 42 | }) 43 | 44 | Convey("Given an odd amount of bytes", t, func() { 45 | oddBytes := []byte{0x04, 0xd2, 0x0a} 46 | 47 | Convey("when converting them into an array of uint16 words", func() { 48 | oddWords := bytesToWordArray(oddBytes...) 49 | 50 | Convey("the array length should be halve the amount of words plus one", func() { 51 | So(len(oddWords), ShouldEqual, 1+len(oddBytes)/2) 52 | }) 53 | 54 | Convey("last byte will be translated as uint16 too", func() { 55 | So(oddWords[0], ShouldEqual, 1234) 56 | So(oddWords[1], ShouldEqual, 10) 57 | }) 58 | }) 59 | }) 60 | } 61 | --------------------------------------------------------------------------------