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 + diff --git a/manage.sh b/manage.sh index 7965452..e491f3d 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 @@ -226,26 +233,37 @@ 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..." - usermod -a -G gpio,i2c,spi "$SERVICE_USER" 2>/dev/null || true - usermod -a -G dialout "$SERVICE_USER" 2>/dev/null || true + 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 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 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 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 @@ -253,32 +271,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/" @@ -287,17 +304,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 ">>> 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 @@ -307,6 +331,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) { @@ -319,11 +344,19 @@ 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..." + 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 +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 @@ -354,7 +387,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!" @@ -394,6 +427,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 @@ -497,7 +547,11 @@ 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 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 @@ -553,6 +607,22 @@ upgrade_repeater() { echo " ⚠ Configuration validation failed, keeping existing config" fi + 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 + done + # 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 @@ -572,6 +642,13 @@ 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 +EOF + chmod 0440 /etc/sudoers.d/pymc-repeater echo " ✓ Permissions updated" echo "[7/9] Reloading systemd..." @@ -607,7 +684,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 @@ -632,7 +709,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." @@ -689,33 +771,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 @@ -849,7 +939,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" @@ -864,7 +961,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/pymc-repeater.service b/pymc-repeater.service index 8c07d40..80b0456 100644 --- a/pymc-repeater.service +++ b/pymc-repeater.service @@ -28,9 +28,9 @@ 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 [Install] WantedBy=multi-user.target 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/radio-settings.json b/radio-settings.json index 88476ce..3e72ab2 100644 --- a/radio-settings.json +++ b/radio-settings.json @@ -48,24 +48,8 @@ "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)", + "pimesh-1w-v1": { + "name": "PiMesh-1W (V1)", "bus_id": 0, "cs_id": 0, "cs_pin": 21, @@ -80,6 +64,24 @@ "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, + "en_pin": 26, + "tx_power": 22, + "use_dio3_tcxo": true, + "use_dio2_rf": true, + "preamble_length": 17 + }, "meshadv-mini": { "name": "MeshAdv Mini", "bus_id": 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), 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() 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}") diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh new file mode 100644 index 0000000..f95c753 --- /dev/null +++ b/scripts/proxmox-install.sh @@ -0,0 +1,263 @@ +#!/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}" +read -sp " Root password [pymc]: " CT_PASSWORD; echo +CT_PASSWORD="${CT_PASSWORD:-pymc}" + +# ── 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 \ + --password "$CT_PASSWORD" \ + --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 + + # 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 + 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 + + # 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" + +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 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 +" + +# ── 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 "═══════════════════════════════════════════════════════════════"