import argparse
import logging
import os
import sys
import time
def _bootstrap_venv() -> None:
import os
import site
here = os.path.abspath(__file__)
for _ in range(6):
here = os.path.dirname(here)
lib = os.path.join(here, ".venv", "lib")
if os.path.isdir(lib):
for entry in os.listdir(lib):
sp = os.path.join(lib, entry, "site-packages")
if os.path.isdir(sp):
site.addsitedir(sp)
return
_bootstrap_venv()
del _bootstrap_venv
import modbus_rs
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("modbus-client")
def _bar(value: float, lo: float, hi: float, width: int = 20) -> str:
fraction = max(0.0, min(1.0, (value - lo) / (hi - lo)))
filled = round(fraction * width)
return "[" + "█" * filled + "░" * (width - filled) + "]"
def _bool_list(values: list[bool]) -> str:
return " ".join(("ON " if v else "off") for v in values)
def run_demo_sync(client: modbus_rs.TcpClient | modbus_rs.SerialClient, iteration: int):
sep = "─" * 60
ir = client.read_input_registers(0, 4)
temp_c = ir[0] / 10.0
hum_pct = ir[1] / 10.0
pressure_hpa = ir[2]
uptime_s = ir[3]
print(f"\n{sep}")
print(f" Iteration #{iteration} uptime={uptime_s}s")
print(sep)
print(f" Temperature : {temp_c:6.1f} °C {_bar(temp_c, 20, 30)}")
print(f" Humidity : {hum_pct:6.1f} %RH {_bar(hum_pct, 40, 60)}")
print(f" Pressure : {pressure_hpa:4d} hPa {_bar(pressure_hpa, 1000, 1020)}")
hr = client.read_holding_registers(0, 4)
setpoint_c = hr[0] / 10.0
output_pct = hr[1]
print(f"\n Setpoint : {setpoint_c:6.1f} °C (holding[0])")
print(f" Output : {output_pct:3d} % (holding[1])")
new_sp = round((setpoint_c + 0.5) * 10) % (300 + 1) new_sp = new_sp if new_sp >= 200 else 200 addr, written_val = client.write_register(0, new_sp)
log.info("wrote setpoint %d (%.1f °C) → holding[%d]",
written_val, written_val / 10.0, addr)
client.write_registers(1, [50, 0, 0])
coils = client.read_coils(0, 8)
print(f"\n Coils [0-7] : {_bool_list(coils)}")
client.write_coil(0, not coils[0])
di = client.read_discrete_inputs(0, 8)
print(f" DI [0-7] : {_bool_list(di)}")
alarm_reg = client.read_holding_registers(7, 1)[0]
alarm_over_temp = bool(alarm_reg & 0x01)
print(f" Over-temp alarm : {'⚠ ACTIVE' if alarm_over_temp else 'OK'}")
fifo = client.read_fifo_queue(0)
if fifo:
print(f" FIFO events : {[hex(x) for x in fifo]}")
else:
print(f" FIFO events : (empty)")
if iteration % 5 == 0:
client.mask_write_register(2, and_mask=0xFFF0, or_mask=0x0100)
log.info("mask_write holding[2] AND=0xFFF0 OR=0x0100")
def make_tcp_client(host: str, port: int, unit_id: int) -> modbus_rs.TcpClient:
return modbus_rs.TcpClient(host, port=port, unit_id=unit_id, timeout_ms=2000)
def make_serial_client(
port: str, baud: int, unit_id: int, mode: str
) -> modbus_rs.SerialClient:
return modbus_rs.SerialClient(port, baud_rate=baud, unit_id=unit_id,
mode=mode, timeout_ms=2000)
def poll_loop(client_factory, interval: float, max_errors: int = 5):
consecutive_errors = 0
iteration = 0
while True:
log.info("Connecting …")
try:
client = client_factory()
client.connect()
log.info("Connected.")
consecutive_errors = 0
while True:
iteration += 1
try:
run_demo_sync(client, iteration)
consecutive_errors = 0
except modbus_rs.ModbusTimeout:
log.warning("Request timed out (iteration %d)", iteration)
consecutive_errors += 1
except modbus_rs.ModbusDeviceException as exc:
code = exc.args[1] if len(exc.args) > 1 else None
if code == 0x01:
log.warning(
"Device exception: %s (expected in demo when FC22 mask-write is not implemented by the server app)",
exc,
)
else:
log.error("Device exception: %s", exc)
consecutive_errors += 1
except modbus_rs.ModbusConnectionError:
log.warning("Connection lost — reconnecting")
break
if consecutive_errors >= max_errors:
log.error("Too many consecutive errors; reconnecting")
break
time.sleep(interval)
except modbus_rs.ModbusConnectionError as exc:
log.error("Cannot connect: %s", exc)
except modbus_rs.ModbusConfigError as exc:
log.error("Configuration error: %s — aborting", exc)
sys.exit(1)
except KeyboardInterrupt:
log.info("Stopped by user")
return
log.info("Waiting 3 s before reconnect …")
try:
time.sleep(3)
except KeyboardInterrupt:
log.info("Stopped by user")
return
def main():
parser = argparse.ArgumentParser(description="modbus-rs Python client example")
parser.add_argument("--mode", choices=["tcp", "serial"], default="tcp")
parser.add_argument("--host", default="127.0.0.1",
help="Modbus TCP host (default: 127.0.0.1)")
parser.add_argument("--port", default=None,
help="TCP port (default: 5020) or serial device path")
parser.add_argument("--baud", type=int, default=9600,
help="Serial baud rate (default: 9600)")
parser.add_argument("--serial-mode", choices=["rtu", "ascii"], default="rtu")
parser.add_argument("--unit-id", type=int, default=1)
parser.add_argument("--interval", type=float, default=2.0,
help="Polling interval in seconds (default: 2.0)")
args = parser.parse_args()
if args.mode == "tcp":
tcp_port = int(args.port) if args.port else 5020
factory = lambda: make_tcp_client(args.host, tcp_port, args.unit_id)
else:
serial_port = args.port or "/dev/ttyUSB0"
factory = lambda: make_serial_client(
serial_port, args.baud, args.unit_id, args.serial_mode
)
poll_loop(factory, interval=args.interval)
if __name__ == "__main__":
main()