From 8f90cf71b0aa2a8988e843c7537fc10df169cad6 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Mon, 23 Feb 2026 21:58:15 +0000 Subject: [PATCH 01/16] Reference for E22p branch --- manage.sh | 4 ++-- pyproject.toml | 2 +- repeater/config.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manage.sh b/manage.sh index 7965452..457a07b 100755 --- a/manage.sh +++ b/manage.sh @@ -354,7 +354,7 @@ EOF echo "Note: Using optimized binary wheels for faster installation" echo "" - if pip install --break-system-packages --no-cache-dir .; then + if pip install --break-system-packages --no-build-isolation --ignore-installed --no-cache-dir .; then echo "" echo "✓ Python package installation completed successfully!" @@ -607,7 +607,7 @@ EOF echo "" # Upgrade packages (uses cache for unchanged dependencies - much faster) - if python3 -m pip install --break-system-packages --upgrade --upgrade-strategy eager .; then + if python3 -m pip install --break-system-packages --no-build-isolation --ignore-installed --upgrade --upgrade-strategy eager .; then echo "" echo "✓ Package and dependencies updated successfully!" else diff --git a/pyproject.toml b/pyproject.toml index bf28765..b65ff27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ keywords = ["mesh", "networking", "lora", "repeater", "daemon", "iot"] dependencies = [ - "pymc_core[hardware] @ git+https://github.com/rightup/pyMC_core.git@feat/newRadios", + "pymc_core[hardware] @ git+https://github.com/rightup/pyMC_core.git@feat/E22p", "pyyaml>=6.0.0", "cherrypy>=18.0.0", "paho-mqtt>=1.6.0", diff --git a/repeater/config.py b/repeater/config.py index b1bfe1b..e95deb0 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -247,6 +247,7 @@ def get_radio_for_board(board_config: dict): "rxen_pin": _parse_int(spi_config["rxen_pin"]), "txled_pin": _parse_int(spi_config.get("txled_pin", -1), default=-1), "rxled_pin": _parse_int(spi_config.get("rxled_pin", -1), default=-1), + "en_pin": _parse_int(spi_config.get("en_pin", -1), default=-1), "use_dio3_tcxo": spi_config.get("use_dio3_tcxo", False), "dio3_tcxo_voltage": float(spi_config.get("dio3_tcxo_voltage", 1.8)), "use_dio2_rf": spi_config.get("use_dio2_rf", False), From 5528ec44e1c1a89f17ece2fc4f959b4afa0b1f6d Mon Sep 17 00:00:00 2001 From: Matt <43940868+MSmithDev@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:05:05 -0500 Subject: [PATCH 02/16] Changed PiMesh settings --- radio-settings.json | 166 ++++++++------------------------------------ 1 file changed, 29 insertions(+), 137 deletions(-) diff --git a/radio-settings.json b/radio-settings.json index 88476ce..8154dcd 100644 --- a/radio-settings.json +++ b/radio-settings.json @@ -16,8 +16,8 @@ "preamble_length": 17, "is_waveshare": true }, - "uconsole_aiov1": { - "name": "uConsole LoRa Module aio v1", + "uconsole": { + "name": "uConsole LoRa Module", "bus_id": 1, "cs_id": 0, "cs_pin": -1, @@ -31,53 +31,38 @@ "tx_power": 22, "preamble_length": 17 }, - "uconsole_aio_v2": { - "name": "uConsole LoRa Module aio v2", - "bus_id": 1, + "pimesh-1w-v1": { + "name": "PiMesh-1W (V1)", + "bus_id": 0, "cs_id": 0, - "cs_pin": -1, - "reset_pin": 25, - "busy_pin": 24, - "irq_pin": 26, + "cs_pin": 21, + "reset_pin": 18, + "busy_pin": 20, + "irq_pin": 16, + "txen_pin": 13, + "rxen_pin": 12, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 22, + "use_dio3_tcxo": true, + "preamble_length": 17 + }, + "pimesh-1w-v2": { + "name": "PiMesh-1W (V2)", + "bus_id": 0, + "cs_id": 0, + "cs_pin": 8, + "reset_pin": 18, + "busy_pin": 5, + "irq_pin": 6, "txen_pin": -1, "rxen_pin": -1, "txled_pin": -1, "rxled_pin": -1, - "tx_power": 22, - "preamble_length": 17, - "use_dio3_tcxo": true, - "use_dio2_rf": true - }, - "pimesh-1w-usa": { - "name": "PiMesh-1W (USA)", - "bus_id": 0, - "cs_id": 0, - "cs_pin": 21, - "reset_pin": 18, - "busy_pin": 20, - "irq_pin": 16, - "txen_pin": 13, - "rxen_pin": 12, - "txled_pin": -1, - "rxled_pin": -1, - "tx_power": 30, - "use_dio3_tcxo": true, - "preamble_length": 17 - }, - "pimesh-1w-uk": { - "name": "PiMesh-1W (UK)", - "bus_id": 0, - "cs_id": 0, - "cs_pin": 21, - "reset_pin": 18, - "busy_pin": 20, - "irq_pin": 16, - "txen_pin": 13, - "rxen_pin": 12, - "txled_pin": -1, - "rxled_pin": -1, + "en_pin": 26, "tx_power": 22, "use_dio3_tcxo": true, + "use_dio2_rf": true, "preamble_length": 17 }, "meshadv-mini": { @@ -95,7 +80,7 @@ "tx_power": 22, "preamble_length": 17 }, - "meshadv": { + "meshadv": { "name": "MeshAdv", "bus_id": 0, "cs_id": 0, @@ -110,99 +95,6 @@ "tx_power": 22, "use_dio3_tcxo": true, "preamble_length": 17 - }, - "zebra": { - "name": "ZebraHat-1W", - "bus_id": 0, - "cs_id": 0, - "cs_pin": 24, - "reset_pin": 17, - "busy_pin": 27, - "irq_pin": 22, - "txen_pin": -1, - "rxen_pin": -1, - "txled_pin": -1, - "rxled_pin": -1, - "tx_power": 18, - "use_dio3_tcxo": true, - "use_dio2_rf": true, - "preamble_length": 17 - }, - "femtofox-1W-SX": { - "name": "FemtoFox SX1262 (1W)", - "bus_id": 0, - "cs_id": 0, - "cs_pin": 16, - "gpio_chip": 1, - "use_gpiod_backend": true, - "reset_pin": 25, - "busy_pin": 22, - "irq_pin": 23, - "txen_pin": -1, - "rxen_pin": 24, - "txled_pin": -1, - "rxled_pin": -1, - "tx_power": 30, - "use_dio3_tcxo": true, - "preamble_length": 17 - }, - "femtofox-2W-SX": { - "name": "FemtoFox SX1262 (2W)", - "bus_id": 0, - "cs_id": 0, - "cs_pin": 16, - "gpio_chip": 1, - "use_gpiod_backend": true, - "reset_pin": 25, - "busy_pin": 22, - "irq_pin": 23, - "txen_pin": -1, - "rxen_pin": 24, - "txled_pin": -1, - "rxled_pin": -1, - "tx_power": 8, - "use_dio2_rf": true, - "use_dio3_tcxo": true - }, - "nebrahat": { - "name": "NebraHat-2W", - "bus_id": 0, - "cs_id": 0, - "cs_pin": 8, - "reset_pin": 18, - "busy_pin": 4, - "irq_pin": 22, - "txen_pin": -1, - "rxen_pin": 25, - "txled_pin": -1, - "rxled_pin": -1, - "tx_power": 8, - "use_dio3_tcxo": true, - "use_dio2_rf": true, - "preamble_length": 17 - }, - "ch341-usb-sx1262": { - "name": "CH341 USB-SPI + SX1262 (example)", - "description": "SX1262 via CH341 USB-to-SPI adapter. NOTE: pin numbers are CH341 GPIO 0-7, not BCM.", - "radio_type": "sx1262_ch341", - "vid": 6790, - "pid": 21778, - "bus_id": 0, - "cs_id": 0, - "cs_pin": 0, - "reset_pin": 2, - "busy_pin": 4, - "irq_pin": 6, - "txen_pin": -1, - "rxen_pin": 1, - "txled_pin": -1, - "rxled_pin": -1, - "tx_power": 22, - "use_dio2_rf": true, - "use_dio3_tcxo": true, - "dio3_tcxo_voltage": 1.8, - "preamble_length": 17, - "is_waveshare": false } } -} +} \ No newline at end of file From 481b3e9718fd572efb5d56be861beddafea474a5 Mon Sep 17 00:00:00 2001 From: Matt <43940868+MSmithDev@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:08:46 -0500 Subject: [PATCH 03/16] Update uconsole configuration and add new modules --- radio-settings.json | 118 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 4 deletions(-) diff --git a/radio-settings.json b/radio-settings.json index 8154dcd..3e72ab2 100644 --- a/radio-settings.json +++ b/radio-settings.json @@ -16,8 +16,8 @@ "preamble_length": 17, "is_waveshare": true }, - "uconsole": { - "name": "uConsole LoRa Module", + "uconsole_aiov1": { + "name": "uConsole LoRa Module aio v1", "bus_id": 1, "cs_id": 0, "cs_pin": -1, @@ -30,6 +30,23 @@ "rxled_pin": -1, "tx_power": 22, "preamble_length": 17 + }, + "uconsole_aio_v2": { + "name": "uConsole LoRa Module aio v2", + "bus_id": 1, + "cs_id": 0, + "cs_pin": -1, + "reset_pin": 25, + "busy_pin": 24, + "irq_pin": 26, + "txen_pin": -1, + "rxen_pin": -1, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 22, + "preamble_length": 17, + "use_dio3_tcxo": true, + "use_dio2_rf": true }, "pimesh-1w-v1": { "name": "PiMesh-1W (V1)", @@ -80,7 +97,7 @@ "tx_power": 22, "preamble_length": 17 }, - "meshadv": { + "meshadv": { "name": "MeshAdv", "bus_id": 0, "cs_id": 0, @@ -95,6 +112,99 @@ "tx_power": 22, "use_dio3_tcxo": true, "preamble_length": 17 + }, + "zebra": { + "name": "ZebraHat-1W", + "bus_id": 0, + "cs_id": 0, + "cs_pin": 24, + "reset_pin": 17, + "busy_pin": 27, + "irq_pin": 22, + "txen_pin": -1, + "rxen_pin": -1, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 18, + "use_dio3_tcxo": true, + "use_dio2_rf": true, + "preamble_length": 17 + }, + "femtofox-1W-SX": { + "name": "FemtoFox SX1262 (1W)", + "bus_id": 0, + "cs_id": 0, + "cs_pin": 16, + "gpio_chip": 1, + "use_gpiod_backend": true, + "reset_pin": 25, + "busy_pin": 22, + "irq_pin": 23, + "txen_pin": -1, + "rxen_pin": 24, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 30, + "use_dio3_tcxo": true, + "preamble_length": 17 + }, + "femtofox-2W-SX": { + "name": "FemtoFox SX1262 (2W)", + "bus_id": 0, + "cs_id": 0, + "cs_pin": 16, + "gpio_chip": 1, + "use_gpiod_backend": true, + "reset_pin": 25, + "busy_pin": 22, + "irq_pin": 23, + "txen_pin": -1, + "rxen_pin": 24, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 8, + "use_dio2_rf": true, + "use_dio3_tcxo": true + }, + "nebrahat": { + "name": "NebraHat-2W", + "bus_id": 0, + "cs_id": 0, + "cs_pin": 8, + "reset_pin": 18, + "busy_pin": 4, + "irq_pin": 22, + "txen_pin": -1, + "rxen_pin": 25, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 8, + "use_dio3_tcxo": true, + "use_dio2_rf": true, + "preamble_length": 17 + }, + "ch341-usb-sx1262": { + "name": "CH341 USB-SPI + SX1262 (example)", + "description": "SX1262 via CH341 USB-to-SPI adapter. NOTE: pin numbers are CH341 GPIO 0-7, not BCM.", + "radio_type": "sx1262_ch341", + "vid": 6790, + "pid": 21778, + "bus_id": 0, + "cs_id": 0, + "cs_pin": 0, + "reset_pin": 2, + "busy_pin": 4, + "irq_pin": 6, + "txen_pin": -1, + "rxen_pin": 1, + "txled_pin": -1, + "rxled_pin": -1, + "tx_power": 22, + "use_dio2_rf": true, + "use_dio3_tcxo": true, + "dio3_tcxo_voltage": 1.8, + "preamble_length": 17, + "is_waveshare": false } } -} \ No newline at end of file +} From edebea6bdaaef24a16a5edff0050398fcf4a291f Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 08:49:51 +0000 Subject: [PATCH 04/16] Add user to plugdev group and install udev rules for CH341 during installation and upgrade --- manage.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/manage.sh b/manage.sh index 457a07b..3f6509a 100755 --- a/manage.sh +++ b/manage.sh @@ -235,6 +235,7 @@ install_repeater() { echo "10"; echo "# Adding user to hardware groups..." usermod -a -G gpio,i2c,spi "$SERVICE_USER" 2>/dev/null || true usermod -a -G dialout "$SERVICE_USER" 2>/dev/null || true + usermod -a -G plugdev "$SERVICE_USER" 2>/dev/null || true echo "20"; echo "# Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater @@ -296,6 +297,13 @@ install_repeater() { echo "55"; echo "# Installing systemd service..." cp "$SCRIPT_DIR/pymc-repeater.service" /etc/systemd/system/ systemctl daemon-reload + + echo "60"; echo "# Installing udev rules for CH341..." + if [ -f "$SCRIPT_DIR/../pyMC_core/99-ch341.rules" ]; then + cp "$SCRIPT_DIR/../pyMC_core/99-ch341.rules" /etc/udev/rules.d/99-ch341.rules + udevadm control --reload-rules 2>/dev/null || true + udevadm trigger 2>/dev/null || true + fi echo "65"; echo "# Setting permissions..." chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater @@ -553,6 +561,22 @@ upgrade_repeater() { echo " ⚠ Configuration validation failed, keeping existing config" fi + echo "[5.5/9] Ensuring user groups and udev rules..." + usermod -a -G gpio,i2c,spi "$SERVICE_USER" 2>/dev/null || true + usermod -a -G dialout "$SERVICE_USER" 2>/dev/null || true + usermod -a -G plugdev "$SERVICE_USER" 2>/dev/null || true + # Install/update CH341 udev rules + SCRIPT_DIR_UPGRADE="$(cd "$(dirname "$0")" && pwd)" + if [ -f "$SCRIPT_DIR_UPGRADE/../pyMC_core/99-ch341.rules" ]; then + cp "$SCRIPT_DIR_UPGRADE/../pyMC_core/99-ch341.rules" /etc/udev/rules.d/99-ch341.rules + udevadm control --reload-rules 2>/dev/null || true + udevadm trigger 2>/dev/null || true + echo " ✓ CH341 udev rules updated" + elif [ -f /etc/udev/rules.d/99-ch341.rules ]; then + echo " ✓ CH341 udev rules already present" + fi + echo " ✓ User groups updated" + echo "[6/9] Fixing permissions..." chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater 2>/dev/null || true chmod 750 "$CONFIG_DIR" "$LOG_DIR" 2>/dev/null || true From 540e5f11fe7b8cedd290cce0beee26384ba43197 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 08:53:12 +0000 Subject: [PATCH 05/16] Add supplementary groups to systemd service --- pymc-repeater.service | 1 + 1 file changed, 1 insertion(+) diff --git a/pymc-repeater.service b/pymc-repeater.service index 8c07d40..774d707 100644 --- a/pymc-repeater.service +++ b/pymc-repeater.service @@ -31,6 +31,7 @@ SyslogIdentifier=pymc-repeater # Security (relaxed for proper operation) NoNewPrivileges=true ReadWritePaths=/var/log/pymc_repeater /var/lib/pymc_repeater /etc/pymc_repeater +SupplementaryGroups=plugdev dialout gpio i2c spi [Install] WantedBy=multi-user.target From 4d2943087f24c67c79f6c6609d7315eb8d8516e7 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 09:15:21 +0000 Subject: [PATCH 06/16] Refactor user group management and add container detection warnings for udev rules --- manage.sh | 36 +++++++++++++++++++++++++++++------- pymc-repeater.service | 2 +- repeater/main.py | 18 ++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/manage.sh b/manage.sh index 3f6509a..4a64a24 100755 --- a/manage.sh +++ b/manage.sh @@ -233,9 +233,9 @@ install_repeater() { fi echo "10"; echo "# Adding user to hardware groups..." - usermod -a -G gpio,i2c,spi "$SERVICE_USER" 2>/dev/null || true - usermod -a -G dialout "$SERVICE_USER" 2>/dev/null || true - usermod -a -G plugdev "$SERVICE_USER" 2>/dev/null || true + 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 + done echo "20"; echo "# Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater @@ -402,6 +402,23 @@ EOF echo " • Set admin password" echo " 3. Log in to your configured repeater" echo "" + # Container detection: warn about host-side udev rules + if [ -f /run/host/container-manager ] || [ -n "${container:-}" ] || grep -qsai 'container=' /proc/1/environ 2>/dev/null || [ -f /.dockerenv ]; then + echo "═══════════════════════════════════════════════════════════════" + echo " ⚠ CONTAINER ENVIRONMENT DETECTED" + echo "═══════════════════════════════════════════════════════════════" + echo "" + echo " USB device udev rules do NOT work inside containers." + echo " You MUST install the CH341 udev rule on the HOST machine:" + echo "" + echo " echo 'SUBSYSTEM==\"usb\", ATTR{idVendor}==\"1a86\", ATTR{idProduct}==\"5512\", MODE=\"0666\"' \\" + echo " | sudo tee /etc/udev/rules.d/99-ch341.rules" + echo " sudo udevadm control --reload-rules" + echo " sudo udevadm trigger --subsystem-match=usb --action=change" + echo "" + echo " Then unplug and replug the CH341 USB adapter." + echo "" + fi echo "═══════════════════════════════════════════════════════════════" echo "" read -p "Press Enter to return to main menu..." || true @@ -562,9 +579,9 @@ upgrade_repeater() { fi echo "[5.5/9] Ensuring user groups and udev rules..." - usermod -a -G gpio,i2c,spi "$SERVICE_USER" 2>/dev/null || true - usermod -a -G dialout "$SERVICE_USER" 2>/dev/null || true - usermod -a -G plugdev "$SERVICE_USER" 2>/dev/null || true + 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 + done # Install/update CH341 udev rules SCRIPT_DIR_UPGRADE="$(cd "$(dirname "$0")" && pwd)" if [ -f "$SCRIPT_DIR_UPGRADE/../pyMC_core/99-ch341.rules" ]; then @@ -656,7 +673,12 @@ EOF if is_running; then echo " ✓ Service is running" - show_info "Upgrade Complete" "Upgrade completed successfully!\n\nVersion: $current_version → $new_version\n\n✓ Service is running\n✓ Configuration preserved" + # Container detection: warn about host-side udev rules + local container_note="" + if [ -f /run/host/container-manager ] || [ -n "${container:-}" ] || grep -qsai 'container=' /proc/1/environ 2>/dev/null || [ -f /.dockerenv ]; then + container_note="\n\n⚠ CONTAINER DETECTED:\nUSB udev rules must be set on the HOST, not here.\nSee documentation for CH341 host-side setup." + fi + show_info "Upgrade Complete" "Upgrade completed successfully!\n\nVersion: $current_version → $new_version\n\n✓ Service is running\n✓ Configuration preserved${container_note}" else echo " ✗ Service failed to start" show_error "Upgrade completed but service failed to start!\n\nVersion updated: $current_version → $new_version\n\nCheck logs from the main menu for details." diff --git a/pymc-repeater.service b/pymc-repeater.service index 774d707..3cbd51d 100644 --- a/pymc-repeater.service +++ b/pymc-repeater.service @@ -31,7 +31,7 @@ SyslogIdentifier=pymc-repeater # Security (relaxed for proper operation) NoNewPrivileges=true ReadWritePaths=/var/log/pymc_repeater /var/lib/pymc_repeater /etc/pymc_repeater -SupplementaryGroups=plugdev dialout gpio i2c spi +SupplementaryGroups=plugdev dialout [Install] WantedBy=multi-user.target diff --git a/repeater/main.py b/repeater/main.py index cc736b0..3808f17 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -491,10 +491,28 @@ class RepeaterDaemon: except Exception as e: logger.debug(f"CH341 reset skipped/failed: {e}") + @staticmethod + def _detect_container() -> bool: + """Detect if running inside an LXC/Docker/systemd-nspawn container.""" + try: + with open("/proc/1/environ", "rb") as f: + if b"container=" in f.read(): + return True + except (OSError, PermissionError): + pass + return os.path.exists("/run/host/container-manager") + async def run(self): logger.info("Repeater daemon started") + # Warn if running inside a container (udev rules won't work here) + if os.path.exists("/.dockerenv") or os.environ.get("container") or self._detect_container(): + logger.warning( + "Container environment detected. " + "USB device udev rules must be configured on the HOST, not inside this container." + ) + try: await self.initialize() From 496c5a960af369f9c73e3f95b6bc917eb4465e39 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 09:29:43 +0000 Subject: [PATCH 07/16] Add Proxmox LXC installer script for pyMC Repeater with USB passthrough --- scripts/proxmox-install.sh | 203 +++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 scripts/proxmox-install.sh diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh new file mode 100644 index 0000000..3c1ca41 --- /dev/null +++ b/scripts/proxmox-install.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# pyMC Repeater - Proxmox LXC Installer +# Creates an LXC container with USB passthrough and installs pyMC Repeater +# +# Usage (run on the Proxmox host): +# bash -c "$(curl -fsSL https://raw.githubusercontent.com/rightup/pyMC_Repeater/main/scripts/proxmox-install.sh)" +# +# License: MIT +# Source: https://github.com/rightup/pyMC_Repeater + +set -euo pipefail + +# ── Defaults ─────────────────────────────────────────────────────────────── +REPO="https://github.com/rightup/pyMC_Repeater.git" +BRANCH="feat/E22p" +CT_TEMPLATE="debian-12-standard" +CT_RAM=1024 +CT_SWAP=512 +CT_DISK=4 +CT_CORES=2 +CT_HOSTNAME="pymc-repeater" +CT_BRIDGE="vmbr0" +CT_STORAGE="local-lvm" +CT_TEMPLATE_STORAGE="local" +CH341_VID="1a86" +CH341_PID="5512" + +# ── Colors ───────────────────────────────────────────────────────────────── +RD="\033[01;31m" GN="\033[1;92m" YW="\033[33m" BL="\033[36m" BLD="\033[1m" CL="\033[m" + +msg_info() { echo -e " ${BL}ℹ${CL} ${1}"; } +msg_ok() { echo -e " ${GN}✓${CL} ${1}"; } +msg_warn() { echo -e " ${YW}⚠${CL} ${1}"; } +msg_error() { echo -e " ${RD}✗${CL} ${1}"; } + +header() { + clear + echo -e "${BLD}" + echo "═══════════════════════════════════════════════════════════════" + echo " pyMC Repeater - Proxmox LXC Installer" + echo "═══════════════════════════════════════════════════════════════" + echo -e "${CL}" +} + +cleanup() { + local exit_code=$? + if [ $exit_code -ne 0 ] && [ -n "${CTID:-}" ] && pct status "$CTID" &>/dev/null; then + echo "" + read -p " Delete the failed container ${CTID}? [y/N]: " -r + if [[ "$REPLY" =~ ^[Yy]$ ]]; then + pct stop "$CTID" 2>/dev/null || true + pct destroy "$CTID" 2>/dev/null || true + msg_ok "Container ${CTID} removed" + fi + fi +} +trap cleanup EXIT + +# ── Preflight checks ────────────────────────────────────────────────────── +header + +if ! command -v pct &>/dev/null; then + msg_error "This script must be run on a Proxmox VE host." + exit 1 +fi + +if [ "$EUID" -ne 0 ]; then + msg_error "Please run as root" + exit 1 +fi + +msg_ok "Running on Proxmox host as root" + +# Check for CH341 +echo "" +if lsusb -d "${CH341_VID}:${CH341_PID}" &>/dev/null; then + msg_ok "CH341 USB device detected" +else + msg_warn "CH341 USB device not found — plug it in before starting the repeater" +fi + +# ── Interactive settings ────────────────────────────────────────────────── +echo "" +echo -e "${BLD}Container Settings${CL} (press Enter for defaults):" +echo "" + +read -p " Hostname [${CT_HOSTNAME}]: " -r input; CT_HOSTNAME="${input:-$CT_HOSTNAME}" +read -p " RAM in MB [${CT_RAM}]: " -r input; CT_RAM="${input:-$CT_RAM}" +read -p " Disk in GB [${CT_DISK}]: " -r input; CT_DISK="${input:-$CT_DISK}" +read -p " CPU cores [${CT_CORES}]: " -r input; CT_CORES="${input:-$CT_CORES}" +read -p " Bridge [${CT_BRIDGE}]: " -r input; CT_BRIDGE="${input:-$CT_BRIDGE}" + +AVAILABLE_STORAGES=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {print $1}' || echo "local-lvm") +echo " Available storages: ${AVAILABLE_STORAGES}" +read -p " Storage [${CT_STORAGE}]: " -r input; CT_STORAGE="${input:-$CT_STORAGE}" +read -p " Git branch [${BRANCH}]: " -r input; BRANCH="${input:-$BRANCH}" + +# ── Get next CTID ───────────────────────────────────────────────────────── +CTID=$(pvesh get /cluster/nextid) + +# ── Confirmation ────────────────────────────────────────────────────────── +echo "" +echo -e "${BLD}Summary:${CL}" +echo " CTID: ${CTID} Host: ${CT_HOSTNAME} RAM: ${CT_RAM}MB Disk: ${CT_DISK}GB" +echo " Cores: ${CT_CORES} Storage: ${CT_STORAGE} Bridge: ${CT_BRIDGE} Branch: ${BRANCH}" +echo " Mode: privileged (required for USB passthrough)" +echo "" +read -p " Proceed? [Y/n]: " -r +[[ "${REPLY:-Y}" =~ ^[Nn]$ ]] && { msg_warn "Aborted"; exit 0; } + +# ── Download template ───────────────────────────────────────────────────── +echo "" +msg_info "Downloading Debian 12 template..." +TEMPLATE_FILE=$(pveam available -section system 2>/dev/null | grep "${CT_TEMPLATE}" | sort -t- -k4 -V | tail -1 | awk '{print $2}') +[ -z "$TEMPLATE_FILE" ] && { msg_error "Template not found. Run: pveam update"; exit 1; } + +pveam list "$CT_TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE_FILE" || \ + pveam download "$CT_TEMPLATE_STORAGE" "$TEMPLATE_FILE" +msg_ok "Template ready" + +# ── Create container ────────────────────────────────────────────────────── +msg_info "Creating LXC container ${CTID}..." +pct create "$CTID" "${CT_TEMPLATE_STORAGE}:vztmpl/${TEMPLATE_FILE}" \ + --hostname "$CT_HOSTNAME" \ + --memory "$CT_RAM" \ + --swap "$CT_SWAP" \ + --cores "$CT_CORES" \ + --rootfs "${CT_STORAGE}:${CT_DISK}" \ + --net0 "name=eth0,bridge=${CT_BRIDGE},ip=dhcp" \ + --unprivileged 0 \ + --features nesting=1 \ + --onboot 1 \ + --start 0 \ + --ostype debian +msg_ok "Container created" + +# ── USB passthrough ─────────────────────────────────────────────────────── +msg_info "Configuring USB passthrough..." +cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF' + +# CH341 USB passthrough for pyMC Repeater +lxc.cgroup2.devices.allow: c 189:* rwm +lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir 0 0 +EOF +msg_ok "USB passthrough configured" + +# ── Host udev rule ──────────────────────────────────────────────────────── +msg_info "Installing CH341 udev rule on host..." +echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="5512", MODE="0666"' \ + > /etc/udev/rules.d/99-ch341.rules +udevadm control --reload-rules +udevadm trigger --subsystem-match=usb --action=change +msg_ok "Host udev rule installed" + +# ── Start container & wait for network ──────────────────────────────────── +msg_info "Starting container..." +pct start "$CTID" +sleep 3 +for _ in $(seq 1 30); do + pct exec "$CTID" -- ping -c1 -W1 8.8.8.8 &>/dev/null && break + sleep 1 +done +msg_ok "Container running with network" + +# ── Bootstrap: install git, clone repo ──────────────────────────────────── +msg_info "Installing git inside container..." +pct exec "$CTID" -- bash -c " + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq && apt-get install -y git whiptail >/dev/null 2>&1 +" +msg_ok "Git installed" + +msg_info "Cloning pyMC_Repeater (branch: ${BRANCH})..." +pct exec "$CTID" -- bash -c "git clone --branch ${BRANCH} ${REPO} /root/pyMC_Repeater" +msg_ok "Repository cloned" + +# ── Run manage.sh install ───────────────────────────────────────────────── +msg_info "Running manage.sh install (this will take several minutes)..." +echo "" +# Use lxc-attach with a pty so manage.sh gets an interactive terminal +lxc-attach -n "$CTID" -- bash -c "cd /root/pyMC_Repeater && TERM=xterm bash manage.sh install" +echo "" +msg_ok "manage.sh install completed" + +# ── Get container IP ────────────────────────────────────────────────────── +sleep 2 +CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') + +# ── Done ────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BLD}" +echo "═══════════════════════════════════════════════════════════════" +echo " ✓ pyMC Repeater Installation Complete!" +echo "═══════════════════════════════════════════════════════════════" +echo -e "${CL}" +echo -e " Container: ${GN}${CTID}${CL} (${CT_HOSTNAME})" +echo -e " IP Address: ${GN}${CT_IP:-unknown}${CL}" +echo -e " Dashboard: ${GN}http://${CT_IP:-}:8000${CL}" +echo "" +echo " Next: open the dashboard and complete the setup wizard" +echo " Management: pct enter ${CTID}, then: cd /opt/pymc_repeater && bash manage.sh" +echo "" +echo "═══════════════════════════════════════════════════════════════" From 0f54a05596d1990dde5235568ec9df7a41c50ab3 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 09:47:36 +0000 Subject: [PATCH 08/16] Proxmox installer PW --- manage.sh | 11 +++++++++-- scripts/proxmox-install.sh | 14 +++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/manage.sh b/manage.sh index 4a64a24..7e0838c 100755 --- a/manage.sh +++ b/manage.sh @@ -182,9 +182,16 @@ install_repeater() { # Welcome screen $DIALOG --backtitle "pyMC Repeater Management" --title "Welcome" --msgbox "\nWelcome to pyMC Repeater Setup\n\nThis installer will configure your Linux system as a LoRa mesh network repeater.\n\nPress OK to continue..." 12 70 - # SPI Check - Universal approach that works on all boards + # SPI Check - skip for CH341 USB-SPI adapter (handles SPI over USB) SPI_MISSING=0 - if ! ls /dev/spidev* >/dev/null 2>&1; then + USES_CH341=0 + if [ -f "$CONFIG_DIR/config.yaml" ]; then + if grep -q "radio_type:.*sx1262_ch341" "$CONFIG_DIR/config.yaml" 2>/dev/null; then + USES_CH341=1 + fi + fi + + if [ "$USES_CH341" -eq 0 ] && ! ls /dev/spidev* >/dev/null 2>&1; then # SPI devices not found, check if we're on a Raspberry Pi and can enable it CONFIG_FILE="" if [ -f "/boot/firmware/config.txt" ]; then diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 3c1ca41..f046d46 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -94,6 +94,8 @@ AVAILABLE_STORAGES=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {prin echo " Available storages: ${AVAILABLE_STORAGES}" read -p " Storage [${CT_STORAGE}]: " -r input; CT_STORAGE="${input:-$CT_STORAGE}" read -p " Git branch [${BRANCH}]: " -r input; BRANCH="${input:-$BRANCH}" +read -sp " Root password [pymc]: " CT_PASSWORD; echo +CT_PASSWORD="${CT_PASSWORD:-pymc}" # ── Get next CTID ───────────────────────────────────────────────────────── CTID=$(pvesh get /cluster/nextid) @@ -131,6 +133,7 @@ pct create "$CTID" "${CT_TEMPLATE_STORAGE}:vztmpl/${TEMPLATE_FILE}" \ --features nesting=1 \ --onboot 1 \ --start 0 \ + --password "$CT_PASSWORD" \ --ostype debian msg_ok "Container created" @@ -167,8 +170,17 @@ msg_info "Installing git inside container..." pct exec "$CTID" -- bash -c " export DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y git whiptail >/dev/null 2>&1 + + # Enable auto-login on console (no password prompt in Proxmox web console) + mkdir -p /etc/systemd/system/container-getty@1.service.d + cat > /etc/systemd/system/container-getty@1.service.d/override.conf <<'AUTOLOGIN' +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM +AUTOLOGIN + systemctl daemon-reload " -msg_ok "Git installed" +msg_ok "Git installed, console auto-login enabled" msg_info "Cloning pyMC_Repeater (branch: ${BRANCH})..." pct exec "$CTID" -- bash -c "git clone --branch ${BRANCH} ${REPO} /root/pyMC_Repeater" From b4f0a42eb226ef730f3f3ba4ebb1b30201d8517c Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 09:59:43 +0000 Subject: [PATCH 09/16] Fix locale warnings and pre-seed config for CH341 in Proxmox installer --- scripts/proxmox-install.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index f046d46..aa84e23 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -169,7 +169,15 @@ msg_ok "Container running with network" msg_info "Installing git inside container..." pct exec "$CTID" -- bash -c " export DEBIAN_FRONTEND=noninteractive - apt-get update -qq && apt-get install -y git whiptail >/dev/null 2>&1 + + # Fix locale warnings + apt-get update -qq + apt-get install -y locales >/dev/null 2>&1 + sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen + locale-gen >/dev/null 2>&1 + echo 'LANG=en_US.UTF-8' > /etc/default/locale + + apt-get install -y git whiptail >/dev/null 2>&1 # Enable auto-login on console (no password prompt in Proxmox web console) mkdir -p /etc/systemd/system/container-getty@1.service.d @@ -180,12 +188,21 @@ ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,3840 AUTOLOGIN systemctl daemon-reload " -msg_ok "Git installed, console auto-login enabled" +msg_ok "Git installed, locale fixed, console auto-login enabled" msg_info "Cloning pyMC_Repeater (branch: ${BRANCH})..." pct exec "$CTID" -- bash -c "git clone --branch ${BRANCH} ${REPO} /root/pyMC_Repeater" msg_ok "Repository cloned" +# Pre-seed config with CH341 radio type so manage.sh skips the SPI check +pct exec "$CTID" -- bash -c " + mkdir -p /etc/pymc_repeater + if [ -f /root/pyMC_Repeater/config.yaml.example ]; then + cp /root/pyMC_Repeater/config.yaml.example /etc/pymc_repeater/config.yaml + sed -i 's/^radio_type:.*/radio_type: sx1262_ch341/' /etc/pymc_repeater/config.yaml + fi +" + # ── Run manage.sh install ───────────────────────────────────────────────── msg_info "Running manage.sh install (this will take several minutes)..." echo "" From b292a2a7107820fe6d5a27fc460228314840dbb1 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 10:26:57 +0000 Subject: [PATCH 10/16] Add login banner and CH341 configuration to Proxmox installer script --- scripts/proxmox-install.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index aa84e23..0308edb 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -187,6 +187,27 @@ ExecStart= ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM AUTOLOGIN systemctl daemon-reload + + # Login banner with system info + cat > /etc/profile.d/pymc-motd.sh <<'MOTD' +#!/bin/sh +HOSTNAME=\$(hostname) +IP=\$(hostname -I | awk '{print \$1}') +OS=\$(. /etc/os-release && echo \"\$NAME\") +VER=\$(. /etc/os-release && echo \"\$VERSION_ID\") +echo \"\" +echo \" pyMC Repeater LXC Container\" +echo \" 🌐 GitHub: https://github.com/rightup/pyMC_Repeater\" +echo \"\" +echo \" 🖥️ OS: \$OS - Version: \$VER\" +echo \" 🏠 Hostname: \$HOSTNAME\" +echo \" 💡 IP Address: \$IP\" +echo \" 📡 Dashboard: http://\$IP:8000\" +echo \"\" +echo \" Management: cd /opt/pymc_repeater && bash manage.sh\" +echo \"\" +MOTD + chmod +x /etc/profile.d/pymc-motd.sh " msg_ok "Git installed, locale fixed, console auto-login enabled" @@ -200,6 +221,17 @@ pct exec "$CTID" -- bash -c " if [ -f /root/pyMC_Repeater/config.yaml.example ]; then cp /root/pyMC_Repeater/config.yaml.example /etc/pymc_repeater/config.yaml sed -i 's/^radio_type:.*/radio_type: sx1262_ch341/' /etc/pymc_repeater/config.yaml + # Add CH341 section if not present + if ! grep -q '^ch341:' /etc/pymc_repeater/config.yaml; then + cat >> /etc/pymc_repeater/config.yaml <<'CH341CFG' + +# CH341 USB-to-SPI adapter settings (only used when radio_type: sx1262_ch341) +# NOTE: VID/PID are integers. Hex is also accepted in YAML, e.g. 0x1A86. +ch341: + vid: 6790 # 0x1A86 + pid: 21778 # 0x5512 +CH341CFG + fi fi " From 169ea4b1e4627241a288259858df62a5f642fe5a Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 10:59:30 +0000 Subject: [PATCH 11/16] Refactor config merging process to strip comments and update radio_type in Proxmox installer --- manage.sh | 11 +++++++++-- scripts/proxmox-install.sh | 13 +------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/manage.sh b/manage.sh index 7e0838c..6cc99f5 100755 --- a/manage.sh +++ b/manage.sh @@ -902,7 +902,14 @@ validate_and_update_config() { # - Adds missing keys from the left operand (example config) local temp_merged="${config_file}.merged" - if "$YQ_CMD" eval-all '. as $item ireduce ({}; . * $item)' "$updated_example" "$config_file" > "$temp_merged" 2>/dev/null; then + # Strip comments from user config before merge to prevent comment accumulation. + # yq preserves comments from both files, so each upgrade cycle would duplicate + # the header and inline comments. We keep only the example's comments. + local stripped_user="${config_file}.stripped" + "$YQ_CMD" eval '... comments=""' "$config_file" > "$stripped_user" 2>/dev/null || cp "$config_file" "$stripped_user" + + if "$YQ_CMD" eval-all '. as $item ireduce ({}; . * $item)' "$updated_example" "$stripped_user" > "$temp_merged" 2>/dev/null; then + rm -f "$stripped_user" # Verify the merged file is valid YAML if "$YQ_CMD" eval '.' "$temp_merged" > /dev/null 2>&1; then mv "$temp_merged" "$config_file" @@ -917,7 +924,7 @@ validate_and_update_config() { fi else echo " ✗ Config merge failed, keeping original" - rm -f "$temp_merged" + rm -f "$temp_merged" "$stripped_user" return 1 fi } diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 0308edb..27cce2c 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -220,18 +220,7 @@ pct exec "$CTID" -- bash -c " mkdir -p /etc/pymc_repeater if [ -f /root/pyMC_Repeater/config.yaml.example ]; then cp /root/pyMC_Repeater/config.yaml.example /etc/pymc_repeater/config.yaml - sed -i 's/^radio_type:.*/radio_type: sx1262_ch341/' /etc/pymc_repeater/config.yaml - # Add CH341 section if not present - if ! grep -q '^ch341:' /etc/pymc_repeater/config.yaml; then - cat >> /etc/pymc_repeater/config.yaml <<'CH341CFG' - -# CH341 USB-to-SPI adapter settings (only used when radio_type: sx1262_ch341) -# NOTE: VID/PID are integers. Hex is also accepted in YAML, e.g. 0x1A86. -ch341: - vid: 6790 # 0x1A86 - pid: 21778 # 0x5512 -CH341CFG - fi + sed -i 's/^radio_type: sx1262$/radio_type: sx1262_ch341/' /etc/pymc_repeater/config.yaml fi " From aa75fac7f2bcfc51c4239348c0f0eac19a9331ad Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 11:14:29 +0000 Subject: [PATCH 12/16] Update Proxmox installer scripts to include CH341 GPIO pin configuration and modify radio type --- manage.sh | 4 ++-- scripts/proxmox-install.sh | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/manage.sh b/manage.sh index 6cc99f5..0449f9d 100755 --- a/manage.sh +++ b/manage.sh @@ -249,7 +249,7 @@ install_repeater() { echo "25"; echo "# Installing system dependencies..." apt-get update -qq - apt-get install -y libffi-dev jq pip python3-rrdtool wget swig build-essential python3-dev + apt-get install -y libffi-dev libusb-1.0-0 jq pip python3-rrdtool wget swig build-essential python3-dev pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true # Install mikefarah yq v4 if not already installed @@ -529,7 +529,7 @@ upgrade_repeater() { echo "[3/9] Updating system dependencies..." apt-get update -qq - apt-get install -y libffi-dev jq pip python3-rrdtool wget swig build-essential python3-dev + apt-get install -y libffi-dev libusb-1.0-0 jq pip python3-rrdtool wget swig build-essential python3-dev pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true # Install mikefarah yq v4 if not already installed diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 27cce2c..f95c753 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -215,12 +215,22 @@ msg_info "Cloning pyMC_Repeater (branch: ${BRANCH})..." pct exec "$CTID" -- bash -c "git clone --branch ${BRANCH} ${REPO} /root/pyMC_Repeater" msg_ok "Repository cloned" -# Pre-seed config with CH341 radio type so manage.sh skips the SPI check +# Pre-seed config with CH341 radio type and correct GPIO pins pct exec "$CTID" -- bash -c " mkdir -p /etc/pymc_repeater if [ -f /root/pyMC_Repeater/config.yaml.example ]; then cp /root/pyMC_Repeater/config.yaml.example /etc/pymc_repeater/config.yaml + # Set radio type to CH341 sed -i 's/^radio_type: sx1262$/radio_type: sx1262_ch341/' /etc/pymc_repeater/config.yaml + # Replace Pi BCM GPIO pins with CH341 GPIO pin numbers (0-7) + sed -i 's/cs_pin: 21/cs_pin: 0/' /etc/pymc_repeater/config.yaml + sed -i 's/reset_pin: 18/reset_pin: 2/' /etc/pymc_repeater/config.yaml + sed -i 's/busy_pin: 20/busy_pin: 4/' /etc/pymc_repeater/config.yaml + sed -i 's/irq_pin: 16/irq_pin: 6/' /etc/pymc_repeater/config.yaml + sed -i 's/rxen_pin: -1/rxen_pin: 1/' /etc/pymc_repeater/config.yaml + # Enable TCXO and DIO2 RF switch for E22 module + sed -i 's/use_dio3_tcxo: false/use_dio3_tcxo: true/' /etc/pymc_repeater/config.yaml + sed -i 's/use_dio2_rf: false/use_dio2_rf: true/' /etc/pymc_repeater/config.yaml fi " From 74914541f227643ab7efe49cc4e7d5ca80edfce2 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 11:33:21 +0000 Subject: [PATCH 13/16] Enhance installation and uninstallation scripts with user-friendly messages, add polkit and sudoers configuration for service management, and improve service restart handling in API endpoints. --- manage.sh | 89 +++++++++++++++++++++++------------ pymc-repeater.service | 3 +- repeater/service_utils.py | 45 +++++++++++++++--- repeater/web/api_endpoints.py | 4 +- 4 files changed, 99 insertions(+), 42 deletions(-) diff --git a/manage.sh b/manage.sh index 0449f9d..7d0e5e6 100755 --- a/manage.sh +++ b/manage.sh @@ -233,27 +233,33 @@ install_repeater() { SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Installation progress - ( - echo "0"; echo "# Creating service user..." + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo " Installing pyMC Repeater" + echo "═══════════════════════════════════════════════════════════════" + 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 - echo "10"; echo "# Adding user to hardware groups..." + 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 done - echo "20"; echo "# Creating directories..." + echo ">>> Creating directories..." mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater - echo "25"; echo "# Installing system dependencies..." + echo ">>> Installing system dependencies..." apt-get update -qq - apt-get install -y libffi-dev libusb-1.0-0 jq pip python3-rrdtool wget swig build-essential python3-dev - pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true + DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev + pip install --break-system-packages setuptools_scm 2>&1 || true # 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 @@ -261,32 +267,31 @@ install_repeater() { 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 + 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 "28"; echo "# Generating version file..." + echo ">>> Generating version file..." cd "$SCRIPT_DIR" # Generate version file using setuptools_scm before copying if [ -d .git ]; then - git fetch --tags 2>/dev/null || true + git fetch --tags >/dev/null 2>&1 || true # Write the version file that will be copied - GENERATED_VERSION=$(python3 -m setuptools_scm 2>&1 || echo "unknown (setuptools_scm not available)") - python3 -c "from setuptools_scm import get_version; get_version(write_to='repeater/_version.py')" 2>&1 || echo " Warning: Could not generate _version.py file" - echo " Generated version: $GENERATED_VERSION" + python3 -m setuptools_scm >/dev/null 2>&1 || true + python3 -c "from setuptools_scm import get_version; get_version(write_to='repeater/_version.py')" >/dev/null 2>&1 || true fi # Clean up stale bytecode in source directory before copying find "$SCRIPT_DIR/repeater" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find "$SCRIPT_DIR/repeater" -type f -name '*.pyc' -delete 2>/dev/null || true - echo "29"; echo "# Cleaning old installation files..." + echo ">>> Cleaning old installation files..." # Remove old repeater directory to ensure clean install rm -rf "$INSTALL_DIR/repeater" 2>/dev/null || true # Clean up old Python bytecode find "$INSTALL_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find "$INSTALL_DIR" -type f -name '*.pyc' -delete 2>/dev/null || true - echo "30"; echo "# Installing files..." + echo ">>> Installing files..." cp -r "$SCRIPT_DIR/repeater" "$INSTALL_DIR/" cp "$SCRIPT_DIR/pyproject.toml" "$INSTALL_DIR/" cp "$SCRIPT_DIR/README.md" "$INSTALL_DIR/" @@ -295,24 +300,24 @@ install_repeater() { cp "$SCRIPT_DIR/radio-settings.json" /var/lib/pymc_repeater/ 2>/dev/null || true cp "$SCRIPT_DIR/radio-presets.json" /var/lib/pymc_repeater/ 2>/dev/null || true - echo "45"; echo "# Installing configuration..." + echo ">>> Installing configuration..." cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml.example" if [ ! -f "$CONFIG_DIR/config.yaml" ]; then cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml" fi - echo "55"; echo "# Installing systemd service..." + echo ">>> Installing systemd service..." cp "$SCRIPT_DIR/pymc-repeater.service" /etc/systemd/system/ systemctl daemon-reload - echo "60"; echo "# Installing udev rules for CH341..." + echo ">>> Installing udev rules for CH341..." if [ -f "$SCRIPT_DIR/../pyMC_core/99-ch341.rules" ]; then cp "$SCRIPT_DIR/../pyMC_core/99-ch341.rules" /etc/udev/rules.d/99-ch341.rules udevadm control --reload-rules 2>/dev/null || true udevadm trigger 2>/dev/null || true fi - echo "65"; echo "# Setting permissions..." + echo ">>> Setting permissions..." chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater chmod 750 "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater # Ensure the service user can create subdirectories in their home directory @@ -322,6 +327,7 @@ install_repeater() { chown -R "$SERVICE_USER:$SERVICE_USER" /var/lib/pymc_repeater/.config # Configure polkit for passwordless service restart + echo ">>> Configuring polkit for service management..." mkdir -p /etc/polkit-1/rules.d cat > /etc/polkit-1/rules.d/10-pymc-repeater.rules <<'EOF' polkit.addRule(function(action, subject) { @@ -334,11 +340,18 @@ polkit.addRule(function(action, subject) { EOF chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules - echo "75"; echo "# Starting service..." + # Also configure sudoers as fallback for service restart + echo ">>> Configuring sudoers for service management..." + cat > /etc/sudoers.d/pymc-repeater <<'EOF' +# Allow repeater user to manage the pymc-repeater service without password +repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart pymc-repeater, /usr/bin/systemctl stop pymc-repeater, /usr/bin/systemctl start pymc-repeater, /usr/bin/systemctl status pymc-repeater +EOF + chmod 0440 /etc/sudoers.d/pymc-repeater + + echo ">>> Enabling service..." systemctl enable "$SERVICE_NAME" - echo "90"; echo "# Installation files complete..." - ) | $DIALOG --backtitle "pyMC Repeater Management" --title "Installing" --gauge "Setting up pyMC Repeater..." 8 70 0 + echo ">>> Installation files complete." # Install Python package outside of progress gauge for better error handling clear @@ -529,7 +542,7 @@ upgrade_repeater() { echo "[3/9] Updating system dependencies..." apt-get update -qq - apt-get install -y libffi-dev libusb-1.0-0 jq pip python3-rrdtool wget swig build-essential python3-dev + apt-get install -y libffi-dev libusb-1.0-0 policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true # Install mikefarah yq v4 if not already installed @@ -620,6 +633,12 @@ polkit.addRule(function(action, subject) { }); EOF chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules + # Also configure sudoers as fallback for service restart + cat > /etc/sudoers.d/pymc-repeater <<'EOF' +# Allow repeater user to manage the pymc-repeater service without password +repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart pymc-repeater, /usr/bin/systemctl stop pymc-repeater, /usr/bin/systemctl start pymc-repeater, /usr/bin/systemctl status pymc-repeater +EOF + chmod 0440 /etc/sudoers.d/pymc-repeater echo " ✓ Permissions updated" echo "[7/9] Reloading systemd..." @@ -742,33 +761,41 @@ uninstall_repeater() { fi if ask_yes_no "Confirm Uninstall" "This will completely remove pyMC Repeater including:\n\n- Service and files\n- Configuration (backup will be created)\n- Logs and data\n\nThis action cannot be undone!\n\nContinue?"; then - ( - echo "0"; echo "# Stopping and disabling service..." + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo " Uninstalling pyMC Repeater" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + echo ">>> Stopping and disabling service..." systemctl stop "$SERVICE_NAME" 2>/dev/null || true systemctl disable "$SERVICE_NAME" 2>/dev/null || true - echo "20"; echo "# Backing up configuration..." + echo ">>> Backing up configuration..." if [ -d "$CONFIG_DIR" ]; then cp -r "$CONFIG_DIR" "/tmp/pymc_repeater_config_backup_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true fi - echo "40"; echo "# Removing service files..." + echo ">>> Removing service files..." rm -f /etc/systemd/system/pymc-repeater.service systemctl daemon-reload - echo "60"; echo "# Removing installation..." + echo ">>> Removing installation..." rm -rf "$INSTALL_DIR" rm -rf "$CONFIG_DIR" rm -rf "$LOG_DIR" rm -rf /var/lib/pymc_repeater - echo "80"; echo "# Removing service user..." + echo ">>> Removing service user..." if id "$SERVICE_USER" &>/dev/null; then userdel "$SERVICE_USER" 2>/dev/null || true fi - echo "100"; echo "# Uninstall complete!" - ) | $DIALOG --backtitle "pyMC Repeater Management" --title "Uninstalling" --gauge "Removing pyMC Repeater..." 8 70 0 + echo ">>> Removing polkit and sudoers rules..." + rm -f /etc/polkit-1/rules.d/10-pymc-repeater.rules + rm -f /etc/sudoers.d/pymc-repeater + + echo ">>> Uninstall complete!" show_info "Uninstall Complete" "\npyMC Repeater has been completely removed.\n\nConfiguration backup saved to /tmp/\n\nThank you for using pyMC Repeater!" fi diff --git a/pymc-repeater.service b/pymc-repeater.service index 3cbd51d..80b0456 100644 --- a/pymc-repeater.service +++ b/pymc-repeater.service @@ -28,8 +28,7 @@ StandardOutput=journal StandardError=journal SyslogIdentifier=pymc-repeater -# Security (relaxed for proper operation) -NoNewPrivileges=true +# Security (relaxed for service self-restart via sudo) ReadWritePaths=/var/log/pymc_repeater /var/lib/pymc_repeater /etc/pymc_repeater SupplementaryGroups=plugdev dialout diff --git a/repeater/service_utils.py b/repeater/service_utils.py index 78ba7e8..99def20 100644 --- a/repeater/service_utils.py +++ b/repeater/service_utils.py @@ -13,12 +13,13 @@ def restart_service() -> Tuple[bool, str]: """ Restart the pymc-repeater service via systemctl. - Uses polkit for authentication (requires proper polkit rules configured). - NoNewPrivileges systemd flag prevents sudo from working. + Tries polkit-based restart first (plain systemctl), then falls back + to sudo-based restart (requires sudoers.d rule installed by manage.sh). Returns: Tuple[bool, str]: (success, message) """ + # Try polkit-based restart first (works on bare metal / VMs with polkit running) try: result = subprocess.run( ['systemctl', 'restart', 'pymc-repeater'], @@ -28,19 +29,49 @@ def restart_service() -> Tuple[bool, str]: ) if result.returncode == 0: - logger.info("Service restart command executed successfully") + logger.info("Service restart via polkit succeeded") return True, "Service restart initiated" + + stderr = result.stderr or "" + if "Access denied" in stderr or "authorization" in stderr.lower(): + logger.info("Polkit denied restart, trying sudo fallback...") else: - error_msg = result.stderr or "Unknown error" - logger.error(f"Service restart failed: {error_msg}") - return False, f"Restart failed: {error_msg}" + # Some other error, still try sudo + logger.warning(f"systemctl restart failed ({result.returncode}): {stderr.strip()}") except subprocess.TimeoutExpired: + # Timeout likely means it's restarting - that's success logger.warning("Service restart command timed out (service may be restarting)") return True, "Service restart initiated (timeout - likely restarting)" except FileNotFoundError: logger.error("systemctl not found") return False, "systemctl not available" except Exception as e: - logger.error(f"Error executing restart command: {e}") + logger.warning(f"Polkit restart attempt failed: {e}") + + # Fallback: use sudo (requires /etc/sudoers.d/pymc-repeater rule) + try: + result = subprocess.run( + ['sudo', '--non-interactive', 'systemctl', 'restart', 'pymc-repeater'], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + logger.info("Service restart via sudo succeeded") + return True, "Service restart initiated" + else: + error_msg = result.stderr or "Unknown error" + logger.error(f"Service restart via sudo failed: {error_msg}") + return False, f"Restart failed: {error_msg}" + + except subprocess.TimeoutExpired: + logger.warning("Sudo restart timed out (service likely restarting)") + return True, "Service restart initiated (timeout - likely restarting)" + except FileNotFoundError: + logger.error("sudo not found - cannot restart service") + return False, "Neither polkit nor sudo available for service restart" + except Exception as e: + logger.error(f"Error executing sudo restart: {e}") return False, f"Restart command failed: {str(e)}" diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 2a2a661..fda34a3 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -491,8 +491,8 @@ class APIEndpoints: import time time.sleep(2) # Give time for response to be sent try: - # Use systemctl without sudo - polkit rules allow the repeater user to restart the service - subprocess.run(['systemctl', 'restart', 'pymc-repeater'], check=False) + from repeater.service_utils import restart_service + restart_service() except Exception as e: logger.error(f"Failed to restart service: {e}") From 83a6ae69ae1519d70f085e821718c65897c5b3bd Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 11:36:46 +0000 Subject: [PATCH 14/16] Add sudo and create sudoers directory for service management in install and upgrade functions --- manage.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manage.sh b/manage.sh index 7d0e5e6..f5a28c2 100755 --- a/manage.sh +++ b/manage.sh @@ -254,7 +254,7 @@ install_repeater() { echo ">>> Installing system dependencies..." apt-get update -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev + DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev pip install --break-system-packages setuptools_scm 2>&1 || true # Install mikefarah yq v4 if not already installed @@ -342,6 +342,7 @@ EOF # Also configure sudoers as fallback for service restart echo ">>> Configuring sudoers for service management..." + mkdir -p /etc/sudoers.d cat > /etc/sudoers.d/pymc-repeater <<'EOF' # Allow repeater user to manage the pymc-repeater service without password repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart pymc-repeater, /usr/bin/systemctl stop pymc-repeater, /usr/bin/systemctl start pymc-repeater, /usr/bin/systemctl status pymc-repeater @@ -542,7 +543,7 @@ upgrade_repeater() { echo "[3/9] Updating system dependencies..." apt-get update -qq - apt-get install -y libffi-dev libusb-1.0-0 policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev + apt-get install -y libffi-dev libusb-1.0-0 sudo policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true # Install mikefarah yq v4 if not already installed @@ -634,6 +635,7 @@ polkit.addRule(function(action, subject) { EOF chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules # Also configure sudoers as fallback for service restart + mkdir -p /etc/sudoers.d cat > /etc/sudoers.d/pymc-repeater <<'EOF' # Allow repeater user to manage the pymc-repeater service without password repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart pymc-repeater, /usr/bin/systemctl stop pymc-repeater, /usr/bin/systemctl start pymc-repeater, /usr/bin/systemctl status pymc-repeater From 91d916798b4679989e4cf113167bc5145c825b14 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 12:03:45 +0000 Subject: [PATCH 15/16] upgrade functions to include conditional polkit installation and improve error handling for missing packages --- manage.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/manage.sh b/manage.sh index f5a28c2..e491f3d 100755 --- a/manage.sh +++ b/manage.sh @@ -254,7 +254,11 @@ install_repeater() { echo ">>> Installing system dependencies..." apt-get update -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev + DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip 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)" pip install --break-system-packages setuptools_scm 2>&1 || true # Install mikefarah yq v4 if not already installed @@ -543,7 +547,11 @@ upgrade_repeater() { echo "[3/9] Updating system dependencies..." apt-get update -qq - apt-get install -y libffi-dev libusb-1.0-0 sudo policykit-1 jq pip python3-rrdtool wget swig build-essential python3-dev + apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip 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 || true # Install mikefarah yq v4 if not already installed From 878218346a7fb0d18ea97b0928586b75349d05d5 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 24 Feb 2026 12:45:47 +0000 Subject: [PATCH 16/16] Add Proxmox installation instructions and CH341 GPIO pin mapping details to README --- README.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/README.md b/README.md index d020fe2..4f02cd0 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,89 @@ The upgrade script will: - Restart the service automatically - Preserve your existing configuration +--- + +## Installing on Proxmox (LXC Container) + +pyMC Repeater can run inside a Proxmox LXC container using a **CH341 USB-to-SPI adapter** for radio communication. This is ideal for headless, always-on deployments without dedicating a full Raspberry Pi. + +### Requirements + +- **Proxmox VE 7.x or 8.x** host +- **CH341 USB-to-SPI adapter** (VID `1a86`, PID `5512`) connected to the Proxmox host +- **SX1262-based LoRa module** (e.g. Ebyte E22-900M30S) wired to the CH341 adapter +- Internet connectivity for the container + +### One-Line Install + +Run this on the **Proxmox host** (not inside a container): + +```bash +bash -c "$(curl -fsSL https://raw.githubusercontent.com/rightup/pyMC_Repeater/main/scripts/proxmox-install.sh)" +``` + +The installer will interactively prompt you for container settings (hostname, RAM, disk, bridge, etc.) and then: + +1. Download a Debian 12 LXC template +2. Create a **privileged** container with USB passthrough +3. Install a host-side udev rule for the CH341 device +4. Clone the repository and pre-seed the config with CH341 GPIO pin mappings +5. Run `manage.sh install` inside the container +6. Display the dashboard URL when finished + +### Default Container Settings + +| Setting | Default | +|-----------|-----------------| +| Hostname | `pymc-repeater` | +| RAM | 1024 MB | +| Disk | 4 GB | +| CPU cores | 2 | +| Bridge | `vmbr0` | +| Storage | `local-lvm` | +| Password | `pymc` | + +### After Installation + +```bash +# Enter the container +pct enter + +# View service logs +journalctl -u pymc-repeater -f + +# Access web dashboard +http://:8000 + +# Manage the repeater +cd /opt/pymc_repeater && bash manage.sh +``` + +### CH341 GPIO Pin Mapping + +The installer pre-configures the CH341 GPIO pins for an E22 module. These differ from the Raspberry Pi BCM pin numbers: + +| Function | CH341 GPIO | Pi BCM (default) | +|----------|-----------|-------------------| +| CS | 0 | 21 | +| RXEN | 1 | -1 | +| Reset | 2 | 18 | +| Busy | 4 | 20 | +| IRQ | 6 | 16 | + +The installer also enables `use_dio3_tcxo` and `use_dio2_rf` for E22 modules. + +### Troubleshooting (Proxmox) + +- **USB device not found**: Make sure the CH341 is plugged into the Proxmox host and shows up with `lsusb -d 1a86:5512` +- **Permission denied on USB**: The installer creates a host udev rule (`/etc/udev/rules.d/99-ch341.rules`). Run `udevadm trigger` on the host if needed +- **Container can't see USB**: Verify USB passthrough lines exist in `/etc/pve/lxc/.conf`: + ``` + lxc.cgroup2.devices.allow: c 189:* rwm + lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir 0 0 + ``` +- **NoBackendError (libusb)**: The installer installs `libusb-1.0-0` automatically. If you see this error, run `apt-get install libusb-1.0-0` inside the container +