feat(pi): add a Wi-Fi hotspot mode for routerless direct streaming

scripts/pi-hotspot.sh turns the receiver into a WPA2 access point via
NetworkManager (ipv4 shared mode gives the Pi built-in DHCP/NAT at
10.42.0.1), so a sender connects directly with no router in between —
which also sidesteps LAN quirks like unreachable 6to4 addresses, since
the direct link is plain private IPv4. The passphrase is generated on
first use and stored root-only in /etc/screencast-hotspot.conf;
on|off|status subcommands manage it, and off restores normal client
Wi-Fi. The hotspot owns wlan0 while active (documented). No receiver
changes were needed: it already announces on every interface.

nmcli property syntax validated against NetworkManager 1.58 with a
disposable never-activated profile; actual AP bring-up can only be
validated on the Pi.
This commit is contained in:
2026-09-07 14:13:48 +02:00
parent 013c2519dc
commit 41f71fd217
4 changed files with 153 additions and 0 deletions
+6
View File
@@ -65,6 +65,12 @@ loopback; current phase is Phase 7.
before the fix). Cross-compiling on the dev machine was considered and before the fix). Cross-compiling on the dev machine was considered and
dropped — the on-Pi build works and the toolchain/container effort was dropped — the on-Pi build works and the toolchain/container effort was
not needed. not needed.
- **Pi Wi-Fi hotspot** (`scripts/pi-hotspot.sh on|off|status`): NetworkManager
AP mode (WPA2, ipv4 shared → built-in DHCP/NAT, Pi at 10.42.0.1). Takes
over wlan0 while active; generated PSK stored in /etc/screencast-hotspot.conf.
No application changes needed — the receiver already announces on all
interfaces. nmcli property syntax validated against NM 1.58 with a
disposable profile; AP bring-up itself can only be validated on the Pi.
- **REAL-HARDWARE VALIDATION (desktop → Pi Zero 2 W over Wi-Fi)**: the full - **REAL-HARDWARE VALIDATION (desktop → Pi Zero 2 W over Wi-Fi)**: the full
chain works on two machines: mDNS discovery → signaling negotiation → chain works on two machines: mDNS discovery → signaling negotiation →
RTP over Wi-Fi → software H.264 decode → fullscreen KMSDRM letterboxed RTP over Wi-Fi → software H.264 decode → fullscreen KMSDRM letterboxed
+19
View File
@@ -90,6 +90,25 @@ Verified behavior: `systemctl stop` results in a clean exit (success,
no restart) and the mDNS announcement is withdrawn — senders stop finding no restart) and the mDNS announcement is withdrawn — senders stop finding
the receiver immediately. the receiver immediately.
## Direct link: receiver as a Wi-Fi hotspot
The receiver can run as a Wi-Fi access point, so a sender connects to the
Pi directly with no router in between (on such a direct link the Pi is
plain private IPv4 — no 6to4 or router quirks):
```sh
sudo scripts/pi-hotspot.sh on # prints SSID + generated password
sudo scripts/pi-hotspot.sh status # shows the saved credentials
sudo scripts/pi-hotspot.sh off # back to normal router Wi-Fi
```
While the hotspot is active it owns the Wi-Fi interface: the Pi leaves any
router network, and the Pi is reachable at `10.42.0.1` (SSH included). On
the sender, join the hotspot's Wi-Fi and run `screencast --send`
discovery and streaming then flow over the direct link. The passphrase is
generated on first use and stored in `/etc/screencast-hotspot.conf` (root
only). Override with `--ssid`, `--psk`, or `--band a` (5 GHz).
## Headless receiver (no window manager) ## Headless receiver (no window manager)
The receiver does not need X11, Wayland, or any window manager: SDL3's The receiver does not need X11, Wayland, or any window manager: SDL3's
+2
View File
@@ -207,4 +207,6 @@ Notes for small boards (e.g. Pi Zero 2 W):
- On a desktop session instead, edit the installed service file and - On a desktop session instead, edit the installed service file and
uncomment the Environment lines for XDG_RUNTIME_DIR/WAYLAND_DISPLAY. uncomment the Environment lines for XDG_RUNTIME_DIR/WAYLAND_DISPLAY.
- avahi-daemon must stay enabled; it is what senders discover. - avahi-daemon must stay enabled; it is what senders discover.
- For a routerless direct link: scripts/pi-hotspot.sh on turns the Pi
into a Wi-Fi access point (see docs/RUNBOOK.md).
EOF EOF
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
#
# pi-hotspot.sh — turn the receiver into a Wi-Fi access point so a sender
# can connect directly, without any router in between.
#
# Usage (on the Pi):
#
# sudo scripts/pi-hotspot.sh on [--ssid NAME] [--psk KEY] [--band bg|a]
# sudo scripts/pi-hotspot.sh off
# sudo scripts/pi-hotspot.sh status
#
# The hotspot takes over the Wi-Fi interface: the Pi leaves your router's
# network while it is active. Connect to the SSID from the sending machine;
# the Pi is reachable at 10.42.0.1 (SSH, screencast) and the sender gets an
# address via the built-in DHCP. Discovery and streaming then run over the
# direct link.
#
# The passphrase is stored in /etc/screencast-hotspot.conf (root only) so
# `status` can show it again.
set -euo pipefail
CONNECTION_NAME="screencast-hotspot"
INTERFACE="${SC_HOTSPOT_INTERFACE:-wlan0}"
CONFIG_FILE="/etc/screencast-hotspot.conf"
log() { printf '\n=== %s\n' "$*"; }
die() { printf 'pi-hotspot: error: %s\n' "$*" >&2; exit 1; }
require_root() {
[ "$(id -u)" = 0 ] || die "run with sudo"
}
random_passphrase() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 12 | tr -d '=+/' | head -c 16
else
tr -dc 'A-Za-z0-9' </dev/urandom | head -c 16
fi
}
cmd_on() {
local ssid="screencast" psk="" band="bg"
while [ $# -gt 0 ]; do
case "$1" in
--ssid) ssid="$2"; shift 2 ;;
--psk) psk="$2"; shift 2 ;;
--band) band="$2"; shift 2 ;;
*) die "unknown option: $1" ;;
esac
done
command -v nmcli >/dev/null 2>&1 || die "NetworkManager (nmcli) not found"
systemctl is-active --quiet NetworkManager || die "NetworkManager is not running"
ip link show "$INTERFACE" >/dev/null 2>&1 || die "interface ${INTERFACE} does not exist"
if [ -z "$psk" ]; then
psk="$(random_passphrase)"
fi
log "warning: this takes over ${INTERFACE}; the Pi leaves any router Wi-Fi while the hotspot is active"
log "creating hotspot '${ssid}' (WPA2, band ${band})"
apt-get install -y --no-install-recommends network-manager dnsmasq-base >/dev/null 2>&1 || true
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true
nmcli connection add \
type wifi \
ifname "$INTERFACE" \
con-name "$CONNECTION_NAME" \
autoconnect yes \
connection.autoconnect-priority 100 \
802-11-wireless.ssid "$ssid" \
802-11-wireless.mode ap \
802-11-wireless.band "$band" \
802-11-wireless-security.key-mgmt wpa-psk \
802-11-wireless-security.psk "$psk" \
ipv4.method shared \
ipv6.method disabled >/dev/null
nmcli connection up "$CONNECTION_NAME" >/dev/null
umask 077
printf 'ssid=%s\npsk=%s\n' "$ssid" "$psk" >"$CONFIG_FILE"
log "hotspot is up"
printf ' SSID: %s\n' "$ssid"
printf ' Password: %s\n' "$psk"
printf ' Pi address: 10.42.0.1 (also announced via mDNS)\n'
printf '\nOn the sending machine: connect to the Wi-Fi above, then\n'
printf ' screencast --send\n'
}
cmd_off() {
log "removing the hotspot (Wi-Fi returns to normal client mode)"
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || {
log "no hotspot configured"
exit 0
}
rm -f "$CONFIG_FILE"
log "done; the Pi will reconnect to any known router Wi-Fi"
}
cmd_status() {
if [ -f "$CONFIG_FILE" ]; then
log "hotspot configuration (${CONFIG_FILE})"
grep -E '^(ssid|psk)=' "$CONFIG_FILE" | sed -e 's/^/ /'
else
log "no hotspot configured (run: sudo scripts/pi-hotspot.sh on)"
fi
nmcli --get-values NAME,TYPE,DEVICE connection show --active 2>/dev/null | grep wifi | sed 's/^/ /' || true
}
case "${1:-}" in
on) require_root; shift; cmd_on "$@" ;;
off) require_root; shift; cmd_off "$@" ;;
status) cmd_status ;;
*)
cat <<'EOF'
usage: sudo scripts/pi-hotspot.sh on [--ssid NAME] [--psk KEY] [--band bg|a]
sudo scripts/pi-hotspot.sh off
sudo scripts/pi-hotspot.sh status
EOF
exit 1
;;
esac