mirror of
https://github.com/dmduran12/pymc_console-dist.git
synced 2026-05-01 19:12:56 +02:00
2927 lines
113 KiB
Bash
Executable File
2927 lines
113 KiB
Bash
Executable File
#!/bin/bash
|
|
# pyMC Console Management Script
|
|
# Install, Upgrade, Configure, and Manage pymc_console stack
|
|
#
|
|
# INSTALLATION FLOW (mirrors upstream pyMC_Repeater):
|
|
# 1. User clones pymc_console to their preferred location (e.g., ~/pymc_console)
|
|
# 2. User runs: sudo ./manage.sh install
|
|
# 3. This script clones pyMC_Repeater as a sibling directory (e.g., ~/pyMC_Repeater)
|
|
# 4. Applies patches to the clone, then copies files to /opt/pymc_repeater
|
|
# 5. Installs Python packages from the clone directory
|
|
# 6. Overlays our React dashboard to the installation
|
|
#
|
|
# This matches upstream's flow where manage.sh runs from within a cloned repo
|
|
# and copies files to /opt. This makes it easier to:
|
|
# - Submit patches as PRs to upstream
|
|
# - Stay compatible with upstream updates
|
|
# - Allow users to switch between console and vanilla pyMC_Repeater
|
|
|
|
# ============================================================================
|
|
# Bootstrap Self-Healing (runs BEFORE anything else)
|
|
# ============================================================================
|
|
# Fixes chicken-and-egg: if git history diverged (e.g., after force-push),
|
|
# the old manage.sh can't pull the new manage.sh. This check runs early
|
|
# and resyncs if needed, then re-execs to run the updated script.
|
|
|
|
_bootstrap_self_heal() {
|
|
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Only heal if we're in a git repo and running as root (upgrade/install context)
|
|
[ ! -d "$script_dir/.git" ] && return 0
|
|
[ "$EUID" -ne 0 ] && return 0
|
|
|
|
# Skip if BOOTSTRAP_DONE is set (prevents infinite loop)
|
|
[ -n "$BOOTSTRAP_DONE" ] && return 0
|
|
|
|
cd "$script_dir" || return 0
|
|
git config --global --add safe.directory "$script_dir" 2>/dev/null || true
|
|
git fetch origin 2>/dev/null || return 0
|
|
|
|
local local_hash=$(git rev-parse HEAD 2>/dev/null)
|
|
local remote_hash=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null)
|
|
|
|
# If already up-to-date, nothing to do
|
|
[ -z "$remote_hash" ] && return 0
|
|
[ "$local_hash" = "$remote_hash" ] && return 0
|
|
|
|
# Check if fast-forward is possible
|
|
if git merge-base --is-ancestor HEAD "$remote_hash" 2>/dev/null; then
|
|
# Fast-forward works, let normal upgrade flow handle it
|
|
return 0
|
|
fi
|
|
|
|
# History diverged! Fix it now.
|
|
echo -e "\033[1;33m⚠ Detected diverged git history - auto-healing...\033[0m"
|
|
if git reset --hard "origin/main" 2>/dev/null || git reset --hard "origin/master" 2>/dev/null; then
|
|
echo -e "\033[0;32m✓ Repository synced - restarting with updated script...\033[0m"
|
|
echo ""
|
|
export BOOTSTRAP_DONE=1
|
|
exec "$script_dir/manage.sh" "$@"
|
|
else
|
|
echo -e "\033[0;31m✗ Auto-heal failed. Manual fix: cd $script_dir && git fetch && git reset --hard origin/main\033[0m"
|
|
fi
|
|
}
|
|
|
|
# Run bootstrap check, passing through all args for re-exec
|
|
_bootstrap_self_heal "$@"
|
|
|
|
set -e
|
|
|
|
# ============================================================================
|
|
# Path Configuration
|
|
# ============================================================================
|
|
|
|
# Script location (where pymc_console was cloned)
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# pyMC_Repeater clone location (sibling to pymc_console)
|
|
# e.g., if SCRIPT_DIR is ~/dev/pymc_console, CLONE_DIR is ~/dev/pyMC_Repeater
|
|
CLONE_DIR="$(dirname "$SCRIPT_DIR")/pyMC_Repeater"
|
|
|
|
# Installation paths (where files are deployed - matches upstream)
|
|
# INSTALL_DIR: Where pyMC_Repeater is installed (matches upstream standard)
|
|
# CONSOLE_DIR: Where pymc_console stores its files (radio presets, dashboard, etc.)
|
|
# UI_DIR: Where our React dashboard is installed (separate from upstream Vue.js)
|
|
INSTALL_DIR="/opt/pymc_repeater"
|
|
CONSOLE_DIR="/opt/pymc_console"
|
|
UI_DIR="/opt/pymc_console/web/html"
|
|
CONFIG_DIR="/etc/pymc_repeater"
|
|
LOG_DIR="/var/log/pymc_repeater"
|
|
SERVICE_USER="repeater"
|
|
|
|
# Legacy alias for compatibility
|
|
REPEATER_DIR="$INSTALL_DIR"
|
|
|
|
# Service name (backend serves both API and static frontend)
|
|
BACKEND_SERVICE="pymc-repeater"
|
|
|
|
# Default branch for installations
|
|
DEFAULT_BRANCH="dev"
|
|
|
|
# Colors for terminal output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
WHITE='\033[97m' # Bright white for glow effect
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Status indicators
|
|
CHECK="${GREEN}✓${NC}"
|
|
CROSS="${RED}✗${NC}"
|
|
ARROW="${CYAN}➜${NC}"
|
|
SPINNER_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
|
|
# ============================================================================
|
|
# Progress Display Functions
|
|
# ============================================================================
|
|
|
|
# Print a step header
|
|
print_step() {
|
|
local step_num="$1"
|
|
local total_steps="$2"
|
|
local description="$3"
|
|
echo ""
|
|
echo -e "${BOLD}${CYAN}[$step_num/$total_steps]${NC} ${BOLD}$description${NC}"
|
|
}
|
|
|
|
# Print success message
|
|
print_success() {
|
|
echo -e " ${CHECK} $1"
|
|
}
|
|
|
|
# Print error message
|
|
print_error() {
|
|
echo -e " ${CROSS} ${RED}$1${NC}"
|
|
}
|
|
|
|
# Print info message
|
|
print_info() {
|
|
echo -e " ${ARROW} $1"
|
|
}
|
|
|
|
# Print warning message
|
|
print_warning() {
|
|
echo -e " ${YELLOW}⚠${NC} $1"
|
|
}
|
|
|
|
# Run a command with spinner and capture output
|
|
run_with_spinner() {
|
|
local description="$1"
|
|
shift
|
|
local cmd="$@"
|
|
local log_file=$(mktemp)
|
|
local pid
|
|
local i=0
|
|
|
|
# Start command in background
|
|
eval "$cmd" > "$log_file" 2>&1 &
|
|
pid=$!
|
|
|
|
# Show spinner while command runs
|
|
printf " ${DIM}%s${NC} " "$description"
|
|
while kill -0 $pid 2>/dev/null; do
|
|
printf "\r ${CYAN}%s${NC} %s" "${SPINNER_CHARS:i++%${#SPINNER_CHARS}:1}" "$description"
|
|
sleep 0.1
|
|
done
|
|
|
|
# Get exit status
|
|
wait $pid
|
|
local exit_code=$?
|
|
|
|
# Clear spinner line and show result
|
|
printf "\r " # Clear the line
|
|
if [ $exit_code -eq 0 ]; then
|
|
echo -e "${CHECK} $description"
|
|
rm -f "$log_file"
|
|
return 0
|
|
else
|
|
echo -e "${CROSS} ${RED}$description${NC}"
|
|
echo -e " ${DIM}Log output:${NC}"
|
|
tail -20 "$log_file" | sed 's/^/ /'
|
|
rm -f "$log_file"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Run a command and show immediate output (for long operations)
|
|
run_with_output() {
|
|
local description="$1"
|
|
shift
|
|
local cmd="$@"
|
|
|
|
echo -e " ${ARROW} $description"
|
|
echo -e " ${DIM}─────────────────────────────────────────${NC}"
|
|
|
|
# Run command with indented output
|
|
if eval "$cmd" 2>&1 | sed 's/^/ /'; then
|
|
echo -e " ${DIM}─────────────────────────────────────────${NC}"
|
|
print_success "$description completed"
|
|
return 0
|
|
else
|
|
echo -e " ${DIM}─────────────────────────────────────────${NC}"
|
|
print_error "$description failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Show a progress bar (updates in place)
|
|
# Usage: show_progress_bar current total [description]
|
|
show_progress_bar() {
|
|
local current=$1
|
|
local total=$2
|
|
local description="${3:-}"
|
|
local width=30
|
|
local percent=$((current * 100 / total))
|
|
local filled=$((current * width / total))
|
|
local empty=$((width - filled))
|
|
|
|
# Build the bar
|
|
local bar=""
|
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
|
|
# Print with carriage return to update in place
|
|
printf "\r ${CYAN}[${bar}]${NC} ${percent}%% ${DIM}${description}${NC} "
|
|
}
|
|
|
|
# Run a long command with elapsed time display
|
|
run_with_elapsed_time() {
|
|
local description="$1"
|
|
shift
|
|
local cmd="$@"
|
|
local log_file=$(mktemp)
|
|
local pid
|
|
local start_time=$(date +%s)
|
|
|
|
# Start command in background
|
|
eval "$cmd" > "$log_file" 2>&1 &
|
|
pid=$!
|
|
|
|
# Show elapsed time while command runs
|
|
printf " ${ARROW} %s " "$description"
|
|
while kill -0 $pid 2>/dev/null; do
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
local mins=$((elapsed / 60))
|
|
local secs=$((elapsed % 60))
|
|
printf "\r ${CYAN}⏱${NC} %s ${DIM}(%dm %02ds)${NC} " "$description" $mins $secs
|
|
sleep 1
|
|
done
|
|
|
|
# Get exit status
|
|
wait $pid
|
|
local exit_code=$?
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
local mins=$((elapsed / 60))
|
|
local secs=$((elapsed % 60))
|
|
|
|
# Clear line and show result
|
|
printf "\r " # Clear
|
|
if [ $exit_code -eq 0 ]; then
|
|
echo -e "${CHECK} $description ${DIM}(${mins}m ${secs}s)${NC}"
|
|
rm -f "$log_file"
|
|
return 0
|
|
else
|
|
echo -e "${CROSS} ${RED}$description${NC} ${DIM}(${mins}m ${secs}s)${NC}"
|
|
echo -e " ${DIM}Log output:${NC}"
|
|
tail -20 "$log_file" | sed 's/^/ /'
|
|
rm -f "$log_file"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Run pip install with real progress bar
|
|
# Parses pip output to show download/install progress
|
|
run_pip_with_progress() {
|
|
local description="$1"
|
|
shift
|
|
local cmd="$@"
|
|
local log_file=$(mktemp)
|
|
local progress_file=$(mktemp)
|
|
local pid
|
|
local start_time=$(date +%s)
|
|
local width=30
|
|
|
|
# Start command in background, capturing output for parsing
|
|
eval "$cmd" 2>&1 | tee "$log_file" | while IFS= read -r line; do
|
|
# Look for pip progress indicators
|
|
if [[ "$line" =~ Downloading\ .*\ \(([0-9.]+)\ ([kMG]?B)\) ]]; then
|
|
echo "Downloading..." > "$progress_file"
|
|
elif [[ "$line" =~ Installing\ collected\ packages ]]; then
|
|
echo "Installing..." > "$progress_file"
|
|
elif [[ "$line" =~ Successfully\ installed ]]; then
|
|
echo "Done" > "$progress_file"
|
|
fi
|
|
done &
|
|
pid=$!
|
|
|
|
# Show progress while command runs
|
|
local phase="Starting"
|
|
printf " ${ARROW} %s " "$description"
|
|
while kill -0 $pid 2>/dev/null; do
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
local mins=$((elapsed / 60))
|
|
local secs=$((elapsed % 60))
|
|
|
|
# Read current phase if available
|
|
[ -f "$progress_file" ] && phase=$(cat "$progress_file" 2>/dev/null || echo "$phase")
|
|
|
|
# Build animated bar
|
|
local anim_pos=$(( (elapsed * 2) % width ))
|
|
local bar=""
|
|
for ((i=0; i<width; i++)); do
|
|
if [ $i -eq $anim_pos ] || [ $i -eq $((anim_pos + 1)) ]; then
|
|
bar+="█"
|
|
else
|
|
bar+="░"
|
|
fi
|
|
done
|
|
|
|
printf "\r ${CYAN}[${bar}]${NC} %s ${DIM}(%dm %02ds)${NC} " "$phase" $mins $secs
|
|
sleep 0.5
|
|
done
|
|
|
|
# Get exit status
|
|
wait $pid
|
|
local exit_code=$?
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
local mins=$((elapsed / 60))
|
|
local secs=$((elapsed % 60))
|
|
|
|
# Cleanup
|
|
rm -f "$progress_file"
|
|
|
|
# Clear line and show result
|
|
printf "\r%-80s\r" " " # Clear the line
|
|
if [ $exit_code -eq 0 ]; then
|
|
echo -e " ${CHECK} $description ${DIM}(${mins}m ${secs}s)${NC}"
|
|
rm -f "$log_file"
|
|
return 0
|
|
else
|
|
echo -e " ${CROSS} ${RED}$description${NC} ${DIM}(${mins}m ${secs}s)${NC}"
|
|
echo -e " ${DIM}Log output:${NC}"
|
|
tail -20 "$log_file" | sed 's/^/ /'
|
|
rm -f "$log_file"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Attempt cubic-in-out easing using bash integer math (approximation)
|
|
# Returns position 0-100 given input 0-100
|
|
cubic_ease_inout() {
|
|
local t=$1 # 0-100
|
|
if [ $t -lt 50 ]; then
|
|
# Ease in: 4 * t^3 (scaled)
|
|
echo $(( (4 * t * t * t) / 10000 ))
|
|
else
|
|
# Ease out: 1 - (-2t + 2)^3 / 2
|
|
local p=$((100 - t))
|
|
echo $(( 100 - (4 * p * p * p) / 10000 ))
|
|
fi
|
|
}
|
|
|
|
# Calculate velocity (derivative) of cubic ease-in-out at point t
|
|
# Returns 0-100 where 100 is max velocity (at t=50, the inflection point)
|
|
cubic_ease_velocity() {
|
|
local t=$1 # 0-100
|
|
# Derivative of cubic ease-in-out: 6t(1-t) scaled to 0-100
|
|
# Max velocity occurs at t=50 (middle of the curve)
|
|
# At t=0 or t=100, velocity is 0 (stationary at endpoints)
|
|
local velocity=$(( (6 * t * (100 - t)) / 100 ))
|
|
# Normalize to 0-100 range (max is 150 at t=50, so scale by 2/3)
|
|
echo $(( (velocity * 100) / 150 ))
|
|
}
|
|
|
|
# Run npm with animated progress bar
|
|
run_npm_with_progress() {
|
|
local description="$1"
|
|
shift
|
|
local cmd="$@"
|
|
local log_file=$(mktemp)
|
|
local pid
|
|
local start_time=$(date +%s)
|
|
local width=40
|
|
local cycle_frames=40 # frames per half-cycle (faster, smoother animation)
|
|
local cursor_width=2 # narrower cursor for higher fidelity
|
|
|
|
# Start command in background
|
|
eval "$cmd" > "$log_file" 2>&1 &
|
|
pid=$!
|
|
|
|
# Show animated progress bar while command runs
|
|
printf " ${ARROW} %s " "$description"
|
|
local frame=0
|
|
while kill -0 $pid 2>/dev/null; do
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
local mins=$((elapsed / 60))
|
|
local secs=$((elapsed % 60))
|
|
|
|
# Calculate position in cycle (0 to cycle_frames*2)
|
|
local cycle_pos=$(( frame % (cycle_frames * 2) ))
|
|
local going_right=1
|
|
[ $cycle_pos -ge $cycle_frames ] && going_right=0
|
|
|
|
# Get linear position within half-cycle (0-100)
|
|
local linear_t
|
|
if [ $going_right -eq 1 ]; then
|
|
linear_t=$(( (cycle_pos * 100) / cycle_frames ))
|
|
else
|
|
linear_t=$(( ((cycle_frames * 2 - cycle_pos) * 100) / cycle_frames ))
|
|
fi
|
|
|
|
# Apply cubic easing
|
|
local eased_t=$(cubic_ease_inout $linear_t)
|
|
|
|
# Calculate velocity for motion blur and glow effects
|
|
local velocity=$(cubic_ease_velocity $linear_t)
|
|
|
|
# Convert to bar position
|
|
local anim_pos=$(( (eased_t * (width - cursor_width)) / 100 ))
|
|
|
|
# Determine trail intensity based on velocity (motion blur when fast)
|
|
# velocity 0-60: no trail, 60-85: near trail, 85+: full trail
|
|
local show_near_trail=0
|
|
[ $velocity -gt 60 ] && show_near_trail=1
|
|
local show_far_trail=0
|
|
[ $velocity -gt 85 ] && show_far_trail=1
|
|
|
|
# Glow on cursor at apex velocity (tight window: 90-100%)
|
|
local cursor_glow=0
|
|
[ $velocity -gt 90 ] && cursor_glow=1
|
|
|
|
# Build bar with velocity-based effects
|
|
local bar=""
|
|
for ((j=0; j<width; j++)); do
|
|
local dist_from_cursor
|
|
if [ $j -lt $anim_pos ]; then
|
|
dist_from_cursor=$((anim_pos - j))
|
|
elif [ $j -ge $((anim_pos + cursor_width)) ]; then
|
|
dist_from_cursor=$((j - anim_pos - cursor_width + 1))
|
|
else
|
|
dist_from_cursor=0
|
|
fi
|
|
|
|
# Build character with motion blur effect
|
|
if [ $dist_from_cursor -eq 0 ]; then
|
|
# Solid cursor - glow at apex velocity (just turns white)
|
|
if [ $cursor_glow -eq 1 ]; then
|
|
bar+="${WHITE}█${CYAN}" # White cursor at peak velocity
|
|
else
|
|
bar+="█" # Normal cyan cursor
|
|
fi
|
|
elif [ $dist_from_cursor -eq 1 ] && [ $show_near_trail -eq 1 ]; then
|
|
bar+="▓" # Near motion blur - appears when moving
|
|
elif [ $dist_from_cursor -eq 2 ] && [ $show_far_trail -eq 1 ]; then
|
|
bar+="▒" # Far motion blur - only at high speed
|
|
else
|
|
bar+="░" # Empty background
|
|
fi
|
|
done
|
|
|
|
printf "\r ${CYAN}[${bar}]${NC} %s ${DIM}(%dm %02ds)${NC} " "$description" $mins $secs
|
|
sleep 0.033 # ~30fps for smoother animation
|
|
((frame++)) || true
|
|
done
|
|
|
|
# Get exit status
|
|
wait $pid
|
|
local exit_code=$?
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
local mins=$((elapsed / 60))
|
|
local secs=$((elapsed % 60))
|
|
|
|
# Clear line and show result
|
|
printf "\r%-80s\r" " " # Clear the line
|
|
if [ $exit_code -eq 0 ]; then
|
|
echo -e " ${CHECK} $description ${DIM}(${mins}m ${secs}s)${NC}"
|
|
rm -f "$log_file"
|
|
return 0
|
|
else
|
|
echo -e " ${CROSS} ${RED}$description${NC} ${DIM}(${mins}m ${secs}s)${NC}"
|
|
echo -e " ${DIM}Log output:${NC}"
|
|
tail -30 "$log_file" | sed 's/^/ /'
|
|
rm -f "$log_file"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Run git clone with real-time progress display
|
|
# Shows actual git progress (objects, files) as they're received
|
|
run_git_clone_with_progress() {
|
|
local branch="$1"
|
|
local repo_url="$2"
|
|
local target_dir="$3"
|
|
local start_time=$(date +%s)
|
|
|
|
echo -e " ${ARROW} Cloning from ${CYAN}github.com/rightup/pyMC_Repeater${NC}"
|
|
echo -e " ${DIM}────────────────────────────────────────${NC}"
|
|
|
|
# Run git clone with progress, parse and display key lines
|
|
git clone -b "$branch" --progress "$repo_url" "$target_dir" 2>&1 | while IFS= read -r line; do
|
|
# Parse git progress output
|
|
if [[ "$line" =~ ^Cloning ]]; then
|
|
printf "\r ${DIM}%-50s${NC}" "Initializing..."
|
|
elif [[ "$line" =~ ^remote:\ Enumerating ]]; then
|
|
printf "\r ${DIM}%-50s${NC}" "Enumerating objects..."
|
|
elif [[ "$line" =~ ^remote:\ Counting ]]; then
|
|
printf "\r ${DIM}%-50s${NC}" "Counting objects..."
|
|
elif [[ "$line" =~ ^remote:\ Compressing ]]; then
|
|
# Extract percentage if present
|
|
if [[ "$line" =~ ([0-9]+)% ]]; then
|
|
printf "\r ${CYAN}Compressing:${NC} ${BASH_REMATCH[1]}%%%-30s" " "
|
|
fi
|
|
elif [[ "$line" =~ ^Receiving\ objects ]]; then
|
|
# Extract percentage
|
|
if [[ "$line" =~ ([0-9]+)% ]]; then
|
|
printf "\r ${CYAN}Receiving:${NC} ${BASH_REMATCH[1]}%%%-30s" " "
|
|
fi
|
|
elif [[ "$line" =~ ^Resolving\ deltas ]]; then
|
|
# Extract percentage
|
|
if [[ "$line" =~ ([0-9]+)% ]]; then
|
|
printf "\r ${CYAN}Resolving:${NC} ${BASH_REMATCH[1]}%%%-30s" " "
|
|
fi
|
|
elif [[ "$line" =~ ^Updating\ files ]]; then
|
|
# Extract percentage
|
|
if [[ "$line" =~ ([0-9]+)% ]]; then
|
|
printf "\r ${CYAN}Extracting:${NC} ${BASH_REMATCH[1]}%%%-30s" " "
|
|
fi
|
|
fi
|
|
done
|
|
|
|
local exit_code=${PIPESTATUS[0]}
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
|
|
# Clear progress line
|
|
printf "\r%-60s\r" " "
|
|
echo -e " ${DIM}────────────────────────────────────────${NC}"
|
|
|
|
if [ $exit_code -eq 0 ]; then
|
|
print_success "Repository cloned ${DIM}(${elapsed}s)${NC}"
|
|
return 0
|
|
else
|
|
print_error "Clone failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Print installation banner
|
|
print_banner() {
|
|
clear
|
|
echo ""
|
|
echo -e "${BOLD}${CYAN}pyMC Console Installer${NC}"
|
|
echo -e "${DIM}React Dashboard + LoRa Mesh Network Repeater${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# Print completion summary
|
|
print_completion() {
|
|
local ip_address="$1"
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD}Installation Complete!${NC} ${CHECK}"
|
|
echo ""
|
|
|
|
# Version and branch summary
|
|
echo -e "${BOLD}Installed Versions:${NC}"
|
|
local core_ver=$(get_core_version)
|
|
local repeater_ver=$(get_repeater_version)
|
|
local console_ver=$(get_console_version)
|
|
local repeater_branch=$(get_repeater_branch)
|
|
local core_branch=$(get_core_branch_from_toml "$CLONE_DIR")
|
|
echo -e " ${DIM}pyMC Core:${NC} ${CYAN}v${core_ver}${NC} ${DIM}@${core_branch}${NC}"
|
|
echo -e " ${DIM}pyMC Repeater:${NC} ${CYAN}v${repeater_ver}${NC} ${DIM}@${repeater_branch}${NC}"
|
|
echo -e " ${DIM}pyMC Console:${NC} ${CYAN}${console_ver}${NC}"
|
|
echo ""
|
|
|
|
# Disk usage report
|
|
echo -e "${BOLD}Disk Usage:${NC}"
|
|
local install_size=$(du -sh "$REPEATER_DIR" 2>/dev/null | cut -f1 || echo "N/A")
|
|
local config_size=$(du -sh "$CONFIG_DIR" 2>/dev/null | cut -f1 || echo "N/A")
|
|
echo -e " ${DIM}Installation:${NC} $install_size"
|
|
echo -e " ${DIM}Configuration:${NC} $config_size"
|
|
echo ""
|
|
|
|
echo -e "${BOLD}Access your dashboard:${NC}"
|
|
echo -e " ${ARROW} Dashboard: ${CYAN}http://$ip_address:8000/${NC}"
|
|
echo -e " ${DIM}(API endpoints also available at /api/*)${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# Cleanup function for error handling
|
|
cleanup_on_error() {
|
|
echo ""
|
|
print_error "Installation failed!"
|
|
echo ""
|
|
echo -e " ${YELLOW}Partial installation may remain. To clean up:${NC}"
|
|
echo -e " ${DIM}sudo ./manage.sh uninstall${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Check the error messages above for details.${NC}"
|
|
echo -e " ${YELLOW}Common issues:${NC}"
|
|
echo -e " ${DIM}- Network connectivity problems${NC}"
|
|
echo -e " ${DIM}- Missing system dependencies${NC}"
|
|
echo -e " ${DIM}- Insufficient disk space${NC}"
|
|
echo -e " ${DIM}- Permission issues${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# TUI Setup
|
|
# ============================================================================
|
|
|
|
# Check if running in interactive terminal
|
|
check_terminal() {
|
|
if [ ! -t 0 ] || [ -z "$TERM" ]; then
|
|
echo "Error: This script requires an interactive terminal."
|
|
echo "Please run from SSH or a local terminal."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Setup dialog/whiptail
|
|
setup_dialog() {
|
|
if command -v whiptail &> /dev/null; then
|
|
DIALOG="whiptail"
|
|
elif command -v dialog &> /dev/null; then
|
|
DIALOG="dialog"
|
|
else
|
|
echo "TUI interface requires whiptail or dialog."
|
|
if [ "$EUID" -eq 0 ]; then
|
|
echo "Installing whiptail..."
|
|
apt-get update -qq && apt-get install -y whiptail
|
|
DIALOG="whiptail"
|
|
else
|
|
echo ""
|
|
echo "Please install whiptail: sudo apt-get install -y whiptail"
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Dialog Helper Functions
|
|
# ============================================================================
|
|
|
|
show_info() {
|
|
$DIALOG --backtitle "pyMC Console Management" --title "$1" --msgbox "$2" 14 70
|
|
}
|
|
|
|
show_error() {
|
|
$DIALOG --backtitle "pyMC Console Management" --title "Error" --msgbox "$1" 10 60
|
|
}
|
|
|
|
ask_yes_no() {
|
|
$DIALOG --backtitle "pyMC Console Management" --title "$1" --yesno "$2" 12 70
|
|
}
|
|
|
|
get_input() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
local default="$3"
|
|
$DIALOG --backtitle "pyMC Console Management" --title "$title" --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
# ============================================================================
|
|
# Status Check Functions
|
|
# ============================================================================
|
|
|
|
is_installed() {
|
|
[ -d "$REPEATER_DIR" ] && [ -f "$REPEATER_DIR/pyproject.toml" ]
|
|
}
|
|
|
|
backend_running() {
|
|
systemctl is-active "$BACKEND_SERVICE" >/dev/null 2>&1
|
|
}
|
|
|
|
# Get pymc_core branch/ref from a pyproject.toml file
|
|
# Usage: get_core_branch_from_toml [path_to_dir_with_toml]
|
|
# Returns: branch name (e.g., "feat/anon-req", "main") or "unknown"
|
|
get_core_branch_from_toml() {
|
|
local dir="${1:-$CLONE_DIR}"
|
|
local toml_file="$dir/pyproject.toml"
|
|
|
|
if [ ! -f "$toml_file" ]; then
|
|
echo "unknown"
|
|
return
|
|
fi
|
|
|
|
# Extract pymc_core git reference from pyproject.toml
|
|
# Format: "pymc_core[hardware] @ git+https://github.com/rightup/pyMC_core.git@feat/anon-req"
|
|
local branch
|
|
branch=$(grep -i 'pymc_core.*@.*git+' "$toml_file" 2>/dev/null | sed -n 's/.*\.git@\([^"]*\).*/\1/p' | head -1)
|
|
|
|
if [ -n "$branch" ]; then
|
|
echo "$branch"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
# Get pyMC_Repeater branch from clone directory
|
|
get_repeater_branch() {
|
|
if [ -d "$CLONE_DIR/.git" ]; then
|
|
cd "$CLONE_DIR" 2>/dev/null && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
# Get pyMC Repeater version from installed pyproject.toml
|
|
get_version() {
|
|
if [ -f "$REPEATER_DIR/pyproject.toml" ]; then
|
|
grep "^version" "$REPEATER_DIR/pyproject.toml" | cut -d'"' -f2 2>/dev/null || echo "unknown"
|
|
else
|
|
echo "not installed"
|
|
fi
|
|
}
|
|
|
|
# Get pyMC Repeater version (alias for clarity)
|
|
get_repeater_version() {
|
|
get_version
|
|
}
|
|
|
|
# Get pymc_core version from installed pip package
|
|
get_core_version() {
|
|
# Try to get version from pip
|
|
local version
|
|
version=$(pip3 show pymc_core 2>/dev/null | grep "^Version:" | awk '{print $2}')
|
|
if [ -n "$version" ]; then
|
|
echo "$version"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
# Get pyMC Console (UI) version from installed VERSION file or GitHub API
|
|
get_console_version() {
|
|
local ui_dir="$UI_DIR"
|
|
|
|
if [ -d "$ui_dir" ]; then
|
|
# First, try to read VERSION file (created during build)
|
|
if [ -f "$ui_dir/VERSION" ]; then
|
|
local ver=$(cat "$ui_dir/VERSION" 2>/dev/null | tr -d '[:space:]')
|
|
if [ -n "$ver" ]; then
|
|
echo "v$ver"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Fallback: check GitHub API for latest release
|
|
local latest_tag
|
|
latest_tag=$(curl -s --max-time 3 "https://api.github.com/repos/${UI_REPO}/releases/latest" 2>/dev/null | grep -oP '"tag_name":\s*"\K[^"]+' | head -1)
|
|
if [ -n "$latest_tag" ]; then
|
|
echo "$latest_tag"
|
|
else
|
|
echo "installed"
|
|
fi
|
|
else
|
|
echo "not installed"
|
|
fi
|
|
}
|
|
|
|
get_status_display() {
|
|
if ! is_installed; then
|
|
echo "Not Installed"
|
|
else
|
|
local version=$(get_version)
|
|
local status="Stopped"
|
|
|
|
backend_running && status="Running"
|
|
|
|
echo "v$version | Service: $status"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Install Function
|
|
# ============================================================================
|
|
|
|
do_install() {
|
|
# Check if already installed
|
|
if is_installed; then
|
|
show_error "pyMC Console is already installed!\n\npyMC_Repeater: $INSTALL_DIR\n\nUse 'upgrade' to update or 'uninstall' first."
|
|
return 1
|
|
fi
|
|
|
|
# Check root
|
|
if [ "$EUID" -ne 0 ]; then
|
|
show_error "Installation requires root privileges.\n\nPlease run: sudo $0 install"
|
|
return 1
|
|
fi
|
|
|
|
# Branch selection
|
|
local branch="${1:-}"
|
|
if [ -z "$branch" ]; then
|
|
branch=$($DIALOG --backtitle "pyMC Console Management" --title "Select Branch" --menu "\nSelect the branch to install from:" 14 60 4 \
|
|
"dev" "Development branch (recommended)" \
|
|
"main" "Stable release" \
|
|
"custom" "Enter custom branch name" 3>&1 1>&2 2>&3)
|
|
|
|
if [ -z "$branch" ]; then
|
|
return 0 # User cancelled
|
|
fi
|
|
|
|
if [ "$branch" = "custom" ]; then
|
|
branch=$(get_input "Custom Branch" "Enter the branch name:" "dev")
|
|
if [ -z "$branch" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Welcome screen
|
|
$DIALOG --backtitle "pyMC Console Management" --title "Welcome" --msgbox "\nWelcome to pyMC Console Setup\n\nThis will install:\n- pyMC Repeater (LoRa mesh repeater)\n- pyMC Console (React dashboard)\n\nBranch: $branch\nClone: $CLONE_DIR\nInstall: $INSTALL_DIR\n\nPress OK to continue..." 18 70
|
|
|
|
# SPI Check (Raspberry Pi)
|
|
check_spi
|
|
|
|
# Set up error handling
|
|
trap cleanup_on_error ERR
|
|
|
|
# Print banner
|
|
print_banner
|
|
echo -e " ${DIM}Branch: $branch${NC}"
|
|
echo -e " ${DIM}Clone: $CLONE_DIR${NC}"
|
|
echo -e " ${DIM}Install: $INSTALL_DIR${NC}"
|
|
|
|
local total_steps=6
|
|
|
|
# =========================================================================
|
|
# Step 1: Install prerequisites (whiptail needed by upstream)
|
|
# =========================================================================
|
|
print_step 1 $total_steps "Installing prerequisites"
|
|
|
|
run_with_spinner "Updating package lists" "apt-get update -qq" || {
|
|
print_error "Failed to update package lists"
|
|
return 1
|
|
}
|
|
|
|
# Install whiptail (needed by upstream) and git
|
|
run_with_spinner "Installing required packages" "apt-get install -y whiptail git curl" || {
|
|
print_error "Failed to install prerequisites"
|
|
return 1
|
|
}
|
|
|
|
# Install yq (we use it for config manipulation)
|
|
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
|
run_with_spinner "Installing yq" "install_yq_silent" || print_warning "yq installation failed (non-critical)"
|
|
else
|
|
print_success "yq already installed"
|
|
fi
|
|
|
|
# =========================================================================
|
|
# Step 2: Clone pyMC_Repeater
|
|
# =========================================================================
|
|
print_step 2 $total_steps "Cloning pyMC_Repeater@$branch"
|
|
|
|
# Remove existing clone if present (fresh install)
|
|
if [ -d "$CLONE_DIR" ]; then
|
|
print_info "Removing existing clone at $CLONE_DIR"
|
|
rm -rf "$CLONE_DIR"
|
|
fi
|
|
|
|
# Mark directories as safe for git (running as root on user-owned dir)
|
|
git config --global --add safe.directory "$CLONE_DIR" 2>/dev/null || true
|
|
git config --global --add safe.directory "$INSTALL_DIR" 2>/dev/null || true
|
|
|
|
run_git_clone_with_progress "$branch" "https://github.com/rightup/pyMC_Repeater.git" "$CLONE_DIR" || {
|
|
print_error "Failed to clone pyMC_Repeater"
|
|
print_info "Check if branch '$branch' exists"
|
|
return 1
|
|
}
|
|
|
|
# Show verified git info so user can confirm what was cloned
|
|
cd "$CLONE_DIR"
|
|
local git_branch=$(git rev-parse --abbrev-ref HEAD)
|
|
local git_commit=$(git rev-parse --short HEAD)
|
|
local git_date=$(git log -1 --format=%cd --date=short)
|
|
local git_msg=$(git log -1 --format=%s | cut -c1-50)
|
|
echo -e " ${BOLD}Source Verification${NC}"
|
|
echo -e " Branch: ${CYAN}${git_branch}${NC}"
|
|
echo -e " Commit: ${CYAN}${git_commit}${NC} ${DIM}(${git_date})${NC}"
|
|
echo -e " Message: ${DIM}${git_msg}...${NC}"
|
|
echo ""
|
|
|
|
# =========================================================================
|
|
# Step 3: Run upstream installer (via UPSTREAM INSTALLATION MANAGER)
|
|
# =========================================================================
|
|
print_step 3 $total_steps "Running pyMC_Repeater installer"
|
|
|
|
# This runs upstream's manage.sh install with our fake dialog to bypass TUI
|
|
# Upstream handles: user creation, directories, deps, pip install, service, config
|
|
run_upstream_installer "install" "$branch" || {
|
|
print_error "Upstream installation failed"
|
|
return 1
|
|
}
|
|
|
|
# =========================================================================
|
|
# Step 4: Apply patches to installed files
|
|
# =========================================================================
|
|
print_step 4 $total_steps "Applying pyMC Console patches"
|
|
|
|
# Apply patches to /opt/pymc_repeater (the installed location, not the clone)
|
|
print_info "Patching installed files..."
|
|
# PATCH 1 & 5 removed - merged upstream in PR #36 (feat/identity branch)
|
|
patch_logging_section "$INSTALL_DIR" # PATCH 2: Ensure logging section exists
|
|
patch_log_level_api "$INSTALL_DIR" # PATCH 3: Log level toggle API
|
|
patch_mesh_cli "$INSTALL_DIR" # PATCH 4: MeshCore CLI parity
|
|
patch_private_key_api "$INSTALL_DIR" # PATCH 6: Private key get/set API
|
|
|
|
# =========================================================================
|
|
# Step 5: Install dashboard and console extras
|
|
# =========================================================================
|
|
print_step 5 $total_steps "Installing pyMC Console dashboard"
|
|
|
|
# Create console directory for our extras
|
|
mkdir -p "$CONSOLE_DIR"
|
|
|
|
# Copy radio settings files to console dir
|
|
if [ -f "$CLONE_DIR/radio-settings.json" ]; then
|
|
cp "$CLONE_DIR/radio-settings.json" "$CONSOLE_DIR/"
|
|
print_success "Copied radio-settings.json"
|
|
fi
|
|
|
|
if [ -f "$CLONE_DIR/radio-presets.json" ]; then
|
|
cp "$CLONE_DIR/radio-presets.json" "$CONSOLE_DIR/"
|
|
print_success "Copied radio-presets.json"
|
|
fi
|
|
|
|
# Install our React dashboard (overlays upstream's Vue.js frontend)
|
|
install_static_frontend || {
|
|
print_error "Frontend installation failed"
|
|
return 1
|
|
}
|
|
|
|
# Fix permissions for console directory
|
|
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONSOLE_DIR" 2>/dev/null || true
|
|
|
|
# =========================================================================
|
|
# Step 6: Finalize installation
|
|
# =========================================================================
|
|
print_step 6 $total_steps "Finalizing installation"
|
|
|
|
# Stop service for now - we'll start it after user configures radio
|
|
# Upstream may have started it, so stop to avoid running with default config
|
|
systemctl stop "$BACKEND_SERVICE" 2>/dev/null || true
|
|
print_success "Installation files ready"
|
|
print_info "Service will start after radio configuration"
|
|
|
|
# Clear error trap
|
|
trap - ERR
|
|
|
|
# =========================================================================
|
|
# Radio Configuration (terminal-based)
|
|
# =========================================================================
|
|
echo ""
|
|
echo -e "${BOLD}${CYAN}Radio Configuration${NC}"
|
|
echo -e "${DIM}Configure your radio settings for your region and hardware${NC}"
|
|
echo ""
|
|
|
|
configure_radio_terminal
|
|
|
|
# NOW start the service with user's configuration
|
|
print_info "Starting service with your configuration..."
|
|
systemctl daemon-reload
|
|
systemctl start "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
if backend_running; then
|
|
print_success "Backend service running"
|
|
else
|
|
print_warning "Service may need GPIO configuration - use './manage.sh gpio'"
|
|
fi
|
|
|
|
# Show completion
|
|
local ip_address=$(hostname -I | awk '{print $1}')
|
|
print_completion "$ip_address"
|
|
|
|
echo -e "${BOLD}Manage your installation:${NC}"
|
|
echo -e " ${DIM}./manage.sh settings${NC} - Configure radio"
|
|
echo -e " ${DIM}./manage.sh gpio${NC} - Configure GPIO pins"
|
|
echo -e " ${DIM}./manage.sh${NC} - Full management menu"
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Upgrade Function
|
|
# ============================================================================
|
|
|
|
do_upgrade() {
|
|
if ! is_installed; then
|
|
show_error "pyMC Console is not installed!\n\nUse 'install' first."
|
|
return 1
|
|
fi
|
|
|
|
if [ "$EUID" -ne 0 ]; then
|
|
show_error "Upgrade requires root privileges.\n\nPlease run: sudo $0 upgrade"
|
|
return 1
|
|
fi
|
|
|
|
# Self-update: pull latest pymc_console repo first, then re-exec if updated
|
|
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
if [ -d "$script_dir/.git" ]; then
|
|
echo ""
|
|
print_info "Checking for pymc_console updates..."
|
|
git config --global --add safe.directory "$script_dir" 2>/dev/null || true
|
|
cd "$script_dir"
|
|
|
|
# Check if there are updates available
|
|
git fetch origin 2>/dev/null || true
|
|
local local_hash=$(git rev-parse HEAD 2>/dev/null)
|
|
local remote_hash=$(git rev-parse origin/main 2>/dev/null || git rev-parse origin/master 2>/dev/null)
|
|
|
|
if [ -n "$remote_hash" ] && [ "$local_hash" != "$remote_hash" ]; then
|
|
print_info "Updates available, pulling..."
|
|
# Try fast-forward first, fall back to reset if history diverged (e.g., after force-push)
|
|
if git pull --ff-only 2>/dev/null; then
|
|
print_success "pymc_console updated - restarting with new version..."
|
|
echo ""
|
|
exec "$script_dir/manage.sh" upgrade
|
|
elif git reset --hard "origin/main" 2>/dev/null || git reset --hard "origin/master" 2>/dev/null; then
|
|
print_success "pymc_console synced (history was rewritten) - restarting..."
|
|
echo ""
|
|
exec "$script_dir/manage.sh" upgrade
|
|
else
|
|
print_warning "Could not auto-update pymc_console (continuing with current version)"
|
|
print_info "You may need to manually run: cd $script_dir && git fetch && git reset --hard origin/main"
|
|
fi
|
|
else
|
|
print_success "pymc_console is up to date"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# Capture current versions BEFORE upgrade
|
|
local current_repeater_ver=$(get_repeater_version)
|
|
local current_core_ver=$(get_core_version)
|
|
local current_console_ver=$(get_console_version)
|
|
|
|
# Get current branch from clone directory or default to dev
|
|
local current_branch="dev"
|
|
if [ -d "$CLONE_DIR/.git" ]; then
|
|
cd "$CLONE_DIR" 2>/dev/null || true
|
|
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "dev")
|
|
fi
|
|
|
|
# Show current versions and upgrade type selection
|
|
local upgrade_type
|
|
upgrade_type=$($DIALOG --backtitle "pyMC Console Management" --title "Upgrade Options" --menu "
|
|
Current Installed Versions:
|
|
─────────────────────────────────────
|
|
pyMC Core: v${current_core_ver}
|
|
pyMC Repeater: v${current_repeater_ver}
|
|
pyMC Console: ${current_console_ver}
|
|
─────────────────────────────────────
|
|
|
|
Select upgrade type:" 19 65 2 \
|
|
"<Console>" "Console Only" \
|
|
"<Package>" "Full pyMC Stack" 3>&1 1>&2 2>&3)
|
|
|
|
if [ -z "$upgrade_type" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local branch="$current_branch"
|
|
local skip_backend=false
|
|
|
|
if [ "$upgrade_type" = "<Console>" ]; then
|
|
# Console-only upgrade
|
|
skip_backend=true
|
|
|
|
if ! ask_yes_no "Confirm Console Upgrade" "
|
|
This will ONLY update the pyMC Console dashboard.
|
|
|
|
pyMC Core and pyMC Repeater will NOT be modified.
|
|
|
|
Current Console: ${current_console_ver}
|
|
New Console: Latest from GitHub
|
|
|
|
Continue?"; then
|
|
return 0
|
|
fi
|
|
else
|
|
# Full upgrade - select branch
|
|
branch=$($DIALOG --backtitle "pyMC Console Management" --title "Select Branch" --menu "
|
|
Full upgrade will update:
|
|
• pyMC Core (mesh library)
|
|
• pyMC Repeater (backend)
|
|
• pyMC Console (dashboard)
|
|
|
|
Current branch: $current_branch
|
|
|
|
Select the branch for pyMC Repeater:" 18 60 4 \
|
|
"dev" "Development branch (recommended)" \
|
|
"main" "Stable release" \
|
|
"keep" "Keep current branch ($current_branch)" \
|
|
"custom" "Enter custom branch name" 3>&1 1>&2 2>&3)
|
|
|
|
if [ -z "$branch" ]; then
|
|
return 0 # User cancelled
|
|
fi
|
|
|
|
if [ "$branch" = "keep" ]; then
|
|
branch="$current_branch"
|
|
elif [ "$branch" = "custom" ]; then
|
|
branch=$(get_input "Custom Branch" "Enter the branch name:" "$current_branch")
|
|
if [ -z "$branch" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if ! ask_yes_no "Confirm Full Upgrade" "
|
|
This will update ALL components:
|
|
|
|
pyMC Core: v${current_core_ver} → (via pip)
|
|
pyMC Repeater: v${current_repeater_ver} → $branch branch
|
|
pyMC Console: ${current_console_ver} → Latest
|
|
|
|
Your configuration will be preserved.
|
|
|
|
Continue?"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Print banner
|
|
print_banner
|
|
if [ "$skip_backend" = true ]; then
|
|
echo -e " ${DIM}Upgrade type: Console Only${NC}"
|
|
else
|
|
echo -e " ${DIM}Upgrade type: Full (Core + Repeater + Console)${NC}"
|
|
echo -e " ${DIM}Target branch: $branch${NC}"
|
|
fi
|
|
echo ""
|
|
echo -e " ${BOLD}Current Versions:${NC}"
|
|
echo -e " ${DIM}pyMC Core:${NC} v${current_core_ver}"
|
|
echo -e " ${DIM}pyMC Repeater:${NC} v${current_repeater_ver}"
|
|
echo -e " ${DIM}pyMC Console:${NC} ${current_console_ver}"
|
|
|
|
local total_steps
|
|
if [ "$skip_backend" = true ]; then
|
|
total_steps=3
|
|
else
|
|
total_steps=5
|
|
fi
|
|
|
|
local step_num=0
|
|
|
|
# =========================================================================
|
|
# Step 1: Backup configuration (both paths)
|
|
# =========================================================================
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Backing up configuration"
|
|
local backup_file="$CONFIG_DIR/config.yaml.backup.$(date +%Y%m%d_%H%M%S)"
|
|
if [ -f "$CONFIG_DIR/config.yaml" ]; then
|
|
cp "$CONFIG_DIR/config.yaml" "$backup_file"
|
|
print_success "Backup saved to: $backup_file"
|
|
else
|
|
print_info "No existing config to backup"
|
|
fi
|
|
|
|
# =========================================================================
|
|
# CONSOLE-ONLY PATH: Skip backend, just update dashboard
|
|
# =========================================================================
|
|
if [ "$skip_backend" = true ]; then
|
|
# Step 2: Update dashboard only
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Updating pyMC Console dashboard"
|
|
install_static_frontend || {
|
|
print_error "Dashboard update failed"
|
|
return 1
|
|
}
|
|
|
|
# Step 3: Restart service
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Restarting service"
|
|
systemctl restart "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
if backend_running; then
|
|
print_success "Service running"
|
|
else
|
|
print_warning "Service may need configuration"
|
|
fi
|
|
|
|
# Show completion (console only)
|
|
local new_console_ver=$(get_console_version)
|
|
local ip_address=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
|
|
|
|
echo ""
|
|
echo -e "${BOLD}${GREEN}════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD}${GREEN} Console Upgrade Complete!${NC}"
|
|
echo -e "${BOLD}${GREEN}════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
# Get branch info
|
|
local repeater_branch=$(get_repeater_branch)
|
|
local core_branch=$(get_core_branch_from_toml "$CLONE_DIR")
|
|
|
|
echo -e " ${BOLD}Versions:${NC}"
|
|
echo -e " ${DIM}pyMC Core:${NC} v${current_core_ver} ${DIM}(unchanged)${NC} ${DIM}@${core_branch}${NC}"
|
|
echo -e " ${DIM}pyMC Repeater:${NC} v${current_repeater_ver} ${DIM}(unchanged)${NC} ${DIM}@${repeater_branch}${NC}"
|
|
echo -e " ${CHECK} pyMC Console: ${DIM}${current_console_ver}${NC} → ${CYAN}${new_console_ver}${NC}"
|
|
echo ""
|
|
echo -e " ${CHECK} Configuration preserved"
|
|
echo -e " ${CHECK} Dashboard: ${CYAN}http://$ip_address:8000${NC}"
|
|
echo ""
|
|
return 0
|
|
fi
|
|
|
|
# =========================================================================
|
|
# FULL UPGRADE PATH: Update Repeater, Core, and Console
|
|
# =========================================================================
|
|
|
|
# Step 2: Update pyMC_Repeater clone
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Updating pyMC_Repeater@$branch"
|
|
|
|
# Mark directories as safe for git (running as root on user-owned dir)
|
|
git config --global --add safe.directory "$CLONE_DIR" 2>/dev/null || true
|
|
git config --global --add safe.directory "$INSTALL_DIR" 2>/dev/null || true
|
|
|
|
# If clone doesn't exist, clone fresh
|
|
if [ ! -d "$CLONE_DIR/.git" ]; then
|
|
print_info "Clone not found, creating fresh clone..."
|
|
rm -rf "$CLONE_DIR" 2>/dev/null || true
|
|
run_git_clone_with_progress "$branch" "https://github.com/rightup/pyMC_Repeater.git" "$CLONE_DIR" || {
|
|
print_error "Failed to clone pyMC_Repeater"
|
|
return 1
|
|
}
|
|
else
|
|
cd "$CLONE_DIR"
|
|
|
|
run_with_spinner "Fetching updates" "git fetch origin" || {
|
|
print_error "Failed to fetch updates"
|
|
return 1
|
|
}
|
|
|
|
# Reset any local changes (from previous patches)
|
|
git reset --hard HEAD 2>/dev/null || true
|
|
git clean -fd 2>/dev/null || true
|
|
|
|
git checkout "$branch" 2>/dev/null || git checkout -b "$branch" "origin/$branch" 2>/dev/null
|
|
|
|
run_with_spinner "Pulling latest changes" "git pull origin $branch" || {
|
|
print_error "Failed to pull branch $branch"
|
|
return 1
|
|
}
|
|
print_success "Repository updated"
|
|
fi
|
|
|
|
# Show verified git info so user can confirm what was pulled
|
|
cd "$CLONE_DIR"
|
|
local git_branch=$(git rev-parse --abbrev-ref HEAD)
|
|
local git_commit=$(git rev-parse --short HEAD)
|
|
local git_date=$(git log -1 --format=%cd --date=short)
|
|
local git_msg=$(git log -1 --format=%s | cut -c1-50)
|
|
echo -e " ${BOLD}Source Verification${NC}"
|
|
echo -e " Branch: ${CYAN}${git_branch}${NC}"
|
|
echo -e " Commit: ${CYAN}${git_commit}${NC} ${DIM}(${git_date})${NC}"
|
|
echo -e " Message: ${DIM}${git_msg}...${NC}"
|
|
echo ""
|
|
|
|
# Step 3: Run upstream upgrade (Repeater + Core via pip)
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Running pyMC_Repeater upgrade (includes pyMC Core)"
|
|
|
|
# This runs upstream's manage.sh upgrade with our fake dialog to bypass TUI
|
|
# Upstream handles: stopping service, updating files, pip install, config merge, starting service
|
|
run_upstream_installer "upgrade" "$branch" || {
|
|
print_error "Upstream upgrade failed"
|
|
return 1
|
|
}
|
|
|
|
# Step 4: Apply patches and update dashboard
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Applying pyMC Console patches & dashboard"
|
|
|
|
# Apply patches to /opt/pymc_repeater (the installed location)
|
|
print_info "Patching installed files..."
|
|
patch_api_endpoints "$INSTALL_DIR" # PATCH 1: Radio config API endpoint
|
|
patch_logging_section "$INSTALL_DIR" # PATCH 2: Ensure logging section exists
|
|
patch_log_level_api "$INSTALL_DIR" # PATCH 3: Log level toggle API
|
|
patch_mesh_cli "$INSTALL_DIR" # PATCH 4: MeshCore CLI parity
|
|
patch_stats_api "$INSTALL_DIR" # PATCH 5: Extend stats API with MeshCore config
|
|
patch_private_key_api "$INSTALL_DIR" # PATCH 6: Private key get/set API
|
|
|
|
# Ensure --log-level DEBUG
|
|
if [ -f /etc/systemd/system/pymc-repeater.service ]; then
|
|
if ! grep -q '\-\-log-level DEBUG' /etc/systemd/system/pymc-repeater.service; then
|
|
sed -i 's|--config /etc/pymc_repeater/config.yaml$|--config /etc/pymc_repeater/config.yaml --log-level DEBUG|' \
|
|
/etc/systemd/system/pymc-repeater.service
|
|
systemctl daemon-reload
|
|
print_success "Added --log-level DEBUG for RX timing fix"
|
|
fi
|
|
fi
|
|
|
|
# Update dashboard from GitHub Releases
|
|
install_static_frontend || {
|
|
print_warning "Dashboard update failed - service will continue with existing UI"
|
|
}
|
|
|
|
# Step 5: Restart service with patches
|
|
((step_num++)) || true
|
|
print_step $step_num $total_steps "Restarting service"
|
|
|
|
systemctl restart "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
if backend_running; then
|
|
print_success "Service running"
|
|
else
|
|
print_warning "Service may need configuration"
|
|
fi
|
|
|
|
# Show completion with version details (all components)
|
|
local new_repeater_ver=$(get_repeater_version)
|
|
local new_core_ver=$(get_core_version)
|
|
local new_console_ver=$(get_console_version)
|
|
local ip_address=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
|
|
|
|
echo ""
|
|
echo -e "${BOLD}${GREEN}════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD}${GREEN} Full Upgrade Complete!${NC}"
|
|
echo -e "${BOLD}${GREEN}════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
# Get branch info from the updated clone
|
|
local core_branch=$(get_core_branch_from_toml "$CLONE_DIR")
|
|
|
|
echo -e " ${BOLD}Versions:${NC}"
|
|
echo -e " ${CHECK} pyMC Core: ${DIM}v${current_core_ver}${NC} → ${CYAN}v${new_core_ver}${NC} ${DIM}@${core_branch}${NC}"
|
|
echo -e " ${CHECK} pyMC Repeater: ${DIM}v${current_repeater_ver}${NC} → ${CYAN}v${new_repeater_ver}${NC} ${DIM}@${branch}${NC}"
|
|
echo -e " ${CHECK} pyMC Console: ${DIM}${current_console_ver}${NC} → ${CYAN}${new_console_ver}${NC}"
|
|
echo ""
|
|
echo -e " ${CHECK} Configuration preserved"
|
|
echo -e " ${CHECK} Dashboard: ${CYAN}http://$ip_address:8000${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Terminal-based Radio Configuration (for install flow)
|
|
# ============================================================================
|
|
|
|
configure_radio_terminal() {
|
|
local config_file="$CONFIG_DIR/config.yaml"
|
|
|
|
if [ ! -f "$config_file" ]; then
|
|
print_warning "Config file not found, skipping radio configuration"
|
|
return 0
|
|
fi
|
|
|
|
# Node name
|
|
local current_name=$(yq '.repeater.node_name' "$config_file" 2>/dev/null || echo "mesh-repeater")
|
|
local random_suffix=$(printf "%04d" $((RANDOM % 10000)))
|
|
local default_name="pyRpt${random_suffix}"
|
|
|
|
if [ "$current_name" = "mesh-repeater-01" ] || [ "$current_name" = "mesh-repeater" ]; then
|
|
current_name="$default_name"
|
|
fi
|
|
|
|
echo -e " ${BOLD}Node Name${NC}"
|
|
read -p " Enter repeater name [$current_name]: " node_name
|
|
node_name=${node_name:-$current_name}
|
|
yq -i ".repeater.node_name = \"$node_name\"" "$config_file"
|
|
print_success "Node name: $node_name"
|
|
echo ""
|
|
|
|
# Radio preset selection
|
|
echo -e " ${BOLD}Radio Preset${NC}"
|
|
echo -e " ${DIM}Select a preset or choose custom to enter manual values${NC}"
|
|
echo ""
|
|
|
|
# Fetch presets from API or local files
|
|
local presets_json=""
|
|
presets_json=$(curl -s --max-time 5 https://api.meshcore.nz/api/v1/config 2>/dev/null)
|
|
|
|
if [ -z "$presets_json" ]; then
|
|
if [ -f "$CONSOLE_DIR/radio-presets.json" ]; then
|
|
presets_json=$(cat "$CONSOLE_DIR/radio-presets.json")
|
|
elif [ -f "$REPEATER_DIR/radio-presets.json" ]; then
|
|
presets_json=$(cat "$REPEATER_DIR/radio-presets.json")
|
|
fi
|
|
fi
|
|
|
|
local preset_count=0
|
|
local preset_titles=()
|
|
local preset_freqs=()
|
|
local preset_sfs=()
|
|
local preset_bws=()
|
|
local preset_crs=()
|
|
|
|
if [ -n "$presets_json" ]; then
|
|
while IFS= read -r line; do
|
|
local title=$(echo "$line" | jq -r '.title')
|
|
local freq=$(echo "$line" | jq -r '.frequency')
|
|
local sf=$(echo "$line" | jq -r '.spreading_factor')
|
|
local bw=$(echo "$line" | jq -r '.bandwidth')
|
|
local cr=$(echo "$line" | jq -r '.coding_rate')
|
|
|
|
if [ -n "$title" ] && [ "$title" != "null" ]; then
|
|
((preset_count++)) || true
|
|
preset_titles+=("$title")
|
|
preset_freqs+=("$freq")
|
|
preset_sfs+=("$sf")
|
|
preset_bws+=("$bw")
|
|
preset_crs+=("$cr")
|
|
echo -e " ${CYAN}$preset_count)${NC} $title ${DIM}(${freq}MHz SF$sf BW${bw}kHz)${NC}"
|
|
fi
|
|
done < <(echo "$presets_json" | jq -c '.[]' 2>/dev/null)
|
|
fi
|
|
|
|
# If no presets loaded, show fallback options with descriptions
|
|
if [ $preset_count -eq 0 ]; then
|
|
echo -e " ${YELLOW}Could not fetch presets from API. Showing common options:${NC}"
|
|
echo ""
|
|
# Fallback presets - matches upstream api.meshcore.nz/api/v1/config + WestCoastMesh
|
|
preset_titles=("USA/Canada (Recommended)" "Australia" "EU/UK (Long Range)" "EU/UK (Narrow)" "New Zealand" "New Zealand (Narrow)" "WestCoastMesh US")
|
|
preset_freqs=("910.525" "915.800" "869.525" "869.618" "917.375" "917.375" "927.875")
|
|
preset_sfs=("7" "10" "11" "8" "11" "7" "7")
|
|
preset_bws=("62.5" "250" "250" "62.5" "250" "62.5" "62.5")
|
|
preset_crs=("5" "5" "5" "8" "5" "5" "5")
|
|
preset_count=${#preset_titles[@]}
|
|
|
|
echo -e " ${CYAN}1)${NC} USA/Canada ${DIM}(910.525MHz SF7 BW62.5kHz CR5 - Recommended)${NC}"
|
|
echo -e " ${CYAN}2)${NC} Australia ${DIM}(915.800MHz SF10 BW250kHz CR5)${NC}"
|
|
echo -e " ${CYAN}3)${NC} EU/UK Long Range ${DIM}(869.525MHz SF11 BW250kHz CR5)${NC}"
|
|
echo -e " ${CYAN}4)${NC} EU/UK Narrow ${DIM}(869.618MHz SF8 BW62.5kHz CR8)${NC}"
|
|
echo -e " ${CYAN}5)${NC} New Zealand ${DIM}(917.375MHz SF11 BW250kHz CR5)${NC}"
|
|
echo -e " ${CYAN}6)${NC} New Zealand Narrow ${DIM}(917.375MHz SF7 BW62.5kHz CR5)${NC}"
|
|
echo -e " ${CYAN}7)${NC} WestCoastMesh US ${DIM}(927.875MHz SF7 BW62.5kHz CR5 - SoCal optimized)${NC}"
|
|
fi
|
|
|
|
echo -e " ${CYAN}C)${NC} Custom ${DIM}(enter values manually)${NC}"
|
|
echo ""
|
|
|
|
read -p " Select preset [1-$preset_count] or C for custom: " preset_choice
|
|
|
|
local freq_mhz bw_khz sf cr
|
|
|
|
if [[ "$preset_choice" =~ ^[Cc]$ ]]; then
|
|
# Custom values
|
|
echo ""
|
|
echo -e " ${BOLD}Custom Radio Settings${NC}"
|
|
|
|
local current_freq=$(yq '.radio.frequency' "$config_file" 2>/dev/null || echo "869618000")
|
|
local current_freq_mhz=$(awk "BEGIN {printf \"%.3f\", $current_freq / 1000000}")
|
|
read -p " Frequency in MHz [$current_freq_mhz]: " freq_mhz
|
|
freq_mhz=${freq_mhz:-$current_freq_mhz}
|
|
|
|
local current_sf=$(yq '.radio.spreading_factor' "$config_file" 2>/dev/null || echo "8")
|
|
read -p " Spreading Factor (7-12) [$current_sf]: " sf
|
|
sf=${sf:-$current_sf}
|
|
|
|
local current_bw=$(yq '.radio.bandwidth' "$config_file" 2>/dev/null || echo "62500")
|
|
local current_bw_khz=$(awk "BEGIN {printf \"%.1f\", $current_bw / 1000}")
|
|
read -p " Bandwidth in kHz [$current_bw_khz]: " bw_khz
|
|
bw_khz=${bw_khz:-$current_bw_khz}
|
|
|
|
local current_cr=$(yq '.radio.coding_rate' "$config_file" 2>/dev/null || echo "8")
|
|
read -p " Coding Rate (5-8) [$current_cr]: " cr
|
|
cr=${cr:-$current_cr}
|
|
|
|
# Apply custom settings
|
|
local freq_hz=$(awk "BEGIN {printf \"%.0f\", $freq_mhz * 1000000}")
|
|
local bw_hz=$(awk "BEGIN {printf \"%.0f\", $bw_khz * 1000}")
|
|
|
|
yq -i ".radio.frequency = $freq_hz" "$config_file"
|
|
yq -i ".radio.spreading_factor = $sf" "$config_file"
|
|
yq -i ".radio.bandwidth = $bw_hz" "$config_file"
|
|
yq -i ".radio.coding_rate = $cr" "$config_file"
|
|
|
|
echo ""
|
|
print_success "Radio: ${freq_mhz}MHz SF$sf BW${bw_khz}kHz CR$cr"
|
|
elif [[ "$preset_choice" =~ ^[0-9]+$ ]] && [ "$preset_choice" -ge 1 ] && [ "$preset_choice" -le "$preset_count" ]; then
|
|
# Use preset
|
|
local idx=$((preset_choice - 1))
|
|
freq_mhz="${preset_freqs[$idx]}"
|
|
sf="${preset_sfs[$idx]}"
|
|
bw_khz="${preset_bws[$idx]}"
|
|
cr="${preset_crs[$idx]}"
|
|
print_success "Using preset: ${preset_titles[$idx]}"
|
|
|
|
# Apply settings
|
|
local freq_hz=$(awk "BEGIN {printf \"%.0f\", $freq_mhz * 1000000}")
|
|
local bw_hz=$(awk "BEGIN {printf \"%.0f\", $bw_khz * 1000}")
|
|
|
|
yq -i ".radio.frequency = $freq_hz" "$config_file"
|
|
yq -i ".radio.spreading_factor = $sf" "$config_file"
|
|
yq -i ".radio.bandwidth = $bw_hz" "$config_file"
|
|
yq -i ".radio.coding_rate = $cr" "$config_file"
|
|
|
|
echo ""
|
|
print_success "Radio: ${freq_mhz}MHz SF$sf BW${bw_khz}kHz CR$cr"
|
|
else
|
|
print_warning "Invalid selection, keeping current radio settings"
|
|
fi
|
|
|
|
# Hardware selection (before TX power so user can override hardware default)
|
|
echo ""
|
|
echo -e " ${BOLD}Hardware Selection${NC}"
|
|
echo -e " ${DIM}Select your LoRa hardware for GPIO configuration${NC}"
|
|
echo ""
|
|
|
|
configure_hardware_terminal "$config_file"
|
|
|
|
# TX Power (after hardware selection so user's choice takes precedence)
|
|
echo -e " ${BOLD}TX Power${NC}"
|
|
local current_power=$(yq '.radio.tx_power' "$config_file" 2>/dev/null || echo "22")
|
|
read -p " TX Power in dBm [$current_power]: " tx_power
|
|
tx_power=${tx_power:-$current_power}
|
|
yq -i ".radio.tx_power = $tx_power" "$config_file"
|
|
print_success "TX Power: ${tx_power}dBm"
|
|
echo ""
|
|
}
|
|
|
|
# Terminal-based hardware/GPIO configuration
|
|
configure_hardware_terminal() {
|
|
local config_file="${1:-$CONFIG_DIR/config.yaml}"
|
|
local hw_config=""
|
|
|
|
# Find hardware presets file
|
|
if [ -f "$CONSOLE_DIR/radio-settings.json" ]; then
|
|
hw_config="$CONSOLE_DIR/radio-settings.json"
|
|
elif [ -f "$REPEATER_DIR/radio-settings.json" ]; then
|
|
hw_config="$REPEATER_DIR/radio-settings.json"
|
|
fi
|
|
|
|
if [ -z "$hw_config" ] || [ ! -f "$hw_config" ]; then
|
|
print_warning "Hardware presets not found, skipping GPIO configuration"
|
|
print_info "Configure GPIO manually with: ./manage.sh gpio"
|
|
return 0
|
|
fi
|
|
|
|
# Build hardware options
|
|
local hw_count=0
|
|
local hw_keys=()
|
|
local hw_names=()
|
|
|
|
while IFS= read -r key; do
|
|
local name=$(jq -r ".hardware.\"$key\".name" "$hw_config" 2>/dev/null)
|
|
if [ -n "$name" ] && [ "$name" != "null" ]; then
|
|
((hw_count++)) || true
|
|
hw_keys+=("$key")
|
|
hw_names+=("$name")
|
|
echo -e " ${CYAN}$hw_count)${NC} $name"
|
|
fi
|
|
done < <(jq -r '.hardware | keys[]' "$hw_config" 2>/dev/null)
|
|
|
|
echo -e " ${CYAN}C)${NC} Custom GPIO ${DIM}(enter pins manually)${NC}"
|
|
echo ""
|
|
|
|
read -p " Select hardware [1-$hw_count] or C for custom: " hw_choice
|
|
|
|
if [[ "$hw_choice" =~ ^[Cc]$ ]]; then
|
|
# Custom GPIO
|
|
echo ""
|
|
echo -e " ${BOLD}Custom GPIO Configuration${NC} ${YELLOW}(BCM pin numbering)${NC}"
|
|
|
|
local current_cs=$(yq '.sx1262.cs_pin' "$config_file" 2>/dev/null || echo "21")
|
|
read -p " Chip Select pin [$current_cs]: " cs_pin
|
|
cs_pin=${cs_pin:-$current_cs}
|
|
|
|
local current_reset=$(yq '.sx1262.reset_pin' "$config_file" 2>/dev/null || echo "18")
|
|
read -p " Reset pin [$current_reset]: " reset_pin
|
|
reset_pin=${reset_pin:-$current_reset}
|
|
|
|
local current_busy=$(yq '.sx1262.busy_pin' "$config_file" 2>/dev/null || echo "20")
|
|
read -p " Busy pin [$current_busy]: " busy_pin
|
|
busy_pin=${busy_pin:-$current_busy}
|
|
|
|
local current_irq=$(yq '.sx1262.irq_pin' "$config_file" 2>/dev/null || echo "16")
|
|
read -p " IRQ pin [$current_irq]: " irq_pin
|
|
irq_pin=${irq_pin:-$current_irq}
|
|
|
|
local current_txen=$(yq '.sx1262.txen_pin' "$config_file" 2>/dev/null || echo "-1")
|
|
read -p " TX Enable pin (-1 to disable) [$current_txen]: " txen_pin
|
|
txen_pin=${txen_pin:-$current_txen}
|
|
|
|
local current_rxen=$(yq '.sx1262.rxen_pin' "$config_file" 2>/dev/null || echo "-1")
|
|
read -p " RX Enable pin (-1 to disable) [$current_rxen]: " rxen_pin
|
|
rxen_pin=${rxen_pin:-$current_rxen}
|
|
|
|
# Apply custom GPIO
|
|
yq -i ".sx1262.cs_pin = $cs_pin" "$config_file"
|
|
yq -i ".sx1262.reset_pin = $reset_pin" "$config_file"
|
|
yq -i ".sx1262.busy_pin = $busy_pin" "$config_file"
|
|
yq -i ".sx1262.irq_pin = $irq_pin" "$config_file"
|
|
yq -i ".sx1262.txen_pin = $txen_pin" "$config_file"
|
|
yq -i ".sx1262.rxen_pin = $rxen_pin" "$config_file"
|
|
|
|
echo ""
|
|
print_success "Custom GPIO: CS=$cs_pin RST=$reset_pin BUSY=$busy_pin IRQ=$irq_pin"
|
|
|
|
elif [[ "$hw_choice" =~ ^[0-9]+$ ]] && [ "$hw_choice" -ge 1 ] && [ "$hw_choice" -le "$hw_count" ]; then
|
|
# Use preset
|
|
local idx=$((hw_choice - 1))
|
|
local hw_key="${hw_keys[$idx]}"
|
|
local hw_name="${hw_names[$idx]}"
|
|
local preset=$(jq ".hardware.\"$hw_key\"" "$hw_config" 2>/dev/null)
|
|
|
|
if [ -n "$preset" ] && [ "$preset" != "null" ]; then
|
|
# Extract all GPIO settings
|
|
local bus_id=$(echo "$preset" | jq -r '.bus_id // 0')
|
|
local cs_id=$(echo "$preset" | jq -r '.cs_id // 0')
|
|
local cs_pin=$(echo "$preset" | jq -r '.cs_pin // 21')
|
|
local reset_pin=$(echo "$preset" | jq -r '.reset_pin // 18')
|
|
local busy_pin=$(echo "$preset" | jq -r '.busy_pin // 20')
|
|
local irq_pin=$(echo "$preset" | jq -r '.irq_pin // 16')
|
|
local txen_pin=$(echo "$preset" | jq -r '.txen_pin // -1')
|
|
local rxen_pin=$(echo "$preset" | jq -r '.rxen_pin // -1')
|
|
local is_waveshare=$(echo "$preset" | jq -r '.is_waveshare // false')
|
|
local use_dio3_tcxo=$(echo "$preset" | jq -r '.use_dio3_tcxo // false')
|
|
local tx_power=$(echo "$preset" | jq -r '.tx_power // 22')
|
|
local preamble_length=$(echo "$preset" | jq -r '.preamble_length // 17')
|
|
|
|
# Apply to config
|
|
yq -i ".sx1262.bus_id = $bus_id" "$config_file"
|
|
yq -i ".sx1262.cs_id = $cs_id" "$config_file"
|
|
yq -i ".sx1262.cs_pin = $cs_pin" "$config_file"
|
|
yq -i ".sx1262.reset_pin = $reset_pin" "$config_file"
|
|
yq -i ".sx1262.busy_pin = $busy_pin" "$config_file"
|
|
yq -i ".sx1262.irq_pin = $irq_pin" "$config_file"
|
|
yq -i ".sx1262.txen_pin = $txen_pin" "$config_file"
|
|
yq -i ".sx1262.rxen_pin = $rxen_pin" "$config_file"
|
|
yq -i ".sx1262.is_waveshare = $is_waveshare" "$config_file"
|
|
yq -i ".sx1262.use_dio3_tcxo = $use_dio3_tcxo" "$config_file"
|
|
# Note: tx_power is set as default but user can override in next step
|
|
yq -i ".radio.tx_power = $tx_power" "$config_file"
|
|
yq -i ".radio.preamble_length = $preamble_length" "$config_file"
|
|
|
|
echo ""
|
|
print_success "Hardware: $hw_name"
|
|
print_success "GPIO: CS=$cs_pin RST=$reset_pin BUSY=$busy_pin IRQ=$irq_pin"
|
|
if [ "$txen_pin" != "-1" ]; then
|
|
print_info "TX/RX Enable: TXEN=$txen_pin RXEN=$rxen_pin"
|
|
fi
|
|
print_info "Default TX Power: ${tx_power}dBm (you can change this next)"
|
|
fi
|
|
else
|
|
print_warning "Invalid selection, keeping current GPIO settings"
|
|
print_info "Configure GPIO later with: ./manage.sh gpio"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Settings Function (Radio Configuration) - TUI version for manage.sh menu
|
|
# ============================================================================
|
|
|
|
do_settings() {
|
|
if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
|
|
show_error "Configuration file not found!\n\nPlease install pyMC Console first."
|
|
return 1
|
|
fi
|
|
|
|
while true; do
|
|
local current_name=$(yq '.repeater.node_name' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "unknown")
|
|
local current_freq=$(yq '.radio.frequency' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "0")
|
|
local current_freq_mhz=$(awk "BEGIN {printf \"%.3f\", $current_freq / 1000000}")
|
|
local current_sf=$(yq '.radio.spreading_factor' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "0")
|
|
local current_bw=$(yq '.radio.bandwidth' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "0")
|
|
local current_bw_khz=$(awk "BEGIN {printf \"%.1f\", $current_bw / 1000}")
|
|
local current_power=$(yq '.radio.tx_power' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "0")
|
|
|
|
CHOICE=$($DIALOG --backtitle "pyMC Console Management" --title "Radio Settings" --menu "\nCurrent Configuration:\n Name: $current_name\n Freq: ${current_freq_mhz}MHz | SF$current_sf | BW${current_bw_khz}kHz | ${current_power}dBm\n\nSelect setting to change:" 20 70 8 \
|
|
"name" "Node name ($current_name)" \
|
|
"preset" "Load radio preset (frequency, SF, BW, CR)" \
|
|
"frequency" "Frequency (${current_freq_mhz}MHz)" \
|
|
"power" "TX Power (${current_power}dBm)" \
|
|
"spreading" "Spreading Factor (SF$current_sf)" \
|
|
"bandwidth" "Bandwidth (${current_bw_khz}kHz)" \
|
|
"apply" "Apply changes and restart" \
|
|
"back" "Back to main menu" 3>&1 1>&2 2>&3)
|
|
|
|
case $CHOICE in
|
|
"name")
|
|
local new_name=$(get_input "Node Name" "Enter repeater node name:" "$current_name")
|
|
if [ -n "$new_name" ]; then
|
|
yq -i ".repeater.node_name = \"$new_name\"" "$CONFIG_DIR/config.yaml"
|
|
show_info "Updated" "Node name set to: $new_name"
|
|
fi
|
|
;;
|
|
"preset")
|
|
select_radio_preset
|
|
;;
|
|
"frequency")
|
|
local new_freq=$(get_input "Frequency" "Enter frequency in MHz (e.g., 869.618):" "$current_freq_mhz")
|
|
if [ -n "$new_freq" ]; then
|
|
local freq_hz=$(awk "BEGIN {printf \"%.0f\", $new_freq * 1000000}")
|
|
yq -i ".radio.frequency = $freq_hz" "$CONFIG_DIR/config.yaml"
|
|
show_info "Updated" "Frequency set to: ${new_freq}MHz"
|
|
fi
|
|
;;
|
|
"power")
|
|
local new_power=$(get_input "TX Power" "Enter TX power in dBm (e.g., 14):" "$current_power")
|
|
if [ -n "$new_power" ]; then
|
|
yq -i ".radio.tx_power = $new_power" "$CONFIG_DIR/config.yaml"
|
|
show_info "Updated" "TX Power set to: ${new_power}dBm"
|
|
fi
|
|
;;
|
|
"spreading")
|
|
local new_sf=$(get_input "Spreading Factor" "Enter spreading factor (7-12):" "$current_sf")
|
|
if [ -n "$new_sf" ]; then
|
|
yq -i ".radio.spreading_factor = $new_sf" "$CONFIG_DIR/config.yaml"
|
|
show_info "Updated" "Spreading factor set to: SF$new_sf"
|
|
fi
|
|
;;
|
|
"bandwidth")
|
|
local new_bw=$(get_input "Bandwidth" "Enter bandwidth in kHz (e.g., 62.5):" "$current_bw_khz")
|
|
if [ -n "$new_bw" ]; then
|
|
local bw_hz=$(awk "BEGIN {printf \"%.0f\", $new_bw * 1000}")
|
|
yq -i ".radio.bandwidth = $bw_hz" "$CONFIG_DIR/config.yaml"
|
|
show_info "Updated" "Bandwidth set to: ${new_bw}kHz"
|
|
fi
|
|
;;
|
|
"apply")
|
|
if [ "$EUID" -eq 0 ]; then
|
|
systemctl restart "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
if backend_running; then
|
|
show_info "Applied" "Configuration applied and service restarted successfully!"
|
|
else
|
|
show_error "Service failed to restart!\n\nCheck logs: journalctl -u $BACKEND_SERVICE"
|
|
fi
|
|
else
|
|
show_info "Note" "Run as root to restart services automatically.\n\nManually restart with:\nsudo systemctl restart $BACKEND_SERVICE"
|
|
fi
|
|
;;
|
|
"back"|"")
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
select_radio_preset() {
|
|
# Fetch presets from API or use local file
|
|
local presets_json=""
|
|
|
|
echo "Fetching radio presets..." >&2
|
|
presets_json=$(curl -s --max-time 5 https://api.meshcore.nz/api/v1/config 2>/dev/null)
|
|
|
|
if [ -z "$presets_json" ]; then
|
|
if [ -f "$CONSOLE_DIR/radio-presets.json" ]; then
|
|
presets_json=$(cat "$CONSOLE_DIR/radio-presets.json")
|
|
elif [ -f "$REPEATER_DIR/radio-presets.json" ]; then
|
|
presets_json=$(cat "$REPEATER_DIR/radio-presets.json")
|
|
else
|
|
show_error "Could not fetch radio presets from API and no local file found."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Build menu from presets
|
|
local menu_items=()
|
|
local index=1
|
|
|
|
while IFS= read -r line; do
|
|
local title=$(echo "$line" | jq -r '.title')
|
|
local freq=$(echo "$line" | jq -r '.frequency')
|
|
local sf=$(echo "$line" | jq -r '.spreading_factor')
|
|
local bw=$(echo "$line" | jq -r '.bandwidth')
|
|
menu_items+=("$index" "$title (${freq}MHz SF$sf BW$bw)")
|
|
((index++)) || true
|
|
done < <(echo "$presets_json" | jq -c '.[]' 2>/dev/null)
|
|
|
|
if [ ${#menu_items[@]} -eq 0 ]; then
|
|
show_error "No presets found in configuration."
|
|
return 1
|
|
fi
|
|
|
|
local selection=$($DIALOG --backtitle "pyMC Console Management" --title "Radio Presets" --menu "Select a radio preset:" 20 70 10 "${menu_items[@]}" 3>&1 1>&2 2>&3)
|
|
|
|
if [ -n "$selection" ]; then
|
|
local preset=$(echo "$presets_json" | jq -c ".[$((selection-1))]" 2>/dev/null)
|
|
|
|
if [ -n "$preset" ] && [ "$preset" != "null" ]; then
|
|
local freq=$(echo "$preset" | jq -r '.frequency')
|
|
local sf=$(echo "$preset" | jq -r '.spreading_factor')
|
|
local bw=$(echo "$preset" | jq -r '.bandwidth')
|
|
local cr=$(echo "$preset" | jq -r '.coding_rate')
|
|
local title=$(echo "$preset" | jq -r '.title')
|
|
|
|
local freq_hz=$(awk "BEGIN {printf \"%.0f\", $freq * 1000000}")
|
|
local bw_hz=$(awk "BEGIN {printf \"%.0f\", $bw * 1000}")
|
|
|
|
yq -i ".radio.frequency = $freq_hz" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".radio.spreading_factor = $sf" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".radio.bandwidth = $bw_hz" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".radio.coding_rate = $cr" "$CONFIG_DIR/config.yaml"
|
|
|
|
show_info "Preset Applied" "Applied preset: $title\n\nFrequency: ${freq}MHz\nSpreading Factor: SF$sf\nBandwidth: ${bw}kHz\nCoding Rate: $cr\n\nRemember to apply changes to restart the service."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# GPIO Function (Advanced Hardware Configuration)
|
|
# ============================================================================
|
|
|
|
do_gpio() {
|
|
# Show warning first
|
|
if ! ask_yes_no "⚠️ Advanced Configuration" "\nWARNING: GPIO Configuration\n\nThese settings are for ADVANCED USERS ONLY.\n\nIncorrect GPIO settings can:\n- Prevent radio communication\n- Cause hardware damage\n- Make the repeater non-functional\n\nOnly proceed if you know your hardware pinout!\n\nContinue?"; then
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$CONFIG_DIR/config.yaml" ]; then
|
|
show_error "Configuration file not found!\n\nPlease install pyMC Console first."
|
|
return 1
|
|
fi
|
|
|
|
while true; do
|
|
# Read current GPIO settings
|
|
local cs_pin=$(yq '.sx1262.cs_pin' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "-1")
|
|
local reset_pin=$(yq '.sx1262.reset_pin' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "-1")
|
|
local busy_pin=$(yq '.sx1262.busy_pin' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "-1")
|
|
local irq_pin=$(yq '.sx1262.irq_pin' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "-1")
|
|
local txen_pin=$(yq '.sx1262.txen_pin' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "-1")
|
|
local rxen_pin=$(yq '.sx1262.rxen_pin' "$CONFIG_DIR/config.yaml" 2>/dev/null || echo "-1")
|
|
|
|
CHOICE=$($DIALOG --backtitle "pyMC Console Management" --title "GPIO Configuration ⚠️" --menu "\nCurrent GPIO Pins (BCM numbering):\n CS: $cs_pin | Reset: $reset_pin | Busy: $busy_pin\n IRQ: $irq_pin | TXEN: $txen_pin | RXEN: $rxen_pin\n\nSelect option:" 20 70 8 \
|
|
"preset" "Load hardware preset" \
|
|
"cs" "Chip Select pin ($cs_pin)" \
|
|
"reset" "Reset pin ($reset_pin)" \
|
|
"busy" "Busy pin ($busy_pin)" \
|
|
"irq" "IRQ pin ($irq_pin)" \
|
|
"txen" "TX Enable pin ($txen_pin, -1=disabled)" \
|
|
"rxen" "RX Enable pin ($rxen_pin, -1=disabled)" \
|
|
"apply" "Apply changes and restart" \
|
|
"back" "Back to main menu" 3>&1 1>&2 2>&3)
|
|
|
|
case $CHOICE in
|
|
"preset")
|
|
select_hardware_preset
|
|
;;
|
|
"cs")
|
|
local new_pin=$(get_input "Chip Select Pin" "Enter CS pin (BCM numbering):" "$cs_pin")
|
|
[ -n "$new_pin" ] && yq -i ".sx1262.cs_pin = $new_pin" "$CONFIG_DIR/config.yaml"
|
|
;;
|
|
"reset")
|
|
local new_pin=$(get_input "Reset Pin" "Enter Reset pin (BCM numbering):" "$reset_pin")
|
|
[ -n "$new_pin" ] && yq -i ".sx1262.reset_pin = $new_pin" "$CONFIG_DIR/config.yaml"
|
|
;;
|
|
"busy")
|
|
local new_pin=$(get_input "Busy Pin" "Enter Busy pin (BCM numbering):" "$busy_pin")
|
|
[ -n "$new_pin" ] && yq -i ".sx1262.busy_pin = $new_pin" "$CONFIG_DIR/config.yaml"
|
|
;;
|
|
"irq")
|
|
local new_pin=$(get_input "IRQ Pin" "Enter IRQ pin (BCM numbering):" "$irq_pin")
|
|
[ -n "$new_pin" ] && yq -i ".sx1262.irq_pin = $new_pin" "$CONFIG_DIR/config.yaml"
|
|
;;
|
|
"txen")
|
|
local new_pin=$(get_input "TX Enable Pin" "Enter TXEN pin (-1 to disable):" "$txen_pin")
|
|
[ -n "$new_pin" ] && yq -i ".sx1262.txen_pin = $new_pin" "$CONFIG_DIR/config.yaml"
|
|
;;
|
|
"rxen")
|
|
local new_pin=$(get_input "RX Enable Pin" "Enter RXEN pin (-1 to disable):" "$rxen_pin")
|
|
[ -n "$new_pin" ] && yq -i ".sx1262.rxen_pin = $new_pin" "$CONFIG_DIR/config.yaml"
|
|
;;
|
|
"apply")
|
|
if [ "$EUID" -eq 0 ]; then
|
|
systemctl restart "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
if backend_running; then
|
|
show_info "Applied" "GPIO configuration applied and service restarted!"
|
|
else
|
|
show_error "Service failed to restart!\n\nGPIO settings may be incorrect.\nCheck logs: journalctl -u $BACKEND_SERVICE"
|
|
fi
|
|
else
|
|
show_info "Note" "Run as root to restart services automatically."
|
|
fi
|
|
;;
|
|
"back"|"")
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
select_hardware_preset() {
|
|
local hw_config=""
|
|
|
|
if [ -f "$CONSOLE_DIR/radio-settings.json" ]; then
|
|
hw_config="$CONSOLE_DIR/radio-settings.json"
|
|
elif [ -f "$REPEATER_DIR/radio-settings.json" ]; then
|
|
hw_config="$REPEATER_DIR/radio-settings.json"
|
|
else
|
|
show_error "Hardware configuration file not found!"
|
|
return 1
|
|
fi
|
|
|
|
# Build menu from hardware presets
|
|
local menu_items=()
|
|
|
|
# Use keys_unsorted to preserve JSON insertion order (matches upstream grep-based parsing)
|
|
while IFS= read -r key; do
|
|
local name=$(jq -r ".hardware.\"$key\".name" "$hw_config")
|
|
menu_items+=("$key" "$name")
|
|
done < <(jq -r '.hardware | keys_unsorted[]' "$hw_config" 2>/dev/null)
|
|
|
|
if [ ${#menu_items[@]} -eq 0 ]; then
|
|
show_error "No hardware presets found."
|
|
return 1
|
|
fi
|
|
|
|
local selection=$($DIALOG --backtitle "pyMC Console Management" --title "Hardware Presets" --menu "Select your hardware:" 20 70 10 "${menu_items[@]}" 3>&1 1>&2 2>&3)
|
|
|
|
if [ -n "$selection" ]; then
|
|
local preset=$(jq ".hardware.\"$selection\"" "$hw_config" 2>/dev/null)
|
|
|
|
if [ -n "$preset" ] && [ "$preset" != "null" ]; then
|
|
# Apply all GPIO settings from preset
|
|
local bus_id=$(echo "$preset" | jq -r '.bus_id // 0')
|
|
local cs_id=$(echo "$preset" | jq -r '.cs_id // 0')
|
|
local cs_pin=$(echo "$preset" | jq -r '.cs_pin // 21')
|
|
local reset_pin=$(echo "$preset" | jq -r '.reset_pin // 18')
|
|
local busy_pin=$(echo "$preset" | jq -r '.busy_pin // 20')
|
|
local irq_pin=$(echo "$preset" | jq -r '.irq_pin // 16')
|
|
local txen_pin=$(echo "$preset" | jq -r '.txen_pin // -1')
|
|
local rxen_pin=$(echo "$preset" | jq -r '.rxen_pin // -1')
|
|
local is_waveshare=$(echo "$preset" | jq -r '.is_waveshare // false')
|
|
local use_dio3_tcxo=$(echo "$preset" | jq -r '.use_dio3_tcxo // false')
|
|
local tx_power=$(echo "$preset" | jq -r '.tx_power // 14')
|
|
|
|
yq -i ".sx1262.bus_id = $bus_id" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.cs_id = $cs_id" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.cs_pin = $cs_pin" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.reset_pin = $reset_pin" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.busy_pin = $busy_pin" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.irq_pin = $irq_pin" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.txen_pin = $txen_pin" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.rxen_pin = $rxen_pin" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.is_waveshare = $is_waveshare" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".sx1262.use_dio3_tcxo = $use_dio3_tcxo" "$CONFIG_DIR/config.yaml"
|
|
yq -i ".radio.tx_power = $tx_power" "$CONFIG_DIR/config.yaml"
|
|
|
|
local name=$(echo "$preset" | jq -r '.name')
|
|
show_info "Preset Applied" "Applied hardware preset: $name\n\nGPIO Pins:\n CS: $cs_pin | Reset: $reset_pin\n Busy: $busy_pin | IRQ: $irq_pin\n TXEN: $txen_pin | RXEN: $rxen_pin\n\nTX Power: ${tx_power}dBm\n\nRemember to apply changes to restart."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Service Control Functions
|
|
# ============================================================================
|
|
|
|
do_start() {
|
|
if [ "$EUID" -ne 0 ]; then
|
|
show_error "Service control requires root privileges.\n\nPlease run: sudo $0 start"
|
|
return 1
|
|
fi
|
|
|
|
echo "Starting service..."
|
|
systemctl start "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
local status="✗"
|
|
backend_running && status="✓"
|
|
|
|
show_info "Service Started" "\npyMC Repeater: $status\n\nDashboard: http://$(hostname -I | awk '{print $1}'):8000/"
|
|
}
|
|
|
|
do_stop() {
|
|
if [ "$EUID" -ne 0 ]; then
|
|
show_error "Service control requires root privileges.\n\nPlease run: sudo $0 stop"
|
|
return 1
|
|
fi
|
|
|
|
echo "Stopping service..."
|
|
systemctl stop "$BACKEND_SERVICE" 2>/dev/null || true
|
|
|
|
show_info "Service Stopped" "\n✓ pyMC Repeater service has been stopped."
|
|
}
|
|
|
|
do_restart() {
|
|
if [ "$EUID" -ne 0 ]; then
|
|
show_error "Service control requires root privileges.\n\nPlease run: sudo $0 restart"
|
|
return 1
|
|
fi
|
|
|
|
echo "Restarting service..."
|
|
systemctl restart "$BACKEND_SERVICE" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
local status="✗"
|
|
backend_running && status="✓"
|
|
|
|
show_info "Service Restarted" "\npyMC Repeater: $status\n\nDashboard: http://$(hostname -I | awk '{print $1}'):8000/"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Uninstall Function
|
|
# ============================================================================
|
|
|
|
do_uninstall() {
|
|
# Get site-packages path for checking leftovers
|
|
local site_packages
|
|
site_packages=$(python3 -c "import site; print(site.getsitepackages()[0])" 2>/dev/null || echo "/usr/local/lib/python3/dist-packages")
|
|
|
|
# Check for ANY installation (old paths, new paths, or site-packages leftovers)
|
|
local found_install=false
|
|
[ -d "$INSTALL_DIR" ] && found_install=true
|
|
[ -d "$CONSOLE_DIR" ] && found_install=true
|
|
[ -d "/opt/pymc_console/pymc_repeater" ] && found_install=true # Old path
|
|
[ -f "/etc/systemd/system/pymc-repeater.service" ] && found_install=true
|
|
[ -d "$site_packages/repeater" ] && found_install=true # pip leftovers
|
|
[ -d "$site_packages/pymc_core" ] && found_install=true # pip leftovers
|
|
|
|
if [ "$found_install" = false ]; then
|
|
show_error "pyMC Console is not installed."
|
|
return 1
|
|
fi
|
|
|
|
if [ "$EUID" -ne 0 ]; then
|
|
show_error "Uninstall requires root privileges.\n\nPlease run: sudo $0 uninstall"
|
|
return 1
|
|
fi
|
|
|
|
# Check if clone directory exists
|
|
local has_clone=false
|
|
[ -d "$CLONE_DIR" ] && has_clone=true
|
|
|
|
local uninstall_msg="\nThis will COMPLETELY REMOVE:\n\n- pyMC Repeater service and files\n- pyMC Console frontend\n- Python packages (pymc_repeater, pymc_core)\n- Configuration files\n- Log files\n- Service user"
|
|
|
|
if [ "$has_clone" = true ]; then
|
|
uninstall_msg="$uninstall_msg\n\nNote: The clone at $CLONE_DIR will be kept.\nYou can remove it manually if desired."
|
|
fi
|
|
|
|
uninstall_msg="$uninstall_msg\n\nThis action cannot be undone!\n\nContinue?"
|
|
|
|
if ! ask_yes_no "⚠️ Confirm Uninstall" "$uninstall_msg"; then
|
|
return 0
|
|
fi
|
|
|
|
clear
|
|
echo "=== pyMC Console Uninstall ==="
|
|
echo ""
|
|
|
|
# =========================================================================
|
|
# Step 1: Run upstream uninstaller (simple - no fancy progress bar needed)
|
|
# =========================================================================
|
|
echo "[1/4] Removing pyMC_Repeater..."
|
|
|
|
# Always do manual cleanup - it's fast and reliable
|
|
# (upstream's uninstaller uses TUI which is complex to wrap)
|
|
systemctl stop "$BACKEND_SERVICE" 2>/dev/null || true
|
|
systemctl disable "$BACKEND_SERVICE" 2>/dev/null || true
|
|
rm -f /etc/systemd/system/pymc-repeater.service
|
|
systemctl daemon-reload
|
|
rm -rf "$INSTALL_DIR"
|
|
rm -rf "$CONFIG_DIR"
|
|
rm -rf "$LOG_DIR"
|
|
rm -rf /var/lib/pymc_repeater
|
|
if id "$SERVICE_USER" &>/dev/null; then
|
|
userdel "$SERVICE_USER" 2>/dev/null || true
|
|
fi
|
|
echo " ✓ pyMC_Repeater removed"
|
|
|
|
# =========================================================================
|
|
# Step 2: Remove pyMC Console extras (not handled by upstream)
|
|
# =========================================================================
|
|
echo "[2/4] Removing pyMC Console extras..."
|
|
rm -rf "$CONSOLE_DIR"
|
|
rm -rf "/opt/pymc_console" # Old path
|
|
echo " ✓ Console directories removed"
|
|
|
|
# =========================================================================
|
|
# Step 3: Clean up any leftover site-packages (pip leftovers)
|
|
# =========================================================================
|
|
echo "[3/4] Cleaning up Python packages..."
|
|
pip uninstall -y pymc_repeater 2>/dev/null || true
|
|
pip uninstall -y pymc_core 2>/dev/null || true
|
|
pip uninstall -y pymc-repeater 2>/dev/null || true
|
|
pip uninstall -y pymc-core 2>/dev/null || true
|
|
# Remove any leftover directories
|
|
rm -rf "$site_packages/repeater" 2>/dev/null || true
|
|
rm -rf "$site_packages/pymc_core" 2>/dev/null || true
|
|
rm -rf "$site_packages/pymc_repeater"* 2>/dev/null || true
|
|
rm -rf "$site_packages/pymc_core"* 2>/dev/null || true
|
|
echo " ✓ Python packages cleaned"
|
|
|
|
# =========================================================================
|
|
# Step 4: Handle clone directory
|
|
# =========================================================================
|
|
echo "[4/4] Finalizing..."
|
|
|
|
echo ""
|
|
echo "=== Uninstall Complete ==="
|
|
echo ""
|
|
|
|
# Offer to delete clone directory
|
|
if [ "$has_clone" = true ]; then
|
|
if ask_yes_no "Remove Clone?" "\nThe pyMC_Repeater clone still exists at:\n$CLONE_DIR\n\nWould you like to remove it as well?"; then
|
|
rm -rf "$CLONE_DIR"
|
|
echo " ✓ Clone directory removed"
|
|
else
|
|
echo " Clone directory preserved at: $CLONE_DIR"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
show_info "Uninstall Complete" "\npyMC Console has been completely removed.\n\nThank you for using pyMC Console!"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
check_spi() {
|
|
# Skip SPI check on non-Linux systems (macOS, etc.)
|
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if SPI is already loaded via kernel module
|
|
if grep -q "spi" /proc/modules 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
# Check for spidev devices (works on Ubuntu and other distros)
|
|
if ls /dev/spidev* &>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if spi_bcm2835 or spi_bcm2708 modules are available (Raspberry Pi)
|
|
if lsmod 2>/dev/null | grep -q "spi_bcm"; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if spidev module is loaded
|
|
if lsmod 2>/dev/null | grep -q "spidev"; then
|
|
return 0
|
|
fi
|
|
|
|
# Raspberry Pi / Ubuntu on Pi: check config.txt locations
|
|
local config_file=""
|
|
if [ -f "/boot/firmware/config.txt" ]; then
|
|
# Ubuntu on Raspberry Pi uses /boot/firmware/
|
|
config_file="/boot/firmware/config.txt"
|
|
elif [ -f "/boot/config.txt" ]; then
|
|
# Raspberry Pi OS uses /boot/
|
|
config_file="/boot/config.txt"
|
|
fi
|
|
|
|
if [ -n "$config_file" ]; then
|
|
# Raspberry Pi (any OS) - can enable via config.txt
|
|
if grep -q "dtparam=spi=on" "$config_file" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
if ask_yes_no "SPI Not Enabled" "\nSPI interface is required but not enabled!\n\nWould you like to enable it now?\n(This will require a reboot)"; then
|
|
echo "dtparam=spi=on" >> "$config_file"
|
|
show_info "SPI Enabled" "\nSPI has been enabled.\n\nSystem will reboot now.\nPlease run this script again after reboot."
|
|
reboot
|
|
else
|
|
show_error "SPI is required for LoRa radio operation.\n\nPlease enable SPI manually and run this script again."
|
|
exit 1
|
|
fi
|
|
else
|
|
# Generic Linux (Ubuntu x86, other SBCs, etc.)
|
|
# Try to load spidev module
|
|
if modprobe spidev 2>/dev/null; then
|
|
if ls /dev/spidev* &>/dev/null; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Still no SPI - warn user
|
|
if ! ask_yes_no "SPI Check" "\nCould not verify SPI is enabled.\n\nFor LoRa radio operation, ensure SPI is enabled on your system.\n\nOn Ubuntu/Debian, you may need to:\n- Load the spidev module: sudo modprobe spidev\n- Enable SPI in device tree overlays\n- Check your hardware supports SPI\n\nContinue anyway?"; then
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
install_yq() {
|
|
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
|
echo "Installing yq..."
|
|
install_yq_silent
|
|
fi
|
|
}
|
|
|
|
# Silent version for use with spinner
|
|
install_yq_silent() {
|
|
local YQ_VERSION="v4.40.5"
|
|
local YQ_BINARY="yq_linux_arm64"
|
|
|
|
if [[ "$(uname -m)" == "x86_64" ]]; then
|
|
YQ_BINARY="yq_linux_amd64"
|
|
elif [[ "$(uname -m)" == "armv7"* ]]; then
|
|
YQ_BINARY="yq_linux_arm"
|
|
elif [[ "$(uname -s)" == "Darwin" ]]; then
|
|
YQ_BINARY="yq_darwin_arm64"
|
|
[[ "$(uname -m)" == "x86_64" ]] && YQ_BINARY="yq_darwin_amd64"
|
|
fi
|
|
|
|
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" && chmod +x /usr/local/bin/yq
|
|
}
|
|
|
|
# ============================================================================
|
|
# UPSTREAM INSTALLATION MANAGER
|
|
# ============================================================================
|
|
# This section handles running pyMC_Repeater's native manage.sh installer.
|
|
# We run upstream's installer directly (user sees their native TUI), then
|
|
# apply our patches and overlay our dashboard afterward.
|
|
#
|
|
# The approach:
|
|
# 1. Clone/update pyMC_Repeater to a sibling directory
|
|
# 2. Run upstream's manage.sh (install/upgrade) in foreground - user sees TUI
|
|
# 3. Apply our patches to the installed files (/opt/pymc_repeater)
|
|
# 4. Overlay our React dashboard
|
|
# 5. Run our radio configuration
|
|
#
|
|
# Note: Upstream's radio config script is temporarily renamed during install
|
|
# so we can run our own configuration flow instead.
|
|
# ============================================================================
|
|
|
|
# Run upstream's manage.sh with a specific action
|
|
# Usage: run_upstream_installer <action> [branch]
|
|
# Actions: install, upgrade
|
|
#
|
|
# Strategy: Let upstream run in foreground so user sees its native TUI.
|
|
# We skip upstream's radio config and do our own after.
|
|
run_upstream_installer() {
|
|
local action="$1"
|
|
local branch="${2:-$DEFAULT_BRANCH}"
|
|
local upstream_script="$CLONE_DIR/manage.sh"
|
|
local exit_code=0
|
|
|
|
# Verify clone exists
|
|
if [ ! -f "$upstream_script" ]; then
|
|
print_error "Upstream manage.sh not found at $upstream_script"
|
|
return 1
|
|
fi
|
|
|
|
# Temporarily rename setup-radio-config.sh to skip upstream's radio config
|
|
# We run our own config after installation
|
|
local radio_config_script="$CLONE_DIR/setup-radio-config.sh"
|
|
local radio_config_backup=""
|
|
if [ -f "$radio_config_script" ]; then
|
|
radio_config_backup="${radio_config_script}.pymc_backup"
|
|
mv "$radio_config_script" "$radio_config_backup"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
|
|
echo -e " ${BOLD}Running pyMC_Repeater $action...${NC}"
|
|
echo -e " ${DIM}You'll see the upstream installer's interface below.${NC}"
|
|
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
|
|
echo ""
|
|
|
|
# Run upstream's manage.sh directly in foreground
|
|
# User sees the native TUI (whiptail dialogs, progress bars, etc.)
|
|
(
|
|
cd "$CLONE_DIR"
|
|
bash "$upstream_script" "$action"
|
|
)
|
|
exit_code=$?
|
|
|
|
echo ""
|
|
echo -e " ${DIM}────────────────────────────────────────────────────────${NC}"
|
|
|
|
# Restore radio config script if we backed it up
|
|
if [ -n "$radio_config_backup" ] && [ -f "$radio_config_backup" ]; then
|
|
mv "$radio_config_backup" "$radio_config_script"
|
|
fi
|
|
|
|
if [ $exit_code -eq 0 ]; then
|
|
echo -e " ${CHECK} pyMC_Repeater $action completed"
|
|
return 0
|
|
else
|
|
echo -e " ${CROSS} ${RED}pyMC_Repeater $action failed${NC}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# PATCH REGISTRY
|
|
# ============================================================================
|
|
# Core patches that enhance pyMC_Repeater with pyMC Console features.
|
|
# These patches are candidates for upstream PR submission.
|
|
#
|
|
# REMOVED (Merged Upstream in PR #36 - feat/identity branch):
|
|
# - patch_api_endpoints - /api/update_radio_config endpoint
|
|
# - patch_stats_api - Extended /api/stats with max_flood_hops, advert_interval_minutes, rx_delay_base
|
|
#
|
|
# Remaining Patches:
|
|
#
|
|
# 2. patch_logging_section (main.py)
|
|
# - Ensures config['logging'] exists before setting level from --log-level arg
|
|
# - Fixes KeyError when service starts with --log-level DEBUG
|
|
# - PR Status: Pending
|
|
#
|
|
# 3. patch_log_level_api (api_endpoints.py)
|
|
# - Adds POST /api/set_log_level endpoint
|
|
# - Allows web UI to toggle log level (INFO/DEBUG) and restart service
|
|
# - PR Status: Pending
|
|
#
|
|
# 4. patch_mesh_cli (mesh_cli.py)
|
|
# - Enhances mesh CLI with MeshCore CommonCLI.cpp parity
|
|
# - tempradio with auto-revert timer, reboot, stats-*, board, neighbor.remove
|
|
# - Implemented via external Python patch script (patches/mesh_cli_enhancements.py)
|
|
# - PR Status: Pending
|
|
#
|
|
# 6. patch_private_key_api (mesh_cli.py)
|
|
# - Adds get/set prv.key for private key management via Terminal
|
|
# - Stores key in config['mesh']['identity_key']
|
|
# - PR Status: Pending
|
|
#
|
|
# NOTE: patch_static_file_serving was removed
|
|
# Upstream's default() method already returns index.html for all unknown routes,
|
|
# which is exactly what a true SPA needs. React Router handles client-side routing.
|
|
#
|
|
# NOTE: GPIO patches (Fix A-D) were removed after discovery that the real issue
|
|
# was a race condition in pymc_core's interrupt initialization. Adding --log-level
|
|
# DEBUG to the service provides enough delay for the asyncio event loop to
|
|
# initialize before interrupt callbacks are registered. See create_backend_service().
|
|
#
|
|
# To generate clean patches for upstream PR:
|
|
# 1. Clone fresh pyMC_Repeater
|
|
# 2. Apply patches via manage.sh upgrade
|
|
# 3. git diff > patches/feature-name.patch
|
|
# ============================================================================
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# PATCH 1: Radio Configuration API Endpoint [REMOVED - MERGED UPSTREAM PR #36]
|
|
# ------------------------------------------------------------------------------
|
|
# This patch was merged upstream in PR #36 to the feat/identity branch.
|
|
# The patch is preserved below (commented out) for reference and for users
|
|
# who may still be on main/dev branches before the PR is merged there.
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# Patch removed - see PR #36: https://github.com/rightup/pyMC_Repeater/pull/36
|
|
patch_api_endpoints() {
|
|
# DISABLED: This patch was merged upstream in PR #36 (feat/identity branch)
|
|
# See: https://github.com/rightup/pyMC_Repeater/pull/36
|
|
# The /api/update_radio_config endpoint is now part of upstream pyMC_Repeater
|
|
print_info "API endpoints patch skipped (merged upstream in PR #36)"
|
|
return 0
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# PATCH 3: Log Level API Endpoint
|
|
# ------------------------------------------------------------------------------
|
|
# File: repeater/web/api_endpoints.py
|
|
# Purpose: Allow web UI to toggle log level (INFO/DEBUG) without SSH
|
|
# Changes:
|
|
# - Add POST /api/set_log_level endpoint
|
|
# - Updates config.yaml -> logging.level
|
|
# - Restarts pymc-repeater service to apply change
|
|
# - Returns success/failure
|
|
# ------------------------------------------------------------------------------
|
|
patch_log_level_api() {
|
|
local target_dir="${1:-$CLONE_DIR}"
|
|
local api_file="$target_dir/repeater/web/api_endpoints.py"
|
|
|
|
if [ ! -f "$api_file" ]; then
|
|
print_warning "api_endpoints.py not found, skipping log level patch"
|
|
return 0
|
|
fi
|
|
|
|
# Check if already patched
|
|
if grep -q 'def set_log_level' "$api_file" 2>/dev/null; then
|
|
print_info "Log level API already patched"
|
|
return 0
|
|
fi
|
|
|
|
# Use Python to add the endpoint
|
|
python3 << PATCHEOF
|
|
import re
|
|
|
|
api_file = "$api_file"
|
|
|
|
with open(api_file, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Add set_log_level endpoint after update_radio_config (or save_cad_settings if radio config not present)
|
|
set_log_level_code = '''
|
|
|
|
@cherrypy.expose
|
|
@cherrypy.tools.json_out()
|
|
@cherrypy.tools.json_in()
|
|
def set_log_level(self):
|
|
"""Set log level and restart service to apply
|
|
|
|
POST /api/set_log_level
|
|
Body: {"level": "DEBUG" | "INFO" | "WARNING"}
|
|
|
|
Returns: {"success": true, "data": {"level": "DEBUG", "restarting": true}}
|
|
"""
|
|
import subprocess
|
|
try:
|
|
self._require_post()
|
|
data = cherrypy.request.json or {}
|
|
|
|
level = data.get("level", "").upper()
|
|
if level not in ("DEBUG", "INFO", "WARNING", "ERROR"):
|
|
return self._error("Invalid log level. Use DEBUG, INFO, WARNING, or ERROR")
|
|
|
|
# Update config.yaml
|
|
config_path = getattr(self, '_config_path', '/etc/pymc_repeater/config.yaml')
|
|
|
|
# Ensure logging section exists
|
|
if "logging" not in self.config:
|
|
self.config["logging"] = {}
|
|
self.config["logging"]["level"] = level
|
|
|
|
# Save config
|
|
self._save_config_to_file(config_path)
|
|
|
|
logger.info(f"Log level changed to {level}, restarting service...")
|
|
|
|
# Schedule service restart in background (so we can return response first)
|
|
# Use subprocess.Popen to not wait for completion
|
|
subprocess.Popen(
|
|
["systemctl", "restart", "pymc-repeater"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True
|
|
)
|
|
|
|
return self._success({
|
|
"level": level,
|
|
"restarting": True,
|
|
"message": f"Log level set to {level}. Service restarting..."
|
|
})
|
|
|
|
except cherrypy.HTTPError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error setting log level: {e}")
|
|
return self._error(str(e))
|
|
'''
|
|
|
|
# Find insertion point - after update_radio_config if it exists, otherwise after save_cad_settings
|
|
if 'def update_radio_config' in content:
|
|
# Insert after update_radio_config
|
|
pattern = r'( def update_radio_config\(self\):.*?return self\._error\(str\(e\)\))'
|
|
match = re.search(pattern, content, re.DOTALL)
|
|
if match:
|
|
insert_pos = match.end()
|
|
content = content[:insert_pos] + set_log_level_code + content[insert_pos:]
|
|
else:
|
|
# Fall back to inserting after save_cad_settings
|
|
pattern = r'( def save_cad_settings\(self\):.*?return self\._error\(e\))'
|
|
match = re.search(pattern, content, re.DOTALL)
|
|
if match:
|
|
insert_pos = match.end()
|
|
content = content[:insert_pos] + set_log_level_code + content[insert_pos:]
|
|
|
|
with open(api_file, 'w') as f:
|
|
f.write(content)
|
|
print("Patched api_endpoints.py with set_log_level")
|
|
PATCHEOF
|
|
|
|
# Verify patch was applied
|
|
if grep -q 'def set_log_level' "$api_file" 2>/dev/null; then
|
|
print_success "Patched api_endpoints.py with set_log_level"
|
|
else
|
|
print_warning "Log level API patch may not have applied correctly"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# PATCH 2: Ensure logging section exists before setting level (main.py)
|
|
# ------------------------------------------------------------------------------
|
|
patch_logging_section() {
|
|
local target_dir="${1:-$CLONE_DIR}"
|
|
local main_file="$target_dir/repeater/main.py"
|
|
|
|
if [ ! -f "$main_file" ]; then
|
|
print_warning "main.py not found, skipping logging patch"
|
|
return 0
|
|
fi
|
|
|
|
# Check if already patched (upstream may have fixed this)
|
|
if grep -q 'if "logging" not in config' "$main_file" 2>/dev/null; then
|
|
print_info "Logging section already guarded (upstream fix)"
|
|
return 0
|
|
fi
|
|
|
|
# Only patch if the vulnerable pattern exists
|
|
if grep -q 'if args.log_level:' "$main_file" 2>/dev/null; then
|
|
python3 << PATCHEOF
|
|
import io, sys
|
|
path = "$main_file"
|
|
with open(path, 'r') as f:
|
|
s = f.read()
|
|
old = """
|
|
if args.log_level:
|
|
config[\"logging\"][\"level\"] = args.log_level
|
|
"""
|
|
new = """
|
|
if args.log_level:
|
|
if \"logging\" not in config:
|
|
config[\"logging\"] = {}
|
|
config[\"logging\"][\"level\"] = args.log_level
|
|
"""
|
|
if old in s and new not in s:
|
|
s = s.replace(old, new)
|
|
else:
|
|
# Try a more flexible replacement using lines
|
|
lines = s.splitlines(True)
|
|
out = []
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
if line.strip().startswith("if args.log_level"):
|
|
out.append(line)
|
|
i += 1
|
|
if i < len(lines) and "config[\"logging\"][\"level\"]" in lines[i]:
|
|
indent = lines[i].split('c')[0] # leading spaces
|
|
out.append(f"{indent}if \"logging\" not in config:\n")
|
|
out.append(f"{indent} config[\"logging\"] = {{}}\n")
|
|
out.append(lines[i])
|
|
i += 1
|
|
continue
|
|
out.append(line)
|
|
i += 1
|
|
s = ''.join(out)
|
|
with open(path, 'w') as f:
|
|
f.write(s)
|
|
print("Patched logging section in main.py")
|
|
PATCHEOF
|
|
# Verify
|
|
if grep -q 'if "logging" not in config' "$main_file"; then
|
|
print_success "Patched logging section in main.py"
|
|
else
|
|
print_warning "Logging patch may not have applied"
|
|
fi
|
|
else
|
|
print_info "No log_level handling found - may be older version"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# PATCH 4: MeshCore CLI Parity (mesh_cli.py)
|
|
# ------------------------------------------------------------------------------
|
|
# File: repeater/handler_helpers/mesh_cli.py
|
|
# Purpose: Enhance mesh CLI with MeshCore CommonCLI.cpp parity
|
|
# Changes:
|
|
# - tempradio with auto-revert timer (saves config, reverts after timeout)
|
|
# - reboot via systemctl restart
|
|
# - neighbor.remove implementation
|
|
# - clear stats implementation
|
|
# - stats-packets, stats-radio, stats-core commands
|
|
# - board command for platform info
|
|
# PR Status: Pending upstream submission
|
|
# ------------------------------------------------------------------------------
|
|
patch_mesh_cli() {
|
|
local target_dir="${1:-$CLONE_DIR}"
|
|
local mesh_cli_file="$target_dir/repeater/handler_helpers/mesh_cli.py"
|
|
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
local patch_script="$script_dir/patches/mesh_cli_enhancements.py"
|
|
|
|
if [ ! -f "$mesh_cli_file" ]; then
|
|
print_warning "mesh_cli.py not found, skipping MeshCore CLI patch"
|
|
return 0
|
|
fi
|
|
|
|
# Check if already patched (look for our marker comment)
|
|
if grep -q 'pymc_console: tempradio' "$mesh_cli_file" 2>/dev/null; then
|
|
print_info "MeshCore CLI patch already applied"
|
|
return 0
|
|
fi
|
|
|
|
# Check if patch script exists
|
|
if [ ! -f "$patch_script" ]; then
|
|
print_warning "Patch script not found: $patch_script"
|
|
print_info "Skipping MeshCore CLI enhancements"
|
|
return 0
|
|
fi
|
|
|
|
# Run the Python patch script
|
|
if python3 "$patch_script" "$mesh_cli_file" 2>/dev/null; then
|
|
print_success "Applied MeshCore CLI parity patch"
|
|
else
|
|
print_warning "MeshCore CLI patch may not have applied correctly"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# PATCH 5: Stats API Extension [REMOVED - MERGED UPSTREAM PR #36]
|
|
# ------------------------------------------------------------------------------
|
|
# This patch was merged upstream in PR #36 to the feat/identity branch.
|
|
# The /api/stats endpoint now includes max_flood_hops, advert_interval_minutes,
|
|
# and rx_delay_base in the response.
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# Patch removed - see PR #36: https://github.com/rightup/pyMC_Repeater/pull/36
|
|
patch_stats_api() {
|
|
# Patch disabled - merged upstream in PR #36 (feat/identity branch)
|
|
print_info "Stats API patch skipped (merged upstream in PR #36)"
|
|
return 0
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# PATCH 6: Private Key API (mesh_cli.py)
|
|
# ------------------------------------------------------------------------------
|
|
# File: repeater/handler_helpers/mesh_cli.py
|
|
# Purpose: Enable get/set prv.key for private key management via Terminal
|
|
# Changes:
|
|
# - get prv.key: Returns 32-byte Ed25519 signing key seed in hex
|
|
# - set prv.key: Stores key in config['mesh']['identity_key'], requires restart
|
|
# Security: Only accessible to admin users via authenticated CLI
|
|
# PR Status: Pending upstream submission
|
|
# ------------------------------------------------------------------------------
|
|
patch_private_key_api() {
|
|
local target_dir="${1:-$CLONE_DIR}"
|
|
local mesh_cli_file="$target_dir/repeater/handler_helpers/mesh_cli.py"
|
|
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
local patch_script="$script_dir/patches/private_key_api.py"
|
|
|
|
if [ ! -f "$mesh_cli_file" ]; then
|
|
print_warning "mesh_cli.py not found, skipping private key API patch"
|
|
return 0
|
|
fi
|
|
|
|
# Check if already patched
|
|
if grep -q 'pymc_console: prv.key' "$mesh_cli_file" 2>/dev/null; then
|
|
print_info "Private key API patch already applied"
|
|
return 0
|
|
fi
|
|
|
|
# Check if patch script exists
|
|
if [ ! -f "$patch_script" ]; then
|
|
print_warning "Patch script not found: $patch_script"
|
|
print_info "Skipping private key API"
|
|
return 0
|
|
fi
|
|
|
|
# Run the Python patch script
|
|
if python3 "$patch_script" "$mesh_cli_file" 2>/dev/null; then
|
|
print_success "Applied private key API patch"
|
|
else
|
|
print_warning "Private key API patch may not have applied correctly"
|
|
fi
|
|
}
|
|
|
|
install_backend_service() {
|
|
# Copy upstream's service file as base (from clone directory)
|
|
local service_file="$CLONE_DIR/pymc-repeater.service"
|
|
|
|
# Fall back to install dir if clone doesn't have it
|
|
if [ ! -f "$service_file" ] && [ -f "$INSTALL_DIR/pymc-repeater.service" ]; then
|
|
service_file="$INSTALL_DIR/pymc-repeater.service"
|
|
fi
|
|
|
|
if [ -f "$service_file" ]; then
|
|
cp "$service_file" /etc/systemd/system/pymc-repeater.service
|
|
|
|
# WORKAROUND: Add --log-level DEBUG to fix pymc_core timing bug on Pi 5
|
|
# Issue: asyncio event loop not ready when interrupt callbacks register
|
|
# The DEBUG flag slows down initialization enough for the event loop to start
|
|
# TODO: File upstream issue at github.com/rightup/pyMC_core
|
|
sed -i 's|--config /etc/pymc_repeater/config.yaml$|--config /etc/pymc_repeater/config.yaml --log-level DEBUG|' \
|
|
/etc/systemd/system/pymc-repeater.service
|
|
|
|
print_success "Installed upstream service file"
|
|
print_info "Added --log-level DEBUG for RX timing fix"
|
|
else
|
|
print_error "Service file not found in pyMC_Repeater repo"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# GitHub repository for UI releases (public distribution repo)
|
|
UI_REPO="dmduran12/pymc_console-dist"
|
|
UI_RELEASE_URL="https://github.com/${UI_REPO}/releases"
|
|
|
|
# Download and install dashboard from GitHub Releases
|
|
# Installs to separate directory (UI_DIR) instead of overwriting upstream Vue.js
|
|
# Configures web.web_path in config.yaml to point to our dashboard
|
|
install_static_frontend() {
|
|
local version="${1:-latest}"
|
|
local target_dir="$UI_DIR"
|
|
local config_file="$CONFIG_DIR/config.yaml"
|
|
local temp_file="/tmp/pymc-ui-$$.tar.gz"
|
|
local download_url
|
|
|
|
# Construct download URL
|
|
if [ "$version" = "latest" ]; then
|
|
download_url="${UI_RELEASE_URL}/latest/download/pymc-ui-latest.tar.gz"
|
|
else
|
|
download_url="${UI_RELEASE_URL}/download/${version}/pymc-ui-${version}.tar.gz"
|
|
fi
|
|
|
|
print_info "Downloading dashboard ($version)..."
|
|
|
|
# Download with curl (preferred - handles redirects better) or wget
|
|
if command -v curl &> /dev/null; then
|
|
if ! curl -fsSL -o "$temp_file" "$download_url"; then
|
|
print_error "Failed to download dashboard from $download_url"
|
|
rm -f "$temp_file"
|
|
return 1
|
|
fi
|
|
elif command -v wget &> /dev/null; then
|
|
# wget needs explicit redirect following for GitHub releases
|
|
if ! wget -q --max-redirect=5 -O "$temp_file" "$download_url"; then
|
|
print_error "Failed to download dashboard from $download_url"
|
|
print_info "Check your internet connection or try a specific version"
|
|
rm -f "$temp_file"
|
|
return 1
|
|
fi
|
|
else
|
|
print_error "Neither curl nor wget found - cannot download dashboard"
|
|
return 1
|
|
fi
|
|
|
|
# Verify download (check file exists and has content)
|
|
if [ ! -s "$temp_file" ]; then
|
|
print_error "Downloaded file is empty - release may not exist"
|
|
print_info "Available releases: ${UI_RELEASE_URL}"
|
|
rm -f "$temp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Remove existing dashboard if present (clean upgrade)
|
|
if [ -d "$target_dir" ]; then
|
|
rm -rf "$target_dir"
|
|
fi
|
|
|
|
# Create parent directories
|
|
mkdir -p "$(dirname "$target_dir")"
|
|
mkdir -p "$target_dir"
|
|
|
|
# Extract to target directory
|
|
if ! tar -xzf "$temp_file" -C "$target_dir" 2>/dev/null; then
|
|
print_error "Failed to extract dashboard archive"
|
|
rm -f "$temp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Clean up temp file
|
|
rm -f "$temp_file"
|
|
|
|
# Set permissions
|
|
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONSOLE_DIR" 2>/dev/null || true
|
|
|
|
# Configure CherryPy to serve our dashboard instead of built-in Vue.js
|
|
# This sets web.web_path in config.yaml
|
|
if [ -f "$config_file" ] && command -v yq &> /dev/null; then
|
|
# Ensure web section exists
|
|
if ! yq eval '.web' "$config_file" 2>/dev/null | grep -q -v "null"; then
|
|
yq -i '.web = {}' "$config_file" 2>/dev/null || true
|
|
fi
|
|
# Set web_path to our dashboard location
|
|
yq -i ".web.web_path = \"$target_dir\"" "$config_file" 2>/dev/null || {
|
|
print_warning "Could not set web_path in config - manual configuration may be required"
|
|
}
|
|
print_success "Configured web_path: $target_dir"
|
|
else
|
|
print_warning "Could not configure web_path - yq not available or config missing"
|
|
print_info "Manually set web.web_path in $config_file to: $target_dir"
|
|
fi
|
|
|
|
local size=$(du -sh "$target_dir" 2>/dev/null | cut -f1 || echo "unknown")
|
|
print_success "Dashboard installed ($size)"
|
|
print_info "Upstream Vue.js preserved at: $INSTALL_DIR/repeater/web/html/"
|
|
print_info "Dashboard will be served at http://<ip>:8000/"
|
|
|
|
return 0
|
|
}
|
|
|
|
# Get available UI versions from GitHub
|
|
get_ui_versions() {
|
|
local releases
|
|
releases=$(curl -s "https://api.github.com/repos/${UI_REPO}/releases" 2>/dev/null |
|
|
grep -oP '"tag_name":\s*"\K[^"]+' | head -10)
|
|
echo "$releases"
|
|
}
|
|
|
|
merge_config() {
|
|
local user_config="$1"
|
|
local example_config="$2"
|
|
|
|
if [ ! -f "$user_config" ] || [ ! -f "$example_config" ]; then
|
|
echo " Config merge skipped (files not found)"
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v yq &> /dev/null; then
|
|
echo " Config merge skipped (yq not available)"
|
|
return 0
|
|
fi
|
|
|
|
local temp_merged="${user_config}.merged"
|
|
|
|
if yq eval-all '. as $item ireduce ({}; . * $item)' "$example_config" "$user_config" > "$temp_merged" 2>/dev/null; then
|
|
if yq eval '.' "$temp_merged" > /dev/null 2>&1; then
|
|
mv "$temp_merged" "$user_config"
|
|
echo " ✓ Configuration merged (user settings preserved, new options added)"
|
|
else
|
|
rm -f "$temp_merged"
|
|
echo " ⚠ Merge validation failed, keeping original"
|
|
fi
|
|
else
|
|
rm -f "$temp_merged"
|
|
echo " ⚠ Merge failed, keeping original"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Menu
|
|
# ============================================================================
|
|
|
|
show_main_menu() {
|
|
local status=$(get_status_display)
|
|
|
|
CHOICE=$($DIALOG --backtitle "pyMC Console Management" --title "pyMC Console" --menu "\nStatus: $status\n\nChoose an action:" 20 70 10 \
|
|
"install" "Install pyMC Console (fresh install)" \
|
|
"upgrade" "Upgrade existing installation" \
|
|
"settings" "Configure radio settings" \
|
|
"gpio" "GPIO configuration (advanced)" \
|
|
"start" "Start services" \
|
|
"stop" "Stop services" \
|
|
"restart" "Restart services" \
|
|
"logs" "View live logs" \
|
|
"uninstall" "Uninstall pyMC Console" \
|
|
"exit" "Exit" 3>&1 1>&2 2>&3)
|
|
|
|
case $CHOICE in
|
|
"install") do_install ;;
|
|
"upgrade") do_upgrade ;;
|
|
"settings") do_settings ;;
|
|
"gpio") do_gpio ;;
|
|
"start") do_start ;;
|
|
"stop") do_stop ;;
|
|
"restart") do_restart ;;
|
|
"logs")
|
|
clear
|
|
echo "=== Live Logs (Press Ctrl+C to return) ==="
|
|
echo ""
|
|
journalctl -u "$BACKEND_SERVICE" -f
|
|
;;
|
|
"uninstall") do_uninstall ;;
|
|
"exit"|"") exit 0 ;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# CLI Help
|
|
# ============================================================================
|
|
|
|
show_help() {
|
|
echo "pyMC Console Management Script"
|
|
echo ""
|
|
echo "Usage: $0 [command]"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " install Install pyMC Console (fresh install)"
|
|
echo " upgrade Upgrade existing installation"
|
|
echo " settings Configure radio settings"
|
|
echo " gpio GPIO configuration (advanced)"
|
|
echo " start Start pyMC Repeater service"
|
|
echo " stop Stop pyMC Repeater service"
|
|
echo " restart Restart pyMC Repeater service"
|
|
echo " uninstall Completely remove pyMC Console"
|
|
echo ""
|
|
echo "Run without arguments for interactive menu."
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Entry Point
|
|
# ============================================================================
|
|
|
|
# Handle CLI arguments
|
|
case "$1" in
|
|
"--help"|"-h")
|
|
show_help
|
|
exit 0
|
|
;;
|
|
"install")
|
|
check_terminal
|
|
setup_dialog
|
|
do_install "$2"
|
|
exit 0
|
|
;;
|
|
"upgrade")
|
|
check_terminal
|
|
setup_dialog
|
|
do_upgrade
|
|
exit 0
|
|
;;
|
|
"settings")
|
|
check_terminal
|
|
setup_dialog
|
|
do_settings
|
|
exit 0
|
|
;;
|
|
"gpio")
|
|
check_terminal
|
|
setup_dialog
|
|
do_gpio
|
|
exit 0
|
|
;;
|
|
"start")
|
|
check_terminal
|
|
setup_dialog
|
|
do_start
|
|
exit 0
|
|
;;
|
|
"stop")
|
|
check_terminal
|
|
setup_dialog
|
|
do_stop
|
|
exit 0
|
|
;;
|
|
"restart")
|
|
check_terminal
|
|
setup_dialog
|
|
do_restart
|
|
exit 0
|
|
;;
|
|
"uninstall")
|
|
check_terminal
|
|
setup_dialog
|
|
do_uninstall
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
# Interactive menu mode
|
|
check_terminal
|
|
setup_dialog
|
|
|
|
while true; do
|
|
show_main_menu
|
|
done
|