# lr-modbus

Lightweight Modbus RTU helpers tailored for FTDI-based hardware-in-the-loop setups. The package powers the CLI and automation utilities that ship with the UART↔RS-485 development board, but the components stand on their own and can be embedded into any Modbus RTU workflow.

## Features
- Blocking client with helpers for holding-register reads, single-register writes, and raw function dispatch
- Reference server with a thread-safe in-memory data model, optional listen-only mode, and deterministic shutdown hooks
- CRC-safe frame codec plus transactional serial transport tuned for half-duplex FTDI adapters
- Batteries-included CLI (`lr-modbus`) for quick experiments, scripting, and test automation

## Installation
```bash
python -m pip install lr-modbus
```

For development work inside this repository, install in editable mode together with the extra tooling:
```bash
python -m pip install -e .[dev]
```

## Quick Start
### CLI
```bash
# Launch a server that seeds two holding registers and exits after 50 requests
lr-modbus --port /dev/ttyUSB0 server --holding 0=0x2A,1=0x1337 --max-requests 50

# Read two registers from another adapter
lr-modbus --port /dev/ttyUSB1 client --action read-holding --address 0 --count 2

# Write a single register
lr-modbus --port /dev/ttyUSB1 client --action write-single --address 5 --value 1234
```

The CLI defaults to 115200-8N1, enforces Modbus RTU `t3.5` frame gaps, and lets you override port settings via `--baudrate`, `--bytesize`, `--parity`, `--stopbits`, and `--timeout`.

### Library usage
```python
from lr_modbus import ModbusClient, ModbusServer, InMemoryDataModel
from lr_modbus.transport import SerialDeviceConfig, create_serial_transport

config = SerialDeviceConfig(port="/dev/ttyUSB0")
transport = create_serial_transport(config)
client = ModbusClient(transport, unit_id=1)

with transport:
    registers = client.read_holding_registers(0, 4)
    client.write_single_register(10, 0xBEEF)
```

To host a test server in the same script:
```python
data_model = InMemoryDataModel({0: 0x2A})
server = ModbusServer(transport, unit_id=1, data_model=data_model)
server.serve_forever(max_requests=10)
```

The transport exposes `transact()`, `read_frame()`, and `write_frame()` helpers for advanced use-cases such as bit-banging DE/RE GPIO hooks.

## Testing
```bash
make test
```

The target runs `pytest` with coverage against `src/lr_modbus`. Tests rely on transport factories, so no physical hardware is required.

## Versioning & publishing
Package metadata and version markers live in `pyproject.toml` and `src/lr_modbus/__init__.py`. To publish a release:
```bash
python -m pip install build twine
python -m build
python -m twine upload dist/*
```

## License
This module follows the same license as the parent repository. See the root `LICENSE` file for details.
