From b6a8ae9751db7fdb41adc0db6b1ac30e9689dfe3 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 16 Apr 2026 20:14:35 -0500 Subject: [PATCH] feat(docs): update Raspberry Pi installation guide for MeshChatX with automated setup scripts and detailed service configuration instructions --- docs/meshchatx_on_raspberry_pi.md | 245 +++++++-- .../meshchatx_on_raspberry_pi.md | 245 +++++++-- scripts/rpi/install_meshchatx.sh | 494 ++++++++++++++++++ 3 files changed, 872 insertions(+), 112 deletions(-) create mode 100755 scripts/rpi/install_meshchatx.sh diff --git a/docs/meshchatx_on_raspberry_pi.md b/docs/meshchatx_on_raspberry_pi.md index bf7c551..03d3477 100644 --- a/docs/meshchatx_on_raspberry_pi.md +++ b/docs/meshchatx_on_raspberry_pi.md @@ -1,99 +1,232 @@ -# MeshChat on a Raspberry Pi +# MeshChatX on Raspberry Pi -A simple guide to install [MeshChat](https://github.com/liamcottle/reticulum-meshchat) on a Raspberry Pi. +This guide shows a simple headless setup for running MeshChatX on a Raspberry Pi 4 +with a web UI you can access from another device on your network. -This would allow you to connect an [RNode](https://github.com/markqvist/RNode_Firmware) (such as a Heltec v3) to the Rasbperry Pi via USB, and then access the MeshChat Web UI from another machine on your network. +This install path uses a release wheel, which already includes frontend assets. -My intended use case is to run the Pi + RNode combo from my solar-powered shed, and access the MeshChat Web UI via WiFi. +## Automated Setup Scripts -> Note: This has been tested on a Raspberry Pi 4 Model B - -## Install Raspberry Pi OS - -If you haven't already done so, the first step is to install Raspberry Pi OS onto an sdcard, and then boot up the Pi. Once booted, follow the below commands. - -## Update System +If you want one-command setup, use the interactive installer from repo root: +```bash +bash scripts/rpi/install_meshchatx.sh ``` + +The installer guides you through: + +- Optional `espeak-ng` install (tries apt/dnf/pacman) +- Install method (`pipx` or `venv + pip`) +- Storage and Reticulum directories +- Bind host and port (with availability check) +- HTTPS on/off (default on) +- Service mode (`system`, `user`, or `none`) +- Service startup verification (`Running on ...`) + +If startup validation fails, it prints recent logs and stops the service to avoid +restart loops. + +The installer also applies compatibility dependencies needed by older wheel +releases. + +For all options: + +```bash +bash scripts/rpi/install_meshchatx.sh --help +``` + +## 1) Install Base Dependencies + +```bash sudo apt update -sudo apt upgrade +sudo apt upgrade -y +sudo apt install -y python3 python3-pip pipx ``` -## Install System Dependencies +## 2) Enable pipx Path -``` -sudo apt install git -sudo apt install python3-pip +```bash +pipx ensurepath +source ~/.profile ``` -## Install NodeJS v24 +If `pipx` is not available in your distro package repo, install it with: -``` -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg -NODE_MAJOR=24 -echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list -sudo apt update -sudo apt install nodejs +```bash +python3 -m pip install --user pipx +python3 -m pipx ensurepath +source ~/.profile ``` -## Install pnpm +## 3) Install MeshChatX with pipx (recommended) -``` -corepack enable -corepack prepare pnpm@latest --activate +Preferred option (recommended): install from a release wheel (4.4.0 or newer), +because the wheel bundles frontend assets. + +```bash +pipx install /path/to/reticulum_meshchatx--py3-none-any.whl ``` -## Install MeshChat +Direct example (v4.4.0): -``` -git clone https://github.com/liamcottle/reticulum-meshchat -cd reticulum-meshchat -pip install -r requirements.txt --break-system-packages -pnpm install --prod -pnpm run build-frontend +```bash +pipx install "https://git.quad4.io/RNS-Things/MeshChatX/releases/download/v4.4.0/reticulum_meshchatx-4.4.0-py3-none-any.whl" ``` -## Run MeshChat +`py3-none-any` wheels are architecture-independent, so the same wheel artifact +works on Raspberry Pi ARM and x86_64 Linux systems. -``` -python meshchat.py --headless --host 0.0.0.0 +Upgrade example: + +```bash +pipx upgrade meshchatx ``` -## Configure Service +## 4) Install MeshChatX without pipx (venv + pip) -Adding a `systemd` service will allow MeshChat to run in the background when you disconnect from the Pi's terminal. +If you prefer not to use pipx: -``` -sudo nano /etc/systemd/system/reticulum-meshchat.service +```bash +mkdir -p ~/meshchatx +cd ~/meshchatx +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install "https://git.quad4.io/RNS-Things/MeshChatX/releases/download/v4.4.0/reticulum_meshchatx-4.4.0-py3-none-any.whl" ``` +Run command in venv mode: + +```bash +~/meshchatx/.venv/bin/meshchatx --headless --host 0.0.0.0 --port 8000 ``` + +## 5) Run MeshChatX (Headless) + +```bash +meshchatx --headless --host 0.0.0.0 --port 8000 +``` + +Then open: + +```text +http://:8000 +``` + +## 6) Configure a systemd Service + +`systemd` keeps MeshChatX running in the background and starts it automatically +on boot. + +You have two service styles: + +- System service (`/etc/systemd/system/...`) for always-on host services. +- User service (`~/.config/systemd/user/...`) for per-user sessions. + +### Option A: System service (recommended for Pi node/server use) + +Create `/etc/systemd/system/meshchatx.service`: + +```ini [Unit] -Description=reticulum-meshchat -After=network.target -StartLimitIntervalSec=0 +Description=MeshChatX Headless (system service) +After=network-online.target +Wants=network-online.target [Service] Type=simple +User=pi +Group=pi +WorkingDirectory=/home/pi/meshchatx +Environment="PATH=/home/pi/.local/bin:/usr/bin:/bin" +ExecStart=/home/pi/.local/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir /home/pi/meshchatx/storage --reticulum-config-dir /home/pi/.reticulum Restart=always -RestartSec=1 -User=liamcottle -Group=liamcottle -WorkingDirectory=/home/liamcottle/reticulum-meshchat -ExecStart=/usr/bin/env python /home/liamcottle/reticulum-meshchat/meshchat.py --headless --host 0.0.0.0 +RestartSec=3 [Install] WantedBy=multi-user.target ``` -> Note: Make sure to update the usernames in the service file if needed. +The above service file is for pipx installs. For venv installs, use: -``` -sudo systemctl enable reticulum-meshchat.service -sudo systemctl start reticulum-meshchat.service -sudo systemctl status reticulum-meshchat.service +```ini +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/home/pi/meshchatx +Environment="PATH=/home/pi/meshchatx/.venv/bin:/usr/bin:/bin" +ExecStart=/home/pi/meshchatx/.venv/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir /home/pi/meshchatx/storage --reticulum-config-dir /home/pi/.reticulum +Restart=always +RestartSec=3 ``` -You should now be able to access MeshChat via your Pi's IP address. +Update `User`, `Group`, and paths if your install location is different. -> Note: Don't forget to include the default port `8000` +Enable and start: + +```bash +mkdir -p /home/pi/meshchatx/storage /home/pi/.reticulum +sudo chown -R pi:pi /home/pi/meshchatx +sudo systemctl daemon-reload +sudo systemctl enable --now meshchatx.service +sudo systemctl status meshchatx.service +``` + +### Option B: User service (no sudo system unit) + +Create `~/.config/systemd/user/meshchatx.service`: + +```ini +[Unit] +Description=MeshChatX Headless (user service) +After=network-online.target + +[Service] +Type=simple +WorkingDirectory=%h/meshchatx +Environment="PATH=%h/.local/bin:/usr/bin:/bin" +ExecStart=%h/.local/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir %h/meshchatx/storage --reticulum-config-dir %h/.reticulum +Restart=always +RestartSec=3 + +[Install] +WantedBy=default.target +``` + +Enable/start user service: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now meshchatx.service +systemctl --user status meshchatx.service +``` + +If you want user services to stay active without login: + +```bash +sudo loginctl enable-linger pi +``` + +### Service management commands + +```bash +sudo systemctl restart meshchatx.service +sudo systemctl stop meshchatx.service +sudo systemctl disable meshchatx.service +``` + +Useful logs and troubleshooting: + +```bash +journalctl -u meshchatx.service -f +journalctl -u meshchatx.service -n 200 --no-pager +systemctl show meshchatx.service -p ExecStart -p User -p Group +``` + +## Notes + +- Reticulum configuration and identity data are stored in the service user's home + directory by default (for example `~/.reticulum` and MeshChatX storage paths). +- If you attach RNode hardware by USB, make sure the service user has permission + to access serial devices (`dialout` group on Debian-based systems). diff --git a/meshchatx/src/frontend/public/meshchatx-docs/meshchatx_on_raspberry_pi.md b/meshchatx/src/frontend/public/meshchatx-docs/meshchatx_on_raspberry_pi.md index 29a363b..03d3477 100644 --- a/meshchatx/src/frontend/public/meshchatx-docs/meshchatx_on_raspberry_pi.md +++ b/meshchatx/src/frontend/public/meshchatx-docs/meshchatx_on_raspberry_pi.md @@ -1,99 +1,232 @@ -# MeshChat on a Raspberry Pi +# MeshChatX on Raspberry Pi -A simple guide to install [MeshChat](https://github.com/liamcottle/reticulum-meshchat) on a Raspberry Pi. +This guide shows a simple headless setup for running MeshChatX on a Raspberry Pi 4 +with a web UI you can access from another device on your network. -This would allow you to connect an [RNode](https://github.com/markqvist/RNode_Firmware) (such as a Heltec v3) to the Rasbperry Pi via USB, and then access the MeshChat Web UI from another machine on your network. +This install path uses a release wheel, which already includes frontend assets. -My intended use case is to run the Pi + RNode combo from my solar-powered shed, and access the MeshChat Web UI via WiFi. +## Automated Setup Scripts -> Note: This has been tested on a Raspberry Pi 4 Model B - -## Install Raspberry Pi OS - -If you haven't already done so, the first step is to install Raspberry Pi OS onto an sdcard, and then boot up the Pi. Once booted, follow the below commands. - -## Update System +If you want one-command setup, use the interactive installer from repo root: +```bash +bash scripts/rpi/install_meshchatx.sh ``` + +The installer guides you through: + +- Optional `espeak-ng` install (tries apt/dnf/pacman) +- Install method (`pipx` or `venv + pip`) +- Storage and Reticulum directories +- Bind host and port (with availability check) +- HTTPS on/off (default on) +- Service mode (`system`, `user`, or `none`) +- Service startup verification (`Running on ...`) + +If startup validation fails, it prints recent logs and stops the service to avoid +restart loops. + +The installer also applies compatibility dependencies needed by older wheel +releases. + +For all options: + +```bash +bash scripts/rpi/install_meshchatx.sh --help +``` + +## 1) Install Base Dependencies + +```bash sudo apt update -sudo apt upgrade +sudo apt upgrade -y +sudo apt install -y python3 python3-pip pipx ``` -## Install System Dependencies +## 2) Enable pipx Path -``` -sudo apt install git -sudo apt install python3-pip +```bash +pipx ensurepath +source ~/.profile ``` -## Install NodeJS v22 +If `pipx` is not available in your distro package repo, install it with: -``` -curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg -NODE_MAJOR=24 -echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list -sudo apt update -sudo apt install nodejs +```bash +python3 -m pip install --user pipx +python3 -m pipx ensurepath +source ~/.profile ``` -## Install pnpm +## 3) Install MeshChatX with pipx (recommended) -``` -corepack enable -corepack prepare pnpm@latest --activate +Preferred option (recommended): install from a release wheel (4.4.0 or newer), +because the wheel bundles frontend assets. + +```bash +pipx install /path/to/reticulum_meshchatx--py3-none-any.whl ``` -## Install MeshChat +Direct example (v4.4.0): -``` -git clone https://github.com/liamcottle/reticulum-meshchat -cd reticulum-meshchat -pip install -r requirements.txt --break-system-packages -pnpm install --prod -pnpm run build-frontend +```bash +pipx install "https://git.quad4.io/RNS-Things/MeshChatX/releases/download/v4.4.0/reticulum_meshchatx-4.4.0-py3-none-any.whl" ``` -## Run MeshChat +`py3-none-any` wheels are architecture-independent, so the same wheel artifact +works on Raspberry Pi ARM and x86_64 Linux systems. -``` -python meshchat.py --headless --host 0.0.0.0 +Upgrade example: + +```bash +pipx upgrade meshchatx ``` -## Configure Service +## 4) Install MeshChatX without pipx (venv + pip) -Adding a `systemd` service will allow MeshChat to run in the background when you disconnect from the Pi's terminal. +If you prefer not to use pipx: -``` -sudo nano /etc/systemd/system/reticulum-meshchat.service +```bash +mkdir -p ~/meshchatx +cd ~/meshchatx +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install "https://git.quad4.io/RNS-Things/MeshChatX/releases/download/v4.4.0/reticulum_meshchatx-4.4.0-py3-none-any.whl" ``` +Run command in venv mode: + +```bash +~/meshchatx/.venv/bin/meshchatx --headless --host 0.0.0.0 --port 8000 ``` + +## 5) Run MeshChatX (Headless) + +```bash +meshchatx --headless --host 0.0.0.0 --port 8000 +``` + +Then open: + +```text +http://:8000 +``` + +## 6) Configure a systemd Service + +`systemd` keeps MeshChatX running in the background and starts it automatically +on boot. + +You have two service styles: + +- System service (`/etc/systemd/system/...`) for always-on host services. +- User service (`~/.config/systemd/user/...`) for per-user sessions. + +### Option A: System service (recommended for Pi node/server use) + +Create `/etc/systemd/system/meshchatx.service`: + +```ini [Unit] -Description=reticulum-meshchat -After=network.target -StartLimitIntervalSec=0 +Description=MeshChatX Headless (system service) +After=network-online.target +Wants=network-online.target [Service] Type=simple +User=pi +Group=pi +WorkingDirectory=/home/pi/meshchatx +Environment="PATH=/home/pi/.local/bin:/usr/bin:/bin" +ExecStart=/home/pi/.local/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir /home/pi/meshchatx/storage --reticulum-config-dir /home/pi/.reticulum Restart=always -RestartSec=1 -User=liamcottle -Group=liamcottle -WorkingDirectory=/home/liamcottle/reticulum-meshchat -ExecStart=/usr/bin/env python /home/liamcottle/reticulum-meshchat/meshchat.py --headless --host 0.0.0.0 +RestartSec=3 [Install] WantedBy=multi-user.target ``` -> Note: Make sure to update the usernames in the service file if needed. +The above service file is for pipx installs. For venv installs, use: -``` -sudo systemctl enable reticulum-meshchat.service -sudo systemctl start reticulum-meshchat.service -sudo systemctl status reticulum-meshchat.service +```ini +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/home/pi/meshchatx +Environment="PATH=/home/pi/meshchatx/.venv/bin:/usr/bin:/bin" +ExecStart=/home/pi/meshchatx/.venv/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir /home/pi/meshchatx/storage --reticulum-config-dir /home/pi/.reticulum +Restart=always +RestartSec=3 ``` -You should now be able to access MeshChat via your Pi's IP address. +Update `User`, `Group`, and paths if your install location is different. -> Note: Don't forget to include the default port `8000` +Enable and start: + +```bash +mkdir -p /home/pi/meshchatx/storage /home/pi/.reticulum +sudo chown -R pi:pi /home/pi/meshchatx +sudo systemctl daemon-reload +sudo systemctl enable --now meshchatx.service +sudo systemctl status meshchatx.service +``` + +### Option B: User service (no sudo system unit) + +Create `~/.config/systemd/user/meshchatx.service`: + +```ini +[Unit] +Description=MeshChatX Headless (user service) +After=network-online.target + +[Service] +Type=simple +WorkingDirectory=%h/meshchatx +Environment="PATH=%h/.local/bin:/usr/bin:/bin" +ExecStart=%h/.local/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir %h/meshchatx/storage --reticulum-config-dir %h/.reticulum +Restart=always +RestartSec=3 + +[Install] +WantedBy=default.target +``` + +Enable/start user service: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now meshchatx.service +systemctl --user status meshchatx.service +``` + +If you want user services to stay active without login: + +```bash +sudo loginctl enable-linger pi +``` + +### Service management commands + +```bash +sudo systemctl restart meshchatx.service +sudo systemctl stop meshchatx.service +sudo systemctl disable meshchatx.service +``` + +Useful logs and troubleshooting: + +```bash +journalctl -u meshchatx.service -f +journalctl -u meshchatx.service -n 200 --no-pager +systemctl show meshchatx.service -p ExecStart -p User -p Group +``` + +## Notes + +- Reticulum configuration and identity data are stored in the service user's home + directory by default (for example `~/.reticulum` and MeshChatX storage paths). +- If you attach RNode hardware by USB, make sure the service user has permission + to access serial devices (`dialout` group on Debian-based systems). diff --git a/scripts/rpi/install_meshchatx.sh b/scripts/rpi/install_meshchatx.sh new file mode 100755 index 0000000..0bad8a4 --- /dev/null +++ b/scripts/rpi/install_meshchatx.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +set -euo pipefail + +DEFAULT_WHEEL_URL="https://git.quad4.io/RNS-Things/MeshChatX/releases/download/v4.4.0/reticulum_meshchatx-4.4.0-py3-none-any.whl" + +RUN_USER="${SUDO_USER:-$USER}" +RUN_GROUP="$(id -gn "$RUN_USER")" +USER_HOME="$(eval echo "~${RUN_USER}")" + +if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then + C_RESET="$(tput sgr0)" + C_BOLD="$(tput bold)" + C_RED="$(tput setaf 1)" + C_GREEN="$(tput setaf 2)" + C_YELLOW="$(tput setaf 3)" + C_BLUE="$(tput setaf 4)" +else + C_RESET="" + C_BOLD="" + C_RED="" + C_GREEN="" + C_YELLOW="" + C_BLUE="" +fi + +note() { + echo "${C_BLUE}${C_BOLD}==>${C_RESET} $*" +} + +warn() { + echo "${C_YELLOW}${C_BOLD}WARN:${C_RESET} $*" +} + +err() { + echo "${C_RED}${C_BOLD}ERROR:${C_RESET} $*" >&2 +} + +ok() { + echo "${C_GREEN}${C_BOLD}OK:${C_RESET} $*" +} + +run_as_user() { + local cmd="$1" + if [[ "$EUID" -eq 0 && "$RUN_USER" != "root" ]]; then + sudo -u "$RUN_USER" -H bash -lc "$cmd" + else + bash -lc "$cmd" + fi +} + +prompt_default() { + local prompt="$1" + local default="$2" + local value="" + read -r -p "$prompt [$default]: " value + if [[ -z "$value" ]]; then + value="$default" + fi + echo "$value" +} + +prompt_yes_no() { + local prompt="$1" + local default="${2:-y}" + local answer="" + local hint="Y/n" + if [[ "$default" == "n" ]]; then + hint="y/N" + fi + + while true; do + read -r -p "$prompt ($hint): " answer + if [[ -z "$answer" ]]; then + answer="$default" + fi + case "${answer,,}" in + y|yes) return 0 ;; + n|no) return 1 ;; + *) + echo "Please answer y or n." + ;; + esac + done +} + +pick_package_manager() { + if command -v apt-get >/dev/null 2>&1; then + echo "apt" + return + fi + if command -v dnf >/dev/null 2>&1; then + echo "dnf" + return + fi + if command -v pacman >/dev/null 2>&1; then + echo "pacman" + return + fi + echo "none" +} + +install_package_if_possible() { + local package="$1" + local mgr + mgr="$(pick_package_manager)" + case "$mgr" in + apt) + if [[ "$EUID" -eq 0 ]]; then + apt-get update && apt-get install -y "$package" + else + sudo apt-get update && sudo apt-get install -y "$package" + fi + ;; + dnf) + if [[ "$EUID" -eq 0 ]]; then + dnf install -y "$package" + else + sudo dnf install -y "$package" + fi + ;; + pacman) + if [[ "$EUID" -eq 0 ]]; then + pacman -Sy --noconfirm "$package" + else + sudo pacman -Sy --noconfirm "$package" + fi + ;; + none) + warn "No supported package manager found (apt/dnf/pacman). Skipping install for $package." + ;; + esac +} + +check_port_available() { + local host="$1" + local port="$2" + python3 - "$host" "$port" <<'PY' +import socket +import sys + +host = sys.argv[1] +port = int(sys.argv[2]) +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +try: + s.bind((host, port)) +except OSError: + sys.exit(1) +finally: + s.close() +sys.exit(0) +PY +} + +detect_arch() { + python3 - <<'PY' +import platform +print(platform.machine().strip().lower()) +PY +} + +is_supported_rpi_arch() { + local arch="$1" + case "$arch" in + armv6l|armv7l|aarch64|arm64) + return 0 + ;; + *) + return 1 + ;; + esac +} + +write_system_service() { + local exec_cmd="$1" + local workdir="$2" + local path_value="$3" + local svc="/etc/systemd/system/meshchatx.service" + + if [[ "$EUID" -eq 0 ]]; then + SUDO="" + else + SUDO="sudo" + fi + + $SUDO tee "$svc" >/dev/null < '${svc_path}' <<'EOF' +[Unit] +Description=MeshChatX Headless (user service) +After=network-online.target + +[Service] +Type=simple +WorkingDirectory=${workdir} +Environment=\"PATH=${path_value}\" +ExecStart=${exec_cmd} +Restart=always +RestartSec=3 + +[Install] +WantedBy=default.target +EOF" +} + +handle_service_start_failure() { + local mode="$1" + local reason="$2" + + err "$reason" + warn "Service startup failed. Recent logs:" + if [[ "$mode" == "system" ]]; then + sudo journalctl -u meshchatx.service -n 200 --no-pager || true + sudo systemctl stop meshchatx.service || true + sudo systemctl reset-failed meshchatx.service || true + else + run_as_user "journalctl --user -u meshchatx.service -n 200 --no-pager" || true + run_as_user "systemctl --user stop meshchatx.service || true" + run_as_user "systemctl --user reset-failed meshchatx.service || true" + fi + err "Service was stopped/reset. Fix config and run installer again." + exit 1 +} + +verify_service_started() { + local mode="$1" + local probe_host="$2" + local probe_port="$3" + local https_enabled="$4" + local tries=40 + local log_cmd="" + local stop_cmd="" + local scheme="https" + if [[ "$https_enabled" == "no" ]]; then + scheme="http" + fi + + if [[ "$mode" == "system" ]]; then + log_cmd="sudo journalctl -u meshchatx.service -n 200 --no-pager" + stop_cmd="sudo systemctl stop meshchatx.service; sudo systemctl reset-failed meshchatx.service" + else + log_cmd="journalctl --user -u meshchatx.service -n 200 --no-pager" + stop_cmd="systemctl --user stop meshchatx.service || true; systemctl --user reset-failed meshchatx.service || true" + fi + + note "Verifying service startup via ${scheme}://${probe_host}:${probe_port}/api/v1/status ..." + for _ in $(seq 1 "$tries"); do + if python3 - "$scheme" "$probe_host" "$probe_port" <<'PY' +import json +import ssl +import sys +import urllib.request + +scheme, host, port = sys.argv[1], sys.argv[2], int(sys.argv[3]) +url = f"{scheme}://{host}:{port}/api/v1/status" + +ctx = ssl._create_unverified_context() if scheme == "https" else None +req = urllib.request.Request(url, method="GET") +try: + with urllib.request.urlopen(req, timeout=2, context=ctx) as resp: + if resp.status != 200: + raise RuntimeError(f"unexpected status {resp.status}") + data = json.loads(resp.read().decode("utf-8")) + if data.get("status") != "ok": + raise RuntimeError("status payload is not ok") +except Exception: + sys.exit(1) +sys.exit(0) +PY + then + ok "Service started successfully (status endpoint is healthy)." + return 0 + fi + sleep 1 + done + + err "Service did not pass status endpoint health check." + warn "Recent logs:" + if [[ "$mode" == "system" ]]; then + sudo journalctl -u meshchatx.service -n 200 --no-pager || true + eval "$stop_cmd" + else + run_as_user "$log_cmd" || true + run_as_user "$stop_cmd" + fi + err "Service was stopped to prevent restart loops." + return 1 +} + +main() { + note "MeshChatX Raspberry Pi Interactive Installer" + echo "Detected user: ${RUN_USER} (group: ${RUN_GROUP})" + local arch + arch="$(detect_arch)" + echo "Detected architecture: ${arch}" + if is_supported_rpi_arch "$arch"; then + ok "Detected Raspberry Pi ARM architecture (${arch})." + else + warn "Detected non-RPi arch (${arch}). Script can still run, but this guide targets Raspberry Pi ARM." + fi + note "The wheel is py3-none-any, so it is architecture-independent (32-bit and 64-bit ARM are supported)." + echo + + if prompt_yes_no "Do you want to install espeak-ng?" "y"; then + note "Installing espeak-ng (best effort)..." + if ! install_package_if_possible "espeak-ng"; then + warn "Could not install espeak-ng automatically; continuing." + fi + else + note "Skipping espeak-ng installation." + fi + + local method_choice="" + while [[ "$method_choice" != "1" && "$method_choice" != "2" ]]; do + echo + echo "Choose installation method:" + echo " 1) pipx (recommended)" + echo " 2) venv + pip" + read -r -p "Selection [1/2]: " method_choice + if [[ -z "$method_choice" ]]; then + method_choice="1" + fi + done + + local wheel_url + wheel_url="$(prompt_default "Wheel URL" "$DEFAULT_WHEEL_URL")" + + local install_root + install_root="$(prompt_default "Install root directory" "${USER_HOME}/meshchatx")" + local storage_dir + storage_dir="$(prompt_default "Storage directory" "${install_root}/storage")" + local rns_dir + rns_dir="$(prompt_default "Reticulum config directory" "${USER_HOME}/.reticulum")" + local bind_host + bind_host="$(prompt_default "Bind IP/host" "0.0.0.0")" + local bind_port + bind_port="$(prompt_default "Bind port" "8000")" + + if check_port_available "$bind_host" "$bind_port"; then + ok "Port ${bind_port} is available on ${bind_host}." + else + warn "Port ${bind_port} appears to be in use on ${bind_host}." + if ! prompt_yes_no "Continue anyway?" "n"; then + err "Aborted." + exit 1 + fi + fi + + local https_enabled="yes" + if ! prompt_yes_no "Enable HTTPS?" "y"; then + https_enabled="no" + fi + + local service_mode="none" + if prompt_yes_no "Do you want to configure a systemd service?" "y"; then + service_mode="" + while [[ "$service_mode" != "system" && "$service_mode" != "user" ]]; do + service_mode="$(prompt_default "Service mode (system/user)" "system")" + done + fi + + local no_https_flag="" + if [[ "$https_enabled" == "no" ]]; then + no_https_flag=" --no-https" + fi + + local probe_host="$bind_host" + if [[ "$bind_host" == "0.0.0.0" ]]; then + probe_host="127.0.0.1" + elif [[ "$bind_host" == "::" ]]; then + probe_host="::1" + fi + + note "Preparing directories..." + if [[ "$EUID" -eq 0 ]]; then + install -d -m 755 -o "$RUN_USER" -g "$RUN_GROUP" "$install_root" "$storage_dir" "$rns_dir" + else + mkdir -p "$install_root" "$storage_dir" "$rns_dir" + if command -v sudo >/dev/null 2>&1; then + sudo install -d -m 755 -o "$RUN_USER" -g "$RUN_GROUP" "$install_root" "$storage_dir" "$rns_dir" >/dev/null 2>&1 || true + fi + fi + + local bin_path="" + local venv_path="${install_root}/.venv" + + if [[ "$method_choice" == "1" ]]; then + if ! command -v pipx >/dev/null 2>&1; then + err "pipx not found. Install pipx first or choose venv method." + exit 1 + fi + note "Installing MeshChatX via pipx..." + run_as_user "pipx ensurepath >/dev/null 2>&1 || true" + run_as_user "pipx install --force '${wheel_url}'" + run_as_user "pipx inject reticulum-meshchatx packaging >/dev/null 2>&1 || true" + bin_path="${USER_HOME}/.local/bin/meshchatx" + else + note "Installing MeshChatX via venv + pip..." + run_as_user "python3 -m venv '${venv_path}'" + run_as_user "'${venv_path}/bin/python' -m pip install --upgrade pip" + run_as_user "'${venv_path}/bin/python' -m pip install '${wheel_url}'" + run_as_user "'${venv_path}/bin/python' -m pip install packaging" + bin_path="${venv_path}/bin/meshchatx" + fi + + if [[ ! -x "$bin_path" ]]; then + err "meshchatx binary not found at: $bin_path" + exit 1 + fi + + local exec_cmd="${bin_path} --headless --host ${bind_host} --port ${bind_port} --storage-dir ${storage_dir} --reticulum-config-dir ${rns_dir}${no_https_flag}" + local path_env="${venv_path}/bin:${USER_HOME}/.local/bin:/usr/bin:/bin" + + if [[ "$service_mode" == "none" ]]; then + ok "Install complete." + echo + echo "Run command:" + echo " ${exec_cmd}" + exit 0 + fi + + if [[ "$service_mode" == "system" ]]; then + note "Creating system service..." + write_system_service "$exec_cmd" "$install_root" "$path_env" + if [[ "$EUID" -eq 0 ]]; then + if ! systemctl daemon-reload; then + handle_service_start_failure "system" "systemctl daemon-reload failed." + fi + if ! systemctl enable --now meshchatx.service; then + handle_service_start_failure "system" "systemctl enable/start failed." + fi + else + if ! sudo systemctl daemon-reload; then + handle_service_start_failure "system" "sudo systemctl daemon-reload failed." + fi + if ! sudo systemctl enable --now meshchatx.service; then + handle_service_start_failure "system" "sudo systemctl enable/start failed." + fi + fi + if ! verify_service_started "system" "$probe_host" "$bind_port" "$https_enabled"; then + exit 1 + fi + ok "System service is enabled and running." + else + if [[ "$service_mode" == "user" ]]; then + note "Creating user service..." + write_user_service "$exec_cmd" "$install_root" "$path_env" + if ! run_as_user "systemctl --user daemon-reload"; then + handle_service_start_failure "user" "systemctl --user daemon-reload failed." + fi + if ! run_as_user "systemctl --user enable --now meshchatx.service"; then + handle_service_start_failure "user" "systemctl --user enable/start failed." + fi + if ! verify_service_started "user" "$probe_host" "$bind_port" "$https_enabled"; then + exit 1 + fi + ok "User service is enabled and running." + fi + fi + + echo + echo "Web UI:" + echo " https://${bind_host}:${bind_port}" + if [[ "$https_enabled" == "no" ]]; then + echo " (HTTP mode enabled)" + fi +} + +main "$@"