Files
richandClaude Opus 5.5 280428296f Add self-advert controls and contact-card QR code
So other companions can add this one (a prerequisite for them sharing
telemetry with it):
- web UI: send a zero-hop or flood advert on demand
- optional automatic adverts ([advert] interval_hours, 3-168 h like the
  repeater firmware; flood or zero-hop), timer persisted in advert.json
  and restarted by a successful manual advert of the same kind
- the companion's own contact card (export_contact) shown as a QR code
  (segno) and copyable meshcore:// link
- UI and README explain that telemetry must be allowed for the contact;
  the favourite flag alone doesn't grant it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:10 -04:00

240 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install (or upgrade) the MeshCore repeater -> InfluxDB collector as a systemd service.
#
# git clone <repo> && cd meshcore-repeater-stat-influx
# sudo ./install.sh # install / upgrade (web UI on port 8080)
# sudo ./install.sh --port 9090 # first install with a different web UI port
# sudo ./install.sh --uninstall # remove service + code (keeps config and state)
#
# Layout:
# /opt/meshcore-influx/ code + Python venv
# /etc/meshcore-influx/config.toml config, also edited by the web UI (owned by the service user)
# /var/lib/meshcore-influx/ state.json (low-battery mode, last poll times)
# meshcore-influx.service runs as system user 'meshcore'
set -euo pipefail
NAME=meshcore-influx
APP_DIR=/opt/$NAME
CONF_DIR=/etc/$NAME
CONF=$CONF_DIR/config.toml
STATE_DIR=/var/lib/$NAME
UNIT=/etc/systemd/system/$NAME.service
SVC_USER=meshcore
SRC_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "run as root: sudo $0 $*"
command -v systemctl >/dev/null || die "systemd is required"
WEB_PORT=8080
UNINSTALL=0
while [[ $# -gt 0 ]]; do
case $1 in
--uninstall) UNINSTALL=1 ;;
--port) [[ ${2:-} =~ ^[0-9]+$ ]] || die "--port needs a number"; WEB_PORT=$2; shift ;;
*) die "usage: $0 [--port N] [--uninstall]" ;;
esac
shift
done
if [[ $UNINSTALL -eq 1 ]]; then
say "Stopping and removing $NAME.service"
systemctl disable --now "$NAME.service" 2>/dev/null || true
rm -f "$UNIT"
systemctl daemon-reload
rm -rf "$APP_DIR"
say "Removed. Kept $CONF_DIR and $STATE_DIR and user '$SVC_USER' (delete manually if wanted)."
exit 0
fi
[[ -f $SRC_DIR/meshcore_influx.py ]] || die "run this from the project checkout ($SRC_DIR has no meshcore_influx.py)"
# ---------------------------------------------------------------- python
say "Checking Python"
# Pick the newest suitable interpreter; a custom python3 earlier in PATH (e.g. an
# old /usr/local build) shouldn't hide the distro's newer one.
PY=""
for cand in python3.13 python3.12 python3.11 python3.10 python3 /usr/bin/python3; do
p=$(command -v "$cand" 2>/dev/null) || continue
if "$p" -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then
PY=$p
break
fi
done
[[ -n $PY ]] || die "Python >= 3.10 not found (found: $(python3 -V 2>&1)); apt install python3 python3-venv"
say "Using $PY ($("$PY" -V 2>&1))"
if ! "$PY" -c 'import venv, ensurepip' 2>/dev/null; then
if command -v apt-get >/dev/null; then
say "Installing python3-venv"
apt-get update -qq
apt-get install -y -qq python3-venv
else
die "Python venv module missing; install your distro's python3-venv package"
fi
fi
# ---------------------------------------------------------------- user
if ! id "$SVC_USER" >/dev/null 2>&1; then
say "Creating system user '$SVC_USER'"
useradd --system --no-create-home --home-dir "$STATE_DIR" --shell /usr/sbin/nologin "$SVC_USER"
fi
# Serial (USB companion) and BLE access.
for grp in dialout uucp bluetooth; do
if getent group "$grp" >/dev/null; then
usermod -aG "$grp" "$SVC_USER"
fi
done
# ---------------------------------------------------------------- code + venv
say "Installing code to $APP_DIR"
install -d -m 755 "$APP_DIR" "$APP_DIR/static"
install -m 644 "$SRC_DIR/meshcore_influx.py" "$SRC_DIR/web.py" "$SRC_DIR/requirements.txt" \
"$SRC_DIR/config.example.toml" "$SRC_DIR/README.md" "$APP_DIR/"
install -m 644 "$SRC_DIR"/static/* "$APP_DIR/static/"
if [[ -d $SRC_DIR/grafana ]]; then
install -d -m 755 "$APP_DIR/grafana"
install -m 644 "$SRC_DIR"/grafana/*.json "$APP_DIR/grafana/"
fi
if [[ -d $SRC_DIR/.git ]] && command -v git >/dev/null; then
git -C "$SRC_DIR" describe --always --dirty --tags 2>/dev/null > "$APP_DIR/VERSION" || true
fi
say "Creating virtualenv and installing dependencies (this can take a minute)"
[[ -x $APP_DIR/.venv/bin/python ]] || "$PY" -m venv "$APP_DIR/.venv"
"$APP_DIR/.venv/bin/pip" install -q --upgrade pip
"$APP_DIR/.venv/bin/pip" install -q --upgrade -r "$APP_DIR/requirements.txt"
"$APP_DIR/.venv/bin/python" -c 'import meshcore, influxdb_client, aiohttp, tomli_w, segno' \
|| die "dependency install failed"
# ---------------------------------------------------------------- config + state
# The web UI rewrites the config, so the service user owns it (it holds secrets: 600).
install -d -m 700 -o "$SVC_USER" -g "$SVC_USER" "$CONF_DIR"
NEW_CONFIG=0
if [[ ! -f $CONF ]]; then
SERIAL=$(ls /dev/serial/by-id/* 2>/dev/null | head -1 || true)
say "Creating starter config $CONF${SERIAL:+ (serial port: $SERIAL)}"
cat > "$CONF" <<EOF
# Managed by the web UI (http://<this host>:$WEB_PORT/). Hand edits are picked up
# automatically, but comments are not preserved when the UI saves.
[connection]
type = "serial"
port = "$SERIAL"
baudrate = 115200
[influxdb]
url = ""
token = ""
org = ""
bucket = ""
[poll]
interval_seconds = 900
retries = 3
timeout_seconds = 0
state_file = "state.json"
local_stats_interval_seconds = 60
low_battery_mv = 3500
recover_battery_mv = 3750
low_battery_interval_seconds = 3600
low_battery_skip = ["neighbours"]
[web]
listen = "0.0.0.0"
port = $WEB_PORT
EOF
NEW_CONFIG=1
fi
chown "$SVC_USER:$SVC_USER" "$CONF"
chmod 600 "$CONF"
install -d -m 750 -o "$SVC_USER" -g "$SVC_USER" "$STATE_DIR"
run_as_svc() { runuser -u "$SVC_USER" -- "$@"; }
CLI="$APP_DIR/.venv/bin/python $APP_DIR/meshcore_influx.py -c $CONF"
# ---------------------------------------------------------------- web password
if ! grep -q '^password_hash' "$CONF"; then
if [[ -t 0 ]]; then
say "Set the web UI admin password (at least 8 characters)"
while true; do
read -rsp " Password: " PW1; echo
read -rsp " Repeat: " PW2; echo
if [[ ${#PW1} -lt 8 ]]; then warn "too short"; continue; fi
if [[ $PW1 != "$PW2" ]]; then warn "passwords don't match"; continue; fi
break
done
printf '%s\n' "$PW1" | run_as_svc $CLI --set-password >/dev/null
unset PW1 PW2
else
warn "no terminal to ask for a web UI password; set one with:"
warn " sudo runuser -u $SVC_USER -- $CLI --set-password"
fi
fi
# ---------------------------------------------------------------- systemd
say "Writing $UNIT"
cat > "$UNIT" <<EOF
[Unit]
Description=MeshCore repeater telemetry -> InfluxDB (with web UI)
Documentation=file://$APP_DIR/README.md
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=$SVC_USER
Group=$SVC_USER
SupplementaryGroups=$(for g in dialout uucp bluetooth; do getent group "$g" >/dev/null && printf '%s ' "$g"; done)
# Relative state_file ("state.json") resolves here.
WorkingDirectory=$STATE_DIR
Environment=PYTHONUNBUFFERED=1
ExecStart=$CLI
Restart=always
RestartSec=30
TimeoutStopSec=20
# Hardening (serial devices and the D-Bus socket for BLE stay reachable)
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=$STATE_DIR $CONF_DIR
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$NAME.service" >/dev/null
# ---------------------------------------------------------------- start / finish
say "Starting $NAME.service"
systemctl restart "$NAME.service"
sleep 3
if ! systemctl is-active --quiet "$NAME.service"; then
warn "service is not running; check: journalctl -u $NAME -n 50"
exit 1
fi
PORT=$(sed -n 's/^port *= *\([0-9][0-9]*\).*/\1/p' <(sed -n '/^\[web\]/,/^\[/p' "$CONF") | head -1)
HOST=$(hostname -I 2>/dev/null | awk '{print $1}')
say "Running. Web UI: http://${HOST:-<this host>}:${PORT:-8080}/"
if [[ $NEW_CONFIG -eq 1 ]]; then
cat <<EOF
Next, in the web UI:
1. Companion radio: check the serial port (or switch to TCP/BLE)
2. InfluxDB: enter URL / org / bucket / token, then "Test connection"
3. Repeaters: "Add repeater" and pick from the repeaters the companion has heard
EOF
fi
if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
warn "ufw is active; to reach the UI from your LAN: sudo ufw allow ${PORT:-8080}/tcp"
fi
echo " Logs: journalctl -u $NAME -f"