Embedded + Infra

Use the GPIOs Like an Engineer
without giving up your server role

You are not wasting a Pi4 by running services on it. This track turns your existing host at 10.10.10.250 into an embedded control node that still runs DNS, SQL, NGINX, and RADIUS. The advanced path includes Rust on real GPIO/I2C stacks and PS4 controller-driven actuation loops with safety gates.

PlatformRaspberry Pi4
PatternServer + Embedded
GPIO Mode3.3V logic
DB SinkPostgreSQL
Edge APINGINX + Python
Embedded CoreRust + rppal
HID ControlPS4 (DualShock 4)
Design Principle

Control loops stay local on the Pi for reliability. Network services expose state and history, but actuator safety decisions must not depend on internet reachability.

Lesson 0

Create Embedded Workspace, Files, and First Hardware Check

Objective: start from scratch with a clean embedded workspace and verify your first GPIO script can be created, run, and validated.

Learning Focus: This lesson removes setup assumptions by establishing exact directory structure, file paths, execution commands, and basic validation steps before any advanced control logic.

Embedded progress depends on disciplined setup. If file layout and run paths are inconsistent, later failures are difficult to isolate because wiring issues, package issues, and script issues all look similar. This lesson gives you a deterministic baseline so each subsequent lesson adds one controlled variable.

Create directories for code and services

Use a stable path so scripts and systemd units always reference the same location.

bash
filesystem bootstrap
sudo mkdir -p /opt/embedded/{bin,logs,data}
sudo chown -R $USER:$USER /opt/embedded
cd /opt/embedded
pwd

Create your first script file

This file proves your write path and Python execution path before adding sensors or service wrappers.

python
/opt/embedded/bin/hello_gpio.py
from gpiozero import LED
from time import sleep

led = LED(17)
for _ in range(3):
    led.on()
    sleep(0.2)
    led.off()
    sleep(0.2)

print("lesson0_gpio_ok")

Run and validate

Use these checks together so you verify syntax, runtime, and basic hardware behavior in one pass.

bash
execution checks
python3 -m py_compile /opt/embedded/bin/hello_gpio.py
python3 /opt/embedded/bin/hello_gpio.py
ls -la /opt/embedded/bin

Create your first test-style guard

This gives a lightweight test pattern you can repeat for utility modules as the codebase grows.

python
/opt/embedded/bin/math_guard.py
def scale_temp(raw: int) -> float:
    return raw / 100.0


if __name__ == "__main__":
    assert scale_temp(2534) == 25.34
    print("lesson0_test_ok")
bash
test run
python3 /opt/embedded/bin/math_guard.py

Checkpoint: both scripts run, syntax checks pass, and you see lesson0_gpio_ok plus lesson0_test_ok outputs.

Lesson 1

GPIO Electrical Safety and Pin Strategy

Objective: protect the Pi and your peripherals before any control loop is deployed.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Teaching Lens

This lesson teaches the non-negotiables that prevent board damage and unstable behavior.

This lesson also focuses on operational reasoning: what healthy behavior looks like, what failure signals look like, and how this step protects the reliability of the lessons that come next.

The first quality gate is electrical correctness. Every signal that enters a Pi GPIO must remain at 3.3V logic levels, and this is not negotiable. If you violate this once, you can permanently damage the SoC input path.

The second quality gate is isolation and return-path clarity. Relay and motor power rails must be isolated from GPIO drive lines through proper interface hardware, and common ground must be deliberate so digital state transitions are stable rather than noisy.

Pin map you will use

This baseline map keeps functions explicit so service scripts and wiring diagrams always match.

wiring-plan
GPIO17 (pin 11): status LED output
    GPIO27 (pin 13): button input (pull-up)
    GPIO22 (pin 15): relay output (through transistor/opto board)
    GPIO2/GPIO3 (pins 3/5): I2C sensor bus
    Power rule: 5V rail powers modules that require 5V, but GPIO logic never sees 5V directly.
Safety Rule

Never drive relay coils directly from a GPIO pin. Use a proper relay module, transistor stage, or opto-isolated board with flyback protection.

Checkpoint: your wiring diagram is finalized and reviewed before power-on.

Lesson 2

Install Embedded Toolchain on Your Existing Server

