Merge pull request #103 from rightup/feat/E22p

Feat/e22p
This commit is contained in:
Lloyd
2026-02-24 12:51:45 +00:00
committed by GitHub
10 changed files with 564 additions and 69 deletions
+83
View File
@@ -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 <CTID>
# View service logs
journalctl -u pymc-repeater -f
# Access web dashboard
http://<container-ip>: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/<CTID>.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
+136 -39
View File
@@ -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
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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",
+20 -18
View File
@@ -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,
+1
View File
@@ -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),
+18
View File
@@ -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()
+38 -7
View File
@@ -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)}"
+2 -2
View File
@@ -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}")
+263
View File
@@ -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:-<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 "═══════════════════════════════════════════════════════════════"