This is the fully working configuration for a web control panel for the 52Pi EP-0099 4-Channel I²C Relay board on a Tracker Raspberry Pi.
Design goals:
- Runs on its own port (no interference with tracker UI)
- Runs as an unprivileged user (no root web server)
- Uses I²C only (no GPIO conflicts)
- Uses a writable lock file path (prevents concurrent I²C writes)
Hardware
Board: 52Pi EP-0099 4-Channel I²C Relay
Bus: 1
Address: 0x10 (default DIP)
Relay control:
- Registers: 0x01 → 0x04
- Values: 0xFF = ON, 0x00 = OFF
Manual control (known working):
i2cset -y 1 0x10 0x01 0xFF
i2cset -y 1 0x10 0x01 0x00
System user + permissions
Create the service user:
sudo useradd -r -s /usr/sbin/nologin relayui
Add it to the i2c group:
sudo usermod -aG i2c relayui
Confirm I²C device permissions:
ls -l /dev/i2c-1
Expected style:
crw-rw---- 1 root i2c ... /dev/i2c-1
Install the app + Python environment
Create the app folder:
sudo mkdir -p /opt/relayui
sudo chown relayui:relayui /opt/relayui
Install venv support:
sudo apt-get install -y python3-venv
Create the venv and install deps:
sudo -u relayui python3 -m venv /opt/relayui/venv
sudo -u relayui /opt/relayui/venv/bin/pip install --upgrade pip
sudo -u relayui /opt/relayui/venv/bin/pip install flask smbus2 filelock
Application file: /opt/relayui/app.py
Create/edit:
sudo -u relayui nano /opt/relayui/app.py
Paste this entire file:
#!/usr/bin/env python3
import os
from flask import Flask, jsonify, request, Response
from smbus2 import SMBus
from filelock import FileLock
I2C_BUS = int(os.environ.get("RELAY_I2C_BUS", "1"))
I2C_ADDR = int(os.environ.get("RELAY_I2C_ADDR", "0x10"), 16)
# IMPORTANT: lock path must be writable by relayui user
LOCK_PATH = os.environ.get("RELAY_I2C_LOCK", "/tmp/relayui-i2c.lock")
BIND_HOST = os.environ.get("RELAY_BIND", "0.0.0.0")
BIND_PORT = int(os.environ.get("RELAY_PORT", "8787"))
app = Flask(__name__)
# In-memory last-commanded state
state = {1: False, 2: False, 3: False, 4: False}
def write_relay(channel: int, on: bool):
if channel not in (1, 2, 3, 4):
raise ValueError("Channel must be 1..4")
# EP-0099 control: registers 0x01..0x04, values 0xFF=ON, 0x00=OFF
value = 0xFF if on else 0x00
with FileLock(LOCK_PATH):
with SMBus(I2C_BUS) as bus:
bus.write_byte_data(I2C_ADDR, channel, value)
state[channel] = on
@app.get("/api/state")
def api_state():
return jsonify(bus=I2C_BUS, addr=hex(I2C_ADDR), relays=state)
@app.post("/api/relay/<int:channel>")
def api_relay(channel):
data = request.get_json(silent=True) or {}
if "on" not in data:
return jsonify(ok=False, error='JSON body must contain {"on": true/false}'), 400
try:
write_relay(channel, bool(data["on"]))
return jsonify(ok=True, channel=channel, on=state[channel])
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
@app.post("/api/all_off")
def api_all_off():
try:
for ch in (1, 2, 3, 4):
write_relay(ch, False)
return jsonify(ok=True)
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
@app.get("/")
def index():
return Response("Relay UI running", mimetype="text/plain")
if __name__ == "__main__":
app.run(host=BIND_HOST, port=BIND_PORT)
Make it executable (optional):
sudo chmod +x /opt/relayui/app.py
systemd service: /etc/systemd/system/relayui.service
Create/edit:
sudo nano /etc/systemd/system/relayui.service
Paste:
[Unit]
Description=EP-0099 Relay Web UI
After=network.target
[Service]
Type=simple
User=relayui
Group=relayui
SupplementaryGroups=i2c
WorkingDirectory=/opt/relayui
Environment=RELAY_PORT=8787
Environment=RELAY_I2C_BUS=1
Environment=RELAY_I2C_ADDR=0x10
# IMPORTANT: writable lock file (fixes permission denied in /run)
Environment=RELAY_I2C_LOCK=/tmp/relayui-i2c.lock
ExecStart=/opt/relayui/venv/bin/python /opt/relayui/app.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
Enable + start:
sudo systemctl daemon-reload
sudo systemctl enable --now relayui
Verification
Check status:
systemctl status relayui --no-pager
Confirm listening port:
ss -ltnp | grep 8787
Test relay 1 via API:
curl -s -X POST http://127.0.0.1:8787/api/relay/1 -H "Content-Type: application/json" -d '{"on": true}'
Expected:
- relay clicks
- JSON ok:true
Open in browser:
http://192.168.8.10:8787/
Notes
- Port 8787 is a non-privileged port, so the service can bind without special capabilities.
- The service runs as relayui (non-root) and only needs membership in the i2c group.
- The lock file uses /tmp to avoid permission issues writing into /run.