Add Calculinux one-time setup script

Sequences the full Lyra/PicoCalc bring-up: opkg python packages, the
rtl8xxxu Wi-Fi boot-scan kick (installed verbatim from the working device),
the deploy user, the read-only SSH clone, the venv + editable install with
TMPDIR off the RAM tmpfs, the login PATH, and the console font. Idempotent;
secrets (deploy key, Wi-Fi PSK) are checked for or read from the environment,
never embedded. README documents both scripts and their run order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.8 committed 2026-07-26 02:18:13 -04:00
1 parent f4abacc3b8
commit 2f75bb597c
2 files changed
+270 -5

No files matched your search

+42 -5
View File
@@ -1,6 +1,43 @@
# meshterm-spi-bridge
# scripts
Expose an **SPI-attached** MeshCore radio to MeshTerm.
Device-side helpers for running MeshTerm on real hardware. Two independent targets
live here, each self-contained:
- **Calculinux device** (Luckfox Lyra / ClockworkPi PicoCalc) — one-time bring-up and
the console font.
- **SPI-attached radio bridge** (`meshterm-spi-bridge`) — expose a bare SPI LoRa chip
to MeshTerm as if it were a companion device.
## Calculinux device (Luckfox Lyra / ClockworkPi PicoCalc)
Helpers for running MeshTerm on a **Luckfox Lyra / ClockworkPi PicoCalc under Calculinux**
(the bare-framebuffer terminal target). They run *on the device* as root, not on your dev
machine.
| Script | What it does |
|---|---|
| [`calculinux-setup.sh`](calculinux-setup.sh) | Complete one-time bring-up: opkg python packages, the Wi-Fi boot-scan kick, the deploy user, the read-only clone, the venv + `pip install -e .`, the login PATH, and the console font. Idempotent. |
| [`calculinux-console-font.sh`](calculinux-console-font.sh) | Just the console font: builds/installs/persists the 512-glyph **meshterm** PSF (Terminus + braille + node glyphs + rounded frame corners + the list cursor). Called by the setup script; also runnable on its own after a MeshTerm update. |
### Usage
Copy the `scripts/` folder to the device and, as root:
```bash
sh calculinux-setup.sh
```
Two prerequisites the setup script checks for but cannot embed:
- **Deploy key** — place the read-only GitHub deploy key at `$HOME/.ssh/id_ed25519`
(mode 600) before running; the clone uses it over SSH.
- **Wi-Fi credentials** — the kick only nudges a network iwd already knows. Either connect
once by hand (`iwctl station wlan0 connect <SSID>`) or export `WIFI_SSID` and `WIFI_PSK`
so the script writes the iwd config (secrets stay in your environment, never in the repo).
Both scripts are POSIX `sh`, idempotent, and safe to re-run.
## meshterm-spi-bridge — expose an SPI-attached radio
Some boards wire the LoRa chip straight to the host's SPI bus — the ClockworkPi
uConsole with the hackergadgets AIO, Waveshare LoRa HATs, and similar. There is
@@ -20,7 +57,7 @@ meshterm --tcp 127.0.0.1:5000
connects exactly as if a real companion device were plugged in.
## Usage
### Usage
Copy `meshterm-spi-bridge` onto the radio host and run it with any Python 3 —
it finds the `pymc_core` runtime and re-execs the bridge under it for you:
@@ -39,7 +76,7 @@ The menu offers:
3. **Uninstall** — removes only the bridge service + its helper copy. It never
touches the `meshcore-console` / `pymc_core` packages.
## Caveats
### Caveats
- **Only one program may use the radio at a time.** Run *either* the
`meshcore-console` GUI *or* this bridge — never both. The bridge preflight
@@ -54,7 +91,7 @@ The menu offers:
today; the node's trace / path-discovery replies aren't yet pushed to the
companion server, so MeshTerm's Trace screen won't populate until that's added.
## Overriding hardware pins
### Overriding hardware pins
Defaults match the hackergadgets uConsole AIO. For other SPI boards, set env
vars before launching (read by the direct-`pymc_core` path):
+228
View File
@@ -0,0 +1,228 @@
#!/bin/sh
# calculinux-setup.sh -- one-time bring-up of MeshTerm on a Luckfox Lyra / PicoCalc
# running Calculinux (Yocto, systemd, read-only root with /usr + /etc + /home overlays
# backed by /data). Run as root on a freshly-flashed SD, over the serial console is fine:
#
# sh calculinux-setup.sh
#
# It is idempotent -- safe to re-run; each phase checks before it acts. It sequences the
# whole deploy this repo's Lyra target needed, in the order the discoveries fell out:
#
# 1. opkg packages Calculinux ships a STRIPPED python3.13 (no pip/venv/ensurepip and
# missing stdlib: sqlite3, ctypes, curses, shlex, xml). /usr is a
# writable overlay, so `opkg install python3-modules python3-pip`
# restores the full interpreter and persists on /data.
# 2. wi-fi kick The USB RTL8188EU (rtl8xxxu) firmware finishes loading AFTER iwd's
# first scans, so on cold boot wlan0 stays UP/NO-CARRIER and the known
# network is never joined. A oneshot service drives RAW `iw` scans
# until a DHCP lease -- a completed raw scan is what lets iwd associate.
# 3. deploy user The `meshterm` login MeshTerm runs under (created if absent; no password is
# set here -- run `passwd meshterm` yourself).
# 4. clone Pull MeshTerm with the READ-ONLY GitHub deploy key over SSH.
# 5. venv + install A venv + `pip install -e .`. pip's C builds hit ENOSPC because /tmp
# is a tiny RAM tmpfs, so TMPDIR is redirected to $HOME/tmp on /data.
# 6. PATH Put the venv's `meshterm` on meshterm's login PATH via ~/.profile.
# 7. console font Hand off to calculinux-console-font.sh (braille + node glyphs +
# rounded frame corners + the list cursor the bare console can't draw).
#
# Two prerequisites this script cannot safely embed and will check for / guide you through:
# * the read-only deploy key at $HOME/.ssh/id_ed25519 (never commit a private key);
# * Wi-Fi credentials known to iwd. The kick only nudges an ALREADY-known network. Either
# provision it once by hand (`iwctl station wlan0 connect <SSID>`), or export
# WIFI_SSID and WIFI_PSK before running and this script writes the iwd config for you.
set -eu
# --- knobs (override via the environment) ----------------------------------------------
DEPLOY_USER="${DEPLOY_USER:-meshterm}"
REPO_SSH="${REPO_SSH:-git@github.com:jpmartineau/MeshTerm.git}"
KEY_PATH="${KEY_PATH:-/home/$DEPLOY_USER/.ssh/id_ed25519}"
CHECKOUT="/home/$DEPLOY_USER/MeshTerm"
WIFI_SSID="${WIFI_SSID:-}" # optional: set both to have iwd credentials written
WIFI_PSK="${WIFI_PSK:-}"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
log() { printf '\n== %s\n' "$*"; }
info() { printf ' %s\n' "$*"; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
runas() { su - "$DEPLOY_USER" -c "$1"; }
[ "$(id -u)" = 0 ] || die "run as root (installs packages, writes /etc, creates the user)"
# --- 1. opkg packages ------------------------------------------------------------------
log "1/7 system packages (opkg)"
if have opkg; then
opkg update >/dev/null 2>&1 || info "opkg update failed (no network yet?) -- continuing"
# git and kbd ship in the base image; the python pieces are what the stripped build drops.
for pkg in python3-modules python3-pip git kbd; do
if opkg status "$pkg" 2>/dev/null | grep -q '^Status:.*installed'; then
info "$pkg already installed"
else
info "installing $pkg"
opkg install "$pkg" || die "opkg install $pkg failed (need network for the feed)"
fi
done
else
info "opkg not found -- assuming packages are already present"
fi
have python3 || die "python3 still missing after opkg (check the opkg feed / network)"
# --- 2. wi-fi boot-scan kick (rtl8xxxu race workaround) --------------------------------
log "2/7 wi-fi boot-scan kick"
# Optionally provision the iwd network so the kick has something known to join. Secrets
# come from the environment only -- nothing is written to disk from this repo.
if [ -n "$WIFI_SSID" ] && [ -n "$WIFI_PSK" ]; then
info "writing iwd credentials for '$WIFI_SSID'"
mkdir -p /var/lib/iwd
printf '[Security]\nPassphrase=%s\n' "$WIFI_PSK" > "/var/lib/iwd/$WIFI_SSID.psk"
chmod 600 "/var/lib/iwd/$WIFI_SSID.psk"
elif [ -n "$WIFI_SSID" ] || [ -n "$WIFI_PSK" ]; then
info "WIFI_SSID and WIFI_PSK must BOTH be set to write credentials -- skipping"
else
info "no WIFI_SSID/WIFI_PSK given -- the kick will nudge whatever iwd already knows"
fi
cat > /etc/wifi-kick.sh <<'KICKEOF'
#!/bin/sh
# rtl8xxxu boot workaround.
#
# On cold boot iwd comes up before the RTL8188EU firmware finishes loading and
# its own scans then return nothing, so wlan0 stays UP/NO-CARRIER and the known
# network (stored PSK) is never joined. A *raw* "iw dev wlan0 scan" does work,
# and a completed raw scan populates the mac80211 results, after which iwd
# immediately authenticates/associates to the known network on its own.
#
# So we just drive raw scans until we get a DHCP lease. Do NOT use
# "iwctl scan" here: it makes iwd grab the device and the raw scan then fails
# busy (that is why a scan-only nudge did not work).
has_ip() {
ip -4 addr show wlan0 2>/dev/null | grep -q "inet "
}
# Wait for the dongle to enumerate.
n=0
while [ $n -lt 60 ]; do
[ -d /sys/class/net/wlan0 ] && break
sleep 1
n=$((n + 1))
done
# Drive raw scans until iwd associates and DHCP lands (~5 min ceiling).
i=0
while [ $i -lt 30 ]; do
has_ip && exit 0
/usr/sbin/ip link set wlan0 up 2>/dev/null
/usr/sbin/iw dev wlan0 scan >/dev/null 2>&1
# Association + DHCP follow a completed scan; poll before scanning again.
j=0
while [ $j -lt 5 ]; do
sleep 2
has_ip && exit 0
j=$((j + 1))
done
i=$((i + 1))
done
exit 0
KICKEOF
chmod +x /etc/wifi-kick.sh
cat > /etc/systemd/system/wifi-kick.service <<'UNITEOF'
[Unit]
Description=Nudge iwd to reconnect wlan0 at boot (rtl8xxxu workaround)
After=iwd.service
Wants=iwd.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/etc/wifi-kick.sh
[Install]
WantedBy=multi-user.target
UNITEOF
systemctl daemon-reload
systemctl enable wifi-kick.service >/dev/null 2>&1 || info "could not enable wifi-kick.service"
info "installed /etc/wifi-kick.sh + wifi-kick.service (enabled)"
# --- 3. deploy user --------------------------------------------------------------------
log "3/7 deploy user '$DEPLOY_USER'"
if id "$DEPLOY_USER" >/dev/null 2>&1; then
info "user exists"
else
info "creating user"
if have useradd; then
useradd -m -s /bin/bash "$DEPLOY_USER" || die "useradd failed"
else
adduser -D -s /bin/bash "$DEPLOY_USER" || die "adduser failed"
fi
# wheel -> sudo, matching the working device; harmless if the group is absent.
(usermod -aG wheel "$DEPLOY_USER" 2>/dev/null || adduser "$DEPLOY_USER" wheel 2>/dev/null) || true
info "no password set -- run: passwd $DEPLOY_USER"
fi
# --- 4. clone MeshTerm (read-only deploy key over SSH) ---------------------------------
log "4/7 clone MeshTerm"
[ -f "$KEY_PATH" ] || die "deploy key not found at $KEY_PATH
place the READ-ONLY GitHub deploy key there first, e.g.:
install -d -m700 -o $DEPLOY_USER -g $DEPLOY_USER /home/$DEPLOY_USER/.ssh
cp id_ed25519 $KEY_PATH && chmod 600 $KEY_PATH
chown $DEPLOY_USER:$DEPLOY_USER $KEY_PATH"
KNOWN_HOSTS="/home/$DEPLOY_USER/.ssh/known_hosts"
SSH_CMD="ssh -i $KEY_PATH -o IdentitiesOnly=yes -o UserKnownHostsFile=$KNOWN_HOSTS"
# Pin github.com's host key up front so the clone never blocks on an interactive prompt.
if ! runas "test -f $KNOWN_HOSTS && grep -q github.com $KNOWN_HOSTS"; then
info "recording github.com host key"
runas "ssh-keyscan -t ed25519 github.com >> $KNOWN_HOSTS 2>/dev/null" || info "ssh-keyscan failed (offline?)"
fi
if [ -d "$CHECKOUT/.git" ]; then
info "already cloned -- pulling"
runas "git -C ~/MeshTerm pull --ff-only" || info "pull failed (offline?) -- continuing"
else
info "cloning $REPO_SSH"
runas "GIT_SSH_COMMAND='$SSH_CMD' git clone $REPO_SSH ~/MeshTerm" \
|| die "clone failed (key not authorized, or offline)"
# Bake the deploy key into the checkout so later pulls just work.
runas "git -C ~/MeshTerm config core.sshCommand '$SSH_CMD'"
fi
# --- 5. venv + editable install (TMPDIR off the RAM tmpfs) -----------------------------
log "5/7 python venv + install"
if runas "test -x ~/MeshTerm/.venv/bin/python"; then
info "venv exists"
else
info "creating venv"
runas "python3 -m venv ~/MeshTerm/.venv" || die "venv creation failed"
fi
info "pip install -e . (TMPDIR on /data to dodge the /tmp ENOSPC)"
runas "mkdir -p ~/tmp && cd ~/MeshTerm && TMPDIR=\$HOME/tmp .venv/bin/pip install -e ." \
|| die "pip install failed"
# --- 6. PATH (login shells) ------------------------------------------------------------
log "6/7 login PATH"
PROFILE="/home/$DEPLOY_USER/.profile"
if [ -f "$PROFILE" ] && grep -q 'MeshTerm/.venv/bin' "$PROFILE"; then
info "already on PATH"
else
info "adding venv to PATH in ~/.profile"
printf '\n# MeshTerm venv on PATH\nexport PATH="$HOME/MeshTerm/.venv/bin:$PATH"\n' >> "$PROFILE"
chown "$DEPLOY_USER:$DEPLOY_USER" "$PROFILE"
fi
# --- 7. console font -------------------------------------------------------------------
log "7/7 console font"
if [ -f "$SCRIPT_DIR/calculinux-console-font.sh" ]; then
sh "$SCRIPT_DIR/calculinux-console-font.sh"
else
info "calculinux-console-font.sh not alongside this script -- skipping (run it separately)"
fi
# --- done ------------------------------------------------------------------------------
log "done"
info "log in as $DEPLOY_USER and run 'meshterm' (live) or 'meshterm --mock' (simulator)."
[ -n "$WIFI_SSID" ] || info "if wi-fi never comes up on boot, provision it once: iwctl station wlan0 connect <SSID>"