mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-12 03:43:04 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a53df3d76f | |||
| 6de2b23266 | |||
| 864030283c | |||
| 751c9e3807 | |||
| fbbc7e6844 | |||
| 4d095dad42 | |||
| fcaad1dacf | |||
| e2ae7e18a6 | |||
| 11f8ea2f43 | |||
| d24a346cf7 | |||
| af6bea5bbe | |||
| 9846666076 | |||
| 42340f683f | |||
| 27e68167d5 | |||
| f0408d263d | |||
| 135d6b1d8d | |||
| 9748058010 | |||
| cc0dc2ffd6 |
+4
-4
@@ -3,7 +3,7 @@
|
||||
|
||||
# Published image to run. Use a different repository or tag if you are testing
|
||||
# a fork or a specific release.
|
||||
PYMC_REPEATER_IMAGE=pymcdev/openhop-repeater:main
|
||||
OPENHOP_REPEATER_IMAGE=openhop/openhop-repeater:main
|
||||
|
||||
# Storage defaults to Docker named volumes. This is the safest option for
|
||||
# Portainer and fresh installs because Docker preserves the image ownership.
|
||||
@@ -12,8 +12,8 @@ PYMC_REPEATER_IMAGE=pymcdev/openhop-repeater:main
|
||||
# sudo mkdir -p /opt/openhop-repeater/config /opt/openhop-repeater/data
|
||||
# sudo chown -R 15888:15888 /opt/openhop-repeater/config /opt/openhop-repeater/data
|
||||
# Then uncomment and adjust these paths:
|
||||
# PYMC_CONFIG_VOLUME=/opt/openhop-repeater/config
|
||||
# PYMC_DATA_VOLUME=/opt/openhop-repeater/data
|
||||
# OPENHOP_CONFIG_VOLUME=/opt/openhop-repeater/config
|
||||
# OPENHOP_DATA_VOLUME=/opt/openhop-repeater/data
|
||||
|
||||
# Serial/SPI/GPIO access uses the host's numeric group IDs. Check your host with:
|
||||
# getent group dialout
|
||||
@@ -29,6 +29,6 @@ GPIO_GID=986
|
||||
SPI_GID=989
|
||||
|
||||
# Local build only. These are used by docker-compose.build.yml if you build the
|
||||
# image yourself instead of pulling PYMC_REPEATER_IMAGE.
|
||||
# image yourself instead of pulling OPENHOP_REPEATER_IMAGE.
|
||||
PUID=15888
|
||||
PGID=15888
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
image_repository:
|
||||
description: "Docker image repository to publish to"
|
||||
required: false
|
||||
default: "pymcdev/openhop-repeater"
|
||||
default: "openhop/openhop-repeater"
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
elif [ "${{ github.repository }}" = "yellowcooln/openhop_repeater" ]; then
|
||||
image_repository="yellowcooln/openhop-repeater"
|
||||
else
|
||||
image_repository="pymcdev/openhop-repeater"
|
||||
image_repository="openhop/openhop-repeater"
|
||||
fi
|
||||
|
||||
echo "Using image repository: ${image_repository}"
|
||||
@@ -107,5 +107,5 @@ jobs:
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
|
||||
https://api.github.com/repos/pyMC-dev/pyMC-HA-Add-on/dispatches \
|
||||
https://api.github.com/repos/openhop-dev/openHop-HA-Add-on/dispatches \
|
||||
-d "{\"event_type\":\"sync-upstream-channel\",\"client_payload\":{\"channel\":\"${CHANNEL}\",\"revision\":\"${REVISION}\"}}"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Lightweight Python MeshCore repeater daemon built on `openhop_core`.
|
||||
|
||||
Formerly `pyMC_Repeater` built on `pyMC_core`.
|
||||
|
||||
openHop Repeater is designed to run continuously on low-power Linux hardware such
|
||||
as Raspberry Pi-class devices, Proxmox LXC containers, and network-attached
|
||||
radio modems. It forwards LoRa packets, exposes a web dashboard, and provides
|
||||
@@ -111,8 +113,8 @@ sudo apt install git -y
|
||||
### Clone The Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/openhop-dev/openhop-repeater.git
|
||||
cd openhop-repeater
|
||||
git clone https://github.com/openhop-dev/openhop_repeater.git
|
||||
cd openhop_repeater
|
||||
```
|
||||
|
||||
### Quick Install
|
||||
@@ -258,7 +260,7 @@ The web interface can upgrade an installation or switch branches.
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
cd openhop-repeater
|
||||
cd openhop_repeater
|
||||
sudo bash ./manage.sh upgrade
|
||||
```
|
||||
|
||||
@@ -295,7 +297,7 @@ Hardware, choose one:
|
||||
Run this command on the Proxmox host, not inside a container:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/pyMC-dev/openhop-repeater/main/scripts/proxmox-install.sh)"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/openhop-dev/openhop_repeater/main/scripts/proxmox-install.sh)"
|
||||
```
|
||||
|
||||
Replace `main` in the URL with another branch name if needed.
|
||||
@@ -416,7 +418,7 @@ that source as a directory, which breaks startup.
|
||||
### Setup
|
||||
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Review `.env` and update `PYMC_REPEATER_IMAGE`, `DIALOUT_GID`,
|
||||
2. Review `.env` and update `OPENHOP_REPEATER_IMAGE`, `DIALOUT_GID`,
|
||||
`GPIO_GID`, or `SPI_GID` if needed.
|
||||
3. Configure `docker-compose.yml` for your hardware and device paths.
|
||||
4. Uncomment the USB device mapping only if your host has that device path.
|
||||
@@ -431,7 +433,7 @@ docker compose up -d
|
||||
```yaml
|
||||
services:
|
||||
openhop-repeater:
|
||||
image: ${PYMC_REPEATER_IMAGE:-pymcdev/openhop-repeater:main}
|
||||
image: ${OPENHOP_REPEATER_IMAGE:-${PYMC_REPEATER_IMAGE:-openhop/openhop-repeater:main}}
|
||||
container_name: openhop-repeater
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -455,8 +457,8 @@ services:
|
||||
- plugdev
|
||||
|
||||
volumes:
|
||||
- ${PYMC_CONFIG_VOLUME:-openhop-repeater-config}:/etc/openhop_repeater
|
||||
- ${PYMC_DATA_VOLUME:-openhop-repeater-data}:/var/lib/openhop_repeater
|
||||
- ${OPENHOP_CONFIG_VOLUME:-${PYMC_CONFIG_VOLUME:-openhop-repeater-config}}:/etc/openhop_repeater
|
||||
- ${OPENHOP_DATA_VOLUME:-${PYMC_DATA_VOLUME:-openhop-repeater-data}}:/var/lib/openhop_repeater
|
||||
|
||||
volumes:
|
||||
openhop-repeater-config:
|
||||
@@ -518,7 +520,7 @@ Pre-commit hooks will automatically:
|
||||
|
||||
## Support
|
||||
|
||||
- [pyMC Core](https://github.com/openhop-dev/openhop-core)
|
||||
- [pyMC Core](https://github.com/openhop-dev/openhop_core)
|
||||
- [MeshCore Discord](https://meshcore.gg)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
|
||||
PIWHEELS_INDEX_URL="https://www.piwheels.org/simple"
|
||||
R2_ENABLED=1
|
||||
YQ_VERSION="${YQ_VERSION:-v4.44.3}"
|
||||
PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/rightup/openhop-core.git}"
|
||||
PYMC_CORE_REPO="${PYMC_CORE_REPO:-https://github.com/openhop-dev/openhop_core.git}"
|
||||
PYMC_CORE_REF="${PYMC_CORE_REF:-}"
|
||||
PYMC_CORE_LOCAL_DIR="${PYMC_CORE_LOCAL_DIR:-}"
|
||||
PYMC_SKIP_BUILDROOT_DEP_INSTALL="${PYMC_SKIP_BUILDROOT_DEP_INSTALL:-0}"
|
||||
@@ -1247,12 +1247,12 @@ prepare_git_version() {
|
||||
git -C "$SCRIPT_DIR" fetch --tags 2>/dev/null || true
|
||||
git_version=$(python3 -m setuptools_scm 2>/dev/null || echo "1.0.5")
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION="$git_version"
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYMC_REPEATER="$git_version"
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$git_version"
|
||||
info "Using version: $git_version"
|
||||
else
|
||||
git_version="1.0.5"
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION="$git_version"
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYMC_REPEATER="$git_version"
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER="$git_version"
|
||||
info "Using fallback version: $git_version"
|
||||
fi
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -15,7 +15,7 @@ Build-Depends: debhelper-compat (= 13),
|
||||
python3-psutil,
|
||||
git
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://github.com/rightup/openhop-repeater
|
||||
Homepage: https://github.com/openhop-dev/openhop_repeater
|
||||
X-Python3-Version: >= 3.9
|
||||
|
||||
Package: openhop-repeater
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
openhop-repeater:
|
||||
image: ${PYMC_REPEATER_IMAGE:-pymcdev/openhop-repeater:main}
|
||||
image: ${OPENHOP_REPEATER_IMAGE:-${PYMC_REPEATER_IMAGE:-openhop/openhop-repeater:main}}
|
||||
container_name: openhop-repeater
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -21,8 +21,8 @@ services:
|
||||
- "${SPI_GID:-989}"
|
||||
- plugdev
|
||||
volumes:
|
||||
- ${PYMC_CONFIG_VOLUME:-openhop-repeater-config}:/etc/openhop_repeater
|
||||
- ${PYMC_DATA_VOLUME:-openhop-repeater-data}:/var/lib/openhop_repeater
|
||||
- ${OPENHOP_CONFIG_VOLUME:-${PYMC_CONFIG_VOLUME:-openhop-repeater-config}}:/etc/openhop_repeater
|
||||
- ${OPENHOP_DATA_VOLUME:-${PYMC_DATA_VOLUME:-openhop-repeater-data}}:/var/lib/openhop_repeater
|
||||
|
||||
volumes:
|
||||
openhop-repeater-config:
|
||||
|
||||
@@ -3,7 +3,7 @@ set -eu
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/openhop_repeater}"
|
||||
CONFIG_DIR="${CONFIG_DIR:-/etc/openhop_repeater}"
|
||||
CONFIG_PATH="${PYMC_REPEATER_CONFIG:-${CONFIG_DIR}/config.yaml}"
|
||||
CONFIG_PATH="${OPENHOP_REPEATER_CONFIG:-${PYMC_REPEATER_CONFIG:-${CONFIG_DIR}/config.yaml}}"
|
||||
EXAMPLE_PATH="${CONFIG_DIR}/config.yaml.example"
|
||||
BUNDLED_EXAMPLE_PATH="${INSTALL_DIR}/config.yaml.example"
|
||||
RUNTIME_USER="${USER:-repeater}"
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ ENV INSTALL_DIR=/opt/openhop_repeater \
|
||||
HOME_DIR=/home/${USER} \
|
||||
PATH=/home/${USER}/.local/bin:${PATH} \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYMC_REPEATER=${PACKAGE_VERSION} \
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENHOP_REPEATER=${PACKAGE_VERSION} \
|
||||
PUID=${PUID} \
|
||||
PGID=${PGID} \
|
||||
DIALOUT_GID=${DIALOUT_GID} \
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
|
||||
# Published image to run. Use a different repository or tag if you are testing
|
||||
# a fork or a specific release.
|
||||
PYMC_REPEATER_IMAGE=pymcdev/openhop-repeater:main
|
||||
OPENHOP_REPEATER_IMAGE=openhop/openhop-repeater:main
|
||||
|
||||
# Storage defaults to Docker named volumes. This is the safest option for
|
||||
# Portainer and fresh installs because Docker preserves the image ownership.
|
||||
@@ -12,8 +12,8 @@ PYMC_REPEATER_IMAGE=pymcdev/openhop-repeater:main
|
||||
# sudo mkdir -p /opt/openhop-repeater/config /opt/openhop-repeater/data
|
||||
# sudo chown -R 15888:15888 /opt/openhop-repeater/config /opt/openhop-repeater/data
|
||||
# Then uncomment and adjust these paths:
|
||||
# PYMC_CONFIG_VOLUME=/opt/openhop-repeater/config
|
||||
# PYMC_DATA_VOLUME=/opt/openhop-repeater/data
|
||||
# OPENHOP_CONFIG_VOLUME=/opt/openhop-repeater/config
|
||||
# OPENHOP_DATA_VOLUME=/opt/openhop-repeater/data
|
||||
|
||||
# SPI/GPIO access uses the host's numeric group IDs. Check your host with:
|
||||
# getent group gpio
|
||||
@@ -26,6 +26,6 @@ GPIO_GID=986
|
||||
SPI_GID=989
|
||||
|
||||
# Local build only. These are used by docker-compose.build.yml if you build the
|
||||
# image yourself instead of pulling PYMC_REPEATER_IMAGE.
|
||||
# image yourself instead of pulling OPENHOP_REPEATER_IMAGE.
|
||||
PUID=15888
|
||||
PGID=15888
|
||||
|
||||
@@ -88,14 +88,51 @@ migrate_legacy_paths() {
|
||||
migrate_one_path "$LEGACY_PYMC_INSTALL_DIR" "$INSTALL_DIR" "install"
|
||||
}
|
||||
|
||||
ensure_service_user_home() {
|
||||
local current_home
|
||||
|
||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
current_home="$(getent passwd "$SERVICE_USER" | cut -d: -f6)"
|
||||
if [ -z "$current_home" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$current_home" != "$DATA_DIR" ]; then
|
||||
mkdir -p "$DATA_DIR" 2>/dev/null || true
|
||||
usermod -d "$DATA_DIR" "$SERVICE_USER" 2>/dev/null || true
|
||||
echo " ✓ Updated $SERVICE_USER home directory: $current_home -> $DATA_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create (or re-create) the dedicated venv for openhop_repeater
|
||||
ensure_venv() {
|
||||
local recreate=0
|
||||
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo ">>> Creating virtual environment at $VENV_DIR ..."
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
# Upgrade pip inside the venv
|
||||
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
recreate=1
|
||||
elif ! "$VENV_PYTHON" -c 'import sys; print(sys.executable)' >/dev/null 2>&1; then
|
||||
# Venv python exists but points to a missing interpreter (stale venv).
|
||||
recreate=1
|
||||
elif ! "$VENV_PYTHON" -m pip --version >/dev/null 2>&1; then
|
||||
# Pip script/shebang can break after Python upgrades; treat as stale.
|
||||
recreate=1
|
||||
fi
|
||||
|
||||
if [ "$recreate" -eq 1 ]; then
|
||||
if [ -d "$VENV_DIR" ]; then
|
||||
echo ">>> Rebuilding broken virtual environment at $VENV_DIR ..."
|
||||
rm -rf "$VENV_DIR"
|
||||
else
|
||||
echo ">>> Creating virtual environment at $VENV_DIR ..."
|
||||
fi
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Always use python -m pip so we don't rely on a potentially stale pip wrapper.
|
||||
"$VENV_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Migrate an existing system-pip install into the venv.
|
||||
@@ -105,6 +142,7 @@ migrate_to_venv() {
|
||||
|
||||
# 1. Ensure the venv exists
|
||||
ensure_venv
|
||||
ensure_service_user_home
|
||||
|
||||
# 2. Remove legacy PYTHONPATH from the service unit
|
||||
local svc_unit="/etc/systemd/system/openhop-repeater.service"
|
||||
@@ -424,6 +462,7 @@ install_repeater() {
|
||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||
useradd --system --home "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER"
|
||||
fi
|
||||
ensure_service_user_home
|
||||
|
||||
disable_legacy_services
|
||||
|
||||
@@ -567,7 +606,7 @@ fi
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
"$VENV_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
fi
|
||||
# ---- Migration: clean up legacy service unit issues ----
|
||||
SVC_UNIT=/etc/systemd/system/openhop-repeater.service
|
||||
@@ -615,15 +654,15 @@ if [ -n "$ARCH_TAG" ]; then
|
||||
PY_TAG=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
|
||||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||||
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_PYTHON" -m pip install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
|
||||
fi
|
||||
# ---- Install openhop_repeater from git ----
|
||||
if "$VENV_PIP" install \
|
||||
if "$VENV_PYTHON" -m pip install \
|
||||
--upgrade \
|
||||
--no-cache-dir \
|
||||
"openhop_repeater[hardware] @ git+https://github.com/rightup/openhop-repeater.git@${CHANNEL}"; then
|
||||
"openhop_repeater[hardware] @ git+https://github.com/openhop-dev/openhop_repeater.git@${CHANNEL}"; then
|
||||
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
|
||||
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/openhop-repeater/${CHANNEL}"
|
||||
RADIO_BASE_URL="https://raw.githubusercontent.com/openhop-dev/openhop_repeater/${CHANNEL}"
|
||||
RADIO_STORAGE_DIR="/var/lib/openhop_repeater"
|
||||
mkdir -p "$RADIO_STORAGE_DIR"
|
||||
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
|
||||
@@ -687,7 +726,7 @@ UPGRADEEOF
|
||||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||||
echo " Checking for R2 wheels (${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG})..."
|
||||
echo " Trying install from R2 pre-built 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 && R2_SUCCESS=1 || R2_SUCCESS=0
|
||||
"$VENV_PYTHON" -m 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 && R2_SUCCESS=1 || R2_SUCCESS=0
|
||||
if [ "$R2_SUCCESS" -eq 1 ]; then
|
||||
echo " ✓ R2 wheels installed"
|
||||
else
|
||||
@@ -696,7 +735,7 @@ UPGRADEEOF
|
||||
fi
|
||||
fi
|
||||
|
||||
if "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]; then
|
||||
if "$VENV_PYTHON" -m pip install --upgrade --no-cache-dir .[hardware]; then
|
||||
echo ""
|
||||
echo "✓ Python package installation completed successfully!"
|
||||
|
||||
@@ -868,6 +907,7 @@ upgrade_repeater() {
|
||||
|
||||
echo "[1.6/9] Ensuring required directories..."
|
||||
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
|
||||
ensure_service_user_home
|
||||
|
||||
echo "[2/9] Backing up configuration..."
|
||||
if [ -d "$CONFIG_DIR" ]; then
|
||||
@@ -1002,7 +1042,7 @@ fi
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
"$VENV_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
fi
|
||||
# ---- Migration: clean up legacy service unit issues ----
|
||||
SVC_UNIT=/etc/systemd/system/openhop-repeater.service
|
||||
@@ -1050,15 +1090,15 @@ python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||||
PY_TAG=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
|
||||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||||
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_PYTHON" -m pip install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
|
||||
fi
|
||||
# ---- Install openhop_repeater from git ----
|
||||
if "$VENV_PIP" install \
|
||||
if "$VENV_PYTHON" -m pip install \
|
||||
--upgrade \
|
||||
--no-cache-dir \
|
||||
"openhop_repeater[hardware] @ git+https://github.com/rightup/openhop-repeater.git@${CHANNEL}"; then
|
||||
"openhop_repeater[hardware] @ git+https://github.com/openhop-dev/openhop_repeater.git@${CHANNEL}"; then
|
||||
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
|
||||
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/openhop-repeater/${CHANNEL}"
|
||||
RADIO_BASE_URL="https://raw.githubusercontent.com/openhop-dev/openhop_repeater/${CHANNEL}"
|
||||
RADIO_STORAGE_DIR="/var/lib/openhop_repeater"
|
||||
mkdir -p "$RADIO_STORAGE_DIR"
|
||||
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
|
||||
@@ -1122,7 +1162,7 @@ UPGRADEEOF
|
||||
WHEEL_BASE="${R2_BASE_URL}/${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG}"
|
||||
echo " Checking for R2 wheels (${ARCH_TAG}/${PLATFORM_TAG}/${PY_TAG})..."
|
||||
echo " Trying install from R2 pre-built 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 && R2_SUCCESS=1 || R2_SUCCESS=0
|
||||
"$VENV_PYTHON" -m 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 && R2_SUCCESS=1 || R2_SUCCESS=0
|
||||
if [ "$R2_SUCCESS" -eq 1 ]; then
|
||||
echo " ✓ R2 wheels installed"
|
||||
else
|
||||
@@ -1131,7 +1171,7 @@ UPGRADEEOF
|
||||
fi
|
||||
fi
|
||||
|
||||
if "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]; then
|
||||
if "$VENV_PYTHON" -m pip install --upgrade --no-cache-dir .[hardware]; then
|
||||
echo ""
|
||||
echo "✓ Package and dependencies upgraded successfully!"
|
||||
else
|
||||
|
||||
+18
-8
@@ -195,7 +195,10 @@ def get_node_info(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
if config_path is None:
|
||||
config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/openhop_repeater/config.yaml")
|
||||
config_path = os.getenv(
|
||||
"OPENHOP_REPEATER_CONFIG",
|
||||
os.getenv("PYMC_REPEATER_CONFIG", "/etc/openhop_repeater/config.yaml"),
|
||||
)
|
||||
|
||||
# Check if config file exists
|
||||
if not Path(config_path).exists():
|
||||
@@ -301,10 +304,11 @@ def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
else:
|
||||
config["repeater"]["identity_key"] = _load_or_create_identity_key()
|
||||
|
||||
if os.getenv("PYMC_REPEATER_LOG_LEVEL"):
|
||||
env_log_level = os.getenv("OPENHOP_REPEATER_LOG_LEVEL", os.getenv("PYMC_REPEATER_LOG_LEVEL"))
|
||||
if env_log_level:
|
||||
if "logging" not in config:
|
||||
config["logging"] = {}
|
||||
config["logging"]["level"] = os.getenv("PYMC_REPEATER_LOG_LEVEL")
|
||||
config["logging"]["level"] = env_log_level
|
||||
|
||||
config = _load_policy_engine_config(config, config_path)
|
||||
|
||||
@@ -323,7 +327,10 @@ def save_config(config_data: Dict[str, Any], config_path: Optional[str] = None)
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if config_path is None:
|
||||
config_path = os.getenv("PYMC_REPEATER_CONFIG", "/etc/openhop_repeater/config.yaml")
|
||||
config_path = os.getenv(
|
||||
"OPENHOP_REPEATER_CONFIG",
|
||||
os.getenv("PYMC_REPEATER_CONFIG", "/etc/openhop_repeater/config.yaml"),
|
||||
)
|
||||
|
||||
try:
|
||||
# Create backup of existing config
|
||||
@@ -415,8 +422,11 @@ def _load_or_create_identity_key(path: Optional[str] = None) -> bytes:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load identity key: {e}")
|
||||
|
||||
# Generate new random key
|
||||
key = os.urandom(32)
|
||||
# Generate new key via MeshCore-compatible keygen; store 32-byte private scalar.
|
||||
from repeater.keygen import generate_meshcore_keypair
|
||||
|
||||
_, private_key = generate_meshcore_keypair()
|
||||
key = private_key[:32]
|
||||
|
||||
# Save it
|
||||
try:
|
||||
@@ -617,7 +627,7 @@ def get_radio_for_board(board_config: dict):
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"pymc_tcp radio requires openhop-core >= the release that includes "
|
||||
"PR pyMC-dev/openhop-core#68 (merged 2026-05-13). "
|
||||
"the openhop-core release with TCP modem support. "
|
||||
"Reinstall the [hardware] extra to pick it up."
|
||||
) from None
|
||||
|
||||
@@ -661,7 +671,7 @@ def get_radio_for_board(board_config: dict):
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"pymc_usb radio requires openhop-core >= the release that includes "
|
||||
"PR pyMC-dev/openhop-core#68 (merged 2026-05-13). "
|
||||
"the openhop-core release with TCP modem support. "
|
||||
"Reinstall the [hardware] extra to pick it up."
|
||||
) from None
|
||||
|
||||
|
||||
+27
-14
@@ -14,6 +14,11 @@ from typing import Optional, Tuple
|
||||
from nacl.bindings import crypto_scalarmult_ed25519_base_noclamp
|
||||
|
||||
|
||||
def _has_disallowed_public_prefix(public_key: bytes) -> bool:
|
||||
"""Return True when public key starts with a forbidden prefix byte."""
|
||||
return bool(public_key) and public_key[0] in (0x00, 0xFF)
|
||||
|
||||
|
||||
def generate_meshcore_keypair() -> Tuple[bytes, bytes]:
|
||||
"""Generate a MeshCore-compatible Ed25519 keypair.
|
||||
|
||||
@@ -21,25 +26,30 @@ def generate_meshcore_keypair() -> Tuple[bytes, bytes]:
|
||||
(public_key, private_key) as raw bytes.
|
||||
public_key is 32 bytes, private_key is 64 bytes.
|
||||
"""
|
||||
# 1. Random 32-byte seed
|
||||
seed = secrets.token_bytes(32)
|
||||
while True:
|
||||
# 1. Random 32-byte seed
|
||||
seed = secrets.token_bytes(32)
|
||||
|
||||
# 2. SHA-512 hash
|
||||
digest = hashlib.sha512(seed).digest()
|
||||
# 2. SHA-512 hash
|
||||
digest = hashlib.sha512(seed).digest()
|
||||
|
||||
# 3. Ed25519 scalar clamping on first 32 bytes
|
||||
clamped = bytearray(digest[:32])
|
||||
clamped[0] &= 248 # Clear bottom 3 bits
|
||||
clamped[31] &= 63 # Clear top 2 bits
|
||||
clamped[31] |= 64 # Set bit 6
|
||||
# 3. Ed25519 scalar clamping on first 32 bytes
|
||||
clamped = bytearray(digest[:32])
|
||||
clamped[0] &= 248 # Clear bottom 3 bits
|
||||
clamped[31] &= 63 # Clear top 2 bits
|
||||
clamped[31] |= 64 # Set bit 6
|
||||
|
||||
# 4. Derive public key
|
||||
public_key = crypto_scalarmult_ed25519_base_noclamp(bytes(clamped))
|
||||
# 4. Derive public key
|
||||
public_key = crypto_scalarmult_ed25519_base_noclamp(bytes(clamped))
|
||||
|
||||
# 5. Private key = [clamped_scalar][sha512_upper_half]
|
||||
private_key = bytes(clamped) + digest[32:64]
|
||||
# Disallow keys with 0x00 / 0xFF leading byte.
|
||||
if _has_disallowed_public_prefix(public_key):
|
||||
continue
|
||||
|
||||
return public_key, private_key
|
||||
# 5. Private key = [clamped_scalar][sha512_upper_half]
|
||||
private_key = bytes(clamped) + digest[32:64]
|
||||
|
||||
return public_key, private_key
|
||||
|
||||
|
||||
def generate_vanity_key(
|
||||
@@ -57,6 +67,9 @@ def generate_vanity_key(
|
||||
"""
|
||||
target = prefix.upper()
|
||||
|
||||
if target.startswith("00") or target.startswith("FF"):
|
||||
return None
|
||||
|
||||
for attempt in range(1, max_iterations + 1):
|
||||
pub, priv = generate_meshcore_keypair()
|
||||
if pub.hex().upper().startswith(target):
|
||||
|
||||
@@ -1580,6 +1580,7 @@ class APIEndpoints:
|
||||
if cherrypy.request.method == "OPTIONS":
|
||||
return ""
|
||||
|
||||
future = None
|
||||
try:
|
||||
self._require_post()
|
||||
if not self.send_advert_func:
|
||||
@@ -1588,7 +1589,6 @@ class APIEndpoints:
|
||||
return self._error("Event loop not available")
|
||||
import asyncio
|
||||
|
||||
future = None
|
||||
future = asyncio.run_coroutine_threadsafe(self.send_advert_func(), self.event_loop)
|
||||
result = future.result(timeout=10)
|
||||
return (
|
||||
@@ -1601,11 +1601,8 @@ class APIEndpoints:
|
||||
"Error sending advert: timeout waiting for advert transmission to complete",
|
||||
exc_info=True,
|
||||
)
|
||||
try:
|
||||
if future is not None:
|
||||
future.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
if future is not None:
|
||||
future.cancel()
|
||||
return self._error("Timed out waiting for advert transmission after 10 seconds")
|
||||
except cherrypy.HTTPError:
|
||||
# Re-raise HTTP errors (like 405 Method Not Allowed) without logging
|
||||
@@ -4259,9 +4256,11 @@ class APIEndpoints:
|
||||
key_was_generated = False
|
||||
if not identity_key:
|
||||
try:
|
||||
# Generate a new random 32-byte key (same method as config.py)
|
||||
random_key = os.urandom(32)
|
||||
identity_key = random_key.hex()
|
||||
# Use MeshCore-compatible keygen and store 32-byte private scalar hex.
|
||||
from repeater.keygen import generate_meshcore_keypair
|
||||
|
||||
_, private_key = generate_meshcore_keypair()
|
||||
identity_key = private_key[:32].hex()
|
||||
key_was_generated = True
|
||||
logger.info(f"Auto-generated identity key for '{name}': {identity_key[:16]}...")
|
||||
except Exception as gen_error:
|
||||
|
||||
@@ -15,7 +15,7 @@ info:
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: openHop Repeater
|
||||
url: https://github.com/rightup/openhop-repeater
|
||||
url: https://github.com/openhop-dev/openhop_repeater
|
||||
|
||||
servers:
|
||||
- url: /api
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
# Creates an LXC container with USB passthrough and installs openHop Repeater
|
||||
#
|
||||
# Usage (run on the Proxmox host):
|
||||
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/pyMC-dev/openhop-repeater/main/scripts/proxmox-install.sh)"
|
||||
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/openhop-dev/openhop_repeater/main/scripts/proxmox-install.sh)"
|
||||
#
|
||||
# License: MIT
|
||||
# Source: https://github.com/openhop-dev/openhop-repeater
|
||||
# Source: https://github.com/openhop-dev/openhop_repeater
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────
|
||||
REPO="https://github.com/openhop-dev/openhop-repeater.git"
|
||||
REPO="https://github.com/openhop-dev/openhop_repeater.git"
|
||||
BRANCH="dev"
|
||||
CT_TEMPLATE="debian-12-standard"
|
||||
CT_RAM=1024
|
||||
@@ -210,7 +210,7 @@ OS=\$(. /etc/os-release && echo \"\$NAME\")
|
||||
VER=\$(. /etc/os-release && echo \"\$VERSION_ID\")
|
||||
echo \"\"
|
||||
echo \" openHop Repeater LXC Container\"
|
||||
echo \" 🌐 GitHub: https://github.com/openhop-dev/openhop-repeater\"
|
||||
echo \" 🌐 GitHub: https://github.com/openhop-dev/openhop_repeater\"
|
||||
echo \"\"
|
||||
echo \" 🖥️ OS: \$OS - Version: \$VER\"
|
||||
echo \" 🏠 Hostname: \$HOSTNAME\"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from repeater.config import BaselineCrcCounterRadio
|
||||
from repeater.config import BaselineCrcCounterRadio, load_config
|
||||
|
||||
|
||||
def test_baseline_crc_counter_radio_reports_delta_from_initial_raw_count():
|
||||
@@ -26,3 +26,20 @@ def test_baseline_crc_counter_radio_handles_delayed_modem_counter():
|
||||
|
||||
raw.crc_error_count = 148
|
||||
assert radio.crc_error_count == 3
|
||||
|
||||
|
||||
def test_load_config_prefers_openhop_env_names(tmp_path, monkeypatch):
|
||||
legacy_path = tmp_path / "legacy.yaml"
|
||||
openhop_path = tmp_path / "openhop.yaml"
|
||||
legacy_path.write_text("repeater:\n identity_key: legacy\n", encoding="utf-8")
|
||||
openhop_path.write_text("repeater:\n identity_key: openhop\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setenv("PYMC_REPEATER_CONFIG", str(legacy_path))
|
||||
monkeypatch.setenv("OPENHOP_REPEATER_CONFIG", str(openhop_path))
|
||||
monkeypatch.setenv("PYMC_REPEATER_LOG_LEVEL", "WARNING")
|
||||
monkeypatch.setenv("OPENHOP_REPEATER_LOG_LEVEL", "DEBUG")
|
||||
|
||||
config = load_config()
|
||||
|
||||
assert config["repeater"]["identity_key"] == "openhop"
|
||||
assert config["logging"]["level"] == "DEBUG"
|
||||
|
||||
Reference in New Issue
Block a user