Objective: add GPIO, I2C, and runtime libraries without disrupting DNS and database services.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Teaching Lens

This lesson teaches staged package setup and interface enablement for stable mixed workloads.

This lesson also focuses on operational reasoning: what healthy behavior looks like, what failure signals look like, and how this step protects the reliability of the lessons that come next.

Run

This block installs command-line GPIO tools plus Python libraries used in the remaining lessons.

bash
sudo apt-get update
sudo apt-get install -y python3-pip python3-venv python3-gpiozero python3-rpi.gpio i2c-tools
python3 -m pip install --break-system-packages lgpio adafruit-circuitpython-bme280 psycopg[binary] flask
sudo raspi-config nonint do_i2c 0
sudo usermod -aG gpio,i2c $USER
sudo reboot

You verify I2C is enabled and your user has gpio and i2c group membership after reboot.

Checkpoint: i2cdetect -y 1 runs and GPIO libraries import without error.

Lesson 3

Build a Deterministic LED Heartbeat Task

Objective: run a low-overhead heartbeat process proving your GPIO output timing path is stable.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Create heartbeat script

The script toggles GPIO17 at known intervals so you can validate scheduler load effects while services run.

python
/opt/embedded/heartbeat.py
from gpiozero import LED
from signal import pause

status_led = LED(17)
status_led.blink(on_time=0.2, off_time=0.8, background=True)
pause()

Create systemd unit

systemd keeps the heartbeat supervised and restartable across reboot events.

ini + bash
/etc/systemd/system/pi-heartbeat.service
sudo mkdir -p /opt/embedded
sudo tee /opt/embedded/heartbeat.py >/dev/null <<'EOF'
from gpiozero import LED
from signal import pause

status_led = LED(17)
status_led.blink(on_time=0.2, off_time=0.8, background=True)
pause()
EOF

sudo tee /etc/systemd/system/pi-heartbeat.service >/dev/null <<'EOF'
[Unit]
Description=Pi GPIO heartbeat
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 /opt/embedded/heartbeat.py
Restart=always
User=pi
Group=pi

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now pi-heartbeat
sudo systemctl status pi-heartbeat --no-pager

Checkpoint: LED blinks in a stable pattern and service auto-starts after reboot.

Lesson 4

Read a Button Input with Debounce

Objective: collect clean digital input events suitable for control logic and audit logs.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Run

This script uses pull-up logic and software debounce so one press produces one event.

python
/opt/embedded/button_reader.py
from gpiozero import Button
from signal import pause
from datetime import datetime

button = Button(27, pull_up=True, bounce_time=0.05)

def on_press():
    print(f"{datetime.now().isoformat()} button_pressed")

button.when_pressed = on_press
pause()

Checkpoint: each physical press logs exactly one event line.

Lesson 5

Attach an I2C Sensor and Read Environmental Data

Objective: ingest real-world analog context (temperature, humidity, pressure) into your platform.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Scan bus and read BME280

Use this sequence to verify physical sensor presence first, then produce parsed values for storage.

bash + python
i2cdetect -y 1

cat <<'EOF' > /opt/embedded/read_bme280.py
import board
import adafruit_bme280

i2c = board.I2C()
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
print(f"temp_c={sensor.temperature:.2f}")
print(f"humidity={sensor.humidity:.2f}")
print(f"pressure_hpa={sensor.pressure:.2f}")
EOF

python3 /opt/embedded/read_bme280.py

Checkpoint: valid sensor values print consistently without bus errors.

Lesson 6

Control a Relay with Fail-Safe Defaults

Objective: drive a real actuator while ensuring safe behavior during reboot and script failure.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Create relay controller

The controller starts in safe-off state and supports explicit on/off command arguments only.

python
/opt/embedded/relay_ctl.py
import sys
from gpiozero import OutputDevice

relay = OutputDevice(22, active_high=True, initial_value=False)

if len(sys.argv) != 2 or sys.argv[1] not in {"on", "off"}:
    raise SystemExit("Usage: relay_ctl.py [on|off]")

if sys.argv[1] == "on":
    relay.on()
else:
    relay.off()
Actuator Safety

For pumps, locks, or heaters, add a hardware interlock and watchdog timeout that forces OFF if process heartbeat is lost.

