Skip to content

Commit 2aa4ac8

Browse files
committed
Add new modbus.py module
1 parent c161662 commit 2aa4ac8

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed

src/server/python/modbus.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
"""@package modbus
2+
Module to work with Modbus protocol.
3+
4+
Includes class Address to work with modbus address.
5+
"""
6+
7+
# Modbus memory types
8+
Memory_Unknown = -1 ##< Memory type for invalid address
9+
Memory_0x = 0 ##< Memory type for coils
10+
Memory_1x = 1 ##< Memory type for input discretes
11+
Memory_3x = 3 ##< Memory type for input registers
12+
Memory_4x = 4 ##< Memory type for iholding registers
13+
14+
Notation_Default = 0 ##< Default notation which is equal to Modbus notation
15+
Notation_Modbus = 1 ##< Standard Modbus address notation like `000001`, `100001`, `300001`, `400001`
16+
Notation_IEC61131 = 2 ##< IEC-61131 address notation like `%%Q0`, `%%I0`, `%%IW0`, `%%MW0`
17+
Notation_IEC61131Hex = 3 ##< IEC-61131 Hex address notation like `%%Q0000h`, `%%I0000h`, `%%IW0000h`, `%%MW0000h`
18+
19+
20+
## @brief Python set that contains supported Modbus Address types
21+
MemoryTypeSet = { Memory_0x, Memory_1x, Memory_3x, Memory_4x }
22+
23+
sIEC61131Prefix0x = "%Q" ##< IEC-61131 address notation prefix for coils
24+
sIEC61131Prefix1x = "%I" ##< IEC-61131 address notation prefix for input discretes
25+
sIEC61131Prefix3x = "%IW" ##< IEC-61131 address notation prefix for input registers
26+
sIEC61131Prefix4x = "%MW" ##< IEC-61131 address notation prefix for holding registers
27+
28+
cIEC61131SuffixHex = 'h' ##< Suffix for IEC-61131 Hex address notation
29+
30+
31+
## @brief Python set that contains supported Modbus address IEC61131 prefixes
32+
IEC61131PrefixMap = {
33+
Memory_0x: sIEC61131Prefix0x,
34+
Memory_1x: sIEC61131Prefix1x,
35+
Memory_3x: sIEC61131Prefix3x,
36+
Memory_4x: sIEC61131Prefix4x,
37+
}
38+
39+
class Address:
40+
"""
41+
@brief Modbus Data Address class. Represents Modbus Data Address.
42+
43+
@details `Address` class is used to represent Modbus Data Address. It contains memory type and offset.
44+
E.g. `modbus.Address(modbus.Memory_4x, 0)` creates `400001` standard address.
45+
E.g. `modbus.Address(400001)` creates `Address` with type `Modbus::Memory_4x` and offset `0`, and
46+
`modbus.Address(1)` creates `modbus.Address` with type `modbus.Memory_0x` and offset `0`.
47+
Class provides convertions from/to string methods.
48+
49+
Class supports next operators and standard functions:
50+
+, -, <, <=, >, >=, ==, !=, hash(), str(), int()
51+
"""
52+
53+
def __init__(self, value=None, offset=None):
54+
"""
55+
@brief Constructor of the class.
56+
57+
@details Can have next forms:
58+
* `Address()` - creates invalid address class
59+
* `Address(Memory_4x, 0)` - creates address for holding registers with `offset=0`
60+
* `Address("%MW0")` - creates address for holding registers with `offset=0`
61+
* `Address("%Q0000h")` - creates address for coils with `offset=0`
62+
* `Address("100001")` - creates address for input discretes with `offset=0`
63+
* `Address(300001)` - creates address for input registers with `offset=0`
64+
65+
"""
66+
self._type = Memory_Unknown
67+
self._offset = 0
68+
if value is None:
69+
pass
70+
elif isinstance(value, int) and offset is None:
71+
self.fromint(value)
72+
elif isinstance(value, str) and offset is None:
73+
self.fromstr(value)
74+
elif isinstance(value, int) and isinstance(offset, int):
75+
self.settype(value)
76+
self.setoffset(offset)
77+
else:
78+
raise ValueError("Invalid constructor parameters")
79+
80+
def isvalid(self) -> bool:
81+
"""
82+
@details Returns `True` if memory type is not `Modbus::Memory_Unknown`, `False` otherwise.
83+
"""
84+
return self._type != Memory_Unknown
85+
86+
def type(self) -> int:
87+
"""
88+
@details Returns memory type of Modbus Data Address.
89+
"""
90+
return self._type
91+
92+
def settype(self, tp: int):
93+
"""
94+
@details Set memory type of Modbus Data Address.
95+
"""
96+
if tp not in MemoryTypeSet:
97+
raise ValueError(f"Invalid memory type: {tp}. Memory type must be [0,1,3,4]")
98+
self._type = tp
99+
100+
def offset(self) -> int:
101+
"""
102+
@details Returns memory offset of Modbus Data Address.
103+
"""
104+
return self._offset
105+
106+
def setoffset(self, offset: int):
107+
"""
108+
@details Set memory offset of Modbus Data Address.
109+
"""
110+
if not (0 <= offset <= 65535):
111+
raise ValueError(f"Invalid offset: {offset}. Offset must be in range [0:65535]")
112+
self._offset = offset
113+
114+
def number(self) -> int:
115+
"""
116+
@details Returns memory number (offset+1) of Modbus Data Address.
117+
"""
118+
return self._offset + 1
119+
120+
def setnumber(self, number: int):
121+
"""
122+
@details Set memory number (offset+1) of Modbus Data Address.
123+
"""
124+
self.setoffset(number - 1)
125+
126+
def fromint(self, v: int):
127+
"""
128+
@details Make modbus address from integer representaion
129+
"""
130+
number = v % 100000
131+
if number < 1 or number > 65536:
132+
self._type = Memory_Unknown
133+
self._offset = 0
134+
raise ValueError(f"Invalid integer '{v}' to convert into Address: number part '{number}' must be [1:65536]")
135+
136+
mem_type = v // 100000
137+
if mem_type in MemoryTypeSet:
138+
self._type = mem_type
139+
self.setoffset(number - 1)
140+
else:
141+
raise ValueError(f"Invalid integer '{v}' to convert into Address: memory type '{mem_type}' must be [0,1,3,4]")
142+
143+
def toint(self) -> int:
144+
"""
145+
@details Converts current Modbus Data Address to `int`,
146+
e.g. `Address(Memory_4x, 0)` will be converted to `400001`.
147+
"""
148+
return (self._type * 100000) + self.number()
149+
150+
def fromstr(self, s: str):
151+
"""
152+
@details Make modbus address from string representaion
153+
"""
154+
def dec_digit(c):
155+
return int(c) if c.isdigit() else -1
156+
157+
def hex_digit(c):
158+
try:
159+
return int(c, 16)
160+
except ValueError:
161+
return -1
162+
163+
if s.startswith('%'):
164+
i = 0
165+
if s.startswith(sIEC61131Prefix3x):
166+
self._type = Memory_3x
167+
i = len(sIEC61131Prefix3x)
168+
elif s.startswith(sIEC61131Prefix4x):
169+
self._type = Memory_4x
170+
i = len(sIEC61131Prefix4x)
171+
elif s.startswith(sIEC61131Prefix0x):
172+
self._type = Memory_0x
173+
i = len(sIEC61131Prefix0x)
174+
elif s.startswith(sIEC61131Prefix1x):
175+
self._type = Memory_1x
176+
i = len(sIEC61131Prefix1x)
177+
else:
178+
raise ValueError(f"Invalid str '{s}' to convert into Address")
179+
180+
offset = 0
181+
suffix = s[-1]
182+
if suffix == cIEC61131SuffixHex:
183+
for c in s[i:-1]:
184+
offset *= 16
185+
d = hex_digit(c)
186+
if d < 0:
187+
return Address()
188+
offset += d
189+
else:
190+
for c in s[i:]:
191+
offset *= 10
192+
d = dec_digit(c)
193+
if d < 0:
194+
return Address()
195+
offset += d
196+
self.setoffset(offset)
197+
else:
198+
acc = 0
199+
for c in s:
200+
d = dec_digit(c)
201+
if d < 0:
202+
return Address()
203+
acc = acc * 10 + d
204+
self.fromint(acc)
205+
206+
def tostr(self, notation: int = Notation_Default) -> str:
207+
"""
208+
@details Returns string repr of Modbus Data Address with specified notation:
209+
* `Notation_Modbus` - `Address(Memory_4x, 0)` will be converted to `"400001"`.
210+
* `Notation_IEC61131` - `Address(Memory_4x, 0)` will be converted to `"%MW0"`.
211+
* `Notation_IEC61131Hex` - `Address(Memory_4x, 0)` will be converted to `"%MW0000h"`.
212+
"""
213+
def to_dec_string(n, width=0):
214+
return str(n).rjust(width, '0') if width else str(n)
215+
216+
def to_hex_string(n):
217+
return format(n, 'X').rjust(4, '0')
218+
219+
if not self.isvalid():
220+
return "Invalid address"
221+
222+
if notation == Notation_IEC61131:
223+
return IEC61131PrefixMap.get(self._type, "") + to_dec_string(self._offset)
224+
elif notation == Notation_IEC61131Hex:
225+
return IEC61131PrefixMap.get(self._type, "") + to_hex_string(self._offset) + cIEC61131SuffixHex
226+
227+
else:
228+
return to_dec_string(self.toint(), 6)
229+
230+
def __int__(self):
231+
"""
232+
@details Return the integer representation of the object by calling the toint() method.
233+
"""
234+
return self.toint()
235+
236+
def __lt__(self, other):
237+
"""
238+
@details Return self.toint() < other.toint()
239+
"""
240+
return self.toint() < other.toint()
241+
242+
def __le__(self, other):
243+
"""
244+
@details Return self.toint() <= other.toint()
245+
"""
246+
return self.toint() <= other.toint()
247+
248+
def __eq__(self, other):
249+
"""
250+
@details Return self.toint() == other.toint()
251+
"""
252+
return self.toint() == other.toint()
253+
254+
def __ne__(self, other):
255+
"""
256+
@details Return self.toint() != other.toint()
257+
"""
258+
return self.toint() != other.toint()
259+
260+
def __gt__(self, other):
261+
"""
262+
@details Return self.toint() > other.toint()
263+
"""
264+
return self.toint() > other.toint()
265+
266+
def __ge__(self, other):
267+
"""
268+
@details Return self.toint() >= other.toint()
269+
"""
270+
return self.toint() >= other.toint()
271+
272+
def __hash__(self):
273+
"""
274+
@details Return the hash of the object.
275+
"""
276+
return self.toint()
277+
278+
def __add__(self, other: int):
279+
"""
280+
@details Return a new Address object with the offset increased by the given integer.
281+
"""
282+
return Address(self._type, self._offset + other)
283+
284+
def __sub__(self, other: int):
285+
"""
286+
@details Return a new Address object with the offset decreased by the given integer.
287+
"""
288+
return Address(self._type, self._offset - other)
289+
290+
def __iadd__(self, other: int):
291+
"""
292+
@details Increase the offset by the given integer.
293+
"""
294+
self.setoffset(self._offset + other)
295+
return self
296+
297+
def __isub__(self, other: int):
298+
"""
299+
@details Decrease the offset by the given integer.
300+
"""
301+
self.setoffset(self._offset - other)
302+
return self
303+
304+
def __repr__(self):
305+
"""
306+
@details Return the string representation of the object.
307+
"""
308+
return self.tostr(Notation_Default)
309+
310+
def __str__(self):
311+
"""
312+
@details Return the string representation of the object.
313+
"""
314+
return self.tostr(Notation_Default)

0 commit comments

Comments
 (0)