mbtoolcli 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mbtoolcli-0.3.1.dist-info/METADATA +473 -0
- mbtoolcli-0.3.1.dist-info/RECORD +16 -0
- mbtoolcli-0.3.1.dist-info/WHEEL +5 -0
- mbtoolcli-0.3.1.dist-info/entry_points.txt +4 -0
- mbtoolcli-0.3.1.dist-info/licenses/LICENSE +21 -0
- mbtoolcli-0.3.1.dist-info/top_level.txt +1 -0
- modbus_slave_sim/__init__.py +42 -0
- modbus_slave_sim/cli.py +903 -0
- modbus_slave_sim/colors.py +57 -0
- modbus_slave_sim/datatypes.py +386 -0
- modbus_slave_sim/gui_server.py +1049 -0
- modbus_slave_sim/master.py +647 -0
- modbus_slave_sim/registers.py +288 -0
- modbus_slave_sim/rtu_server.py +158 -0
- modbus_slave_sim/simulation.py +318 -0
- modbus_slave_sim/tcp_server.py +178 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ANSI color utilities for terminal output.
|
|
3
|
+
|
|
4
|
+
Automatically disables colors when output is not a TTY (e.g., piped output).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _supports_color() -> bool:
|
|
12
|
+
"""Check if the terminal supports ANSI colors."""
|
|
13
|
+
if os.getenv("NO_COLOR") or os.getenv("ANSI_COLORS_DISABLED"):
|
|
14
|
+
return False
|
|
15
|
+
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
|
16
|
+
return False
|
|
17
|
+
if sys.platform == "win32":
|
|
18
|
+
# Enable ANSI on Windows 10+
|
|
19
|
+
try:
|
|
20
|
+
import ctypes
|
|
21
|
+
kernel32 = ctypes.windll.kernel32
|
|
22
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
23
|
+
except Exception:
|
|
24
|
+
return False
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_ENABLED = _supports_color()
|
|
29
|
+
|
|
30
|
+
# Reset
|
|
31
|
+
RST = "\033[0m" if _ENABLED else ""
|
|
32
|
+
|
|
33
|
+
# Regular colors
|
|
34
|
+
RED = "\033[31m" if _ENABLED else ""
|
|
35
|
+
GREEN = "\033[32m" if _ENABLED else ""
|
|
36
|
+
YELLOW = "\033[33m" if _ENABLED else ""
|
|
37
|
+
BLUE = "\033[34m" if _ENABLED else ""
|
|
38
|
+
MAGENTA = "\033[35m" if _ENABLED else ""
|
|
39
|
+
CYAN = "\033[36m" if _ENABLED else ""
|
|
40
|
+
WHITE = "\033[37m" if _ENABLED else ""
|
|
41
|
+
DIM = "\033[2m" if _ENABLED else ""
|
|
42
|
+
|
|
43
|
+
# Bold
|
|
44
|
+
BOLD = "\033[1m" if _ENABLED else ""
|
|
45
|
+
RED_BOLD = "\033[1;31m" if _ENABLED else ""
|
|
46
|
+
GREEN_BOLD = "\033[1;32m" if _ENABLED else ""
|
|
47
|
+
YELLOW_BOLD = "\033[1;33m" if _ENABLED else ""
|
|
48
|
+
CYAN_BOLD = "\033[1;36m" if _ENABLED else ""
|
|
49
|
+
MAGENTA_BOLD = "\033[1;35m" if _ENABLED else ""
|
|
50
|
+
WHITE_BOLD = "\033[1;37m" if _ENABLED else ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def c(text: str, color: str) -> str:
|
|
54
|
+
"""Wrap text with color and reset."""
|
|
55
|
+
if not _ENABLED or not color:
|
|
56
|
+
return text
|
|
57
|
+
return f"{color}{text}{RST}"
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data type conversion utilities for Modbus register values.
|
|
3
|
+
|
|
4
|
+
Supports common data types used in industrial automation:
|
|
5
|
+
- int16/uint16: 16-bit integer (1 register)
|
|
6
|
+
- int32/uint32: 32-bit integer (2 registers)
|
|
7
|
+
- float32: 32-bit IEEE 754 float (2 registers)
|
|
8
|
+
- hex: Hexadecimal display
|
|
9
|
+
|
|
10
|
+
Supports byte order variants for 32-bit types:
|
|
11
|
+
- ABCD: Big-Endian (standard, default)
|
|
12
|
+
- CDBA: Word swap (common in Siemens)
|
|
13
|
+
- BADC: Byte swap
|
|
14
|
+
- DCBA: Little-Endian
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import List, Optional, Tuple, Union
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataType(Enum):
|
|
23
|
+
"""Supported data types for Modbus registers."""
|
|
24
|
+
INT16 = "int16"
|
|
25
|
+
UINT16 = "uint16"
|
|
26
|
+
INT32 = "int32"
|
|
27
|
+
UINT32 = "uint32"
|
|
28
|
+
FLOAT32 = "float"
|
|
29
|
+
HEX = "hex"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ByteOrder(Enum):
|
|
33
|
+
"""Byte order for multi-register (32-bit) values.
|
|
34
|
+
|
|
35
|
+
ABCD = standard Big-Endian (register0=high, register1=low)
|
|
36
|
+
CDBA = word swap (register0=low, register1=high)
|
|
37
|
+
BADC = byte swap within words
|
|
38
|
+
DCBA = full Little-Endian
|
|
39
|
+
"""
|
|
40
|
+
ABCD = "ABCD"
|
|
41
|
+
CDBA = "CDBA"
|
|
42
|
+
BADC = "BADC"
|
|
43
|
+
DCBA = "DCBA"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
DEFAULT_BYTE_ORDER = ByteOrder.ABCD
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_byte_order(order_str: str) -> ByteOrder:
|
|
50
|
+
"""Parse byte order string to ByteOrder enum."""
|
|
51
|
+
order_str = order_str.upper().strip()
|
|
52
|
+
mapping = {
|
|
53
|
+
"ABCD": ByteOrder.ABCD,
|
|
54
|
+
"BE": ByteOrder.ABCD,
|
|
55
|
+
"BIGENDIAN": ByteOrder.ABCD,
|
|
56
|
+
"CDBA": ByteOrder.CDBA,
|
|
57
|
+
"WORDSWAP": ByteOrder.CDBA,
|
|
58
|
+
"BADC": ByteOrder.BADC,
|
|
59
|
+
"BYTESWAP": ByteOrder.BADC,
|
|
60
|
+
"DCBA": ByteOrder.DCBA,
|
|
61
|
+
"LE": ByteOrder.DCBA,
|
|
62
|
+
"LITTLEENDIAN": ByteOrder.DCBA,
|
|
63
|
+
}
|
|
64
|
+
if order_str in mapping:
|
|
65
|
+
return mapping[order_str]
|
|
66
|
+
raise ValueError(f"Unknown byte order: {order_str}. Supported: {list(mapping.keys())}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _swap_bytes(val: int) -> int:
|
|
70
|
+
"""Swap bytes within a 16-bit value: 0xABCD -> 0xCDAB."""
|
|
71
|
+
return ((val & 0xFF) << 8) | ((val >> 8) & 0xFF)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _reorder_registers(registers: List[int], order: ByteOrder) -> Tuple[int, int]:
|
|
75
|
+
"""Reorder 2 registers according to byte order, return (high_word, low_word)."""
|
|
76
|
+
r0 = registers[0] & 0xFFFF
|
|
77
|
+
r1 = registers[1] & 0xFFFF
|
|
78
|
+
if order == ByteOrder.ABCD:
|
|
79
|
+
return r0, r1
|
|
80
|
+
elif order == ByteOrder.CDBA:
|
|
81
|
+
return r1, r0
|
|
82
|
+
elif order == ByteOrder.BADC:
|
|
83
|
+
return _swap_bytes(r0), _swap_bytes(r1)
|
|
84
|
+
elif order == ByteOrder.DCBA:
|
|
85
|
+
return _swap_bytes(r1), _swap_bytes(r0)
|
|
86
|
+
return r0, r1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _reorder_to_registers(high: int, low: int, order: ByteOrder) -> List[int]:
|
|
90
|
+
"""Convert (high_word, low_word) to register list according to byte order."""
|
|
91
|
+
if order == ByteOrder.ABCD:
|
|
92
|
+
return [high, low]
|
|
93
|
+
elif order == ByteOrder.CDBA:
|
|
94
|
+
return [low, high]
|
|
95
|
+
elif order == ByteOrder.BADC:
|
|
96
|
+
return [_swap_bytes(high), _swap_bytes(low)]
|
|
97
|
+
elif order == ByteOrder.DCBA:
|
|
98
|
+
return [_swap_bytes(low), _swap_bytes(high)]
|
|
99
|
+
return [high, low]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Register count needed for each data type
|
|
103
|
+
DATA_TYPE_REGISTERS = {
|
|
104
|
+
DataType.INT16: 1,
|
|
105
|
+
DataType.UINT16: 1,
|
|
106
|
+
DataType.INT32: 2,
|
|
107
|
+
DataType.UINT32: 2,
|
|
108
|
+
DataType.FLOAT32: 2,
|
|
109
|
+
DataType.HEX: 1,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_data_type(type_str: str) -> DataType:
|
|
114
|
+
"""
|
|
115
|
+
Parse data type string to DataType enum.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
type_str: Type string (e.g., "int16", "uint16", "3:float", "4:int32")
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
DataType enum value
|
|
122
|
+
"""
|
|
123
|
+
# Handle modpoll-style type strings like "3:int" or "4:float"
|
|
124
|
+
if ":" in type_str:
|
|
125
|
+
_, type_name = type_str.split(":", 1)
|
|
126
|
+
type_str = type_name
|
|
127
|
+
|
|
128
|
+
type_str = type_str.lower().strip()
|
|
129
|
+
|
|
130
|
+
mapping = {
|
|
131
|
+
"int16": DataType.INT16,
|
|
132
|
+
"int": DataType.INT16,
|
|
133
|
+
"16bit": DataType.INT16,
|
|
134
|
+
"uint16": DataType.UINT16,
|
|
135
|
+
"uint": DataType.UINT16,
|
|
136
|
+
"u16": DataType.UINT16,
|
|
137
|
+
"int32": DataType.INT32,
|
|
138
|
+
"32int": DataType.INT32,
|
|
139
|
+
"i32": DataType.INT32,
|
|
140
|
+
"uint32": DataType.UINT32,
|
|
141
|
+
"32uint": DataType.UINT32,
|
|
142
|
+
"u32": DataType.UINT32,
|
|
143
|
+
"float": DataType.FLOAT32,
|
|
144
|
+
"float32": DataType.FLOAT32,
|
|
145
|
+
"f32": DataType.FLOAT32,
|
|
146
|
+
"real": DataType.FLOAT32,
|
|
147
|
+
"hex": DataType.HEX,
|
|
148
|
+
"hexadecimal": DataType.HEX,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if type_str in mapping:
|
|
152
|
+
return mapping[type_str]
|
|
153
|
+
|
|
154
|
+
raise ValueError(f"Unknown data type: {type_str}. Supported: {list(mapping.keys())}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def registers_to_int16(registers: List[int], signed: bool = True) -> int:
|
|
158
|
+
"""
|
|
159
|
+
Convert 1 register to int16/uint16.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
registers: List of register values (16-bit each)
|
|
163
|
+
signed: If True, return signed int16
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Integer value
|
|
167
|
+
"""
|
|
168
|
+
if len(registers) < 1:
|
|
169
|
+
raise ValueError("int16 requires 1 register")
|
|
170
|
+
|
|
171
|
+
value = registers[0] & 0xFFFF
|
|
172
|
+
if signed and value >= 0x8000:
|
|
173
|
+
value -= 0x10000
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def registers_to_int32(
|
|
178
|
+
registers: List[int],
|
|
179
|
+
signed: bool = True,
|
|
180
|
+
byte_order: ByteOrder = DEFAULT_BYTE_ORDER,
|
|
181
|
+
) -> int:
|
|
182
|
+
"""
|
|
183
|
+
Convert 2 registers to int32/uint32.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
registers: List of register values (16-bit each)
|
|
187
|
+
signed: If True, return signed int32
|
|
188
|
+
byte_order: Byte order for register arrangement
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Integer value
|
|
192
|
+
"""
|
|
193
|
+
if len(registers) < 2:
|
|
194
|
+
raise ValueError("int32 requires 2 registers")
|
|
195
|
+
|
|
196
|
+
high, low = _reorder_registers(registers, byte_order)
|
|
197
|
+
value = (high << 16) | low
|
|
198
|
+
|
|
199
|
+
if signed and value >= 0x80000000:
|
|
200
|
+
value -= 0x100000000
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def registers_to_float32(
|
|
205
|
+
registers: List[int],
|
|
206
|
+
byte_order: ByteOrder = DEFAULT_BYTE_ORDER,
|
|
207
|
+
) -> float:
|
|
208
|
+
"""
|
|
209
|
+
Convert 2 registers to float32 (IEEE 754).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
registers: List of register values (16-bit each)
|
|
213
|
+
byte_order: Byte order for register arrangement
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Float value
|
|
217
|
+
"""
|
|
218
|
+
if len(registers) < 2:
|
|
219
|
+
raise ValueError("float32 requires 2 registers")
|
|
220
|
+
|
|
221
|
+
high, low = _reorder_registers(registers, byte_order)
|
|
222
|
+
value = (high << 16) | low
|
|
223
|
+
|
|
224
|
+
return struct.unpack(">f", struct.pack(">I", value))[0]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def registers_to_hex(registers: List[int], count: int = 1) -> str:
|
|
228
|
+
"""
|
|
229
|
+
Convert registers to hexadecimal string.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
registers: List of register values
|
|
233
|
+
count: Number of registers to format
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Hex string like "0x1234" or "0x1234 0x5678"
|
|
237
|
+
"""
|
|
238
|
+
hex_values = []
|
|
239
|
+
for i in range(min(count, len(registers))):
|
|
240
|
+
hex_values.append(f"0x{registers[i] & 0xFFFF:04X}")
|
|
241
|
+
return " ".join(hex_values)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def int16_to_registers(value: int, signed: bool = True) -> List[int]:
|
|
245
|
+
"""
|
|
246
|
+
Convert int16/uint16 to register list.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
value: Integer value
|
|
250
|
+
signed: If True, value is signed
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List with 1 register value
|
|
254
|
+
"""
|
|
255
|
+
if signed:
|
|
256
|
+
if not (-32768 <= value <= 32767):
|
|
257
|
+
raise ValueError(f"int16 value out of range: {value}")
|
|
258
|
+
if value < 0:
|
|
259
|
+
value = value + 0x10000
|
|
260
|
+
else:
|
|
261
|
+
if not (0 <= value <= 65535):
|
|
262
|
+
raise ValueError(f"uint16 value out of range: {value}")
|
|
263
|
+
|
|
264
|
+
return [value & 0xFFFF]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def int32_to_registers(
|
|
268
|
+
value: int,
|
|
269
|
+
signed: bool = True,
|
|
270
|
+
byte_order: ByteOrder = DEFAULT_BYTE_ORDER,
|
|
271
|
+
) -> List[int]:
|
|
272
|
+
"""
|
|
273
|
+
Convert int32/uint32 to register list.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
value: Integer value
|
|
277
|
+
signed: If True, value is signed
|
|
278
|
+
byte_order: Byte order for register arrangement
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List with 2 register values
|
|
282
|
+
"""
|
|
283
|
+
if signed:
|
|
284
|
+
if not (-2147483648 <= value <= 2147483647):
|
|
285
|
+
raise ValueError(f"int32 value out of range: {value}")
|
|
286
|
+
if value < 0:
|
|
287
|
+
value = value + 0x100000000
|
|
288
|
+
else:
|
|
289
|
+
if not (0 <= value <= 4294967295):
|
|
290
|
+
raise ValueError(f"uint32 value out of range: {value}")
|
|
291
|
+
|
|
292
|
+
high = (value >> 16) & 0xFFFF
|
|
293
|
+
low = value & 0xFFFF
|
|
294
|
+
return _reorder_to_registers(high, low, byte_order)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def float32_to_registers(
|
|
298
|
+
value: float,
|
|
299
|
+
byte_order: ByteOrder = DEFAULT_BYTE_ORDER,
|
|
300
|
+
) -> List[int]:
|
|
301
|
+
"""
|
|
302
|
+
Convert float32 to register list.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
value: Float value
|
|
306
|
+
byte_order: Byte order for register arrangement
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
List with 2 register values
|
|
310
|
+
"""
|
|
311
|
+
packed = struct.pack(">f", value)
|
|
312
|
+
integer_value = struct.unpack(">I", packed)[0]
|
|
313
|
+
|
|
314
|
+
high = (integer_value >> 16) & 0xFFFF
|
|
315
|
+
low = integer_value & 0xFFFF
|
|
316
|
+
return _reorder_to_registers(high, low, byte_order)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def convert_registers(
|
|
320
|
+
registers: List[int],
|
|
321
|
+
data_type: DataType,
|
|
322
|
+
count: int = 1,
|
|
323
|
+
byte_order: ByteOrder = DEFAULT_BYTE_ORDER,
|
|
324
|
+
) -> Union[str, int, float]:
|
|
325
|
+
"""
|
|
326
|
+
Convert registers to specified data type.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
registers: List of register values
|
|
330
|
+
data_type: Target data type
|
|
331
|
+
count: Number of registers to convert (for hex display)
|
|
332
|
+
byte_order: Byte order for 32-bit types
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Converted value as string, int, or float
|
|
336
|
+
"""
|
|
337
|
+
if data_type == DataType.INT16:
|
|
338
|
+
return registers_to_int16(registers, signed=True)
|
|
339
|
+
elif data_type == DataType.UINT16:
|
|
340
|
+
return registers_to_int16(registers, signed=False)
|
|
341
|
+
elif data_type == DataType.INT32:
|
|
342
|
+
return registers_to_int32(registers, signed=True, byte_order=byte_order)
|
|
343
|
+
elif data_type == DataType.UINT32:
|
|
344
|
+
return registers_to_int32(registers, signed=False, byte_order=byte_order)
|
|
345
|
+
elif data_type == DataType.FLOAT32:
|
|
346
|
+
return registers_to_float32(registers, byte_order=byte_order)
|
|
347
|
+
elif data_type == DataType.HEX:
|
|
348
|
+
return registers_to_hex(registers, count=count)
|
|
349
|
+
else:
|
|
350
|
+
raise ValueError(f"Unsupported data type: {data_type}")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def format_value(
|
|
354
|
+
value: Union[str, int, float],
|
|
355
|
+
data_type: DataType,
|
|
356
|
+
hex_format: bool = False,
|
|
357
|
+
) -> str:
|
|
358
|
+
"""
|
|
359
|
+
Format value for display.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
value: Value to format
|
|
363
|
+
data_type: Data type of the value
|
|
364
|
+
hex_format: If True, also show hex representation
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Formatted string
|
|
368
|
+
"""
|
|
369
|
+
if data_type == DataType.HEX:
|
|
370
|
+
return str(value)
|
|
371
|
+
elif data_type == DataType.FLOAT32:
|
|
372
|
+
return f"{value:.6f}"
|
|
373
|
+
elif data_type in (DataType.INT16, DataType.INT32):
|
|
374
|
+
result = str(value)
|
|
375
|
+
if hex_format and isinstance(value, int):
|
|
376
|
+
result += f" (0x{value & 0xFFFFFFFF:08X}" if value < 0 else f" (0x{value:08X}"
|
|
377
|
+
result += ")"
|
|
378
|
+
return result
|
|
379
|
+
elif data_type in (DataType.UINT16, DataType.UINT32):
|
|
380
|
+
result = str(value)
|
|
381
|
+
if hex_format:
|
|
382
|
+
hex_width = 4 if data_type == DataType.UINT16 else 8
|
|
383
|
+
result += f" (0x{value:0{hex_width}X})"
|
|
384
|
+
return result
|
|
385
|
+
else:
|
|
386
|
+
return str(value)
|