-
Notifications
You must be signed in to change notification settings - Fork 18
/
minimalmodbus.py
2551 lines (1862 loc) · 96.2 KB
/
minimalmodbus.py
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
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
#
# Copyright 2015 Jonas Berg
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
.. moduleauthor:: Jonas Berg <[email protected]>
MinimalModbus: A Python driver for the Modbus RTU and Modbus ASCII protocols via serial port (via RS485 or RS232).
"""
__author__ = 'Jonas Berg'
__email__ = '[email protected]'
__url__ = 'https://github.com/pyhys/minimalmodbus'
__license__ = 'Apache License, Version 2.0'
__version__ = '0.7'
__status__ = 'Beta'
import os
import serial
import struct
import sys
import time
if sys.version > '3':
import binascii
# Allow long also in Python3
# http://python3porting.com/noconv.html
if sys.version > '3':
long = int
_NUMBER_OF_BYTES_PER_REGISTER = 2
_SECONDS_TO_MILLISECONDS = 1000
_ASCII_HEADER = ':'
_ASCII_FOOTER = '\r\n'
# Several instrument instances can share the same serialport
_SERIALPORTS = {}
_LATEST_READ_TIMES = {}
####################
## Default values ##
####################
BAUDRATE = 19200
"""Default value for the baudrate in Baud (int)."""
PARITY = serial.PARITY_NONE
"""Default value for the parity. See the pySerial module for documentation. Defaults to serial.PARITY_NONE"""
BYTESIZE = 8
"""Default value for the bytesize (int)."""
STOPBITS = 1
"""Default value for the number of stopbits (int)."""
TIMEOUT = 0.05
"""Default value for the timeout value in seconds (float)."""
CLOSE_PORT_AFTER_EACH_CALL = False
"""Default value for port closure setting."""
#####################
## Named constants ##
#####################
MODE_RTU = 'rtu'
MODE_ASCII = 'ascii'
##############################
## Modbus instrument object ##
##############################
class Instrument():
"""Instrument class for talking to instruments (slaves) via the Modbus RTU or ASCII protocols (via RS485 or RS232).
Args:
* port (str): The serial port name, for example ``/dev/ttyUSB0`` (Linux), ``/dev/tty.usbserial`` (OS X) or ``COM4`` (Windows).
* slaveaddress (int): Slave address in the range 1 to 247 (use decimal numbers, not hex).
* mode (str): Mode selection. Can be MODE_RTU or MODE_ASCII.
"""
def __init__(self, port, slaveaddress, mode=MODE_RTU):
if port not in _SERIALPORTS or not _SERIALPORTS[port]:
self.serial = _SERIALPORTS[port] = serial.Serial(port=port, baudrate=BAUDRATE, parity=PARITY, bytesize=BYTESIZE, stopbits=STOPBITS, timeout=TIMEOUT)
else:
self.serial = _SERIALPORTS[port]
if self.serial.port is None:
self.serial.open()
"""The serial port object as defined by the pySerial module. Created by the constructor.
Attributes:
- port (str): Serial port name.
- Most often set by the constructor (see the class documentation).
- baudrate (int): Baudrate in Baud.
- Defaults to :data:`BAUDRATE`.
- parity (probably int): Parity. See the pySerial module for documentation.
- Defaults to :data:`PARITY`.
- bytesize (int): Bytesize in bits.
- Defaults to :data:`BYTESIZE`.
- stopbits (int): The number of stopbits.
- Defaults to :data:`STOPBITS`.
- timeout (float): Timeout value in seconds.
- Defaults to :data:`TIMEOUT`.
"""
self.address = slaveaddress
"""Slave address (int). Most often set by the constructor (see the class documentation). """
self.mode = mode
"""Slave mode (str), can be MODE_RTU or MODE_ASCII. Most often set by the constructor (see the class documentation).
New in version 0.6.
"""
self.debug = False
"""Set this to :const:`True` to print the communication details. Defaults to :const:`False`."""
self.close_port_after_each_call = CLOSE_PORT_AFTER_EACH_CALL
"""If this is :const:`True`, the serial port will be closed after each call. Defaults to :data:`CLOSE_PORT_AFTER_EACH_CALL`. To change it, set the value ``minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL=True`` ."""
self.precalculate_read_size = True
"""If this is :const:`False`, the serial port reads until timeout
instead of just reading a specific number of bytes. Defaults to :const:`True`.
New in version 0.5.
"""
self.handle_local_echo = False
"""Set to to :const:`True` if your RS-485 adaptor has local echo enabled.
Then the transmitted message will immeadiately appear at the receive line of the RS-485 adaptor.
MinimalModbus will then read and discard this data, before reading the data from the slave.
Defaults to :const:`False`.
New in version 0.7.
"""
if self.close_port_after_each_call:
self.serial.close()
def __repr__(self):
"""String representation of the :class:`.Instrument` object."""
return "{}.{}<id=0x{:x}, address={}, mode={}, close_port_after_each_call={}, precalculate_read_size={}, debug={}, serial={}>".format(
self.__module__,
self.__class__.__name__,
id(self),
self.address,
self.mode,
self.close_port_after_each_call,
self.precalculate_read_size,
self.debug,
self.serial,
)
######################################
## Methods for talking to the slave ##
######################################
def read_bit(self, registeraddress, functioncode=2):
"""Read one bit from the slave.
Args:
* registeraddress (int): The slave register address (use decimal numbers, not hex).
* functioncode (int): Modbus function code. Can be 1 or 2.
Returns:
The bit value 0 or 1 (int).
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [1, 2])
return self._genericCommand(functioncode, registeraddress)
def write_bit(self, registeraddress, value, functioncode=5):
"""Write one bit to the slave.
Args:
* registeraddress (int): The slave register address (use decimal numbers, not hex).
* value (int): 0 or 1
* functioncode (int): Modbus function code. Can be 5 or 15.
Returns:
None
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [5, 15])
_checkInt(value, minvalue=0, maxvalue=1, description='input value')
self._genericCommand(functioncode, registeraddress, value)
def read_register(self, registeraddress, numberOfDecimals=0, functioncode=3, signed=False):
"""Read an integer from one 16-bit register in the slave, possibly scaling it.
The slave register can hold integer values in the range 0 to 65535 ("Unsigned INT16").
Args:
* registeraddress (int): The slave register address (use decimal numbers, not hex).
* numberOfDecimals (int): The number of decimals for content conversion.
* functioncode (int): Modbus function code. Can be 3 or 4.
* signed (bool): Whether the data should be interpreted as unsigned or signed.
If a value of 77.0 is stored internally in the slave register as 770, then use ``numberOfDecimals=1``
which will divide the received data by 10 before returning the value.
Similarly ``numberOfDecimals=2`` will divide the received data by 100 before returning the value.
Some manufacturers allow negative values for some registers. Instead of
an allowed integer range 0 to 65535, a range -32768 to 32767 is allowed. This is
implemented as any received value in the upper range (32768 to 65535) is
interpreted as negative value (in the range -32768 to -1).
Use the parameter ``signed=True`` if reading from a register that can hold
negative values. Then upper range data will be automatically converted into
negative return values (two's complement).
============== ================== ================ ===============
``signed`` Data type in slave Alternative name Range
============== ================== ================ ===============
:const:`False` Unsigned INT16 Unsigned short 0 to 65535
:const:`True` INT16 Short -32768 to 32767
============== ================== ================ ===============
Returns:
The register data in numerical value (int or float).
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [3, 4])
_checkInt(numberOfDecimals, minvalue=0, maxvalue=10, description='number of decimals')
_checkBool(signed, description='signed')
return self._genericCommand(functioncode, registeraddress, numberOfDecimals=numberOfDecimals, signed=signed)
def write_register(self, registeraddress, value, numberOfDecimals=0, functioncode=16, signed=False):
"""Write an integer to one 16-bit register in the slave, possibly scaling it.
The slave register can hold integer values in the range 0 to 65535 ("Unsigned INT16").
Args:
* registeraddress (int): The slave register address (use decimal numbers, not hex).
* value (int or float): The value to store in the slave register (might be scaled before sending).
* numberOfDecimals (int): The number of decimals for content conversion.
* functioncode (int): Modbus function code. Can be 6 or 16.
* signed (bool): Whether the data should be interpreted as unsigned or signed.
To store for example ``value=77.0``, use ``numberOfDecimals=1`` if the slave register will hold it as 770 internally.
This will multiply ``value`` by 10 before sending it to the slave register.
Similarly ``numberOfDecimals=2`` will multiply ``value`` by 100 before sending it to the slave register.
For discussion on negative values, the range and on alternative names, see :meth:`.read_register`.
Use the parameter ``signed=True`` if writing to a register that can hold
negative values. Then negative input will be automatically converted into
upper range data (two's complement).
Returns:
None
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [6, 16])
_checkInt(numberOfDecimals, minvalue=0, maxvalue=10, description='number of decimals')
_checkBool(signed, description='signed')
_checkNumerical(value, description='input value')
self._genericCommand(functioncode, registeraddress, value, numberOfDecimals, signed=signed)
def read_long(self, registeraddress, functioncode=3, signed=False):
"""Read a long integer (32 bits) from the slave.
Long integers (32 bits = 4 bytes) are stored in two consecutive 16-bit registers in the slave.
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* functioncode (int): Modbus function code. Can be 3 or 4.
* signed (bool): Whether the data should be interpreted as unsigned or signed.
============== ================== ================ ==========================
``signed`` Data type in slave Alternative name Range
============== ================== ================ ==========================
:const:`False` Unsigned INT32 Unsigned long 0 to 4294967295
:const:`True` INT32 Long -2147483648 to 2147483647
============== ================== ================ ==========================
Returns:
The numerical value (int).
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [3, 4])
_checkBool(signed, description='signed')
return self._genericCommand(functioncode, registeraddress, numberOfRegisters=2, signed=signed, payloadformat='long')
def write_long(self, registeraddress, value, signed=False):
"""Write a long integer (32 bits) to the slave.
Long integers (32 bits = 4 bytes) are stored in two consecutive 16-bit registers in the slave.
Uses Modbus function code 16.
For discussion on number of bits, number of registers, the range
and on alternative names, see :meth:`.read_long`.
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* value (int or long): The value to store in the slave.
* signed (bool): Whether the data should be interpreted as unsigned or signed.
Returns:
None
Raises:
ValueError, TypeError, IOError
"""
MAX_VALUE_LONG = 4294967295 # Unsigned INT32
MIN_VALUE_LONG = -2147483648 # INT32
_checkInt(value, minvalue=MIN_VALUE_LONG, maxvalue=MAX_VALUE_LONG, description='input value')
_checkBool(signed, description='signed')
self._genericCommand(16, registeraddress, value, numberOfRegisters=2, signed=signed, payloadformat='long')
def read_float(self, registeraddress, functioncode=3, numberOfRegisters=2):
"""Read a floating point number from the slave.
Floats are stored in two or more consecutive 16-bit registers in the slave. The
encoding is according to the standard IEEE 754.
There are differences in the byte order used by different manufacturers. A floating
point value of 1.0 is encoded (in single precision) as 3f800000 (hex). In this
implementation the data will be sent as ``'\\x3f\\x80'`` and ``'\\x00\\x00'``
to two consecutetive registers . Make sure to test that it makes sense for your instrument.
It is pretty straight-forward to change this code if some other byte order is
required by anyone (see support section).
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* functioncode (int): Modbus function code. Can be 3 or 4.
* numberOfRegisters (int): The number of registers allocated for the float. Can be 2 or 4.
====================================== ================= =========== =================
Type of floating point number in slave Size Registers Range
====================================== ================= =========== =================
Single precision (binary32) 32 bits (4 bytes) 2 registers 1.4E-45 to 3.4E38
Double precision (binary64) 64 bits (8 bytes) 4 registers 5E-324 to 1.8E308
====================================== ================= =========== =================
Returns:
The numerical value (float).
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [3, 4])
_checkInt(numberOfRegisters, minvalue=2, maxvalue=4, description='number of registers')
return self._genericCommand(functioncode, registeraddress, numberOfRegisters=numberOfRegisters, payloadformat='float')
def write_float(self, registeraddress, value, numberOfRegisters=2):
"""Write a floating point number to the slave.
Floats are stored in two or more consecutive 16-bit registers in the slave.
Uses Modbus function code 16.
For discussion on precision, number of registers and on byte order, see :meth:`.read_float`.
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* value (float or int): The value to store in the slave
* numberOfRegisters (int): The number of registers allocated for the float. Can be 2 or 4.
Returns:
None
Raises:
ValueError, TypeError, IOError
"""
_checkNumerical(value, description='input value')
_checkInt(numberOfRegisters, minvalue=2, maxvalue=4, description='number of registers')
self._genericCommand(16, registeraddress, value, \
numberOfRegisters=numberOfRegisters, payloadformat='float')
def read_string(self, registeraddress, numberOfRegisters=16, functioncode=3):
"""Read a string from the slave.
Each 16-bit register in the slave are interpreted as two characters (1 byte = 8 bits).
For example 16 consecutive registers can hold 32 characters (32 bytes).
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* numberOfRegisters (int): The number of registers allocated for the string.
* functioncode (int): Modbus function code. Can be 3 or 4.
Returns:
The string (str).
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [3, 4])
_checkInt(numberOfRegisters, minvalue=1, description='number of registers for read string')
return self._genericCommand(functioncode, registeraddress, \
numberOfRegisters=numberOfRegisters, payloadformat='string')
def write_string(self, registeraddress, textstring, numberOfRegisters=16):
"""Write a string to the slave.
Each 16-bit register in the slave are interpreted as two characters (1 byte = 8 bits).
For example 16 consecutive registers can hold 32 characters (32 bytes).
Uses Modbus function code 16.
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* textstring (str): The string to store in the slave
* numberOfRegisters (int): The number of registers allocated for the string.
If the textstring is longer than the 2*numberOfRegisters, an error is raised.
Shorter strings are padded with spaces.
Returns:
None
Raises:
ValueError, TypeError, IOError
"""
_checkInt(numberOfRegisters, minvalue=1, description='number of registers for write string')
_checkString(textstring, 'input string', minlength=1, maxlength=2 * numberOfRegisters)
self._genericCommand(16, registeraddress, textstring, \
numberOfRegisters=numberOfRegisters, payloadformat='string')
def read_registers(self, registeraddress, numberOfRegisters, functioncode=3):
"""Read integers from 16-bit registers in the slave.
The slave registers can hold integer values in the range 0 to 65535 ("Unsigned INT16").
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* numberOfRegisters (int): The number of registers to read.
* functioncode (int): Modbus function code. Can be 3 or 4.
Any scaling of the register data, or converting it to negative number (two's complement)
must be done manually.
Returns:
The register data (a list of int).
Raises:
ValueError, TypeError, IOError
"""
_checkFunctioncode(functioncode, [3, 4])
_checkInt(numberOfRegisters, minvalue=1, description='number of registers')
return self._genericCommand(functioncode, registeraddress, \
numberOfRegisters=numberOfRegisters, payloadformat='registers')
def write_registers(self, registeraddress, values):
"""Write integers to 16-bit registers in the slave.
The slave register can hold integer values in the range 0 to 65535 ("Unsigned INT16").
Uses Modbus function code 16.
The number of registers that will be written is defined by the length of the ``values`` list.
Args:
* registeraddress (int): The slave register start address (use decimal numbers, not hex).
* values (list of int): The values to store in the slave registers.
Any scaling of the register data, or converting it to negative number (two's complement)
must be done manually.
Returns:
None
Raises:
ValueError, TypeError, IOError
"""
if not isinstance(values, list):
raise TypeError('The "values parameter" must be a list. Given: {0!r}'.format(values))
_checkInt(len(values), minvalue=1, description='length of input list')
# Note: The content of the list is checked at content conversion.
self._genericCommand(16, registeraddress, values, numberOfRegisters=len(values), payloadformat='registers')
#####################
## Generic command ##
#####################
def _genericCommand(self, functioncode, registeraddress, value=None, \
numberOfDecimals=0, numberOfRegisters=1, signed=False, payloadformat=None):
"""Generic command for reading and writing registers and bits.
Args:
* functioncode (int): Modbus function code.
* registeraddress (int): The register address (use decimal numbers, not hex).
* value (numerical or string or None or list of int): The value to store in the register. Depends on payloadformat.
* numberOfDecimals (int): The number of decimals for content conversion. Only for a single register.
* numberOfRegisters (int): The number of registers to read/write. Only certain values allowed, depends on payloadformat.
* signed (bool): Whether the data should be interpreted as unsigned or signed. Only for a single register or for payloadformat='long'.
* payloadformat (None or string): None, 'long', 'float', 'string', 'register', 'registers'. Not necessary for single registers or bits.
If a value of 77.0 is stored internally in the slave register as 770,
then use ``numberOfDecimals=1`` which will divide the received data from the slave by 10
before returning the value. Similarly ``numberOfDecimals=2`` will divide
the received data by 100 before returning the value. Same functionality is also used
when writing data to the slave.
Returns:
The register data in numerical value (int or float), or the bit value 0 or 1 (int), or ``None``.
Raises:
ValueError, TypeError, IOError
"""
NUMBER_OF_BITS = 1
NUMBER_OF_BYTES_FOR_ONE_BIT = 1
NUMBER_OF_BYTES_BEFORE_REGISTERDATA = 1
ALL_ALLOWED_FUNCTIONCODES = list(range(1, 7)) + [15, 16] # To comply with both Python2 and Python3
MAX_NUMBER_OF_REGISTERS = 255
# Payload format constants, so datatypes can be told apart.
# Note that bit datatype not is included, because it uses other functioncodes.
PAYLOADFORMAT_LONG = 'long'
PAYLOADFORMAT_FLOAT = 'float'
PAYLOADFORMAT_STRING = 'string'
PAYLOADFORMAT_REGISTER = 'register'
PAYLOADFORMAT_REGISTERS = 'registers'
ALL_PAYLOADFORMATS = [PAYLOADFORMAT_LONG, PAYLOADFORMAT_FLOAT, \
PAYLOADFORMAT_STRING, PAYLOADFORMAT_REGISTER, PAYLOADFORMAT_REGISTERS]
## Check input values ##
_checkFunctioncode(functioncode, ALL_ALLOWED_FUNCTIONCODES) # Note: The calling facade functions should validate this
_checkRegisteraddress(registeraddress)
_checkInt(numberOfDecimals, minvalue=0, description='number of decimals')
_checkInt(numberOfRegisters, minvalue=1, maxvalue=MAX_NUMBER_OF_REGISTERS, description='number of registers')
_checkBool(signed, description='signed')
if payloadformat is not None:
if payloadformat not in ALL_PAYLOADFORMATS:
raise ValueError('Wrong payload format variable. Given: {0!r}'.format(payloadformat))
## Check combinations of input parameters ##
numberOfRegisterBytes = numberOfRegisters * _NUMBER_OF_BYTES_PER_REGISTER
# Payload format
if functioncode in [3, 4, 6, 16] and payloadformat is None:
payloadformat = PAYLOADFORMAT_REGISTER
if functioncode in [3, 4, 6, 16]:
if payloadformat not in ALL_PAYLOADFORMATS:
raise ValueError('The payload format is unknown. Given format: {0!r}, functioncode: {1!r}.'.\
format(payloadformat, functioncode))
else:
if payloadformat is not None:
raise ValueError('The payload format given is not allowed for this function code. ' + \
'Given format: {0!r}, functioncode: {1!r}.'.format(payloadformat, functioncode))
# Signed and numberOfDecimals
if signed:
if payloadformat not in [PAYLOADFORMAT_REGISTER, PAYLOADFORMAT_LONG]:
raise ValueError('The "signed" parameter can not be used for this data format. ' + \
'Given format: {0!r}.'.format(payloadformat))
if numberOfDecimals > 0 and payloadformat != PAYLOADFORMAT_REGISTER:
raise ValueError('The "numberOfDecimals" parameter can not be used for this data format. ' + \
'Given format: {0!r}.'.format(payloadformat))
# Number of registers
if functioncode not in [3, 4, 16] and numberOfRegisters != 1:
raise ValueError('The numberOfRegisters is not valid for this function code. ' + \
'NumberOfRegisters: {0!r}, functioncode {1}.'.format(numberOfRegisters, functioncode))
if functioncode == 16 and payloadformat == PAYLOADFORMAT_REGISTER and numberOfRegisters != 1:
raise ValueError('Wrong numberOfRegisters when writing to a ' + \
'single register. Given {0!r}.'.format(numberOfRegisters))
# Note: For function code 16 there is checking also in the content conversion functions.
# Value
if functioncode in [5, 6, 15, 16] and value is None:
raise ValueError('The input value is not valid for this function code. ' + \
'Given {0!r} and {1}.'.format(value, functioncode))
if functioncode == 16 and payloadformat in [PAYLOADFORMAT_REGISTER, PAYLOADFORMAT_FLOAT, PAYLOADFORMAT_LONG]:
_checkNumerical(value, description='input value')
if functioncode == 6 and payloadformat == PAYLOADFORMAT_REGISTER:
_checkNumerical(value, description='input value')
# Value for string
if functioncode == 16 and payloadformat == PAYLOADFORMAT_STRING:
_checkString(value, 'input string', minlength=1, maxlength=numberOfRegisterBytes)
# Note: The string might be padded later, so the length might be shorter than numberOfRegisterBytes.
# Value for registers
if functioncode == 16 and payloadformat == PAYLOADFORMAT_REGISTERS:
if not isinstance(value, list):
raise TypeError('The value parameter must be a list. Given {0!r}.'.format(value))
if len(value) != numberOfRegisters:
raise ValueError('The list length does not match number of registers. ' + \
'List: {0!r}, Number of registers: {1!r}.'.format(value, numberOfRegisters))
## Build payload to slave ##
if functioncode in [1, 2]:
payloadToSlave = _numToTwoByteString(registeraddress) + \
_numToTwoByteString(NUMBER_OF_BITS)
elif functioncode in [3, 4]:
payloadToSlave = _numToTwoByteString(registeraddress) + \
_numToTwoByteString(numberOfRegisters)
elif functioncode == 5:
payloadToSlave = _numToTwoByteString(registeraddress) + \
_createBitpattern(functioncode, value)
elif functioncode == 6:
payloadToSlave = _numToTwoByteString(registeraddress) + \
_numToTwoByteString(value, numberOfDecimals, signed=signed)
elif functioncode == 15:
payloadToSlave = _numToTwoByteString(registeraddress) + \
_numToTwoByteString(NUMBER_OF_BITS) + \
_numToOneByteString(NUMBER_OF_BYTES_FOR_ONE_BIT) + \
_createBitpattern(functioncode, value)
elif functioncode == 16:
if payloadformat == PAYLOADFORMAT_REGISTER:
registerdata = _numToTwoByteString(value, numberOfDecimals, signed=signed)
elif payloadformat == PAYLOADFORMAT_STRING:
registerdata = _textstringToBytestring(value, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_LONG:
registerdata = _longToBytestring(value, signed, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_FLOAT:
registerdata = _floatToBytestring(value, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_REGISTERS:
registerdata = _valuelistToBytestring(value, numberOfRegisters)
assert len(registerdata) == numberOfRegisterBytes
payloadToSlave = _numToTwoByteString(registeraddress) + \
_numToTwoByteString(numberOfRegisters) + \
_numToOneByteString(numberOfRegisterBytes) + \
registerdata
## Communicate ##
payloadFromSlave = self._performCommand(functioncode, payloadToSlave)
## Check the contents in the response payload ##
if functioncode in [1, 2, 3, 4]:
_checkResponseByteCount(payloadFromSlave) # response byte count
if functioncode in [5, 6, 15, 16]:
_checkResponseRegisterAddress(payloadFromSlave, registeraddress) # response register address
if functioncode == 5:
_checkResponseWriteData(payloadFromSlave, _createBitpattern(functioncode, value)) # response write data
if functioncode == 6:
_checkResponseWriteData(payloadFromSlave, \
_numToTwoByteString(value, numberOfDecimals, signed=signed)) # response write data
if functioncode == 15:
_checkResponseNumberOfRegisters(payloadFromSlave, NUMBER_OF_BITS) # response number of bits
if functioncode == 16:
_checkResponseNumberOfRegisters(payloadFromSlave, numberOfRegisters) # response number of registers
## Calculate return value ##
if functioncode in [1, 2]:
registerdata = payloadFromSlave[NUMBER_OF_BYTES_BEFORE_REGISTERDATA:]
if len(registerdata) != NUMBER_OF_BYTES_FOR_ONE_BIT:
raise ValueError('The registerdata length does not match NUMBER_OF_BYTES_FOR_ONE_BIT. ' + \
'Given {0}.'.format(len(registerdata)))
return _bitResponseToValue(registerdata)
if functioncode in [3, 4]:
registerdata = payloadFromSlave[NUMBER_OF_BYTES_BEFORE_REGISTERDATA:]
if len(registerdata) != numberOfRegisterBytes:
raise ValueError('The registerdata length does not match number of register bytes. ' + \
'Given {0!r} and {1!r}.'.format(len(registerdata), numberOfRegisterBytes))
if payloadformat == PAYLOADFORMAT_STRING:
return _bytestringToTextstring(registerdata, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_LONG:
return _bytestringToLong(registerdata, signed, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_FLOAT:
return _bytestringToFloat(registerdata, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_REGISTERS:
return _bytestringToValuelist(registerdata, numberOfRegisters)
elif payloadformat == PAYLOADFORMAT_REGISTER:
return _twoByteStringToNum(registerdata, numberOfDecimals, signed=signed)
raise ValueError('Wrong payloadformat for return value generation. ' + \
'Given {0}'.format(payloadformat))
##########################################
## Communication implementation details ##
##########################################
def _performCommand(self, functioncode, payloadToSlave):
"""Performs the command having the *functioncode*.
Args:
* functioncode (int): The function code for the command to be performed. Can for example be 'Write register' = 16.
* payloadToSlave (str): Data to be transmitted to the slave (will be embedded in slaveaddress, CRC etc)
Returns:
The extracted data payload from the slave (a string). It has been stripped of CRC etc.
Raises:
ValueError, TypeError.
Makes use of the :meth:`_communicate` method. The request is generated
with the :func:`_embedPayload` function, and the parsing of the
response is done with the :func:`_extractPayload` function.
"""
DEFAULT_NUMBER_OF_BYTES_TO_READ = 1000
_checkFunctioncode(functioncode, None)
_checkString(payloadToSlave, description='payload')
# Build request
request = _embedPayload(self.address, self.mode, functioncode, payloadToSlave)
# Calculate number of bytes to read
number_of_bytes_to_read = DEFAULT_NUMBER_OF_BYTES_TO_READ
if self.precalculate_read_size:
try:
number_of_bytes_to_read = _predictResponseSize(self.mode, functioncode, payloadToSlave)
except:
if self.debug:
template = 'MinimalModbus debug mode. Could not precalculate response size for Modbus {} mode. ' + \
'Will read {} bytes. request: {!r}'
_print_out(template.format(self.mode, number_of_bytes_to_read, request))
# Communicate
response = self._communicate(request, number_of_bytes_to_read)
# Extract payload
payloadFromSlave = _extractPayload(response, self.address, self.mode, functioncode)
return payloadFromSlave
def _communicate(self, request, number_of_bytes_to_read):
"""Talk to the slave via a serial port.
Args:
request (str): The raw request that is to be sent to the slave.
number_of_bytes_to_read (int): number of bytes to read
Returns:
The raw data (string) returned from the slave.
Raises:
TypeError, ValueError, IOError
Note that the answer might have strange ASCII control signs, which
makes it difficult to print it in the promt (messes up a bit).
Use repr() to make the string printable (shows ASCII values for control signs.)
Will block until reaching *number_of_bytes_to_read* or timeout.
If the attribute :attr:`Instrument.debug` is :const:`True`, the communication details are printed.
If the attribute :attr:`Instrument.close_port_after_each_call` is :const:`True` the
serial port is closed after each call.
Timing::
Request from master (Master is writing)
|
| Response from slave (Master is reading)
| |
----W----R----------------------------W-------R----------------------------------------
| | |
|<----- Silent period ------>| |
| |
Roundtrip time ---->|-------|<--
The resolution for Python's time.time() is lower on Windows than on Linux.
It is about 16 ms on Windows according to
http://stackoverflow.com/questions/157359/accurate-timestamping-in-python
For Python3, the information sent to and from pySerial should be of the type bytes.
This is taken care of automatically by MinimalModbus.
"""
_checkString(request, minlength=1, description='request')
_checkInt(number_of_bytes_to_read)
if self.debug:
_print_out('\nMinimalModbus debug mode. Writing to instrument (expecting {} bytes back): {!r} ({})'. \
format(number_of_bytes_to_read, request, _hexlify(request)))
if self.close_port_after_each_call:
self.serial.open()
#self.serial.flushInput() TODO
if sys.version_info[0] > 2:
request = bytes(request, encoding='latin1') # Convert types to make it Python3 compatible
# Sleep to make sure 3.5 character times have passed
minimum_silent_period = _calculate_minimum_silent_period(self.serial.baudrate)
time_since_read = time.time() - _LATEST_READ_TIMES.get(self.serial.port, 0)
if time_since_read < minimum_silent_period:
sleep_time = minimum_silent_period - time_since_read
if self.debug:
template = 'MinimalModbus debug mode. Sleeping for {:.1f} ms. ' + \
'Minimum silent period: {:.1f} ms, time since read: {:.1f} ms.'
text = template.format(
sleep_time * _SECONDS_TO_MILLISECONDS,
minimum_silent_period * _SECONDS_TO_MILLISECONDS,
time_since_read * _SECONDS_TO_MILLISECONDS)
_print_out(text)
time.sleep(sleep_time)
elif self.debug:
template = 'MinimalModbus debug mode. No sleep required before write. ' + \
'Time since previous read: {:.1f} ms, minimum silent period: {:.2f} ms.'
text = template.format(
time_since_read * _SECONDS_TO_MILLISECONDS,
minimum_silent_period * _SECONDS_TO_MILLISECONDS)
_print_out(text)
# Write request
latest_write_time = time.time()
self.serial.write(request)
# Read and discard local echo
if self.handle_local_echo:
localEchoToDiscard = self.serial.read(len(request))
if self.debug:
template = 'MinimalModbus debug mode. Discarding this local echo: {!r} ({} bytes).'
text = template.format(localEchoToDiscard, len(localEchoToDiscard))
_print_out(text)
if localEchoToDiscard != request:
template = 'Local echo handling is enabled, but the local echo does not match the sent request. ' + \
'Request: {!r} ({} bytes), local echo: {!r} ({} bytes).'
text = template.format(request, len(request), localEchoToDiscard, len(localEchoToDiscard))
raise IOError(text)
# Read response
answer = self.serial.read(number_of_bytes_to_read)
_LATEST_READ_TIMES[self.serial.port] = time.time()
if self.close_port_after_each_call:
self.serial.close()
if sys.version_info[0] > 2:
answer = str(answer, encoding='latin1') # Convert types to make it Python3 compatible
if self.debug:
template = 'MinimalModbus debug mode. Response from instrument: {!r} ({}) ({} bytes), ' + \
'roundtrip time: {:.1f} ms. Timeout setting: {:.1f} ms.\n'
text = template.format(
answer,
_hexlify(answer),
len(answer),
(_LATEST_READ_TIMES.get(self.serial.port, 0) - latest_write_time) * _SECONDS_TO_MILLISECONDS,
self.serial.timeout * _SECONDS_TO_MILLISECONDS)
_print_out(text)
if len(answer) == 0:
raise IOError('No communication with the instrument (no answer)')
return answer
####################
# Payload handling #
####################
def _embedPayload(slaveaddress, mode, functioncode, payloaddata):
"""Build a request from the slaveaddress, the function code and the payload data.
Args:
* slaveaddress (int): The address of the slave.
* mode (str): The modbus protcol mode (MODE_RTU or MODE_ASCII)
* functioncode (int): The function code for the command to be performed. Can for example be 16 (Write register).
* payloaddata (str): The byte string to be sent to the slave.
Returns:
The built (raw) request string for sending to the slave (including CRC etc).
Raises:
ValueError, TypeError.
The resulting request has the format:
* RTU Mode: slaveaddress byte + functioncode byte + payloaddata + CRC (which is two bytes).
* ASCII Mode: header (:) + slaveaddress (2 characters) + functioncode (2 characters) + payloaddata + LRC (which is two characters) + footer (CRLF)
The LRC or CRC is calculated from the byte string made up of slaveaddress + functioncode + payloaddata.
The header, LRC/CRC, and footer are excluded from the calculation.
"""
_checkSlaveaddress(slaveaddress)
_checkMode(mode)
_checkFunctioncode(functioncode, None)
_checkString(payloaddata, description='payload')
firstPart = _numToOneByteString(slaveaddress) + _numToOneByteString(functioncode) + payloaddata
if mode == MODE_ASCII:
request = _ASCII_HEADER + \
_hexencode(firstPart) + \
_hexencode(_calculateLrcString(firstPart)) + \
_ASCII_FOOTER
else:
request = firstPart + _calculateCrcString(firstPart)
return request
def _extractPayload(response, slaveaddress, mode, functioncode):
"""Extract the payload data part from the slave's response.
Args:
* response (str): The raw response byte string from the slave.
* slaveaddress (int): The adress of the slave. Used here for error checking only.
* mode (str): The modbus protcol mode (MODE_RTU or MODE_ASCII)
* functioncode (int): Used here for error checking only.
Returns:
The payload part of the *response* string.
Raises:
ValueError, TypeError. Raises an exception if there is any problem with the received address, the functioncode or the CRC.
The received response should have the format:
* RTU Mode: slaveaddress byte + functioncode byte + payloaddata + CRC (which is two bytes)
* ASCII Mode: header (:) + slaveaddress byte + functioncode byte + payloaddata + LRC (which is two characters) + footer (CRLF)
For development purposes, this function can also be used to extract the payload from the request sent TO the slave.