forked from xiegeo/modbusone
/
modbus.go
386 lines (351 loc) · 10.7 KB
/
modbus.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
//Package modbusone provides a Modbus library to implement both server and client
//using one set of APIs.
//
//For sample code, see examples/memory, and handler2serial_test.go
package modbusone
import (
"fmt"
)
//Server is the common interface for all Clients and Servers that use ProtocalHandlers
type Server interface {
Serve(handler ProtocalHandler) error
}
//ProtocalHandler handles PDUs based on if it is a write or read from the local
//perspective.
type ProtocalHandler interface {
//OnInput is called on the server for a write request,
//or on the client for read reply.
//For write to server on server side, data is part of req.
//For read from server on client side, req is the req from client, and
//data is part of reply.
OnWrite(req PDU, data []byte) error
//OnOutput is called on the server for a read request,
//or on the client before write requst.
//For read from server on the server side, req is from client and data is
//part of reply.
//For write to server on the client side, req is from local action
//(such as RTUClient.StartTransaction), and data will be added to req to send
//to server.
OnRead(req PDU) (data []byte, err error)
//OnError is called on the client when it receive a well formed
//error from server
OnError(req PDU, errRep PDU)
}
//FunctionCode Modebus function codes
type FunctionCode byte
//Implemented FunctionCodes
const (
FcReadCoils FunctionCode = 1
FcReadDiscreteInputs FunctionCode = 2
FcReadHoldingRegisters FunctionCode = 3
FcReadInputRegisters FunctionCode = 4
FcWriteSingleCoil FunctionCode = 5
FcWriteSingleRegister FunctionCode = 6
FcWriteMultipleCoils FunctionCode = 15
FcWriteMultipleRegisters FunctionCode = 16
//FcMaskWriteRegister FunctionCode = 22
//FcReadWriteMultipleRegisters FunctionCode = 23
//FcReadFIFOQueue FunctionCode = 24 //not supported for now
)
//Valid test if FunctionCode is a supported function, and not an error response
func (f FunctionCode) Valid() bool {
return (f > 0 && f < 7) || (f > 14 && f < 17) //|| (f > 21 && f < 24)
}
//MaxRange is the largest address in the Modbus protocol.
func (f FunctionCode) MaxRange() uint16 {
return 0xFFFF
}
//MaxPerPacket returns the max number of values a FunctionCode can carry.
func (f FunctionCode) MaxPerPacket() uint16 {
switch f {
case FcReadCoils, FcReadDiscreteInputs:
return 2000
case FcReadHoldingRegisters, FcReadInputRegisters:
return 125 //0x007D
case FcWriteSingleCoil, FcWriteSingleRegister:
return 1
case FcWriteMultipleCoils:
return 0x07B0 //1968
case FcWriteMultipleRegisters:
return 0x007B
}
return 0 //unsupported functions
}
//MaxPerPacketSized returns the max number of values a FunctionCode can carry,
//if we are to further limit PDU packet size from MaxRTUSize.
//At least 1 (8 for bools) is returned if size is too small.
func (f FunctionCode) MaxPerPacketSized(size int) uint16 {
if size > MaxPDUSize {
size = MaxPDUSize
}
if size < 10 {
debugf("warning: PDU packet size is only %v", size)
}
s := uint16(size)
switch f {
case FcReadCoils, FcReadDiscreteInputs:
if s < 4 {
return 8
}
if s == MaxPDUSize {
//one byte is not used even at max
s--
}
q := (s - 2) * 8
return q
case FcReadHoldingRegisters, FcReadInputRegisters:
if s < 6 {
return 1
}
return (s - 2) / 2
case FcWriteSingleCoil, FcWriteSingleRegister:
return 1
case FcWriteMultipleCoils:
if s < 8 {
return 8
}
if s == MaxPDUSize {
s--
}
q := (s - 6) * 8
return q
case FcWriteMultipleRegisters:
if s < 10 {
return 1
}
return (s - 6) / 2
}
return 0 //unsupported functions
}
//MakeRequestHeader makes a particular pdu without any data, to be used for
//client side StartTransaction.
//The inverse functions are PDU.GetFunctionCode() .GetAddress() and .GetRequestCount()
func (f FunctionCode) MakeRequestHeader(address, quantity uint16) (PDU, error) {
if quantity > f.MaxPerPacket() {
return nil, fmt.Errorf("%v can not pack %v at once", f, quantity)
}
if uint32(address)+uint32(quantity) > uint32(f.MaxRange()) {
return nil, fmt.Errorf("%v + %v out of range %v", address, quantity-1, f.MaxRange())
}
header := []byte{byte(f), byte(address >> 8), byte(address)}
if f.IsSingle() {
return PDU(header), nil
}
header = append(header, byte(quantity>>8), byte(quantity))
if f == FcWriteMultipleCoils {
return PDU(append(header, byte((quantity+7)/8))), nil
}
if f == FcWriteMultipleRegisters {
return PDU(append(header, byte(quantity*2))), nil
}
return PDU(header), nil
}
//IsUint16 returns true if the FunctionCode concerns 16bit values
func (f FunctionCode) IsUint16() bool {
switch f {
case 3, 4, 6, 16:
return true
}
return false
}
//IsBool returns true if the FunctionCode concerns boolean values
func (f FunctionCode) IsBool() bool {
switch f {
case 1, 2, 5, 15:
return true
}
return false
}
//IsSingle returns true if the FunctionCode can transmit only one value
func (f FunctionCode) IsSingle() bool {
switch f {
case 5, 6:
return true
}
return false
}
//IsWriteToServer returns true if the FunctionCode is a write.
//FunctionCode 23 is both a read and write.
func (f FunctionCode) IsWriteToServer() bool {
switch f {
case 5, 6, 15, 16, 22, 23:
return true
}
return false
}
//IsReadToServer returns true if the FunctionCode is a write.
//FunctionCode 23 is both a read and write.
func (f FunctionCode) IsReadToServer() bool {
switch f {
case 1, 2, 3, 4, 23:
return true
}
return false
}
//SeparateError test if FunctionCode is an error response, and also return the version
//without error flag set
func (f FunctionCode) SeparateError() (bool, FunctionCode) {
return f > 0x7f, f & 0x7f
}
//WithError return a copy of FunctionCode with the error flag set.
func (f FunctionCode) WithError() FunctionCode {
return f + 0x80
}
//ExceptionCode Modbus exception codes
type ExceptionCode byte
//Defined exception codes, 5 to 11 are not used
const (
//EcOK is invented for no error
EcOK ExceptionCode = 0
//EcInternal is invented for error reading ExceptionCode
EcInternal ExceptionCode = 255
EcIllegalFunction ExceptionCode = 1
EcIllegalDataAddress ExceptionCode = 2
EcIllegalDataValue ExceptionCode = 3
EcServerDeviceFailure ExceptionCode = 4
EcAcknowledge ExceptionCode = 5
EcServerDeviceBusy ExceptionCode = 6
EcMemoryParityError ExceptionCode = 8
EcGatewayPathUnavailable ExceptionCode = 10
EcGatewayTargetDeviceFailedToRespond ExceptionCode = 11
)
//Error implements error for ExceptionCode
func (e ExceptionCode) Error() string {
return fmt.Sprintf("ExceptionCode:0x%02X", byte(e))
}
//ToExceptionCode turns an error into an ExceptionCode (to send in PDU), best
//effort with EcServerDeviceFailure as fail back.
func ToExceptionCode(err error) ExceptionCode {
if err == nil {
debugf("ToExceptionCode: unexpected covert nil error to ExceptionCode")
}
e, ok := err.(ExceptionCode)
if ok {
return e
}
if err == ErrFcNotSupported {
return EcIllegalFunction
}
return EcServerDeviceFailure
}
//PDU is the Modbus Protocol Data Unit
type PDU []byte
//ExceptionReplyPacket make a PDU packet to reply to request req with ExceptionCode e
func ExceptionReplyPacket(req PDU, e ExceptionCode) PDU {
fc := req.GetFunctionCode()
return PDU([]byte{byte(fc) | 0x80, byte(e)})
}
//MatchPDU returns true if ans is a valid reply to ask, including normal and
//error code replies.
func MatchPDU(ask PDU, ans PDU) bool {
rf := ask.GetFunctionCode()
af := ans.GetFunctionCode()
return rf == af%128
}
//ValidateRequest tests for errors in a received Request PDU packet.
//Use ToExceptionCode to get the ExceptionCode for error.
//Checks for errors 2 and 3 are done in GetRequestValues.
func (p PDU) ValidateRequest() error {
if !p.GetFunctionCode().Valid() {
return EcIllegalFunction
}
if len(p) < 3 {
return EcIllegalDataAddress
}
return nil
}
//GetFunctionCode returns the function code
func (p PDU) GetFunctionCode() FunctionCode {
if len(p) <= 0 {
return FunctionCode(0)
}
return FunctionCode(p[0])
}
//GetAddress returns the stating address,
//If PDU is invalid, behavior is undefined (can panic).
func (p PDU) GetAddress() uint16 {
return uint16(p[1])<<8 | uint16(p[2])
}
//GetRequestCount returns the number of values requested,
//If PDU is invalid (too short), behavior is undefined (can panic).
func (p PDU) GetRequestCount() uint16 {
if p.GetFunctionCode().IsSingle() {
return 1
}
return uint16(p[3])<<8 | uint16(p[4])
}
//GetRequestValues returns the values in a write request
func (p PDU) GetRequestValues() ([]byte, error) {
f := p.GetFunctionCode()
if f == 0 {
return nil, EcIllegalFunction
}
if f.IsSingle() {
if len(p) != 5 {
debugf("fc %v got %v pdu bytes, expected 5", p.GetFunctionCode(), len(p))
return nil, EcIllegalDataValue
}
return p[3:], nil
}
lb := len(p) - 6
if lb < 1 {
debugf("fc %v got %v pdu bytes, expected > 6", p.GetFunctionCode(), len(p))
return nil, EcIllegalDataValue
}
if lb != int(p[5]) && !OverSizeSupport {
debugf("decleared %v bytes of data, but got %v bytes", p[5], lb)
return nil, EcIllegalDataValue
}
l := int(p.GetRequestCount())
// check if start + count is highter than max range
if l+int(p.GetAddress()) > int(p.GetFunctionCode().MaxRange()) {
debugf("address out of range")
return nil, EcIllegalDataAddress
}
if f.IsUint16() {
//16 bits registers
if lb != l*2 {
debugf("%v registers does not fit in %v bytes", l, lb)
return nil, EcIllegalDataValue
}
} else {
//bools
if lb != (l+7)/8 {
debugf("%v bools does not fit in %v bytes", l, lb)
return nil, EcIllegalDataValue
}
}
return p[6:], nil
}
//GetReplyValues returns the values in a read reply
func (p PDU) GetReplyValues() ([]byte, error) {
l := len(p) - 2 //bytes of values
if l < 1 || l != int(p[1]) {
return nil, fmt.Errorf("length mismatch with bytes")
}
return p[2:], nil
}
//MakeReadReply produces the reply PDU based on the request PDU and read data
func (p PDU) MakeReadReply(data []byte) PDU {
return PDU(append([]byte{byte(p.GetFunctionCode()), byte(len(data))}, data...))
}
//MakeWriteRequest produces the request PDU based on the request PDU header and
//(locally) read data
func (p PDU) MakeWriteRequest(data []byte) PDU {
fc := p.GetFunctionCode()
switch fc {
case FcWriteSingleCoil, FcWriteSingleRegister:
return append(p[:3], data...)
case FcWriteMultipleCoils, FcWriteMultipleRegisters:
return append(p[:6], data...)
}
debugf("MakeRequestData unsupported for %v\n", fc)
return nil
}
//MakeWriteReply assumes the request is a successful write, and make the associated response
func (p PDU) MakeWriteReply() PDU {
if len(p) > 5 {
return p[:5] //works for 5,6,15,16
}
return p
}