HSSP Gate Control System — Technical Report
HSSP Gate Control System — Technical Report
HSSP Gate Control System — Technical Report
Web code-entry page + local Python poller + Wi-Fi relay. Prepared as system documentation.
1. Overview
This system lets an authorized user type a numeric code into a web page. The code is written to a local file on the web server. A Python script running on a PC on the same network polls that file, validates the code, and — if valid and fresh — pulses a Wi-Fi relay (e.g. a Shelly device) that is wired to the gate motor and/or lighting circuit.
2. Components
| Component | Role | Technology |
|---|---|---|
index.php | Code entry form, writes code + timestamp to file | PHP |
2r.txt | Shared hand-off file between web page and poller | Plain text |
gate_poller.py | Watches the file, validates code, triggers relay | Python 3 |
| Wi-Fi relay (Shelly or similar) | Physically switches the gate motor / lights | Local HTTP API |
3. Web Page — index.php
Responsibilities:
- Prompt for a numeric code via a simple HTML form.
- Require HTTP Basic Authentication before the page loads, so it isn't usable by anyone who stumbles on the URL.
- Validate the submitted code against a format pattern (
^[0-9]{4,8}$) before writing it anywhere. - Rate-limit submissions (minimum 2 seconds between accepted writes) to slow down brute-force attempts.
- Write
code|timestampto2r.txt, so the poller can judge whether the code is still fresh. - Auto-refresh itself every 20 seconds so the form resets for the next person — no redirect to any other site or domain.
Key snippet — auth check
if (!isset($_SERVER['PHP_AUTH_USER']) ||
$_SERVER['PHP_AUTH_USER'] !== $AUTH_USER ||
$_SERVER['PHP_AUTH_PW'] !== $AUTH_PASS) {
header('WWW-Authenticate: Basic realm="Gate Control"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
Key snippet — validated write with rate limit
if ($now - $lastTime < $MinGapSec) {
// too soon, reject
} elseif (preg_match('/^[0-9]{4,8}$/', $code)) {
file_put_contents($File, $code . "|" . $now . "\n", LOCK_EX);
}
4. PC Poller — gate_poller.py
Runs continuously on a PC on the same network as the relay. Each cycle:
- Reads
2r.txt; skips if empty or already processed (tracked by timestamp). - Rejects codes older than
MAX_CODE_AGE_S(default 30s) — prevents an old code left in the file from triggering the gate later (replay protection). - Rejects codes not in the
VALID_CODESallow-list. - On a valid, fresh code: calls the relay's local HTTP API to turn it
on, waitsRELAY_PULSE_S, then turns itoff— a momentary pulse, matching how most gate motors expect a trigger signal. - Clears
2r.txtafter every attempt (valid or not) so nothing is processed twice. - Logs every event (received code, accepted/rejected, relay result) to
gate_poller.log.
Key snippet — relay pulse
def pulse_gate():
if call_relay(True):
time.sleep(RELAY_PULSE_S)
call_relay(False)
Configuration values to set before running
| Variable | Purpose |
|---|---|
CODE_FILE | Must match the path index.php writes to |
VALID_CODES | Set of codes allowed to trigger the gate |
RELAY_IP | Local IP address of the Wi-Fi relay |
MAX_CODE_AGE_S | Anti-replay window, in seconds |
5. Relay Integration (Shelly)
Shelly Gen1 devices expose a simple local HTTP API:
GET http://<relay_ip>/relay/0?turn=on
GET http://<relay_ip>/relay/0?turn=off
Gen2/Gen3 (Plus/Pro) devices use a JSON-RPC style endpoint instead:
GET http://<relay_ip>/rpc/Switch.Set?id=0&on=true
For other relay brands (Sonoff/eWeLink, TP-Link Kasa, Tasmota-flashed devices), the
call_relay() function in the poller is the only part that needs to change — swap in that
vendor's local API call.
6. Security Considerations & Hardening Checklist
| Risk | Mitigation in this design | Recommended if internet-facing |
|---|---|---|
| Unauthorized access to the form | HTTP Basic Auth | Use HTTPS so credentials aren't sent in the clear; consider a stronger auth method |
| Brute-forcing the code | 2-second rate limit per submission | Add a lockout after N failed attempts, and/or longer/alphanumeric codes |
| Replaying an old code | Timestamp + 30s freshness check in the poller | Consider one-time codes instead of a fixed allow-list |
| Plaintext code storage | N/A — short-lived hand-off file, cleared after each use | If retained long-term, don't store codes in plaintext |
| Network exposure | Assumes local/trusted network | Do not expose 2r.txt or the PHP page directly to the public internet without a firewall, VPN, or reverse proxy with auth |
index.php
(admin / change-me-now) must be changed, and the page should sit behind HTTPS
before it is reachable from outside your home/local network.
7. Setup Steps
- Place
index.phpon your web server (e.g. a Raspberry Pi running Apache/Nginx + PHP, or any LAN-accessible PHP host). - Edit
$AUTH_USER/$AUTH_PASSinindex.php. - Confirm
2r.txtis writable by the web server process, and reachable by the PC running the poller (same machine, or a shared/mounted path). - On the PC:
pip install requests. - Edit
CODE_FILE,VALID_CODES, andRELAY_IPat the top ofgate_poller.py. - Confirm the relay's Gen (1 vs 2/3) and adjust the API call in
call_relay()if needed. - Run
python3 gate_poller.pyand watchgate_poller.logwhile testing a submission from the web page. - Set the poller to run as a background service (e.g. a systemd service on Linux, or Task Scheduler on Windows) so it survives reboots.
8. Testing Checklist
- Submit a valid code → relay should pulse, log should show "Gate pulsed successfully."
- Submit an invalid-format code (letters, too short/long) → page shows "Invalid code format," nothing written.
- Submit two valid codes within 2 seconds → second one should be rejected by the rate limit.
- Manually write an old timestamp into
2r.txt→ poller should log "Ignored stale code." - Stop the poller, submit a code, restart the poller → confirm it does not fire on stale leftover data older than
MAX_CODE_AGE_S.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>HSSP Gate Control System — Technical Report</title> <style> :root { --bg: #ffffff; --fg: #1a1a1a; --muted: #555; --accent: #2a6f97; --code-bg: #f4f6f8; --border: #ddd; --warn-bg: #fff4e5; --warn-border: #e8a33d; } @media (prefers-color-scheme: dark) { :root { --bg:#14181c; --fg:#e8e8e8; --muted:#aaa; --accent:#6fb3d9; --code-bg:#1e242a; --border:#333; --warn-bg:#2a2210; --warn-border:#9c6b1f; } } body { background:var(--bg); color:var(--fg); font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif; line-height:1.6; max-width:860px; margin:0 auto; padding:40px 20px; } h1 { font-size:1.9em; border-bottom:3px solid var(--accent); padding-bottom:10px; } h2 { color:var(--accent); margin-top:2em; border-bottom:1px solid var(--border); padding-bottom:6px; } h3 { margin-top:1.4em; } code, pre { background:var(--code-bg); border:1px solid var(--border); border-radius:4px; } code { padding:2px 6px; font-size:0.92em; } pre { padding:14px; overflow-x:auto; } pre code { border:none; padding:0; } table { border-collapse:collapse; width:100%; margin:1em 0; } th, td { border:1px solid var(--border); padding:8px 10px; text-align:left; font-size:0.95em; } th { background:var(--code-bg); } .warn { background:var(--warn-bg); border:1px solid var(--warn-border); border-radius:6px; padding:12px 16px; margin:1em 0; } .muted { color:var(--muted); font-size:0.92em; } .flow { font-family: monospace; background:var(--code-bg); border:1px solid var(--border); border-radius:6px; padding:16px; white-space:pre; overflow-x:auto; } footer { margin-top:3em; padding-top:1em; border-top:1px solid var(--border); color:var(--muted); font-size:0.85em; } </style> </head> <body> <h1>HSSP Gate Control System — Technical Report</h1> <p class="muted">Web code-entry page + local Python poller + Wi-Fi relay. Prepared as system documentation.</p> <h2>1. Overview</h2> <p>This system lets an authorized user type a numeric code into a web page. The code is written to a local file on the web server. A Python script running on a PC on the same network polls that file, validates the code, and — if valid and fresh — pulses a Wi-Fi relay (e.g. a Shelly device) that is wired to the gate motor and/or lighting circuit.</p> <div class="flow"> [ Browser ] --POST code--> [ index.php on web server ] --writes--> [ 2r.txt ] | (polled every 1s) v [ gate_poller.py on PC ] | HTTP call if code valid v [ Wi-Fi Relay ] --> Gate / Lights </div> <div class="warn"> <strong>Scope note:</strong> This design assumes the web server, the file it writes to, and the PC running the poller are all on a trusted local network, or the file is synced over a secure private channel. It is not intended to be exposed directly to the public internet without the hardening steps in Section 5. </div> <h2>2. Components</h2> <table> <tr><th>Component</th><th>Role</th><th>Technology</th></tr> <tr><td><code>index.php</code></td><td>Code entry form, writes code + timestamp to file</td><td>PHP</td></tr> <tr><td><code>2r.txt</code></td><td>Shared hand-off file between web page and poller</td><td>Plain text</td></tr> <tr><td><code>gate_poller.py</code></td><td>Watches the file, validates code, triggers relay</td><td>Python 3</td></tr> <tr><td>Wi-Fi relay (Shelly or similar)</td><td>Physically switches the gate motor / lights</td><td>Local HTTP API</td></tr> </table> <h2>3. Web Page — <code>index.php</code></h2> <p>Responsibilities:</p> <ul> <li>Prompt for a numeric code via a simple HTML form.</li> <li>Require HTTP Basic Authentication before the page loads, so it isn't usable by anyone who stumbles on the URL.</li> <li>Validate the submitted code against a format pattern (<code>^[0-9]{4,8}$</code>) before writing it anywhere.</li> <li>Rate-limit submissions (minimum 2 seconds between accepted writes) to slow down brute-force attempts.</li> <li>Write <code>code|timestamp</code> to <code>2r.txt</code>, so the poller can judge whether the code is still fresh.</li> <li>Auto-refresh itself every 20 seconds so the form resets for the next person — no redirect to any other site or domain.</li> </ul> <h3>Key snippet — auth check</h3> <pre><code>if (!isset($_SERVER['PHP_AUTH_USER']) || $_SERVER['PHP_AUTH_USER'] !== $AUTH_USER || $_SERVER['PHP_AUTH_PW'] !== $AUTH_PASS) { header('WWW-Authenticate: Basic realm="Gate Control"'); header('HTTP/1.0 401 Unauthorized'); exit; }</code></pre> <h3>Key snippet — validated write with rate limit</h3> <pre><code>if ($now - $lastTime < $MinGapSec) { // too soon, reject } elseif (preg_match('/^[0-9]{4,8}$/', $code)) { file_put_contents($File, $code . "|" . $now . "\n", LOCK_EX); }</code></pre> <h2>4. PC Poller — <code>gate_poller.py</code></h2> <p>Runs continuously on a PC on the same network as the relay. Each cycle:</p> <ol> <li>Reads <code>2r.txt</code>; skips if empty or already processed (tracked by timestamp).</li> <li>Rejects codes older than <code>MAX_CODE_AGE_S</code> (default 30s) — prevents an old code left in the file from triggering the gate later (replay protection).</li> <li>Rejects codes not in the <code>VALID_CODES</code> allow-list.</li> <li>On a valid, fresh code: calls the relay's local HTTP API to turn it <code>on</code>, waits <code>RELAY_PULSE_S</code>, then turns it <code>off</code> — a momentary pulse, matching how most gate motors expect a trigger signal.</li> <li>Clears <code>2r.txt</code> after every attempt (valid or not) so nothing is processed twice.</li> <li>Logs every event (received code, accepted/rejected, relay result) to <code>gate_poller.log</code>.</li> </ol> <h3>Key snippet — relay pulse</h3> <pre><code>def pulse_gate(): if call_relay(True): time.sleep(RELAY_PULSE_S) call_relay(False)</code></pre> <h3>Configuration values to set before running</h3> <table> <tr><th>Variable</th><th>Purpose</th></tr> <tr><td><code>CODE_FILE</code></td><td>Must match the path <code>index.php</code> writes to</td></tr> <tr><td><code>VALID_CODES</code></td><td>Set of codes allowed to trigger the gate</td></tr> <tr><td><code>RELAY_IP</code></td><td>Local IP address of the Wi-Fi relay</td></tr> <tr><td><code>MAX_CODE_AGE_S</code></td><td>Anti-replay window, in seconds</td></tr> </table> <h2>5. Relay Integration (Shelly)</h2> <p>Shelly Gen1 devices expose a simple local HTTP API:</p> <pre><code>GET http://<relay_ip>/relay/0?turn=on GET http://<relay_ip>/relay/0?turn=off</code></pre> <p>Gen2/Gen3 (Plus/Pro) devices use a JSON-RPC style endpoint instead:</p> <pre><code>GET http://<relay_ip>/rpc/Switch.Set?id=0&on=true</code></pre> <p class="muted">For other relay brands (Sonoff/eWeLink, TP-Link Kasa, Tasmota-flashed devices), the <code>call_relay()</code> function in the poller is the only part that needs to change — swap in that vendor's local API call.</p> <h2>6. Security Considerations & Hardening Checklist</h2> <table> <tr><th>Risk</th><th>Mitigation in this design</th><th>Recommended if internet-facing</th></tr> <tr><td>Unauthorized access to the form</td><td>HTTP Basic Auth</td><td>Use HTTPS so credentials aren't sent in the clear; consider a stronger auth method</td></tr> <tr><td>Brute-forcing the code</td><td>2-second rate limit per submission</td><td>Add a lockout after N failed attempts, and/or longer/alphanumeric codes</td></tr> <tr><td>Replaying an old code</td><td>Timestamp + 30s freshness check in the poller</td><td>Consider one-time codes instead of a fixed allow-list</td></tr> <tr><td>Plaintext code storage</td><td>N/A — short-lived hand-off file, cleared after each use</td><td>If retained long-term, don't store codes in plaintext</td></tr> <tr><td>Network exposure</td><td>Assumes local/trusted network</td><td>Do not expose <code>2r.txt</code> or the PHP page directly to the public internet without a firewall, VPN, or reverse proxy with auth</td></tr> </table> <div class="warn"> <strong>Do not deploy as-is to a public server.</strong> The default credentials in <code>index.php</code> (<code>admin</code> / <code>change-me-now</code>) must be changed, and the page should sit behind HTTPS before it is reachable from outside your home/local network. </div> <h2>7. Setup Steps</h2> <ol> <li>Place <code>index.php</code> on your web server (e.g. a Raspberry Pi running Apache/Nginx + PHP, or any LAN-accessible PHP host).</li> <li>Edit <code>$AUTH_USER</code> / <code>$AUTH_PASS</code> in <code>index.php</code>.</li> <li>Confirm <code>2r.txt</code> is writable by the web server process, and reachable by the PC running the poller (same machine, or a shared/mounted path).</li> <li>On the PC: <code>pip install requests</code>.</li> <li>Edit <code>CODE_FILE</code>, <code>VALID_CODES</code>, and <code>RELAY_IP</code> at the top of <code>gate_poller.py</code>.</li> <li>Confirm the relay's Gen (1 vs 2/3) and adjust the API call in <code>call_relay()</code> if needed.</li> <li>Run <code>python3 gate_poller.py</code> and watch <code>gate_poller.log</code> while testing a submission from the web page.</li> <li>Set the poller to run as a background service (e.g. a systemd service on Linux, or Task Scheduler on Windows) so it survives reboots.</li> </ol> <h2>8. Testing Checklist</h2> <ul> <li>Submit a valid code → relay should pulse, log should show "Gate pulsed successfully."</li> <li>Submit an invalid-format code (letters, too short/long) → page shows "Invalid code format," nothing written.</li> <li>Submit two valid codes within 2 seconds → second one should be rejected by the rate limit.</li> <li>Manually write an old timestamp into <code>2r.txt</code> → poller should log "Ignored stale code."</li> <li>Stop the poller, submit a code, restart the poller → confirm it does <em>not</em> fire on stale leftover data older than <code>MAX_CODE_AGE_S</code>.</li> </ul> <footer> System documentation generated for the HSSP gate/light control project. Review the security checklist in Section 6 before exposing this system beyond a trusted local network. </footer> </body> </html>