Skip to content

Daemon Setup On PI

Dilf edited this page Feb 17, 2026 · 4 revisions

The Pi daemon (enclosure_daemon.py) is a Flask HTTP server that runs permanently in the background on the Raspberry Pi. It owns the USB serial connection to the ESP32 exclusively, and exposes a local HTTP API on port 8070 that the OctoPrint plugin talks to.

Prerequisites

  • Raspberry Pi running OctoPi with SSH enabled
  • ESP32 flashed with the HotBox firmware and connected via USB
  • The repo cloned on your development machine

How It Works

The daemon is the middleman between OctoPrint and the ESP32. Nothing else talks to the serial port directly.

OctoPrint Plugin
   │  HTTP  →  127.0.0.1:8070
   ▼
Pi Daemon  (enclosure_daemon.py)
   │  USB Serial  →  /dev/ttyUSB0  @  115200 baud
   ▼
ESP32

Startup sequence

  1. connect_serial() opens /dev/ttyUSB0 and sends PING commands until the ESP32 responds. Retries indefinitely — this handles the Pi booting faster than the ESP32 initialises.
  2. A background thread starts running poller(), which sends GET STATUS to the ESP32 every 2 seconds and caches the result.
  3. The Flask HTTP server starts on 127.0.0.1:8070 and begins accepting requests.

Why a background poller?

Rather than hitting the serial port on every HTTP request (slow, and risks contention), the poller keeps a fresh _last_status snapshot in memory. When the OctoPrint plugin calls GET /status, the cached value is returned instantly — no serial I/O required.

Serial locking

A threading.Lock() wraps every serial read/write. This prevents the background poller and an incoming HTTP request from both trying to write to the serial port simultaneously, which would corrupt the data stream.

Reconnection handling

If the serial port drops (ESP32 reset, USB cable glitch), the daemon:

  1. Detects the failure on the next write or read attempt
  2. Forcefully closes and discards the broken port handle
  3. Backs off for 3 seconds before retrying
  4. Reopens /dev/ttyUSB0 automatically on the next command

No reboot required — the daemon is self-healing.


Installation

Step 1 — SSH into the Pi

Run this on your computer:

ssh pi@octopi.local

Enter the Pi's password when prompted (default: raspberry unless changed).


Step 2 — Create the daemon directory and virtual environment

On the Pi, create a dedicated folder and an isolated Python environment for the daemon:

mkdir -p ~/enclosure_daemon
python3 -m venv ~/enclosure_daemon/venv

Why a virtual environment? OctoPi manages its own Python environment separately. Installing packages into the system Python risks breaking OctoPrint. A venv keeps the daemon's dependencies (flask, pyserial) fully isolated.


Step 3 — Install dependencies

Activate the venv and install the required packages:

source ~/enclosure_daemon/venv/bin/activate
pip install --upgrade pip
pip install flask pyserial
deactivate

Verify the packages installed correctly:

~/enclosure_daemon/venv/bin/pip list | grep -E "Flask|pyserial"

Expected output:

Flask       3.x.x
pyserial    3.x.x

Step 4 — Copy the daemon script to the Pi

Option A — Recommended: copy from your computer via scp

Run this on your computer (not inside SSH):

scp daemon/enclosure_daemon.py pi@octopi.local:/home/pi/enclosure_daemon/

Option B — Edit directly on the Pi

If you don't have the repo on your computer, SSH in and create the file manually:

nano ~/enclosure_daemon/enclosure_daemon.py

In nano:

  • Paste the daemon code
  • Ctrl+O then Enter to save
  • Ctrl+X to exit

Step 5 — Verify the file is in place

ls -lh ~/enclosure_daemon/

Expected output:

drwxr-xr-x  enclosure_daemon/
├── enclosure_daemon.py
└── venv/

Step 6 — Test-run the daemon manually

Before setting it up as a service, do a quick sanity check by running it directly. Make sure the ESP32 is plugged into the Pi via USB first.

source ~/enclosure_daemon/venv/bin/activate
python3 ~/enclosure_daemon/enclosure_daemon.py

You should see:

[daemon] Waiting for ESP32...
[serial] Opened /dev/ttyUSB0
[daemon] Connected to /dev/ttyUSB0 at 115200
[daemon] ESP responded: @1 OK PONG
Enclosure daemon running on port 8070...
 * Running on http://127.0.0.1:8070
[poll] reply: @2 OK TEMP 24.60 RPM 0 HEATER 0.0 ...

With the daemon still running, open a second SSH session and test the HTTP API:

curl -s http://127.0.0.1:8070/status ; echo

Expected response:

{
  "ok": true,
  "parsed": {
    "TEMP": "24.60",
    "RPM": "0",
    "HEATER": "0.0",
    "EXHAUST": "0.0",
    "SETPOINT": "25.0",
    "MODE": "OFF",
    "CONTROL": "0",
    "SAFETY": "0"
  },
  "raw": "@2 OK TEMP 24.60 RPM 0 ...",
  "ts": 1771245866.52,
  "error": ""
}

If this works, press Ctrl+C to stop the manual run and proceed to the systemd setup below.

If /dev/ttyUSB0 not found:

ls /dev/ttyUSB*

If nothing shows, the ESP32 isn't being detected. Try a different USB cable or port, or check that the CP210x/CH340 driver is available on the Pi.


Systemd Service Setup

Running the daemon manually only lasts until you close the SSH session. The systemd service makes it run permanently in the background and restart automatically if it ever crashes.