Checkpoint: relay state changes only on explicit command and defaults to OFF at startup.

Lesson 7

Map Service Health to a Physical LED

Objective: expose infrastructure status in hardware so you can diagnose at a glance without shell access.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Create health mapper

This script checks core services and changes blink rate by severity.

python
/opt/embedded/health_led.py
import subprocess
import time
from gpiozero import LED

led = LED(17)
services = ["bind9", "postgresql", "nginx", "freeradius"]

def healthy(name):
    return subprocess.run(["systemctl", "is-active", "--quiet", name]).returncode == 0

while True:
    failed = sum(1 for s in services if not healthy(s))
    if failed == 0:
        led.blink(on_time=0.06, off_time=1.2, n=1, background=False)
    elif failed == 1:
        led.blink(on_time=0.15, off_time=0.2, n=2, background=False)
    else:
        led.blink(on_time=0.1, off_time=0.1, n=5, background=False)
    time.sleep(1.0)

Checkpoint: LED pattern changes when you intentionally stop one service and returns to normal after recovery.

Lesson 8

Write GPIO and Sensor Events into PostgreSQL

Objective: persist embedded events so control behavior can be analyzed, replayed, and audited.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Create schema and logger

This model stores event type, source pin, and payload JSON for flexible queries later.

sql + python
psql -U dns_user -d dns_analytics -h localhost <<'EOF'
CREATE TABLE IF NOT EXISTS embedded_events (
  id BIGSERIAL PRIMARY KEY,
  ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  event_type TEXT NOT NULL,
  source_pin INT,
  payload JSONB NOT NULL
);
EOF

cat <<'EOF' > /opt/embedded/log_event.py
import json
import psycopg

conn = psycopg.connect("host=localhost dbname=dns_analytics user=dns_user password=ChangeThisPassword!")
with conn, conn.cursor() as cur:
    cur.execute(
        "INSERT INTO embedded_events(event_type, source_pin, payload) VALUES (%s, %s, %s)",
        ("button_pressed", 27, json.dumps({"state": "pressed"}))
    )
print("event_saved")
EOF

python3 /opt/embedded/log_event.py

Checkpoint: query returns inserted rows from embedded_events.

Lesson 9

Expose Safe Device Control Behind NGINX

Objective: publish a minimal local API for controlled actuation while preserving strict command boundaries.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Create local API

The API only accepts known actions and calls your relay controller script as a constrained backend operation.

python
/opt/embedded/device_api.py
from flask import Flask, request, jsonify
import subprocess

app = Flask(__name__)

@app.post('/relay')
def relay():
    action = request.json.get('action', '')
    if action not in {'on', 'off'}:
        return jsonify({'error': 'invalid action'}), 400
    subprocess.run(['/usr/bin/python3', '/opt/embedded/relay_ctl.py', action], check=True)
    return jsonify({'ok': True, 'action': action})

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=9100)

Proxy with NGINX

This route keeps API private to your local domain and centralizes TLS/access policy in NGINX.

