Python daemon and WebSocket API for remotely switching power outputs through R421B16 Modbus relay boards.
RCPD backend provides a small daemon for controlling relay outputs over Modbus RTU. It is primarily intended for 16-channel R421B16 RS485 relay boards connected to a Raspberry Pi or another Linux device.
The original use case is remote power control in a server room: switching or restarting devices that do not provide IPMI, managed PDU support, or another dedicated remote power interface.
The backend currently provides:
- R421B16 relay board support over Modbus RTU
- a long-running
rcpd.pydaemon - a WebSocket API for control and state queries
- MySQL/MariaDB-backed board and relay configuration
- hardware smoke test script
- protocol unit tests
The daemon keeps Modbus access serialized, periodically reads relay states, and exposes the latest known state through the WebSocket API.
/
βββ rcpd.py # Main daemon entry point
βββ ws_server.py # WebSocket server and immediate API responses
βββ protocol.py # WebSocket command validation and address parsing
βββ models.py # Peewee database models
βββ repository.py # Database read/query helpers
βββ requirements.txt # Python dependencies
βββ .env.example # Example local configuration
βββ relay_drivers/
β βββ R421B16.py # R421B16 relay board driver
β βββ modbus.py # Low-level Modbus RTU helper
β βββ serial_ports.py # Serial port discovery helper
βββ examples/
β βββ hardware_smoke_test.py # Hardware smoke test sequence
β βββ rcpd_tui.py # SSH-friendly terminal UI prototype
β βββ websocket_manual_toggle_demo.py # Simple guarded manual toggle demo
βββ docs/
β βββ database/
β β βββ rcpd_schema_with_demo_data.sql # Demo database schema and data
β βββ hardware/
β β βββ R421B16/ # Relay board datasheets and wiring reference
β β βββ Waveshare-RS485-CAN-HAT/ # Raspberry Pi HAT reference material
β βββ systemd/
β βββ rcpd.service.example # Example systemd unit
βββ tests/
βββ test_protocol.py # WebSocket protocol validation tests
βββ test_r421b16.py # R421B16 driver unit tests
βββ test_websocket_manual_toggle_demo.py # Manual toggle demo helper tests
βββ test_ws_server.py # WebSocket queue handling tests
Runtime requirements:
- Python 3
- MySQL or MariaDB
- RS485 serial interface
- one or more R421B16 relay boards
Python dependencies:
peewee
PyMySQL
python-dotenv
pyserial
termcolor
websockets
Install dependencies:
python3 -m pip install -r requirements.txtCreate a local .env file from the example:
cp .env.example .envExample configuration:
DB_HOST=localhost
DB_USER=rcpd
DB_PASS=change-me
DB_NAME=rcpd
DEVICE=/dev/ttyAMA0
BAUD_RATE=9600
WS_SERVER_LISTENING_ADDR=127.0.0.1
WS_SERVER_LISTENING_PORT=8001
COMMAND_QUEUE_MAX_SIZE=100
LOG_FILE=/var/log/rcpd.log
PID_FILE=/var/run/rcpd.pid
LOG_LEVEL=INFOCommon serial devices:
/dev/ttyAMA0 Raspberry Pi HAT such as Waveshare RS485 CAN HAT
/dev/ttyUSB0 USB-to-RS485 converter, often CH340-based
If you use a Raspberry Pi, the Waveshare RS485 CAN HAT is a recommended option.
The R421B16 factory default baud rate is usually 9600. If your boards were reconfigured, update BAUD_RATE in .env.
The WebSocket server listens on 127.0.0.1 by default. Use 0.0.0.0 only when the API should be reachable from other hosts.
COMMAND_QUEUE_MAX_SIZE limits how many relay control commands may wait for Modbus processing. When the queue is full, new relay commands are rejected with an ERROR WebSocket response instead of being accepted indefinitely.
LOG_LEVEL controls file logging and optional console logging. Use INFO for normal operation and DEBUG for detailed troubleshooting.
Start the daemon:
./rcpd.pyMirror logs to the console:
./rcpd.py --debugShow version:
./rcpd.py --versionThe daemon writes detailed logs to LOG_FILE and stores its process lock in PID_FILE.
Relay states are logged on the first successful read and then only when a state changes at INFO level. Repeated unchanged state reads are logged only at DEBUG level.
On SIGINT or SIGTERM, the daemon requests a graceful shutdown, stops the WebSocket server, closes the serial/Modbus connection, and removes its PID file.
An example systemd unit is available in docs/systemd/rcpd.service.example.
This is mainly useful on systemd-based Linux distributions such as Raspberry Pi OS, Debian, or Ubuntu. Before installing the service, review and adjust at least these values:
UserandGroupWorkingDirectoryExecStart- paths in
.env, especiallyLOG_FILEandPID_FILE
Example installation:
sudo cp docs/systemd/rcpd.service.example /etc/systemd/system/rcpd.service
sudo systemctl daemon-reload
sudo systemctl enable --now rcpdCheck service status:
sudo systemctl status rcpdFollow service logs:
sudo journalctl -u rcpd -fThe daemon reads .env from its working directory, so WorkingDirectory in the service unit must point to the backend directory containing rcpd.py and .env.
Default endpoint:
ws://127.0.0.1:8001
The WebSocket API uses a small custom JSON protocol.
Each request must be a JSON object containing exactly one command. Incoming messages are validated before they are accepted. Invalid JSON, unknown commands, missing board addresses, invalid relay numbers, unsupported argument types, and out-of-range values are rejected and not added to the command queue.
There are two command groups:
- helper commands, which do not operate on a specific relay board
- relay commands, where the command payload is keyed by Modbus board address
Relay command payloads use this general shape:
{ "COMMAND": { "BOARD_ADDRESS": { "relays": [1, 2, 3] } } }Helper commands:
{ "CMD_HELLO": null }
{ "CMD_GETCONFIG": null }
{ "CMD_GETSTATES": null }
{ "CMD_RSTQUEUE": null }
{ "CFG_CHANGED": null }Helper command meaning:
CMD_HELLO- simple daemon availability check.CMD_GETCONFIG- returns relay board, relay label, and relay contact type configuration from the database.CMD_GETSTATES- returns the latest known relay state snapshot.CMD_RSTQUEUE- clears queued control commands that have not been processed yet.CFG_CHANGED- tells the daemon to reload relay board configuration from the database.
Relay commands:
These commands can switch one relay, multiple relays, or all relays on a selected board.
{ "CMD_ON": { "0x1": { "relays": [1, 2, 3] } } }
{ "CMD_OFF": { "0x1": { "relays": [1, 2, 3] } } }
{ "CMD_TOGGLE": { "0x1": { "relays": [1, 2, 3] } } }
{ "CMD_LATCH": { "0x1": { "relays": [1] } } }
{ "CMD_MOMENTARY": { "0x1": { "relays": [1] } } }
{ "CMD_DELAY": { "0x1": { "relays": [1], "delay": 5 } } }
{ "CMD_ON_ALL": { "0x1": null } }
{ "CMD_OFF_ALL": { "0x1": null } }Relay command meaning:
CMD_ON/CMD_OFF- switch selected relay numbers on or off.CMD_TOGGLE- toggle selected relay numbers.CMD_ON_ALL/CMD_OFF_ALL- switch all relays on the selected board.CMD_LATCH- board-level latch/interlock command for selected relays.CMD_MOMENTARY- board-level momentary pulse command for selected relays.CMD_DELAY- board-level delayed off command using thedelayvalue.
Observed R421B16 behavior from the hardware smoke test:
CMD_LATCHswitches the selected relay on and switches other relays on the same board off. When multiple relays are sent through the current API, commands are executed sequentially, so the last relay in the list remains on.CMD_MOMENTARYswitches the selected relay on briefly and then the board switches it off again automatically. The observed pulse length was about one second.CMD_DELAYswitches the selected relay on immediately and the board switches it off after the requesteddelayvalue. When multiple relays are sent, they are commanded sequentially, so their delayed switch-off times may be staggered.
Relay numbers are 1..16.
Board addresses can be decimal or hexadecimal strings, for example 1, 24, 0x01, or 0x18.
Relay contact_type values are NO or NC. This is configuration metadata for clients; CMD_ON and CMD_OFF still mean physical relay coil on/off and are not inverted by contact_type.
Each WebSocket response includes:
result-OKorERRORmessage- short status textrelay_states- latest known relay state snapshotin_queue- number of queued control commands
Relay commands are accepted into a bounded processing queue. If the queue is full, the command is rejected and is not executed.
Run the hardware smoke test against a running daemon:
python3 examples/hardware_smoke_test.pyThe smoke test connects to the WebSocket API, asks the daemon to reload configuration, clears the command queue, and sends a fixed sequence of relay commands to verify that the daemon, Modbus communication, and relay board responses work together.
The tested board addresses are configured near the top of the script in BOARD_ADDRESSES (0x1 and 0x2 by default). Use one address, for example ["0x1"], when only one board should be tested. The full relay command sequence is executed for one configured board address, then for the next one. After the switching sequence, it keeps polling relay states every STATE_POLL_INTERVAL seconds until interrupted.
The smoke test sends real relay commands. Use it only when the connected hardware and powered devices can be safely switched.
There is also a simple guarded manual toggle demo:
python3 examples/websocket_manual_toggle_demo.pyIt loads relay configuration from the daemon through CMD_GETCONFIG, prints relay contact types as [NO] or [NC], shows a simple colored relay state overview for enabled boards, validates board/relay input against that configuration, toggles one relay at a time, and can reset the command queue with rq. It is intentionally small and is not the primary TUI client.
The terminal UI prototype can be started with:
python3 examples/rcpd_tui.pyIt loads relay rows from CMD_GETCONFIG, refreshes relay states and queue depth through CMD_GETSTATES, and provides SSH-friendly navigation, scrolling, action focus, and Enter-driven row actions. Disabled boards are shown as non-interactive DISABLED board headers without relay rows.
Additional project documentation is stored in docs/:
docs/database/- demo database schema and demo data, including relaycontact_typemetadata.docs/hardware/R421B16/- relay board reference files and wiring diagram.docs/hardware/Waveshare-RS485-CAN-HAT/- Raspberry Pi HAT reference material. See also the Waveshare RS485 CAN HAT wiki.docs/systemd/- example systemd service unit.
Run unit tests:
python3 -m unittest discover -s testsor:
bash tests/run_unittest.shRun a syntax/bytecode check:
python3 -m compileall .Relay state refresh is optimized for the R421B16 board.
The daemon reads all 16 relay states from one board with a single Modbus FC3 request (Read Holding Registers). This was verified on real hardware with mbpoll:
mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 1 -c 16 /dev/ttyAMA0Standard multi-write Modbus functions were also tested, but the R421B16 board did not respond:
- FC15
Write Multiple Coils- connection timed out - FC16
Write Multiple Holding Registers- connection timed out
For this reason, multi-relay write commands are intentionally implemented as a sequence of single-relay control commands. Bulk relay writes are not implemented.
- This project is intentionally focused on the backend daemon.
- The current implementation assumes R421B16-style relay numbering
1..16in the public WebSocket protocol. - Modbus board addresses are validated in the range
0..63, matching the 6 DIP switch address range used by the R421B16 board. - Console output, logs, and WebSocket messages are in English; code comments are currently mostly Czech.
- Erriez/R421A08-rs485-8ch-relay-board for publishing the R421A08 RS485 relay board project, which this project was originally based on and from which some files were adapted.
This project is provided free of charge for personal, educational, and experimental use.
Some files in relay_drivers/ are based on the MIT-licensed Erriez R421A08 relay board project and keep their original MIT license header.