Step 1 — Copy the service file to the Pi

From your computer (not inside SSH):

scp systemd/enclosure-daemon.service pi@octopi.local:/tmp/

Step 2 — Move the service file into systemd

SSH into the Pi if you aren't already connected:

ssh pi@octopi.local

Move the file into the systemd directory (requires sudo):

sudo mv /tmp/enclosure-daemon.service /etc/systemd/system/

Verify it landed correctly:

cat /etc/systemd/system/enclosure-daemon.service

You should see the full service file contents with the [Unit], [Service], and [Install] sections.


Step 3 — Reload systemd

After adding any new service file, systemd must be told to re-read its configuration:

sudo systemctl daemon-reload

Always run this after creating or editing a .service file — without it, systemd uses the old (or non-existent) version.


Step 4 — Enable the service

Enable the daemon so it starts automatically on every boot:

sudo systemctl enable enclosure-daemon

Expected output:

Created symlink /etc/systemd/system/multi-user.target.wants/enclosure-daemon.service
  → /etc/systemd/system/enclosure-daemon.service.

Step 5 — Start the daemon now

Enable only registers it for future boots. Start it immediately with:

sudo systemctl start enclosure-daemon

Step 6 — Verify the service is running

sudo systemctl status enclosure-daemon --no-pager

You should see:

● enclosure-daemon.service - ESP32 Enclosure Daemon
     Loaded: loaded (/etc/systemd/system/enclosure-daemon.service; enabled)
     Active: active (running) since ...
   Main PID: XXXX (python)

The key lines to check:

  • Active: active (running) — daemon is up ✅
  • enabled in the Loaded line — will start on next boot ✅

If you see failed or inactive, jump to Troubleshooting below.


Step 7 — Confirm the daemon is polling the ESP32

Watch the live log output for a few seconds:

sudo journalctl -u enclosure-daemon -f

You should see [poll] reply: lines arriving every 2 seconds:

[daemon] Waiting for ESP32...
[serial] Opened /dev/ttyUSB0
[daemon] ESP responded: @1 OK PONG
Enclosure daemon running on port 8070...
[poll] reply: @2 OK TEMP 24.60 RPM 0 HEATER 0.0 EXHAUST 0.0 ...
[poll] reply: @3 OK TEMP 24.61 RPM 0 HEATER 0.0 EXHAUST 0.0 ...

Press Ctrl+C to exit the log view.


Step 8 — Final API check

Confirm the HTTP API is working end-to-end:

# Status (uses cached poller data — no serial I/O)
curl -s http://127.0.0.1:8070/status ; echo

Ping (hits the ESP32 live)

curl -s http://127.0.0.1:8070/ping ; echo

Change mode to MANUAL

curl -s -X POST http://127.0.0.1:8070/mode
-H "Content-Type: application/json"
-d '{"mode":"MANUAL"}' ; echo

Set heater to 30%

curl -s -X POST http://127.0.0.1:8070/heater
-H "Content-Type: application/json"
-d '{"value":30}' ; echo

Set exhaust to 50%

curl -s -X POST http://127.0.0.1:8070/exhaust
-H "Content-Type: application/json"
-d '{"value":50}' ; echo

Set target temperature (only active in AUTO mode)

curl -s -X POST http://127.0.0.1:8070/setpoint
-H "Content-Type: application/json"
-d '{"c":35}' ; echo

All of these should return JSON with "ok": true.


HTTP API Reference

All endpoints are on 127.0.0.1:8070 — localhost only, not accessible from the network directly.

Method Endpoint Body Description
GET /ping Sends PING to ESP32 live, returns PONG reply
GET /status Returns cached status snapshot (updated every 2s by poller)
POST /setpoint {"c": 35.0} Sets PID target temperature in °C (range: 10–50)
POST /mode {"mode": "AUTO"} Sets control mode: OFF, AUTO, or MANUAL
POST /heater {"value": 30.0} Sets heater duty 0–100%. MANUAL mode only.
POST /exhaust {"value": 80.0} Sets exhaust fan duty 0–100%. MANUAL mode only.

Status shows ok: false

The daemon is running but lost contact with the ESP32. Check the live log:

sudo journalctl -u enclosure-daemon -f

You'll see one of:

  • [serial] Write error ... closing port — USB connection dropped. Try unplugging and replugging the ESP32. The daemon reconnects automatically.
  • [poll] No reply — ESP32 is connected but not responding. May have crashed or be mid-reboot. Wait a few seconds — it will recover.

/dev/ttyUSB0 doesn't exist

ls /dev/ttyUSB* /dev/ttyACM*

If nothing appears, the Pi isn't detecting the ESP32 at all:

  • Try a different USB cable (data cable, not charge-only)
  • Try a different USB port on the Pi
  • Check dmesg | tail -n 20 for USB device detection messages

Daemon starts but polls show None

[poll] reply: None
[poll] reply: None

The ESP32 is connected but not responding to GET STATUS. Possible causes:

  • Firmware hasn't finished booting yet — wait 5–10 seconds
  • Wrong baud rate — confirm BAUD = 115200 in the daemon matches the firmware's Serial.begin(115200)
  • ESP32 is in a fault state — connect to it directly via the Arduino Serial Monitor and send PING manually to check

Pi user can't access /dev/ttyUSB0

# Check group membership
groups pi

Add pi to dialout group if missing

sudo usermod -aG dialout pi

Reboot for the group change to take effect

sudo reboot

Clone this wiki locally