diff --git a/README.md b/README.md index 3ca41d1..d8ae0ac 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # modbus-cli ``` - _ _ _ - _ __ ___ __| |__ _ _ ___ __ _| (_) - | ' \/ _ \/ _` / _` || (_-< / _| | | | - |_|_|_\___/\__,_\__,_|\_/__/ \__|_|_|_| + __ __ ___ ___ ___ _ _ ___ ___ _ ___ + | \/ |/ _ \| \| _ )| | | / __| ___ / __| | |_ _| + | |\/| | (_) | |) | _ \| |_| \__ \ |___| (__| |__ | | + |_| |_|\___/|___/|___/ \___/|___/ \___|____|___| ``` **Like curl, but for Modbus.** @@ -87,6 +87,12 @@ modbus read --serial /dev/ttyUSB0 40001 --slave 2 --baudrate 19200 # Signed 16-bit values modbus read 192.168.1.10 40001 -c 5 -f signed + +# Decode as float32 values (2 registers per float) +modbus read 192.168.1.10 40001 -c 4 --float + +# Adjust byte/word ordering for vendor-specific layouts +modbus read 192.168.1.10 40001 -c 4 --float --byte-order BE --word-order LE ``` Output includes styled panels, connection status, and visual value bars showing register magnitude at a glance. diff --git a/modbus_cli/cli.py b/modbus_cli/cli.py index 3cd7a95..ca3bd72 100644 --- a/modbus_cli/cli.py +++ b/modbus_cli/cli.py @@ -2,6 +2,7 @@ import sys import time +import struct import click from pymodbus.client import ModbusTcpClient, ModbusSerialClient @@ -86,6 +87,27 @@ def _format_value(value, fmt): return str(value) +def _decode_float32_pair(words, byte_order="BE", word_order="BE"): + """Decode 2x16-bit Modbus words into one IEEE-754 float32 value.""" + if len(words) != 2: + raise ValueError("Float decoding requires exactly two 16-bit words") + + ordered_words = list(words) + if word_order == "LE": + ordered_words.reverse() + + data = bytearray() + for word in ordered_words: + hi = (word >> 8) & 0xFF + lo = word & 0xFF + if byte_order == "BE": + data.extend((hi, lo)) + else: + data.extend((lo, hi)) + + return struct.unpack(">f", bytes(data))[0] + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -130,8 +152,16 @@ def cli(ctx): @click.option("--format", "-f", "fmt", type=click.Choice(["decimal", "hex", "bin", "signed"]), default="decimal", help="Output format (default: decimal).") +@click.option("--float", "decode_float", is_flag=True, + help="Decode register pairs as 32-bit IEEE 754 floats.") +@click.option("--byte-order", type=click.Choice(["BE", "LE"]), + default="BE", show_default=True, + help="Byte order within each 16-bit register for --float mode.") +@click.option("--word-order", type=click.Choice(["BE", "LE"]), + default="BE", show_default=True, + help="Word order across each 32-bit float pair for --float mode.") @click.option("--timeout", default=3.0, help="Timeout in seconds (default: 3).") -def read(host, address, port, serial, baudrate, slave, count, reg_type, fmt, timeout): +def read(host, address, port, serial, baudrate, slave, count, reg_type, fmt, decode_float, byte_order, word_order, timeout): """Read Modbus registers. \b @@ -165,6 +195,50 @@ def read(host, address, port, serial, baudrate, slave, count, reg_type, fmt, tim else: values = resp.registers + if decode_float: + if detected_type in ("coil", "discrete"): + error_panel("--float is only supported for holding/input registers") + sys.exit(1) + if count % 2 != 0: + error_panel("--float requires an even --count (2 registers per float)") + sys.exit(1) + + table = Table( + show_header=True, + header_style="bold #00d4aa", + border_style="#636e72", + title_style="bold #7c6ff7", + row_styles=["", "dim"], + pad_edge=True, + expand=False, + ) + table.add_column("Address", style="bold #7c6ff7", justify="right", min_width=8) + table.add_column("Float", style="bold #00d4aa", justify="right", min_width=12) + table.add_column("Raw Words", style="#dfe6e9", justify="right", min_width=18) + + for i in range(0, len(values), 2): + addr_display = address + i if not reg_type else raw_address + i + pair = [int(values[i]), int(values[i + 1])] + decoded = _decode_float32_pair(pair, byte_order=byte_order, word_order=word_order) + table.add_row( + str(addr_display), + f"{decoded:.6g}", + f"[{pair[0]}, {pair[1]}]", + ) + + console.print(Panel( + table, + border_style="#636e72", + title=f"[bold #00d4aa]{detected_type}[/] [dim]float32 decode[/]", + subtitle=( + f"[dim]{count} register(s), byte-order={byte_order}, " + f"word-order={word_order}, target={target}[/]" + ), + padding=(1, 2), + )) + console.print() + return + table = Table( show_header=True, header_style="bold #00d4aa", diff --git a/modbus_cli/theme.py b/modbus_cli/theme.py index 308248a..7d24eed 100644 --- a/modbus_cli/theme.py +++ b/modbus_cli/theme.py @@ -36,10 +36,10 @@ console = Console(theme=custom_theme) -BANNER = r"""[bold #00d4aa] _ _ _ [/] -[bold #1ad4b8] _ __ ___ __| |__ _ _ ___ __ _| (_)[/] -[bold #33d4c6] | ' \/ _ \/ _` / _` || (_-< / _| | | |[/] -[bold #4dd4d4] |_|_|_\___/\__,_\__,_|\_/__/ \__|_|_|_|[/] +BANNER = r"""[bold #00d4aa] __ __ ___ ___ ___ _ _ ___ ___ _ ___[/] +[bold #1ad4b8] | \/ |/ _ \| \| _ )| | | / __| ___ / __| | |_ _|[/] +[bold #33d4c6] | |\/| | (_) | |) | _ \| |_| \__ \ |___| (__| |__ | |[/] +[bold #4dd4d4] |_| |_|\___/|___/|___/ \___/|___/ \___|____|___|[/] """ TAGLINE = "[dim]like curl, but for modbus[/dim]"