From 7cbaa9115ed26438f514e25eb6e63fb8cf5338c5 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:48:40 -0400 Subject: [PATCH 01/22] Add Buildroot support to manage script --- manage.sh | 191 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 144 insertions(+), 47 deletions(-) diff --git a/manage.sh b/manage.sh index 6659397..50694f4 100755 --- a/manage.sh +++ b/manage.sh @@ -17,6 +17,134 @@ SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}" R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps" R2_ENABLED=1 # Set to 0 to disable R2 wheels and always build from source +# --------------------------------------------------------------------------- +# Platform detection / compatibility helpers +# --------------------------------------------------------------------------- + +is_buildroot() { + [ -f /etc/pymc-image-build-id ] && return 0 + [ -f /etc/os-release ] && grep -q '^ID=buildroot$' /etc/os-release 2>/dev/null && return 0 + return 1 +} + +group_exists() { + local group_name="$1" + if command -v getent >/dev/null 2>&1; then + getent group "$group_name" >/dev/null 2>&1 + else + grep -q "^${group_name}:" /etc/group 2>/dev/null + fi +} + +ensure_group_line() { + local group_name="$1" + local gid="$2" + group_exists "$group_name" && return 0 + printf '%s:x:%s:\n' "$group_name" "$gid" >> /etc/group +} + +ensure_service_user() { + if id "$SERVICE_USER" >/dev/null 2>&1; then + return 0 + fi + + if command -v useradd >/dev/null 2>&1; then + useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER" + return 0 + fi + + if is_buildroot; then + ensure_group_line "$SERVICE_USER" 990 + printf '%s:x:990:990::/var/lib/pymc_repeater:/sbin/nologin\n' "$SERVICE_USER" >> /etc/passwd + if [ -f /etc/shadow ]; then + printf '%s:!:19000:0:99999:7:::\n' "$SERVICE_USER" >> /etc/shadow + fi + return 0 + fi + + echo "Error: useradd is not available on this system." >&2 + exit 1 +} + +add_user_to_group() { + local user_name="$1" + local group_name="$2" + + group_exists "$group_name" || return 0 + + if command -v usermod >/dev/null 2>&1; then + usermod -a -G "$group_name" "$user_name" 2>/dev/null || true + return 0 + fi + + if is_buildroot; then + local current_line current_members gid new_members escaped_line + current_line=$(grep "^${group_name}:" /etc/group 2>/dev/null || true) + [ -n "$current_line" ] || return 0 + current_members=$(printf '%s' "$current_line" | cut -d: -f4) + case ",${current_members}," in + *,"${user_name}",*) return 0 ;; + esac + if [ -n "$current_members" ]; then + new_members="${current_members},${user_name}" + else + new_members="${user_name}" + fi + gid=$(printf '%s' "$current_line" | cut -d: -f3) + escaped_line=$(printf '%s\n' "$current_line" | sed 's/[].[^$\\*]/\\&/g') + sed -i "s/^${escaped_line}\$/${group_name}:x:${gid}:${new_members}/" /etc/group + fi +} + +install_system_packages() { + if is_buildroot; then + echo " Buildroot image detected; using preinstalled system packages." + return 0 + fi + + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev + DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \ + || DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \ + || echo " Warning: Could not install polkit (sudo fallback will be used)" +} + +install_bootstrap_python_packages() { + if is_buildroot; then + return 0 + fi + + pip install --break-system-packages setuptools_scm >/dev/null 2>&1 \ + || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 \ + || true +} + +ensure_yq() { + if command -v yq &>/dev/null && [[ "$(yq --version 2>&1)" == *"mikefarah/yq"* ]]; then + return 0 + fi + + echo ">>> Installing yq..." + local yq_version="v4.40.5" + local yq_binary="yq_linux_arm64" + + case "$(uname -m)" in + x86_64) + yq_binary="yq_linux_amd64" + ;; + armv7*|armv6*|armhf) + yq_binary="yq_linux_arm" + ;; + aarch64|arm64) + yq_binary="yq_linux_arm64" + ;; + esac + + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}" 2>/dev/null \ + && chmod +x /usr/local/bin/yq +} + # --------------------------------------------------------------------------- # Virtual-environment helpers # --------------------------------------------------------------------------- @@ -103,13 +231,18 @@ elif command -v dialog &> /dev/null; then DIALOG="dialog" else echo "TUI interface requires whiptail or dialog." - if [ "$EUID" -eq 0 ]; then + if [ "$EUID" -eq 0 ] && ! is_buildroot; then echo "Installing whiptail..." apt-get update -qq && apt-get install -y whiptail DIALOG="whiptail" else echo "" - echo "Please install whiptail: sudo apt-get install -y whiptail" + if is_buildroot; then + echo "This Buildroot image is expected to ship with dialog preinstalled." + echo "Please rebuild the image with dialog enabled, or run from a system that has dialog." + else + echo "Please install whiptail: sudo apt-get install -y whiptail" + fi echo "Then run this script again." exit 1 fi @@ -326,44 +459,25 @@ install_repeater() { echo "" echo ">>> Creating service user..." - if ! id "$SERVICE_USER" &>/dev/null; then - useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER" - fi + ensure_service_user ( echo "10"; echo "# Adding user to hardware groups..." for grp in plugdev dialout gpio i2c spi; do - getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true + add_user_to_group "$SERVICE_USER" "$grp" done echo "20"; echo "# Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater echo "25"; echo "# Installing system dependencies..." - apt-get update -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev - # Install polkit (package name varies by distro version) - DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \ - || DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \ - || echo " Warning: Could not install polkit (sudo fallback will be used)" - # setuptools_scm needed for git version detection during build - pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true + install_system_packages + install_bootstrap_python_packages echo "28"; echo "# Creating virtual environment..." ensure_venv - # Install mikefarah yq v4 if not already installed - if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then - echo ">>> Installing yq..." - YQ_VERSION="v4.40.5" - YQ_BINARY="yq_linux_arm64" - if [[ "$(uname -m)" == "x86_64" ]]; then - YQ_BINARY="yq_linux_amd64" - elif [[ "$(uname -m)" == "armv7"* ]]; then - YQ_BINARY="yq_linux_arm" - fi - wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" 2>/dev/null && chmod +x /usr/local/bin/yq - fi + ensure_yq echo "29"; echo "# Installing files..." cp "$SCRIPT_DIR/manage.sh" "$INSTALL_DIR/" 2>/dev/null || true @@ -744,26 +858,9 @@ upgrade_repeater() { fi echo "[3/9] Updating system dependencies..." - apt-get update -qq - - apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev - # Install polkit (package name varies by distro version) - apt-get install -y policykit-1 2>/dev/null \ - || apt-get install -y polkitd pkexec 2>/dev/null \ - || echo " Warning: Could not install polkit (sudo fallback will be used)" - pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true - - # Install mikefarah yq v4 if not already installed - if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then - YQ_VERSION="v4.40.5" - YQ_BINARY="yq_linux_arm64" - if [[ "$(uname -m)" == "x86_64" ]]; then - YQ_BINARY="yq_linux_amd64" - elif [[ "$(uname -m)" == "armv7"* ]]; then - YQ_BINARY="yq_linux_arm" - fi - wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" && chmod +x /usr/local/bin/yq - fi + install_system_packages + install_bootstrap_python_packages + ensure_yq echo " ✓ Dependencies updated" echo "[4/9] Installing files..." @@ -784,7 +881,7 @@ upgrade_repeater() { echo "[5.5/9] Ensuring user groups and udev rules..." for grp in plugdev dialout gpio i2c spi; do - getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true + add_user_to_group "$SERVICE_USER" "$grp" done # Install/update CH341 udev rules SCRIPT_DIR_UPGRADE="$(cd "$(dirname "$0")" && pwd)" From 34fe07d7b0ceb2c1df141ed44a68215fcb857eb6 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:26:18 -0400 Subject: [PATCH 02/22] Split Buildroot flow into dedicated manager --- buildroot-manage.sh | 461 ++++++++++++++++++++++++++++++++++++++++++++ manage.sh | 191 +++++------------- 2 files changed, 508 insertions(+), 144 deletions(-) create mode 100644 buildroot-manage.sh diff --git a/buildroot-manage.sh b/buildroot-manage.sh new file mode 100644 index 0000000..22cd67a --- /dev/null +++ b/buildroot-manage.sh @@ -0,0 +1,461 @@ +#!/bin/bash +# Buildroot/Luckfox management entrypoint for pyMC Repeater + +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +INSTALL_DIR="/opt/pymc_repeater" +VENV_DIR="$INSTALL_DIR/venv" +VENV_PIP="$VENV_DIR/bin/pip" +VENV_PYTHON="$VENV_DIR/bin/python" +CONFIG_DIR="/etc/pymc_repeater" +LOG_DIR="/var/log/pymc_repeater" +DATA_DIR="/var/lib/pymc_repeater" +SERVICE_USER="repeater" +INIT_SCRIPT="/etc/init.d/S80pymc-repeater" +PIDFILE="/var/run/pymc-repeater.pid" +LOGFILE="$LOG_DIR/repeater.log" +SERVICE_NAME="pymc-repeater" +SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}" + +R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps" +R2_ENABLED=1 + +stage() { + printf '\n==> %s\n' "$1" +} + +info() { + printf ' - %s\n' "$1" +} + +warn() { + printf ' - %s\n' "$1" >&2 +} + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" +} + +is_buildroot() { + [ -f /etc/pymc-image-build-id ] && return 0 + [ -f /etc/os-release ] && grep -q '^ID=buildroot$' /etc/os-release 2>/dev/null && return 0 + return 1 +} + +ensure_root() { + [ "${EUID}" -eq 0 ] || fail "This command must be run as root." +} + +group_exists() { + grep -q "^$1:" /etc/group 2>/dev/null +} + +ensure_group_line() { + local group_name="$1" + local gid="$2" + group_exists "$group_name" && return 0 + printf '%s:x:%s:\n' "$group_name" "$gid" >> /etc/group +} + +ensure_service_user() { + if id "$SERVICE_USER" >/dev/null 2>&1; then + return 0 + fi + + if command -v useradd >/dev/null 2>&1; then + useradd --system --home "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER" + return 0 + fi + + ensure_group_line "$SERVICE_USER" 990 + printf '%s:x:990:990::%s:/sbin/nologin\n' "$SERVICE_USER" "$DATA_DIR" >> /etc/passwd + if [ -f /etc/shadow ]; then + printf '%s:!:19000:0:99999:7:::\n' "$SERVICE_USER" >> /etc/shadow + fi +} + +add_user_to_group() { + local user_name="$1" + local group_name="$2" + local current_line current_members gid escaped_line new_members + + group_exists "$group_name" || return 0 + current_line=$(grep "^${group_name}:" /etc/group 2>/dev/null || true) + [ -n "$current_line" ] || return 0 + current_members=$(printf '%s' "$current_line" | cut -d: -f4) + case ",${current_members}," in + *,"${user_name}",*) return 0 ;; + esac + + if [ -n "$current_members" ]; then + new_members="${current_members},${user_name}" + else + new_members="${user_name}" + fi + gid=$(printf '%s' "$current_line" | cut -d: -f3) + escaped_line=$(printf '%s\n' "$current_line" | sed 's/[].[^$\\*]/\\&/g') + sed -i "s/^${escaped_line}\$/${group_name}:x:${gid}:${new_members}/" /etc/group +} + +install_system_packages() { + if is_buildroot; then + info "Buildroot image detected; using preinstalled packages." + return 0 + fi + + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev +} + +ensure_yq() { + local yq_version="v4.40.5" + local yq_binary="yq_linux_arm64" + + if command -v yq >/dev/null 2>&1 && yq --version 2>&1 | grep -q 'mikefarah/yq'; then + return 0 + fi + + case "$(uname -m)" in + x86_64) yq_binary="yq_linux_amd64" ;; + armv7*|armv6*|armhf) yq_binary="yq_linux_arm" ;; + aarch64|arm64) yq_binary="yq_linux_arm64" ;; + esac + + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}" + chmod +x /usr/local/bin/yq +} + +ensure_venv() { + if [ ! -x "$VENV_PYTHON" ]; then + stage "Creating virtual environment" + python3 -m venv --system-site-packages "$VENV_DIR" + "$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true + fi +} + +preinstall_r2_wheels() { + local machine_arch arch_tag platform_tag py_tag wheel_base + + [ "$R2_ENABLED" -eq 1 ] || return 0 + + machine_arch=$(uname -m) + case "$machine_arch" in + aarch64) arch_tag="arm64"; platform_tag="aarch64" ;; + armv7l|armv7) arch_tag="armv7"; platform_tag="armv7l" ;; + x86_64) arch_tag="x86_64"; platform_tag="x86_64" ;; + *) return 0 ;; + esac + + py_tag=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311") + wheel_base="${R2_BASE_URL}/${arch_tag}/${platform_tag}/${py_tag}" + "$VENV_PIP" install --find-links "${wheel_base}/index.html" --no-cache-dir \ + "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" >/dev/null 2>&1 || true +} + +create_init_script() { + cat > "$INIT_SCRIPT" </dev/null; then + echo "${SERVICE_NAME} is already running." + return 0 + fi + start-stop-daemon --start --quiet --background --make-pidfile --pidfile "\$PIDFILE" \\ + --chuid "\$RUN_AS" --exec /bin/sh -- -c "cd \"\$WORKDIR\" && exec \"\$DAEMON\" -m repeater.main --config \"\$CONFIG_FILE\" >>\"\$LOGFILE\" 2>&1" +} + +stop() { + if [ ! -f "\$PIDFILE" ]; then + echo "${SERVICE_NAME} is not running." + return 0 + fi + start-stop-daemon --stop --quiet --retry 5 --pidfile "\$PIDFILE" || true + rm -f "\$PIDFILE" +} + +status() { + if [ -f "\$PIDFILE" ] && kill -0 "$(cat "\$PIDFILE")" 2>/dev/null; then + echo "${SERVICE_NAME} is running." + return 0 + fi + echo "${SERVICE_NAME} is stopped." + return 1 +} + +case "\${1:-}" in + start) start ;; + stop) stop ;; + restart) stop; start ;; + status) status ;; + *) + echo "Usage: \$0 {start|stop|restart|status}" + exit 1 + ;; +esac +EOF + chmod 0755 "$INIT_SCRIPT" +} + +service_exists() { + [ -x "$INIT_SCRIPT" ] +} + +is_installed() { + [ -d "$INSTALL_DIR" ] && service_exists +} + +is_running() { + [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null +} + +get_version() { + if [ -x "$VENV_PYTHON" ]; then + "$VENV_PYTHON" -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null || echo "not installed" + else + echo "not installed" + fi +} + +doctor() { + stage "Checking Buildroot image baseline" + + for cmd in bash git python3 dialog jq wget sudo sqlite3 start-stop-daemon; do + if command -v "$cmd" >/dev/null 2>&1; then + info "found $cmd" + else + warn "missing $cmd" + fi + done + + if python3 -m venv --help >/dev/null 2>&1; then + info "python venv support available" + else + warn "python venv support missing" + fi + + if python3 - <<'PY' +modules = [ + "sqlite3", + "yaml", + "cherrypy", + "cherrypy_cors", + "autocommand", + "jaraco.collections", + "jaraco.text", + "paho.mqtt.client", + "psutil", + "jwt", + "ws4py", + "nacl", + "periphery", + "spidev", + "serial", + "usb", + "Crypto", +] +for module in modules: + __import__(module) +PY + then + info "python runtime packages are present" + else + warn "python runtime packages are missing" + fi + + for path in /dev/spidev* /dev/gpiochip*; do + [ -e "$path" ] && info "detected $path" + done +} + +install_repeater() { + local git_version machine_arch arch_tag platform_tag py_tag wheel_base ip_address + + ensure_root + stage "Preparing Buildroot installation" + install_system_packages + ensure_service_user + + for grp in plugdev dialout gpio i2c spi; do + add_user_to_group "$SERVICE_USER" "$grp" + done + + mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" "$DATA_DIR/.config/pymc_repeater" + chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" + chmod 755 "$INSTALL_DIR" "$DATA_DIR" + chmod 750 "$CONFIG_DIR" "$LOG_DIR" + + cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml.example" + [ -f "$CONFIG_DIR/config.yaml" ] || cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml" + cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true + cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true + + ensure_yq + ensure_venv + + if [ -d "$SCRIPT_DIR/.git" ]; then + git -C "$SCRIPT_DIR" fetch --tags 2>/dev/null || true + git_version=$(python3 -m setuptools_scm 2>/dev/null || echo "1.0.5") + export SETUPTOOLS_SCM_PRETEND_VERSION="$git_version" + else + export SETUPTOOLS_SCM_PRETEND_VERSION="1.0.5" + fi + + if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then + export PIP_ONLY_BINARY=pycryptodome,cffi,PyNaCl,psutil + fi + + preinstall_r2_wheels + + stage "Installing pyMC Repeater into venv" + (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + + create_init_script + + stage "Starting service" + "$INIT_SCRIPT" restart + + ip_address=$(hostname -I | awk '{print $1}') + if is_running; then + printf '\nService is running on: http://%s:8000\n' "${ip_address}" + else + fail "Installation completed but the service failed to start. Check: sh $0 logs" + fi +} + +upgrade_repeater() { + ensure_root + is_installed || fail "Service is not installed." + + ensure_venv + preinstall_r2_wheels + + stage "Upgrading pyMC Repeater" + (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + "$INIT_SCRIPT" restart +} + +uninstall_repeater() { + ensure_root + + stage "Removing service" + service_exists && "$INIT_SCRIPT" stop || true + rm -f "$INIT_SCRIPT" + rm -rf "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" +} + +manage_service() { + local action="$1" + ensure_root + service_exists || fail "Service is not installed." + "$INIT_SCRIPT" "$action" +} + +show_status() { + local ip_address version + version=$(get_version) + ip_address=$(hostname -I | awk '{print $1}') + + if ! is_installed; then + printf 'Installation Status: Not Installed\n' + return 0 + fi + + printf 'Installation Status: Installed\n' + printf 'Version: %s\n' "$version" + printf 'Install Directory: %s\n' "$INSTALL_DIR" + printf 'Config Directory: %s\n' "$CONFIG_DIR" + printf 'Log File: %s\n' "$LOGFILE" + printf 'Dashboard: http://%s:8000\n' "$ip_address" + if is_running; then + printf 'Service Status: Running\n' + else + printf 'Service Status: Stopped\n' + fi +} + +show_logs() { + mkdir -p "$LOG_DIR" + touch "$LOGFILE" + tail -f "$LOGFILE" +} + +run_debug() { + ensure_root + mkdir -p "$LOG_DIR" "$DATA_DIR" + exec "$VENV_PYTHON" -m repeater.main --config "$CONFIG_DIR/config.yaml" +} + +delegate_to_stock_manage() { + exec bash "$SCRIPT_DIR/manage.sh" "$@" +} + +usage() { + cat <<'EOF' +Usage: bash buildroot-manage.sh + +Commands: + doctor Check Buildroot/Luckfox prerequisites + install Install pyMC Repeater on the Buildroot image + upgrade Upgrade the Buildroot installation from the checked-out repo + config Run the stock interactive config flow + start Start the init.d service + stop Stop the init.d service + restart Restart the init.d service + status Show Buildroot service status + logs Tail the Buildroot log file + uninstall Remove the Buildroot installation + debug Run repeater.main in the foreground +EOF +} + +case "${1:-}" in + doctor) + doctor + ;; + install) + install_repeater + ;; + upgrade) + upgrade_repeater + ;; + config) + shift + delegate_to_stock_manage config "$@" + ;; + start|stop|restart) + manage_service "$1" + ;; + status) + show_status + ;; + logs) + show_logs + ;; + uninstall) + uninstall_repeater + ;; + debug) + run_debug + ;; + ""|help|-h|--help) + usage + ;; + *) + fail "Unknown command: ${1}" + ;; +esac diff --git a/manage.sh b/manage.sh index 50694f4..6659397 100755 --- a/manage.sh +++ b/manage.sh @@ -17,134 +17,6 @@ SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}" R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps" R2_ENABLED=1 # Set to 0 to disable R2 wheels and always build from source -# --------------------------------------------------------------------------- -# Platform detection / compatibility helpers -# --------------------------------------------------------------------------- - -is_buildroot() { - [ -f /etc/pymc-image-build-id ] && return 0 - [ -f /etc/os-release ] && grep -q '^ID=buildroot$' /etc/os-release 2>/dev/null && return 0 - return 1 -} - -group_exists() { - local group_name="$1" - if command -v getent >/dev/null 2>&1; then - getent group "$group_name" >/dev/null 2>&1 - else - grep -q "^${group_name}:" /etc/group 2>/dev/null - fi -} - -ensure_group_line() { - local group_name="$1" - local gid="$2" - group_exists "$group_name" && return 0 - printf '%s:x:%s:\n' "$group_name" "$gid" >> /etc/group -} - -ensure_service_user() { - if id "$SERVICE_USER" >/dev/null 2>&1; then - return 0 - fi - - if command -v useradd >/dev/null 2>&1; then - useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER" - return 0 - fi - - if is_buildroot; then - ensure_group_line "$SERVICE_USER" 990 - printf '%s:x:990:990::/var/lib/pymc_repeater:/sbin/nologin\n' "$SERVICE_USER" >> /etc/passwd - if [ -f /etc/shadow ]; then - printf '%s:!:19000:0:99999:7:::\n' "$SERVICE_USER" >> /etc/shadow - fi - return 0 - fi - - echo "Error: useradd is not available on this system." >&2 - exit 1 -} - -add_user_to_group() { - local user_name="$1" - local group_name="$2" - - group_exists "$group_name" || return 0 - - if command -v usermod >/dev/null 2>&1; then - usermod -a -G "$group_name" "$user_name" 2>/dev/null || true - return 0 - fi - - if is_buildroot; then - local current_line current_members gid new_members escaped_line - current_line=$(grep "^${group_name}:" /etc/group 2>/dev/null || true) - [ -n "$current_line" ] || return 0 - current_members=$(printf '%s' "$current_line" | cut -d: -f4) - case ",${current_members}," in - *,"${user_name}",*) return 0 ;; - esac - if [ -n "$current_members" ]; then - new_members="${current_members},${user_name}" - else - new_members="${user_name}" - fi - gid=$(printf '%s' "$current_line" | cut -d: -f3) - escaped_line=$(printf '%s\n' "$current_line" | sed 's/[].[^$\\*]/\\&/g') - sed -i "s/^${escaped_line}\$/${group_name}:x:${gid}:${new_members}/" /etc/group - fi -} - -install_system_packages() { - if is_buildroot; then - echo " Buildroot image detected; using preinstalled system packages." - return 0 - fi - - apt-get update -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev - DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \ - || DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \ - || echo " Warning: Could not install polkit (sudo fallback will be used)" -} - -install_bootstrap_python_packages() { - if is_buildroot; then - return 0 - fi - - pip install --break-system-packages setuptools_scm >/dev/null 2>&1 \ - || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 \ - || true -} - -ensure_yq() { - if command -v yq &>/dev/null && [[ "$(yq --version 2>&1)" == *"mikefarah/yq"* ]]; then - return 0 - fi - - echo ">>> Installing yq..." - local yq_version="v4.40.5" - local yq_binary="yq_linux_arm64" - - case "$(uname -m)" in - x86_64) - yq_binary="yq_linux_amd64" - ;; - armv7*|armv6*|armhf) - yq_binary="yq_linux_arm" - ;; - aarch64|arm64) - yq_binary="yq_linux_arm64" - ;; - esac - - wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}" 2>/dev/null \ - && chmod +x /usr/local/bin/yq -} - # --------------------------------------------------------------------------- # Virtual-environment helpers # --------------------------------------------------------------------------- @@ -231,18 +103,13 @@ elif command -v dialog &> /dev/null; then DIALOG="dialog" else echo "TUI interface requires whiptail or dialog." - if [ "$EUID" -eq 0 ] && ! is_buildroot; then + if [ "$EUID" -eq 0 ]; then echo "Installing whiptail..." apt-get update -qq && apt-get install -y whiptail DIALOG="whiptail" else echo "" - if is_buildroot; then - echo "This Buildroot image is expected to ship with dialog preinstalled." - echo "Please rebuild the image with dialog enabled, or run from a system that has dialog." - else - echo "Please install whiptail: sudo apt-get install -y whiptail" - fi + echo "Please install whiptail: sudo apt-get install -y whiptail" echo "Then run this script again." exit 1 fi @@ -459,25 +326,44 @@ install_repeater() { echo "" echo ">>> Creating service user..." - ensure_service_user + if ! id "$SERVICE_USER" &>/dev/null; then + useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER" + fi ( echo "10"; echo "# Adding user to hardware groups..." for grp in plugdev dialout gpio i2c spi; do - add_user_to_group "$SERVICE_USER" "$grp" + getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true done echo "20"; echo "# Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater echo "25"; echo "# Installing system dependencies..." - install_system_packages - install_bootstrap_python_packages + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev + # Install polkit (package name varies by distro version) + DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \ + || DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \ + || echo " Warning: Could not install polkit (sudo fallback will be used)" + # setuptools_scm needed for git version detection during build + pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true echo "28"; echo "# Creating virtual environment..." ensure_venv - ensure_yq + # Install mikefarah yq v4 if not already installed + if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then + echo ">>> Installing yq..." + YQ_VERSION="v4.40.5" + YQ_BINARY="yq_linux_arm64" + if [[ "$(uname -m)" == "x86_64" ]]; then + YQ_BINARY="yq_linux_amd64" + elif [[ "$(uname -m)" == "armv7"* ]]; then + YQ_BINARY="yq_linux_arm" + fi + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" 2>/dev/null && chmod +x /usr/local/bin/yq + fi echo "29"; echo "# Installing files..." cp "$SCRIPT_DIR/manage.sh" "$INSTALL_DIR/" 2>/dev/null || true @@ -858,9 +744,26 @@ upgrade_repeater() { fi echo "[3/9] Updating system dependencies..." - install_system_packages - install_bootstrap_python_packages - ensure_yq + apt-get update -qq + + apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev + # Install polkit (package name varies by distro version) + apt-get install -y policykit-1 2>/dev/null \ + || apt-get install -y polkitd pkexec 2>/dev/null \ + || echo " Warning: Could not install polkit (sudo fallback will be used)" + pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true + + # Install mikefarah yq v4 if not already installed + if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then + YQ_VERSION="v4.40.5" + YQ_BINARY="yq_linux_arm64" + if [[ "$(uname -m)" == "x86_64" ]]; then + YQ_BINARY="yq_linux_amd64" + elif [[ "$(uname -m)" == "armv7"* ]]; then + YQ_BINARY="yq_linux_arm" + fi + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" && chmod +x /usr/local/bin/yq + fi echo " ✓ Dependencies updated" echo "[4/9] Installing files..." @@ -881,7 +784,7 @@ upgrade_repeater() { echo "[5.5/9] Ensuring user groups and udev rules..." for grp in plugdev dialout gpio i2c spi; do - add_user_to_group "$SERVICE_USER" "$grp" + getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true done # Install/update CH341 udev rules SCRIPT_DIR_UPGRADE="$(cd "$(dirname "$0")" && pwd)" From 4d6993c9e1fd9381c28702c951bc5c00ad400cc1 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:30:18 -0400 Subject: [PATCH 03/22] Allow Buildroot manager to run under sh --- buildroot-manage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 22cd67a..e9443ef 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -49,7 +49,7 @@ is_buildroot() { } ensure_root() { - [ "${EUID}" -eq 0 ] || fail "This command must be run as root." + [ "$(id -u 2>/dev/null || echo 1)" -eq 0 ] || fail "This command must be run as root." } group_exists() { From b58578acd585456d963f2fac1ff81194d78690ae Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:31:46 -0400 Subject: [PATCH 04/22] Drop yq dependency from Buildroot install flow --- buildroot-manage.sh | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index e9443ef..ad40e3b 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -114,24 +114,6 @@ install_system_packages() { libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev } -ensure_yq() { - local yq_version="v4.40.5" - local yq_binary="yq_linux_arm64" - - if command -v yq >/dev/null 2>&1 && yq --version 2>&1 | grep -q 'mikefarah/yq'; then - return 0 - fi - - case "$(uname -m)" in - x86_64) yq_binary="yq_linux_amd64" ;; - armv7*|armv6*|armhf) yq_binary="yq_linux_arm" ;; - aarch64|arm64) yq_binary="yq_linux_arm64" ;; - esac - - wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}" - chmod +x /usr/local/bin/yq -} - ensure_venv() { if [ ! -x "$VENV_PYTHON" ]; then stage "Creating virtual environment" @@ -304,7 +286,6 @@ install_repeater() { cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true - ensure_yq ensure_venv if [ -d "$SCRIPT_DIR/.git" ]; then From 6c0f4fb8423754ef95bfeb6e1897eced690d0062 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:35:16 -0400 Subject: [PATCH 05/22] Fix init script generation for BusyBox --- buildroot-manage.sh | 61 +++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index ad40e3b..01e5bf1 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -142,57 +142,70 @@ preinstall_r2_wheels() { } create_init_script() { - cat > "$INIT_SCRIPT" < "$INIT_SCRIPT" <<'EOF' #!/bin/sh -DAEMON="${VENV_PYTHON}" -PIDFILE="${PIDFILE}" -LOGFILE="${LOGFILE}" -WORKDIR="${DATA_DIR}" -CONFIG_FILE="${CONFIG_DIR}/config.yaml" -RUN_AS="${SERVICE_USER}" +DAEMON="__DAEMON__" +PIDFILE="__PIDFILE__" +LOGFILE="__LOGFILE__" +WORKDIR="__WORKDIR__" +CONFIG_FILE="__CONFIG_FILE__" +RUN_AS="__RUN_AS__" start() { - mkdir -p "$(dirname "\$PIDFILE")" "$(dirname "\$LOGFILE")" "\$WORKDIR" - if [ -f "\$PIDFILE" ] && kill -0 "$(cat "\$PIDFILE")" 2>/dev/null; then - echo "${SERVICE_NAME} is already running." + mkdir -p "$(dirname "$PIDFILE")" "$(dirname "$LOGFILE")" "$WORKDIR" + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "__SERVICE_NAME__ is already running." return 0 fi - start-stop-daemon --start --quiet --background --make-pidfile --pidfile "\$PIDFILE" \\ - --chuid "\$RUN_AS" --exec /bin/sh -- -c "cd \"\$WORKDIR\" && exec \"\$DAEMON\" -m repeater.main --config \"\$CONFIG_FILE\" >>\"\$LOGFILE\" 2>&1" + start-stop-daemon --start --quiet --background --make-pidfile --pidfile "$PIDFILE" \ + --chuid "$RUN_AS" --exec /bin/sh -- -c "cd \"$WORKDIR\" && exec \"$DAEMON\" -m repeater.main --config \"$CONFIG_FILE\" >>\"$LOGFILE\" 2>&1" } stop() { - if [ ! -f "\$PIDFILE" ]; then - echo "${SERVICE_NAME} is not running." + if [ ! -f "$PIDFILE" ]; then + echo "__SERVICE_NAME__ is not running." return 0 fi - start-stop-daemon --stop --quiet --retry 5 --pidfile "\$PIDFILE" || true - rm -f "\$PIDFILE" + start-stop-daemon --stop --quiet --retry 5 --pidfile "$PIDFILE" >/dev/null 2>&1 || true + rm -f "$PIDFILE" } status() { - if [ -f "\$PIDFILE" ] && kill -0 "$(cat "\$PIDFILE")" 2>/dev/null; then - echo "${SERVICE_NAME} is running." + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "__SERVICE_NAME__ is running." return 0 fi - echo "${SERVICE_NAME} is stopped." + echo "__SERVICE_NAME__ is stopped." return 1 } -case "\${1:-}" in +case "${1:-}" in start) start ;; stop) stop ;; restart) stop; start ;; status) status ;; *) - echo "Usage: \$0 {start|stop|restart|status}" + echo "Usage: $0 {start|stop|restart|status}" exit 1 ;; esac EOF + sed -i \ + -e "s|__DAEMON__|${VENV_PYTHON}|g" \ + -e "s|__PIDFILE__|${PIDFILE}|g" \ + -e "s|__LOGFILE__|${LOGFILE}|g" \ + -e "s|__WORKDIR__|${DATA_DIR}|g" \ + -e "s|__CONFIG_FILE__|${CONFIG_DIR}/config.yaml|g" \ + -e "s|__RUN_AS__|${SERVICE_USER}|g" \ + -e "s|__SERVICE_NAME__|${SERVICE_NAME}|g" \ + "$INIT_SCRIPT" chmod 0755 "$INIT_SCRIPT" } +get_primary_ip() { + ip -o -4 addr show dev eth0 2>/dev/null | awk 'NR==1 { sub(/\/.*/, "", $4); print $4; exit }' +} + service_exists() { [ -x "$INIT_SCRIPT" ] } @@ -265,7 +278,7 @@ PY } install_repeater() { - local git_version machine_arch arch_tag platform_tag py_tag wheel_base ip_address + local git_version ip_address ensure_root stage "Preparing Buildroot installation" @@ -310,7 +323,7 @@ install_repeater() { stage "Starting service" "$INIT_SCRIPT" restart - ip_address=$(hostname -I | awk '{print $1}') + ip_address=$(get_primary_ip) if is_running; then printf '\nService is running on: http://%s:8000\n' "${ip_address}" else @@ -349,7 +362,7 @@ manage_service() { show_status() { local ip_address version version=$(get_version) - ip_address=$(hostname -I | awk '{print $1}') + ip_address=$(get_primary_ip) if ! is_installed; then printf 'Installation Status: Not Installed\n' From e36b477230177b55e60f30252bf89445f440a43b Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:14:45 -0400 Subject: [PATCH 06/22] Run Buildroot service as root --- buildroot-manage.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 01e5bf1..5873f3e 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -11,7 +11,7 @@ VENV_PYTHON="$VENV_DIR/bin/python" CONFIG_DIR="/etc/pymc_repeater" LOG_DIR="/var/log/pymc_repeater" DATA_DIR="/var/lib/pymc_repeater" -SERVICE_USER="repeater" +SERVICE_USER="root" INIT_SCRIPT="/etc/init.d/S80pymc-repeater" PIDFILE="/var/run/pymc-repeater.pid" LOGFILE="$LOG_DIR/repeater.log" @@ -64,6 +64,10 @@ ensure_group_line() { } ensure_service_user() { + if [ "$SERVICE_USER" = "root" ]; then + return 0 + fi + if id "$SERVICE_USER" >/dev/null 2>&1; then return 0 fi @@ -85,6 +89,10 @@ add_user_to_group() { local group_name="$2" local current_line current_members gid escaped_line new_members + if [ "$user_name" = "root" ]; then + return 0 + fi + group_exists "$group_name" || return 0 current_line=$(grep "^${group_name}:" /etc/group 2>/dev/null || true) [ -n "$current_line" ] || return 0 @@ -285,12 +293,8 @@ install_repeater() { install_system_packages ensure_service_user - for grp in plugdev dialout gpio i2c spi; do - add_user_to_group "$SERVICE_USER" "$grp" - done - mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" "$DATA_DIR/.config/pymc_repeater" - chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" + chown -R root:root "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" chmod 755 "$INSTALL_DIR" "$DATA_DIR" chmod 750 "$CONFIG_DIR" "$LOG_DIR" From bc809c30217ecd26a719bb0eb40903d202ad71d3 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:01:03 -0400 Subject: [PATCH 07/22] Explain Buildroot install progress --- buildroot-manage.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 5873f3e..04dfe83 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -125,8 +125,14 @@ install_system_packages() { ensure_venv() { if [ ! -x "$VENV_PYTHON" ]; then stage "Creating virtual environment" + info "Creating $VENV_DIR" + info "This can take a minute on Buildroot flash storage." python3 -m venv --system-site-packages "$VENV_DIR" + info "Bootstrapping pip, setuptools, and wheel" "$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true + info "Virtual environment is ready" + else + info "Using existing virtual environment at $VENV_DIR" fi } @@ -145,8 +151,11 @@ preinstall_r2_wheels() { py_tag=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311") wheel_base="${R2_BASE_URL}/${arch_tag}/${platform_tag}/${py_tag}" + stage "Checking optional wheel cache" + info "Trying prebuilt Python wheels for $arch_tag/$py_tag" "$VENV_PIP" install --find-links "${wheel_base}/index.html" --no-cache-dir \ "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" >/dev/null 2>&1 || true + info "Wheel cache step finished" } create_init_script() { @@ -293,6 +302,10 @@ install_repeater() { install_system_packages ensure_service_user + stage "Preparing directories and config" + info "Install dir: $INSTALL_DIR" + info "Config dir: $CONFIG_DIR" + info "Data dir: $DATA_DIR" mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" "$DATA_DIR/.config/pymc_repeater" chown -R root:root "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" chmod 755 "$INSTALL_DIR" "$DATA_DIR" @@ -306,22 +319,30 @@ install_repeater() { ensure_venv if [ -d "$SCRIPT_DIR/.git" ]; then + stage "Inspecting checked-out repo version" + info "Fetching tags for setuptools_scm version detection" git -C "$SCRIPT_DIR" fetch --tags 2>/dev/null || true git_version=$(python3 -m setuptools_scm 2>/dev/null || echo "1.0.5") export SETUPTOOLS_SCM_PRETEND_VERSION="$git_version" + info "Using version: $git_version" else export SETUPTOOLS_SCM_PRETEND_VERSION="1.0.5" + info "Using fallback version: 1.0.5" fi if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then export PIP_ONLY_BINARY=pycryptodome,cffi,PyNaCl,psutil + info "Non-Luckfox board detected; preferring binary wheels for heavy packages" fi preinstall_r2_wheels stage "Installing pyMC Repeater into venv" + info "Running pip install for the checked-out repo" + info "This is the slowest step and may take several minutes." (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + stage "Writing Buildroot init service" create_init_script stage "Starting service" From 95918dc43d2504d1988f9392a957d2057b40acde Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:03:57 -0400 Subject: [PATCH 08/22] Prefer Rightup wheels on Buildroot install --- buildroot-manage.sh | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 04dfe83..78b9ee0 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -136,28 +136,51 @@ ensure_venv() { fi } -preinstall_r2_wheels() { +get_r2_wheel_base() { local machine_arch arch_tag platform_tag py_tag wheel_base - [ "$R2_ENABLED" -eq 1 ] || return 0 + [ "$R2_ENABLED" -eq 1 ] || return 1 machine_arch=$(uname -m) case "$machine_arch" in aarch64) arch_tag="arm64"; platform_tag="aarch64" ;; armv7l|armv7) arch_tag="armv7"; platform_tag="armv7l" ;; x86_64) arch_tag="x86_64"; platform_tag="x86_64" ;; - *) return 0 ;; + *) return 1 ;; esac py_tag=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311") wheel_base="${R2_BASE_URL}/${arch_tag}/${platform_tag}/${py_tag}" + printf '%s\n' "$wheel_base" +} + +preinstall_r2_wheels() { + local wheel_base + + wheel_base=$(get_r2_wheel_base 2>/dev/null || true) + [ -n "$wheel_base" ] || return 0 + stage "Checking optional wheel cache" - info "Trying prebuilt Python wheels for $arch_tag/$py_tag" + info "Trying prebuilt Python wheels from ${wheel_base}/index.html" "$VENV_PIP" install --find-links "${wheel_base}/index.html" --no-cache-dir \ "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" >/dev/null 2>&1 || true info "Wheel cache step finished" } +install_repo_into_venv() { + local wheel_base + + wheel_base=$(get_r2_wheel_base 2>/dev/null || true) + if [ -n "$wheel_base" ]; then + info "Final install will prefer Rightup prebuilt wheels" + info "Wheel index: ${wheel_base}/index.html" + (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir \ + --find-links "${wheel_base}/index.html" --prefer-binary .[hardware]) + else + (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + fi +} + create_init_script() { cat > "$INIT_SCRIPT" <<'EOF' #!/bin/sh @@ -340,7 +363,7 @@ install_repeater() { stage "Installing pyMC Repeater into venv" info "Running pip install for the checked-out repo" info "This is the slowest step and may take several minutes." - (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + install_repo_into_venv stage "Writing Buildroot init service" create_init_script @@ -364,7 +387,7 @@ upgrade_repeater() { preinstall_r2_wheels stage "Upgrading pyMC Repeater" - (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + install_repo_into_venv "$INIT_SCRIPT" restart } From ba2136dfa66d3c7849bab28afeccf6076082116c Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:10:50 -0400 Subject: [PATCH 09/22] Avoid source builds on Buildroot install --- buildroot-manage.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 78b9ee0..9175260 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -20,6 +20,7 @@ SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}" R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps" R2_ENABLED=1 +BINARY_ONLY_PACKAGES="pyyaml,pycryptodome,cffi,PyNaCl,psutil" stage() { printf '\n==> %s\n' "$1" @@ -162,7 +163,7 @@ preinstall_r2_wheels() { stage "Checking optional wheel cache" info "Trying prebuilt Python wheels from ${wheel_base}/index.html" - "$VENV_PIP" install --find-links "${wheel_base}/index.html" --no-cache-dir \ + "$VENV_PIP" install --find-links "${wheel_base}/index.html" --only-binary=:all: --no-cache-dir \ "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" >/dev/null 2>&1 || true info "Wheel cache step finished" } @@ -175,9 +176,11 @@ install_repo_into_venv() { info "Final install will prefer Rightup prebuilt wheels" info "Wheel index: ${wheel_base}/index.html" (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir \ - --find-links "${wheel_base}/index.html" --prefer-binary .[hardware]) + --find-links "${wheel_base}/index.html" --prefer-binary \ + --only-binary "${BINARY_ONLY_PACKAGES}" --no-build-isolation .[hardware]) else - (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]) + (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir \ + --only-binary "${BINARY_ONLY_PACKAGES}" --no-build-isolation .[hardware]) fi } @@ -354,8 +357,11 @@ install_repeater() { fi if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then - export PIP_ONLY_BINARY=pycryptodome,cffi,PyNaCl,psutil + export PIP_ONLY_BINARY="${BINARY_ONLY_PACKAGES}" info "Non-Luckfox board detected; preferring binary wheels for heavy packages" + else + export PIP_ONLY_BINARY="${BINARY_ONLY_PACKAGES}" + info "Buildroot install will use binary-only wheels for heavy packages" fi preinstall_r2_wheels From 0713b571d8dc288b37a677917cea4caaacb29bbe Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:58:13 -0400 Subject: [PATCH 10/22] Install Buildroot deps from wheel sources --- buildroot-manage.sh | 92 +++++++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 9175260..2d571b7 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -19,8 +19,33 @@ SERVICE_NAME="pymc-repeater" SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}" R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps" +PIWHEELS_INDEX_URL="https://www.piwheels.org/simple" R2_ENABLED=1 -BINARY_ONLY_PACKAGES="pyyaml,pycryptodome,cffi,PyNaCl,psutil" +PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/pyMC_core.git}" +PYMC_CORE_REF="${PYMC_CORE_REF:-dev}" +WHEEL_DEPENDENCIES=( + "pyyaml>=6.0.0" + "cherrypy>=18.0.0" + "cherrypy-cors==1.7.0" + "paho-mqtt>=1.6.0" + "psutil>=5.9.0" + "pyjwt>=2.8.0" + "ws4py>=0.6.0" + "pycryptodome>=3.23.0" + "PyNaCl>=1.5.0" + "cffi" + "pyserial" + "pyusb" + "spidev" + "python-periphery" + "autocommand" + "jaraco.collections" + "jaraco.text" + "jaraco.context" + "tempora" + "zc.lockfile" + "httpagentparser>=1.5" +) stage() { printf '\n==> %s\n' "$1" @@ -168,20 +193,47 @@ preinstall_r2_wheels() { info "Wheel cache step finished" } -install_repo_into_venv() { +install_buildroot_dependencies() { local wheel_base wheel_base=$(get_r2_wheel_base 2>/dev/null || true) + stage "Installing Python dependency wheels" if [ -n "$wheel_base" ]; then - info "Final install will prefer Rightup prebuilt wheels" - info "Wheel index: ${wheel_base}/index.html" - (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir \ - --find-links "${wheel_base}/index.html" --prefer-binary \ - --only-binary "${BINARY_ONLY_PACKAGES}" --no-build-isolation .[hardware]) - else - (cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir \ - --only-binary "${BINARY_ONLY_PACKAGES}" --no-build-isolation .[hardware]) + info "Using Rightup wheels: ${wheel_base}/index.html" fi + info "Using piwheels fallback: ${PIWHEELS_INDEX_URL}" + + if [ -n "$wheel_base" ]; then + "$VENV_PIP" install --upgrade --no-cache-dir --only-binary=:all: \ + --find-links "${wheel_base}/index.html" \ + --extra-index-url "${PIWHEELS_INDEX_URL}" \ + "${WHEEL_DEPENDENCIES[@]}" + else + "$VENV_PIP" install --upgrade --no-cache-dir --only-binary=:all: \ + --extra-index-url "${PIWHEELS_INDEX_URL}" \ + "${WHEEL_DEPENDENCIES[@]}" + fi +} + +install_core_into_venv() { + local core_repo core_spec + + core_repo="$PYMC_CORE_REPO" + case "$core_repo" in + *.git) ;; + *) core_repo="${core_repo}.git" ;; + esac + core_spec="pyMC_core[hardware] @ git+${core_repo}@${PYMC_CORE_REF}" + stage "Installing pyMC_core" + info "Repo: ${PYMC_CORE_REPO}" + info "Ref: ${PYMC_CORE_REF}" + "$VENV_PIP" install --upgrade --no-cache-dir --no-deps --no-build-isolation "$core_spec" +} + +install_repeater_package() { + stage "Installing pyMC Repeater into venv" + info "Installing checked-out repo without re-resolving dependencies" + "$VENV_PIP" install --upgrade --no-cache-dir --no-deps --no-build-isolation "$SCRIPT_DIR" } create_init_script() { @@ -356,20 +408,10 @@ install_repeater() { info "Using fallback version: 1.0.5" fi - if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then - export PIP_ONLY_BINARY="${BINARY_ONLY_PACKAGES}" - info "Non-Luckfox board detected; preferring binary wheels for heavy packages" - else - export PIP_ONLY_BINARY="${BINARY_ONLY_PACKAGES}" - info "Buildroot install will use binary-only wheels for heavy packages" - fi - preinstall_r2_wheels - - stage "Installing pyMC Repeater into venv" - info "Running pip install for the checked-out repo" - info "This is the slowest step and may take several minutes." - install_repo_into_venv + install_buildroot_dependencies + install_core_into_venv + install_repeater_package stage "Writing Buildroot init service" create_init_script @@ -393,7 +435,9 @@ upgrade_repeater() { preinstall_r2_wheels stage "Upgrading pyMC Repeater" - install_repo_into_venv + install_buildroot_dependencies + install_core_into_venv + install_repeater_package "$INIT_SCRIPT" restart } From 07dc287f50ff89fceab8d824215b219715441688 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:58:58 -0400 Subject: [PATCH 11/22] Keep Buildroot manager sh-compatible --- buildroot-manage.sh | 56 +++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 2d571b7..141b296 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -23,29 +23,31 @@ PIWHEELS_INDEX_URL="https://www.piwheels.org/simple" R2_ENABLED=1 PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/pyMC_core.git}" PYMC_CORE_REF="${PYMC_CORE_REF:-dev}" -WHEEL_DEPENDENCIES=( - "pyyaml>=6.0.0" - "cherrypy>=18.0.0" - "cherrypy-cors==1.7.0" - "paho-mqtt>=1.6.0" - "psutil>=5.9.0" - "pyjwt>=2.8.0" - "ws4py>=0.6.0" - "pycryptodome>=3.23.0" - "PyNaCl>=1.5.0" - "cffi" - "pyserial" - "pyusb" - "spidev" - "python-periphery" - "autocommand" - "jaraco.collections" - "jaraco.text" - "jaraco.context" - "tempora" - "zc.lockfile" - "httpagentparser>=1.5" -) +set_wheel_dependencies() { + set -- \ + "pyyaml>=6.0.0" \ + "cherrypy>=18.0.0" \ + "cherrypy-cors==1.7.0" \ + "paho-mqtt>=1.6.0" \ + "psutil>=5.9.0" \ + "pyjwt>=2.8.0" \ + "ws4py>=0.6.0" \ + "pycryptodome>=3.23.0" \ + "PyNaCl>=1.5.0" \ + "cffi" \ + "pyserial" \ + "pyusb" \ + "spidev" \ + "python-periphery" \ + "autocommand" \ + "jaraco.collections" \ + "jaraco.text" \ + "jaraco.context" \ + "tempora" \ + "zc.lockfile" \ + "httpagentparser>=1.5" + printf '%s\n' "$@" +} stage() { printf '\n==> %s\n' "$1" @@ -195,8 +197,10 @@ preinstall_r2_wheels() { install_buildroot_dependencies() { local wheel_base + local deps wheel_base=$(get_r2_wheel_base 2>/dev/null || true) + deps=$(set_wheel_dependencies) stage "Installing Python dependency wheels" if [ -n "$wheel_base" ]; then info "Using Rightup wheels: ${wheel_base}/index.html" @@ -204,14 +208,16 @@ install_buildroot_dependencies() { info "Using piwheels fallback: ${PIWHEELS_INDEX_URL}" if [ -n "$wheel_base" ]; then + # shellcheck disable=SC2086 "$VENV_PIP" install --upgrade --no-cache-dir --only-binary=:all: \ --find-links "${wheel_base}/index.html" \ --extra-index-url "${PIWHEELS_INDEX_URL}" \ - "${WHEEL_DEPENDENCIES[@]}" + $deps else + # shellcheck disable=SC2086 "$VENV_PIP" install --upgrade --no-cache-dir --only-binary=:all: \ --extra-index-url "${PIWHEELS_INDEX_URL}" \ - "${WHEEL_DEPENDENCIES[@]}" + $deps fi } From a6818367e8a465204318d50529d2e73ec1bb6fff Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:04:15 -0400 Subject: [PATCH 12/22] Fail fast on unusable Buildroot native modules --- buildroot-manage.sh | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 141b296..4fba8fc 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -378,6 +378,38 @@ PY done } +check_venv_runtime() { + "$VENV_PYTHON" - <<'PY' +checks = [ + ("import yaml", "PyYAML"), + ("import cherrypy", "CherryPy"), + ("import cherrypy_cors", "cherrypy-cors"), + ("import paho.mqtt.client", "paho-mqtt"), + ("import psutil", "psutil"), + ("import jwt", "PyJWT"), + ("import ws4py", "ws4py"), + ("import nacl", "PyNaCl"), + ("import periphery", "python-periphery"), + ("import spidev", "spidev"), + ("import serial", "pyserial"), + ("import usb", "pyusb"), + ("from Crypto.Cipher import AES", "pycryptodome AES backend"), +] +failed = [] +for stmt, label in checks: + try: + exec(stmt, {}) + except Exception as exc: + failed.append((label, exc)) + +if failed: + print("Buildroot runtime validation failed:") + for label, exc in failed: + print(f" - {label}: {exc}") + raise SystemExit(1) +PY +} + install_repeater() { local git_version ip_address @@ -419,6 +451,13 @@ install_repeater() { install_core_into_venv install_repeater_package + stage "Validating installed runtime" + if check_venv_runtime; then + info "Installed Python runtime looks usable" + else + fail "Installed packages are present but one or more native modules are unusable on this image." + fi + stage "Writing Buildroot init service" create_init_script @@ -444,6 +483,12 @@ upgrade_repeater() { install_buildroot_dependencies install_core_into_venv install_repeater_package + stage "Validating installed runtime" + if check_venv_runtime; then + info "Installed Python runtime looks usable" + else + fail "Installed packages are present but one or more native modules are unusable on this image." + fi "$INIT_SCRIPT" restart } From 1b3f0490ecf97e38b6b7e7ca856f62465a2610e7 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:22:13 -0400 Subject: [PATCH 13/22] Repair Buildroot venv build backend --- buildroot-manage.sh | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 4fba8fc..3251e03 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -157,13 +157,37 @@ ensure_venv() { info "This can take a minute on Buildroot flash storage." python3 -m venv --system-site-packages "$VENV_DIR" info "Bootstrapping pip, setuptools, and wheel" - "$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true + "$VENV_PIP" install --upgrade --no-cache-dir pip setuptools wheel info "Virtual environment is ready" else info "Using existing virtual environment at $VENV_DIR" fi } +ensure_venv_build_backend() { + if "$VENV_PYTHON" - <<'PY' +import setuptools +import setuptools.build_meta +import wheel +PY + then + info "venv build backend is ready" + return 0 + fi + + stage "Repairing venv build backend" + info "Ensuring pip, setuptools, and wheel are installed inside the venv" + "$VENV_PYTHON" -m ensurepip --upgrade >/dev/null 2>&1 || true + "$VENV_PIP" install --upgrade --no-cache-dir pip setuptools wheel + + "$VENV_PYTHON" - <<'PY' +import setuptools +import setuptools.build_meta +import wheel +PY + info "venv build backend repaired" +} + get_r2_wheel_base() { local machine_arch arch_tag platform_tag py_tag wheel_base @@ -433,6 +457,7 @@ install_repeater() { cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true ensure_venv + ensure_venv_build_backend if [ -d "$SCRIPT_DIR/.git" ]; then stage "Inspecting checked-out repo version" @@ -477,6 +502,7 @@ upgrade_repeater() { is_installed || fail "Service is not installed." ensure_venv + ensure_venv_build_backend preinstall_r2_wheels stage "Upgrading pyMC Repeater" From f92dd4ab5f13c8ed5b940467374d88a428eef421 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:36:20 -0400 Subject: [PATCH 14/22] Recreate contaminated Buildroot venvs --- buildroot-manage.sh | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 3251e03..8f20365 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -151,11 +151,23 @@ install_system_packages() { } ensure_venv() { + local recreate=0 + + if [ -f "$VENV_DIR/pyvenv.cfg" ] && grep -Eq '^include-system-site-packages *= *true$' "$VENV_DIR/pyvenv.cfg"; then + stage "Rebuilding virtual environment" + info "Existing venv uses system site-packages and is not supported on Buildroot." + recreate=1 + fi + + if [ "$recreate" -eq 1 ]; then + rm -rf "$VENV_DIR" + fi + if [ ! -x "$VENV_PYTHON" ]; then stage "Creating virtual environment" info "Creating $VENV_DIR" info "This can take a minute on Buildroot flash storage." - python3 -m venv --system-site-packages "$VENV_DIR" + python3 -m venv "$VENV_DIR" info "Bootstrapping pip, setuptools, and wheel" "$VENV_PIP" install --upgrade --no-cache-dir pip setuptools wheel info "Virtual environment is ready" @@ -175,17 +187,23 @@ PY return 0 fi - stage "Repairing venv build backend" - info "Ensuring pip, setuptools, and wheel are installed inside the venv" - "$VENV_PYTHON" -m ensurepip --upgrade >/dev/null 2>&1 || true + stage "Rebuilding virtual environment" + warn "Existing venv is contaminated or incomplete; recreating it cleanly." + rm -rf "$VENV_DIR" + python3 -m venv "$VENV_DIR" "$VENV_PIP" install --upgrade --no-cache-dir pip setuptools wheel - "$VENV_PYTHON" - <<'PY' + if "$VENV_PYTHON" - <<'PY' import setuptools import setuptools.build_meta import wheel PY - info "venv build backend repaired" + then + info "venv build backend repaired" + return 0 + fi + + fail "Unable to prepare an isolated venv with setuptools.build_meta on this Buildroot image." } get_r2_wheel_base() { From 7e541cd1f1931c6474c039e29f461f7b202be7e6 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:52:52 -0400 Subject: [PATCH 15/22] Use image runtime modules on Buildroot --- buildroot-manage.sh | 46 ++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 8f20365..cef6cbd 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -25,21 +25,13 @@ PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/pyMC_core.git}" PYMC_CORE_REF="${PYMC_CORE_REF:-dev}" set_wheel_dependencies() { set -- \ - "pyyaml>=6.0.0" \ "cherrypy>=18.0.0" \ "cherrypy-cors==1.7.0" \ "paho-mqtt>=1.6.0" \ - "psutil>=5.9.0" \ "pyjwt>=2.8.0" \ "ws4py>=0.6.0" \ - "pycryptodome>=3.23.0" \ - "PyNaCl>=1.5.0" \ - "cffi" \ - "pyserial" \ - "pyusb" \ - "spidev" \ - "python-periphery" \ "autocommand" \ + "backports.tarfile" \ "jaraco.collections" \ "jaraco.text" \ "jaraco.context" \ @@ -231,10 +223,8 @@ preinstall_r2_wheels() { [ -n "$wheel_base" ] || return 0 stage "Checking optional wheel cache" - info "Trying prebuilt Python wheels from ${wheel_base}/index.html" - "$VENV_PIP" install --find-links "${wheel_base}/index.html" --only-binary=:all: --no-cache-dir \ - "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" >/dev/null 2>&1 || true - info "Wheel cache step finished" + info "Native Python modules come from the Buildroot image." + info "Skipping native wheel preload from ${wheel_base}/index.html" } install_buildroot_dependencies() { @@ -263,6 +253,34 @@ install_buildroot_dependencies() { fi } +link_system_site_packages() { + local venv_site_dir pth_file system_paths + + venv_site_dir=$("$VENV_PYTHON" - <<'PY' +import site +for path in site.getsitepackages(): + if path.endswith("site-packages"): + print(path) + break +PY +) + [ -n "$venv_site_dir" ] || fail "Could not determine venv site-packages directory." + + system_paths=$(python3 - <<'PY' +import site +for path in site.getsitepackages(): + if path.startswith("/usr/lib/python") and path.endswith("site-packages"): + print(path) +PY +) + + [ -n "$system_paths" ] || return 0 + + pth_file="$venv_site_dir/buildroot-system-site-packages.pth" + printf '%s\n' "$system_paths" > "$pth_file" + info "Linked image-provided Python runtime into the venv" +} + install_core_into_venv() { local core_repo core_spec @@ -493,6 +511,7 @@ install_repeater() { install_buildroot_dependencies install_core_into_venv install_repeater_package + link_system_site_packages stage "Validating installed runtime" if check_venv_runtime; then @@ -527,6 +546,7 @@ upgrade_repeater() { install_buildroot_dependencies install_core_into_venv install_repeater_package + link_system_site_packages stage "Validating installed runtime" if check_venv_runtime; then info "Installed Python runtime looks usable" From e5c76327004d5940a3b6a9fd063d1af51adcd918 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:05:59 -0400 Subject: [PATCH 16/22] Handle Buildroot service restarts --- repeater/service_utils.py | 41 +++++++++++++++++++++++++++++--- repeater/web/update_endpoints.py | 4 ++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/repeater/service_utils.py b/repeater/service_utils.py index 14a1481..a127e48 100644 --- a/repeater/service_utils.py +++ b/repeater/service_utils.py @@ -4,22 +4,57 @@ Provides functions for service control operations like restart. """ import logging +import os import subprocess from typing import Tuple logger = logging.getLogger("ServiceUtils") +INIT_SCRIPT = "/etc/init.d/S80pymc-repeater" + + +def is_buildroot() -> bool: + if os.path.exists("/etc/pymc-image-build-id"): + return True + if os.path.exists("/etc/os-release"): + try: + with open("/etc/os-release", "r", encoding="utf-8") as handle: + return any(line.strip() == "ID=buildroot" for line in handle) + except OSError: + return False + return False def restart_service() -> Tuple[bool, str]: """ - Restart the pymc-repeater service via systemctl. + Restart the pymc-repeater service. - Tries polkit-based restart first (plain systemctl), then falls back - to sudo-based restart (requires sudoers.d rule installed by manage.sh). + On Buildroot/Luckfox, use the shipped init script directly. + On systemd hosts, try polkit-based restart first (plain systemctl), then + fall back to sudo-based restart (requires sudoers.d rule installed by + manage.sh). Returns: Tuple[bool, str]: (success, message) """ + if is_buildroot(): + if not os.path.exists(INIT_SCRIPT): + logger.error("Buildroot init script not found: %s", INIT_SCRIPT) + return False, f"init script not found: {INIT_SCRIPT}" + + try: + subprocess.Popen( + ["/bin/sh", "-c", f"sleep 1; exec {INIT_SCRIPT} restart >/dev/null 2>&1"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + logger.info("Service restart scheduled via Buildroot init script") + return True, "Service restart initiated" + except Exception as exc: + logger.error(f"Buildroot restart failed: {exc}") + return False, f"Restart failed: {exc}" + # Try polkit-based restart first (works on bare metal / VMs with polkit running) try: result = subprocess.run( diff --git a/repeater/web/update_endpoints.py b/repeater/web/update_endpoints.py index 941fa78..800c8d6 100644 --- a/repeater/web/update_endpoints.py +++ b/repeater/web/update_endpoints.py @@ -685,6 +685,10 @@ def _migrate_service_unit() -> None: """Strip legacy PYTHONPATH, fix WorkingDirectory, and ensure ExecStart uses the venv python in the systemd service unit. """ + if os.path.exists("/etc/pymc-image-build-id"): + logger.info("[Update] Buildroot image detected, skipping systemd unit migration.") + return + import subprocess as _sp _SVC_UNIT = "/etc/systemd/system/pymc-repeater.service" _VENV_PYTHON = "/opt/pymc_repeater/venv/bin/python" From cc9c81de2ad9d8c0f8a156614f66a04e3482b439 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:57:21 -0400 Subject: [PATCH 17/22] Seed Buildroot config from repo installer --- buildroot-manage.sh | 279 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index cef6cbd..8cdd62a 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -68,6 +68,71 @@ is_buildroot() { return 1 } +prompt_value() { + local prompt="$1" + local default_value="${2:-}" + local reply="" + + if [ -n "${reply:-}" ]; then + : + fi + + if [ ! -t 0 ]; then + printf '%s\n' "$default_value" + return 0 + fi + + if [ -n "$default_value" ]; then + printf '%s [%s]: ' "$prompt" "$default_value" >&2 + else + printf '%s: ' "$prompt" >&2 + fi + IFS= read -r reply || true + reply=${reply:-$default_value} + printf '%s\n' "$reply" +} + +prompt_secret() { + local prompt="$1" + local first="" + local second="" + + if [ ! -t 0 ]; then + fail "Interactive admin password prompt requires a TTY. Set PYMC_ADMIN_PASSWORD instead." + fi + + while true; do + read -r -s -p "${prompt}: " first >&2 + printf '\n' >&2 + read -r -s -p "Confirm ${prompt,,}: " second >&2 + printf '\n' >&2 + + [ -n "$first" ] || { + warn "Password cannot be empty." + continue + } + [ "${#first}" -ge 6 ] || { + warn "Password must be at least 6 characters." + continue + } + [ "$first" = "$second" ] || { + warn "Passwords do not match." + continue + } + + printf '%s\n' "$first" + return 0 + done +} + +normalize_radio_profile() { + case "$1" in + 1|v2|V2|pimesh-v2|pimesh_v2|pimesh2|PiMeshV2) printf 'v2\n' ;; + 2|v1|V1|meshadv|MeshAdv|pimesh-v1|pimesh_v1|pimesh1) printf 'v1\n' ;; + *) return 1 ;; + esac +} + ensure_root() { [ "$(id -u 2>/dev/null || echo 1)" -eq 0 ] || fail "This command must be run as root." } @@ -367,6 +432,210 @@ get_primary_ip() { ip -o -4 addr show dev eth0 2>/dev/null | awk 'NR==1 { sub(/\/.*/, "", $4); print $4; exit }' } +get_config_value() { + local key_path="$1" + local fallback="${2:-}" + + python3 - "$CONFIG_DIR/config.yaml" "$key_path" "$fallback" <<'PY' +import sys +import yaml + +config_path, key_path, fallback = sys.argv[1:4] +try: + with open(config_path, "r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) or {} +except FileNotFoundError: + print(fallback) + raise SystemExit(0) + +current = data +for part in key_path.split("."): + if isinstance(current, dict) and part in current: + current = current[part] + else: + print(fallback) + raise SystemExit(0) + +if current in (None, ""): + print(fallback) +elif isinstance(current, float): + rendered = ("%f" % current).rstrip("0").rstrip(".") + print(rendered or "0") +else: + print(current) +PY +} + +get_radio_frequency_mhz() { + python3 - "$CONFIG_DIR/config.yaml" <<'PY' +import yaml + +with open(__import__("sys").argv[1], "r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) or {} +freq = ((data.get("radio") or {}).get("frequency")) or 910525000 +print(f"{float(freq) / 1_000_000:.3f}".rstrip("0").rstrip(".")) +PY +} + +get_radio_bandwidth_khz() { + python3 - "$CONFIG_DIR/config.yaml" <<'PY' +import yaml + +with open(__import__("sys").argv[1], "r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) or {} +bw = ((data.get("radio") or {}).get("bandwidth")) or 62500 +print(f"{float(bw) / 1000:.3f}".rstrip("0").rstrip(".")) +PY +} + +write_repeater_config() { + local node_name="$1" + local admin_password="$2" + local jwt_secret="$3" + local freq_mhz="$4" + local sf="$5" + local bw_khz="$6" + local coding_rate="$7" + local tx_power="$8" + local profile="$9" + + python3 - "$CONFIG_DIR/config.yaml" "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$profile" <<'PY' +import sys +import yaml + +config_path, node_name, admin_password, jwt_secret, freq_mhz, sf, bw_khz, coding_rate, tx_power, profile = sys.argv[1:11] + +with open(config_path, "r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) or {} + +repeater = data.setdefault("repeater", {}) +security = repeater.setdefault("security", {}) +radio = data.setdefault("radio", {}) +sx1262 = data.setdefault("sx1262", {}) + +repeater["node_name"] = node_name +security["admin_password"] = admin_password +security["jwt_secret"] = jwt_secret + +radio["frequency"] = int(round(float(freq_mhz) * 1_000_000)) +radio["spreading_factor"] = int(sf) +radio["bandwidth"] = int(round(float(bw_khz) * 1000)) +radio["coding_rate"] = int(coding_rate) +radio["tx_power"] = int(tx_power) + +sx1262["bus_id"] = 0 +sx1262["cs_id"] = 0 +sx1262["txled_pin"] = -1 +sx1262["rxled_pin"] = -1 +sx1262["dio3_tcxo_voltage"] = 1.8 +sx1262["use_dio3_tcxo"] = True +sx1262["is_waveshare"] = False + +if profile == "v2": + sx1262["cs_pin"] = -1 + sx1262["reset_pin"] = 54 + sx1262["busy_pin"] = 122 + sx1262["irq_pin"] = 121 + sx1262["en_pin"] = 0 + sx1262["txen_pin"] = -1 + sx1262["rxen_pin"] = -1 + sx1262["use_dio2_rf"] = True +else: + sx1262["cs_pin"] = 145 + sx1262["reset_pin"] = 54 + sx1262["busy_pin"] = 123 + sx1262["irq_pin"] = 55 + sx1262["en_pin"] = -1 + sx1262["txen_pin"] = 52 + sx1262["rxen_pin"] = 53 + sx1262["use_dio2_rf"] = False + +with open(config_path, "w", encoding="utf-8") as fh: + yaml.safe_dump(data, fh, sort_keys=False) +PY +} + +select_radio_profile() { + local choice="${LUCKFOX_RADIO_PROFILE:-${PYMC_RADIO_PROFILE:-}}" + + if [ -n "$choice" ]; then + normalize_radio_profile "$choice" || fail "Unknown radio profile choice: $choice" + return 0 + fi + + printf 'Select Luckfox radio profile:\n' >&2 + printf ' 1) PiMesh V2\n' >&2 + printf ' 2) PiMesh V1 / MeshAdv\n' >&2 + choice=$(prompt_value "Profile" "1") + normalize_radio_profile "$choice" || fail "Unknown radio profile choice: $choice" +} + +seed_repeater_config() { + local node_name admin_password jwt_secret profile freq_mhz sf bw_khz coding_rate tx_power + + stage "Configuring repeater" + + node_name="${PYMC_NODE_NAME:-}" + [ -n "$node_name" ] || node_name=$(prompt_value "Repeater name" "$(get_config_value repeater.node_name luckfox-repeater)") + [ -n "$node_name" ] || fail "Repeater name cannot be empty." + + profile=$(select_radio_profile) + + admin_password="${PYMC_ADMIN_PASSWORD:-}" + [ -n "$admin_password" ] || admin_password=$(prompt_secret "Admin password") + + jwt_secret="${PYMC_JWT_SECRET:-}" + [ -n "$jwt_secret" ] || jwt_secret=$(python3 -c 'import secrets; print(secrets.token_hex(32))') + + freq_mhz="${PYMC_RADIO_FREQUENCY_MHZ:-}" + [ -n "$freq_mhz" ] || freq_mhz=$(prompt_value "Frequency MHz" "$(get_radio_frequency_mhz)") + + sf="${PYMC_RADIO_SF:-}" + [ -n "$sf" ] || sf=$(prompt_value "Spreading factor" "$(get_config_value radio.spreading_factor 7)") + + bw_khz="${PYMC_RADIO_BANDWIDTH_KHZ:-}" + [ -n "$bw_khz" ] || bw_khz=$(prompt_value "Bandwidth kHz" "$(get_radio_bandwidth_khz)") + + coding_rate="${PYMC_RADIO_CODING_RATE:-}" + [ -n "$coding_rate" ] || coding_rate=$(prompt_value "Coding rate" "$(get_config_value radio.coding_rate 5)") + + tx_power="${PYMC_RADIO_TX_POWER_DBM:-}" + [ -n "$tx_power" ] || tx_power=$(prompt_value "TX power dBm" "$(get_config_value radio.tx_power 22)") + + write_repeater_config "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$profile" + info "Saved config for ${node_name}" +} + +configure_repeater() { + ensure_root + [ -f "$CONFIG_DIR/config.yaml" ] || fail "Config file is missing. Run install first." + seed_repeater_config + if service_exists; then + "$INIT_SCRIPT" restart + fi +} + +configure_radio_profile() { + local profile + ensure_root + [ -f "$CONFIG_DIR/config.yaml" ] || fail "Config file is missing. Run install first." + profile=$(select_radio_profile) + write_repeater_config \ + "$(get_config_value repeater.node_name luckfox-repeater)" \ + "$(get_config_value repeater.security.admin_password admin123)" \ + "$(get_config_value repeater.security.jwt_secret "$(python3 -c 'import secrets; print(secrets.token_hex(32))')")" \ + "$(get_radio_frequency_mhz)" \ + "$(get_config_value radio.spreading_factor 7)" \ + "$(get_radio_bandwidth_khz)" \ + "$(get_config_value radio.coding_rate 5)" \ + "$(get_config_value radio.tx_power 22)" \ + "$profile" + info "Applied Luckfox radio profile: $profile" + if service_exists; then + "$INIT_SCRIPT" restart + fi +} + service_exists() { [ -x "$INIT_SCRIPT" ] } @@ -520,6 +789,8 @@ install_repeater() { fail "Installed packages are present but one or more native modules are unusable on this image." fi + seed_repeater_config + stage "Writing Buildroot init service" create_init_script @@ -619,6 +890,8 @@ Commands: doctor Check Buildroot/Luckfox prerequisites install Install pyMC Repeater on the Buildroot image upgrade Upgrade the Buildroot installation from the checked-out repo + configure Prompt for repeater settings and rewrite config.yaml + radio-profile Reapply the Luckfox radio pin profile only config Run the stock interactive config flow start Start the init.d service stop Stop the init.d service @@ -640,6 +913,12 @@ case "${1:-}" in upgrade) upgrade_repeater ;; + configure) + configure_repeater + ;; + radio-profile) + configure_radio_profile + ;; config) shift delegate_to_stock_manage config "$@" From ab2c82db163993ea8e8c377b6e62b6c7f772c2f1 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:01:32 -0400 Subject: [PATCH 18/22] Drive Buildroot radio config from JSON --- buildroot-manage.sh | 322 +++++++++++++++++++++++++++------- radio-settings-buildroot.json | 73 ++++++++ 2 files changed, 327 insertions(+), 68 deletions(-) create mode 100644 radio-settings-buildroot.json diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 8cdd62a..2a5f6be 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -23,6 +23,9 @@ PIWHEELS_INDEX_URL="https://www.piwheels.org/simple" R2_ENABLED=1 PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/pyMC_core.git}" PYMC_CORE_REF="${PYMC_CORE_REF:-dev}" +RADIO_SETTINGS_JSON="$SCRIPT_DIR/radio-settings.json" +RADIO_PRESETS_JSON="$SCRIPT_DIR/radio-presets.json" +BUILDROOT_RADIO_SETTINGS_JSON="$SCRIPT_DIR/radio-settings-buildroot.json" set_wheel_dependencies() { set -- \ "cherrypy>=18.0.0" \ @@ -73,10 +76,6 @@ prompt_value() { local default_value="${2:-}" local reply="" - if [ -n "${reply:-}" ]; then - : - fi - if [ ! -t 0 ]; then printf '%s\n' "$default_value" return 0 @@ -125,14 +124,6 @@ prompt_secret() { done } -normalize_radio_profile() { - case "$1" in - 1|v2|V2|pimesh-v2|pimesh_v2|pimesh2|PiMeshV2) printf 'v2\n' ;; - 2|v1|V1|meshadv|MeshAdv|pimesh-v1|pimesh_v1|pimesh1) printf 'v1\n' ;; - *) return 1 ;; - esac -} - ensure_root() { [ "$(id -u 2>/dev/null || echo 1)" -eq 0 ] || fail "This command must be run as root." } @@ -466,6 +457,146 @@ else: PY } +list_buildroot_boards() { + python3 - "$BUILDROOT_RADIO_SETTINGS_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +for index, (key, entry) in enumerate(data.get("buildroot_hardware", {}).items(), start=1): + print(f"{index}|{key}|{entry.get('name', key)}|{entry.get('description', '')}") +PY +} + +resolve_buildroot_board() { + local choice="$1" + + python3 - "$BUILDROOT_RADIO_SETTINGS_JSON" "$choice" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +choice = sys.argv[2].strip().lower() +boards = list((data.get("buildroot_hardware") or {}).items()) + +for index, (key, entry) in enumerate(boards, start=1): + aliases = {str(index), key.lower(), str(entry.get("name", "")).lower()} + aliases.update(alias.lower() for alias in entry.get("aliases", [])) + if choice in aliases: + print(key) + raise SystemExit(0) + +raise SystemExit(1) +PY +} + +get_default_buildroot_board() { + python3 - "$BUILDROOT_RADIO_SETTINGS_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +default_board = data.get("default_board") +boards = data.get("buildroot_hardware", {}) +if default_board and default_board in boards: + print(default_board) +else: + for key in boards: + print(key) + break +PY +} + +get_buildroot_board_label() { + local board_key="$1" + python3 - "$BUILDROOT_RADIO_SETTINGS_JSON" "$board_key" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +entry = (data.get("buildroot_hardware") or {}).get(sys.argv[2], {}) +print(entry.get("name", sys.argv[2])) +PY +} + +list_radio_presets() { + python3 - "$RADIO_PRESETS_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +entries = ((data.get("config") or {}).get("suggested_radio_settings") or {}).get("entries", []) +for index, entry in enumerate(entries, start=1): + print(f"{index}|{entry.get('title', '')}|{entry.get('description', '')}") +PY +} + +resolve_radio_preset() { + local choice="$1" + + python3 - "$RADIO_PRESETS_JSON" "$choice" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +choice = sys.argv[2].strip().lower() +entries = ((data.get("config") or {}).get("suggested_radio_settings") or {}).get("entries", []) +for index, entry in enumerate(entries, start=1): + title = entry.get("title", "") + if choice in {str(index), title.lower()}: + print(title) + raise SystemExit(0) + +raise SystemExit(1) +PY +} + +get_default_radio_preset() { + python3 - "$BUILDROOT_RADIO_SETTINGS_JSON" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +print(data.get("default_radio_preset", "")) +PY +} + +get_radio_preset_field() { + local preset_title="$1" + local field="$2" + + python3 - "$RADIO_PRESETS_JSON" "$preset_title" "$field" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as fh: + data = json.load(fh) + +preset_title, field = sys.argv[2:4] +entries = ((data.get("config") or {}).get("suggested_radio_settings") or {}).get("entries", []) +for entry in entries: + if entry.get("title") == preset_title: + print(entry.get(field, "")) + raise SystemExit(0) + +raise SystemExit(1) +PY +} + get_radio_frequency_mhz() { python3 - "$CONFIG_DIR/config.yaml" <<'PY' import yaml @@ -488,6 +619,56 @@ print(f"{float(bw) / 1000:.3f}".rstrip("0").rstrip(".")) PY } +select_buildroot_board() { + local choice="${LUCKFOX_RADIO_PROFILE:-${PYMC_RADIO_PROFILE:-${PYMC_BUILDROOT_BOARD:-}}}" + local default_board + + default_board=$(get_default_buildroot_board) + + if [ -n "$choice" ]; then + resolve_buildroot_board "$choice" || fail "Unknown Buildroot board choice: $choice" + return 0 + fi + + printf 'Select Luckfox radio board:\n' >&2 + while IFS='|' read -r index key name description; do + [ -n "$index" ] || continue + printf ' %s) %s' "$index" "$name" >&2 + [ -n "$description" ] && printf ' - %s' "$description" >&2 + printf '\n' >&2 + done <&2 + while IFS='|' read -r index title description; do + [ -n "$index" ] || continue + printf ' %s) %s' "$index" "$title" >&2 + [ -n "$description" ] && printf ' - %s' "$description" >&2 + printf '\n' >&2 + done <&2 - printf ' 1) PiMesh V2\n' >&2 - printf ' 2) PiMesh V1 / MeshAdv\n' >&2 - choice=$(prompt_value "Profile" "1") - normalize_radio_profile "$choice" || fail "Unknown radio profile choice: $choice" -} - seed_repeater_config() { - local node_name admin_password jwt_secret profile freq_mhz sf bw_khz coding_rate tx_power + local node_name admin_password jwt_secret board_key board_name preset_title freq_mhz sf bw_khz coding_rate tx_power stage "Configuring repeater" @@ -579,7 +759,9 @@ seed_repeater_config() { [ -n "$node_name" ] || node_name=$(prompt_value "Repeater name" "$(get_config_value repeater.node_name luckfox-repeater)") [ -n "$node_name" ] || fail "Repeater name cannot be empty." - profile=$(select_radio_profile) + board_key=$(select_buildroot_board) + board_name=$(get_buildroot_board_label "$board_key") + info "Selected board: $board_name" admin_password="${PYMC_ADMIN_PASSWORD:-}" [ -n "$admin_password" ] || admin_password=$(prompt_secret "Admin password") @@ -587,22 +769,25 @@ seed_repeater_config() { jwt_secret="${PYMC_JWT_SECRET:-}" [ -n "$jwt_secret" ] || jwt_secret=$(python3 -c 'import secrets; print(secrets.token_hex(32))') + preset_title=$(select_radio_preset) + info "Using preset: $preset_title" + freq_mhz="${PYMC_RADIO_FREQUENCY_MHZ:-}" - [ -n "$freq_mhz" ] || freq_mhz=$(prompt_value "Frequency MHz" "$(get_radio_frequency_mhz)") + [ -n "$freq_mhz" ] || freq_mhz=$(prompt_value "Frequency MHz" "$(get_radio_preset_field "$preset_title" frequency)") sf="${PYMC_RADIO_SF:-}" - [ -n "$sf" ] || sf=$(prompt_value "Spreading factor" "$(get_config_value radio.spreading_factor 7)") + [ -n "$sf" ] || sf=$(prompt_value "Spreading factor" "$(get_radio_preset_field "$preset_title" spreading_factor)") bw_khz="${PYMC_RADIO_BANDWIDTH_KHZ:-}" - [ -n "$bw_khz" ] || bw_khz=$(prompt_value "Bandwidth kHz" "$(get_radio_bandwidth_khz)") + [ -n "$bw_khz" ] || bw_khz=$(prompt_value "Bandwidth kHz" "$(get_radio_preset_field "$preset_title" bandwidth)") coding_rate="${PYMC_RADIO_CODING_RATE:-}" - [ -n "$coding_rate" ] || coding_rate=$(prompt_value "Coding rate" "$(get_config_value radio.coding_rate 5)") + [ -n "$coding_rate" ] || coding_rate=$(prompt_value "Coding rate" "$(get_radio_preset_field "$preset_title" coding_rate)") tx_power="${PYMC_RADIO_TX_POWER_DBM:-}" [ -n "$tx_power" ] || tx_power=$(prompt_value "TX power dBm" "$(get_config_value radio.tx_power 22)") - write_repeater_config "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$profile" + write_repeater_config "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$board_key" info "Saved config for ${node_name}" } @@ -616,10 +801,11 @@ configure_repeater() { } configure_radio_profile() { - local profile + local board_key + ensure_root [ -f "$CONFIG_DIR/config.yaml" ] || fail "Config file is missing. Run install first." - profile=$(select_radio_profile) + board_key=$(select_buildroot_board) write_repeater_config \ "$(get_config_value repeater.node_name luckfox-repeater)" \ "$(get_config_value repeater.security.admin_password admin123)" \ @@ -629,8 +815,8 @@ configure_radio_profile() { "$(get_radio_bandwidth_khz)" \ "$(get_config_value radio.coding_rate 5)" \ "$(get_config_value radio.tx_power 22)" \ - "$profile" - info "Applied Luckfox radio profile: $profile" + "$board_key" + info "Applied board profile: $(get_buildroot_board_label "$board_key")" if service_exists; then "$INIT_SCRIPT" restart fi @@ -891,7 +1077,7 @@ Commands: install Install pyMC Repeater on the Buildroot image upgrade Upgrade the Buildroot installation from the checked-out repo configure Prompt for repeater settings and rewrite config.yaml - radio-profile Reapply the Luckfox radio pin profile only + radio-profile Reapply the Luckfox board radio config only config Run the stock interactive config flow start Start the init.d service stop Stop the init.d service diff --git a/radio-settings-buildroot.json b/radio-settings-buildroot.json new file mode 100644 index 0000000..db2d720 --- /dev/null +++ b/radio-settings-buildroot.json @@ -0,0 +1,73 @@ +{ + "default_board": "luckfox-pimesh-v2", + "default_radio_preset": "USA/Canada (Recommended)", + "buildroot_hardware": { + "luckfox-pimesh-v2": { + "name": "Luckfox PiMesh V2", + "description": "Luckfox Pico Pi with PiMesh-1W V2 / E22P wiring", + "hardware_id": "pimesh-1w-v2", + "aliases": [ + "1", + "v2", + "pimesh-v2", + "pimesh-1w-v2" + ], + "sx1262_overrides": { + "cs_pin": -1, + "reset_pin": 54, + "busy_pin": 122, + "irq_pin": 121, + "en_pin": 0, + "txen_pin": -1, + "rxen_pin": -1, + "use_dio2_rf": true, + "use_dio3_tcxo": true, + "dio3_tcxo_voltage": 1.8 + } + }, + "luckfox-pimesh-v1": { + "name": "Luckfox PiMesh V1", + "description": "Luckfox Pico Pi with PiMesh-1W V1 wiring", + "hardware_id": "pimesh-1w-v1", + "aliases": [ + "2", + "v1", + "pimesh-v1", + "pimesh-1w-v1" + ], + "sx1262_overrides": { + "cs_pin": 145, + "reset_pin": 54, + "busy_pin": 123, + "irq_pin": 55, + "en_pin": -1, + "txen_pin": 52, + "rxen_pin": 53, + "use_dio2_rf": false, + "use_dio3_tcxo": true, + "dio3_tcxo_voltage": 1.8 + } + }, + "luckfox-meshadv": { + "name": "Luckfox MeshAdv", + "description": "Luckfox Pico Pi with MeshAdv wiring", + "hardware_id": "meshadv", + "aliases": [ + "3", + "meshadv" + ], + "sx1262_overrides": { + "cs_pin": 145, + "reset_pin": 54, + "busy_pin": 123, + "irq_pin": 55, + "en_pin": -1, + "txen_pin": 52, + "rxen_pin": 53, + "use_dio2_rf": false, + "use_dio3_tcxo": true, + "dio3_tcxo_voltage": 1.8 + } + } + } +} From 9677a39aa8ad07ca9cec06305fdf764b58f6bc9a Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:05:31 -0400 Subject: [PATCH 19/22] Make Buildroot password prompt sh-safe --- buildroot-manage.sh | 81 +++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 2a5f6be..8315dcd 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -93,35 +93,66 @@ prompt_value() { prompt_secret() { local prompt="$1" - local first="" - local second="" - if [ ! -t 0 ]; then - fail "Interactive admin password prompt requires a TTY. Set PYMC_ADMIN_PASSWORD instead." - fi + python3 - "$prompt" <<'PY' +import sys +import termios +import tty - while true; do - read -r -s -p "${prompt}: " first >&2 - printf '\n' >&2 - read -r -s -p "Confirm ${prompt,,}: " second >&2 - printf '\n' >&2 +prompt = sys.argv[1] - [ -n "$first" ] || { - warn "Password cannot be empty." - continue - } - [ "${#first}" -ge 6 ] || { - warn "Password must be at least 6 characters." - continue - } - [ "$first" = "$second" ] || { - warn "Passwords do not match." - continue - } +if not sys.stdin.isatty(): + print("Interactive admin password prompt requires a TTY. Set PYMC_ADMIN_PASSWORD instead.", file=sys.stderr) + raise SystemExit(1) - printf '%s\n' "$first" - return 0 - done +fd = sys.stdin.fileno() + +def read_secret(label: str) -> str: + sys.stderr.write(f"{label}: ") + sys.stderr.flush() + original = termios.tcgetattr(fd) + chars = [] + try: + tty.setraw(fd) + while True: + ch = sys.stdin.read(1) + if ch in ("\r", "\n"): + sys.stderr.write("\n") + sys.stderr.flush() + return "".join(chars) + if ch == "\x03": + raise KeyboardInterrupt + if ch in ("\x7f", "\b"): + if chars: + chars.pop() + sys.stderr.write("\b \b") + sys.stderr.flush() + continue + if not ch or ord(ch) < 32: + continue + chars.append(ch) + sys.stderr.write("*") + sys.stderr.flush() + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, original) + +while True: + first = read_secret(prompt) + second = read_secret(f"Confirm {prompt.lower()}") + + if not first: + print(" - Password cannot be empty.", file=sys.stderr) + continue + if len(first) < 6: + print(" - Password must be at least 6 characters.", file=sys.stderr) + continue + if first != second: + print(" - Passwords do not match.", file=sys.stderr) + continue + + print(first) + break +PY } ensure_root() { From 74f5963a8557b7b5e65f16ad86eaaceff64fb491 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:07:32 -0400 Subject: [PATCH 20/22] Use Buildroot config flow by default --- buildroot-manage.sh | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 8315dcd..0524fad 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -95,44 +95,46 @@ prompt_secret() { local prompt="$1" python3 - "$prompt" <<'PY' +import os import sys import termios import tty prompt = sys.argv[1] - -if not sys.stdin.isatty(): +try: + tty_stream = open("/dev/tty", "r+", encoding="utf-8", newline="") +except OSError: print("Interactive admin password prompt requires a TTY. Set PYMC_ADMIN_PASSWORD instead.", file=sys.stderr) raise SystemExit(1) -fd = sys.stdin.fileno() +fd = tty_stream.fileno() def read_secret(label: str) -> str: - sys.stderr.write(f"{label}: ") - sys.stderr.flush() + tty_stream.write(f"{label}: ") + tty_stream.flush() original = termios.tcgetattr(fd) chars = [] try: tty.setraw(fd) while True: - ch = sys.stdin.read(1) + ch = tty_stream.read(1) if ch in ("\r", "\n"): - sys.stderr.write("\n") - sys.stderr.flush() + tty_stream.write("\n") + tty_stream.flush() return "".join(chars) if ch == "\x03": raise KeyboardInterrupt if ch in ("\x7f", "\b"): if chars: chars.pop() - sys.stderr.write("\b \b") - sys.stderr.flush() + tty_stream.write("\b \b") + tty_stream.flush() continue if not ch or ord(ch) < 32: continue chars.append(ch) - sys.stderr.write("*") - sys.stderr.flush() + tty_stream.write("*") + tty_stream.flush() finally: termios.tcsetattr(fd, termios.TCSADRAIN, original) @@ -152,6 +154,8 @@ while True: print(first) break + +tty_stream.close() PY } @@ -1107,9 +1111,9 @@ Commands: doctor Check Buildroot/Luckfox prerequisites install Install pyMC Repeater on the Buildroot image upgrade Upgrade the Buildroot installation from the checked-out repo - configure Prompt for repeater settings and rewrite config.yaml + config Prompt for repeater settings and rewrite config.yaml + configure Same as config radio-profile Reapply the Luckfox board radio config only - config Run the stock interactive config flow start Start the init.d service stop Stop the init.d service restart Restart the init.d service @@ -1130,16 +1134,12 @@ case "${1:-}" in upgrade) upgrade_repeater ;; - configure) + config|configure) configure_repeater ;; radio-profile) configure_radio_profile ;; - config) - shift - delegate_to_stock_manage config "$@" - ;; start|stop|restart) manage_service "$1" ;; From 91025e4970eb8083983639437b3e0fd18db93cfe Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:09:09 -0400 Subject: [PATCH 21/22] Use raw tty for Buildroot password prompt --- buildroot-manage.sh | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 0524fad..733b5b9 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -102,41 +102,35 @@ import tty prompt = sys.argv[1] try: - tty_stream = open("/dev/tty", "r+", encoding="utf-8", newline="") + tty_fd = os.open("/dev/tty", os.O_RDWR) except OSError: print("Interactive admin password prompt requires a TTY. Set PYMC_ADMIN_PASSWORD instead.", file=sys.stderr) raise SystemExit(1) -fd = tty_stream.fileno() - def read_secret(label: str) -> str: - tty_stream.write(f"{label}: ") - tty_stream.flush() - original = termios.tcgetattr(fd) + os.write(tty_fd, f"{label}: ".encode()) + original = termios.tcgetattr(tty_fd) chars = [] try: - tty.setraw(fd) + tty.setraw(tty_fd) while True: - ch = tty_stream.read(1) - if ch in ("\r", "\n"): - tty_stream.write("\n") - tty_stream.flush() + ch = os.read(tty_fd, 1) + if ch in (b"\r", b"\n"): + os.write(tty_fd, b"\n") return "".join(chars) - if ch == "\x03": + if ch == b"\x03": raise KeyboardInterrupt - if ch in ("\x7f", "\b"): + if ch in (b"\x7f", b"\x08"): if chars: chars.pop() - tty_stream.write("\b \b") - tty_stream.flush() + os.write(tty_fd, b"\b \b") continue - if not ch or ord(ch) < 32: + if not ch or ch[0] < 32: continue - chars.append(ch) - tty_stream.write("*") - tty_stream.flush() + chars.append(ch.decode(errors="ignore")) + os.write(tty_fd, b"*") finally: - termios.tcsetattr(fd, termios.TCSADRAIN, original) + termios.tcsetattr(tty_fd, termios.TCSADRAIN, original) while True: first = read_secret(prompt) @@ -154,8 +148,7 @@ while True: print(first) break - -tty_stream.close() +os.close(tty_fd) PY } From d22ba91f19cd7f5f16148c797cbadb28366f9b96 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:11:44 -0400 Subject: [PATCH 22/22] Apply Buildroot preset values directly --- buildroot-manage.sh | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 733b5b9..17b283d 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -116,7 +116,7 @@ def read_secret(label: str) -> str: while True: ch = os.read(tty_fd, 1) if ch in (b"\r", b"\n"): - os.write(tty_fd, b"\n") + os.write(tty_fd, b"\r\n") return "".join(chars) if ch == b"\x03": raise KeyboardInterrupt @@ -800,20 +800,11 @@ seed_repeater_config() { preset_title=$(select_radio_preset) info "Using preset: $preset_title" - freq_mhz="${PYMC_RADIO_FREQUENCY_MHZ:-}" - [ -n "$freq_mhz" ] || freq_mhz=$(prompt_value "Frequency MHz" "$(get_radio_preset_field "$preset_title" frequency)") - - sf="${PYMC_RADIO_SF:-}" - [ -n "$sf" ] || sf=$(prompt_value "Spreading factor" "$(get_radio_preset_field "$preset_title" spreading_factor)") - - bw_khz="${PYMC_RADIO_BANDWIDTH_KHZ:-}" - [ -n "$bw_khz" ] || bw_khz=$(prompt_value "Bandwidth kHz" "$(get_radio_preset_field "$preset_title" bandwidth)") - - coding_rate="${PYMC_RADIO_CODING_RATE:-}" - [ -n "$coding_rate" ] || coding_rate=$(prompt_value "Coding rate" "$(get_radio_preset_field "$preset_title" coding_rate)") - - tx_power="${PYMC_RADIO_TX_POWER_DBM:-}" - [ -n "$tx_power" ] || tx_power=$(prompt_value "TX power dBm" "$(get_config_value radio.tx_power 22)") + freq_mhz="${PYMC_RADIO_FREQUENCY_MHZ:-$(get_radio_preset_field "$preset_title" frequency)}" + sf="${PYMC_RADIO_SF:-$(get_radio_preset_field "$preset_title" spreading_factor)}" + bw_khz="${PYMC_RADIO_BANDWIDTH_KHZ:-$(get_radio_preset_field "$preset_title" bandwidth)}" + coding_rate="${PYMC_RADIO_CODING_RATE:-$(get_radio_preset_field "$preset_title" coding_rate)}" + tx_power="${PYMC_RADIO_TX_POWER_DBM:-$(get_config_value radio.tx_power 22)}" write_repeater_config "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$board_key" info "Saved config for ${node_name}"