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.
@@ -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)