Files
light-heart-labs--dreamserver/ods/docs/SETUP-CARD.md
T
wehub-resource-sync 9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:33 +08:00

4.3 KiB

Setup card generator

scripts/generate-setup-card.py produces printable 4x6 setup cards for fulfillment: drop one in the box with each ODS unit so the recipient can scan two QR codes without typing anything. The default card opens the first-run wizard; --mode factory-owner prints the owner-card handoff that opens ODS Talk.

What it generates

A portrait card with:

  1. Top band - "ODS" wordmark, the unit's mDNS name (e.g. ods.local), and a one-line tagline.
  2. Two QR codes side by side:
    • Left - JOIN WI-FI. Encodes the standard WIFI:T:WPA;S:<ssid>;P:<password>;; format that iOS Camera and Android auto-recognize.
    • Right - OPEN SETUP or OPEN ODS TALK. In default setup mode this is a setup URL such as http://192.168.7.1/setup. In factory-owner mode this is the owner magic-link URL generated by Setup / Owner.
  3. Plain-text fallback block - SSID, password, and URL printed verbatim for phones that will not scan the QR.
  4. Footer - "ODS is open-source - light-heart-labs.com", plus an optional per-unit serial number.

Card is 1200x1800 px at 300 DPI = exactly 4x6 inches. PNG is the default output; PDF is available with --format pdf or a .pdf output path.

Requirements

pip install 'qrcode[pil]'

This is an operator-side tool, not a runtime service, so the deps are not bundled with any ODS container.

Usage

Setup wizard card:

python3 scripts/generate-setup-card.py \
  --ssid 'ODS-Setup-A4F2' \
  --password 'xxxxxxxx' \
  --setup-url 'http://192.168.7.1/setup' \
  --device-name 'ods-a4f2.local' \
  --serial 'DRM-2026-A4F2' \
  --output cards/card-A4F2.png

Factory owner card:

python3 scripts/generate-setup-card.py \
  --mode factory-owner \
  --ssid 'ODS-Setup-A4F2' \
  --password 'xxxxxxxx' \
  --owner-url 'http://auth.ods-a4f2.local/magic-link/OWNER_TOKEN' \
  --device-name 'ods-a4f2.local' \
  --serial 'DRM-2026-A4F2' \
  --output cards/owner-card-A4F2.pdf

Flags:

Flag Required Notes
--mode no setup by default; use factory-owner for Wi-Fi + owner ODS Talk QR cards.
--ssid yes Wi-Fi SSID of the device's setup AP.
--password no Wi-Fi password. Omit for open networks; the Wi-Fi QR will use T:nopass automatically.
--security no WPA (default), WEP, or nopass.
--setup-url setup mode URL the QR opens, e.g. http://192.168.7.1/setup.
--owner-url factory-owner mode Owner magic-link URL created from Setup / Owner. Treat it as a physical credential.
--device-name no mDNS hostname printed on the card. Defaults to ods.local.
--serial no Optional serial / batch ID printed in the footer right corner.
--format no png or pdf. If omitted, .pdf output paths write PDF; everything else writes PNG.
--output / -o yes Output path. Parent directory is created if missing.

Exit codes: 0 on success, 2 for missing dependencies or invalid flags.

Batch generation

Looping the script over a list of pre-baked AP credentials is the recommended fulfillment flow:

while IFS=',' read -r ssid password serial owner_url; do
  python3 scripts/generate-setup-card.py \
    --mode factory-owner \
    --ssid "$ssid" \
    --password "$password" \
    --owner-url "$owner_url" \
    --device-name "ods-$(printf '%s' "$serial" | tr '[:upper:]' '[:lower:]').local" \
    --serial "$serial" \
    --output "cards/$serial.pdf"
done < unit-credentials.csv

Security notes

  • The plaintext AP password is on the card by design. Treat the card as a physical credential.
  • A factory owner URL is a permanent credential until revoked. Print only the intended card, do not publish screenshots, and revoke/reprint from Setup / Owner when a card is lost or replaced.
  • Factory owner cards open the mobile ODS Talk portal. It is backed by Hermes, but it avoids the full advanced Hermes interface for the recipient.
  • The setup URL does not carry a credential. It points at the device's setup-mode IP; if the recipient has not joined the AP yet, the URL times out.
  • Serial / batch identifiers are optional and intended for fulfillment, not authentication.

Limitations / future work

  • No splash logo. The wordmark is set in the system bold font.
  • No multi-language. Text is English-only.
  • No Hidden SSID flag. Standard Wi-Fi QR supports H:true;. Today the script hardcodes H:false.