feat: Add Modbus RTU support with multi-drop architecture#83
Conversation
This commit adds Modbus RTU (serial) communication support to the modbus master plugin, enabling serial communication with Modbus slave devices alongside existing TCP functionality. Key changes: 1. Serial ports REST endpoint (webserver/app.py): - New GET endpoint at /api/serial-ports - Lists available serial ports using pyserial 2. Connection manager refactoring (modbus_master_connection.py): - Support for both TCP and RTU transport types - Factory method _create_client() for transport-specific clients - RTU parameters: serial_port, baud_rate, parity, stop_bits, data_bits 3. Config model updates (modbus_master_config_model.py): - TransportType and ParityType type literals - RTU-specific fields in ModbusDeviceConfig - Transport-specific validation rules - Multi-drop validation (duplicate slave IDs on same bus) 4. Plugin updates (modbus_master_plugin.py): - New ModbusRtuBusHandler class for multi-drop RTU support - One thread per serial bus, handling multiple slave IDs - group_rtu_devices_by_bus() helper function - Updated start_loop() to separate TCP and RTU device handling 5. Dependencies (requirements.txt): - Added pyserial>=3.5 Multi-drop RTU architecture: - Devices on same serial port share a single connection - Each device has unique slave_id (1-247) - IO operations include slave_id to route to correct device - Bus grouping key: serial_port:baud_rate:parity:stop_bits:data_bits Backward compatibility: - Existing TCP configurations work without changes - Missing transport field defaults to "tcp" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds Modbus RTU (serial) communication support alongside the existing Modbus TCP functionality, implementing a multi-drop architecture that allows multiple devices to share a single serial port using unique slave IDs.
Changes:
- Adds REST API endpoint to list available serial ports
- Extends configuration model to support both TCP and RTU transport types with appropriate validation
- Refactors connection manager to create either TCP or RTU clients based on transport type
- Implements multi-drop RTU bus handler that manages multiple slave devices on a single serial connection
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| webserver/app.py | Adds /api/serial-ports endpoint to list available serial ports for frontend configuration |
| modbus_master_config_model.py | Extends device config with RTU parameters, transport-specific validation, and multi-drop slave ID conflict detection |
| requirements.txt | Adds pyserial dependency for serial port communication |
| modbus_master_plugin.py | Implements RTU bus handler for multi-drop architecture and updates plugin startup to handle both TCP and RTU devices |
| modbus_master_connection.py | Refactors connection manager to support both TCP and RTU transports with factory method for client creation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| all_cycle_times = [p.cycle_time_ms for d in devices for p in d.io_points] | ||
| self.gcd_cycle_time_ms = calculate_gcd_of_cycle_times( | ||
| [type('obj', (object,), {'cycle_time_ms': ct})() for ct in all_cycle_times] | ||
| ) if all_cycle_times else 1000 |
There was a problem hiding this comment.
The creation of anonymous objects using type('obj', (object,), {'cycle_time_ms': ct})() is unnecessarily complex and hard to understand. Consider creating a simple dataclass or named tuple to represent cycle time objects, or refactor calculate_gcd_of_cycle_times to accept a list of integers directly instead of requiring objects with a cycle_time_ms attribute.
| gcd_cycle_time_seconds = self.gcd_cycle_time_ms / 1000.0 | ||
|
|
||
| if not self.all_io_points: | ||
| self.logger.warn(f"[{self.name}] No I/O points defined. Stopping thread.") |
There was a problem hiding this comment.
The warn method is deprecated. Use warning instead to follow Python's standard logging best practices.
| self.logger.warn(f"[{self.name}] No I/O points defined. Stopping thread.") | |
| self.logger.warning(f"[{self.name}] No I/O points defined. Stopping thread.") |
| address, count=count, device_id=slave_id | ||
| ) | ||
| else: | ||
| self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}") |
There was a problem hiding this comment.
The warn method is deprecated. Use warning instead to follow Python's standard logging best practices.
| self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}") | |
| self.logger.warning(f"[{self.name}] Unsupported read FC: {point.fc}") |
| address, values_to_write, device_id=slave_id | ||
| ) | ||
| else: | ||
| self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}") |
There was a problem hiding this comment.
The warn method is deprecated. Use warning instead to follow Python's standard logging best practices.
| self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}") | |
| self.logger.warning(f"[{self.name}] Unsupported write FC: {point.fc}") |
The handle_list_serial_ports endpoint in app.py needs pyserial to enumerate available serial ports, but it runs in the main webserver context. Previously pyserial was only installed in the modbus_master plugin's virtual environment. This fix adds pyserial to the main requirements.txt so the serial port listing API endpoint works correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Changes
1. Serial Ports REST Endpoint (
webserver/app.py)GET /api/serial-portsendpoint2. Connection Manager (
modbus_master_connection.py)_create_client()creates appropriate pymodbus client3. Config Model (
modbus_master_config_model.py)TransportType('tcp' | 'rtu') andParityType('N' | 'E' | 'O')ModbusDeviceConfigwith RTU-specific fields4. Plugin Updates (
modbus_master_plugin.py)ModbusRtuBusHandlerclass for multi-drop RTU supportgroup_rtu_devices_by_bus()helper functionstart_loop()to separate TCP and RTU device handling5. Dependencies (
requirements.txt)pyserial>=3.5Multi-Drop Architecture
serial_port:baud_rate:parity:stop_bits:data_bitsBackward Compatibility
transportfield defaults to 'tcp'Test plan
🤖 Generated with Claude Code