Skip to content

Repository files navigation

⚑ Remote Controlled Power Distribution (RCPD)

Python daemon and WebSocket API for remotely switching power outputs through R421B16 Modbus relay boards.

🎯 Overview

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.py daemon
  • 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.

πŸ—‚οΈ Project Structure

/
β”œβ”€β”€ 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

βš™οΈ Requirements

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.txt

πŸ”§ Configuration

Create a local .env file from the example:

cp .env.example .env

Example 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=INFO

Common 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.

πŸš€ Running

Start the daemon:

./rcpd.py

Mirror logs to the console:

./rcpd.py --debug

Show version:

./rcpd.py --version

The 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.

🧩 systemd Service

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:

  • User and Group
  • WorkingDirectory
  • ExecStart
  • paths in .env, especially LOG_FILE and PID_FILE

Example installation:

sudo cp docs/systemd/rcpd.service.example /etc/systemd/system/rcpd.service
sudo systemctl daemon-reload
sudo systemctl enable --now rcpd

Check service status:

sudo systemctl status rcpd

Follow service logs:

sudo journalctl -u rcpd -f

The daemon reads .env from its working directory, so WorkingDirectory in the service unit must point to the backend directory containing rcpd.py and .env.

πŸ”Œ WebSocket API

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 the delay value.

Observed R421B16 behavior from the hardware smoke test:

  • CMD_LATCH switches 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_MOMENTARY switches the selected relay on briefly and then the board switches it off again automatically. The observed pulse length was about one second.
  • CMD_DELAY switches the selected relay on immediately and the board switches it off after the requested delay value. 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 - OK or ERROR
  • message - short status text
  • relay_states - latest known relay state snapshot
  • in_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.

πŸ§ͺ Examples

Run the hardware smoke test against a running daemon:

python3 examples/hardware_smoke_test.py

The 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.py

It 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.py

It 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.

πŸ“š Documentation

Additional project documentation is stored in docs/:

  • docs/database/ - demo database schema and demo data, including relay contact_type metadata.
  • 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.

βœ… Tests

Run unit tests:

python3 -m unittest discover -s tests

or:

bash tests/run_unittest.sh

Run a syntax/bytecode check:

python3 -m compileall .

πŸ”Œ R421B16 Modbus Notes

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/ttyAMA0

Standard 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.

πŸ“ Notes

  • This project is intentionally focused on the backend daemon.
  • The current implementation assumes R421B16-style relay numbering 1..16 in 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.

πŸ™ Special Thanks

License

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.

About

Small daemon and terminal client for remotely switching power outputs, primarily using multiple 16-channel Modbus relay boards R421B16.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Sponsor this project

Contributors

Languages