Add multi-panel Pico UDP firmware and Python LED control.
Pico panels get static IPs on 10.1.1.10–14 with per-panel LED counts, Makefile deploy targets, and Python examples for animations, sync tests, and direct Pi SPI control. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
116
scripts/panel_deploy.sh
Executable file
116
scripts/panel_deploy.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build and upload portal panel firmware over USB.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/panel_deploy.sh
|
||||
# ./scripts/panel_deploy.sh --build-only
|
||||
# ./scripts/panel_deploy.sh --upload-only
|
||||
#
|
||||
# Options:
|
||||
# --build-only Build, do not upload
|
||||
# --upload-only Upload existing build, skip compile
|
||||
# --panel-id N Pass -DPANEL_ID_BUILD=N to cmake
|
||||
# --no-dhcp Pass -DPORTAL_USE_DHCP=OFF to cmake
|
||||
# --num-leds N Pass -DNUM_LEDS_BUILD=N to cmake
|
||||
#
|
||||
# Environment:
|
||||
# PICO_SDK_PATH Path to pico-sdk (default: ~/pico/pico-sdk)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_DIR="${ROOT}/firmware/panel/build"
|
||||
PICO_SDK_PATH="${PICO_SDK_PATH:-${HOME}/pico/pico-sdk}"
|
||||
JOBS="$(nproc)"
|
||||
|
||||
DO_BUILD=1
|
||||
DO_UPLOAD=1
|
||||
CMAKE_EXTRA=()
|
||||
|
||||
die() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--build-only)
|
||||
DO_UPLOAD=0
|
||||
shift
|
||||
;;
|
||||
--upload-only)
|
||||
DO_BUILD=0
|
||||
shift
|
||||
;;
|
||||
--panel-id)
|
||||
[[ $# -ge 2 ]] || die "--panel-id requires a number"
|
||||
CMAKE_EXTRA+=("-DPANEL_ID_BUILD=${2}")
|
||||
shift 2
|
||||
;;
|
||||
--no-dhcp)
|
||||
CMAKE_EXTRA+=("-DPORTAL_USE_DHCP=OFF")
|
||||
shift
|
||||
;;
|
||||
--num-leds)
|
||||
[[ $# -ge 2 ]] || die "--num-leds requires a number"
|
||||
CMAKE_EXTRA+=("-DNUM_LEDS_BUILD=${2}")
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1 (try --help)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
build_firmware() {
|
||||
[[ -d "${PICO_SDK_PATH}" ]] || die "pico-sdk not found at ${PICO_SDK_PATH} (set PICO_SDK_PATH)"
|
||||
command -v cmake >/dev/null || die "cmake is required"
|
||||
command -v make >/dev/null || die "make is required"
|
||||
|
||||
export PICO_SDK_PATH
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
|
||||
echo "==> Building panel firmware in ${BUILD_DIR}"
|
||||
if [[ ! -f "${BUILD_DIR}/CMakeCache.txt" ]]; then
|
||||
cmake -S "${ROOT}/firmware/panel" -B "${BUILD_DIR}" "${CMAKE_EXTRA[@]}"
|
||||
elif ((${#CMAKE_EXTRA[@]} > 0)); then
|
||||
cmake -S "${ROOT}/firmware/panel" -B "${BUILD_DIR}" "${CMAKE_EXTRA[@]}"
|
||||
fi
|
||||
cmake --build "${BUILD_DIR}" -j "${JOBS}"
|
||||
|
||||
[[ -f "${BUILD_DIR}/panel.uf2" ]] || die "build did not produce panel.uf2"
|
||||
echo "==> Build OK: ${BUILD_DIR}/panel.uf2"
|
||||
}
|
||||
|
||||
upload_usb() {
|
||||
command -v picotool >/dev/null || die "picotool not found (install from https://github.com/raspberrypi/picotool)"
|
||||
if picotool version 2>&1 | grep -q 'without USB support'; then
|
||||
die "picotool was built without USB support; rebuild with libusb-1.0-0-dev installed"
|
||||
fi
|
||||
|
||||
echo "==> Uploading over USB"
|
||||
if ! picotool load -x -f "${BUILD_DIR}/panel.uf2" 2>&1 | tee /tmp/picotool_load.log; then
|
||||
die "picotool load failed"
|
||||
fi
|
||||
if ! grep -q 'Loading into Flash' /tmp/picotool_load.log; then
|
||||
die "picotool did not flash (hold BOOTSEL, plug USB, retry)"
|
||||
fi
|
||||
echo "==> USB upload OK"
|
||||
}
|
||||
|
||||
if ((DO_BUILD)); then
|
||||
build_firmware
|
||||
fi
|
||||
|
||||
if ((DO_UPLOAD)); then
|
||||
[[ -f "${BUILD_DIR}/panel.uf2" ]] || die "panel.uf2 not found; run without --upload-only first"
|
||||
upload_usb
|
||||
fi
|
||||
117
scripts/setup_ap.sh
Executable file
117
scripts/setup_ap.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# Set up a WiFi access point on wlan0 and share internet from ethernet (eth0).
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./scripts/setup_ap.sh
|
||||
# sudo PORTAL_AP_SSID=mywifi PORTAL_AP_PASSWORD=secret ./scripts/setup_ap.sh
|
||||
#
|
||||
# Environment:
|
||||
# PORTAL_AP_SSID WiFi name (default: portal)
|
||||
# PORTAL_AP_PASSWORD WPA password, 8+ chars (default: portal1234)
|
||||
# PORTAL_AP_CON_NAME NetworkManager connection name (default: portal-ap)
|
||||
# PORTAL_AP_IF AP interface (default: wlan0)
|
||||
# PORTAL_WAN_IF Uplink interface (default: eth0)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SSID="${PORTAL_AP_SSID:-portal}"
|
||||
PASSWORD="${PORTAL_AP_PASSWORD:-portal1234}"
|
||||
CON_NAME="${PORTAL_AP_CON_NAME:-portal-ap}"
|
||||
AP_IF="${PORTAL_AP_IF:-wlan0}"
|
||||
WAN_IF="${PORTAL_WAN_IF:-eth0}"
|
||||
|
||||
die() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
die "run as root: sudo $0"
|
||||
fi
|
||||
|
||||
if ((${#PASSWORD} < 8)); then
|
||||
die "PORTAL_AP_PASSWORD must be at least 8 characters"
|
||||
fi
|
||||
|
||||
if ! command -v nmcli >/dev/null; then
|
||||
die "NetworkManager (nmcli) is required"
|
||||
fi
|
||||
|
||||
if ! systemctl is-active --quiet NetworkManager; then
|
||||
die "NetworkManager is not running"
|
||||
fi
|
||||
|
||||
if ! ip link show "${WAN_IF}" >/dev/null 2>&1; then
|
||||
die "uplink interface ${WAN_IF} not found"
|
||||
fi
|
||||
|
||||
if ! ip link show "${AP_IF}" >/dev/null 2>&1; then
|
||||
die "AP interface ${AP_IF} not found"
|
||||
fi
|
||||
|
||||
if ! ip link show "${WAN_IF}" | grep -q "state UP"; then
|
||||
echo "Warning: ${WAN_IF} is not UP — connect ethernet before clients can reach the internet." >&2
|
||||
fi
|
||||
|
||||
# NM shared AP mode uses hostapd on many Pi images.
|
||||
if ! command -v hostapd >/dev/null; then
|
||||
echo "Installing hostapd..."
|
||||
apt-get update -qq
|
||||
apt-get install -y hostapd
|
||||
fi
|
||||
|
||||
# Allow forwarding between AP clients and ethernet.
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
cat >/etc/sysctl.d/99-portal-ap.conf <<'EOF'
|
||||
# portal AP: share ethernet to WiFi clients
|
||||
net.ipv4.ip_forward=1
|
||||
EOF
|
||||
|
||||
rfkill unblock wifi >/dev/null 2>&1 || true
|
||||
nmcli radio wifi on >/dev/null 2>&1 || true
|
||||
|
||||
if nmcli -t -f NAME connection show | grep -Fxq "${CON_NAME}"; then
|
||||
echo "Updating existing connection '${CON_NAME}'..."
|
||||
nmcli connection delete "${CON_NAME}" >/dev/null
|
||||
fi
|
||||
|
||||
echo "Creating access point '${SSID}' on ${AP_IF}..."
|
||||
nmcli connection add type wifi ifname "${AP_IF}" con-name "${CON_NAME}" \
|
||||
autoconnect yes \
|
||||
ssid "${SSID}" \
|
||||
802-11-wireless.mode ap \
|
||||
802-11-wireless.band bg \
|
||||
ipv4.method shared \
|
||||
ipv6.method ignore \
|
||||
wifi-sec.key-mgmt wpa-psk \
|
||||
wifi-sec.psk "${PASSWORD}" >/dev/null
|
||||
|
||||
# Prefer ethernet for upstream internet when both are available.
|
||||
nmcli connection modify "${CON_NAME}" ipv4.never-default no
|
||||
if nmcli -t -f NAME connection show | grep -Fxq "Wired connection 1"; then
|
||||
nmcli connection modify "Wired connection 1" connection.autoconnect-priority 100
|
||||
fi
|
||||
nmcli connection modify "${CON_NAME}" connection.autoconnect-priority 50
|
||||
|
||||
echo "Starting access point..."
|
||||
nmcli connection up "${CON_NAME}"
|
||||
|
||||
AP_IP="$(nmcli -g IP4.ADDRESS device show "${AP_IF}" 2>/dev/null | cut -d/ -f1)"
|
||||
WAN_IP="$(nmcli -g IP4.ADDRESS device show "${WAN_IF}" 2>/dev/null | cut -d/ -f1 || true)"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Access point is running.
|
||||
|
||||
SSID: ${SSID}
|
||||
Password: ${PASSWORD}
|
||||
AP IP: ${AP_IP:-unknown} (${AP_IF})
|
||||
Uplink: ${WAN_IF} ${WAN_IP:+($WAN_IP)}
|
||||
|
||||
Clients receive DHCP from the Pi and internet is shared via NAT through ${WAN_IF}.
|
||||
The AP starts automatically on boot.
|
||||
|
||||
Stop it with:
|
||||
sudo ./scripts/stop_ap.sh
|
||||
|
||||
EOF
|
||||
62
scripts/stop_ap.sh
Executable file
62
scripts/stop_ap.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop the portal access point and return wlan0 to WiFi client mode.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CON_NAME="${PORTAL_AP_CON_NAME:-portal-ap}"
|
||||
WLAN_IF="${PORTAL_AP_IF:-wlan0}"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Error: run as root: sudo $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v nmcli >/dev/null; then
|
||||
echo "Error: NetworkManager (nmcli) is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if nmcli -t -f NAME connection show | grep -Fxq "${CON_NAME}"; then
|
||||
nmcli connection down "${CON_NAME}" 2>/dev/null || true
|
||||
nmcli connection modify "${CON_NAME}" connection.autoconnect no
|
||||
echo "Access point '${CON_NAME}' stopped (autoconnect disabled)."
|
||||
else
|
||||
echo "Connection '${CON_NAME}' not found."
|
||||
fi
|
||||
|
||||
rfkill unblock wifi >/dev/null 2>&1 || true
|
||||
nmcli radio wifi on >/dev/null 2>&1 || true
|
||||
nmcli device disconnect "${WLAN_IF}" 2>/dev/null || true
|
||||
nmcli device wifi rescan 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Saved client profiles, newest first (skip the AP connection).
|
||||
mapfile -t CLIENT_CONS < <(
|
||||
nmcli -t -f NAME,TYPE,TIMESTAMP connection show | awk -F: -v ap="${CON_NAME}" '
|
||||
$2 == "802-11-wireless" && $1 != ap { print $3 "\t" $1 }
|
||||
' | sort -rn | cut -f2
|
||||
)
|
||||
|
||||
CONNECTED=0
|
||||
for client_con in "${CLIENT_CONS[@]}"; do
|
||||
key_mgmt="$(nmcli -g 802-11-wireless-security.key-mgmt connection show "${client_con}" 2>/dev/null || true)"
|
||||
if [[ -z "${key_mgmt}" ]]; then
|
||||
echo "Skipping '${client_con}': missing 802-11-wireless-security.key-mgmt."
|
||||
echo " Fix: sudo nmcli connection delete ${client_con}"
|
||||
echo " sudo nmcli device wifi connect <SSID> password <pass>"
|
||||
continue
|
||||
fi
|
||||
if nmcli connection up "${client_con}" ifname "${WLAN_IF}" 2>/dev/null; then
|
||||
echo "wlan0 connected as client: ${client_con}"
|
||||
CONNECTED=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ((CONNECTED == 0)); then
|
||||
echo "wlan0 is in client mode but no saved network connected."
|
||||
echo "List networks: nmcli device wifi list"
|
||||
echo "Connect: sudo nmcli device wifi connect <SSID> password <pass>"
|
||||
fi
|
||||
|
||||
echo "Remove AP permanently: sudo nmcli connection delete ${CON_NAME}"
|
||||
Reference in New Issue
Block a user