feat(infrastructure): implement ModbusRelayController with timeout handling
Add real Modbus TCP communication through ModbusRelayController: - T025a: Connection setup with Arc<Mutex<Context>> and configurable timeout - T025b: read_coils_with_timeout() helper wrapping tokio::time::timeout - T025c: write_single_coil_with_timeout() with nested Result handling - T025d: RelayController::read_relay_state() using timeout helper - T025e: RelayController::write_relay_state() with state conversion - Additional: Complete RelayController trait with all required methods - Domain support: RelayId::to_modbus_address(), RelayState conversion helpers Implements hexagonal architecture with infrastructure layer properly depending on domain types. Includes structured logging at key operations. TDD phase: green (implementation following test stubs from T023-T024) Ref: T025a-T025e (specs/001-modbus-relay-control/tasks.md)
This commit is contained in:
@@ -331,13 +331,13 @@
|
||||
|
||||
**Complexity**: High → Broken into 6 sub-tasks
|
||||
**Uncertainty**: High
|
||||
**Rationale**: Nested Result handling, Arc<Mutex> synchronization, timeout wrapping
|
||||
**Rationale**: Nested Result handling, `Arc<Mutex>` synchronization, timeout wrapping
|
||||
**Protocol**: Native Modbus TCP (MBAP header, no CRC16 validation)
|
||||
|
||||
- [ ] **T025a** [US1] [TDD] Implement ModbusRelayController connection setup
|
||||
- Struct: ModbusRelayController { ctx: Arc<Mutex<Context>>, timeout_duration: Duration }
|
||||
- Constructor: new(host, port, slave_id, timeout_secs) → Result<Self, ControllerError>
|
||||
- Use tokio_modbus::client::tcp::connect_slave()
|
||||
- [x] **T025a** [US1] [TDD] Implement ModbusRelayController connection setup
|
||||
- Struct: `ModbusRelayController { ctx: Arc<Mutex<Context>>, timeout_duration: Duration }`
|
||||
- Constructor: `new(host, port, slave_id, timeout_secs) → Result<Self, ControllerError>`
|
||||
- Use `tokio_modbus::client::tcp::connect_slave()`
|
||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
||||
- **Complexity**: Medium | **Uncertainty**: Medium
|
||||
|
||||
@@ -372,13 +372,13 @@
|
||||
```
|
||||
|
||||
**TDD Checklist** (write these tests FIRST):
|
||||
- [ ] Test: new() with valid config connects successfully
|
||||
- [ ] Test: new() with invalid host returns ConnectionError
|
||||
- [ ] Test: new() stores correct timeout_duration
|
||||
- [x] Test: `new()` with valid config connects successfully
|
||||
- [x] Test: `new()` with invalid host returns ConnectionError
|
||||
- [x] Test: `new()` stores correct timeout_duration
|
||||
|
||||
- [ ] **T025b** [US1] [TDD] Implement timeout-wrapped read_coils helper
|
||||
- Private method: read_coils_with_timeout(addr: u16, count: u16) → Result<Vec<bool>, ControllerError>
|
||||
- Wrap ctx.read_coils() with tokio::time::timeout()
|
||||
- [x] **T025b** [US1] [TDD] Implement timeout-wrapped read_coils helper
|
||||
- Private method: `read_coils_with_timeout(addr: u16, count: u16) → Result<Vec<bool>, ControllerError>`
|
||||
- Wrap `ctx.read_coils()` with `tokio::time::timeout()`
|
||||
- Handle nested Result: timeout → io::Error → Modbus Exception
|
||||
- **Note**: Modbus TCP uses MBAP header (no CRC validation needed)
|
||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
||||
@@ -407,13 +407,13 @@
|
||||
```
|
||||
|
||||
**TDD Checklist**:
|
||||
- [ ] Test: read_coils_with_timeout() returns coil values on success
|
||||
- [ ] Test: read_coils_with_timeout() returns Timeout error when operation exceeds timeout
|
||||
- [ ] Test: read_coils_with_timeout() returns ConnectionError on io::Error
|
||||
- [ ] Test: read_coils_with_timeout() returns ModbusException on protocol error
|
||||
- [x] Test: `read_coils_with_timeout()` returns coil values on success
|
||||
- [x] Test: `read_coils_with_timeout()` returns Timeout error when operation exceeds timeout
|
||||
- [x] Test: `read_coils_with_timeout()` returns ConnectionError on io::Error
|
||||
- [x] Test: `read_coils_with_timeout()` returns ModbusException on protocol error
|
||||
|
||||
- [ ] **T025c** [US1] [TDD] Implement timeout-wrapped write_single_coil helper
|
||||
- Private method: write_single_coil_with_timeout(addr: u16, value: bool) → Result<(), ControllerError>
|
||||
- [x] **T025c** [US1] [TDD] Implement timeout-wrapped `write_single_coil` helper
|
||||
- Private method: `write_single_coil_with_timeout(addr: u16, value: bool) → Result<(), ControllerError>`
|
||||
- Similar nested Result handling as T025b
|
||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
||||
- **Complexity**: Low | **Uncertainty**: Low
|
||||
@@ -438,13 +438,13 @@
|
||||
```
|
||||
|
||||
**TDD Checklist**:
|
||||
- [ ] Test: write_single_coil_with_timeout() succeeds for valid write
|
||||
- [ ] Test: write_single_coil_with_timeout() returns Timeout on slow device
|
||||
- [ ] Test: write_single_coil_with_timeout() returns appropriate error on failure
|
||||
- [x] Test: `write_single_coil_with_timeout()` succeeds for valid write
|
||||
- [x] Test: `write_single_coil_with_timeout()` returns Timeout on slow device
|
||||
- [x] Test: `write_single_coil_with_timeout()` returns appropriate error on failure
|
||||
|
||||
- [ ] **T025d** [US1] [TDD] Implement RelayController::read_state() using helpers
|
||||
- [x] **T025d** [US1] [TDD] Implement RelayController::read_state() using helpers
|
||||
- Convert RelayId → ModbusAddress (0-based)
|
||||
- Call read_coils_with_timeout(addr, 1)
|
||||
- Call `read_coils_with_timeout(addr, 1)`
|
||||
- Convert bool → RelayState
|
||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
||||
- **Complexity**: Low | **Uncertainty**: Low
|
||||
@@ -463,14 +463,14 @@
|
||||
```
|
||||
|
||||
**TDD Checklist**:
|
||||
- [ ] Test: read_state(RelayId(1)) returns On when coil is true
|
||||
- [ ] Test: read_state(RelayId(1)) returns Off when coil is false
|
||||
- [ ] Test: read_state() propagates ControllerError from helper
|
||||
- [x] Test: `read_state(RelayId(1))` returns On when coil is true
|
||||
- [x] Test: `read_state(RelayId(1))` returns Off when coil is false
|
||||
- [x] Test: `read_state()` propagates ControllerError from helper
|
||||
|
||||
- [ ] **T025e** [US1] [TDD] Implement RelayController::write_state() using helpers
|
||||
- [x] **T025e** [US1] [TDD] Implement `RelayController::write_state()` using helpers
|
||||
- Convert RelayId → ModbusAddress
|
||||
- Convert RelayState → bool (On=true, Off=false)
|
||||
- Call write_single_coil_with_timeout()
|
||||
- Call `write_single_coil_with_timeout()`
|
||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
||||
- **Complexity**: Low | **Uncertainty**: Low
|
||||
|
||||
@@ -484,13 +484,13 @@
|
||||
```
|
||||
|
||||
**TDD Checklist**:
|
||||
- [ ] Test: write_state(RelayId(1), RelayState::On) writes true to coil
|
||||
- [ ] Test: write_state(RelayId(1), RelayState::Off) writes false to coil
|
||||
- [x] Test: `write_state(RelayId(1), RelayState::On)` writes true to coil
|
||||
- [x] Test: `write_state(RelayId(1), RelayState::Off)` writes false to coil
|
||||
|
||||
- [ ] **T025f** [US1] [TDD] Implement RelayController::read_all() and write_all()
|
||||
- read_all(): Call read_coils_with_timeout(0, 8), map to Vec<(RelayId, RelayState)>
|
||||
- write_all(): Loop over RelayId 1-8, call write_state() for each
|
||||
- Add firmware_version() method (read holding register 0x9999, optional)
|
||||
- [x] **T025f** [US1] [TDD] Implement `RelayController::read_all()` and `write_all()`
|
||||
- `read_all()`: Call `read_coils_with_timeout(0, 8)`, map to `Vec<(RelayId, RelayState)>`
|
||||
- `write_all()`: Loop over RelayId 1-8, call `write_state()` for each
|
||||
- Add `firmware_version()` method (read holding register 0x9999, optional)
|
||||
- **File**: src/infrastructure/modbus/modbus_controller.rs
|
||||
- **Complexity**: Medium | **Uncertainty**: Low
|
||||
|
||||
@@ -518,9 +518,9 @@
|
||||
```
|
||||
|
||||
**TDD Checklist**:
|
||||
- [ ] Test: read_all() returns 8 relay states
|
||||
- [ ] Test: write_all(RelayState::On) turns all relays on
|
||||
- [ ] Test: write_all(RelayState::Off) turns all relays off
|
||||
- [x] Test: `read_all()` returns 8 relay states
|
||||
- [x] Test: `write_all(RelayState::On)` turns all relays on
|
||||
- [x] Test: `write_all(RelayState::Off)` turns all relays off
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user