Skip to content

feat: Add Modbus RTU support with multi-drop architecture#83

Merged
thiagoralves merged 2 commits into
developmentfrom
feature/modbus-rtu-support
Jan 21, 2026
Merged

feat: Add Modbus RTU support with multi-drop architecture#83
thiagoralves merged 2 commits into
developmentfrom
feature/modbus-rtu-support

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

Summary

  • Adds Modbus RTU (serial) communication support alongside existing Modbus TCP functionality
  • Implements multi-drop architecture allowing multiple devices on the same serial port with different slave IDs
  • Adds REST endpoint to list available serial ports for the frontend

Changes

1. Serial Ports REST Endpoint (webserver/app.py)

  • New GET /api/serial-ports endpoint
  • Returns list of available serial ports with device path and description

2. Connection Manager (modbus_master_connection.py)

  • Refactored to support both TCP and RTU transport types
  • Factory method _create_client() creates appropriate pymodbus client
  • Added RTU parameters: serial_port, baud_rate, parity, stop_bits, data_bits

3. Config Model (modbus_master_config_model.py)

  • Added TransportType ('tcp' | 'rtu') and ParityType ('N' | 'E' | 'O')
  • Extended ModbusDeviceConfig with RTU-specific fields
  • Transport-specific validation rules
  • Multi-drop validation (prevents 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 Architecture

  • Devices on the same serial port share a single connection
  • Each device has a unique slave_id (1-247)
  • All IO operations include the device-specific slave_id
  • 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'

Test plan

  • Test existing Modbus TCP devices still work
  • Test RTU device with single slave ID
  • Test multi-drop RTU with multiple devices on same serial port
  • Test serial ports API endpoint returns correct ports
  • Test connection retry and reconnection logic for RTU

🤖 Generated with Claude Code

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>
@thiagoralves thiagoralves requested a review from Copilot January 21, 2026 03:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +485 to +488
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
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.")
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warn method is deprecated. Use warning instead to follow Python's standard logging best practices.

Suggested change
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.")

Copilot uses AI. Check for mistakes.
address, count=count, device_id=slave_id
)
else:
self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}")
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warn method is deprecated. Use warning instead to follow Python's standard logging best practices.

Suggested change
self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}")
self.logger.warning(f"[{self.name}] Unsupported read FC: {point.fc}")

Copilot uses AI. Check for mistakes.
address, values_to_write, device_id=slave_id
)
else:
self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}")
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warn method is deprecated. Use warning instead to follow Python's standard logging best practices.

Suggested change
self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}")
self.logger.warning(f"[{self.name}] Unsupported write FC: {point.fc}")

Copilot uses AI. Check for mistakes.
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>
@thiagoralves thiagoralves merged commit 414bb91 into development Jan 21, 2026
1 check passed
@thiagoralves thiagoralves deleted the feature/modbus-rtu-support branch January 21, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants