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