docs: add project specs and documentation for Modbus relay control

Initialize project documentation structure:
- Add CLAUDE.md with development guidelines and architecture principles
- Add project constitution (v1.1.0) with hexagonal architecture and SOLID principles
- Add MCP server configuration for Context7 integration

Feature specification (001-modbus-relay-control):
- Complete feature spec for web-based Modbus relay control system
- Implementation plan with TDD approach using SQLx for persistence
- Type-driven development design for domain types
- Technical decisions document (SQLx over rusqlite, SQLite persistence)
- Detailed task breakdown (94 tasks across 8 phases)
- Specification templates for future features

Documentation:
- Modbus POE ETH Relay hardware documentation
- Modbus Application Protocol specification (PDF)

Project uses SQLx for compile-time verified SQL queries, aligned with
type-driven development principles.
This commit is contained in:
2025-12-21 18:19:21 +01:00
parent d5a2859b64
commit a683810bdc
15 changed files with 7960 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,514 @@
# Modbus POE ETH Relay
Parsed from https://www.waveshare.com/wiki/Modbus_POE_ETH_Relay
# Overview
## Hardware Description
### Hardware Connection
- Connect the Modbus POE ETH Relay to the LAN via a network cable, and supply power through the power port or the POE.
### Electrical and Relay Safety Instructions
- This product must be operated by professional electricians or qualified personnel. During use, ensure electrical safety, leakage protection, and proper insulation.
- Before installing, maintaining, or replacing the relay device, always turn off the power and unplug the device.
- Do not attempt to disassemble the relay device to avoid damage or the risk of electric shock.
- Properly install and place the relay device. Do not use it in humid, overheated, flammable, or explosive environments to prevent accidents caused by improper installation or use.
#### 1. Load Matching
- Ensure the relay's rated voltage and current match the load. Do not exceed the rated capacity.
- For inductive loads (motors, coils, lamps, etc.), the starting current may be much higher than the rated current. Choose a relay with sufficient current margin.
#### 2. Short Circuit and Overcurrent Protection
- Install a **fuse** or **circuit breaker** in the relay circuit to prevent damage due to short circuits or accidental overcurrent. - Ensure the load circuit has no short circuits during wiring, and select protection components with appropriate current ratings if necessary.
#### 3. Arc and Switching Protection
- Relay switching generates arcs, which can cause contact wear or welding.
- For inductive loads, it is recommended to use **RC snubber circuits** or **varistors** for arc suppression.
#### 4. Installation Environment
- Do not use the relay in humid, high-temperature, flammable, explosive, or dusty environments.
- Install the relay securely to avoid vibrations or shocks that may cause misoperation or damage.
#### 5. Power-Off Operation
- Always cut off power before maintenance, wiring, or replacing the relay to ensure personnel and device safety.
- Latching relays are only powered when changing state. Avoid strong vibrations or strong magnetic fields while the relay is unpowered.
#### 6. Status Confirmation
- After powering on, confirm or reset the relay status as needed to prevent abnormal operation caused by transportation, installation, or external disturbances.
- Avoid power interruption during relay operation to prevent uncertain status or contact damage.
#### 7. Regular Inspection
- Periodically inspect relay contacts, terminals, and insulation to ensure proper operation.
- If abnormal heating, odor, or burn marks are detected, immediately cut off power and replace the relay.
### Indicator Light Description
| Indicator | Status description |
| RUN indicator | Ethernet port running indicator, outputs a square wave with a period of 2 seconds when the Ethernet port working normally. |
| STA indicator | MCU indicator, blinking when the MCU working normally. |
| TXD indicator | Send indicator, lights up when sending data. |
| RXD indicator | Receive indicator, lights up when receiving data. |
| Green indicator on Ethernet port | The green indicator will be on when TCP connection is established, which can be used to determine whether the module has established a communication link with the host software. |
| Yellow indicator on Ethernet port | Data activity indicator, when data is transmitted through the Ethernet port, the yellow indicator changes its state, which can be used to determine if there is data transmission |
# Module Parameter Configuration
The module needs to set the module parameters before communication, such as IP address, serial port format, Modbus protocol, etc. There are two modes of setting parameters: Vircom software configuration and web configuration.
Vircom software configuration allows for setting more parameters, but requires software installation. Web configuration does not require installation, but you need to know the IP address first, and the configuration parameters are few. It is recommended to use Virom for configuration.
Note:
1. The configuration can be done in any way, and it is recommended to use Virom software for first test.
2. It is recommended to modify only the IP address for the first configuration, other parameters are not recommended to be modified. The serial port parameters must be the default parameters; modifying the serial port parameters will result in no communication.
3. The module supports both Modbus RTU and Modbus TCP protocols. In the Advanced Settings -> Transfer Protocol, you can choose "None", which means the Modbus RTU protocol. It is not recommended to modify during the first configuration.
4. The selected Modbus TCP protocol must be configured using the Virom software and set to a non-storage Modbus gateway, otherwise the communication will not be normal.
## Virom Software Mode Configuration
### General Settings
Connect the module to the hardware and connect it to the network. Run the VirCom software (the computer on which Vircom is installed must be on the same LAN as the module).
The operation is as follows:
- 1. Click `Device`
- 2. Click `Auto Search`
- 3. Software search recognizes the device connected to the LAN
- 4. Select the device, and then click `Edit Device` or double-click the searched device directly - 5. Set up the device parameters:
- Modify the "IP mode" to a static assigned address, set the IP address, note that the static IP address entered must not be used by other devices, and it needs to be on the same LAN as the computer.
- The working mode is TCP server. The serial port setting is 115200 by default and cannot be modified.
- The "Transfer Protocol" in "Advanced Settings" defaults to "None", which means using the Modbus RTU protocol; if you select "Modbus TCP protocol", then use the Modbus TCP communication protocol.
- Click on "More Advanced Settings..." and select the Modbus Gateway Type as a non-storage Modbus gateway.
- Modify the "IP mode" to a static assigned address, set the IP address, note that the static IP address entered must not be used by other devices, and it needs to be on the same LAN as the computer.
- 6. Once the settings are complete, click `Modify Setting`
- 7. Click `Restart Dev`, wait for the module to restart, and the new settings will take effect.
Note: It is recommended to modify only the IP address for the first configuration, and do not modify other parameters.
See the figure below for details:
Note: The default Modbus gateway type is storage type, which will automatically send query commands several times, which may cause the controller chip to fail to respond, resulting in no response to the query commands. Therefore, you need to set it as Multi-host non-storage type.
### Protocol Setting
Note: It is recommended to use the default Modbus RTU protocol for the first configuration and no modifications are needed.
Although the module transmits data through the network port, it supports two Modbus protocols: Modbus RTU and Modbus TCP. By default, data is transparently transmitted, i.e. using the Modbus RTU protocol.
#### Modbus TCP Protocol Settings
- The "Transfer Protocol" in the "Advanced Settings" can be set to "Modbus TCP protocol". In this case, the Modbus RTU protocol of the main controller will be converted to the Modbus TCP protocol and transmitted through the network port.
- In this case, the device port automatically changes to 502. Users can use the Modbus TCP tool to connect to the IP port 502 of the serial port server.
- Click on "More Advanced Settings..." and select the Modbus Gateway Type as a non-storage Modbus gateway.
#### Modbus RTU Protocol Settings
- Set "Transfer Protocol" in the "Advanced Settings" to "None", and change to use Modbus RTU protocol.
- Click on "More Advanced Settings..." and select the Modbus Gateway Type as a non-storage Modbus gateway.
Note: The default Modbus gateway type is storage type, which will automatically send query commands several times, which may cause the controller chip to fail to respond, resulting in no response to the query commands. Therefore, you need to set it as Multi-host non-storage type.
### Virtual Serial Port Setting
The module transmits data through a network port (TCP/UDP protocol). In order to enable users to use the PoE port communication even with developed serial port software, a virtual serial port needs to be added. If not needed, this part can be skipped.
- First, install the virtual serial driver Virtual serial port driver
- Run Vircom and the user program on the same computer.
- Vircom creates a virtual COM port and connects this COM port to the serial server. When the user program uses the COM communication, it can send data to the user's serial port device through the Vircom serial port server.
The following steps demonstrate this operation:
- Click on "Serial Port & Device Management" on the Vircom main interface, then click "Add" and select to add COM2 (Among them, COM2 is the newly emerging COM port on the computer).
- Then enter the device management and double-click the device that needs to be bound to COM2. As shown in the diagram, select COM2 from the Virtual Serial Port list in the top left corner. Then click on "Modify Setting" and then click on "Restart Device".
- Return to the main interface of Vircom. It can be seen that COM2 has been connected to the device whose IP is 192.168.1.200. In this case, the virtual serial port COM2 can be used instead of the network port for communication.
## WEB Configuration
Using Vircom, you can search for and configure device parameters in different network segments. For Web configuration, you must first ensure that the computer and the serial server are in the same IP segment, and you need to know the IP address of the serial server in advance.
But Web configuration can be done on any computer without Vircom. (Different products have different web interfaces, which can be switched between Chinese and English)
1. Enter the IP address of the serial server in the browser, such as http://192.168.1.200 to open the following web page
2. In the Password field, enter your password: The default login password is not set or is set to 123456. If no password is set, you can enter any password and click the Login button to log in. After setting the password to log in, the settings at "Modify Web Login Key" will take effect:
3. The serial server parameters can be modified on the web page that appears.
4. After modifying the parameters, click the "Submit" button.
Attention: The system has added webpage settings function by default when it leaves the factory. If the configuration interface page file is overwritten and the webpage cannot be opened, the webpage file needs to be downloaded again.
Please refer to RS485 TO ETH (B) Manual
# Example Demonstration
The demo shows how the following two software operate.
SSCOM serial port debugging assistant is more convenient to operate, free of installation, and more convenient for complete display and analysis of instructions, but the disadvantage is that the data is not intuitive.
Modbus Poll software is directly operated on the register, and the data display is more convenient to observe, but the disadvantage is that the instruction is not displayed completely, so you need to be familiar with the Modbus register operation.
You can test using any method. It is recommended to use the SSCOM serial port debugging assistant software for the first test.
### SSCOM Serial Port Debugging Assistant
Modbus RTU Command: The default configuration is the Modbus RTU command
- 1. Open the serial port debugging assistant window
- 2. Select TCPClient for port number
- 3. Modify the remote IP and port number according to the Vircom settings above
- 4. Click the "Connect" button to connect to the TCP server
- 5. The green light of the network port will light up when the connection is successful
- 6. Click Multi-Char to open the Send Multi-Char window, the default display is the Modbus RTU command, click the corresponding function to send the corresponding command.
- 7. If you use the custom input box below to send the command, you need to set Verify as ModbusCRC16
Configure Modbus TCP Directives: If you want to set it as a Modbus TCP Directive, you need to change the commands
- 1. Click on the Import ini button in the Send Multi-Char column
- 2. Select the modbus tcp.ini file to import the Modbus TCP command
Note: If a popup error message says "A component named HEX0 already exists", then you need to close and reopen the software, which will reload the files and refresh the buttons.
- 3. After successful import, the following is displayed, click on the function to send the corresponding command.
Note: Modbus tcp does not require CRC checksum, select None for Verify.
- For detailed Modubs commands, please see the development protocol.
### Modbus Poll Software
It is not convenient to use the SSCOM software for observing the data, you can select Modbus Poll software to read the data. Download and install the Modbus Poll software.
- 1. Open Modbus Poll software
- 2. Select Setup->Read/Write Definition, select the actual device address for Slave ID, select 01 Read Coils function code for Function, and change Quantity to 8 channels. Click OK to confirm.
- 3. If:
- you are using the Modbus RTU protocol, select Connection->Connect Setup, select Modbus RTU/ASCII Over TCP/IP for Connection, select RTU for Mode, and enter the correct IP address and port number. Click OK to connect.
- you are using the Modbus TCP protocol, select Connection->Connect Setup, select Modbus TCP/IP for Connection, and enter the correct IP address and port number. Click OK to connect.
4. After the connection is normal, you can check the current relay status. Select the corresponding channel, then double-click the status value to pop up the send page. Choose On or Off, then Click Send to control the relay opening and closing.
# Demo
### Raspberry Pi
Connect the Raspberry Pi and the ModBus POE ETH Relay module to the same LAN.
Open the Raspberry Pi terminal and run the program by entering the following command:
```sh
sudo apt-get install unzip
wget https://files.waveshare.com/wiki/Modbus-POE-ETH-Relay/Modbus_POE_ETH_Relay_Code.zip
unzip Modbus_POE_ETH_Relay_Code.zip
cd Modbus_POE_ETH_Relay_Code
#modbus rtu protocol
vi modbus_rtu.py #Change the IP address and port number according to the actual situation
sudo python3 modbus_rtu.py
#modbus tcp protocol
vi modbus_tcp.py #Change the IP address and port number according to the actual situation
sudo python3 modbus_tcp.py
```
Note: To run this demo, you need to modify the demo file to change the IP address and port number to the actual IP address and port number of the ModBus POE ETH Relay.
# Modbus RTU Development Protocol V2
## Function Code Introduction
| Function Code | Description | Note |
|---------------|-----------------------|-------------------------------|
| 01 | Read coil status | Read relay status |
| 03 | Read holding register | Read the address and version |
| 05 | Write single coil | Write single relay |
| 06 | Write single register | Set the baud rate and address |
| 0F | Write multiple coils | Write all relays |
## Register Address Introduction
| Address (HEX) | Address storage content | Register value | Permission | Modbus Function Code |
|------------------+----------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+------------+----------------------|
| 0x0000 …… 0x0007 | Channel 1~8 relay address | 0xFF00: relay on / 0x0000: relay off / 0x5500: relay toggle | Read/Write | 0x01, 0x05, 0x0F |
| 0x00FF | Control all relays | 0xFF00: all relays on / 0x0000: all relays off / 0x5500: all relays toggle | Write | 0x05 |
| 0x0100 …… 0x0107 | Channel 1~8 relay toggle address | 0xFF00: relay toggle / 0x0000: relay unchanged | Write | 0x05, 0x0F |
| 0x01FF | Control all relays toggle | 0xFF00: all relays toggle / 0x0000: all relays unchanged | Write | 0x05 |
| 0x0200 …… 0x0207 | Channel 1~8 relay flash on | Interval time: data×100ms / Value: 0x0007, Interval time: 7×100MS = 700MS | Write | 0x05 |
| 0x0400 …… 0x0407 | Channel 1~8 relay flash off | Interval time: data×100ms / Value: 0x0007, Interval time: 7×100MS = 700MS | Write | 0x05 |
| 4x4000 | Device Address | Directly store Modbus address / Device address: 0x0001 | Read | 0x03 |
| 4x8000 | Software Version | Converting to decimal and then shifting the decimal point two places to the left will represent the software version / 0x0064 = 100 = V1.00 | Read | 0x03 |
## Operation Command Introduction
### Control Single Relay
Send code: 01 05 00 00 FF 00 8C 3A
| Field | Description | Note |
|-------|----------------|-------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Relay control |
| 00 00 | Address | The register address of the relay to be controlled, 0x0000-0x0007 |
| FF 00 | Command | 0xFF00: relay on; 0x0000: relay off; 0x5500: relay toggle |
| 8C 3A | CRC16 | The CRC16 checksum of the first 6 bytes of data |
Return code: 01 05 00 00 FF 00 8C 3A
| Field | Description | Note |
|-------|----------------|-------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Relay control |
| 00 00 | Address | The register address of the relay to be controlled, 0x0000-0x0007 |
| FF 00 | Command | 0xFF00: relay on; 0x0000: relay off; 0x5500: relay toggle |
| 8C 3A | CRC16 | The CRC16 checksum of the first 6 bytes of data |
For example:
[Address 1 device]:
```
Relay 0 on: 01 05 00 00 FF 00 8C 3A
Relay 0 off: 01 05 00 00 00 00 CD CA
Relay 1 on: 01 05 00 01 FF 00 DD FA
Relay 1 off: 01 05 00 01 00 00 9C 0A
Relay 2 on: 01 05 00 02 FF 00 2D FA
Relay 2 off: 01 05 00 02 00 00 6C 0A
Relay 3 on: 01 05 00 03 FF 00 7C 3A
Relay 3 off: 01 05 00 03 00 00 3D CA
Relay 0 toggle: 01 05 00 00 55 00 F2 9A
Relay 1 toggle: 01 05 00 01 55 00 A3 5A
Relay 2 toggle: 01 05 00 02 55 00 53 5A
Relay 3 toggle: 01 05 00 03 55 00 02 9A
```
### Control All Relays
Send code: 01 05 00 FF FF 00 BC 0A
| Field | Description | Note |
|-------|----------------|-----------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Relay control |
| 00 FF | Address | Fixed 0x00FF |
| FF 00 | Command | 0xFF00: relay on; 0x0000: relay off; 0x5500: relay toggle |
| BC 0A | CRC16 | The CRC16 checksum of the first 6 bytes of data |
Return code: 01 05 00 FF FF 00 BC 0A
| Field | Description | Note |
|-------|----------------|-----------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Relay control |
| 00 FF | Address | Fixed 0x00FF |
| FF 00 | Command | 0xFF00: relay on; 0x0000: relay off; 0x5500: relay toggle |
| BC 0A | CRC16 | The CRC16 checksum of the first 6 bytes of data |
For example: [Address 1 device]:
All relays on: 01 05 00 FF FF 00 BC 0A All relays off: 01 05 00 FF 00 00 FD FA All relays toggle: 01 05 00 FF 55 00 C2 AA
### Read Relay Status
Send code: 01 01 00 00 00 08 3D CC
| Field | Description | Note |
|-------|---------------------|-------------------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 01 | 01 Command | Query relay status |
| 00 00 | Relay Start Address | The register address of the relay, 0x0000 - 0x0007 |
| 00 08 | Relay Number | The number of relays to be read, which must not exceed the maximum number of relays |
| 3D CC | CRC16 | The CRC16 checksum of the first 6 bytes of data |
Receive code: 01 01 01 00 51 88
| Field | Description | Note |
|-------|----------------|---------------------------------------------------------------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 01 | 01 Command | Query relay status |
| 01 | Byte Number | The number of all bytes of the returned status information |
| 00 | Query status | Received relay status Bit0: the first relay status; Bit1: the second relay status; And so on, with the idle high bit being zero |
| 51 88 | CRC16 | The CRC16 checksum of the first 6 bytes of data |
For example: [Address 1 device]
Send: 01 01 00 00 00 08 3D CC Receive: 01 01 01 00 51 88 //All relays off Send: 01 01 00 00 00 08 3D CC Receive: 01 01 01 01 90 48 //Relay 0 is on, others are off Send: 01 01 00 00 00 08 3D CC Receive: 01 01 01 41 91 B8 //Relay 0 and 6 are on, others are off
### Write Relay Status
Send code: 01 0F 00 00 00 08 01 FF BE D5
| Field | Description | Note |
|-------|---------------------|-----------------------------------------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 0F | 0F Command | Write relay status |
| 00 00 | Relay Start Address | The register address of the relay to be controlled, 0x0000 - 0x0007 |
| 00 08 | Relay Number | The number of relays to be operated, which must not exceed the maximum number of relays |
| 01 | Byte Number | The byte number of the status |
| FF | Relay Status | Bit0: the first relay status; Bit1: the second relay status; And so on, with the idle high bit being zero |
| BE D5 | CRC16 | The CRC16 checksum of the first 6 bytes of data |
Receive code: 01 0F 00 00 00 08 54 0D
| Field | Description | Note |
|-------|---------------------|---------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 0F | 0F Command | Control all registers |
| 00 00 | Relay Start Address | The register address of the relay to be controlled, 0x0000 - 0x0007 |
| 00 08 | Relay Number | The number of relays to be operated |
| 54 0D | CRC16 | The CRC16 checksum of the first 6 bytes of data |
For example: [Address 1 device]
All relays on: 01 0F 00 00 00 08 01 FF BE D5 All relays off: 01 0F 00 00 00 08 01 00 FE 95 0-1 on; 2-7 off: 01 0F 00 00 00 08 01 03 BE 94
### Relay Flash ON/OFF Command
Send code: 01 05 02 00 00 07 8D B0
| Field | Description | Note |
|-------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Single control command |
| 02 | Command | 02: flash on, 04: flash off |
| 00 | Relay Address | The address of the relay to be controlled, 0x00~0x07 |
| 00 07 | Interval Time | The interval time: data*100ms Value: 0x0007, Interval time: 7*100MS = 700MS The maximum setting for the flash-on flash-off time is 0x7FFF |
| 8D B0 | CRC16 | The CRC16 checksum of the first 6 bytes of data |
Receive code: 01 05 02 00 00 07 8D B0
| Field | Description | Note |
|-------|----------------|-----------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Single control command |
| 02 | Command | 02: flash on, 04: flash off |
| 00 | Relay Address | The address of the relay to be controlled, 0x00~0x07 |
| 00 07 | Interval Time | The interval time: data*100ms Value: 0x0007, Interval time: 7*100MS = 700MS |
| 8D B0 | CRC16 | The CRC16 checksum of the first 6 bytes of data |
For example: [Address 1 device]
Relay 0 flash on: 01 05 02 00 00 07 8D B0 //700MS Relay 1 flash on: 01 05 02 01 00 08 9C 74 //800MS Relay 0 flash off: 01 05 04 00 00 05 0C F9 //500MS Relay 1 flash off: 01 05 04 01 00 06 1D 38 //600MS
### Read Software Version Command
Send code: 01 03 80 00 00 01 AD CA
| Field | Description | Note |
|-------|------------------|-------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 03 | 03 Command | Read Holding Register |
| 80 00 | Command register | 0x8000: read software version |
| 00 01 | Byte Number | Fixed 0x0001 |
| AD CA | CRC16 | The CRC16 checksum of the first 6 bytes of data |
Receive code: 01 03 02 00 C8 B9 D2
| Field | Description | Note |
|-------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 03 | 03 Command | Read Holding Register |
| 02 | Byte Number | The number of bytes returned |
| 00 C8 | Software Version | Converting to decimal and then shifting the decimal point two places to the left will represent the software version 0x00C8 = 200 = V2.00 |
| B9 D2 | CRC16 | The CRC16 checksum of the first 6 bytes of data |
For example:
Send: 01 03 80 00 00 01 AD CA Receive: 01 03 02 00 C8 B9 D2 //0x00C8 = 200 =V2.00
### Exception Function Code
When the received command is incorrect or the device is abnormal, an exception response will be returned in the following format:
Receive: 01 85 03 02 91
| Field | Description | Note |
|-------|-------------------------|------------------------------------------------------------------------------|
| 01 | Device Address | 0x00 indicates the broadcast address, 0x01-0xFF indicates the device address |
| 85 | Exception Function Code | Exception function code = Request function code + 0x80 |
| 03 | Byte Number | Exception Code |
| 02 91 | CRC16 | The CRC16 checksum of the first 6 bytes of data |
An exception code is a single-byte value that indicates the type of error. Several commonly used exception codes defined by the Modbus protocol:
| Exception Code | Name | Description |
|----------------|----------------------|-------------------------------------------------------------------------|
| 0x01 | Illegal Function | The requested function code is not supported |
| 0x02 | Illegal Data Address | The requested data address is incorrect |
| 0x03 | Illegal Data Value | The requested data value or operation cannot be executed |
| 0x04 | Server Failure | Server equipment failure |
| 0x05 | Response | The request has been received and is being processed |
| 0x06 | Device Busy | The device is currently busy and cannot perform the requested operation |
## Modbus TCP Command Introduction
Here is a brief introduction to Modbus TCP and Modbus RTU protocol conversion using the above commands to open the first relay as an example.
- Modbus RTU command: 01 05 00 00 FF 00 8C 3A
| Field | Description | Note |
|-------|----------------|------------------------------------------------------------------------------------|
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Relay control |
| 00 00 | Address | The register address of the relay to be controlled, 0x00, that is, the first relay |
| FF 00 | Command | 0xFF00: Relay on |
| 8C 3A | CRC16 | The CRC16 checksum of the first 6 bytes of data |
- Modbus TCP command: 00 00 00 00 00 06 01 05 00 00 FF 00
| Field | Description | Note |
|-------|----------------|------------------------------------------------------------------------------------|
| 00 00 | Message Label | Both be 0x00 |
| 00 00 | modbus Label | Must both be 0, which means this is Modbus communication |
| 00 06 | Byte Length | Indicates the number of all bytes that follow, followed by 6 bytes |
| 01 | Device Address | Fixed 0x01 |
| 05 | 05 Command | Relay control |
| 00 00 | Address | The register address of the relay to be controlled, 0x00, that is, the first relay |
| FF 00 | Command | 0xFF00: Relay on |
By comparing the commands above, we can observe that to convert a Modbus RTU command to Modbus TCP protocol, the CRC check is removed, and the command is prefixed with five 0x00 bytes followed by a byte representing the length.
## Advanced Applications
- Relay control through Alibaba Cloud MQTT
- Relay control through Waveshare Cloud
- Relay control through HTTP GET/POST
# Resources
### Software
- Vircom configuration software
- Virtual serial port driver
- Sscom software
- Modbus Poll software
- SecureCRT software

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
# Implementation Decisions
**Date**: 2025-12-28
**Feature**: Modbus Relay Control System
## User Decisions
### Q1: Communication Pattern
**Decision**: HTTP Polling (as specified in spec)
**Rationale**: WebSocket would be overkill for this project scale
### Q2: Frontend Development Approach
**Decision**: Develop frontend alongside backend, but API endpoints must be implemented first before corresponding frontend features
**Approach**: API-first development - implement and test each endpoint before building UI for it
### Q3: Hardware Availability
**Decision**: Physical hardware available for testing
**Details**:
- 8-channel Modbus relay device accessible now
- IP address: Variable (configurable)
- Port: 501 or 502 (confirm in docs: `docs/Modbus_POE_ETH_Relay.md`)
- Device will be available during development phase
### Q4: Relay Label Persistence
**Decision**: SQLite database with SQLx
**Implementation Priority**:
1. **Preferred**: SQLite database with SQLx (compile-time SQL verification, async-native, type-safe)
2. **Alternative**: YAML file (read at startup, write on update)
**Recommendation**: Use SQLite with SQLx for MVP - simpler than managing YAML file updates, good for future features, aligns with type-driven development principles
### Q5: Error Recovery Strategy
**Decision**: Exponential retry with timeout
**Strategy**:
- When device becomes unhealthy/unavailable: attempt reconnection every 5 seconds
- Maximum retry duration: 5 minutes
- After 5 minutes: give up and mark device as unhealthy
- Resume connection attempts when user makes new API request
- Background task monitors connection health
### Q6: Firmware Version
**Decision**: Check docs for availability, hide if unavailable
**Behavior**:
- If firmware version available via Modbus: Display in health endpoint
- If not available: Omit field entirely from health response (not null/empty string)
- Action: Verify in `docs/Modbus_POE_ETH_Relay.md`
### Q7: Deployment Environment
**Development**: Thinkpad x220 (NixOS)
**Production Backend**: Raspberry Pi 3B+ (available next week) - on same network as relay device
**Production Frontend**: Cloudflare Pages (or equivalent static hosting)
**Reverse Proxy**: Traefik on Raspberry Pi with Authelia middleware for authentication
**Network**: Raspberry Pi on same network as relay device, frontend accesses backend via HTTPS through Traefik
### Q8: Testing Approach
**Decision**: Implement both real hardware tests AND mocks
**Rationale**:
- Hardware available now for integration testing
- Mocks needed for future maintenance (after device shipped)
- Mocks enable fast unit tests without hardware dependency
- Follows TDD principles with mock-based development
**Testing Strategy**:
1. **Unit Tests**: Use mocks (mockall) - fast, no hardware needed
2. **Integration Tests**: Use real hardware - verify actual Modbus communication
3. **CI/CD**: Use mocks (hardware not available in CI)
4. **Manual Testing**: Use real hardware during development
## Derived Decisions
### Deployment Architecture
**Decision**: Frontend on Cloudflare Pages, backend on Raspberry Pi behind Traefik reverse proxy
**Components**:
- **Frontend**: Static Vue 3 app hosted on Cloudflare Pages (fast global CDN delivery)
- **Backend**: Rust HTTP API on Raspberry Pi (same local network as Modbus relay device)
- **Reverse Proxy**: Traefik on Raspberry Pi providing:
- HTTPS termination (TLS certificates)
- Authelia middleware for user authentication
- Reverse proxy routing to backend HTTP service
- **Communication**: Frontend → HTTPS (via Traefik) → Backend → Modbus TCP → Relay Device
**Rationale**:
- Frontend on CDN provides fast page loads from anywhere
- Backend must be local to Modbus device (local network communication)
- Traefik handles authentication/HTTPS without application-level complexity
- Backend runs HTTP internally, Traefik handles TLS termination
**Security Layers**:
1. Authelia authentication at reverse proxy (user login)
2. HTTPS encryption for frontend-backend communication
3. Unencrypted Modbus TCP on local network only (acceptable for local-only device)
### Architecture Approach
**Decision**: Hexagonal Architecture with trait-based abstraction
**Layers**:
- **Domain**: Pure business logic (RelayId, RelayState, Relay entity)
- **Application**: Use cases (GetRelayStatus, ToggleRelay, BulkControl)
- **Infrastructure**: Modbus client implementation + SQLite repository
- **Presentation**: HTTP API handlers (Poem)
### Database Choice
**Decision**: SQLite with SQLx for relay labels and configuration
**Why SQLx over rusqlite**:
- **Compile-time SQL verification**: Queries are checked against actual database schema during compilation
- **Type safety**: Column types verified to match Rust types at compile time
- **Async-native**: Built for tokio async/await (no need for `spawn_blocking` wrappers)
- **Type-driven development alignment**: "Parse, don't validate" - SQL errors caught at compile time, not runtime
- **Better observability**: Built-in query logging and tracing integration
- **Macro-based queries**: `query!` and `query_as!` macros provide ergonomic, safe database access
**Benefits of SQLite**:
- No external dependencies (embedded)
- ACID transactions for label updates
- Simple schema (one table for relay labels)
- Easy to back up (single file)
- Works on both NixOS and Raspberry Pi
**Schema**:
```sql
CREATE TABLE relay_labels (
relay_id INTEGER PRIMARY KEY CHECK(relay_id >= 1 AND relay_id <= 8),
label TEXT NOT NULL CHECK(length(label) <= 50)
);
```
**Dependencies**:
```toml
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
```
### Modbus Port Discovery
**Confirmed from Documentation** (`docs/Modbus_POE_ETH_Relay.md`):
- **Modbus RTU over TCP**: Uses TCP server mode, port is configurable (typically 8234 or custom)
- **Modbus TCP**: Port automatically changes to **502** when "Modbus TCP protocol" is selected in Advanced Settings
- **Recommended**: Use Modbus RTU over TCP (default, simpler configuration)
- **Device must be configured as**: "Multi-host non-storage type" gateway (CRITICAL - storage type sends spurious queries)
### Firmware Version Availability
**Confirmed from Documentation** (`docs/Modbus_POE_ETH_Relay.md:417-442`):
- **Available**: YES - Firmware version can be read via Modbus function code 0x03
- **Register Address**: 0x8000 (Read Holding Register)
- **Command**: `01 03 80 00 00 01 AD CA`
- **Response Format**: 2-byte value, convert to decimal and divide by 100 (e.g., 0x00C8 = 200 = v2.00)
- **Implementation**: Read once at startup and cache, update on successful reconnection
### Connection Management
**Decision**: Background connection health monitor
**Behavior**:
- Monitor task checks connection every 5 seconds
- On failure: retry with exponential backoff (max 5 seconds interval)
- After 5 minutes of failures: mark unhealthy, stop retrying
- On new API request: resume connection attempts
- On successful reconnection: reset retry counter, mark healthy
### Frontend Technology Stack
**Decision**: Vue 3 + TypeScript + Vite
**Components**:
- OpenAPI TypeScript client generation (type-safe API calls)
- HTTP polling with `setInterval` (2-second intervals)
- Reactive state management (ref/reactive, no Pinia needed for this simple app)
- UI library: TBD (Nuxt UI, Vuetify, or custom - decide during frontend implementation)
## Next Steps
1. ✅ Verify Modbus port in documentation
2. ✅ Design architecture approaches (minimal, clean, pragmatic)
3. ✅ Select approach with user
4. ✅ Create detailed implementation plan
5. ✅ Begin TDD implementation
## Notes
- User has hardware access now, but device will ship after first version
- Mocks are critical for long-term maintainability
- SQLite preferred over YAML for runtime updates
- Connection retry strategy balances responsiveness with resource usage

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,718 @@
# Research Document: Modbus Relay Control System
**Created**: 2025-12-28
**Feature**: [spec.md](./spec.md)
**Status**: Complete
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [Tokio-Modbus Research](#tokio-modbus-research)
3. [WebSocket vs HTTP Polling](#websocket-vs-http-polling)
4. [Existing Codebase Patterns](#existing-codebase-patterns)
5. [Integration Recommendations](#integration-recommendations)
---
## Executive Summary
### Key Decisions
| Decision Area | Recommendation | Rationale |
|---------------------------|--------------------------------------|---------------------------------------------------------|
| **Modbus Library** | tokio-modbus 0.17.0 | Native async/await, production-ready, good testability |
| **Communication Pattern** | HTTP Polling (as in spec) | Simpler, reliable, adequate for 10 users @ 2s intervals |
| **Connection Management** | Arc<Mutex<Context>> for MVP | Single device, simple, can upgrade later if needed |
| **Retry Strategy** | Simple retry-once helper | Matches FR-007 requirement |
| **Testing Approach** | Trait-based abstraction with mockall | Enables >90% coverage without hardware |
### User Input Analysis
**User requested**: "Use tokio-modbus crate, poem-openapi for REST API, Vue.js with WebSocket for real-time updates"
**Findings**:
- ✅ tokio-modbus 0.17.0: Excellent choice, validated by research
- ✅ poem-openapi: Already in use, working well
- ⚠️ **WebSocket vs HTTP Polling**: Spec says HTTP polling (FR-028). WebSocket adds 43x complexity for negligible benefit at this scale.
**RECOMMENDATION**: Maintain HTTP polling as specified. WebSocket complexity not justified for 10 concurrent users with 2-second update intervals.
### Deployment Architecture
**User clarification (2025-12-29)**: Frontend on Cloudflare Pages, backend on Raspberry Pi behind Traefik with Authelia
**Architecture**:
- **Frontend**: Cloudflare Pages (Vue 3 static build) - global CDN delivery
- **Backend**: Raspberry Pi HTTP API (same local network as Modbus device)
- **Reverse Proxy**: Traefik on Raspberry Pi
- HTTPS termination (TLS certificates)
- Authelia middleware for authentication
- Routes frontend requests to backend HTTP service
- **Communication Flow**:
- Frontend (CDN) → HTTPS → Traefik (HTTPS termination + auth) → Backend (HTTP) → Modbus TCP → Device
**Security**:
- Frontend-Backend: HTTPS via Traefik (encrypted, authenticated)
- Backend-Device: Modbus TCP on local network (unencrypted, local only)
---
## Tokio-Modbus Research
### Decision: Recommended Patterns
**Primary Recommendation**: Use tokio-modbus 0.17.0 with a custom trait-based abstraction layer (`RelayController` trait) for testability. Implement connection management using Arc<Mutex<Context>> for MVP.
### Technical Details
**Version**: tokio-modbus 0.17.0 (latest stable, released 2025-10-22)
**Protocol**: Modbus RTU over TCP (NOT Modbus TCP)
- Hardware uses RTU protocol tunneled over TCP
- Includes CRC16 validation
- Different from native Modbus TCP (no CRC, different framing)
**Connection Strategy**:
- Shared `Arc<Mutex<Context>>` for simplicity
- Single persistent connection (only one device)
- Can migrate to dedicated async task pattern if reconnection logic needed
**Timeout Handling**:
- Wrap all operations with `tokio::time::timeout(Duration::from_secs(3), ...)`
- **CRITICAL**: tokio-modbus has NO built-in timeouts
**Retry Logic**:
- Implement simple retry-once helper per FR-007
- Matches specification requirement
**Testing**:
- Use `mockall` crate with `async-trait` for unit testing
- Trait abstraction enables testing without hardware
- Supports >90% test coverage target (NFR-013)
### Critical Gotchas
1. **Device Gateway Configuration**: Hardware MUST be set to "Multi-host non-storage type" - default storage type sends spurious queries causing failures
2. **No Built-in Timeouts**: tokio-modbus has NO automatic timeouts - must wrap every operation with `tokio::time::timeout`
3. **RTU vs TCP Confusion**: Device uses Modbus RTU protocol over TCP (with CRC), not native Modbus TCP protocol
4. **Address Indexing**: Relays labeled 1-8, but Modbus addresses are 0-7 (use newtype pattern with conversion methods)
5. **Nested Result Handling**: Returns `Result<Result<T, Exception>, std::io::Error>` - must handle both layers (use `???` triple-question-mark pattern)
6. **Concurrent Access**: Context is not thread-safe - requires `Arc<Mutex>` or dedicated task serialization
### Code Examples
**Basic Connection Setup**:
```rust
use tokio_modbus::prelude::*;
use tokio::time::{timeout, Duration};
// Connect to device
let socket_addr = "192.168.1.200:8234".parse()?;
let mut ctx = tcp::connect(socket_addr).await?;
// Set slave ID (unit identifier)
ctx.set_slave(Slave(0x01));
// Read all 8 relay states with timeout
let states = timeout(
Duration::from_secs(3),
ctx.read_coils(0x0000, 8)
).await???; // Triple-? handles timeout + transport + exception errors
```
**Toggle Relay with Retry**:
```rust
async fn toggle_relay(
ctx: &mut Context,
relay_id: u8, // 1-8
) -> Result<(), RelayError> {
let addr = (relay_id - 1) as u16; // Convert to 0-7
// Read current state
let states = timeout(Duration::from_secs(3), ctx.read_coils(addr, 1))
.await???;
let current = states[0];
// Write opposite state with retry
let new_state = !current;
let write_op = || async {
timeout(Duration::from_secs(3), ctx.write_single_coil(addr, new_state))
.await
};
// Retry once on failure (FR-007)
match write_op().await {
Ok(Ok(Ok(()))) => Ok(()),
Err(_) | Ok(Err(_)) | Ok(Ok(Err(_))) => {
tracing::warn!("Write failed, retrying");
write_op().await???
}
}
}
```
**Trait-Based Abstraction for Testing**:
```rust
use async_trait::async_trait;
#[async_trait]
pub trait RelayController: Send + Sync {
async fn read_all_states(&mut self) -> Result<Vec<bool>, RelayError>;
async fn write_state(&mut self, relay_id: RelayId, state: RelayState) -> Result<(), RelayError>;
}
// Real implementation with tokio-modbus
pub struct ModbusRelayController {
ctx: Arc<Mutex<Context>>,
}
#[async_trait]
impl RelayController for ModbusRelayController {
async fn read_all_states(&mut self) -> Result<Vec<bool>, RelayError> {
let mut ctx = self.ctx.lock().await;
timeout(Duration::from_secs(3), ctx.read_coils(0, 8))
.await
.map_err(|_| RelayError::Timeout)?
.map_err(RelayError::Transport)?
.map_err(RelayError::Exception)
}
// ... other methods
}
// Mock for testing (using mockall)
mock! {
pub RelayController {}
#[async_trait]
impl RelayController for RelayController {
async fn read_all_states(&mut self) -> Result<Vec<bool>, RelayError>;
async fn write_state(&mut self, relay_id: RelayId, state: RelayState) -> Result<(), RelayError>;
}
}
```
### Alternatives Considered
1. **modbus-robust**: Provides auto-reconnection but lacks retry logic and timeouts - insufficient for production
2. **bb8 connection pool**: Overkill for single-device scenario, adds unnecessary complexity
3. **Synchronous modbus-rs**: Would block Tokio threads, poor scalability for concurrent users
4. **Custom Modbus implementation**: Reinventing wheel, error-prone, significant development time
### Resources
- [GitHub - slowtec/tokio-modbus](https://github.com/slowtec/tokio-modbus)
- [tokio-modbus on docs.rs](https://docs.rs/tokio-modbus/)
- [Context7 MCP: `/slowtec/tokio-modbus`](mcp://context7/slowtec/tokio-modbus)
- [Context7 MCP: `/websites/rs_tokio-modbus_0_16_3_tokio_modbus`](mcp://context7/websites/rs_tokio-modbus_0_16_3_tokio_modbus)
---
## WebSocket vs HTTP Polling
### Recommendation: HTTP Polling (as specified)
The specification's decision to use HTTP polling is technically sound. **HTTP polling is the better choice** for this specific use case.
### Performance at Your Scale (10 users, 2-second intervals)
**Bandwidth Comparison:**
- HTTP Polling: ~20 Kbps (10 users × 0.5 req/sec × 500 bytes × 8)
- WebSocket: ~2.4 Kbps sustained
- **Difference: 17.6 Kbps** - negligible on any modern network
**Server Load:**
- HTTP Polling: 5 requests/second system-wide (trivial)
- WebSocket: 10 persistent connections (~80-160 KB memory)
- **Verdict: Both are trivial at this scale**
### Implementation Complexity
**HTTP Polling:**
- Backend: 0 lines (reuse existing `GET /api/relays`)
- Frontend: ~10 lines (simple setInterval)
- **Total effort: 15 minutes**
**WebSocket:**
- Backend: ~115 lines (handler + background poller + channel setup)
- Frontend: ~135 lines (WebSocket manager + reconnection logic)
- Testing: ~180 lines (connection lifecycle + reconnection tests)
- **Total effort: 2-3 days + ongoing maintenance**
**Complexity ratio: 43x more code for WebSocket**
### Reliability & Error Handling
**HTTP Polling Advantages:**
- Stateless (automatic recovery on next poll)
- Standard HTTP error codes
- Works everywhere (proxies, firewalls, old browsers)
- No connection state management
- Simple testing
**WebSocket Challenges:**
- Connection lifecycle management
- Exponential backoff reconnection logic
- State synchronization on reconnect
- Thundering herd problem (all clients reconnect after server restart)
- May fail behind corporate proxies (requires fallback to HTTP polling anyway)
### Decision Matrix
| Criterion | HTTP Polling | WebSocket | Weight |
|-----------|--------------|-----------|--------|
| Simplicity | 5 | 2 | 3x |
| Reliability | 5 | 3 | 3x |
| Testing | 5 | 2 | 2x |
| Performance @ 10 users | 4 | 5 | 1x |
| Scalability to 100+ | 3 | 5 | 1x |
| Architecture fit | 5 | 3 | 2x |
**Weighted Scores:**
- **HTTP Polling: 4.56/5**
- **WebSocket: 3.19/5**
HTTP Polling scores **43% higher** when complexity, reliability, and testing are properly weighted for this project's scale.
### When WebSocket Makes Sense
WebSocket advantages manifest at:
- **100+ concurrent users** (4x throughput advantage becomes meaningful)
- **Sub-second update requirements** (<1 second intervals)
- **High-frequency updates** where latency matters
- **Bidirectional communication** (chat, gaming, trading systems)
For relay control with 2-second polling:
- Latency: 0-4 seconds (avg 2 sec) - **acceptable for lights/pumps**
- Not a real-time critical system (not chat, gaming, or trading)
### Migration Path (If Needed Later)
Starting with HTTP polling does NOT prevent WebSocket adoption later:
1. **Phase 1:** Add `/api/ws` endpoint (non-breaking change)
2. **Phase 2:** Progressive enhancement (detect WebSocket support)
3. **Phase 3:** Gradual rollout with monitoring
**Key Point:** HTTP polling provides a baseline. Adding WebSocket later is straightforward, but removing WebSocket complexity is harder.
### Poem WebSocket Support (For Reference)
Poem has excellent WebSocket support through `poem::web::websocket`:
```rust
use poem::web::websocket::{WebSocket, Message};
#[handler]
async fn ws_handler(
ws: WebSocket,
state_tx: Data<&watch::Sender<RelayCollection>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| async move {
let (mut sink, mut stream) = socket.split();
let mut rx = state_tx.subscribe();
// Send initial state
let initial = rx.borrow().clone();
sink.send(Message::text(serde_json::to_string(&initial)?)).await?;
// Stream updates
while rx.changed().await.is_ok() {
let state = rx.borrow().clone();
sink.send(Message::text(serde_json::to_string(&state)?)).await?;
}
})
}
```
**Broadcasting Pattern**: Use `tokio::sync::watch` channel:
- Maintains only most recent value (perfect for relay state)
- Automatic deduplication of identical states
- New connections get immediate state snapshot
- Memory-efficient (single state copy)
### Resources
- [Poem WebSocket API Documentation](https://docs.rs/poem/latest/poem/web/websocket/)
- [HTTP vs WebSockets Performance](https://blog.feathersjs.com/http-vs-websockets-a-performance-comparison-da2533f13a77)
- [Tokio Channels Tutorial](https://tokio.rs/tokio/tutorial/channels)
---
## Existing Codebase Patterns
### Architecture Overview
The current codebase is a well-structured Rust backend API using Poem framework with OpenAPI support, following clean architecture principles.
**Current Structure**:
```
src/
├── lib.rs - Library entry point, orchestrates application setup
├── main.rs - Binary entry point, calls lib::run()
├── startup.rs - Application builder, server configuration, route setup
├── settings.rs - Configuration from YAML files + environment variables
├── telemetry.rs - Logging and tracing setup
├── route/ - HTTP endpoint handlers
│ ├── mod.rs - API aggregation and OpenAPI tags
│ ├── health.rs - Health check endpoints
│ └── meta.rs - Application metadata endpoints
└── middleware/ - Custom middleware implementations
├── mod.rs
└── rate_limit.rs - Rate limiting middleware using governor
```
### Key Patterns Discovered
#### 1. Route Registration Pattern
**Location**: `src/startup.rs:95-107`
```rust
fn setup_app(settings: &Settings) -> poem::Route {
let api_service = OpenApiService::new(
Api::from(settings).apis(),
settings.application.clone().name,
settings.application.clone().version,
)
.url_prefix("/api");
let ui = api_service.swagger_ui();
poem::Route::new()
.nest("/api", api_service.clone())
.nest("/specs", api_service.spec_endpoint_yaml())
.nest("/", ui)
}
```
**Key Insights**:
- OpenAPI service created with all API handlers via `.apis()` tuple
- URL prefix `/api` applied to all API routes
- Swagger UI automatically mounted at root `/`
- OpenAPI spec YAML available at `/specs`
#### 2. API Handler Organization Pattern
**Location**: `src/route/mod.rs:14-37`
```rust
#[derive(Tags)]
enum ApiCategory {
Health,
Meta,
}
pub(crate) struct Api {
health: health::HealthApi,
meta: meta::MetaApi,
}
impl From<&Settings> for Api {
fn from(value: &Settings) -> Self {
let health = health::HealthApi;
let meta = meta::MetaApi::from(&value.application);
Self { health, meta }
}
}
impl Api {
pub fn apis(self) -> (health::HealthApi, meta::MetaApi) {
(self.health, self.meta)
}
}
```
**Key Insights**:
- `Tags` enum groups APIs into categories for OpenAPI documentation
- Aggregator struct (`Api`) holds all API handler instances
- Dependency injection via `From<&Settings>` trait
- `.apis()` method returns tuple of all handlers
#### 3. OpenAPI Handler Definition Pattern
**Location**: `src/route/health.rs:7-29`
```rust
#[derive(ApiResponse)]
enum HealthResponse {
#[oai(status = 200)]
Ok,
#[oai(status = 429)]
TooManyRequests,
}
#[derive(Default, Clone)]
pub struct HealthApi;
#[OpenApi(tag = "ApiCategory::Health")]
impl HealthApi {
#[oai(path = "/health", method = "get")]
async fn ping(&self) -> HealthResponse {
tracing::event!(target: "backend::health", tracing::Level::DEBUG,
"Accessing health-check endpoint");
HealthResponse::Ok
}
}
```
**Key Insights**:
- Response types are enums with `#[derive(ApiResponse)]`
- Each variant maps to HTTP status code via `#[oai(status = N)]`
- Handlers use `#[OpenApi(tag = "...")]` for categorization
- Type-safe responses at compile time
- Tracing at architectural boundaries
#### 4. JSON Response Pattern with DTOs
**Location**: `src/route/meta.rs:9-56`
```rust
#[derive(Object, Debug, Clone, serde::Serialize, serde::Deserialize)]
struct Meta {
version: String,
name: String,
}
#[derive(ApiResponse)]
enum MetaResponse {
#[oai(status = 200)]
Meta(Json<Meta>),
#[oai(status = 429)]
TooManyRequests,
}
#[OpenApi(tag = "ApiCategory::Meta")]
impl MetaApi {
#[oai(path = "/meta", method = "get")]
async fn meta(&self) -> Result<MetaResponse> {
Ok(MetaResponse::Meta(Json(self.into())))
}
}
```
**Key Insights**:
- DTOs use `#[derive(Object)]` for OpenAPI schema generation
- Response variants can hold `Json<T>` payloads
- Handler struct holds state/configuration
- Returns `Result<MetaResponse>` for error handling
#### 5. Middleware Composition Pattern
**Location**: `src/startup.rs:59-91`
```rust
let app = value
.app
.with(RateLimit::new(&rate_limit_config))
.with(Cors::new())
.data(value.settings);
```
**Key Insights**:
- Middleware applied via `.with()` method chaining
- Order matters: RateLimit → CORS → data injection
- Settings injected as shared data via `.data()`
- Configuration drives middleware behavior
#### 6. Configuration Management Pattern
**Location**: `src/settings.rs:40-62`
```rust
let settings = config::Config::builder()
.add_source(config::File::from(settings_directory.join("base.yaml")))
.add_source(config::File::from(
settings_directory.join(environment_filename),
))
.add_source(
config::Environment::with_prefix("APP")
.prefix_separator("__")
.separator("__"),
)
.build()?;
```
**Key Insights**:
- Three-tier configuration: base → environment-specific → env vars
- Environment detected via `APP_ENVIRONMENT` variable
- Environment variables use `APP__` prefix with double underscore separators
- Type-safe deserialization
#### 7. Testing Pattern
**Location**: `src/route/health.rs:31-38`
```rust
#[tokio::test]
async fn health_check_works() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let resp = cli.get("/api/health").send().await;
resp.assert_status_is_ok();
}
```
**Key Insights**:
- Test helper creates full application with random port
- `TestClient` provides fluent assertion API
- Tests are async with `#[tokio::test]`
- Real application used in tests
### Type System Best Practices
Current code demonstrates excellent TyDD:
- `Environment` enum instead of strings
- `RateLimitConfig` newtype instead of raw numbers
- `ApiResponse` enums for type-safe HTTP responses
### Architecture Compliance
**Current Layers**:
1. **Presentation Layer**: `src/route/*` - HTTP adapters
2. **Infrastructure Layer**: `src/middleware/*`, `src/startup.rs`, `src/telemetry.rs`
**Missing Layers** (to be added for Modbus):
3. **Domain Layer**: Pure relay logic, no Modbus knowledge
4. **Application Layer**: Use cases (get status, toggle)
---
## Integration Recommendations
### Recommended Architecture for Modbus Feature
Following hexagonal architecture principles from constitution:
```
src/
├── domain/
│ └── relay/
│ ├── mod.rs - Domain types (RelayId, RelayState, Relay)
│ ├── relay.rs - Relay entity
│ ├── error.rs - Domain errors
│ └── repository.rs - RelayRepository trait
├── application/
│ └── relay/
│ ├── mod.rs - Use case exports
│ ├── get_status.rs - GetRelayStatus use case
│ ├── toggle.rs - ToggleRelay use case
│ └── bulk_control.rs - BulkControl use case
├── infrastructure/
│ └── modbus/
│ ├── mod.rs - Modbus exports
│ ├── client.rs - ModbusRelayRepository implementation
│ ├── config.rs - Modbus configuration
│ └── error.rs - Modbus-specific errors
└── route/
└── relay.rs - HTTP adapter (presentation layer)
```
### Integration Points
| Component | File | Action |
|-----------|------|--------|
| **API Category** | `src/route/mod.rs` | Add `Relay` to `ApiCategory` enum |
| **API Aggregator** | `src/route/mod.rs` | Add `relay: RelayApi` field to `Api` struct |
| **API Tuple** | `src/route/mod.rs` | Add `RelayApi` to `.apis()` return tuple |
| **Settings** | `src/settings.rs` | Add `ModbusSettings` struct and `modbus` field |
| **Config Files** | `settings/base.yaml` | Add `modbus:` section |
| **Shared State** | `src/startup.rs` | Inject `ModbusClient` via `.data()` |
| **Dependencies** | `Cargo.toml` | Add `tokio-modbus`, `async-trait`, `mockall` |
### Example: New Route Handler
```rust
// src/route/relay.rs
use poem::Result;
use poem_openapi::{ApiResponse, Object, OpenApi, payload::Json, param::Path};
use crate::domain::relay::{RelayId, RelayState, Relay};
#[derive(Object, Serialize, Deserialize)]
struct RelayDto {
id: u8,
state: String, // "on" or "off"
label: Option<String>,
}
#[derive(ApiResponse)]
enum RelayResponse {
#[oai(status = 200)]
Status(Json<RelayDto>),
#[oai(status = 400)]
BadRequest,
#[oai(status = 503)]
ServiceUnavailable,
}
#[OpenApi(tag = "ApiCategory::Relay")]
impl RelayApi {
#[oai(path = "/relays/:id", method = "get")]
async fn get_status(&self, id: Path<u8>) -> Result<RelayResponse> {
let relay_id = RelayId::new(id.0)
.map_err(|_| poem::Error::from_status(StatusCode::BAD_REQUEST))?;
// Use application layer use case
match self.get_status_use_case.execute(relay_id).await {
Ok(relay) => Ok(RelayResponse::Status(Json(relay.into()))),
Err(_) => Ok(RelayResponse::ServiceUnavailable),
}
}
}
```
### Example: Settings Extension
```rust
// src/settings.rs
#[derive(Debug, serde::Deserialize, Clone)]
pub struct ModbusSettings {
pub host: String,
pub port: u16,
pub slave_id: u8,
pub timeout_seconds: u64,
}
#[derive(Debug, serde::Deserialize, Clone)]
pub struct Settings {
pub application: ApplicationSettings,
pub debug: bool,
pub frontend_url: String,
pub rate_limit: RateLimitSettings,
pub modbus: ModbusSettings, // New field
}
```
```yaml
# settings/base.yaml
modbus:
host: "192.168.1.100"
port: 502
slave_id: 1
timeout_seconds: 3
```
---
## Summary
### Key Takeaways
1. **tokio-modbus 0.17.0**: Excellent choice, use trait abstraction for testability
2. **HTTP Polling**: Maintain spec decision, simpler and adequate for scale
3. **Hexagonal Architecture**: Add domain/application layers following existing patterns
4. **Type-Driven Development**: Apply newtype pattern (RelayId, RelayState)
5. **Testing**: Use mockall with async-trait for >90% coverage without hardware
### Next Steps
1. **Clarifying Questions**: Resolve ambiguities in requirements
2. **Architecture Design**: Create multiple implementation approaches
3. **Final Plan**: Select approach and create detailed implementation plan
4. **Implementation**: Follow TDD workflow with types-first design
---
**End of Research Document**

View File

@@ -0,0 +1,51 @@
# Specification Quality Checklist: Modbus Relay Control System
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2025-12-28
**Feature**: [spec.md](./spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- **Note**: Specification intentionally includes some implementation constraints (Rust, Poem, tokio-modbus) per project constitution requirements (NFR-009, NFR-014, NFR-015). These are architectural constraints, not implementation details of business logic.
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- **Resolution**: FR-023 clarified by user - backend starts successfully even when device unhealthy, frontend displays error as part of Health story
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- **Note**: SC-010 references cargo tarpaulin as measurement tool, which is acceptable for NFR validation
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Quality Assessment
**Overall Status**: ✅ **READY FOR PLANNING**
### Strengths
- Comprehensive coverage of 5 prioritized, independently testable user stories
- 37 functional + 21 non-functional requirements provide clear scope
- Edge cases thoroughly documented with specific mitigation strategies
- Success criteria are measurable and aligned with user stories
- Clear boundaries with explicit "Out of Scope" section
- Risk matrix identifies key concerns with mitigation approaches
### Notes
- Specification includes architectural constraints (hexagonal architecture, TDD, TyDD) per project constitution
- These constraints are non-negotiable project requirements, not arbitrary implementation details
- User clarification resolved FR-023 regarding startup behavior when device is unhealthy
- Specification ready for `/sdd:02-plan` stage

View File

@@ -0,0 +1,315 @@
# Feature Specification: Modbus Relay Control System
**Feature Branch**: `001-modbus-relay-control`
**Created**: 2025-12-28
**Status**: Draft
**Input**: User description: "Modbus relay control system: backend reads relay and writes states via Modbus, exposes REST API, frontend displays relay states and allows toggling."
## Executive Summary
### Problem Statement
Users currently require specialized Modbus software (Modbus Poll, SSCOM) to interact with an 8-channel relay device, creating barriers to adoption and limiting remote access capabilities. The lack of a web-based interface prevents non-technical users from controlling relays and limits integration possibilities.
### Proposed Solution
A web application consisting of:
- **Rust Backend**: Modbus RTU over TCP integration + RESTful HTTP API (deployed on Raspberry Pi)
- **Vue.js Frontend**: Real-time relay status display and control interface (deployed on Cloudflare Pages)
- **Reverse Proxy**: Traefik with Authelia middleware for authentication and HTTPS termination
- **Local Network**: Raspberry Pi on same network as Modbus relay device
### Value Proposition
- **Accessibility**: Control relays from any browser without specialized software
- **Usability**: Intuitive UI eliminates need for Modbus protocol knowledge
- **Foundation**: Enables future automation, scheduling, and integration capabilities
- **Deployment**: Self-contained system with no external dependencies
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Monitor Relay Status (Priority: P1)
As a user, I want to see the current state (on/off) of all 8 relays in real-time so I can verify the physical system state without being physically present.
**Why this priority**: Foundation capability - all other features depend on accurate state visibility. Delivers immediate value by eliminating need for physical inspection or specialized software.
**Independent Test**: Can be fully tested by loading the web interface and verifying displayed states match physical relay states (verified with multimeter or visual indicators). Delivers value even without control capabilities.
**Acceptance Scenarios**:
1. **Given** all relays are OFF, **When** I load the web interface, **Then** I see 8 relays each displaying "OFF" state
2. **Given** relay #3 is ON and others are OFF, **When** I load the interface, **Then** I see relay #3 showing "ON" and others showing "OFF"
3. **Given** the interface is loaded, **When** relay state changes externally (via Modbus Poll), **Then** the interface updates within 2 seconds to reflect the new state
4. **Given** the Modbus device is unreachable, **When** I load the interface, **Then** I see an error message indicating the device is unavailable
---
### User Story 2 - Toggle Individual Relay (Priority: P1)
As a user, I want to toggle any relay on or off with a single click so I can control connected devices remotely.
**Why this priority**: Core use case - enables remote control capability. Combined with Story 1, creates a complete minimal viable product.
**Independent Test**: Can be tested by clicking any relay toggle button and observing both UI update and physical relay click/LED change. Delivers standalone value for remote control.
**Acceptance Scenarios**:
1. **Given** relay #5 is OFF, **When** I click the toggle button for relay #5, **Then** relay #5 turns ON and the UI reflects this within 1 second
2. **Given** relay #2 is ON, **When** I click the toggle button for relay #2, **Then** relay #2 turns OFF and the UI reflects this within 1 second
3. **Given** the Modbus device is unreachable, **When** I attempt to toggle a relay, **Then** I see an error message and the UI does not change
4. **Given** I toggle relay #1, **When** the Modbus command times out, **Then** I see a timeout error and can retry
---
### User Story 3 - Bulk Relay Control (Priority: P2)
As a user, I want to turn all relays ON or OFF simultaneously so I can quickly reset the entire system or enable/disable all connected devices at once.
**Why this priority**: Efficiency improvement for common scenarios (system shutdown, initialization). Not critical for MVP but significantly improves user experience.
**Independent Test**: Can be tested by clicking "All ON" or "All OFF" buttons and verifying all 8 physical relays respond. Delivers value for batch operations without requiring individual story implementations.
**Acceptance Scenarios**:
1. **Given** relays have mixed states (some ON, some OFF), **When** I click "All ON", **Then** all 8 relays turn ON within 2 seconds
2. **Given** all relays are ON, **When** I click "All OFF", **Then** all 8 relays turn OFF within 2 seconds
3. **Given** I click "All ON" and relay #4 fails to respond, **Then** I see an error for relay #4 but other relays still turn ON
4. **Given** the Modbus device is unreachable, **When** I click "All ON", **Then** I see an error message and no state changes occur
---
### User Story 4 - System Health Monitoring (Priority: P2)
As a user, I want to see device connectivity status and firmware version so I can diagnose issues and verify device compatibility.
**Why this priority**: Operational value for troubleshooting. Not required for basic control but critical for production reliability and maintenance.
**Independent Test**: Can be tested by viewing the health status section, disconnecting the Modbus device, and observing status change. Delivers standalone diagnostic value.
**Acceptance Scenarios**:
1. **Given** the Modbus device is connected and responsive, **When** I view the health status, **Then** I see "Healthy" status with firmware version displayed
2. **Given** the Modbus device is unreachable, **When** the backend starts, **Then** the backend starts successfully and the frontend displays "Unhealthy - Device Unreachable" status
3. **Given** the Modbus device becomes unreachable during operation, **When** I view the health status, **Then** I see "Unhealthy - Connection Lost" with timestamp of last successful communication
4. **Given** the Modbus device responds but with CRC errors, **When** I view health status, **Then** I see "Degraded - Communication Errors" with error count
---
### User Story 5 - Relay Labeling (Priority: P3)
As a user, I want to assign custom labels to each relay (e.g., "Garage Light", "Water Pump") so I can identify relays by purpose instead of numbers.
**Why this priority**: Usability enhancement - makes system more intuitive for production use. Not required for MVP but improves long-term user experience.
**Independent Test**: Can be tested by assigning a label to relay #1, refreshing the page, and verifying the label persists. Delivers value for multi-relay installations without requiring other stories.
**Acceptance Scenarios**:
1. **Given** I am viewing relay #3, **When** I click "Edit Label" and enter "Office Fan", **Then** relay #3 displays "Office Fan (Relay 3)"
2. **Given** relay #7 has label "Water Pump", **When** I refresh the page, **Then** relay #7 still shows "Water Pump (Relay 7)"
3. **Given** I have labeled multiple relays, **When** I toggle a relay by label, **Then** the correct physical relay responds
4. **Given** two relays have similar labels, **When** I search for a label, **Then** both matching relays are highlighted
---
### Edge Cases
- **Network Partition**: What happens when the Raspberry Pi loses connectivity to the Modbus device mid-operation?
- Backend marks device unhealthy, frontend displays error state, pending operations fail gracefully with clear error messages
- **Concurrent Control**: How does system handle multiple users toggling the same relay simultaneously?
- Last-write-wins semantics, each client receives updated state via polling within 2 seconds
- **Modbus Timeout**: What happens when a relay command times out?
- Backend retries once automatically, if retry fails, returns error to frontend with clear timeout message
- **Partial Bulk Failure**: What happens when "All ON" command succeeds for 7 relays but relay #4 fails?
- Frontend displays partial success with list of failed relays, successful relays remain ON, user can retry failed relays individually
- **Rapid Toggle Requests**: How does system handle user clicking toggle button repeatedly in quick succession?
- Frontend debounces clicks (500ms), backend queues commands serially, prevents command flooding
- **Device Firmware Mismatch**: What happens if relay device firmware version is incompatible?
- Backend logs firmware version, health check displays warning if version is untested, system attempts normal operation with degraded status
- **State Inconsistency**: What happens if Modbus read shows relay state different from expected state after write?
- Backend logs inconsistency, frontend displays actual state (read value), user sees visual indication of unexpected state
- **Browser Compatibility**: How does frontend handle older browsers without modern JavaScript features?
- Vue.js build targets ES2015+, displays graceful error message on IE11 and older, works on all modern browsers (Chrome, Firefox, Safari, Edge)
## Requirements *(mandatory)*
### Functional Requirements
#### Backend - Modbus Integration
- **FR-001**: System MUST establish Modbus RTU over TCP connection to relay device on configurable IP and port (default: device IP, port 502)
- **FR-002**: System MUST use Modbus function code 0x01 (Read Coils) to read all 8 relay states (addresses 0-7)
- **FR-003**: System MUST use Modbus function code 0x05 (Write Single Coil) to toggle individual relays
- **FR-004**: System MUST use Modbus function code 0x0F (Write Multiple Coils) for bulk operations (All ON/All OFF)
- **FR-005**: System MUST validate Modbus CRC16 checksums on all received messages
- **FR-006**: System MUST timeout Modbus operations after 3 seconds
- **FR-007**: System MUST retry failed Modbus commands exactly once before returning error
- **FR-008**: System MUST handle Modbus exception codes (0x01-0x04) and map to user-friendly error messages
- **FR-009**: System MUST use tokio-modbus library version 0.17.0 for Modbus protocol implementation
- **FR-010**: System MUST support configurable Modbus device address (default: 0x01)
#### Backend - REST API
- **FR-011**: System MUST expose `GET /api/relays` endpoint returning array of all relay states (id, state, label)
- **FR-012**: System MUST expose `POST /api/relays/{id}/toggle` endpoint to toggle relay {id} (id: 1-8)
- **FR-013**: System MUST expose `POST /api/relays/bulk` endpoint accepting `{"operation": "all_on" | "all_off"}`
- **FR-014**: System MUST expose `GET /api/health` endpoint returning device status (healthy/unhealthy, firmware version, last_contact timestamp)
- **FR-015**: System MUST expose `PUT /api/relays/{id}/label` endpoint to update relay label (max 50 characters)
- **FR-016**: System MUST return HTTP 200 for successful operations with JSON response body
- **FR-017**: System MUST return HTTP 500 for Modbus communication failures with error details
- **FR-018**: System MUST return HTTP 400 for invalid request parameters (e.g., relay id out of range)
- **FR-019**: System MUST return HTTP 504 for Modbus timeout errors
- **FR-020**: System MUST include OpenAPI 3.0 specification accessible at `/api/specs`
- **FR-021**: System MUST apply rate limiting middleware (100 requests/minute per IP)
- **FR-022**: System MUST apply CORS middleware allowing all origins (local network deployment)
- **FR-023**: System MUST start successfully even if Modbus device is unreachable at startup, marking device as unhealthy
- **FR-024**: System MUST persist relay labels to configuration file (YAML) for persistence across restarts
#### Frontend - User Interface
- **FR-025**: UI MUST display all 8 relays in a grid layout with clear ON/OFF state indication (color-coded)
- **FR-026**: UI MUST provide toggle button for each relay that triggers `POST /api/relays/{id}/toggle`
- **FR-027**: UI MUST provide "All ON" and "All OFF" buttons that trigger `POST /api/relays/bulk`
- **FR-028**: UI MUST poll `GET /api/relays` every 2 seconds to refresh relay states
- **FR-029**: UI MUST display loading indicator while relay operations are in progress
- **FR-030**: UI MUST display error messages when API calls fail, with specific error text from backend
- **FR-031**: UI MUST display health status section showing device connectivity and firmware version
- **FR-032**: UI MUST display "Unhealthy - Device Unreachable" message when backend reports device unreachable
- **FR-033**: UI MUST provide inline label editing for each relay (click to edit, save on blur/enter)
- **FR-034**: UI MUST be responsive and functional on desktop (>1024px), tablet (768-1024px), and mobile (320-767px)
- **FR-035**: UI MUST disable toggle buttons and show error when device is unhealthy
- **FR-036**: UI MUST show timestamp of last successful state update
- **FR-037**: UI MUST debounce toggle button clicks to 500ms to prevent rapid repeated requests
### Non-Functional Requirements
#### Performance
- **NFR-001**: System MUST respond to `GET /api/relays` within 100ms (excluding Modbus communication time)
- **NFR-002**: System MUST complete relay toggle operations within 1 second (including Modbus communication)
- **NFR-003**: System MUST handle 10 concurrent users without performance degradation
- **NFR-004**: Frontend MUST render initial page load within 2 seconds on 10 Mbps connection
#### Reliability
- **NFR-005**: System MUST maintain 95% successful operation rate for Modbus commands
- **NFR-006**: System MUST recover automatically from temporary Modbus connection loss within 5 seconds
- **NFR-007**: System MUST log all Modbus errors with structured logging (timestamp, error code, relay id)
- **NFR-008**: Backend MUST continue serving health and API endpoints even when Modbus device is unreachable
#### Security
- **NFR-009**: Backend MUST run on local network with Modbus device (no direct public internet exposure)
- **NFR-010**: System MUST NOT implement application-level authentication (handled by Traefik middleware with Authelia)
- **NFR-011**: Frontend-to-backend communication MUST use HTTPS via Traefik reverse proxy (backend itself runs HTTP, Traefik handles TLS termination)
- **NFR-012**: System MUST validate all API inputs to prevent injection attacks
- **NFR-013-SEC**: Backend-to-Modbus communication uses unencrypted Modbus TCP (local network only)
#### Maintainability
- **NFR-014**: Code MUST achieve >90% test coverage for domain logic (relay control, Modbus abstraction)
- **NFR-015**: System MUST follow hexagonal architecture with trait-based Modbus abstraction for testability
- **NFR-016**: System MUST use Type-Driven Development (TyDD) with newtype pattern for RelayId, RelayState, ModbusCommand
- **NFR-017**: All public APIs MUST have OpenAPI documentation
- **NFR-018-MAINT**: Code MUST pass `cargo clippy` with zero warnings on all, pedantic, and nursery lints
#### Observability
- **NFR-019**: System MUST emit structured logs at all architectural boundaries (API, Modbus)
- **NFR-020**: System MUST log relay state changes with timestamp, relay id, old state, new state
- **NFR-021**: System MUST expose Prometheus metrics endpoint at `/metrics` (request count, error rate, Modbus latency)
- **NFR-022**: System MUST log startup configuration (Modbus host/port, relay count) at INFO level
### Key Entities
- **Relay**: Represents a single relay channel (1-8) with properties: id (1-8), state (ON/OFF), label (optional, max 50 chars)
- **RelayState**: Enum representing ON or OFF state
- **RelayId**: Newtype wrapping u8 with validation (1-8 range), implements TyDD pattern
- **ModbusCommand**: Enum representing Modbus operations (ReadCoils, WriteSingleCoil, WriteMultipleCoils)
- **DeviceHealth**: Struct representing Modbus device status (`healthy: bool`, `firmware_version: Option<String>`, `last_contact: Option<DateTime>`)
- **RelayLabel**: Newtype wrapping String with validation (max 50 chars, alphanumeric + spaces)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can view all 8 relay states within 2 seconds of loading the web interface
- **SC-002**: Users can toggle any relay with physical relay response within 1 second of button click
- **SC-003**: System achieves 95% successful operation rate for relay toggle commands over 24-hour period
- **SC-004**: Web interface is accessible and functional on Chrome, Firefox, Safari, and Edge browsers
- **SC-005**: Users can successfully use the interface on mobile devices (portrait and landscape)
- **SC-006**: Backend starts successfully and serves health endpoint even when Modbus device is disconnected
- **SC-007**: Frontend displays clear error message within 2 seconds when Modbus device is unhealthy
- **SC-008**: System supports 10 concurrent users performing toggle operations without performance degradation
- **SC-009**: All 8 relays turn ON within 2 seconds when "All ON" button is clicked
- **SC-010**: Domain logic achieves >90% test coverage as measured by `cargo tarpaulin`
### User Experience Goals
- **UX-001**: Non-technical users can control relays without referring to documentation
- **UX-002**: Error messages clearly explain problem and suggest remediation (e.g., "Device unreachable - check network connection")
- **UX-003**: Relay labels make it intuitive to identify relay purpose without memorizing numbers
## Dependencies & Assumptions
### Dependencies
- **Hardware**: 8-channel Modbus POE ETH Relay device (documented in `docs/Modbus_POE_ETH_Relay.md`)
- **Network**: Local network connectivity between Raspberry Pi and relay device
- **Libraries**: tokio-modbus 0.17.0, Poem 3.1, poem-openapi 5.1, Tokio 1.48
- **Frontend**: Vue.js 3.x, TypeScript, Vite build tool
- **Backend Deployment**: Raspberry Pi (or equivalent) running Linux with Docker
- **Frontend Deployment**: Cloudflare Pages (or equivalent static hosting)
- **Reverse Proxy**: Traefik with Authelia middleware for authentication
### Assumptions
- **ASM-001**: Relay device uses Modbus RTU over TCP protocol (per hardware documentation)
- **ASM-002**: Relay device supports standard Modbus function codes 0x01, 0x05, 0x0F
- **ASM-003**: Local network provides reliable connectivity (>95% uptime)
- **ASM-004**: Traefik reverse proxy with Authelia middleware provides adequate authentication
- **ASM-005**: Single user will control relays at a time in most scenarios (concurrent control is edge case)
- **ASM-006**: Relay device exposes 8 coils at Modbus addresses 0-7
- **ASM-007**: Device firmware is compatible with tokio-modbus library
- **ASM-008**: Raspberry Pi has sufficient resources (CPU, memory) to run Rust backend
- **ASM-009**: Cloudflare Pages or equivalent CDN provides fast frontend delivery
- **ASM-010**: Frontend can reach backend via HTTPS through Traefik reverse proxy
## Out of Scope
The following capabilities are explicitly excluded from this specification:
- **Application-Level Authentication**: No user login, role-based access control, or API keys (handled by Traefik/Authelia)
- **Historical Data**: No database, state logging, or historical relay state tracking
- **Scheduling**: No timer-based relay control or automation rules
- **Multiple Devices**: No support for controlling multiple relay devices simultaneously
- **Advanced Modbus Features**: No support for flash modes, timing operations, or device reconfiguration
- **Mobile Native Apps**: Web interface only, no iOS/Android native applications
- **Cloud Backend**: Backend runs on local network (Raspberry Pi), frontend served from Cloudflare Pages
- **Real-time Updates**: HTTP polling only (no WebSocket, Server-Sent Events)
## Risks & Mitigations
| Risk | Impact | Probability | Mitigation |
|--------------------------------------------|--------|-------------|------------------------------------------------------------------------|
| Modbus device firmware incompatibility | High | Low | Test with actual hardware early, document compatible firmware versions |
| Network latency exceeds timeout thresholds | Medium | Medium | Make timeouts configurable, implement adaptive retry logic |
| Concurrent control causes state conflicts | Low | Medium | Implement last-write-wins with clear state refresh in UI |
| Frontend polling overwhelms backend | Low | Low | Rate limit API endpoints, make poll interval configurable |
| Raspberry Pi resource exhaustion | Medium | Low | Benchmark with 10 concurrent users, optimize Modbus connection pooling |
## Revision History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0 | 2025-12-28 | Business Analyst Agent | Initial specification based on user input |
| 1.1 | 2025-12-28 | User Clarification | FR-023 clarified: Backend starts successfully even when device unhealthy, frontend displays error (part of Health story) |
| 1.2 | 2025-12-29 | User Clarification | Architecture updated: Frontend on Cloudflare Pages, backend on RPi behind Traefik with Authelia. Updated NFR-009 to NFR-013-SEC to reflect HTTPS via reverse proxy, authentication via Traefik middleware |

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

243
specs/constitution.md Normal file
View File

@@ -0,0 +1,243 @@
<!--
SYNC IMPACT REPORT
==================
Version Change: 1.0.0 → 1.1.0
Amendment Date: 2025-12-27
Type: MINOR - New principle added
Modified Principles:
- Principle I: Enhanced with explicit SOLID principles reference
Added Sections:
- New Principle VI: SOLID Principles
Removed Sections:
- None
Templates Status:
✅ plan-template.md - already aligned
✅ spec-template.md - already aligned
✅ spec-checklist.md - already aligned
✅ tasks-template.md - already aligned
Follow-up TODOs:
- None (all changes integrated)
Rationale:
Adding SOLID principles as an explicit constitutional principle reinforces
the existing clean architecture and hexagonal architecture principles with
concrete design guidelines. This is a MINOR bump because it adds new
governance guidance without changing existing principles.
-->
# STA (Smart Temperature & Appliance Control) Constitution
## Core Principles
### I. Hexagonal Architecture (Clean Architecture)
The system MUST follow hexagonal architecture principles with clear separation of concerns:
- **Domain Layer**: Pure business logic with no external dependencies
- **Application Layer**: Use cases and orchestration logic
- **Infrastructure Layer**: External concerns (HTTP, Modbus, persistence)
- **Presentation Layer**: API contracts and DTOs
All dependencies MUST point inward. Infrastructure and presentation layers depend on domain/application, never the reverse. This ensures testability, maintainability, and framework independence.
**SOLID Alignment**: This principle directly enforces Dependency Inversion Principle (DIP) through inward-pointing dependencies and Interface Segregation Principle (ISP) through layer boundaries.
**Rationale**: Hexagonal architecture enables independent testing of business logic, technology substitution without domain changes, and clear ownership boundaries between layers.
### II. Domain-Driven Design
The domain model MUST be rich and expressive:
- Domain entities encapsulate business rules and invariants
- Value objects are immutable and self-validating
- Repositories abstract persistence concerns
- Services contain domain logic that doesn't belong to entities
- Clear ubiquitous language shared between code and specifications
Domain types MUST NOT leak across architectural boundaries. DTOs and domain entities are distinct.
**Rationale**: DDD ensures the codebase reflects real-world domain concepts, making it easier to reason about, maintain, and evolve as business requirements change.
### III. Test-First Development (NON-NEGOTIABLE)
TDD is mandatory for all feature development:
1. Write failing tests first
2. User reviews and approves test scenarios
3. Implement minimal code to pass tests
4. Refactor while keeping tests green
Test coverage MUST include:
- Unit tests for domain logic (isolated, fast)
- Integration tests for infrastructure adapters (Modbus, HTTP)
- Contract tests for API endpoints
- Mock-based tests to avoid hardware dependencies during CI
**Rationale**: Test-first ensures specifications are validated before implementation, prevents regression, and serves as executable documentation. The red-green-refactor cycle enforces disciplined development.
### IV. API-First Design
All functionality MUST be exposed through well-defined API contracts:
- RESTful HTTP API for web interface (Poem + OpenAPI)
- Modbus TCP protocol for relay hardware communication
- Clear separation between public API contracts and internal implementation
- OpenAPI specifications generated and maintained automatically
- API versioning strategy for backward compatibility
Backend (Rust) and frontend (TypeScript/Vue) communicate exclusively through documented API contracts.
**Rationale**: API-first design enables parallel frontend/backend development, clear integration points, and prevents tight coupling between presentation and business logic.
### V. Observability & Monitoring
Production systems MUST be observable:
- Structured logging at all architectural boundaries (tracing crate)
- Request/response logging for HTTP and Modbus communication
- Health check endpoints for system status
- Error context preserved across layer boundaries (thiserror)
- JSON log format for production environments
- Human-readable format for development
Debugging MUST be possible without modifying code.
**Rationale**: Observability enables rapid diagnosis of production issues, performance analysis, and understanding system behavior under real-world conditions.
### VI. SOLID Principles
All code MUST adhere to SOLID design principles:
**Single Responsibility Principle (SRP)**:
- Each module, class, or function has ONE reason to change
- Domain entities focus on business rules only
- Infrastructure adapters focus on external integration only
- Controllers/handlers focus on request orchestration only
**Open/Closed Principle (OCP)**:
- Entities open for extension via traits/interfaces
- Closed for modification through trait implementations
- New behavior added through new implementations, not modifications
**Liskov Substitution Principle (LSP)**:
- Trait implementations MUST be substitutable without breaking behavior
- Mock implementations MUST honor trait contracts
- Repository implementations MUST preserve domain semantics
**Interface Segregation Principle (ISP)**:
- Traits MUST be focused and minimal
- Clients depend only on methods they use
- Large interfaces split into role-specific traits
**Dependency Inversion Principle (DIP)**:
- High-level domain logic depends on abstractions (traits)
- Low-level infrastructure implements abstractions
- Dependencies injected through constructors/builders
- No direct instantiation of concrete infrastructure types in domain
**Rationale**: SOLID principles ensure code remains maintainable, testable, and extensible as the system evolves. They provide concrete design rules that support the broader clean architecture goals.
## Technology Stack
### Backend (Rust)
- **Web Framework**: Poem with OpenAPI support (poem-openapi)
- **Async Runtime**: Tokio
- **Modbus Protocol**: tokio-modbus 0.17.0
- **Configuration**: config crate with YAML support
- **Logging**: tracing + tracing-subscriber
- **Error Handling**: thiserror
- **Testing**: Built-in Rust test framework + mockall
### Frontend (TypeScript/Vue)
- **Framework**: Vue 3 with TypeScript
- **HTTP Client**: Type-safe API client generated from OpenAPI specs
- **Build Tool**: Vite
- **State Management**: Pinia (if needed for complex state)
### Architecture Patterns
- Clean/Hexagonal architecture with explicit layer boundaries
- Repository pattern for persistence abstraction
- Trait-based dependency injection
- Mock-based testing to avoid hardware dependencies
- SOLID principles applied to all design decisions
## Development Workflow
### Feature Development Process
1. **Specification Phase**:
- Write feature specification in `specs/<feature>/spec.md`
- Define data model in `specs/<feature>/data-model.md`
- Create implementation plan in `specs/<feature>/plan.md`
- Document API contracts in `specs/<feature>/contracts/`
2. **Test-First Implementation**:
- Write failing tests for domain logic
- Write failing tests for infrastructure adapters
- Write failing contract tests for API endpoints
- Get user approval on test scenarios
- Implement code to pass tests
- Refactor while maintaining green tests
3. **Integration & Validation**:
- Integration tests with mocked hardware
- Real hardware testing (when available)
- OpenAPI documentation validation
- Code review focusing on architectural compliance
### Code Review Requirements
All code changes MUST be reviewed for:
- Compliance with hexagonal architecture principles
- SOLID principles adherence
- Domain model clarity and expressiveness
- Test coverage and quality
- API contract adherence
- Observability (logging, error context)
- No domain logic leaking into infrastructure layer
### Quality Gates
Code MUST NOT be merged unless:
- All tests pass (unit + integration)
- Test coverage meets minimum thresholds
- Architecture review confirms layer separation
- SOLID principles validated (no SRP/DIP violations)
- OpenAPI specs are up-to-date
- Logging captures key operational events
## Governance
This constitution supersedes all other development practices and guidelines. All architectural decisions MUST align with the principles defined herein.
### Amendment Process
Amendments to this constitution require:
1. Documented rationale for the change
2. Impact analysis on existing codebase
3. Migration plan if breaking existing patterns
4. Approval before implementation
### Compliance Verification
- All pull requests MUST verify constitutional compliance
- Architecture decisions MUST be justified against these principles
- Complexity introduced MUST be necessary to uphold principles
- Violations MUST be addressed before merge
### Version Control
This constitution uses semantic versioning:
- **MAJOR**: Breaking changes to core principles
- **MINOR**: New principles or significant clarifications
- **PATCH**: Typo fixes, wording improvements
**Version**: 1.1.0 | **Ratified**: 2025-12-27 | **Last Amended**: 2025-12-27

View File

@@ -0,0 +1,104 @@
# Implementation Plan: [FEATURE]
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
[Extract from feature spec: primary requirement + technical approach from research]
## Technical Context
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
for the project. The structure here is presented in advisory capacity to guide
the iteration process.
-->
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
**Project Type**: [single/web/mobile - determines source structure]
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
[Gates determined based on constitution file]
## Project Structure
### Documentation (this feature)
```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
<!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
real paths (e.g., apps/admin, packages/something). The delivered plan must
not include Option labels.
-->
```text
# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
src/
├── models/
├── services/
├── cli/
└── lib/
tests/
├── contract/
├── integration/
└── unit/
# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
backend/
├── src/
│ ├── models/
│ ├── services/
│ └── api/
└── tests/
frontend/
├── src/
│ ├── components/
│ ├── pages/
│ └── services/
└── tests/
# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
api/
└── [same as backend above]
ios/ or android/
└── [platform-specific structure: feature modules, UI flows, platform tests]
```
**Structure Decision**: [Document the selected structure and reference the real
directories captured above]
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |

View File

@@ -0,0 +1,34 @@
# Specification Quality Checklist: [FEATURE NAME]
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: [DATE]
**Feature**: [Link to spec.md]
## Content Quality
- [ ] No implementation details (languages, frameworks, APIs)
- [ ] Focused on user value and business needs
- [ ] Written for non-technical stakeholders
- [ ] All mandatory sections completed
## Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [ ] All acceptance scenarios are defined
- [ ] Edge cases are identified
- [ ] Scope is clearly bounded
- [ ] Dependencies and assumptions identified
## Feature Readiness
- [ ] All functional requirements have clear acceptance criteria
- [ ] User scenarios cover primary flows
- [ ] Feature meets measurable outcomes defined in Success Criteria
- [ ] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/sdd:01-specify` or `/sdd:01-plan`

View File

@@ -0,0 +1,115 @@
# Feature Specification: [FEATURE NAME]
**Feature Branch**: `[###-feature-name]`
**Created**: [DATE]
**Status**: Draft
**Input**: User description: "$ARGUMENTS"
## User Scenarios & Testing *(mandatory)*
<!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be:
- Developed independently
- Tested independently
- Deployed independently
- Demonstrated to users independently
-->
### User Story 1 - [Brief Title] (Priority: P1)
[Describe this user journey in plain language]
**Why this priority**: [Explain the value and why it has this priority level]
**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
**Acceptance Scenarios**:
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
---
### User Story 2 - [Brief Title] (Priority: P2)
[Describe this user journey in plain language]
**Why this priority**: [Explain the value and why it has this priority level]
**Independent Test**: [Describe how this can be tested independently]
**Acceptance Scenarios**:
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
---
### User Story 3 - [Brief Title] (Priority: P3)
[Describe this user journey in plain language]
**Why this priority**: [Explain the value and why it has this priority level]
**Independent Test**: [Describe how this can be tested independently]
**Acceptance Scenarios**:
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
---
[Add more user stories as needed, each with an assigned priority]
### Edge Cases
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right edge cases.
-->
- What happens when [boundary condition]?
- How does system handle [error scenario]?
## Requirements *(mandatory)*
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right functional requirements.
-->
### Functional Requirements
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
*Example of marking unclear requirements:*
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
### Key Entities *(include if feature involves data)*
- **[Entity 1]**: [What it represents, key attributes without implementation]
- **[Entity 2]**: [What it represents, relationships to other entities]
## Success Criteria *(mandatory)*
<!--
ACTION REQUIRED: Define measurable success criteria.
These must be technology-agnostic and measurable.
-->
### Measurable Outcomes
- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]

View File

@@ -0,0 +1,251 @@
---
description: "Task list template for feature implementation"
---
# Tasks: [FEATURE NAME]
**Input**: Design documents from `/specs/[###-feature-name]/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
## Path Conventions
- **Single project**: `src/`, `tests/` at repository root
- **Web app**: `backend/src/`, `frontend/src/`
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
- Paths shown below assume single project - adjust based on plan.md structure
<!--
============================================================================
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
The /speckit.tasks command MUST replace these with actual tasks based on:
- User stories from spec.md (with their priorities P1, P2, P3...)
- Feature requirements from plan.md
- Entities from data-model.md
- Endpoints from contracts/
Tasks MUST be organized by user story so each story can be:
- Implemented independently
- Tested independently
- Delivered as an MVP increment
DO NOT keep these sample tasks in the generated tasks.md file.
============================================================================
-->
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Project initialization and basic structure
- [ ] T001 Create project structure per implementation plan
- [ ] T002 Initialize [language] project with [framework] dependencies
- [ ] T003 [P] Configure linting and formatting tools
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
Examples of foundational tasks (adjust based on your project):
- [ ] T004 Setup database schema and migrations framework
- [ ] T005 [P] Implement authentication/authorization framework
- [ ] T006 [P] Setup API routing and middleware structure
- [ ] T007 Create base models/entities that all stories depend on
- [ ] T008 Configure error handling and logging infrastructure
- [ ] T009 Setup environment configuration management
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
---
## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
### Implementation for User Story 1
- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T016 [US1] Add validation and error handling
- [ ] T017 [US1] Add logging for user story 1 operations
**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
---
## Phase 4: User Story 2 - [Title] (Priority: P2)
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
### Implementation for User Story 2
- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
- [ ] T021 [US2] Implement [Service] in src/services/[service].py
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
---
## Phase 5: User Story 3 - [Title] (Priority: P3)
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
### Implementation for User Story 3
- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
**Checkpoint**: All user stories should now be independently functional
---
[Add more user story phases as needed, following the same pattern]
---
## Phase N: Polish & Cross-Cutting Concerns
**Purpose**: Improvements that affect multiple user stories
- [ ] TXXX [P] Documentation updates in docs/
- [ ] TXXX Code cleanup and refactoring
- [ ] TXXX Performance optimization across all stories
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
- [ ] TXXX Security hardening
- [ ] TXXX Run quickstart.md validation
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Stories (Phase 3+)**: All depend on Foundational phase completion
- User stories can then proceed in parallel (if staffed)
- Or sequentially in priority order (P1 → P2 → P3)
- **Polish (Final Phase)**: Depends on all desired user stories being complete
### User Story Dependencies
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
### Within Each User Story
- Tests (if included) MUST be written and FAIL before implementation
- Models before services
- Services before endpoints
- Core implementation before integration
- Story complete before moving to next priority
### Parallel Opportunities
- All Setup tasks marked [P] can run in parallel
- All Foundational tasks marked [P] can run in parallel (within Phase 2)
- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
- All tests for a user story marked [P] can run in parallel
- Models within a story marked [P] can run in parallel
- Different user stories can be worked on in parallel by different team members
---
## Parallel Example: User Story 1
```bash
# Launch all tests for User Story 1 together (if tests requested):
Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
Task: "Integration test for [user journey] in tests/integration/test_[name].py"
# Launch all models for User Story 1 together:
Task: "Create [Entity1] model in src/models/[entity1].py"
Task: "Create [Entity2] model in src/models/[entity2].py"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
3. Complete Phase 3: User Story 1
4. **STOP and VALIDATE**: Test User Story 1 independently
5. Deploy/demo if ready
### Incremental Delivery
1. Complete Setup + Foundational → Foundation ready
2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
3. Add User Story 2 → Test independently → Deploy/Demo
4. Add User Story 3 → Test independently → Deploy/Demo
5. Each story adds value without breaking previous stories
### Parallel Team Strategy
With multiple developers:
1. Team completes Setup + Foundational together
2. Once Foundational is done:
- Developer A: User Story 1
- Developer B: User Story 2
- Developer C: User Story 3
3. Stories complete and integrate independently
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Each user story should be independently completable and testable
- Verify tests fail before implementing
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently
- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence