refactor:rename-project-to-openhop

This commit is contained in:
Lloyd
2026-06-24 23:27:49 +01:00
parent 2b07e79ccd
commit 2b67dea96b
83 changed files with 719 additions and 649 deletions
+5 -5
View File
@@ -3,17 +3,17 @@
# Published image to run. Use a different repository or tag if you are testing
# a fork or a specific release.
PYMC_REPEATER_IMAGE=pymcdev/pymc-repeater:main
PYMC_REPEATER_IMAGE=pymcdev/openhop-repeater:main
# Storage defaults to Docker named volumes. This is the safest option for
# Portainer and fresh installs because Docker preserves the image ownership.
# To use host bind mounts instead, create the folders first and make them
# writable by UID/GID 15888:
# sudo mkdir -p /opt/pymc-repeater/config /opt/pymc-repeater/data
# sudo chown -R 15888:15888 /opt/pymc-repeater/config /opt/pymc-repeater/data
# sudo mkdir -p /opt/openhop-repeater/config /opt/openhop-repeater/data
# sudo chown -R 15888:15888 /opt/openhop-repeater/config /opt/openhop-repeater/data
# Then uncomment and adjust these paths:
# PYMC_CONFIG_VOLUME=/opt/pymc-repeater/config
# PYMC_DATA_VOLUME=/opt/pymc-repeater/data
# PYMC_CONFIG_VOLUME=/opt/openhop-repeater/config
# PYMC_DATA_VOLUME=/opt/openhop-repeater/data
# Serial/SPI/GPIO access uses the host's numeric group IDs. Check your host with:
# getent group dialout
+7 -7
View File
@@ -10,14 +10,14 @@ on:
image_repository:
description: "Docker image repository to publish to"
required: false
default: "pymcdev/pymc-repeater"
default: "pymcdev/openhop-repeater"
jobs:
docker:
if: |
github.event_name == 'workflow_dispatch' ||
github.repository == 'pyMC-dev/pyMC_Repeater' ||
github.repository == 'yellowcooln/pyMC_Repeater'
github.repository == 'pyMC-dev/openhop-repeater' ||
github.repository == 'yellowcooln/openhop-repeater'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -58,10 +58,10 @@ jobs:
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.image_repository }}" ]; then
image_repository="${{ inputs.image_repository }}"
elif [ "${{ github.repository }}" = "yellowcooln/pyMC_Repeater" ]; then
image_repository="yellowcooln/pymc-repeater"
elif [ "${{ github.repository }}" = "yellowcooln/openhop-repeater" ]; then
image_repository="yellowcooln/openhop-repeater"
else
image_repository="pymcdev/pymc-repeater"
image_repository="pymcdev/openhop-repeater"
fi
echo "Using image repository: ${image_repository}"
@@ -92,7 +92,7 @@ jobs:
cache-to: type=gha,mode=max
- name: Notify Home Assistant add-on repository
if: github.repository == 'pyMC-dev/pyMC_Repeater'
if: github.repository == 'pyMC-dev/openhop-repeater'
env:
DISPATCH_TOKEN: ${{ secrets.HA_ADDON_REPO_DISPATCH_TOKEN }}
CHANNEL: ${{ github.ref_name }}
+3 -3
View File
@@ -28,9 +28,9 @@ share/python-wheels/
DEBIAN/
debian/files
debian/.debhelper/
debian/pymc-repeater/
debian/pymc-repeater.debhelper.log
debian/pymc-repeater.substvars
debian/openhop-repeater/
debian/openhop-repeater.debhelper.log
debian/openhop-repeater.substvars
# Virtual environments
.venv/
+33 -33
View File
@@ -1,8 +1,8 @@
# pyMC Repeater
# openHop Repeater
Lightweight Python MeshCore repeater daemon built on `pymc_core`.
Lightweight Python MeshCore repeater daemon built on `openhop_core`.
pyMC Repeater is designed to run continuously on low-power Linux hardware such
openHop Repeater is designed to run continuously on low-power Linux hardware such
as Raspberry Pi-class devices, Proxmox LXC containers, and network-attached
radio modems. It forwards LoRa packets, exposes a web dashboard, and provides
configuration tools for radio setup, policy management, monitoring, and
@@ -29,7 +29,7 @@ integrations.
## Overview
The repeater daemon runs as a background service and forwards LoRa packets using
the `pymc_core` dispatcher and routing stack. The project favors a simple,
the `openhop_core` dispatcher and routing stack. The project favors a simple,
hackable architecture:
- CherryPy provides a lightweight HTTP server for the web UI and API.
@@ -59,7 +59,7 @@ Historical statistics and performance metrics.
## Supported Hardware
pyMC Repeater supports these radio backends:
openHop Repeater supports these radio backends:
- **SX1262 over Linux SPI**: set `radio_type: sx1262`
- **SX1262 over CH341 USB-to-SPI**: set `radio_type: sx1262_ch341`
@@ -111,8 +111,8 @@ sudo apt install git -y
### Clone The Repository
```bash
git clone https://github.com/pyMC-dev/pyMC_Repeater.git
cd pyMC_Repeater
git clone https://github.com/pyMC-dev/openhop-repeater.git
cd openhop-repeater
```
### Quick Install
@@ -124,17 +124,17 @@ sudo bash ./manage.sh install
The installer will:
- Create a dedicated `repeater` service user with hardware access
- Install application files to `/opt/pymc_repeater`
- Create the configuration directory at `/etc/pymc_repeater`
- Create the log directory at `/var/log/pymc_repeater`
- Install application files to `/opt/openhop_repeater`
- Create the configuration directory at `/etc/openhop_repeater`
- Create the log directory at `/var/log/openhop_repeater`
- Launch the interactive radio and hardware setup wizard
- Install and enable the `pymc-repeater` systemd service
- Install and enable the `openhop-repeater` systemd service
After installation:
```bash
# View live logs
sudo journalctl -u pymc-repeater -f
sudo journalctl -u openhop-repeater -f
```
Open the web dashboard at:
@@ -160,7 +160,7 @@ pip install -e ".[dev]"
The main configuration file is created during installation:
```text
/etc/pymc_repeater/config.yaml
/etc/openhop_repeater/config.yaml
```
### Setup Wizard
@@ -207,19 +207,19 @@ TX power defaults to 14 dBm and can be changed later.
To reconfigure radio and hardware settings after installation:
```bash
sudo bash setup-radio-config.sh /etc/pymc_repeater
sudo bash setup-radio-config.sh /etc/openhop_repeater
```
You can also launch the management menu:
```bash
sudo ./manage.sh
sudo systemctl restart pymc-repeater
sudo systemctl restart openhop-repeater
```
### Optional pyMC_Glass Integration
pyMC Repeater supports an optional `glass` configuration section for
openHop Repeater supports an optional `glass` configuration section for
pyMC_Glass control-plane integration. When enabled, the repeater sends periodic
`/inform` payloads to pyMC_Glass, receives queued commands, and reports command
results on the next inform cycle.
@@ -258,7 +258,7 @@ The web interface can upgrade an installation or switch branches.
### CLI
```bash
cd pyMC_Repeater
cd openhop-repeater
sudo bash ./manage.sh upgrade
```
@@ -272,7 +272,7 @@ The upgrade script will:
## Proxmox LXC Installation
pyMC Repeater can run inside a Proxmox LXC container using a CH341 USB-to-SPI
openHop Repeater can run inside a Proxmox LXC container using a CH341 USB-to-SPI
adapter or a TCP modem. This is useful for headless, always-on deployments
without dedicating a full Raspberry Pi.
@@ -295,7 +295,7 @@ Hardware, choose one:
Run this command on the Proxmox host, not inside a container:
```bash
bash -c "$(curl -fsSL https://raw.githubusercontent.com/pyMC-dev/pyMC_Repeater/main/scripts/proxmox-install.sh)"
bash -c "$(curl -fsSL https://raw.githubusercontent.com/pyMC-dev/openhop-repeater/main/scripts/proxmox-install.sh)"
```
Replace `main` in the URL with another branch name if needed.
@@ -315,7 +315,7 @@ disk, bridge, etc.) and then:
| Setting | Default |
|---------|---------|
| Container ID | Next available |
| Hostname | `pymc-repeater` |
| Hostname | `openhop-repeater` |
| RAM | 1024 MB |
| Disk | 4 GB |
| CPU cores | 2 |
@@ -330,10 +330,10 @@ disk, bridge, etc.) and then:
pct enter <CTID>
# View service logs
journalctl -u pymc-repeater -f
journalctl -u openhop-repeater -f
# Manage the repeater
cd /opt/pymc_repeater
cd /opt/openhop_repeater
bash manage.sh
```
@@ -394,7 +394,7 @@ The script prompts before each optional removal step.
## Docker Compose
You can run pyMC Repeater in Docker using the published image.
You can run openHop Repeater in Docker using the published image.
Copy `.env.example` to `.env` before starting:
@@ -410,7 +410,7 @@ root-owned `./config` and `./data` bind mount folders on first start. If you
want host bind mounts, use absolute host paths and pre-create/chown them to
`15888:15888`.
Do not mount `./config.yaml:/etc/pymc_repeater/config.yaml`; Docker can create
Do not mount `./config.yaml:/etc/openhop_repeater/config.yaml`; Docker can create
that source as a directory, which breaks startup.
### Setup
@@ -430,9 +430,9 @@ docker compose up -d
```yaml
services:
pymc-repeater:
image: ${PYMC_REPEATER_IMAGE:-pymcdev/pymc-repeater:main}
container_name: pymc-repeater
openhop-repeater:
image: ${PYMC_REPEATER_IMAGE:-pymcdev/openhop-repeater:main}
container_name: openhop-repeater
restart: unless-stopped
ports:
- 8000:8000
@@ -455,12 +455,12 @@ services:
- plugdev
volumes:
- ${PYMC_CONFIG_VOLUME:-pymc-repeater-config}:/etc/pymc_repeater
- ${PYMC_DATA_VOLUME:-pymc-repeater-data}:/var/lib/pymc_repeater
- ${PYMC_CONFIG_VOLUME:-openhop-repeater-config}:/etc/openhop_repeater
- ${PYMC_DATA_VOLUME:-openhop-repeater-data}:/var/lib/openhop_repeater
volumes:
pymc-repeater-config:
pymc-repeater-data:
openhop-repeater-config:
openhop-repeater-data:
```
## Roadmap
@@ -509,7 +509,7 @@ pre-commit run --all-files
```
Hardware support for LoRa radio drivers is included in the base installation
through `pymc_core[hardware]`.
through `openhop_core[hardware]`.
Pre-commit hooks will automatically:
- Lint and auto-fix Python issues with Ruff
@@ -518,7 +518,7 @@ Pre-commit hooks will automatically:
## Support
- [pyMC Core](https://github.com/pyMC-dev/pyMC_core)
- [pyMC Core](https://github.com/pyMC-dev/openhop-core)
- [MeshCore Discord](https://meshcore.gg)
## Disclaimer
+24 -24
View File
@@ -1,28 +1,28 @@
#!/bin/bash
# Buildroot/Luckfox management entrypoint for pyMC Repeater
# Buildroot/Luckfox management entrypoint for openHop Repeater
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
INSTALL_DIR="/opt/pymc_repeater"
INSTALL_DIR="/opt/openhop_repeater"
VENV_DIR="$INSTALL_DIR/venv"
VENV_PIP="$VENV_DIR/bin/pip"
VENV_PYTHON="$VENV_DIR/bin/python"
CONFIG_DIR="/etc/pymc_repeater"
LOG_DIR="/var/log/pymc_repeater"
DATA_DIR="/var/lib/pymc_repeater"
CONFIG_DIR="/etc/openhop_repeater"
LOG_DIR="/var/log/openhop_repeater"
DATA_DIR="/var/lib/openhop_repeater"
SERVICE_USER="root"
INIT_SCRIPT="/etc/init.d/S80pymc-repeater"
PIDFILE="/var/run/pymc-repeater.pid"
INIT_SCRIPT="/etc/init.d/S80openhop-repeater"
PIDFILE="/var/run/openhop-repeater.pid"
LOGFILE="$LOG_DIR/repeater.log"
SERVICE_NAME="pymc-repeater"
SERVICE_NAME="openhop-repeater"
SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}"
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
PIWHEELS_INDEX_URL="https://www.piwheels.org/simple"
R2_ENABLED=1
YQ_VERSION="${YQ_VERSION:-v4.44.3}"
PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/pyMC_core.git}"
PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/openhop-core.git}"
PYMC_CORE_REF="${PYMC_CORE_REF:-}"
PYMC_CORE_LOCAL_DIR="${PYMC_CORE_LOCAL_DIR:-}"
PYMC_SKIP_BUILDROOT_DEP_INSTALL="${PYMC_SKIP_BUILDROOT_DEP_INSTALL:-0}"
@@ -486,9 +486,9 @@ cleanup_legacy_install_state() {
for path in \
"$INSTALL_DIR/repeater" \
"$INSTALL_DIR/pymc_core" \
"$INSTALL_DIR/pyMC_Repeater" \
"$INSTALL_DIR/pyMC_core"
"$INSTALL_DIR/openhop_core" \
"$INSTALL_DIR/openhop-repeater" \
"$INSTALL_DIR/openhop-core"
do
if [ -e "$path" ]; then
rm -rf "$path"
@@ -641,8 +641,8 @@ install_core_into_venv() {
local core_repo core_ref core_spec
if [ -n "$PYMC_CORE_LOCAL_DIR" ]; then
[ -d "$PYMC_CORE_LOCAL_DIR" ] || fail "Missing local pyMC_core checkout: $PYMC_CORE_LOCAL_DIR"
stage "Installing pyMC_core"
[ -d "$PYMC_CORE_LOCAL_DIR" ] || fail "Missing local openhop-core checkout: $PYMC_CORE_LOCAL_DIR"
stage "Installing openhop-core"
info "Local dir: ${PYMC_CORE_LOCAL_DIR}"
"$VENV_PIP" install --upgrade --no-cache-dir --no-deps --no-build-isolation "$PYMC_CORE_LOCAL_DIR"
return 0
@@ -654,8 +654,8 @@ install_core_into_venv() {
*) core_repo="${core_repo}.git" ;;
esac
core_ref=$(resolve_core_ref)
core_spec="pyMC_core[hardware] @ git+${core_repo}@${core_ref}"
stage "Installing pyMC_core"
core_spec="openhop-core[hardware] @ git+${core_repo}@${core_ref}"
stage "Installing openhop-core"
info "Repo: ${PYMC_CORE_REPO}"
info "Ref: ${core_ref}"
"$VENV_PIP" install --upgrade --no-cache-dir --no-deps --no-build-isolation "$core_spec"
@@ -667,7 +667,7 @@ import glob
import json
import os
matches = glob.glob("/opt/pymc_repeater/venv/lib/python*/site-packages/pymc_core-*.dist-info/direct_url.json")
matches = glob.glob("/opt/openhop_repeater/venv/lib/python*/site-packages/openhop_core-*.dist-info/direct_url.json")
for path in matches:
try:
with open(path, "r", encoding="utf-8") as fh:
@@ -693,7 +693,7 @@ resolve_core_commit() {
}
install_repeater_package() {
stage "Installing pyMC Repeater into venv"
stage "Installing openHop Repeater into venv"
info "Installing checked-out repo without re-resolving dependencies"
"$VENV_PIP" install --upgrade --no-cache-dir --no-deps --no-build-isolation "$SCRIPT_DIR"
}
@@ -1232,7 +1232,7 @@ start_or_restart_service() {
get_version() {
if [ -x "$VENV_PYTHON" ]; then
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null || echo "not installed"
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('openhop_repeater'))" 2>/dev/null || echo "not installed"
else
echo "not installed"
fi
@@ -1355,7 +1355,7 @@ install_repeater() {
info "Install dir: $INSTALL_DIR"
info "Config dir: $CONFIG_DIR"
info "Data dir: $DATA_DIR"
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" "$DATA_DIR/.config/pymc_repeater"
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" "$DATA_DIR/.config/openhop_repeater"
chown -R root:root "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
chmod 755 "$INSTALL_DIR" "$DATA_DIR"
chmod 750 "$CONFIG_DIR" "$LOG_DIR"
@@ -1417,7 +1417,7 @@ upgrade_repeater() {
git_version=$(prepare_git_version)
preinstall_r2_wheels
stage "Upgrading pyMC Repeater"
stage "Upgrading openHop Repeater"
if [ "${PYMC_FORCE_DEPS:-0}" = "1" ]; then
info "Forcing dependency reinstall"
install_buildroot_dependencies
@@ -1431,13 +1431,13 @@ upgrade_repeater() {
target_core_commit=$(resolve_core_commit)
installed_core_commit=$(get_installed_core_commit)
if [ -n "$target_core_commit" ] && [ "$installed_core_commit" = "$target_core_commit" ] && [ "${PYMC_FORCE_CORE:-0}" != "1" ]; then
info "pyMC_core is already at ${target_core_commit}; skipping reinstall"
info "openhop-core is already at ${target_core_commit}; skipping reinstall"
else
install_core_into_venv
fi
if [ "$current_version" = "$git_version" ] && [ "${PYMC_FORCE_REPEATER:-0}" != "1" ]; then
info "pyMC Repeater is already at ${git_version}; skipping reinstall"
info "openHop Repeater is already at ${git_version}; skipping reinstall"
else
ensure_yq >/dev/null 2>&1 || true
install_repeater_package
@@ -1521,7 +1521,7 @@ Usage: bash buildroot-manage.sh <command>
Commands:
doctor Check Buildroot/Luckfox prerequisites
install Install pyMC Repeater on the Buildroot image
install Install openHop Repeater on the Buildroot image
upgrade Upgrade the Buildroot installation from the checked-out repo
config Prompt for repeater settings and rewrite config.yaml
configure Same as config
+8 -8
View File
@@ -151,7 +151,7 @@ gps:
# File source settings (used when source: file)
# The file may contain raw NMEA lines or JSON with a "sentences" list /
# "last_sentence" field.
source_path: "/var/lib/pymc_repeater/gps_nmea.txt"
source_path: "/var/lib/openhop_repeater/gps_nmea.txt"
poll_interval_seconds: 2.0
# Modem HTTP source settings (used when source: modem_http)
@@ -406,7 +406,7 @@ radio:
# Use implicit header mode
implicit_header: false
# KISS modem (when radio_type: kiss). Requires pyMC_core with KISS support.
# KISS modem (when radio_type: kiss). Requires openhop-core with KISS support.
# kiss:
# port: "/dev/ttyUSB0"
# baud_rate: 9600
@@ -422,7 +422,7 @@ radio:
# # kiss_full_duplex: false # disable carrier-sense/CSMA entirely (not recommended)
# pymc_usb firmware modem over Wi-Fi/TCP (when radio_type: pymc_tcp).
# Requires pyMC_core with the TCPLoRaRadio driver
# Requires openhop-core with the TCPLoRaRadio driver
# pymc_tcp:
# host: "pymc-3e2834.local" # modem hostname / mDNS name / LAN IP
# port: 5055 # firmware default
@@ -432,7 +432,7 @@ radio:
# lbt_max_attempts: 5
# pymc_usb firmware modem over USB-CDC (when radio_type: pymc_usb).
# Requires pyMC_core with the USBLoRaRadio driver
# Requires openhop-core with the USBLoRaRadio driver
# pymc_usb:
# port: "/dev/ttyACM0" # USB-CDC device; udev rule may symlink to /dev/lora-modem
# baudrate: 921600 # must match firmware monitor_speed
@@ -492,8 +492,8 @@ duty_cycle:
# Storage Configuration
storage:
# Directory for persistent storage files (SQLite, RRD).
# Use a writable path for local/dev (e.g. "./var/pymc_repeater" or "~/var/pymc_repeater").
storage_dir: "/var/lib/pymc_repeater"
# Use a writable path for local/dev (e.g. "./var/openhop_repeater" or "~/var/openhop_repeater").
storage_dir: "/var/lib/openhop_repeater"
# Data retention settings
retention:
@@ -526,7 +526,7 @@ mqtt_brokers:
# format: meshcoretomqtt|letsmesh|waev|mqtt
# meshcoretomqtt - canonical open-source MC2MQTT topic structure
# letsmesh, waev - MC2MQTT family flavors (same topic structure, network-specific identity)
# mqtt - legacy pyMC_Repeater local-broker convention (custom topic, singular 'packet')
# mqtt - legacy openhop-repeater local-broker convention (custom topic, singular 'packet')
# retain_status: true|false # Sets MQTT "retain" on status messages so they remain on the broker when disconnected. Also enforces a QOS of 1 (guaranteed delivery)
# tls:
# enabled: true|false # Enable TLS. If the endpoint's certificate is self-signed, the Root CA should be added to the OS's certificate store.
@@ -602,7 +602,7 @@ glass:
api_token: ""
# Where cert_renewal payloads are written
cert_store_dir: "/etc/pymc_repeater/glass"
cert_store_dir: "/etc/openhop_repeater/glass"
logging:
# Log level: DEBUG, INFO, WARNING, ERROR
+20 -20
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# Convert MeshCore firmware 64-byte private key to pyMC_Repeater format
# Convert MeshCore firmware 64-byte private key to openhop-repeater format
#
# Usage: sudo ./convert_firmware_key.sh <64-byte-hex-key> [--output-format=<yaml|identity>] [config-path]
# Example: sudo ./convert_firmware_key.sh 987BDA619630197351F2B3040FD19B2EE0DEE357DD69BBEEE295786FA78A4D5F298B0BF1B7DE73CBC23257CDB2C562F5033DF58C232916432948B0F6BA4448F2
@@ -12,18 +12,18 @@ if [ $# -eq 0 ]; then
echo "Usage: sudo $0 <64-byte-hex-key> [--output-format=<yaml|identity>] [config-path]"
echo ""
echo "This script imports a 64-byte MeshCore firmware private key into"
echo "pyMC_Repeater for full identity compatibility."
echo "openhop-repeater for full identity compatibility."
echo ""
echo "The 64-byte key format: [32-byte scalar][32-byte nonce]"
echo " - Enables same node address as firmware device"
echo " - Supports signing using MeshCore/orlp ed25519 algorithm"
echo " - Fully compatible with pyMC_core LocalIdentity"
echo " - Fully compatible with openhop-core LocalIdentity"
echo ""
echo "Arguments:"
echo " --output-format: Optional output format (yaml|identity, default: yaml)"
echo " yaml - Store in config.yaml (embedded binary)"
echo " identity - Save to identity.key file (base64 encoded)"
echo " config-path: Optional path to config.yaml (default: /etc/pymc_repeater/config.yaml)"
echo " config-path: Optional path to config.yaml (default: /etc/openhop_repeater/config.yaml)"
echo ""
echo "Examples:"
echo " # Save to config.yaml (default)"
@@ -67,7 +67,7 @@ fi
# Set default config path if not provided
if [ -z "$CONFIG_PATH" ]; then
CONFIG_PATH="/etc/pymc_repeater/config.yaml"
CONFIG_PATH="/etc/openhop_repeater/config.yaml"
fi
# Validate hex string
@@ -92,7 +92,7 @@ if [ "$OUTPUT_FORMAT" = "yaml" ]; then
fi
else
# For identity format, use system-wide location matching config.yaml
IDENTITY_DIR="/etc/pymc_repeater"
IDENTITY_DIR="/etc/openhop_repeater"
IDENTITY_PATH="$IDENTITY_DIR/identity.key"
fi
@@ -261,39 +261,39 @@ fi
# Offer to restart service (only relevant for yaml format)
if [ "$OUTPUT_FORMAT" = "yaml" ]; then
if systemctl is-active --quiet pymc-repeater 2>/dev/null; then
read -p "Restart pymc-repeater service now? (yes/no): " RESTART
if systemctl is-active --quiet openhop-repeater 2>/dev/null; then
read -p "Restart openhop-repeater service now? (yes/no): " RESTART
if [ "$RESTART" = "yes" ]; then
systemctl restart pymc-repeater
systemctl restart openhop-repeater
echo "✓ Service restarted"
echo ""
echo "Check logs for new identity:"
echo " sudo journalctl -u pymc-repeater -f | grep -i 'identity\|hash'"
echo " sudo journalctl -u openhop-repeater -f | grep -i 'identity\|hash'"
else
echo "Remember to restart the service:"
echo " sudo systemctl restart pymc-repeater"
echo " sudo systemctl restart openhop-repeater"
fi
else
echo "Note: pymc-repeater service is not running"
echo "Start it with: sudo systemctl start pymc-repeater"
echo "Note: openhop-repeater service is not running"
echo "Start it with: sudo systemctl start openhop-repeater"
fi
else
echo "Identity key saved to file."
echo ""
if systemctl is-active --quiet pymc-repeater 2>/dev/null; then
read -p "Restart pymc-repeater service now? (yes/no): " RESTART
if systemctl is-active --quiet openhop-repeater 2>/dev/null; then
read -p "Restart openhop-repeater service now? (yes/no): " RESTART
if [ "$RESTART" = "yes" ]; then
systemctl restart pymc-repeater
systemctl restart openhop-repeater
echo "✓ Service restarted"
echo ""
echo "Check logs for new identity:"
echo " sudo journalctl -u pymc-repeater -f | grep -i 'identity\|hash'"
echo " sudo journalctl -u openhop-repeater -f | grep -i 'identity\|hash'"
else
echo "Remember to restart the service:"
echo " sudo systemctl restart pymc-repeater"
echo " sudo systemctl restart openhop-repeater"
fi
else
echo "Note: pymc-repeater service is not running"
echo "Start it with: sudo systemctl start pymc-repeater"
echo "Note: openhop-repeater service is not running"
echo "Start it with: sudo systemctl start openhop-repeater"
fi
fi
+1 -1
View File
@@ -3,4 +3,4 @@
*.substvars
.debhelper/
files
pymc-repeater/
openhop-repeater/
+1 -1
View File
@@ -1,4 +1,4 @@
pymc-repeater (1.0.5~dev0) unstable; urgency=medium
openhop-repeater (1.0.5~dev0) unstable; urgency=medium
* Development build from git commit 7112da9
* Version: 1.0.5.post0
+5 -5
View File
@@ -1,4 +1,4 @@
Source: pymc-repeater
Source: openhop-repeater
Section: net
Priority: optional
Maintainer: Rightup <rightup@pymc.dev>
@@ -15,10 +15,10 @@ Build-Depends: debhelper-compat (= 13),
python3-psutil,
git
Standards-Version: 4.6.2
Homepage: https://github.com/rightup/pyMC_Repeater
Homepage: https://github.com/rightup/openhop-repeater
X-Python3-Version: >= 3.9
Package: pymc-repeater
Package: openhop-repeater
Architecture: all
Depends: ${python3:Depends},
${misc:Depends},
@@ -36,8 +36,8 @@ Recommends: python3-periphery,
Description: PyMC Repeater Daemon
A mesh networking repeater daemon for LoRa devices.
.
This package provides the pymc-repeater service for managing
This package provides the openhop-repeater service for managing
mesh network repeater functionality with a web interface.
.
Note: This package will install pymc_core, cherrypy-cors, and ws4py
Note: This package will install openhop_core, cherrypy-cors, and ws4py
from PyPI during postinst as they are not available in Debian repos.
+1 -1
View File
@@ -1 +1 @@
pymc-repeater
openhop-repeater
-3
View File
@@ -1,3 +0,0 @@
etc/pymc_repeater
var/log/pymc_repeater
usr/share/pymc_repeater
-3
View File
@@ -1,3 +0,0 @@
config.yaml.example usr/share/pymc_repeater/
radio-presets.json usr/share/pymc_repeater/
radio-settings.json usr/share/pymc_repeater/
-57
View File
@@ -1,57 +0,0 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Create system user
if ! getent passwd pymc-repeater >/dev/null; then
adduser --system --group --home /var/lib/pymc-repeater \
--gecos "PyMC Repeater Service" pymc-repeater
fi
# Add user to gpio and spi groups for hardware access
if getent group gpio >/dev/null; then
usermod -a -G gpio pymc-repeater
fi
if getent group spi >/dev/null; then
usermod -a -G spi pymc-repeater
fi
# Create and set permissions on data directory
mkdir -p /var/lib/pymc_repeater
chown -R pymc-repeater:pymc-repeater /var/lib/pymc_repeater
chmod 750 /var/lib/pymc_repeater
# Set permissions
chown -R pymc-repeater:pymc-repeater /etc/pymc_repeater
chown -R pymc-repeater:pymc-repeater /var/log/pymc-repeater
chmod 750 /etc/pymc_repeater
chmod 750 /var/log/pymc-repeater
# Copy example config if no config exists
if [ ! -f /etc/pymc_repeater/config.yaml ]; then
cp /usr/share/pymc_repeater/config.yaml.example /etc/pymc_repeater/config.yaml
chown pymc-repeater:pymc-repeater /etc/pymc_repeater/config.yaml
chmod 640 /etc/pymc_repeater/config.yaml
fi
# Install pymc_core from PyPI if not already installed
if ! python3 -c "import pymc_core" 2>/dev/null; then
echo "Installing pymc_core[hardware] from PyPI..."
python3 -m pip install --break-system-packages 'pymc_core[hardware]>=1.0.7' || true
fi
# Install packages not available in Debian repos
if ! python3 -c "import cherrypy_cors" 2>/dev/null; then
echo "Installing cherrypy-cors from PyPI..."
python3 -m pip install --break-system-packages 'cherrypy-cors==1.7.0' || true
fi
if ! python3 -c "import ws4py" 2>/dev/null; then
echo "Installing ws4py from PyPI..."
python3 -m pip install --break-system-packages 'ws4py>=0.5.1' || true
fi
;;
esac
#DEBHELPER#
exit 0
-18
View File
@@ -1,18 +0,0 @@
#!/bin/sh
set -e
case "$1" in
purge)
# Remove user and directories
if getent passwd pymc-repeater >/dev/null; then
deluser --system pymc-repeater || true
fi
rm -rf /etc/pymc-repeater
rm -rf /var/log/pymc-repeater
rm -rf /var/lib/pymc-repeater
;;
esac
#DEBHELPER#
exit 0
-19
View File
@@ -1,19 +0,0 @@
[Unit]
Description=PyMC Repeater Daemon
After=network.target
[Service]
Type=simple
User=pymc-repeater
Group=pymc-repeater
WorkingDirectory=/etc/pymc-repeater
ExecStart=/usr/bin/pymc-repeater
Restart=always
RestartSec=10
# Allow GPS time sync to update CLOCK_REALTIME without running as root
CapabilityBoundingSet=CAP_SYS_TIME
AmbientCapabilities=CAP_SYS_TIME
[Install]
WantedBy=multi-user.target
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/make -f
# -*- makefile -*-
export PYBUILD_NAME=pymc-repeater
export PYBUILD_NAME=openhop-repeater
export DH_VERBOSE=1
%:
@@ -19,4 +19,4 @@ override_dh_auto_test:
rm -f repeater/_version.py
override_dh_installsystemd:
dh_installsystemd --name=pymc-repeater
dh_installsystemd --name=openhop-repeater
+2 -2
View File
@@ -1,6 +1,6 @@
services:
pymc-repeater:
image: pymc-repeater:local
openhop-repeater:
image: openhop-repeater:local
build:
context: .
dockerfile: dockerfile
+7 -7
View File
@@ -1,7 +1,7 @@
services:
pymc-repeater:
image: ${PYMC_REPEATER_IMAGE:-pymcdev/pymc-repeater:main}
container_name: pymc-repeater
openhop-repeater:
image: ${PYMC_REPEATER_IMAGE:-pymcdev/openhop-repeater:main}
container_name: openhop-repeater
restart: unless-stopped
ports:
- 8000:8000
@@ -21,9 +21,9 @@ services:
- "${SPI_GID:-989}"
- plugdev
volumes:
- ${PYMC_CONFIG_VOLUME:-pymc-repeater-config}:/etc/pymc_repeater
- ${PYMC_DATA_VOLUME:-pymc-repeater-data}:/var/lib/pymc_repeater
- ${PYMC_CONFIG_VOLUME:-openhop-repeater-config}:/etc/openhop_repeater
- ${PYMC_DATA_VOLUME:-openhop-repeater-data}:/var/lib/openhop_repeater
volumes:
pymc-repeater-config:
pymc-repeater-data:
openhop-repeater-config:
openhop-repeater-data:
+4 -4
View File
@@ -1,8 +1,8 @@
#!/bin/sh
set -eu
INSTALL_DIR="${INSTALL_DIR:-/opt/pymc_repeater}"
CONFIG_DIR="${CONFIG_DIR:-/etc/pymc_repeater}"
INSTALL_DIR="${INSTALL_DIR:-/opt/openhop_repeater}"
CONFIG_DIR="${CONFIG_DIR:-/etc/openhop_repeater}"
CONFIG_PATH="${PYMC_REPEATER_CONFIG:-${CONFIG_DIR}/config.yaml}"
EXAMPLE_PATH="${CONFIG_DIR}/config.yaml.example"
BUNDLED_EXAMPLE_PATH="${INSTALL_DIR}/config.yaml.example"
@@ -22,7 +22,7 @@ fail_bad_config_mount() {
echo "Invalid Docker config mount: ${CONFIG_PATH} is a directory, but it must be the config file." >&2
echo "This usually happens when ./config.yaml is bind-mounted before that host file exists." >&2
echo "Use the supported folder mount instead:" >&2
echo " - ./config:/etc/pymc_repeater" >&2
echo " - ./config:/etc/openhop_repeater" >&2
echo "Then place the config at ./config/config.yaml." >&2
print_permission_help
exit 1
@@ -40,7 +40,7 @@ copy_or_die() {
use_runtime_merged_config() {
src="$1"
runtime_dir="$(mktemp -d /tmp/pymc-repeater-config.XXXXXX)"
runtime_dir="$(mktemp -d /tmp/openhop-repeater-config.XXXXXX)"
runtime_config="${runtime_dir}/config.yaml"
if ! cp "${src}" "${runtime_config}"; then
+3 -3
View File
@@ -11,9 +11,9 @@ ARG SPI_GID=989
ARG TARGETARCH
ARG YQ_VERSION=v4.40.5
ENV INSTALL_DIR=/opt/pymc_repeater \
CONFIG_DIR=/etc/pymc_repeater \
DATA_DIR=/var/lib/pymc_repeater \
ENV INSTALL_DIR=/opt/openhop_repeater \
CONFIG_DIR=/etc/openhop_repeater \
DATA_DIR=/var/lib/openhop_repeater \
HOME_DIR=/home/${USER} \
PATH=/home/${USER}/.local/bin:${PATH} \
PYTHONUNBUFFERED=1 \
+1 -1
View File
@@ -1,6 +1,6 @@
# Adding a New Sensor Plug-in
Sensors in pyMC_Repeater are self-contained modules that live in `repeater/sensors/`. The subsystem is plug-in based: adding a new sensor requires only one new file. The manager discovers and loads it automatically at runtime by importing the module named after the sensor type.
Sensors in openhop-repeater are self-contained modules that live in `repeater/sensors/`. The subsystem is plug-in based: adding a new sensor requires only one new file. The manager discovers and loads it automatically at runtime by importing the module named after the sensor type.
---
+5 -5
View File
@@ -3,17 +3,17 @@
# Published image to run. Use a different repository or tag if you are testing
# a fork or a specific release.
PYMC_REPEATER_IMAGE=pymcdev/pymc-repeater:main
PYMC_REPEATER_IMAGE=pymcdev/openhop-repeater:main
# Storage defaults to Docker named volumes. This is the safest option for
# Portainer and fresh installs because Docker preserves the image ownership.
# To use host bind mounts instead, create the folders first and make them
# writable by UID/GID 15888:
# sudo mkdir -p /opt/pymc-repeater/config /opt/pymc-repeater/data
# sudo chown -R 15888:15888 /opt/pymc-repeater/config /opt/pymc-repeater/data
# sudo mkdir -p /opt/openhop-repeater/config /opt/openhop-repeater/data
# sudo chown -R 15888:15888 /opt/openhop-repeater/config /opt/openhop-repeater/data
# Then uncomment and adjust these paths:
# PYMC_CONFIG_VOLUME=/opt/pymc-repeater/config
# PYMC_DATA_VOLUME=/opt/pymc-repeater/data
# PYMC_CONFIG_VOLUME=/opt/openhop-repeater/config
# PYMC_DATA_VOLUME=/opt/openhop-repeater/data
# SPI/GPIO access uses the host's numeric group IDs. Check your host with:
# getent group gpio
+223 -124
View File
@@ -1,18 +1,24 @@
#!/bin/bash
# pyMC Repeater Management Script - Deploy, Upgrade, Uninstall
# openHop Repeater Management Script - Deploy, Upgrade, Uninstall
set -e
INSTALL_DIR="/opt/pymc_repeater"
INSTALL_DIR="/opt/openhop_repeater"
VENV_DIR="$INSTALL_DIR/venv"
VENV_PIP="$VENV_DIR/bin/pip"
VENV_PYTHON="$VENV_DIR/bin/python"
CONFIG_DIR="/etc/pymc_repeater"
LOG_DIR="/var/log/pymc_repeater"
CONFIG_DIR="/etc/openhop_repeater"
LOG_DIR="/var/log/openhop_repeater"
DATA_DIR="/var/lib/openhop_repeater"
SERVICE_USER="repeater"
SERVICE_NAME="pymc-repeater"
SERVICE_NAME="openhop-repeater"
SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}"
LEGACY_INSTALL_DIR="/opt/openhop-repeater"
LEGACY_CONFIG_DIR="/etc/openhop-repeater"
LEGACY_LOG_DIR="/var/log/openhop-repeater"
LEGACY_DATA_DIR="/var/lib/openhop-repeater"
# R2 Wheels Configuration improves install speed on ARM devices
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
R2_ENABLED=1 # Set to 0 to disable R2 wheels and always build from source
@@ -21,7 +27,68 @@ R2_ENABLED=1 # Set to 0 to disable R2 wheels and always build from source
# Virtual-environment helpers
# ---------------------------------------------------------------------------
# Create (or re-create) the dedicated venv for pymc_repeater
cleanup_stale_source_trees() {
local removed=0
local path
for path in \
"$INSTALL_DIR/repeater" \
"$INSTALL_DIR/openhop_core" \
"$INSTALL_DIR/openhop-repeater" \
"$INSTALL_DIR/openhop-core" \
"$LEGACY_INSTALL_DIR/repeater" \
"$LEGACY_INSTALL_DIR/openhop_core" \
"$LEGACY_INSTALL_DIR/openhop-repeater" \
"$LEGACY_INSTALL_DIR/openhop-core"
do
if [ -e "$path" ]; then
rm -rf "$path"
removed=1
echo " ✓ Removed stale source tree at $path"
fi
done
if [ "$removed" -eq 0 ]; then
echo " ✓ No stale source-tree paths found"
fi
}
migrate_legacy_paths() {
local timestamp legacy current label backup_path
timestamp="$(date +%Y%m%d_%H%M%S)"
migrate_one_path() {
legacy="$1"
current="$2"
label="$3"
if [ ! -e "$legacy" ]; then
return 0
fi
mkdir -p "$current" 2>/dev/null || true
if [ ! -e "$current" ] || [ -z "$(ls -A "$current" 2>/dev/null)" ]; then
rm -rf "$current" 2>/dev/null || true
mv "$legacy" "$current"
echo " ✓ Migrated legacy $label path: $legacy -> $current"
return 0
fi
cp -an "$legacy"/. "$current"/ 2>/dev/null || true
backup_path="${legacy}.migrated.${timestamp}"
mv "$legacy" "$backup_path"
echo " ✓ Merged legacy $label data into $current"
echo " ✓ Archived legacy $label path at $backup_path"
}
migrate_one_path "$LEGACY_CONFIG_DIR" "$CONFIG_DIR" "config"
migrate_one_path "$LEGACY_LOG_DIR" "$LOG_DIR" "log"
migrate_one_path "$LEGACY_DATA_DIR" "$DATA_DIR" "data"
migrate_one_path "$LEGACY_INSTALL_DIR" "$INSTALL_DIR" "install"
}
# Create (or re-create) the dedicated venv for openhop_repeater
ensure_venv() {
if [ ! -x "$VENV_PYTHON" ]; then
echo ">>> Creating virtual environment at $VENV_DIR ..."
@@ -40,15 +107,16 @@ migrate_to_venv() {
ensure_venv
# 2. Remove legacy PYTHONPATH from the service unit
local svc_unit="/etc/systemd/system/pymc-repeater.service"
local svc_unit="/etc/systemd/system/openhop-repeater.service"
if [ -f "$svc_unit" ]; then
if grep -q 'PYTHONPATH' "$svc_unit" 2>/dev/null; then
sed -i '/^Environment=.*PYTHONPATH/d' "$svc_unit"
echo " ✓ Removed legacy PYTHONPATH from service unit"
fi
# 3. Fix WorkingDirectory if still pointing at old source
if grep -q 'WorkingDirectory=/opt/pymc_repeater' "$svc_unit" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|' "$svc_unit"
if grep -q 'WorkingDirectory=/opt/openhop_repeater\|WorkingDirectory=/opt/openhop-repeater' "$svc_unit" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$svc_unit"
sed -i 's|WorkingDirectory=/opt/openhop-repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$svc_unit"
echo " ✓ Fixed WorkingDirectory in service unit"
fi
# 4. Ensure ExecStart uses the venv python
@@ -60,15 +128,12 @@ migrate_to_venv() {
fi
# 5. Remove the package from system python (best-effort)
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
python3 -m pip uninstall -y openhop_repeater 2>/dev/null || true
python3 -m pip uninstall -y openhop_core 2>/dev/null || true
echo " ✓ Cleaned up system-level packages (if any)"
# 6. Remove stale source trees that could shadow the venv package
if [ -d "$INSTALL_DIR/repeater" ]; then
rm -rf "$INSTALL_DIR/repeater"
echo " ✓ Removed stale source tree from $INSTALL_DIR/repeater"
fi
cleanup_stale_source_trees
}
is_silent_flag() {
@@ -117,22 +182,22 @@ fi
# Function to show info box
show_info() {
$DIALOG --backtitle "pyMC Repeater Management" --title "$1" --msgbox "$2" 12 70
$DIALOG --backtitle "openHop Repeater Management" --title "$1" --msgbox "$2" 12 70
}
# Function to show error box
show_error() {
$DIALOG --backtitle "pyMC Repeater Management" --title "Error" --msgbox "$1" 8 60
$DIALOG --backtitle "openHop Repeater Management" --title "Error" --msgbox "$1" 8 60
}
# Function to ask yes/no question
ask_yes_no() {
$DIALOG --backtitle "pyMC Repeater Management" --title "$1" --yesno "$2" 10 70
$DIALOG --backtitle "openHop Repeater Management" --title "$1" --yesno "$2" 10 70
}
# Function to show progress
show_progress() {
echo "$2" | $DIALOG --backtitle "pyMC Repeater Management" --title "$1" --gauge "$3" 8 70 0
echo "$2" | $DIALOG --backtitle "openHop Repeater Management" --title "$1" --gauge "$3" 8 70 0
}
# Function to check if service exists
@@ -159,11 +224,11 @@ is_enabled() {
get_version() {
# Read version from the pip-installed package in the venv
if [ -x "$VENV_PYTHON" ]; then
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null \
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('openhop_repeater'))" 2>/dev/null \
|| echo "not installed"
else
# Fallback: try system python for pre-migration installs
python3 -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null \
python3 -c "from importlib.metadata import version; print(version('openhop_repeater'))" 2>/dev/null \
|| echo "not installed"
fi
}
@@ -183,11 +248,11 @@ get_status_display() {
show_main_menu() {
local status=$(get_status_display)
CHOICE=$($DIALOG --backtitle "pyMC Repeater Management" --title "pyMC Repeater Management" --menu "\nCurrent Status: $status\n\nChoose an action:" 18 70 9 \
"install" "Install pyMC Repeater" \
CHOICE=$($DIALOG --backtitle "openHop Repeater Management" --title "openHop Repeater Management" --menu "\nCurrent Status: $status\n\nChoose an action:" 18 70 9 \
"install" "Install openHop Repeater" \
"upgrade" "Upgrade existing installation" \
"reset" "reset existing installation to defaults" \
"uninstall" "Remove pyMC Repeater completely" \
"uninstall" "Remove openHop Repeater completely" \
"config" "Configure radio settings" \
"start" "Start the service" \
"stop" "Stop the service" \
@@ -199,7 +264,7 @@ show_main_menu() {
case $CHOICE in
"install")
if is_installed; then
show_error "pyMC Repeater is already installed!\n\nUse 'upgrade' to update or 'uninstall' first."
show_error "openHop Repeater is already installed!\n\nUse 'upgrade' to update or 'uninstall' first."
else
install_repeater
fi
@@ -208,21 +273,21 @@ show_main_menu() {
if is_installed; then
upgrade_repeater "false"
else
show_error "pyMC Repeater is not installed!\n\nUse 'install' first."
show_error "openHop Repeater is not installed!\n\nUse 'install' first."
fi
;;
"reset")
if is_installed; then
reset_repeater
else
show_error "pyMC Repeater is not installed!\n\nUse 'install' first."
show_error "openHop Repeater is not installed!\n\nUse 'install' first."
fi
;;
"uninstall")
if is_installed; then
uninstall_repeater
else
show_error "pyMC Repeater is not installed."
show_error "openHop Repeater is not installed."
fi
;;
"config")
@@ -240,7 +305,7 @@ show_main_menu() {
"logs")
clear
echo -e "\033[1;36m╔══════════════════════════════════════════════════════════════════════╗\033[0m"
echo -e "\033[1;36m║\033[0m \033[1;37mpyMC Repeater - Live Logs\033[0m \033[1;36m║\033[0m"
echo -e "\033[1;36m║\033[0m \033[1;37mopenHop Repeater - Live Logs\033[0m \033[1;36m║\033[0m"
echo -e "\033[1;36m║\033[0m \033[0;90m(Press Ctrl+C to return)\033[0m \033[1;36m║\033[0m"
echo -e "\033[1;36m╚══════════════════════════════════════════════════════════════════════╝\033[0m"
echo ""
@@ -265,7 +330,7 @@ install_repeater() {
# Welcome screen (Bypass if the script was passd with the "install" option, assume we want a silent install)
if [[ "${1:-}" != "install" ]]; then
$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
$DIALOG --backtitle "openHop Repeater Management" --title "Welcome" --msgbox "\nWelcome to openHop Repeater Setup\n\nThis installer will configure your Linux system as a LoRa mesh network repeater.\n\nPress OK to continue..." 12 70
fi
# SPI Check - Universal approach that works on all boards (skip for CH341 USB-SPI adapter)
@@ -321,13 +386,13 @@ install_repeater() {
# Installation progress
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " Installing pyMC Repeater"
echo " Installing openHop 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"
useradd --system --home "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER"
fi
(
@@ -336,8 +401,12 @@ install_repeater() {
getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true
done
echo "20"; echo "# Creating directories..."
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
echo "20"; echo "# Migrating legacy paths..."
migrate_legacy_paths
cleanup_stale_source_trees
echo "23"; echo "# Creating directories..."
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
echo "25"; echo "# Installing system dependencies..."
apt-get update -qq
@@ -367,9 +436,9 @@ install_repeater() {
echo "29"; echo "# Installing files..."
cp "$SCRIPT_DIR/manage.sh" "$INSTALL_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/pymc-repeater.service" "$INSTALL_DIR/" 2>/dev/null || true
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
cp "$SCRIPT_DIR/openhop-repeater.service" "$INSTALL_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true
echo "45"; echo "# Installing configuration..."
cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml.example"
@@ -378,28 +447,28 @@ install_repeater() {
fi
echo "55"; echo "# Installing systemd service..."
cp "$SCRIPT_DIR/pymc-repeater.service" /etc/systemd/system/
cp "$SCRIPT_DIR/openhop-repeater.service" /etc/systemd/system/
systemctl daemon-reload
echo "58"; 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
if [ -f "$SCRIPT_DIR/../openhop-core/99-ch341.rules" ]; then
cp "$SCRIPT_DIR/../openhop-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..."
# Venv stays root-owned (pip runs as root); service user only needs read+execute
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
chmod 750 "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
chmod 750 "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
# Ensure manage.sh and support files in INSTALL_DIR are accessible
chown root:root "$INSTALL_DIR"
chmod 755 "$INSTALL_DIR"
# Ensure the service user can create subdirectories in their home directory
chmod 755 /var/lib/pymc_repeater
chmod 755 "$DATA_DIR"
# Pre-create the .config directory that the service will need
mkdir -p /var/lib/pymc_repeater/.config/pymc_repeater
chown -R "$SERVICE_USER:$SERVICE_USER" /var/lib/pymc_repeater/.config
mkdir -p "$DATA_DIR/.config/openhop_repeater"
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR/.config"
# Configure polkit for passwordless service restart
@@ -410,38 +479,38 @@ install_repeater() {
echo "Polkit 0.106 or greater detected, using rules file"
echo ">>> Configuring polkit for service management..."
mkdir -p /etc/polkit-1/rules.d
cat > /etc/polkit-1/rules.d/10-pymc-repeater.rules <<'EOF'
cat > /etc/polkit-1/rules.d/10-openhop-repeater.rules <<'EOF'
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "pymc-repeater.service" &&
action.lookup("unit") == "openhop-repeater.service" &&
subject.user == "repeater") {
return polkit.Result.YES;
}
});
EOF
chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules
chmod 0644 /etc/polkit-1/rules.d/10-openhop-repeater.rules
else
echo "Polkit 0.105 or less detected, using pkla file"
mkdir -p /etc/polkit-1/localauthority/50-local.d
cat > /etc/polkit-1/localauthority/50-local.d/10-pymc-repeater.pkla <<'EOF'
[Allow repeater to restart pymc-repeater service]
cat > /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla <<'EOF'
[Allow repeater to restart openhop-repeater service]
Identity=unix-user:repeater
Action=org.freedesktop.systemd1.manage-units
ResultAny=yes
ResultInactive=yes
ResultActive=yes
EOF
chmod 0644 /etc/polkit-1/localauthority/50-local.d/10-pymc-repeater.pkla
chmod 0644 /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla
fi
# 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, /usr/local/bin/pymc-do-upgrade
cat > /etc/sudoers.d/openhop-repeater <<'EOF'
# Allow repeater user to manage the openhop-repeater service without password
repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart openhop-repeater, /usr/bin/systemctl stop openhop-repeater, /usr/bin/systemctl start openhop-repeater, /usr/bin/systemctl status openhop-repeater, /usr/local/bin/pymc-do-upgrade
EOF
chmod 0440 /etc/sudoers.d/pymc-repeater
chmod 0440 /etc/sudoers.d/openhop-repeater
echo ">>> Installing OTA upgrade wrapper..."
cat > /usr/local/bin/pymc-do-upgrade <<'UPGRADEEOF'
@@ -451,7 +520,7 @@ EOF
set -e
CHANNEL="${1:-main}"
PRETEND_VERSION="${2:-}"
VENV_DIR="/opt/pymc_repeater/venv"
VENV_DIR="/opt/openhop_repeater/venv"
VENV_PIP="$VENV_DIR/bin/pip"
VENV_PYTHON="$VENV_DIR/bin/python"
# Validate: only allow safe git ref characters
@@ -468,14 +537,22 @@ if [ ! -x "$VENV_PYTHON" ]; then
python3 -m venv --system-site-packages "$VENV_DIR"
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
fi
# ---- Legacy path migration: openhop-repeater -> openhop_repeater ----
if [ -d /opt/openhop-repeater ] && [ ! -e /opt/openhop_repeater ]; then
mv /opt/openhop-repeater /opt/openhop_repeater
fi
# ---- Migration: clean up legacy service unit issues ----
SVC_UNIT=/etc/systemd/system/pymc-repeater.service
SVC_UNIT=/etc/systemd/system/openhop-repeater.service
if grep -q 'PYTHONPATH' "$SVC_UNIT" 2>/dev/null; then
sed -i '/^Environment=.*PYTHONPATH/d' "$SVC_UNIT"
systemctl daemon-reload
fi
if grep -q 'WorkingDirectory=/opt/pymc_repeater' "$SVC_UNIT" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|' "$SVC_UNIT"
if grep -q 'WorkingDirectory=/opt/openhop_repeater' "$SVC_UNIT" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
systemctl daemon-reload
fi
if grep -q 'WorkingDirectory=/opt/openhop-repeater' "$SVC_UNIT" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/openhop-repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
systemctl daemon-reload
fi
if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
@@ -483,10 +560,13 @@ if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
systemctl daemon-reload
fi
# ---- Remove stale source trees that shadow the venv package ----
[ -d /opt/pymc_repeater/repeater ] && rm -rf /opt/pymc_repeater/repeater
[ -d /opt/openhop_repeater/repeater ] && rm -rf /opt/openhop_repeater/repeater
[ -d /opt/openhop-repeater/repeater ] && rm -rf /opt/openhop-repeater/repeater
[ -d /opt/openhop_repeater/openhop-repeater ] && rm -rf /opt/openhop_repeater/openhop-repeater
[ -d /opt/openhop-repeater/openhop-repeater ] && rm -rf /opt/openhop-repeater/openhop-repeater
# ---- Remove old system-level packages to avoid confusion ----
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
python3 -m pip uninstall -y openhop_repeater 2>/dev/null || true
python3 -m pip uninstall -y openhop_core 2>/dev/null || true
# ---- Try R2 wheels first for faster OTA upgrades ----
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
MACHINE_ARCH=$(uname -m)
@@ -502,14 +582,14 @@ if [ -n "$ARCH_TAG" ]; then
echo "[pymc-do-upgrade] Trying dependencies from R2 wheels..."
"$VENV_PIP" install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
fi
# ---- Install pymc_repeater from git ----
# ---- Install openhop_repeater from git ----
if "$VENV_PIP" install \
--upgrade \
--no-cache-dir \
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"; then
"openhop_repeater[hardware] @ git+https://github.com/rightup/openhop-repeater.git@${CHANNEL}"; then
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/pyMC_Repeater/${CHANNEL}"
RADIO_STORAGE_DIR="/var/lib/pymc_repeater"
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/openhop-repeater/${CHANNEL}"
RADIO_STORAGE_DIR="/var/lib/openhop_repeater"
mkdir -p "$RADIO_STORAGE_DIR"
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
wget -qO "$RADIO_STORAGE_DIR/radio-presets.json" "${RADIO_BASE_URL}/radio-presets.json" 2>/dev/null || true
@@ -523,13 +603,13 @@ UPGRADEEOF
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
) | $DIALOG --backtitle "openHop Repeater Management" --title "Installing" --gauge "Setting up openHop Repeater..." 8 70 0
# Install Python package outside of progress gauge for better error handling
clear
echo "=== Installing Python Dependencies ==="
echo ""
echo "Installing pymc_repeater and dependencies (including pymc_core from PyPI)..."
echo "Installing openhop_repeater and dependencies (including openhop_core from PyPI)..."
echo "This may take a few minutes..."
echo ""
@@ -556,7 +636,7 @@ UPGRADEEOF
# Ensure venv exists
ensure_venv
echo "Installing pymc_repeater into venv ($VENV_DIR)..."
echo "Installing openhop_repeater into venv ($VENV_DIR)..."
# Attempt R2 wheels first for faster installation
if [ "$R2_ENABLED" -eq 1 ]; then
@@ -660,7 +740,7 @@ reset_repeater() {
local current_version=$(get_version)
if ask_yes_no "Confirm Reset of pyMC Repeater restoring to default configuration.\n\nContinue?"; then
if ask_yes_no "Confirm Reset of openHop Repeater restoring to default configuration.\n\nContinue?"; then
# Show info that upgrade is starting
show_info "Reseting" "Starting reset process...\n\nProgress will be shown in the terminal."
@@ -731,7 +811,7 @@ upgrade_repeater() {
local current_version=$(get_version)
if [[ "$silent" != "true" ]]; then
if ! ask_yes_no "Confirm Upgrade" "Current version: $current_version\n\nThis will upgrade pyMC Repeater while preserving your configuration.\n\nContinue?"; then
if ! ask_yes_no "Confirm Upgrade" "Current version: $current_version\n\nThis will upgrade openHop Repeater while preserving your configuration.\n\nContinue?"; then
return 0
fi
@@ -746,6 +826,10 @@ upgrade_repeater() {
echo "[1/9] Stopping service..."
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
echo "[1.5/9] Migrating legacy paths..."
migrate_legacy_paths
cleanup_stale_source_trees
echo "[2/9] Backing up configuration..."
if [ -d "$CONFIG_DIR" ]; then
cp -r "$CONFIG_DIR" "$CONFIG_DIR.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
@@ -777,11 +861,11 @@ upgrade_repeater() {
echo "[4/9] Installing files..."
SCRIPT_DIR="$(dirname "$0")"
if ! cp "$SCRIPT_DIR/pymc-repeater.service" /etc/systemd/system/; then
if ! cp "$SCRIPT_DIR/openhop-repeater.service" /etc/systemd/system/; then
echo " ⚠ Warning: Failed to update service file old service file may remain"
fi
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
cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true
echo " ✓ Files updated"
echo "[5/9] Validating and updating configuration..."
@@ -797,8 +881,8 @@ upgrade_repeater() {
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
if [ -f "$SCRIPT_DIR_UPGRADE/../openhop-core/99-ch341.rules" ]; then
cp "$SCRIPT_DIR_UPGRADE/../openhop-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"
@@ -810,15 +894,15 @@ upgrade_repeater() {
echo "[6/9] Fixing permissions..."
# Venv stays root-owned (pip runs as root); service user only needs read+execute
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater 2>/dev/null || true
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" 2>/dev/null || true
chown root:root "$INSTALL_DIR" 2>/dev/null || true
chmod 755 "$INSTALL_DIR" 2>/dev/null || true
chmod 750 "$CONFIG_DIR" "$LOG_DIR" 2>/dev/null || true
chmod 755 /var/lib/pymc_repeater 2>/dev/null || true
chmod 755 "$DATA_DIR" 2>/dev/null || true
# Pre-create the .config directory that the service will need
mkdir -p /var/lib/pymc_repeater/.config/pymc_repeater 2>/dev/null || true
chown -R "$SERVICE_USER:$SERVICE_USER" /var/lib/pymc_repeater/.config 2>/dev/null || true
mkdir -p "$DATA_DIR/.config/openhop_repeater" 2>/dev/null || true
chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR/.config" 2>/dev/null || true
# Configure polkit for passwordless service restart
POLKIT_VERSION=$(pkaction --version 2>/dev/null | awk '{print $NF}')
@@ -826,36 +910,36 @@ upgrade_repeater() {
echo "Polkit 0.106 or greater detected, using rules file"
echo ">>> Configuring polkit for service management..."
mkdir -p /etc/polkit-1/rules.d
cat > /etc/polkit-1/rules.d/10-pymc-repeater.rules <<'EOF'
cat > /etc/polkit-1/rules.d/10-openhop-repeater.rules <<'EOF'
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "pymc-repeater.service" &&
action.lookup("unit") == "openhop-repeater.service" &&
subject.user == "repeater") {
return polkit.Result.YES;
}
});
EOF
chmod 0644 /etc/polkit-1/rules.d/10-pymc-repeater.rules
chmod 0644 /etc/polkit-1/rules.d/10-openhop-repeater.rules
else
echo "Polkit 0.105 or less detected, using pkla file"
mkdir -p /etc/polkit-1/localauthority/50-local.d
cat > /etc/polkit-1/localauthority/50-local.d/10-pymc-repeater.pkla <<'EOF'
[Allow repeater to restart pymc-repeater service]
cat > /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla <<'EOF'
[Allow repeater to restart openhop-repeater service]
Identity=unix-user:repeater
Action=org.freedesktop.systemd1.manage-units
ResultAny=yes
ResultInactive=yes
ResultActive=yes
EOF
chmod 0644 /etc/polkit-1/localauthority/50-local.d/10-pymc-repeater.pkla
chmod 0644 /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla
fi
# 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, /usr/local/bin/pymc-do-upgrade
cat > /etc/sudoers.d/openhop-repeater <<'EOF'
# Allow repeater user to manage the openhop-repeater service without password
repeater ALL=(root) NOPASSWD: /usr/bin/systemctl restart openhop-repeater, /usr/bin/systemctl stop openhop-repeater, /usr/bin/systemctl start openhop-repeater, /usr/bin/systemctl status openhop-repeater, /usr/local/bin/pymc-do-upgrade
EOF
chmod 0440 /etc/sudoers.d/pymc-repeater
chmod 0440 /etc/sudoers.d/openhop-repeater
# Install / refresh OTA upgrade wrapper
cat > /usr/local/bin/pymc-do-upgrade <<'UPGRADEEOF'
#!/bin/bash
@@ -864,7 +948,7 @@ EOF
set -e
CHANNEL="${1:-main}"
PRETEND_VERSION="${2:-}"
VENV_DIR="/opt/pymc_repeater/venv"
VENV_DIR="/opt/openhop_repeater/venv"
VENV_PIP="$VENV_DIR/bin/pip"
VENV_PYTHON="$VENV_DIR/bin/python"
# Validate: only allow safe git ref characters
@@ -881,14 +965,22 @@ if [ ! -x "$VENV_PYTHON" ]; then
python3 -m venv --system-site-packages "$VENV_DIR"
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
fi
# ---- Legacy path migration: openhop-repeater -> openhop_repeater ----
if [ -d /opt/openhop-repeater ] && [ ! -e /opt/openhop_repeater ]; then
mv /opt/openhop-repeater /opt/openhop_repeater
fi
# ---- Migration: clean up legacy service unit issues ----
SVC_UNIT=/etc/systemd/system/pymc-repeater.service
SVC_UNIT=/etc/systemd/system/openhop-repeater.service
if grep -q 'PYTHONPATH' "$SVC_UNIT" 2>/dev/null; then
sed -i '/^Environment=.*PYTHONPATH/d' "$SVC_UNIT"
systemctl daemon-reload
fi
if grep -q 'WorkingDirectory=/opt/pymc_repeater' "$SVC_UNIT" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|' "$SVC_UNIT"
if grep -q 'WorkingDirectory=/opt/openhop_repeater' "$SVC_UNIT" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
systemctl daemon-reload
fi
if grep -q 'WorkingDirectory=/opt/openhop-repeater' "$SVC_UNIT" 2>/dev/null; then
sed -i 's|WorkingDirectory=/opt/openhop-repeater|WorkingDirectory=/var/lib/openhop_repeater|' "$SVC_UNIT"
systemctl daemon-reload
fi
if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
@@ -896,10 +988,13 @@ if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
systemctl daemon-reload
fi
# ---- Remove stale source trees that shadow the venv package ----
[ -d /opt/pymc_repeater/repeater ] && rm -rf /opt/pymc_repeater/repeater
[ -d /opt/openhop_repeater/repeater ] && rm -rf /opt/openhop_repeater/repeater
[ -d /opt/openhop-repeater/repeater ] && rm -rf /opt/openhop-repeater/repeater
[ -d /opt/openhop_repeater/openhop-repeater ] && rm -rf /opt/openhop_repeater/openhop-repeater
[ -d /opt/openhop-repeater/openhop-repeater ] && rm -rf /opt/openhop-repeater/openhop-repeater
# ---- Remove old system-level packages to avoid confusion ----
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
python3 -m pip uninstall -y openhop_repeater 2>/dev/null || true
python3 -m pip uninstall -y openhop_core 2>/dev/null || true
# ---- Try R2 wheels first for faster OTA upgrades ----
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
MACHINE_ARCH=$(uname -m)
@@ -915,14 +1010,14 @@ python3 -m pip uninstall -y pymc_core 2>/dev/null || true
echo "[pymc-do-upgrade] Trying dependencies from R2 wheels..."
"$VENV_PIP" install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
fi
# ---- Install pymc_repeater from git ----
# ---- Install openhop_repeater from git ----
if "$VENV_PIP" install \
--upgrade \
--no-cache-dir \
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"; then
"openhop_repeater[hardware] @ git+https://github.com/rightup/openhop-repeater.git@${CHANNEL}"; then
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/pyMC_Repeater/${CHANNEL}"
RADIO_STORAGE_DIR="/var/lib/pymc_repeater"
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/openhop-repeater/${CHANNEL}"
RADIO_STORAGE_DIR="/var/lib/openhop_repeater"
mkdir -p "$RADIO_STORAGE_DIR"
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
wget -qO "$RADIO_STORAGE_DIR/radio-presets.json" "${RADIO_BASE_URL}/radio-presets.json" 2>/dev/null || true
@@ -939,7 +1034,7 @@ UPGRADEEOF
echo "=== Installing Python Dependencies ==="
echo ""
echo "Updating pymc_repeater and dependencies (including pymc_core from PyPI)..."
echo "Updating openhop_repeater and dependencies (including openhop_core from PyPI)..."
echo "This may take a few minutes..."
echo ""
@@ -969,7 +1064,7 @@ UPGRADEEOF
migrate_to_venv
# Install into the venv (clean, no system-packages flags needed)
echo "Upgrading pymc_repeater into venv ($VENV_DIR)..."
echo "Upgrading openhop_repeater into venv ($VENV_DIR)..."
# Attempt R2 wheels first for faster installation
if [ "$R2_ENABLED" -eq 1 ]; then
@@ -1091,10 +1186,10 @@ uninstall_repeater() {
return
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
if ask_yes_no "Confirm Uninstall" "This will completely remove openHop 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 ""
echo "═══════════════════════════════════════════════════════════════"
echo " Uninstalling pyMC Repeater"
echo " Uninstalling openHop Repeater"
echo "═══════════════════════════════════════════════════════════════"
echo ""
@@ -1105,24 +1200,28 @@ uninstall_repeater() {
(
echo "20"; 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
cp -r "$CONFIG_DIR" "/tmp/openhop_repeater_config_backup_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
fi
echo "40"; echo "# Removing service files..."
rm -f /etc/systemd/system/pymc-repeater.service
rm -f /etc/systemd/system/openhop-repeater.service
systemctl daemon-reload
echo "50"; echo "# Removing polkit and sudoers rules..."
rm -f /etc/polkit-1/rules.d/10-pymc-repeater.rules || true
rm -f /etc/polkit-1/localauthority/50-local.d/10-pymc-repeater.pkla || true
rm -f /etc/sudoers.d/pymc-repeater
rm -f /etc/polkit-1/rules.d/10-openhop-repeater.rules || true
rm -f /etc/polkit-1/localauthority/50-local.d/10-openhop-repeater.pkla || true
rm -f /etc/sudoers.d/openhop-repeater
rm -f /usr/local/bin/pymc-do-upgrade
echo "60"; echo "# Removing installation..."
rm -rf "$INSTALL_DIR"
rm -rf "$CONFIG_DIR"
rm -rf "$LOG_DIR"
rm -rf /var/lib/pymc_repeater
rm -rf "$DATA_DIR"
rm -rf "$LEGACY_INSTALL_DIR"
rm -rf "$LEGACY_CONFIG_DIR"
rm -rf "$LEGACY_LOG_DIR"
rm -rf "$LEGACY_DATA_DIR"
echo "80"; echo "# Removing service user..."
if id "$SERVICE_USER" &>/dev/null; then
@@ -1130,9 +1229,9 @@ uninstall_repeater() {
fi
echo "100"; echo "# Uninstall complete!"
) | $DIALOG --backtitle "pyMC Repeater Management" --title "Uninstalling" --gauge "Removing pyMC Repeater..." 8 70 0
) | $DIALOG --backtitle "openHop Repeater Management" --title "Uninstalling" --gauge "Removing openHop Repeater..." 8 70 0
show_info "Uninstall Complete" "\npyMC Repeater has been completely removed.\n\nConfiguration backup saved to /tmp/\n\nThank you for using pyMC Repeater!"
show_info "Uninstall Complete" "\nopenHop Repeater has been completely removed.\n\nConfiguration backup saved to /tmp/\n\nThank you for using openHop Repeater!"
fi
}
@@ -1167,9 +1266,9 @@ manage_service() {
systemctl start "$SERVICE_NAME"
if is_running; then
if [[ "$silent" == "true" ]]; then
echo "✓ pyMC Repeater service has been started successfully."
echo "✓ openHop Repeater service has been started successfully."
else
show_info "Service Started" "\n✓ pyMC Repeater service has been started successfully."
show_info "Service Started" "\n✓ openHop Repeater service has been started successfully."
fi
else
if [[ "$silent" == "true" ]]; then
@@ -1183,18 +1282,18 @@ manage_service() {
"stop")
systemctl stop "$SERVICE_NAME"
if [[ "$silent" == "true" ]]; then
echo "✓ pyMC Repeater service has been stopped."
echo "✓ openHop Repeater service has been stopped."
else
show_info "Service Stopped" "\n✓ pyMC Repeater service has been stopped."
show_info "Service Stopped" "\n✓ openHop Repeater service has been stopped."
fi
;;
"restart")
systemctl restart "$SERVICE_NAME"
if is_running; then
if [[ "$silent" == "true" ]]; then
echo "✓ pyMC Repeater service has been restarted successfully."
echo "✓ openHop Repeater service has been restarted successfully."
else
show_info "Service Restarted" "\n✓ pyMC Repeater service has been restarted successfully."
show_info "Service Restarted" "\n✓ openHop Repeater service has been restarted successfully."
fi
else
if [[ "$silent" == "true" ]]; then
@@ -1324,14 +1423,14 @@ validate_and_update_config() {
# Main script logic
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "pyMC Repeater Management Script"
echo "openHop Repeater Management Script"
echo ""
echo "Usage: $0 [action]"
echo ""
echo "Actions:"
echo " install - Install pyMC Repeater"
echo " install - Install openHop Repeater"
echo " upgrade - Upgrade existing installation (CLI is silent by default; use --interactive to show dialogs)"
echo " uninstall - Remove pyMC Repeater"
echo " uninstall - Remove openHop Repeater"
echo " config - Configure radio settings"
echo " start - Start the service (CLI is silent by default; use --interactive to show dialogs)"
echo " stop - Stop the service (CLI is silent by default; use --interactive to show dialogs)"
@@ -1355,7 +1454,7 @@ if [ "$1" = "debug" ]; then
echo "Script: $0"
echo ""
echo "Testing dialog..."
$DIALOG --backtitle "pyMC Repeater Management" --title "Test" --msgbox "Dialog test successful!" 8 40
$DIALOG --backtitle "openHop Repeater Management" --title "Test" --msgbox "Dialog test successful!" 8 40
echo "Dialog test completed."
exit 0
fi
@@ -1393,7 +1492,7 @@ case "$1" in
"logs")
clear
echo -e "\033[1;36m╔══════════════════════════════════════════════════════════════════════╗\033[0m"
echo -e "\033[1;36m║\033[0m \033[1;37mpyMC Repeater - Live Logs\033[0m \033[1;36m║\033[0m"
echo -e "\033[1;36m║\033[0m \033[1;37mopenHop Repeater - Live Logs\033[0m \033[1;36m║\033[0m"
echo -e "\033[1;36m║\033[0m \033[0;90m(Press Ctrl+C to return)\033[0m \033[1;36m║\033[0m"
echo -e "\033[1;36m╚══════════════════════════════════════════════════════════════════════╝\033[0m"
echo ""
@@ -1,8 +1,8 @@
#Systemd service file template for Py MC - Meshcore Repeater Daemon.
#Install as /etc/systemd/system/pymc-repeater.service
#Systemd service file template for openHop - Meshcore Repeater Daemon.
#Install as /etc/systemd/system/openhop-repeater.service
[Unit]
Description=pyMC Repeater Daemon
Description=openHop Repeater Daemon
After=network-online.target dbus.socket
Wants=network-online.target
Requires=dbus.socket
@@ -11,10 +11,10 @@ Requires=dbus.socket
Type=simple
User=repeater
Group=repeater
WorkingDirectory=/var/lib/pymc_repeater
WorkingDirectory=/var/lib/openhop_repeater
# Start command - use venv python to avoid system package conflicts
ExecStart=/opt/pymc_repeater/venv/bin/python -m repeater.main --config /etc/pymc_repeater/config.yaml
ExecStart=/opt/openhop_repeater/venv/bin/python -m repeater.main --config /etc/openhop_repeater/config.yaml
# Restart on failure
Restart=on-failure
@@ -29,10 +29,10 @@ MemoryHigh=256M
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=pymc-repeater
SyslogIdentifier=openhop-repeater
# Security (relaxed for service self-restart via sudo)
ReadWritePaths=/var/log/pymc_repeater /var/lib/pymc_repeater /etc/pymc_repeater
ReadWritePaths=/var/log/openhop_repeater /var/lib/openhop_repeater /etc/openhop_repeater
SupplementaryGroups=plugdev dialout
# Allow GPS time sync to update CLOCK_REALTIME without running as root, also allow changing user via polkit or sudo
+4 -4
View File
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel", "setuptools_scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pymc_repeater"
name = "openhop_repeater"
dynamic = ["version"]
authors = [
{name = "Rightup", email = "rightup@pymc.dev"},
@@ -29,7 +29,7 @@ keywords = ["mesh", "networking", "lora", "repeater", "daemon", "iot"]
dependencies = [
"pymc_core[hardware] @ git+https://github.com/rightup/pyMC_core.git@dev",
"openhop_core[hardware]",
"pyyaml>=6.0.0",
"cherrypy>=18.0.0",
"paho-mqtt>=1.6.0",
@@ -45,7 +45,7 @@ dependencies = [
[project.optional-dependencies]
# SX1262/SPI support (Linux only; required for Raspberry Pi HATs)
hardware = [
"pymc_core[hardware] @ git+https://github.com/rightup/pyMC_core.git@dev",
"openhop_core[hardware] @ git+https://github.com/rightup/openhop-core.git@dev",
]
# RRD metrics (Performance Metrics chart); system librrd required (e.g. apt install rrdtool)
rrd = [
@@ -59,7 +59,7 @@ dev = [
]
[project.scripts]
pymc-repeater = "repeater.main:main"
openhop-repeater = "repeater.main:main"
pymc-cli = "repeater.local_cli:main"
[tool.setuptools.packages.find]
+1 -1
View File
@@ -411,7 +411,7 @@
},
"kiss": {
"name": "KISS modem (serial)",
"description": "MeshCore KISS modem over serial - requires pyMC_core with KISS support.",
"description": "MeshCore KISS modem over serial - requires openhop-core with KISS support.",
"connection_type": "usb",
"radio_type": "kiss",
"tx_power": 14,
+1 -1
View File
@@ -4,6 +4,6 @@ except ImportError:
try:
from importlib.metadata import version
__version__ = version("pymc_repeater")
__version__ = version("openhop_repeater")
except Exception:
__version__ = "unknown"
+1 -1
View File
@@ -1,4 +1,4 @@
"""Companion identity support for pyMC Repeater.
"""Companion identity support for openHop Repeater.
Exposes the MeshCore companion frame protocol over TCP for standard clients.
"""
+1 -1
View File
@@ -13,7 +13,7 @@ import logging
from enum import Enum
from typing import Any, Callable, Optional
from pymc_core.companion import CompanionBridge
from openhop_core.companion import CompanionBridge
logger = logging.getLogger("RepeaterCompanionBridge")
+3 -3
View File
@@ -1,11 +1,11 @@
"""Companion frame protocol constants — re-exported from pyMC_core.
"""Companion frame protocol constants — re-exported from openhop-core.
All protocol constants now live in :mod:`pymc_core.companion.constants`.
All protocol constants now live in :mod:`openhop_core.companion.constants`.
This module re-exports them so existing repeater imports continue to work.
"""
# Re-exports; F401 ignored for re-exported names.
from pymc_core.companion.constants import ( # noqa: F401
from openhop_core.companion.constants import ( # noqa: F401
ADV_TYPE_CHAT,
ADV_TYPE_REPEATER,
ADV_TYPE_ROOM,
+6 -6
View File
@@ -1,7 +1,7 @@
"""
Repeater-specific CompanionFrameServer with SQLite persistence.
Thin subclass of :class:`pymc_core.companion.frame_server.CompanionFrameServer`
Thin subclass of :class:`openhop_core.companion.frame_server.CompanionFrameServer`
that adds SQLite-backed message, contact, and channel persistence via a
``sqlite_handler`` dependency.
"""
@@ -12,9 +12,9 @@ import asyncio
import logging
from typing import Optional
from pymc_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
from pymc_core.companion.frame_server import CompanionFrameServer as _BaseFrameServer
from pymc_core.companion.models import QueuedMessage
from openhop_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
from openhop_core.companion.frame_server import CompanionFrameServer as _BaseFrameServer
from openhop_core.companion.models import QueuedMessage
logger = logging.getLogger("CompanionFrameServer")
@@ -45,8 +45,8 @@ class CompanionFrameServer(_BaseFrameServer):
port=port,
bind_address=bind_address,
client_idle_timeout_sec=client_idle_timeout_sec,
device_model="pyMC-Repeater-Companion",
device_version=None, # use FIRMWARE_VER_CODE from pyMC_core
device_model="openHop-Repeater-Companion",
device_version=None, # use FIRMWARE_VER_CODE from openhop-core
build_date="13 Feb 2026",
local_hash=local_hash,
stats_getter=stats_getter,
+1 -1
View File
@@ -50,7 +50,7 @@ def derive_companion_public_key_hex(identity_key: Any) -> Optional[str]:
if raw is None:
return None
try:
from pymc_core import LocalIdentity
from openhop_core import LocalIdentity
identity = LocalIdentity(seed=raw)
return identity.get_public_key().hex()
+4 -4
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from typing import Any, Dict, Optional
from pymc_core.companion.constants import DEFAULT_MAX_CONTACTS
from openhop_core.companion.constants import DEFAULT_MAX_CONTACTS
logger = logging.getLogger(__name__)
@@ -14,7 +14,7 @@ _INVALID_NODE_NAME_CHARS = "\n\r\x00"
# Optional per-companion RepeaterCompanionBridge constructor settings (power-user).
COMPANION_BRIDGE_SETTING_KEYS = frozenset({"max_contacts", "offline_queue_size"})
# Settings that must not be applied from config (fixed at pymc_core defaults).
# Settings that must not be applied from config (fixed at openhop_core defaults).
_COMPANION_IGNORED_BRIDGE_KEYS = frozenset({"max_channels", "adv_type"})
# Contact flag bit 0 marks a favourite (protected from forced-trim eviction).
@@ -102,7 +102,7 @@ def parse_companion_bridge_kwargs(settings: dict) -> Dict[str, int]:
def effective_max_contacts(bridge_kwargs: Dict[str, int]) -> int:
"""Return max_contacts from parsed kwargs or pymc_core default."""
"""Return max_contacts from parsed kwargs or openhop_core default."""
return bridge_kwargs.get("max_contacts", DEFAULT_MAX_CONTACTS)
@@ -253,7 +253,7 @@ def format_companion_bridge_limits(bridge_kwargs: Dict[str, int]) -> str:
def companion_hash_str_from_identity_key(identity_key: Any) -> str:
"""Derive companion_hash storage key (0xHH) from an identity_key config value."""
from pymc_core import LocalIdentity
from openhop_core import LocalIdentity
if isinstance(identity_key, str):
key_bytes = bytes.fromhex(normalize_companion_identity_key(identity_key))
+21 -21
View File
@@ -140,7 +140,7 @@ def resolve_storage_dir(
config: Dict[str, Any],
*,
config_path: Optional[str] = None,
default: str = "/var/lib/pymc_repeater",
default: str = "/var/lib/openhop_repeater",
) -> Path:
storage_dir_cfg = (
@@ -195,7 +195,7 @@ def get_node_info(config: Dict[str, Any]) -> Dict[str, Any]:
def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
if config_path is None:
config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/pymc_repeater/config.yaml")
config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/openhop_repeater/config.yaml")
# Check if config file exists
if not Path(config_path).exists():
@@ -235,7 +235,7 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
"request_timeout_seconds": 10,
"verify_tls": True,
"api_token": None,
"cert_store_dir": "/etc/pymc_repeater/glass",
"cert_store_dir": "/etc/openhop_repeater/glass",
}
if "gps" not in config:
@@ -323,7 +323,7 @@ def save_config(config_data: Dict[str, Any], config_path: Optional[str] = None)
True if successful, False otherwise
"""
if config_path is None:
config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/pymc_repeater/config.yaml")
config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/openhop_repeater/config.yaml")
try:
# Create backup of existing config
@@ -387,16 +387,16 @@ def _load_or_create_identity_key(path: Optional[str] = None) -> bytes:
if path is None:
# Check system-wide location first (matches config.yaml location)
system_key_path = Path("/etc/pymc_repeater/identity.key")
system_key_path = Path("/etc/openhop_repeater/identity.key")
if system_key_path.exists():
key_path = system_key_path
else:
# Follow XDG spec
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
if xdg_config_home:
config_dir = Path(xdg_config_home) / "pymc_repeater"
config_dir = Path(xdg_config_home) / "openhop_repeater"
else:
config_dir = Path.home() / ".config" / "pymc_repeater"
config_dir = Path.home() / ".config" / "openhop_repeater"
key_path = config_dir / "identity.key"
else:
key_path = Path(path)
@@ -475,7 +475,7 @@ def get_radio_for_board(board_config: dict):
radio_type = "kiss"
if radio_type in ("sx1262", "sx1262_ch341"):
from pymc_core.hardware.sx1262_wrapper import SX1262Radio
from openhop_core.hardware.sx1262_wrapper import SX1262Radio
# Get radio and SPI configuration - all settings must be in config file
spi_config = board_config.get("sx1262")
@@ -492,8 +492,8 @@ def get_radio_for_board(board_config: dict):
if not ch341_cfg:
raise ValueError("Missing 'ch341' section in configuration file")
from pymc_core.hardware.lora.LoRaRF.SX126x import set_spi_transport
from pymc_core.hardware.transports.ch341_spi_transport import CH341SPITransport
from openhop_core.hardware.lora.LoRaRF.SX126x import set_spi_transport
from openhop_core.hardware.transports.ch341_spi_transport import CH341SPITransport
vid = _parse_int(ch341_cfg.get("vid"), default=0x1A86)
pid = _parse_int(ch341_cfg.get("pid"), default=0x5512)
@@ -534,7 +534,7 @@ def get_radio_for_board(board_config: dict):
combined_config["en_pins"] = en_pins
# Add optional GPIO parameters if specified in config
# These wont be supported by older versions of pymc_core
# These wont be supported by older versions of openhop_core
if "gpio_chip" in spi_config:
combined_config["gpio_chip"] = _parse_int(spi_config["gpio_chip"], default=0)
if "use_gpiod_backend" in spi_config:
@@ -554,16 +554,16 @@ def get_radio_for_board(board_config: dict):
elif radio_type == "kiss":
try:
from pymc_core.hardware.kiss_modem_wrapper import KissModemWrapper
from openhop_core.hardware.kiss_modem_wrapper import KissModemWrapper
except ImportError:
try:
from pymc_core.hardware.kiss_serial_wrapper import (
from openhop_core.hardware.kiss_serial_wrapper import (
KissSerialWrapper as KissModemWrapper,
)
except ImportError:
raise RuntimeError(
"KISS modem support requires pyMC_core with KISS support. "
"Install your fork with: pip install -e /path/to/pyMC_core"
"KISS modem support requires openhop-core with KISS support. "
"Install your fork with: pip install -e /path/to/openhop-core"
) from None
kiss_config = board_config.get("kiss")
@@ -613,11 +613,11 @@ def get_radio_for_board(board_config: dict):
elif radio_type == "pymc_tcp":
try:
from pymc_core.hardware.tcp_radio import TCPLoRaRadio
from openhop_core.hardware.tcp_radio import TCPLoRaRadio
except ImportError:
raise RuntimeError(
"pymc_tcp radio requires pyMC_core >= the release that includes "
"PR pyMC-dev/pyMC_core#68 (merged 2026-05-13). "
"pymc_tcp radio requires openhop-core >= the release that includes "
"PR pyMC-dev/openhop-core#68 (merged 2026-05-13). "
"Reinstall the [hardware] extra to pick it up."
) from None
@@ -657,11 +657,11 @@ def get_radio_for_board(board_config: dict):
elif radio_type == "pymc_usb":
try:
from pymc_core.hardware.usb_radio import USBLoRaRadio
from openhop_core.hardware.usb_radio import USBLoRaRadio
except ImportError:
raise RuntimeError(
"pymc_usb radio requires pyMC_core >= the release that includes "
"PR pyMC-dev/pyMC_core#68 (merged 2026-05-13). "
"pymc_usb radio requires openhop-core >= the release that includes "
"PR pyMC-dev/openhop-core#68 (merged 2026-05-13). "
"Reinstall the [hardware] extra to pick it up."
) from None
+3 -3
View File
@@ -47,7 +47,7 @@ class GlassHandler:
self.verify_tls = True
self.api_token = "" # nosec - runtime config value, not a hardcoded credential
self.inform_interval_seconds = 30
self.cert_store_dir = "/etc/pymc_repeater/glass"
self.cert_store_dir = "/etc/openhop_repeater/glass"
self._cert_expires_at: Optional[str] = None
self.mqtt_enabled = False
self.mqtt_broker_host = "localhost"
@@ -132,8 +132,8 @@ class GlassHandler:
int(glass_cfg.get("inform_interval_seconds", self.inform_interval_seconds))
)
self.cert_store_dir = str(
glass_cfg.get("cert_store_dir", "/etc/pymc_repeater/glass")
or "/etc/pymc_repeater/glass"
glass_cfg.get("cert_store_dir", "/etc/openhop_repeater/glass")
or "/etc/openhop_repeater/glass"
)
self.client_cert_path = (
str(glass_cfg.get("client_cert_path")).strip()
+2 -2
View File
@@ -255,7 +255,7 @@ class _BrokerConnection:
)
self.base_topic = f"meshcore/{self.iata_code}/{self.public_key}"
from pymc_core.protocol.utils import PAYLOAD_TYPES
from openhop_core.protocol.utils import PAYLOAD_TYPES
disallowed_types = broker.get("disallowed_packet_types", [])
type_name_map = {name: code for code, name in PAYLOAD_TYPES.items()}
@@ -973,7 +973,7 @@ class MeshCoreToMqttPusher:
"model": "PyMC-Repeater",
"firmware_version": self.app_version,
"radio": radio_config or self.radio_config,
"client_version": f"pyMC_repeater/{self.app_version}",
"client_version": f"openhop_repeater/{self.app_version}",
"stats": {**live_stats, "errors": 0, "queue_len": 0, **(extra_stats or {})},
}
+3 -3
View File
@@ -1201,9 +1201,9 @@ class SQLiteHandler:
return cached["value"]
cutoff = now - (hours * 3600)
# Align with pyMC_core feat/newRadios PAYLOAD_TYPES (0x0B = CONTROL)
# Align with openhop-core feat/newRadios PAYLOAD_TYPES (0x0B = CONTROL)
try:
from pymc_core.protocol.utils import PAYLOAD_TYPES as _PT
from openhop_core.protocol.utils import PAYLOAD_TYPES as _PT
_human = {
"REQ": "Request",
@@ -1764,7 +1764,7 @@ class SQLiteHandler:
A base64-encoded transport key derived from the name
"""
try:
from pymc_core.protocol.transport_keys import get_auto_key_for
from openhop_core.protocol.transport_keys import get_auto_key_for
key_bytes = get_auto_key_for(name)
+5 -5
View File
@@ -6,9 +6,9 @@ import time
from collections import OrderedDict, deque
from typing import Optional, Tuple
from pymc_core.node.handlers.base import BaseHandler
from pymc_core.protocol import Packet
from pymc_core.protocol.constants import (
from openhop_core.node.handlers.base import BaseHandler
from openhop_core.protocol import Packet
from openhop_core.protocol.constants import (
MAX_PATH_SIZE,
PAYLOAD_TYPE_ADVERT,
PAYLOAD_TYPE_ANON_REQ,
@@ -19,7 +19,7 @@ from pymc_core.protocol.constants import (
ROUTE_TYPE_TRANSPORT_DIRECT,
ROUTE_TYPE_TRANSPORT_FLOOD,
)
from pymc_core.protocol.packet_utils import PacketHeaderUtils, PathUtils
from openhop_core.protocol.packet_utils import PacketHeaderUtils, PathUtils
from repeater.airtime import AirtimeManager
from repeater.data_acquisition import StorageCollector
@@ -820,7 +820,7 @@ class RepeaterHandler(BaseHandler):
return False, "No storage available for transport key validation"
try:
from pymc_core.protocol.transport_keys import calc_transport_code
from openhop_core.protocol.transport_keys import calc_transport_code
# Check cache validity
current_time = time.time()
+1 -1
View File
@@ -1,4 +1,4 @@
"""Handler helper modules for pyMC Repeater."""
"""Handler helper modules for openHop Repeater."""
from .advert import AdvertHelper
from .discovery import DiscoveryHelper
+2 -2
View File
@@ -2,8 +2,8 @@ import logging
import time
from typing import Dict, Optional
from pymc_core.protocol import Identity
from pymc_core.protocol.constants import PUB_KEY_SIZE
from openhop_core.protocol import Identity
from openhop_core.protocol.constants import PUB_KEY_SIZE
logger = logging.getLogger("ACL")
+3 -3
View File
@@ -1,5 +1,5 @@
"""
Advertisement packet handling helper for pyMC Repeater.
Advertisement packet handling helper for openHop Repeater.
This module processes advertisement packets for neighbor tracking and discovery.
Includes adaptive rate limiting based on mesh activity.
@@ -13,7 +13,7 @@ from collections import OrderedDict, deque
from enum import Enum
from typing import Dict, Optional, Tuple
from pymc_core.node.handlers.advert import AdvertHandler
from openhop_core.node.handlers.advert import AdvertHandler
logger = logging.getLogger("AdvertHelper")
@@ -603,7 +603,7 @@ class AdvertHelper:
return
# Get route type from packet header
from pymc_core.protocol.constants import PH_ROUTE_MASK
from openhop_core.protocol.constants import PH_ROUTE_MASK
route_type = packet.header & PH_ROUTE_MASK
+3 -3
View File
@@ -1,5 +1,5 @@
"""
Discovery request/response handling helper for pyMC Repeater.
Discovery request/response handling helper for openHop Repeater.
This module handles the processing and response to discovery requests,
allowing other nodes to discover repeaters on the mesh network.
@@ -9,7 +9,7 @@ import asyncio
import logging
import secrets
from pymc_core.node.handlers.control import ControlHandler
from openhop_core.node.handlers.control import ControlHandler
logger = logging.getLogger("DiscoveryHelper")
@@ -133,7 +133,7 @@ class DiscoveryHelper:
try:
our_pub_key = self.local_identity.get_public_key()
from pymc_core.protocol.packet_builder import PacketBuilder
from openhop_core.protocol.packet_builder import PacketBuilder
response_packet = PacketBuilder.create_discovery_response(
tag=tag,
+5 -5
View File
@@ -1,5 +1,5 @@
"""
Login/ANON_REQ packet handling helper for pyMC Repeater.
Login/ANON_REQ packet handling helper for openHop Repeater.
This module processes login requests and manages authentication for all identities.
"""
@@ -8,9 +8,9 @@ import asyncio
import logging
import time
from pymc_core.node.handlers.anon_request import AnonRateLimiter, AnonRequestHandler
from pymc_core.node.handlers.login_server import LoginServerHandler
from pymc_core.protocol.constants import PAYLOAD_TYPE_ANON_REQ
from openhop_core.node.handlers.anon_request import AnonRateLimiter, AnonRequestHandler
from openhop_core.node.handlers.login_server import LoginServerHandler
from openhop_core.protocol.constants import PAYLOAD_TYPE_ANON_REQ
logger = logging.getLogger("LoginHelper")
@@ -167,7 +167,7 @@ class LoginHelper:
emit the ``*`` wildcard region first (unless unscoped flood is denied),
then each allow-flood named region with a leading ``#`` stripped, with no
trailing comma. The firmware wildcard is the always-present default flood
scope; pyMC_repeater models that via ``mesh.unscoped_flood_allow``
scope; openhop_repeater models that via ``mesh.unscoped_flood_allow``
(falling back to ``mesh.global_flood_allow``, default allow).
"""
parts = []
+1 -1
View File
@@ -12,7 +12,7 @@ class PathHelper:
async def process_path_packet(self, packet):
from pymc_core.protocol.crypto import CryptoUtils
from openhop_core.protocol.crypto import CryptoUtils
try:
if len(packet.payload) < 2:
+4 -4
View File
@@ -1,5 +1,5 @@
"""
Protocol request (REQ) handling helper for pyMC Repeater.
Protocol request (REQ) handling helper for openHop Repeater.
Provides repeater-specific callbacks for status and telemetry requests.
"""
@@ -9,7 +9,7 @@ import logging
import struct
import time
from pymc_core.node.handlers.protocol_request import (
from openhop_core.node.handlers.protocol_request import (
REQ_TYPE_GET_ACCESS_LIST,
REQ_TYPE_GET_NEIGHBOURS,
REQ_TYPE_GET_OWNER_INFO,
@@ -368,14 +368,14 @@ class ProtocolRequestHelper:
Matches C++ simple_repeater: sprintf("%s\\n%s\\n%s", FIRMWARE_VERSION, node_name, owner_info)
"""
repeater_cfg = self.config.get("repeater", {})
node_name = repeater_cfg.get("node_name", "pyMC_Repeater")
node_name = repeater_cfg.get("node_name", "openhop-repeater")
owner_info = repeater_cfg.get("owner_info", "")
# Version: use package version if available, fallback to "pyMC"
try:
from importlib.metadata import version as pkg_version
fw_version = pkg_version("pymc-repeater")
fw_version = pkg_version("openhop-repeater")
except Exception:
fw_version = "pyMC"
+5 -5
View File
@@ -4,8 +4,8 @@ import secrets
import time
from typing import Dict
from pymc_core.protocol import CryptoUtils, PacketBuilder
from pymc_core.protocol.constants import PAYLOAD_TYPE_TXT_MSG
from openhop_core.protocol import CryptoUtils, PacketBuilder
from openhop_core.protocol.constants import PAYLOAD_TYPE_TXT_MSG
logger = logging.getLogger("RoomServer")
@@ -102,8 +102,8 @@ class RoomServer:
return False
try:
from pymc_core.protocol import PacketBuilder
from pymc_core.protocol.constants import (
from openhop_core.protocol import PacketBuilder
from openhop_core.protocol.constants import (
ADVERT_FLAG_HAS_NAME,
ADVERT_FLAG_IS_ROOM_SERVER,
)
@@ -357,7 +357,7 @@ class RoomServer:
timestamp.to_bytes(4, "little") + bytes([flags]) + author_prefix + message_bytes
)
# Calculate expected ACK (same algorithm as pymc_core)
# Calculate expected ACK (same algorithm as openhop_core)
attempt = 0
pack_data = PacketBuilder._pack_timestamp_data(timestamp, attempt, message_bytes)
ack_hash = CryptoUtils.sha256(pack_data + client_info.id.get_public_key())[:4]
+5 -5
View File
@@ -1,5 +1,5 @@
"""
Text message (TXT_MSG) handling helper for pyMC Repeater.
Text message (TXT_MSG) handling helper for openHop Repeater.
This module processes incoming text messages for all managed identities
(repeater identity + identity manager identities).
@@ -10,8 +10,8 @@ import asyncio
import logging
import time
from pymc_core.node.handlers.text import TextMessageHandler
from pymc_core.protocol import CryptoUtils, Identity
from openhop_core.node.handlers.text import TextMessageHandler
from openhop_core.protocol import CryptoUtils, Identity
from .mesh_cli import MeshCLI
from .room_server import RoomServer
@@ -603,8 +603,8 @@ class TextHelper:
"""
import time
from pymc_core.protocol import PacketBuilder
from pymc_core.protocol.constants import PAYLOAD_TYPE_TXT_MSG
from openhop_core.protocol import PacketBuilder
from openhop_core.protocol.constants import PAYLOAD_TYPE_TXT_MSG
try:
src_hash = original_packet.payload[1]
+5 -5
View File
@@ -1,5 +1,5 @@
"""
Trace packet handling helper for pyMC Repeater.
Trace packet handling helper for openHop Repeater.
This module handles the processing and forwarding of trace packets,
which are used for network diagnostics to track the path and SNR
@@ -11,10 +11,10 @@ import logging
import time
from typing import Any, Dict, List
from pymc_core.hardware.signal_utils import snr_register_to_db
from pymc_core.node.handlers.trace import TraceHandler
from pymc_core.protocol.constants import MAX_PATH_SIZE, ROUTE_TYPE_DIRECT
from pymc_core.protocol.packet_utils import PathUtils
from openhop_core.hardware.signal_utils import snr_register_to_db
from openhop_core.node.handlers.trace import TraceHandler
from openhop_core.protocol.constants import MAX_PATH_SIZE, ROUTE_TYPE_DIRECT
from openhop_core.protocol.packet_utils import PathUtils
logger = logging.getLogger("TraceHelper")
+4 -4
View File
@@ -1,5 +1,5 @@
"""
CLI client for pyMC Repeater.
CLI client for openHop Repeater.
Connects to an already-running repeater daemon via its HTTP API.
Reads admin password and HTTP port from the local config.yaml automatically.
"""
@@ -9,7 +9,7 @@ from typing import Optional
from urllib.parse import urlparse
CONFIG_PATHS = [
"/etc/pymc_repeater/config.yaml",
"/etc/openhop_repeater/config.yaml",
"config.yaml",
]
@@ -79,7 +79,7 @@ def run_client_cli(host: str = "127.0.0.1", port: int = 8000, password: Optional
print("Error: Authentication failed. Check password or repeater status.")
sys.exit(1)
print(f"\npyMC Repeater CLI (connected to {base_url})")
print(f"\nopenHop Repeater CLI (connected to {base_url})")
print("Type 'help' for available commands, 'exit' to quit.\n")
while True:
@@ -123,7 +123,7 @@ def main():
import argparse
parser = argparse.ArgumentParser(
description="Connect to a running pyMC Repeater and issue CLI commands"
description="Connect to a running openHop Repeater and issue CLI commands"
)
parser.add_argument(
"--config",
+19 -19
View File
@@ -175,8 +175,8 @@ class RepeaterDaemon:
self.radio = NullRadio()
try:
from pymc_core import LocalIdentity
from pymc_core.node.dispatcher import Dispatcher
from openhop_core import LocalIdentity
from openhop_core.node.dispatcher import Dispatcher
self.dispatcher = Dispatcher(self.radio)
logger.info("Dispatcher initialized")
@@ -307,7 +307,7 @@ class RepeaterDaemon:
# Initialize ConfigManager for centralized config management
self.config_manager = ConfigManager(
config_path=getattr(self, "config_path", "/etc/pymc_repeater/config.yaml"),
config_path=getattr(self, "config_path", "/etc/openhop_repeater/config.yaml"),
config=self.config,
daemon_instance=self,
)
@@ -395,7 +395,7 @@ class RepeaterDaemon:
# Load companion identities (CompanionBridge + frame server per companion)
await self._load_companion_identities()
# Subscribe to raw RX in pyMC_core so we can push PUSH_CODE_LOG_RX_DATA to companion clients
# Subscribe to raw RX in openhop-core so we can push PUSH_CODE_LOG_RX_DATA to companion clients
self.dispatcher.add_raw_rx_subscriber(self._on_raw_rx_for_companions)
n = len(getattr(self, "companion_frame_servers", []))
logger.info(
@@ -431,7 +431,7 @@ class RepeaterDaemon:
raise
async def _load_additional_identities(self):
from pymc_core import LocalIdentity
from openhop_core import LocalIdentity
identities_config = self.config.get("identities", {})
@@ -493,8 +493,8 @@ class RepeaterDaemon:
async def _load_companion_identities(self) -> None:
"""Load companion identities from config and create CompanionBridge + frame server for each."""
from pymc_core import LocalIdentity
from pymc_core.companion.models import Channel
from openhop_core import LocalIdentity
from openhop_core.companion.models import Channel
from repeater.companion import CompanionFrameServer, RepeaterCompanionBridge
@@ -651,7 +651,7 @@ class RepeaterDaemon:
for msg_dict in sqlite_handler.companion_load_messages(
companion_hash_str, limit=retention or 100
):
from pymc_core.companion.models import QueuedMessage
from openhop_core.companion.models import QueuedMessage
sk = msg_dict.get("sender_key", b"")
if isinstance(sk, str):
@@ -717,8 +717,8 @@ class RepeaterDaemon:
Creates RepeaterCompanionBridge, CompanionFrameServer, starts the server,
and registers with identity_manager. Raises on error.
"""
from pymc_core import LocalIdentity
from pymc_core.companion.models import Channel
from openhop_core import LocalIdentity
from openhop_core.companion.models import Channel
from repeater.companion import CompanionFrameServer, RepeaterCompanionBridge
from repeater.companion.constants import DEFAULT_PUBLIC_CHANNEL_SECRET
@@ -837,7 +837,7 @@ class RepeaterDaemon:
for msg_dict in sqlite_handler.companion_load_messages(
companion_hash_str, limit=retention or 100
):
from pymc_core.companion.models import QueuedMessage
from openhop_core.companion.models import QueuedMessage
sk = msg_dict.get("sender_key", b"")
if isinstance(sk, str):
@@ -1139,8 +1139,8 @@ class RepeaterDaemon:
return False
try:
from pymc_core.protocol import PacketBuilder
from pymc_core.protocol.constants import ADVERT_FLAG_HAS_NAME, ADVERT_FLAG_IS_REPEATER
from openhop_core.protocol import PacketBuilder
from openhop_core.protocol.constants import ADVERT_FLAG_HAS_NAME, ADVERT_FLAG_IS_REPEATER
# Get node name and location from config
repeater_config = self.config.get("repeater", {})
@@ -1332,7 +1332,7 @@ class RepeaterDaemon:
radio_type_raw = self.config.get("radio_type")
radio_type = "" if radio_type_raw is None else str(radio_type_raw).lower()
if radio_type == "sx1262_ch341":
from pymc_core.hardware.ch341.ch341_async import CH341Async
from openhop_core.hardware.ch341.ch341_async import CH341Async
CH341Async.reset_instance()
except Exception as e:
@@ -1402,7 +1402,7 @@ class RepeaterDaemon:
config=self.config,
event_loop=current_loop,
daemon_instance=self,
config_path=getattr(self, "config_path", "/etc/pymc_repeater/config.yaml"),
config_path=getattr(self, "config_path", "/etc/openhop_repeater/config.yaml"),
)
try:
@@ -1410,7 +1410,7 @@ class RepeaterDaemon:
except Exception as e:
logger.error(f"Failed to start HTTP server: {e}")
# Run dispatcher (handles RX/TX via pymc_core)
# Run dispatcher (handles RX/TX via openhop_core)
try:
await self.dispatcher.run_forever()
except asyncio.CancelledError:
@@ -1441,10 +1441,10 @@ def main():
import argparse
parser = argparse.ArgumentParser(description="pyMC Repeater Daemon")
parser = argparse.ArgumentParser(description="openHop Repeater Daemon")
parser.add_argument(
"--config",
help="Path to config file (default: /etc/pymc_repeater/config.yaml)",
help="Path to config file (default: /etc/openhop_repeater/config.yaml)",
)
parser.add_argument(
"--log-level",
@@ -1456,7 +1456,7 @@ def main():
# Load configuration
config = load_config(args.config)
config_path = args.config if args.config else "/etc/pymc_repeater/config.yaml"
config_path = args.config if args.config else "/etc/openhop_repeater/config.yaml"
if args.log_level:
if "logging" not in config:
+12 -12
View File
@@ -2,18 +2,18 @@ import asyncio
import logging
import time
from pymc_core.node.handlers.ack import AckHandler
from pymc_core.node.handlers.advert import AdvertHandler
from pymc_core.node.handlers.control import ControlHandler
from pymc_core.node.handlers.group_text import GroupTextHandler
from pymc_core.node.handlers.login_response import LoginResponseHandler
from pymc_core.node.handlers.login_server import LoginServerHandler
from pymc_core.node.handlers.path import PathHandler
from pymc_core.node.handlers.protocol_request import ProtocolRequestHandler
from pymc_core.node.handlers.protocol_response import ProtocolResponseHandler
from pymc_core.node.handlers.text import TextMessageHandler
from pymc_core.node.handlers.trace import TraceHandler
from pymc_core.protocol.constants import (
from openhop_core.node.handlers.ack import AckHandler
from openhop_core.node.handlers.advert import AdvertHandler
from openhop_core.node.handlers.control import ControlHandler
from openhop_core.node.handlers.group_text import GroupTextHandler
from openhop_core.node.handlers.login_response import LoginResponseHandler
from openhop_core.node.handlers.login_server import LoginServerHandler
from openhop_core.node.handlers.path import PathHandler
from openhop_core.node.handlers.protocol_request import ProtocolRequestHandler
from openhop_core.node.handlers.protocol_response import ProtocolResponseHandler
from openhop_core.node.handlers.text import TextMessageHandler
from openhop_core.node.handlers.trace import TraceHandler
from openhop_core.protocol.constants import (
PH_ROUTE_MASK,
ROUTE_TYPE_DIRECT,
ROUTE_TYPE_TRANSPORT_DIRECT,
+2 -2
View File
@@ -5,8 +5,8 @@ import logging
from dataclasses import dataclass
from typing import Any, Optional
from pymc_core.protocol.constants import PAYLOAD_TYPE_GRP_DATA, PAYLOAD_TYPE_GRP_TXT
from pymc_core.protocol.crypto import CryptoUtils
from openhop_core.protocol.constants import PAYLOAD_TYPE_GRP_DATA, PAYLOAD_TYPE_GRP_TXT
from openhop_core.protocol.crypto import CryptoUtils
logger = logging.getLogger("PolicyEngine")
+7 -7
View File
@@ -1,5 +1,5 @@
"""
Service management utilities for pyMC Repeater.
Service management utilities for openHop Repeater.
Provides functions for service control operations like restart.
"""
@@ -12,7 +12,7 @@ import time
from typing import Dict, Optional, Tuple
logger = logging.getLogger("ServiceUtils")
INIT_SCRIPT = "/etc/init.d/S80pymc-repeater"
INIT_SCRIPT = "/etc/init.d/S80openhop-repeater"
BUILDROOT_METADATA_PATH = "/etc/pymc-image-build-id"
_CONTAINER_RESTART_DELAY_SECONDS = 1.0
_SH_BIN = shutil.which("sh") or "sh"
@@ -91,14 +91,14 @@ def get_container_restart_message() -> str:
"""Return the user-facing restart message for containerized installs."""
return (
"Container restart initiated. "
"If you are running pyMC Repeater via Docker or Home Assistant, pull or rebuild "
"If you are running openHop Repeater via Docker or Home Assistant, pull or rebuild "
"a newer image for packaged image updates to take effect."
)
def restart_service() -> Tuple[bool, str]:
"""
Restart the pymc-repeater service.
Restart the openhop-repeater service.
On Buildroot/Luckfox, use the shipped init script directly.
On systemd hosts, try polkit-based restart first (plain systemctl), then
@@ -135,7 +135,7 @@ def restart_service() -> Tuple[bool, str]:
# Try polkit-based restart first (works on bare metal / VMs with polkit running)
try:
result = subprocess.run(
[_SYSTEMCTL_BIN, "restart", "pymc-repeater"],
[_SYSTEMCTL_BIN, "restart", "openhop-repeater"],
capture_output=True,
text=True,
timeout=5,
@@ -162,10 +162,10 @@ def restart_service() -> Tuple[bool, str]:
except Exception as e:
logger.warning(f"Polkit restart attempt failed: {e}")
# Fallback: use sudo (requires /etc/sudoers.d/pymc-repeater rule)
# Fallback: use sudo (requires /etc/sudoers.d/openhop-repeater rule)
try:
result = subprocess.run(
[_SUDO_BIN, "--non-interactive", _SYSTEMCTL_BIN, "restart", "pymc-repeater"],
[_SUDO_BIN, "--non-interactive", _SYSTEMCTL_BIN, "restart", "openhop-repeater"],
capture_output=True,
text=True,
timeout=5,
+12 -12
View File
@@ -9,7 +9,7 @@ from typing import Callable, Optional
import cherrypy
import yaml
from pymc_core.protocol import CryptoUtils
from openhop_core.protocol import CryptoUtils
from repeater import __version__
from repeater.companion.identity_resolve import (
@@ -198,7 +198,7 @@ class APIEndpoints:
self.config = config or {}
self.event_loop = event_loop
self.daemon_instance = daemon_instance
self._config_path = config_path or "/etc/pymc_repeater/config.yaml"
self._config_path = config_path or "/etc/openhop_repeater/config.yaml"
self.cad_calibration = CADCalibrationEngine(daemon_instance, event_loop)
# Initialize ConfigManager for centralized config management
@@ -1427,9 +1427,9 @@ class APIEndpoints:
stats["site_name"] = self.config.get("web", {}).get("site_name", "")
stats["version"] = __version__
try:
import pymc_core
import openhop_core
stats["core_version"] = pymc_core.__version__
stats["core_version"] = openhop_core.__version__
except ImportError:
stats["core_version"] = "unknown"
image_info = get_buildroot_image_info()
@@ -2162,7 +2162,7 @@ class APIEndpoints:
@cherrypy.tools.json_out()
@cherrypy.tools.json_in()
def restart_service(self):
"""Restart the pymc-repeater service via systemctl."""
"""Restart the openhop-repeater service via systemctl."""
# Enable CORS for this endpoint only if configured
self._set_cors_headers()
@@ -3819,7 +3819,7 @@ class APIEndpoints:
self.config["mesh"]["unscoped_flood_allow"] = unscoped_flood_allow
# Get the actual config path from daemon instance (same as CAD settings)
config_path = getattr(self, "_config_path", "/etc/pymc_repeater/config.yaml")
config_path = getattr(self, "_config_path", "/etc/openhop_repeater/config.yaml")
if self.daemon_instance and hasattr(self.daemon_instance, "config_path"):
config_path = self.daemon_instance.config_path
@@ -3932,7 +3932,7 @@ class APIEndpoints:
trace_tag = secrets.randbits(32)
# Create trace packet
from pymc_core.protocol import PacketBuilder
from openhop_core.protocol import PacketBuilder
path_bytes = list(target_hash.to_bytes(byte_count, "big"))
packet = PacketBuilder.create_trace(
@@ -4349,7 +4349,7 @@ class APIEndpoints:
companion_activation_error = None
if identity_type == "room_server" and self.daemon_instance:
try:
from pymc_core import LocalIdentity
from openhop_core import LocalIdentity
# Create LocalIdentity from the key (convert hex string to bytes)
if isinstance(identity_key, bytes):
@@ -4673,7 +4673,7 @@ class APIEndpoints:
if needs_reload and self.daemon_instance:
try:
from pymc_core import LocalIdentity
from openhop_core import LocalIdentity
final_name = identity["name"] # Could be new_name
identity_key = identity["identity_key"]
@@ -4961,8 +4961,8 @@ class APIEndpoints:
):
"""Send advert for a room server identity"""
try:
from pymc_core.protocol import PacketBuilder
from pymc_core.protocol.constants import (
from openhop_core.protocol import PacketBuilder
from openhop_core.protocol.constants import (
ADVERT_FLAG_HAS_NAME,
ADVERT_FLAG_IS_ROOM_SERVER,
)
@@ -6588,7 +6588,7 @@ class APIEndpoints:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pyMC Repeater API Documentation</title>
<title>openHop Repeater API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.10.0/swagger-ui.css">
<style>
body {
+1 -1
View File
@@ -15,7 +15,7 @@ import time
from typing import Optional
import cherrypy
from pymc_core.companion.constants import DEFAULT_OFFLINE_QUEUE_SIZE
from openhop_core.companion.constants import DEFAULT_OFFLINE_QUEUE_SIZE
from repeater.companion.utils import (
trim_companion_contacts_to_fit,
+4 -4
View File
@@ -1,8 +1,8 @@
openapi: 3.0.0
info:
title: pyMC Repeater API
title: openHop Repeater API
description: |
REST API for pyMC Repeater - LoRa mesh network repeater with room server functionality.
REST API for openHop Repeater - LoRa mesh network repeater with room server functionality.
## Features
- System statistics and monitoring
@@ -14,8 +14,8 @@ info:
- Noise floor monitoring
version: 1.0.0
contact:
name: pyMC Repeater
url: https://github.com/rightup/pyMC_Repeater
name: openHop Repeater
url: https://github.com/rightup/openhop-repeater
servers:
- url: /api
+96 -25
View File
@@ -1,5 +1,5 @@
"""
OTA Update endpoints for pyMC Repeater.
OTA Update endpoints for openHop Repeater.
Provides server-side GitHub version checks, background pip-based upgrades with
SSE progress streaming, and release-channel switching.
@@ -19,6 +19,7 @@ import json
import logging
import os
import re
import shutil
import ssl
# Required for fixed internal maintenance commands.
@@ -41,10 +42,10 @@ logger = logging.getLogger("HTTPServer")
# Repository constants
# ---------------------------------------------------------------------------
GITHUB_OWNER = "rightup"
GITHUB_REPO = "pyMC_Repeater"
GITHUB_REPO = "openhop-repeater"
GITHUB_RAW_BASE = f"https://raw.githubusercontent.com/{GITHUB_OWNER}/{GITHUB_REPO}"
GITHUB_API_BASE = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}"
PACKAGE_NAME = "pymc_repeater"
PACKAGE_NAME = "openhop_repeater"
# How long (seconds) before a cached check result expires
CHECK_CACHE_TTL = 600 # 10 minutes
@@ -52,6 +53,8 @@ _RM_BIN = "/bin/rm"
_SED_BIN = "/usr/bin/sed"
_SYSTEMCTL_BIN = "/bin/systemctl"
_SUDO_BIN = "/usr/bin/sudo"
_INSTALL_DIR = "/opt/openhop_repeater"
_LEGACY_INSTALL_DIR = "/opt/openhop-repeater"
_github_ssl_ctx: Optional[ssl.SSLContext] = None
_disk_version_mismatch_logged: Optional[tuple] = None
@@ -69,8 +72,9 @@ def _get_github_ssl_context() -> ssl.SSLContext:
def _find_buildroot_upgrade_helper() -> Optional[str]:
candidates = [
"/root/pyMC_Repeater/buildroot-manage.sh",
"/opt/pymc_repeater/pyMC_Repeater/buildroot-manage.sh",
"/root/openhop-repeater/buildroot-manage.sh",
"/opt/openhop_repeater/openhop-repeater/buildroot-manage.sh",
"/opt/openhop-repeater/openhop-repeater/buildroot-manage.sh",
"/root/scripts/buildroot-manage.sh",
]
for path in candidates:
@@ -89,7 +93,7 @@ class _RateLimitError(Exception):
def _get_installed_version(force_refresh: bool = False) -> str:
"""
Return the highest dist-info version found for pymc_repeater across all
Return the highest dist-info version found for openhop_repeater across all
directories the running interpreter actually uses.
Search strategy (union of all three to cover venvs, system, dist-packages):
@@ -135,7 +139,7 @@ def _get_installed_version(force_refresh: bool = False) -> str:
if p and ("site-packages" in p or "dist-packages" in p) and p not in dirs:
dirs.append(p)
# Explicitly include the dedicated venv's site-packages
_venv_site = "/opt/pymc_repeater/venv/lib"
_venv_site = "/opt/openhop_repeater/venv/lib"
if os.path.isdir(_venv_site):
for child in os.listdir(_venv_site):
sp = os.path.join(_venv_site, child, "site-packages")
@@ -228,7 +232,7 @@ def _get_installed_version(force_refresh: bool = False) -> str:
# Channels file persisted so the choice survives daemon restarts
_CHANNELS_FILE = "/var/lib/pymc_repeater/.update_channel"
_CHANNELS_FILE = "/var/lib/openhop_repeater/.update_channel"
def _detect_channel_from_dist_info() -> Optional[str]:
@@ -466,7 +470,7 @@ def _fetch_url(url: str, timeout: int = 10) -> str:
Raises _RateLimitError on HTTP 403 so callers can back off gracefully.
"""
installed = _get_installed_version()
headers = {"User-Agent": f"pymc-repeater/{installed}"}
headers = {"User-Agent": f"openhop-repeater/{installed}"}
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
@@ -565,7 +569,7 @@ def _cleanup_stale_dist_info(allow_sudo: bool = True) -> None:
except AttributeError:
pass
# Also scan the dedicated venv's site-packages
_venv_site = "/opt/pymc_repeater/venv/lib"
_venv_site = "/opt/openhop_repeater/venv/lib"
if os.path.isdir(_venv_site):
for child in os.listdir(_venv_site):
sp = os.path.join(_venv_site, child, "site-packages")
@@ -574,7 +578,7 @@ def _cleanup_stale_dist_info(allow_sudo: bool = True) -> None:
pkg_glob = PACKAGE_NAME.replace("-", "_") + "-*.dist-info"
# Collect {path: version_str} for every pymc_repeater dist-info we find
# Collect {path: version_str} for every openhop_repeater dist-info we find
found: dict = {}
for site_dir in dirs:
for meta_dir in glob.glob(os.path.join(site_dir, pkg_glob)):
@@ -794,15 +798,24 @@ def _migrate_service_unit() -> None:
logger.info("[Update] Buildroot image detected, skipping systemd unit migration.")
return
_SVC_UNIT = "/etc/systemd/system/pymc-repeater.service"
_VENV_PYTHON = "/opt/pymc_repeater/venv/bin/python"
_SVC_UNIT = "/etc/systemd/system/openhop-repeater.service"
_VENV_PYTHON = "/opt/openhop_repeater/venv/bin/python"
try:
subprocess.run([_SED_BIN, "-i", "/^Environment=.*PYTHONPATH/d", _SVC_UNIT], check=False) # nosec B603
subprocess.run(
[
_SED_BIN,
"-i",
"s|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|",
"s|WorkingDirectory=/opt/openhop_repeater|WorkingDirectory=/var/lib/openhop_repeater|",
_SVC_UNIT,
],
check=False,
) # nosec B603
subprocess.run(
[
_SED_BIN,
"-i",
"s|WorkingDirectory=/opt/openhop-repeater|WorkingDirectory=/var/lib/openhop_repeater|",
_SVC_UNIT,
],
check=False,
@@ -817,6 +830,68 @@ def _migrate_service_unit() -> None:
logger.warning(f"[Update] Service unit migration failed: {exc}")
def _merge_tree_missing_only(src: str, dst: str) -> None:
"""Merge src into dst without overwriting existing files."""
if not os.path.isdir(src):
return
for root, dirs, files in os.walk(src):
rel = os.path.relpath(root, src)
target_root = dst if rel == "." else os.path.join(dst, rel)
os.makedirs(target_root, exist_ok=True)
for d in dirs:
os.makedirs(os.path.join(target_root, d), exist_ok=True)
for f in files:
src_file = os.path.join(root, f)
dst_file = os.path.join(target_root, f)
if not os.path.exists(dst_file):
shutil.copy2(src_file, dst_file)
def _migrate_legacy_install_path() -> None:
"""Handle /opt/openhop-repeater -> /opt/openhop_repeater migration safely."""
if not os.path.exists(_LEGACY_INSTALL_DIR):
return
if not os.path.exists(_INSTALL_DIR):
os.rename(_LEGACY_INSTALL_DIR, _INSTALL_DIR)
_state.append_line(
f"[pyMC updater] Migrated legacy install path: {_LEGACY_INSTALL_DIR} -> {_INSTALL_DIR}"
)
return
_merge_tree_missing_only(_LEGACY_INSTALL_DIR, _INSTALL_DIR)
backup_path = f"{_LEGACY_INSTALL_DIR}.migrated.{time.strftime('%Y%m%d_%H%M%S')}"
os.rename(_LEGACY_INSTALL_DIR, backup_path)
_state.append_line(f"[pyMC updater] Merged legacy install data into {_INSTALL_DIR}")
_state.append_line(f"[pyMC updater] Archived legacy install path at {backup_path}")
def _cleanup_stale_source_trees() -> None:
stale_paths = [
"/opt/openhop_repeater/repeater",
"/opt/openhop_repeater/openhop_core",
"/opt/openhop_repeater/openhop-repeater",
"/opt/openhop_repeater/openhop-core",
"/opt/openhop-repeater/repeater",
"/opt/openhop-repeater/openhop_core",
"/opt/openhop-repeater/openhop-repeater",
"/opt/openhop-repeater/openhop-core",
]
removed_any = False
for stale_src in stale_paths:
if os.path.isdir(stale_src):
removed_any = True
_state.append_line(f"[pyMC updater] Removing stale source tree: {stale_src}")
shutil.rmtree(stale_src, ignore_errors=True)
if not removed_any:
_state.append_line("[pyMC updater] No stale source-tree paths found")
def _do_install() -> None:
channel = _state.channel
@@ -850,7 +925,7 @@ def _do_install() -> None:
env = _os.environ.copy()
env["SETUPTOOLS_SCM_PRETEND_VERSION"] = _state.latest_version or "1.0.0"
_VENV_DIR = "/opt/pymc_repeater/venv"
_VENV_DIR = "/opt/openhop_repeater/venv"
_VENV_PIP = os.path.join(_VENV_DIR, "bin", "pip")
_VENV_PYTHON = os.path.join(_VENV_DIR, "bin", "python")
@@ -874,6 +949,7 @@ def _do_install() -> None:
)
cmd = ["/bin/sh", _BUILDROOT_UPGRADE_HELPER, "upgrade"]
elif is_root:
_migrate_legacy_install_path()
_migrate_service_unit()
# Ensure venv exists (migration from system-pip era)
@@ -883,18 +959,13 @@ def _do_install() -> None:
_run([_VENV_PIP, "install", "--upgrade", "pip", "setuptools", "wheel"], env=env)
# Clean up system-level packages to avoid shadowing
_run(["/usr/bin/python3", "-m", "pip", "uninstall", "-y", "pymc_repeater"], env=env)
_run(["/usr/bin/python3", "-m", "pip", "uninstall", "-y", "pymc_core"], env=env)
_run(["/usr/bin/python3", "-m", "pip", "uninstall", "-y", "openhop_repeater"], env=env)
_run(["/usr/bin/python3", "-m", "pip", "uninstall", "-y", "openhop_core"], env=env)
# Remove stale source tree that could shadow the venv package
stale_src = "/opt/pymc_repeater/repeater"
if os.path.isdir(stale_src):
_state.append_line("[pyMC updater] Removing stale source tree…")
import shutil
# Remove stale source trees that could shadow the venv package
_cleanup_stale_source_trees()
shutil.rmtree(stale_src, ignore_errors=True)
install_spec = f"pymc_repeater[hardware] @ git+https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}.git@{channel}"
install_spec = f"openhop_repeater[hardware] @ git+https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}.git@{channel}"
_state.append_line("[pyMC updater] Running as root venv pip install")
_state.append_line(f"[pyMC updater] Target: {install_spec}")
cmd = [
+7 -7
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# Build development .deb package for pyMC_Repeater
# Build development .deb package for openhop-repeater
# Allows building from untagged commits with ~dev version suffix
set -euo pipefail
@@ -31,7 +31,7 @@ log_step() {
cd "$(dirname "$0")/.."
PROJECT_ROOT=$(pwd)
log_info "Building development .deb package for pyMC_Repeater..."
log_info "Building development .deb package for openhop-repeater..."
log_info "Project root: $PROJECT_ROOT"
# Check if we're in a git repository
@@ -69,7 +69,7 @@ log_info "Debian version: $DEBIAN_VERSION"
log_step "Updating debian/changelog..."
CHANGELOG_DATE=$(date -R)
cat > debian/changelog << EOF
pymc-repeater ($DEBIAN_VERSION) unstable; urgency=medium
openhop-repeater ($DEBIAN_VERSION) unstable; urgency=medium
* Development build from git commit $(git rev-parse --short HEAD)
* Version: $VERSION
@@ -81,11 +81,11 @@ log_info "Changelog updated with version $DEBIAN_VERSION"
# Clean previous builds
log_step "Cleaning previous builds..."
rm -rf debian/pymc-repeater/
rm -rf debian/openhop-repeater/
rm -rf debian/.debhelper/
rm -rf debian/files
rm -f debian/pymc-repeater.*.debhelper
rm -f debian/pymc-repeater.substvars
rm -f debian/openhop-repeater.*.debhelper
rm -f debian/openhop-repeater.substvars
rm -f debian/*.log
rm -rf .pybuild/
rm -rf build/
@@ -108,7 +108,7 @@ else
fi
# Find and display the built package
DEB_FILE=$(find .. -maxdepth 1 -name "pymc-repeater_${DEBIAN_VERSION}_*.deb" -type f | head -n 1)
DEB_FILE=$(find .. -maxdepth 1 -name "openhop-repeater_${DEBIAN_VERSION}_*.deb" -type f | head -n 1)
if [ -n "$DEB_FILE" ]; then
log_info ""
+7 -7
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# Build production .deb package for pyMC_Repeater
# Build production .deb package for openhop-repeater
# Requires a clean git tag - fails if not on a tagged commit
set -euo pipefail
@@ -31,7 +31,7 @@ log_step() {
cd "$(dirname "$0")/.."
PROJECT_ROOT=$(pwd)
log_info "Building production .deb package for pyMC_Repeater..."
log_info "Building production .deb package for openhop-repeater..."
log_info "Project root: $PROJECT_ROOT"
# Check if we're in a git repository
@@ -106,7 +106,7 @@ log_info "Debian version: $DEBIAN_VERSION"
log_step "Updating debian/changelog..."
CHANGELOG_DATE=$(date -R)
cat > debian/changelog << EOF
pymc-repeater ($DEBIAN_VERSION) stable; urgency=medium
openhop-repeater ($DEBIAN_VERSION) stable; urgency=medium
* Production release $VERSION
* Git tag: $TAG
@@ -119,11 +119,11 @@ log_info "Changelog updated with version $DEBIAN_VERSION"
# Clean previous builds
log_step "Cleaning previous builds..."
rm -rf debian/pymc-repeater/
rm -rf debian/openhop-repeater/
rm -rf debian/.debhelper/
rm -rf debian/files
rm -f debian/pymc-repeater.*.debhelper
rm -f debian/pymc-repeater.substvars
rm -f debian/openhop-repeater.*.debhelper
rm -f debian/openhop-repeater.substvars
rm -f debian/*.log
rm -rf .pybuild/
rm -rf build/
@@ -146,7 +146,7 @@ else
fi
# Find and display the built package
DEB_FILE=$(find .. -maxdepth 1 -name "pymc-repeater_${DEBIAN_VERSION}_*.deb" -type f | head -n 1)
DEB_FILE=$(find .. -maxdepth 1 -name "openhop-repeater_${DEBIAN_VERSION}_*.deb" -type f | head -n 1)
if [ -n "$DEB_FILE" ]; then
# Run lintian to check package quality
+27 -27
View File
@@ -1,24 +1,24 @@
#!/usr/bin/env bash
# pyMC Repeater - Proxmox LXC Installer
# Creates an LXC container with USB passthrough and installs pyMC Repeater
# openHop Repeater - Proxmox LXC Installer
# Creates an LXC container with USB passthrough and installs openHop Repeater
#
# Usage (run on the Proxmox host):
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/pyMC-dev/pyMC_Repeater/main/scripts/proxmox-install.sh)"
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/pyMC-dev/openhop-repeater/main/scripts/proxmox-install.sh)"
#
# License: MIT
# Source: https://github.com/pyMC-dev/pyMC_Repeater
# Source: https://github.com/pyMC-dev/openhop-repeater
set -euo pipefail
# ── Defaults ───────────────────────────────────────────────────────────────
REPO="https://github.com/pyMC-dev/pyMC_Repeater.git"
REPO="https://github.com/pyMC-dev/openhop-repeater.git"
BRANCH="dev"
CT_TEMPLATE="debian-12-standard"
CT_RAM=1024
CT_SWAP=512
CT_DISK=4
CT_CORES=2
CT_HOSTNAME="pymc-repeater"
CT_HOSTNAME="openhop-repeater"
CT_BRIDGE="vmbr0"
CT_STORAGE="local-lvm"
CT_TEMPLATE_STORAGE="local"
@@ -37,7 +37,7 @@ header() {
clear
echo -e "${BLD}"
echo "═══════════════════════════════════════════════════════════════"
echo " pyMC Repeater - Proxmox LXC Installer"
echo " openHop Repeater - Proxmox LXC Installer"
echo "═══════════════════════════════════════════════════════════════"
echo -e "${CL}"
}
@@ -154,7 +154,7 @@ msg_ok "Container created"
msg_info "Configuring USB passthrough..."
cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF'
# CH341 USB passthrough for pyMC Repeater
# CH341 USB passthrough for openHop 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
@@ -209,41 +209,41 @@ 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/pyMC-dev/pyMC_Repeater\"
echo \" openHop Repeater LXC Container\"
echo \" 🌐 GitHub: https://github.com/pyMC-dev/openhop-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 \" Management: cd /opt/openhop_repeater && bash manage.sh\"
echo \"\"
MOTD
chmod +x /etc/profile.d/pymc-motd.sh
"
msg_ok "curl/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_info "Cloning openhop-repeater (branch: ${BRANCH})..."
pct exec "$CTID" -- bash -c "git clone --branch ${BRANCH} ${REPO} /root/openhop-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
mkdir -p /etc/openhop_repeater
if [ -f /root/openhop-repeater/config.yaml.example ]; then
cp /root/openhop-repeater/config.yaml.example /etc/openhop_repeater/config.yaml
# Set radio type to CH341
sed -i 's/^radio_type: sx1262$/radio_type: sx1262_ch341/' /etc/pymc_repeater/config.yaml
sed -i 's/^radio_type: sx1262$/radio_type: sx1262_ch341/' /etc/openhop_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
sed -i 's/cs_pin: 21/cs_pin: 0/' /etc/openhop_repeater/config.yaml
sed -i 's/reset_pin: 18/reset_pin: 2/' /etc/openhop_repeater/config.yaml
sed -i 's/busy_pin: 20/busy_pin: 4/' /etc/openhop_repeater/config.yaml
sed -i 's/irq_pin: 16/irq_pin: 6/' /etc/openhop_repeater/config.yaml
sed -i 's/rxen_pin: -1/rxen_pin: 1/' /etc/openhop_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
sed -i 's/use_dio3_tcxo: false/use_dio3_tcxo: true/' /etc/openhop_repeater/config.yaml
sed -i 's/use_dio2_rf: false/use_dio2_rf: true/' /etc/openhop_repeater/config.yaml
fi
"
@@ -251,7 +251,7 @@ pct exec "$CTID" -- bash -c "
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"
lxc-attach -n "$CTID" -- bash -c "cd /root/openhop-repeater && TERM=xterm bash manage.sh install"
echo ""
msg_ok "manage.sh install completed"
@@ -263,7 +263,7 @@ CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')
echo ""
echo -e "${BLD}"
echo "═══════════════════════════════════════════════════════════════"
echo " ✓ pyMC Repeater Installation Complete!"
echo " ✓ openHop Repeater Installation Complete!"
echo "═══════════════════════════════════════════════════════════════"
echo -e "${CL}"
echo -e " Container: ${GN}${CTID}${CL} (${CT_HOSTNAME})"
@@ -271,6 +271,6 @@ 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 " Management: pct enter ${CTID}, then: cd /opt/openhop_repeater && bash manage.sh"
echo ""
echo "═══════════════════════════════════════════════════════════════"
+2 -2
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# Setup Debian/Ubuntu build environment for pyMC_Repeater
# Setup Debian/Ubuntu build environment for openhop-repeater
# This script installs all required build dependencies using apt
set -euo pipefail
@@ -28,7 +28,7 @@ if [ "$EUID" -ne 0 ]; then
exit 1
fi
log_info "Setting up build environment for pyMC_Repeater..."
log_info "Setting up build environment for openhop-repeater..."
# Update package list
log_info "Updating package lists..."
+4 -4
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# Radio configuration setup script for pyMC Repeater
# Radio configuration setup script for openHop Repeater
CONFIG_DIR="${1:-.}"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
@@ -15,7 +15,7 @@ else
SED_OPTS=(-i)
fi
echo "=== pyMC Repeater Radio Configuration ==="
echo "=== openHop Repeater Radio Configuration ==="
echo ""
# Step 0: Repeater Name
@@ -48,7 +48,7 @@ echo ""
echo "=== Step 0.5: Select Radio Type ==="
echo ""
echo " 1) SX1262 hardware (SPI LoRa module - Raspberry Pi HAT, etc.)"
echo " 2) KISS modem (serial TNC - requires pyMC_core with KISS support)"
echo " 2) KISS modem (serial TNC - requires openhop-core with KISS support)"
echo ""
read -p "Select radio type (1 or 2): " radio_type_sel
@@ -435,7 +435,7 @@ if [ "$RADIO_TYPE" = "sx1262" ] && [ -n "$bus_id" ]; then
fi
# Enable and start the service
SERVICE_NAME="pymc-repeater"
SERVICE_NAME="openhop-repeater"
if systemctl list-unit-files | grep -q "^$SERVICE_NAME\.service"; then
echo ""
echo "Enabling and starting the $SERVICE_NAME service..."
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Setup script for pymc_repeater
Setup script for openhop_repeater
Version is managed by setuptools_scm from git tags
"""
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pymc_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
from openhop_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
from repeater.companion.bridge import RepeaterCompanionBridge, _to_json_safe
from repeater.companion.frame_server import CompanionFrameServer
+1 -1
View File
@@ -2,7 +2,7 @@
import pytest
from pymc_core import LocalIdentity
from openhop_core import LocalIdentity
from repeater.companion.bridge import RepeaterCompanionBridge, _prefs_bytes_from_json
+1 -1
View File
@@ -22,7 +22,7 @@ from repeater.companion.utils import (
validate_companion_config_capacity,
)
# pymc_core defaults (CompanionBridge / ContactStore)
# openhop_core defaults (CompanionBridge / ContactStore)
_DEFAULT_MAX_CONTACTS = 1000
+4 -4
View File
@@ -1,5 +1,5 @@
"""
tests for pyMC_Repeater engine.py RepeaterHandler.
tests for openhop-repeater engine.py RepeaterHandler.
Covers: flood_forward, direct_forward, process_packet, duplicate detection,
mark_seen, validate_packet, packet scoring, TX delay, cache management,
@@ -12,8 +12,8 @@ import time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pymc_core.protocol import Packet, PacketBuilder
from pymc_core.protocol.constants import (
from openhop_core.protocol import Packet, PacketBuilder
from openhop_core.protocol.constants import (
MAX_PATH_SIZE,
PH_ROUTE_MASK,
PH_TYPE_SHIFT,
@@ -1754,7 +1754,7 @@ class TestMissedEngineBranches:
pkt = _make_transport_flood_packet(payload=b"\x01\x02", path=b"")
pkt.transport_codes = [0xCAFE, 0xBEEF]
with patch("pymc_core.protocol.transport_keys.calc_transport_code", return_value=0xCAFE):
with patch("openhop_core.protocol.transport_keys.calc_transport_code", return_value=0xCAFE):
allowed_1, reason_1 = handler._check_transport_codes(pkt)
allowed_2, reason_2 = handler._check_transport_codes(pkt)
+3 -3
View File
@@ -1,7 +1,7 @@
"""
Tests for flood packet loop detection and duplicate suppression.
Exercises the real RepeaterHandler engine with real pymc_core Packet/PathUtils
Exercises the real RepeaterHandler engine with real openhop_core Packet/PathUtils
objects to verify:
- Duplicate packet suppression via calculate_packet_hash (SHA256-based)
- Loop detection modes (off, minimal, moderate, strict) with real path bytes
@@ -15,8 +15,8 @@ objects to verify:
from unittest.mock import MagicMock, patch
from pymc_core.protocol import Packet, PathUtils
from pymc_core.protocol.constants import (
from openhop_core.protocol import Packet, PathUtils
from openhop_core.protocol.constants import (
ROUTE_TYPE_FLOOD,
ROUTE_TYPE_TRANSPORT_FLOOD,
)
@@ -56,7 +56,7 @@ async def test_path_helper_updates_client_out_path_on_valid_decrypt():
packet = _PathPacket(payload=b"\x11\x22\xaa\xbb\xcc")
with patch(
"pymc_core.protocol.crypto.CryptoUtils.mac_then_decrypt", return_value=b"\x02\x99\x88\x01"
"openhop_core.protocol.crypto.CryptoUtils.mac_then_decrypt", return_value=b"\x02\x99\x88\x01"
):
handled = await helper.process_path_packet(packet)
@@ -82,7 +82,7 @@ async def test_path_helper_returns_false_for_non_matching_or_invalid_inputs():
is False
)
with patch("pymc_core.protocol.crypto.CryptoUtils.mac_then_decrypt", return_value=None):
with patch("openhop_core.protocol.crypto.CryptoUtils.mac_then_decrypt", return_value=None):
assert await helper.process_path_packet(_PathPacket(payload=b"\x11\x22\xaa\xbb")) is False
@@ -356,7 +356,7 @@ async def test_text_helper_send_cli_reply_uses_direct_path_from_client():
reply_packet = SimpleNamespace(path=bytearray(), path_len=0)
with (
patch("pymc_core.protocol.PacketBuilder.create_datagram", return_value=reply_packet),
patch("openhop_core.protocol.PacketBuilder.create_datagram", return_value=reply_packet),
patch("repeater.handler_helpers.text.asyncio.sleep", new_callable=AsyncMock),
):
await helper._send_cli_reply(
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pymc_core.protocol.constants import PAYLOAD_TYPE_ANON_REQ, ROUTE_TYPE_DIRECT
from openhop_core.protocol.constants import PAYLOAD_TYPE_ANON_REQ, ROUTE_TYPE_DIRECT
from repeater.handler_helpers.discovery import DiscoveryHelper
from repeater.handler_helpers.login import LoginHelper
from repeater.handler_helpers.trace import TraceHelper
@@ -258,7 +258,7 @@ def test_discovery_send_response_without_injector_is_safe():
helper = DiscoveryHelper(local_identity=FakeIdentity(0x42), packet_injector=None)
with patch(
"pymc_core.protocol.packet_builder.PacketBuilder.create_discovery_response",
"openhop_core.protocol.packet_builder.PacketBuilder.create_discovery_response",
return_value=object(),
):
helper._send_discovery_response(tag=5, node_type=2, inbound_snr=1.0, prefix_only=False)
+1 -1
View File
@@ -241,7 +241,7 @@ async def test_send_advert_branches_and_success_path():
)
packet = SimpleNamespace(calculate_packet_hash=lambda: b"\xab" * 16)
with patch("pymc_core.protocol.PacketBuilder.create_advert", return_value=packet):
with patch("openhop_core.protocol.PacketBuilder.create_advert", return_value=packet):
ok = await daemon.send_advert()
assert ok is True
+1 -1
View File
@@ -46,7 +46,7 @@ async def test_load_additional_identities_valid_and_invalid_entries():
daemon.identity_manager = SimpleNamespace(list_identities=lambda: [1, 2])
daemon._register_identity_everywhere = MagicMock(return_value=True)
with patch("pymc_core.LocalIdentity", _FakeLocalIdentity):
with patch("openhop_core.LocalIdentity", _FakeLocalIdentity):
await daemon._load_additional_identities()
# Only valid entries should be registered (including 64-byte firmware keys).
+1 -1
View File
@@ -32,7 +32,7 @@ class _FakeIdentity:
def _make_config(format_value: str = "letsmesh", iata_code: str = "LAX") -> dict:
"""Minimal pyMC_Repeater config sufficient to construct MeshCoreToMqttPusher."""
"""Minimal openhop-repeater config sufficient to construct MeshCoreToMqttPusher."""
return {
"repeater": {"node_name": "test-node"},
"radio": {
+12 -12
View File
@@ -19,18 +19,18 @@ import time
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from pymc_core.node.handlers.ack import AckHandler
from pymc_core.node.handlers.advert import AdvertHandler
from pymc_core.node.handlers.control import ControlHandler
from pymc_core.node.handlers.group_text import GroupTextHandler
from pymc_core.node.handlers.login_response import LoginResponseHandler
from pymc_core.node.handlers.login_server import LoginServerHandler
from pymc_core.node.handlers.path import PathHandler
from pymc_core.node.handlers.protocol_request import ProtocolRequestHandler
from pymc_core.node.handlers.protocol_response import ProtocolResponseHandler
from pymc_core.node.handlers.text import TextMessageHandler
from pymc_core.node.handlers.trace import TraceHandler
from pymc_core.protocol.constants import ROUTE_TYPE_DIRECT
from openhop_core.node.handlers.ack import AckHandler
from openhop_core.node.handlers.advert import AdvertHandler
from openhop_core.node.handlers.control import ControlHandler
from openhop_core.node.handlers.group_text import GroupTextHandler
from openhop_core.node.handlers.login_response import LoginResponseHandler
from openhop_core.node.handlers.login_server import LoginServerHandler
from openhop_core.node.handlers.path import PathHandler
from openhop_core.node.handlers.protocol_request import ProtocolRequestHandler
from openhop_core.node.handlers.protocol_response import ProtocolResponseHandler
from openhop_core.node.handlers.text import TextMessageHandler
from openhop_core.node.handlers.trace import TraceHandler
from openhop_core.protocol.constants import ROUTE_TYPE_DIRECT
from repeater.packet_router import (
PacketRouter,
+3 -3
View File
@@ -8,9 +8,9 @@ returns 2 or 3. The dispatcher applies this in send_packet() before transmit.
import pytest
from pymc_core.node.dispatcher import Dispatcher
from pymc_core.protocol import Packet
from pymc_core.protocol.constants import (
from openhop_core.node.dispatcher import Dispatcher
from openhop_core.protocol import Packet
from openhop_core.protocol.constants import (
PAYLOAD_TYPE_ADVERT,
PH_TYPE_SHIFT,
ROUTE_TYPE_FLOOD,
+6 -6
View File
@@ -1,5 +1,5 @@
"""
Integration tests for multi-byte path hash support using real pymc_core protocol objects.
Integration tests for multi-byte path hash support using real openhop_core protocol objects.
Exercises actual Packet, PathUtils, PacketBuilder, and engine forwarding
rather than mocking the protocol layer. Covers:
@@ -16,9 +16,9 @@ import struct
from unittest.mock import MagicMock, patch
import pytest
from pymc_core.node.handlers.trace import TraceHandler
from pymc_core.protocol import Packet, PacketBuilder, PathUtils
from pymc_core.protocol.constants import (
from openhop_core.node.handlers.trace import TraceHandler
from openhop_core.protocol import Packet, PacketBuilder, PathUtils
from openhop_core.protocol.constants import (
MAX_PATH_SIZE,
PATH_HASH_COUNT_MASK,
PATH_HASH_SIZE_SHIFT,
@@ -730,7 +730,7 @@ class TestTraceHelperMultibyte:
"""TraceHelper._should_forward_trace with 2-byte TRACE payload hashes."""
def test_should_forward_when_next_hop_matches_pubkey_prefix(self):
from pymc_core.protocol import LocalIdentity
from openhop_core.protocol import LocalIdentity
from repeater.handler_helpers.trace import TraceHelper
@@ -752,7 +752,7 @@ class TestTraceHelperMultibyte:
assert th._should_forward_trace(pkt, trace_bytes, flags, hash_width)
def test_should_not_forward_when_next_hop_mismatch(self):
from pymc_core.protocol import LocalIdentity
from openhop_core.protocol import LocalIdentity
from repeater.handler_helpers.trace import TraceHelper
+3 -3
View File
@@ -1,9 +1,9 @@
from unittest.mock import patch
import yaml
from pymc_core.protocol.constants import PAYLOAD_TYPE_GRP_TXT
from pymc_core.protocol.identity import LocalIdentity
from pymc_core.protocol.packet_builder import PacketBuilder
from openhop_core.protocol.constants import PAYLOAD_TYPE_GRP_TXT
from openhop_core.protocol.identity import LocalIdentity
from openhop_core.protocol.packet_builder import PacketBuilder
from repeater.policy_engine import PolicyEngine
+1 -1
View File
@@ -1,6 +1,6 @@
"""Regression tests guarding against Python 3.10 compatibility breakage.
pyMC Repeater supports Python 3.10+ (LuckFox Pico Ultra ships with 3.10).
openHop Repeater supports Python 3.10+ (LuckFox Pico Ultra ships with 3.10).
These tests scan the source tree statically so regressions are caught in CI
without needing a 3.10 interpreter in the test environment.
"""
+11 -11
View File
@@ -20,7 +20,7 @@ def test_get_radio_for_board_passes_en_pins(monkeypatch):
return _DummyRadio()
monkeypatch.setattr(
"pymc_core.hardware.sx1262_wrapper.SX1262Radio",
"openhop_core.hardware.sx1262_wrapper.SX1262Radio",
_DummySX1262Radio,
)
@@ -81,7 +81,7 @@ def _pymc_radio_cfg():
def test_get_radio_for_board_pymc_tcp(monkeypatch):
pytest.importorskip("pymc_core.hardware.tcp_radio")
pytest.importorskip("openhop_core.hardware.tcp_radio")
captured = {}
class _DummyTCPLoRaRadio(_DummyRadio):
@@ -89,7 +89,7 @@ def test_get_radio_for_board_pymc_tcp(monkeypatch):
captured.update(kwargs)
monkeypatch.setattr(
"pymc_core.hardware.tcp_radio.TCPLoRaRadio",
"openhop_core.hardware.tcp_radio.TCPLoRaRadio",
_DummyTCPLoRaRadio,
)
@@ -119,10 +119,10 @@ def test_get_radio_for_board_pymc_tcp(monkeypatch):
def test_get_radio_for_board_pymc_tcp_requires_host(monkeypatch):
pytest.importorskip("pymc_core.hardware.tcp_radio")
pytest.importorskip("openhop_core.hardware.tcp_radio")
monkeypatch.setattr(
"pymc_core.hardware.tcp_radio.TCPLoRaRadio",
"openhop_core.hardware.tcp_radio.TCPLoRaRadio",
lambda **kwargs: _DummyRadio(),
)
@@ -137,7 +137,7 @@ def test_get_radio_for_board_pymc_tcp_requires_host(monkeypatch):
def test_get_radio_for_board_pymc_usb(monkeypatch):
pytest.importorskip("pymc_core.hardware.usb_radio")
pytest.importorskip("openhop_core.hardware.usb_radio")
captured = {}
class _DummyUSBLoRaRadio(_DummyRadio):
@@ -145,7 +145,7 @@ def test_get_radio_for_board_pymc_usb(monkeypatch):
captured.update(kwargs)
monkeypatch.setattr(
"pymc_core.hardware.usb_radio.USBLoRaRadio",
"openhop_core.hardware.usb_radio.USBLoRaRadio",
_DummyUSBLoRaRadio,
)
@@ -170,10 +170,10 @@ def test_get_radio_for_board_pymc_usb(monkeypatch):
def test_get_radio_for_board_pymc_usb_requires_port(monkeypatch):
pytest.importorskip("pymc_core.hardware.usb_radio")
pytest.importorskip("openhop_core.hardware.usb_radio")
monkeypatch.setattr(
"pymc_core.hardware.usb_radio.USBLoRaRadio",
"openhop_core.hardware.usb_radio.USBLoRaRadio",
lambda **kwargs: _DummyRadio(),
)
@@ -195,7 +195,7 @@ def test_get_radio_for_board_pymc_usb_requires_port(monkeypatch):
def _kiss_capture_radio_config(monkeypatch):
"""Patch KissModemWrapper to capture the radio_config it is built with."""
pytest.importorskip("pymc_core.hardware.kiss_modem_wrapper")
pytest.importorskip("openhop_core.hardware.kiss_modem_wrapper")
captured = {}
class _DummyKissWrapper(_DummyRadio):
@@ -203,7 +203,7 @@ def _kiss_capture_radio_config(monkeypatch):
captured["kwargs"] = kwargs
monkeypatch.setattr(
"pymc_core.hardware.kiss_modem_wrapper.KissModemWrapper",
"openhop_core.hardware.kiss_modem_wrapper.KissModemWrapper",
_DummyKissWrapper,
)
return captured
+6 -6
View File
@@ -68,7 +68,7 @@ def test_generate_transport_key_uses_implicit_hashtag_region(tmp_path, monkeypat
h = _make_handler(tmp_path)
captured = {}
fake_transport_keys = types.ModuleType("pymc_core.protocol.transport_keys")
fake_transport_keys = types.ModuleType("openhop_core.protocol.transport_keys")
def _fake_get_auto_key_for(name: str) -> bytes:
captured["name"] = name
@@ -76,15 +76,15 @@ def test_generate_transport_key_uses_implicit_hashtag_region(tmp_path, monkeypat
fake_transport_keys.get_auto_key_for = _fake_get_auto_key_for
fake_protocol = types.ModuleType("pymc_core.protocol")
fake_protocol = types.ModuleType("openhop_core.protocol")
fake_protocol.transport_keys = fake_transport_keys
fake_core = types.ModuleType("pymc_core")
fake_core = types.ModuleType("openhop_core")
fake_core.protocol = fake_protocol
monkeypatch.setitem(sys.modules, "pymc_core", fake_core)
monkeypatch.setitem(sys.modules, "pymc_core.protocol", fake_protocol)
monkeypatch.setitem(sys.modules, "pymc_core.protocol.transport_keys", fake_transport_keys)
monkeypatch.setitem(sys.modules, "openhop_core", fake_core)
monkeypatch.setitem(sys.modules, "openhop_core.protocol", fake_protocol)
monkeypatch.setitem(sys.modules, "openhop_core.protocol.transport_keys", fake_transport_keys)
generated = h.generate_transport_key("eu")
generated_bytes = base64.b64decode(generated)
@@ -29,7 +29,7 @@ def _make_collector() -> StorageCollector:
patch("repeater.data_acquisition.storage_collector.RRDToolHandler"),
patch("repeater.data_acquisition.hardware_stats.HardwareStatsCollector"),
):
collector = StorageCollector(config={"storage": {"storage_dir": "/tmp/pymc_repeater_test"}})
collector = StorageCollector(config={"storage": {"storage_dir": "/tmp/openhop_repeater_test"}})
# Stop any real stats-broadcast thread started during construction so the tests
# drive the loop deterministically.
+1 -1
View File
@@ -33,7 +33,7 @@ def _fake_thread(*args, **kwargs):
def test_jwt_warning_fix_guard():
# Guard test file import path and ensure this module executes in suite.
assert ue.PACKAGE_NAME == "pymc_repeater"
assert ue.PACKAGE_NAME == "openhop_repeater"
def test_has_update_paths():