nginx.conf
inside home-core server block
location /device/ {
    proxy_pass http://127.0.0.1:9100/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Checkpoint: local POST requests to https://home.codeandcore.home/device/relay switch the relay only with valid actions.

Lesson 10

Production Hardening for a Real Embedded Node

Objective: make your Pi robust against power events, software faults, and accidental unsafe commands.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Teaching Lens

This lesson teaches reliability layers expected in real embedded deployments.

This lesson also focuses on operational reasoning: what healthy behavior looks like, what failure signals look like, and how this step protects the reliability of the lessons that come next.

Reliability in embedded systems is the outcome of layered controls, not one feature. You need restart policy coverage for process failure, watchdog behavior for lockups, and hardware-default safe states so outputs do not drift during boot transitions.

This lesson also teaches operational memory. If control events are not logged and backed up, failure analysis becomes guesswork. Auditability is part of control quality, not a separate concern.

Runbook implementation

Run these actions as a final commissioning sequence before connecting high-impact loads. The order matters because each step reduces a different class of risk.

runbook
Enable the hardware watchdog and validate forced process restart behavior.
Apply systemd Restart=always and RestartSec to all embedded services.
Use pull-up or pull-down resistors so floating inputs cannot trigger control logic.
Confirm relay outputs default OFF during boot and after crash recovery.
Add a physical emergency stop path for dangerous actuators.
Back up /opt/embedded and PostgreSQL embedded_events daily.
Document pin map and cabinet wiring inside your repository.
Completion Standard

You are complete when the Pi provides stable infrastructure services and deterministic GPIO behavior with auditable event history and safe fallback states.

Lesson 11

Install Rust Embedded Toolchain on Pi4

Objective: move your hardware control path from scripting to a compiled Rust runtime suitable for long-lived services.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Teaching Lens

This lesson teaches deterministic deployment: reproducible builds, pinned dependencies, and a dedicated service binary.

This lesson also focuses on operational reasoning: what healthy behavior looks like, what failure signals look like, and how this step protects the reliability of the lessons that come next.

Run

This sequence installs Rust, creates a new project, and brings in the crates needed for GPIO, I2C, controller input, JSON logging, and PostgreSQL writes.

bash
sudo apt-get update
sudo apt-get install -y build-essential pkg-config libudev-dev libdbus-1-dev bluetooth bluez bluez-tools
curl https://sh.rustup.rs -sSf | sh -s -- -y
source $HOME/.cargo/env
cargo new --bin /opt/embedded/rust-edge
cd /opt/embedded/rust-edge
cargo add rppal gilrs serde serde_json anyhow tokio tokio-postgres chrono --features tokio/full

You verify cargo build succeeds and all crate dependencies resolve on the Pi.

Checkpoint: Rust project compiles locally and is ready for hardware interfaces.

Lesson 12

Implement Real I2C Sensor Read Path in Rust

Objective: collect sensor data over I2C in Rust and push typed records to PostgreSQL.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Replace main.rs with I2C + SQL sample

This code reads a common BME280-compatible register block pattern over I2C, builds structured telemetry, and writes a JSON payload to your existing embedded_events table.

rust
/opt/embedded/rust-edge/src/main.rs
use anyhow::Result;
use chrono::Utc;
use rppal::i2c::I2c;
use serde_json::json;
use tokio_postgres::NoTls;

#[tokio::main]
async fn main() -> Result<()> {
    let mut i2c = I2c::new()?;
    i2c.set_slave_address(0x76)?;

    let mut buf = [0u8; 8];
    i2c.block_read(0xF7, &mut buf)?;

    let raw_pressure = ((buf[0] as u32) << 12) | ((buf[1] as u32) << 4) | ((buf[2] as u32) >> 4);
    let raw_temp = ((buf[3] as u32) << 12) | ((buf[4] as u32) << 4) | ((buf[5] as u32) >> 4);
    let raw_humidity = ((buf[6] as u16) << 8) | (buf[7] as u16);

    let payload = json!({
        "ts": Utc::now().to_rfc3339(),
        "raw_temp": raw_temp,
        "raw_pressure": raw_pressure,
        "raw_humidity": raw_humidity,
        "sensor": "bme280",
        "bus": "i2c-1",
        "address": "0x76"
    });

    let (client, conn) = tokio_postgres::connect(
        "host=localhost user=dns_user password=ChangeThisPassword! dbname=dns_analytics",
        NoTls,
    )
    .await?;

    tokio::spawn(async move {
        if let Err(e) = conn.await {
            eprintln!("postgres connection error: {e}");
        }
    });

    client
        .execute(
            "INSERT INTO embedded_events (event_type, source_pin, payload) VALUES ($1, $2, $3::jsonb)",
            &[&"i2c_sample", &2i32, &payload.to_string()],
        )
        .await?;

    println!("i2c_sample_saved");
    Ok(())
}

Run and verify

Build once, execute, then confirm a new event row exists for the I2C sample.

bash
cd /opt/embedded/rust-edge
cargo run --release
psql -U dns_user -d dns_analytics -h localhost -c "SELECT ts, event_type, payload->>'sensor' AS sensor FROM embedded_events ORDER BY id DESC LIMIT 5;"

Checkpoint: Rust process reads I2C and persists telemetry without Python in the loop.

Lesson 13

Pair PS4 Controller and Map It to Safe GPIO Actions

Objective: use a DualShock 4 as a local control console with dead-man semantics and explicit safety limits.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Teaching Lens

This lesson teaches human-in-the-loop control design instead of raw remote toggling.

This lesson also focuses on operational reasoning: what healthy behavior looks like, what failure signals look like, and how this step protects the reliability of the lessons that come next.

The PS4 controller is treated as a supervised control surface, not an unrestricted remote. Pairing stability and input integrity come first because control semantics are meaningless if events are dropped or duplicated during reconnect conditions.

The key safety model is dead-man gating. A dedicated hold action enables actuation, and output must return to OFF on disconnect, idle timeout, or gate release. That behavior prevents stale input state from energizing hardware unexpectedly.

Pair controller on Pi

Pair via Bluetooth once, then keep trust enabled for reconnect after reboot.

bash
bluetoothctl interactive
sudo systemctl enable --now bluetooth
bluetoothctl

power on
agent on
default-agent
scan on
pair <DS4_MAC>
trust <DS4_MAC>
connect <DS4_MAC>
scan off
quit

Use Rust gamepad loop with dead-man gate

This control loop only allows relay actuation while L2 is held (dead-man). Releasing L2 forces immediate OFF even if other button events occur.

rust
replace main.rs for controller mode
use anyhow::Result;
use gilrs::{Axis, Button, EventType, Gilrs};
use rppal::gpio::Gpio;
use std::time::{Duration, Instant};

fn main() -> Result<()> {
    let mut gilrs = Gilrs::new()?;
    let gpio = Gpio::new()?;
    let mut relay = gpio.get(22)?.into_output_low();

    let mut deadman = false;
    let mut last_event = Instant::now();

    loop {
        while let Some(ev) = gilrs.next_event() {
            last_event = Instant::now();
            match ev.event {
                EventType::ButtonPressed(Button::LeftTrigger2, _) => deadman = true,
                EventType::ButtonReleased(Button::LeftTrigger2, _) => {
                    deadman = false;
                    relay.set_low();
                }
                EventType::ButtonPressed(Button::South, _) if deadman => relay.set_high(),
                EventType::ButtonPressed(Button::East, _) => relay.set_low(),
                EventType::AxisChanged(Axis::LeftStickY, v, _) if deadman => {
                    if v < -0.8 { relay.set_high(); }
                    if v > 0.8 { relay.set_low(); }
                }
                _ => {}
            }
        }

        if last_event.elapsed() > Duration::from_secs(2) {
            relay.set_low();
        }

        std::thread::sleep(Duration::from_millis(10));
    }
}
No Blind Automation

Do not connect this profile directly to hazardous loads until you add hardware cutoffs, watchdog relays, and a manual emergency stop.

Checkpoint: PS4 controller can command relay ON only while dead-man button is held and always returns OFF on disconnect/idle timeout.

Lesson 14

Run Rust Control Stack as a Production Service

Objective: ship your Rust control path as a managed daemon with logs, restart policy, and service isolation.

Learning Focus: This lesson builds practical engineering judgment, not just task completion. As you run each step, connect the action to runtime behavior, failure signals, and design trade-offs so you can adapt the pattern in real systems.

Build, install, and service-wrap

This packaging flow creates a dedicated binary target and runs it under systemd with restart semantics and environment isolation.

bash + systemd
rust controller daemon
cd /opt/embedded/rust-edge
cargo build --release
sudo install -m 0755 target/release/rust-edge /usr/local/bin/rust-edge

sudo tee /etc/systemd/system/rust-edge.service >/dev/null <<'EOF'
[Unit]
Description=Rust Edge Control Loop (GPIO/I2C/PS4)
After=network-online.target bluetooth.service
Wants=network-online.target bluetooth.service

[Service]
ExecStart=/usr/local/bin/rust-edge
Restart=always
RestartSec=2
User=pi
Group=pi
WorkingDirectory=/opt/embedded/rust-edge
NoNewPrivileges=true
Environment=RUST_LOG=info

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now rust-edge
sudo systemctl status rust-edge --no-pager
sudo journalctl -u rust-edge -n 80 --no-pager

This lesson teaches how to deploy Rust hardware control as a real service in your infrastructure stack.

You verify controller input, GPIO output, and auto-restart behavior all survive reboot and service crashes.

Checkpoint: your Pi now runs a production Rust control daemon with I2C ingestion and PS4 input control semantics.