From 1f88b73b067df57a4c210d2238bf3c2aa0f0d2ec Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:01:05 -0400 Subject: [PATCH 1/6] Pass configurable SX1262 timing delay --- repeater/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/repeater/config.py b/repeater/config.py index 7f09568..9b74187 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -326,6 +326,8 @@ def get_radio_for_board(board_config: dict): combined_config["gpio_chip"] = _parse_int(spi_config["gpio_chip"], default=0) if "use_gpiod_backend" in spi_config: combined_config["use_gpiod_backend"] = spi_config["use_gpiod_backend"] + if "radio_timing_delay" in spi_config: + combined_config["radio_timing_delay"] = float(spi_config["radio_timing_delay"]) radio = SX1262Radio.get_instance(**combined_config) From 83de21005e47c43ac3af695ff09d65c6e459b878 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:05:31 -0400 Subject: [PATCH 2/6] Fix backspace handling in Buildroot prompts --- buildroot-manage.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 39c9a6b..11b1fd9 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -148,6 +148,16 @@ need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" } +normalize_interactive_tty() { + [ -t 0 ] || return 0 + [ -r /dev/tty ] || return 0 + + # Buildroot shells sometimes end up with a bad erase character, which + # makes backspace print ^H during interactive prompts. + stty sane < /dev/tty >/dev/null 2>&1 || true + stty erase '^H' < /dev/tty >/dev/null 2>&1 || true +} + 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 @@ -164,6 +174,8 @@ prompt_value() { return 0 fi + normalize_interactive_tty + if [ -n "$default_value" ]; then printf '%s [%s]: ' "$prompt" "$default_value" >&2 else @@ -177,6 +189,8 @@ prompt_value() { prompt_secret() { local prompt="$1" + normalize_interactive_tty + python3 - "$prompt" <<'PY' import os import sys From 97fde6e0dda2f9c15e248706eba6a594e569a1fc Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:09:57 -0400 Subject: [PATCH 3/6] Set Pico Pi radio timing override in Buildroot profiles --- radio-settings-buildroot.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/radio-settings-buildroot.json b/radio-settings-buildroot.json index 16e1494..6093c4c 100644 --- a/radio-settings-buildroot.json +++ b/radio-settings-buildroot.json @@ -18,6 +18,7 @@ "reset_pin": 54, "busy_pin": 122, "irq_pin": 121, + "radio_timing_delay": 0.012, "en_pin": 0, "txen_pin": -1, "rxen_pin": -1, @@ -42,6 +43,7 @@ "reset_pin": 54, "busy_pin": 123, "irq_pin": 55, + "radio_timing_delay": 0.012, "en_pin": -1, "txen_pin": 52, "rxen_pin": 53, @@ -64,6 +66,7 @@ "reset_pin": 54, "busy_pin": 123, "irq_pin": 55, + "radio_timing_delay": 0.012, "en_pin": -1, "txen_pin": 52, "rxen_pin": 53, From b0b0be18c5259698a7b355a84d3d3e72b34fbb39 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:17:39 -0400 Subject: [PATCH 4/6] Preserve config comments on Buildroot --- buildroot-manage.sh | 87 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index 11b1fd9..f53f2f2 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -21,6 +21,7 @@ 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 +YQ_VERSION="${YQ_VERSION:-v4.44.3}" PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/pyMC_core.git}" PYMC_CORE_REF="${PYMC_CORE_REF:-}" RADIO_SETTINGS_JSON="$SCRIPT_DIR/radio-settings.json" @@ -364,6 +365,59 @@ install_system_packages() { libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev } +get_yq_cmd() { + local candidate + + for candidate in /usr/local/bin/yq /usr/bin/yq; do + if [ -x "$candidate" ] && "$candidate" --version 2>&1 | grep -q "mikefarah/yq"; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +ensure_yq() { + local yq_cmd yq_binary + + yq_cmd=$(get_yq_cmd 2>/dev/null || true) + if [ -n "$yq_cmd" ]; then + printf '%s\n' "$yq_cmd" + return 0 + fi + + need_cmd wget + + case "$(uname -m)" in + aarch64|arm64) yq_binary="yq_linux_arm64" ;; + x86_64|amd64) yq_binary="yq_linux_amd64" ;; + armv7l|armv7) yq_binary="yq_linux_arm" ;; + *) + warn "Unable to install yq automatically on unsupported architecture: $(uname -m)" + return 1 + ;; + esac + + stage "Installing yq" + info "Fetching mikefarah/yq ${YQ_VERSION}" + wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${yq_binary}" || { + warn "Failed to download yq; config updates will keep working but comments will not be preserved." + rm -f /usr/local/bin/yq + return 1 + } + chmod +x /usr/local/bin/yq + + yq_cmd=$(get_yq_cmd 2>/dev/null || true) + if [ -n "$yq_cmd" ]; then + printf '%s\n' "$yq_cmd" + return 0 + fi + + warn "Installed yq is not the expected mikefarah/yq binary." + return 1 +} + ensure_venv() { local recreate=0 @@ -951,13 +1005,22 @@ write_repeater_config() { local tx_power="$8" local board_key="$9" - python3 - "$CONFIG_DIR/config.yaml" "$RADIO_SETTINGS_JSON" "$BUILDROOT_RADIO_SETTINGS_JSON" "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$board_key" <<'PY' + local config_path temp_config example_config yq_cmd stripped_user temp_merged + + config_path="$CONFIG_DIR/config.yaml" + example_config="$CONFIG_DIR/config.yaml.example" + temp_config=$(mktemp) + stripped_user=$(mktemp) + temp_merged=$(mktemp) + + python3 - "$config_path" "$temp_config" "$RADIO_SETTINGS_JSON" "$BUILDROOT_RADIO_SETTINGS_JSON" "$node_name" "$admin_password" "$jwt_secret" "$freq_mhz" "$sf" "$bw_khz" "$coding_rate" "$tx_power" "$board_key" <<'PY' import json import sys import yaml ( config_path, + output_path, radio_settings_path, buildroot_settings_path, node_name, @@ -969,7 +1032,7 @@ import yaml coding_rate, tx_power, board_key, -) = sys.argv[1:13] +) = sys.argv[1:14] with open(config_path, "r", encoding="utf-8") as fh: data = yaml.safe_load(fh) or {} @@ -1016,9 +1079,25 @@ if data["radio_type"] == "sx1262_ch341": if "pid" in hardware: ch341["pid"] = hardware["pid"] -with open(config_path, "w", encoding="utf-8") as fh: +with open(output_path, "w", encoding="utf-8") as fh: yaml.safe_dump(data, fh, sort_keys=False) PY + + yq_cmd=$(ensure_yq 2>/dev/null || true) + if [ -n "$yq_cmd" ] && [ -f "$example_config" ]; then + "$yq_cmd" eval '... comments=""' "$temp_config" > "$stripped_user" 2>/dev/null || cp "$temp_config" "$stripped_user" + if "$yq_cmd" eval-all '. as $item ireduce ({}; . * $item)' "$example_config" "$stripped_user" > "$temp_merged" 2>/dev/null \ + && "$yq_cmd" eval '.' "$temp_merged" >/dev/null 2>&1; then + mv "$temp_merged" "$config_path" + else + warn "yq merge failed; writing config without preserving comments." + cp "$temp_config" "$config_path" + fi + else + cp "$temp_config" "$config_path" + fi + + rm -f "$temp_config" "$stripped_user" "$temp_merged" } seed_repeater_config() { @@ -1235,6 +1314,7 @@ install_repeater() { ensure_root stage "Preparing Buildroot installation" install_system_packages + ensure_yq >/dev/null 2>&1 || true ensure_service_user stage "Preparing directories and config" @@ -1324,6 +1404,7 @@ upgrade_repeater() { if [ "$current_version" = "$git_version" ] && [ "${PYMC_FORCE_REPEATER:-0}" != "1" ]; then info "pyMC Repeater is already at ${git_version}; skipping reinstall" else + ensure_yq >/dev/null 2>&1 || true install_repeater_package fi link_system_site_packages From 465372587bf42e68f2aa6c15e738cbb9cff1bd6b Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:20:22 -0400 Subject: [PATCH 5/6] Fix Buildroot yq comment merge --- buildroot-manage.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildroot-manage.sh b/buildroot-manage.sh index f53f2f2..2c4e69c 100644 --- a/buildroot-manage.sh +++ b/buildroot-manage.sh @@ -399,8 +399,8 @@ ensure_yq() { ;; esac - stage "Installing yq" - info "Fetching mikefarah/yq ${YQ_VERSION}" + printf '\n==> Installing yq\n' >&2 + printf ' - Fetching mikefarah/yq %s\n' "$YQ_VERSION" >&2 wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${yq_binary}" || { warn "Failed to download yq; config updates will keep working but comments will not be preserved." rm -f /usr/local/bin/yq From 8856268ef282812849cdaadadc8a3562586880c1 Mon Sep 17 00:00:00 2001 From: Yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:27:56 -0400 Subject: [PATCH 6/6] Expose Buildroot image version --- repeater/service_utils.py | 26 ++++++++++++++++++++++++-- repeater/web/api_endpoints.py | 7 +++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/repeater/service_utils.py b/repeater/service_utils.py index a127e48..9a9c4a1 100644 --- a/repeater/service_utils.py +++ b/repeater/service_utils.py @@ -6,14 +6,15 @@ Provides functions for service control operations like restart. import logging import os import subprocess -from typing import Tuple +from typing import Dict, Optional, Tuple logger = logging.getLogger("ServiceUtils") INIT_SCRIPT = "/etc/init.d/S80pymc-repeater" +BUILDROOT_METADATA_PATH = "/etc/pymc-image-build-id" def is_buildroot() -> bool: - if os.path.exists("/etc/pymc-image-build-id"): + if os.path.exists(BUILDROOT_METADATA_PATH): return True if os.path.exists("/etc/os-release"): try: @@ -24,6 +25,27 @@ def is_buildroot() -> bool: return False +def get_buildroot_image_info() -> Dict[str, str]: + info: Dict[str, str] = {} + + try: + with open(BUILDROOT_METADATA_PATH, "r", encoding="utf-8") as handle: + for line in handle: + line = line.strip() + if not line or "=" not in line: + continue + key, value = line.split("=", 1) + info[key.strip()] = value.strip() + except OSError: + return {} + + return info + + +def get_buildroot_image_version() -> Optional[str]: + return get_buildroot_image_info().get("image_version") + + def restart_service() -> Tuple[bool, str]: """ Restart the pymc-repeater service. diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index c18bc13..3379a74 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -16,6 +16,7 @@ from repeater.companion.identity_resolve import ( heal_companion_empty_names, ) from repeater.config import update_unscoped_flood_policy +from repeater.service_utils import get_buildroot_image_info from .auth.middleware import require_auth from .auth_endpoints import AuthAPIEndpoints @@ -625,6 +626,12 @@ class APIEndpoints: stats["core_version"] = pymc_core.__version__ except ImportError: stats["core_version"] = "unknown" + image_info = get_buildroot_image_info() + if image_info: + if image_info.get("image_name"): + stats["image_name"] = image_info["image_name"] + if image_info.get("image_version"): + stats["image_version"] = image_info["image_version"] return stats except Exception as e: logger.error(f"Error serving stats: {e}")