mirror of
https://github.com/agessaman/meshcore-packet-capture.git
synced 2026-04-20 23:23:37 +00:00
- Updated .gitignore to exclude advert_state.json. - Added version checks for MeshCore in install.sh to ensure compatibility with multi-byte path support. - Implemented path length decoding in packet_capture.py to handle new MeshCore firmware specifications. - Adjusted requirements.txt to require meshcore version 2.2.31 or higher for multi-byte path support.
2671 lines
105 KiB
Bash
Executable file
2671 lines
105 KiB
Bash
Executable file
#!/bin/bash
|
||
# ============================================================================
|
||
# MeshCore Packet Capture - Interactive Installer
|
||
# ============================================================================
|
||
set -e
|
||
|
||
SCRIPT_VERSION="1.2.1"
|
||
DEFAULT_REPO="agessaman/meshcore-packet-capture"
|
||
DEFAULT_BRANCH="main"
|
||
|
||
# Parse command line arguments
|
||
CONFIG_URL=""
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--config)
|
||
CONFIG_URL="$2"
|
||
shift 2
|
||
;;
|
||
--repo)
|
||
DEFAULT_REPO="$2"
|
||
shift 2
|
||
;;
|
||
--branch)
|
||
DEFAULT_BRANCH="$2"
|
||
shift 2
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1"
|
||
echo "Usage: $0 [--config URL] [--repo owner/repo] [--branch branch-name]"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Use environment variables if set, otherwise use defaults/args
|
||
REPO="${INSTALL_REPO:-$DEFAULT_REPO}"
|
||
BRANCH="${INSTALL_BRANCH:-$DEFAULT_BRANCH}"
|
||
MIN_MESHCORE_VERSION="2.2.31"
|
||
ENABLE_LEGACY_DECODER_PATH="${PACKETCAPTURE_ENABLE_LEGACY_DECODER_PATH:-false}"
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Helper functions
|
||
print_header() {
|
||
echo -e "\n${BLUE}═══════════════════════════════════════════════════${NC}"
|
||
echo -e "${BLUE} $1${NC}"
|
||
echo -e "${BLUE}═══════════════════════════════════════════════════${NC}\n"
|
||
}
|
||
|
||
# Cross-platform timeout function
|
||
run_with_timeout() {
|
||
local timeout_seconds=$1
|
||
shift
|
||
local cmd=("$@")
|
||
|
||
if command -v timeout &> /dev/null; then
|
||
# Linux: use timeout command
|
||
timeout "$timeout_seconds" "${cmd[@]}"
|
||
elif command -v perl &> /dev/null; then
|
||
# macOS: use perl alarm
|
||
perl -e 'alarm shift; exec @ARGV' "$timeout_seconds" "${cmd[@]}"
|
||
elif command -v gtimeout &> /dev/null; then
|
||
# macOS with coreutils: use gtimeout
|
||
gtimeout "$timeout_seconds" "${cmd[@]}"
|
||
else
|
||
# Fallback: run without timeout (not ideal but better than failing)
|
||
"${cmd[@]}"
|
||
fi
|
||
}
|
||
|
||
# Create version info file with installer version and git hash
|
||
create_version_info() {
|
||
local git_hash="unknown"
|
||
local git_branch="${BRANCH}"
|
||
local git_repo="${REPO}"
|
||
|
||
# Try to resolve the branch/tag to a specific commit hash via GitHub API
|
||
if command -v curl >/dev/null 2>&1; then
|
||
# Try to get commit SHA from GitHub API
|
||
local api_url="https://api.github.com/repos/${git_repo}/commits/${git_branch}"
|
||
git_hash=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"sha"' | cut -d'"' -f4 | head -c7)
|
||
[ -z "$git_hash" ] && git_hash="unknown"
|
||
fi
|
||
|
||
# Create version info JSON file
|
||
cat > "$INSTALL_DIR/.version_info" <<EOF
|
||
{
|
||
"installer_version": "${SCRIPT_VERSION}",
|
||
"git_hash": "${git_hash}",
|
||
"git_branch": "${git_branch}",
|
||
"git_repo": "${git_repo}",
|
||
"install_date": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||
}
|
||
EOF
|
||
|
||
print_info "Version info saved: ${SCRIPT_VERSION}-${git_hash} (${git_repo}@${git_branch})"
|
||
}
|
||
|
||
print_success() {
|
||
echo -e "${GREEN}✓${NC} $1"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "${RED}✗${NC} $1"
|
||
}
|
||
|
||
print_warning() {
|
||
echo -e "${YELLOW}⚠${NC} $1"
|
||
}
|
||
|
||
print_info() {
|
||
echo -e "${BLUE}ℹ${NC} $1"
|
||
}
|
||
|
||
# Compare semantic-ish versions (numeric components only)
|
||
is_version_at_least() {
|
||
local python_cmd="$1"
|
||
local installed_version="$2"
|
||
local min_version="$3"
|
||
|
||
"$python_cmd" - "$installed_version" "$min_version" << 'PY'
|
||
import re
|
||
import sys
|
||
|
||
installed = sys.argv[1]
|
||
minimum = sys.argv[2]
|
||
|
||
def normalize(version: str):
|
||
# Keep numeric components only; this handles versions like 2.2.31rc1.
|
||
parts = [int(x) for x in re.findall(r"\d+", version)]
|
||
return parts or [0]
|
||
|
||
left = normalize(installed)
|
||
right = normalize(minimum)
|
||
size = max(len(left), len(right))
|
||
left += [0] * (size - len(left))
|
||
right += [0] * (size - len(right))
|
||
|
||
sys.exit(0 if left >= right else 1)
|
||
PY
|
||
}
|
||
|
||
# Validate meshcore availability and minimum version for a given Python interpreter.
|
||
check_meshcore_version() {
|
||
local python_cmd="$1"
|
||
local context="$2"
|
||
local min_version="${3:-$MIN_MESHCORE_VERSION}"
|
||
|
||
if ! command -v "$python_cmd" &> /dev/null; then
|
||
print_warning "Python command '$python_cmd' not found during $context"
|
||
return 1
|
||
fi
|
||
|
||
local installed_version
|
||
installed_version=$("$python_cmd" -c "import meshcore; print(getattr(meshcore, '__version__', '0.0.0'))" 2>/dev/null || true)
|
||
if [ -z "$installed_version" ]; then
|
||
print_warning "meshcore not available during $context"
|
||
print_info "Install or upgrade meshcore to version $min_version or newer"
|
||
print_info "Manual update command: $python_cmd -m pip install --upgrade \"meshcore>=$min_version\""
|
||
return 1
|
||
fi
|
||
|
||
if ! is_version_at_least "$python_cmd" "$installed_version" "$min_version"; then
|
||
print_warning "meshcore $installed_version detected during $context"
|
||
print_info "meshcore $min_version or newer is required for multi-byte path support"
|
||
print_info "Manual update command: $python_cmd -m pip install --upgrade \"meshcore>=$min_version\""
|
||
return 1
|
||
fi
|
||
|
||
print_info "meshcore version check passed ($installed_version >= $min_version)"
|
||
return 0
|
||
}
|
||
|
||
# Create runtime launcher with meshcore version guard for services and Docker.
|
||
create_runtime_launcher() {
|
||
local launcher_file="$INSTALL_DIR/start_packet_capture.sh"
|
||
cat > "$launcher_file" << EOF
|
||
#!/bin/sh
|
||
set -e
|
||
|
||
MIN_MESHCORE_VERSION="\${MIN_MESHCORE_VERSION:-$MIN_MESHCORE_VERSION}"
|
||
PYTHON_BIN="\${PYTHON_BIN:-python3}"
|
||
SCRIPT_DIR="\$(cd "\$(dirname "\$0")" && pwd)"
|
||
|
||
if ! command -v "\$PYTHON_BIN" >/dev/null 2>&1; then
|
||
echo "ERROR: Python interpreter '\$PYTHON_BIN' not found."
|
||
exit 1
|
||
fi
|
||
|
||
INSTALLED_MESHCORE_VERSION=\$("\$PYTHON_BIN" -c "import meshcore; print(getattr(meshcore, '__version__', '0.0.0'))" 2>/dev/null || true)
|
||
if [ -z "\$INSTALLED_MESHCORE_VERSION" ]; then
|
||
echo "ERROR: meshcore is not installed for '\$PYTHON_BIN'."
|
||
echo "ERROR: Install meshcore >= \$MIN_MESHCORE_VERSION for multi-byte path support."
|
||
echo "ERROR: Manual update command: \$PYTHON_BIN -m pip install --upgrade \"meshcore>=\$MIN_MESHCORE_VERSION\""
|
||
exit 1
|
||
fi
|
||
|
||
if ! "\$PYTHON_BIN" - "\$INSTALLED_MESHCORE_VERSION" "\$MIN_MESHCORE_VERSION" << 'PY'
|
||
import re
|
||
import sys
|
||
|
||
installed = sys.argv[1]
|
||
minimum = sys.argv[2]
|
||
|
||
def normalize(version: str):
|
||
parts = [int(x) for x in re.findall(r"\d+", version)]
|
||
return parts or [0]
|
||
|
||
left = normalize(installed)
|
||
right = normalize(minimum)
|
||
size = max(len(left), len(right))
|
||
left += [0] * (size - len(left))
|
||
right += [0] * (size - len(right))
|
||
sys.exit(0 if left >= right else 1)
|
||
PY
|
||
then
|
||
echo "ERROR: meshcore \$INSTALLED_MESHCORE_VERSION is too old."
|
||
echo "ERROR: meshcore >= \$MIN_MESHCORE_VERSION is required for multi-byte path support."
|
||
echo "ERROR: Manual update command: \$PYTHON_BIN -m pip install --upgrade \"meshcore>=\$MIN_MESHCORE_VERSION\""
|
||
exit 1
|
||
fi
|
||
|
||
exec "\$PYTHON_BIN" "\$SCRIPT_DIR/packet_capture.py"
|
||
EOF
|
||
chmod +x "$launcher_file"
|
||
}
|
||
|
||
# Detect available serial devices
|
||
detect_serial_devices() {
|
||
local devices=()
|
||
|
||
if [ "$(uname)" = "Darwin" ]; then
|
||
# macOS: Use /dev/cu.* devices (callout devices, preferred over tty.*)
|
||
# Look for common USB serial adapters
|
||
while IFS= read -r device; do
|
||
devices+=("$device")
|
||
done < <(ls /dev/cu.usb* /dev/cu.wchusbserial* /dev/cu.SLAB_USBtoUART* 2>/dev/null | sort)
|
||
elif [ "$(uname)" = "FreeBSD" ]; then
|
||
# FreeBSD: Use /dev/cuaU* devices (callout devices, preferred over ttyU*)
|
||
while IFS= read -r device; do
|
||
devices+=("$device")
|
||
done < <(ls /dev/cuaU* | grep -v -E '\.(lock|init)$' 2>/dev/null | sort)
|
||
else
|
||
# Linux: Prefer /dev/serial/by-id/ for persistent naming
|
||
if [ -d /dev/serial/by-id ]; then
|
||
while IFS= read -r device; do
|
||
devices+=("$device")
|
||
done < <(ls -1 /dev/serial/by-id/ 2>/dev/null | sed 's|^|/dev/serial/by-id/|')
|
||
fi
|
||
|
||
# Also check /dev/ttyACM* and /dev/ttyUSB* as fallback
|
||
while IFS= read -r device; do
|
||
# Only add if not already in list via by-id
|
||
local already_added=false
|
||
for existing in "${devices[@]}"; do
|
||
if [ "$(readlink -f "$existing" 2>/dev/null)" = "$device" ]; then
|
||
already_added=true
|
||
break
|
||
fi
|
||
done
|
||
if [ "$already_added" = false ]; then
|
||
devices+=("$device")
|
||
fi
|
||
done < <(ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null | sort)
|
||
fi
|
||
|
||
printf '%s\n' "${devices[@]}"
|
||
}
|
||
|
||
# Scan for BLE devices using Python helper
|
||
scan_ble_devices() {
|
||
echo ""
|
||
print_info "Scanning for BLE devices..."
|
||
echo "This may take 10-15 seconds..."
|
||
echo ""
|
||
|
||
# Check if Python and meshcore are available
|
||
if ! command -v python3 &> /dev/null; then
|
||
print_warning "Python3 not found - cannot scan for BLE devices"
|
||
return 1
|
||
fi
|
||
|
||
# Check if bleak is available (meshcore is validated separately below)
|
||
if ! python3 -c "import bleak" 2>/dev/null; then
|
||
print_warning "bleak not available - cannot scan for BLE devices"
|
||
print_info "BLE scanning requires the meshcore library and its dependencies"
|
||
print_info "These will be installed after the main installation completes"
|
||
return 1
|
||
fi
|
||
|
||
if ! check_meshcore_version "python3" "BLE scanning preflight"; then
|
||
print_warning "Cannot scan for BLE devices with incompatible meshcore version"
|
||
return 1
|
||
fi
|
||
|
||
# Create a temporary BLE scan helper script
|
||
local temp_script="/tmp/ble_scan_helper.py"
|
||
cat > "$temp_script" << 'EOF'
|
||
#!/usr/bin/env python3
|
||
"""
|
||
BLE Device Scanner Helper for MeshCore Packet Capture Installer
|
||
Uses the meshcore library to scan for MeshCore BLE devices
|
||
"""
|
||
|
||
import asyncio
|
||
import sys
|
||
import json
|
||
from bleak import BleakScanner
|
||
from bleak.backends.device import BLEDevice
|
||
from bleak.backends.scanner import AdvertisementData
|
||
|
||
async def scan_ble_devices():
|
||
"""Scan for MeshCore BLE devices using BleakScanner"""
|
||
try:
|
||
print("Scanning for MeshCore BLE devices...", file=sys.stderr, flush=True)
|
||
|
||
# Scan for all devices first, then filter
|
||
devices = await BleakScanner.discover(timeout=10.0)
|
||
|
||
# Filter to only MeshCore devices
|
||
meshcore_devices = []
|
||
for device in devices:
|
||
if device.name:
|
||
# Check for MeshCore-* or Meshcore-* devices
|
||
if device.name.startswith("MeshCore-") or device.name.startswith("Meshcore-"):
|
||
meshcore_devices.append(device)
|
||
# Also check for T1000 devices
|
||
elif "T1000" in device.name:
|
||
meshcore_devices.append(device)
|
||
|
||
devices = meshcore_devices
|
||
|
||
if not devices:
|
||
print("No MeshCore BLE devices found", file=sys.stderr, flush=True)
|
||
return []
|
||
|
||
# Format devices for the installer
|
||
formatted_devices = []
|
||
for device in devices:
|
||
device_info = {
|
||
"address": device.address,
|
||
"name": device.name or "Unknown",
|
||
"rssi": None # RSSI is not easily accessible in this context
|
||
}
|
||
formatted_devices.append(device_info)
|
||
|
||
# Output as JSON for the installer to parse
|
||
print(json.dumps(formatted_devices), flush=True)
|
||
return formatted_devices
|
||
|
||
except Exception as e:
|
||
print(f"Error scanning for BLE devices: {e}", file=sys.stderr, flush=True)
|
||
return []
|
||
|
||
def main():
|
||
"""Main function to run the BLE scan"""
|
||
try:
|
||
devices = asyncio.run(scan_ble_devices())
|
||
if not devices:
|
||
sys.exit(1)
|
||
except KeyboardInterrupt:
|
||
print("Scan interrupted by user", file=sys.stderr, flush=True)
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"Unexpected error: {e}", file=sys.stderr, flush=True)
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
EOF
|
||
|
||
# Run the BLE scan helper
|
||
local scan_output
|
||
local scan_error
|
||
if scan_output=$(python3 "$temp_script" 2>/tmp/ble_scan_error); then
|
||
# Parse JSON output
|
||
local devices_json="$scan_output"
|
||
local device_count=$(echo "$devices_json" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data))" 2>/dev/null)
|
||
|
||
# Check if device_count is a valid number
|
||
if ! [[ "$device_count" =~ ^[0-9]+$ ]]; then
|
||
print_warning "Failed to parse BLE scan results"
|
||
return 1
|
||
fi
|
||
|
||
if [ "$device_count" -eq 0 ]; then
|
||
print_warning "No MeshCore BLE devices found"
|
||
return 1
|
||
fi
|
||
|
||
print_success "Found $device_count MeshCore BLE device(s):"
|
||
echo ""
|
||
|
||
# Display devices and store device info
|
||
local i=1
|
||
local device_addresses=()
|
||
local device_names=()
|
||
|
||
# Parse devices and store in arrays
|
||
while IFS= read -r device_info; do
|
||
local name=$(echo "$device_info" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('name', 'Unknown'))")
|
||
local address=$(echo "$device_info" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('address', 'Unknown'))")
|
||
|
||
echo " $i) $name ($address)"
|
||
device_addresses+=("$address")
|
||
device_names+=("$name")
|
||
((i++))
|
||
done < <(echo "$devices_json" | python3 -c "import sys, json; data=json.load(sys.stdin); [print(json.dumps(device)) for device in data]")
|
||
|
||
echo " $((device_count + 1))) Enter device manually"
|
||
echo " 0) Scan again"
|
||
echo ""
|
||
|
||
while true; do
|
||
local choice=$(prompt_input "Select device [0-$((device_count + 1))]" "1")
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 0 ] && [ "$choice" -le $((device_count + 1)) ]; then
|
||
if [ "$choice" -eq 0 ]; then
|
||
# Rescan for devices
|
||
echo ""
|
||
print_info "Rescanning for BLE devices..."
|
||
return 2 # Special return code to indicate rescan requested
|
||
elif [ "$choice" -eq $((device_count + 1)) ]; then
|
||
# Manual entry - use existing values as defaults
|
||
EXISTING_BLE_DEVICE=$(read_env_value "PACKETCAPTURE_BLE_DEVICE")
|
||
EXISTING_BLE_NAME=$(read_env_value "PACKETCAPTURE_BLE_NAME")
|
||
local manual_mac=$(prompt_input "Enter BLE device MAC address" "$EXISTING_BLE_DEVICE")
|
||
local manual_name=$(prompt_input "Enter device name (optional)" "$EXISTING_BLE_NAME")
|
||
if [ -n "$manual_mac" ]; then
|
||
SELECTED_BLE_DEVICE="$manual_mac"
|
||
SELECTED_BLE_NAME="$manual_name"
|
||
return 0
|
||
fi
|
||
else
|
||
# Selected from list
|
||
local device_index=$((choice - 1))
|
||
SELECTED_BLE_DEVICE="${device_addresses[$device_index]}"
|
||
SELECTED_BLE_NAME="${device_names[$device_index]}"
|
||
return 0
|
||
fi
|
||
else
|
||
print_error "Invalid choice. Please enter a number between 0 and $((device_count + 1))"
|
||
fi
|
||
done
|
||
else
|
||
print_warning "Failed to scan for BLE devices using meshcore library"
|
||
if [ -f /tmp/ble_scan_error ]; then
|
||
local error_msg=$(cat /tmp/ble_scan_error)
|
||
if [ -n "$error_msg" ]; then
|
||
print_info "Error details: $error_msg"
|
||
fi
|
||
rm -f /tmp/ble_scan_error
|
||
fi
|
||
/tmp/device_list
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Check BLE pairing status and handle pairing if needed
|
||
handle_ble_pairing() {
|
||
local device_address="$1"
|
||
local device_name="$2"
|
||
|
||
echo ""
|
||
print_info "Checking BLE pairing status for $device_name ($device_address)..."
|
||
|
||
if [ -z "$device_name" ] || [ -z "$device_address" ]; then
|
||
print_error "Invalid device information: name='$device_name', address='$device_address'"
|
||
return 1
|
||
fi
|
||
|
||
# Use the actual ble_pairing_helper.py script instead of embedded code
|
||
local temp_script="$INSTALL_DIR/ble_pairing_helper.py"
|
||
|
||
# Check if script exists
|
||
if [ ! -f "$temp_script" ]; then
|
||
print_error "BLE pairing helper script not found at $temp_script"
|
||
print_info "This should have been installed earlier. Continuing without pairing check..."
|
||
return 1
|
||
fi
|
||
|
||
# Determine which Python to use (prefer venv if it exists, otherwise system)
|
||
local python_cmd="python3"
|
||
if [ -f "$INSTALL_DIR/venv/bin/python3" ]; then
|
||
python_cmd="$INSTALL_DIR/venv/bin/python3"
|
||
print_info "Using virtual environment Python"
|
||
else
|
||
print_info "Using system Python (venv not yet created)"
|
||
fi
|
||
|
||
# Check bleak availability first (meshcore version is validated separately)
|
||
if ! "$python_cmd" -c "import bleak" 2>/dev/null; then
|
||
print_warning "BLE dependency bleak is not available yet"
|
||
print_info "The virtual environment will be set up after device configuration."
|
||
print_info "You may need to pair the device manually, or re-run the installer after dependencies are installed."
|
||
return 1
|
||
fi
|
||
|
||
if ! check_meshcore_version "$python_cmd" "BLE pairing preflight"; then
|
||
print_warning "Skipping automatic pairing check due to incompatible meshcore version"
|
||
return 1
|
||
fi
|
||
|
||
# Pre-pairing disconnect to ensure device is available
|
||
if command -v bluetoothctl &> /dev/null; then
|
||
print_info "Ensuring device is disconnected before pairing check..."
|
||
bluetoothctl disconnect "$device_address" 2>/dev/null || true
|
||
print_info "Waiting for device to become available..."
|
||
sleep 5 # Increased wait time for device to become available
|
||
fi
|
||
|
||
# Quick pairing check first (shorter timeout - if already paired, we're done)
|
||
local pairing_output
|
||
print_info "Checking if device is already paired..."
|
||
print_info "Device: $device_name ($device_address)"
|
||
|
||
# Run with explicit stderr redirection and capture
|
||
rm -f /tmp/ble_pairing_error
|
||
|
||
# Quick check with shorter timeout - just to see if already paired
|
||
if pairing_output=$(run_with_timeout 15 "$python_cmd" "$temp_script" "$device_address" "$device_name" 2>/tmp/ble_pairing_error); then
|
||
local pairing_status=$(echo "$pairing_output" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['status'])" 2>/dev/null)
|
||
|
||
if [ "$pairing_status" = "paired" ]; then
|
||
print_success "Device is already paired and ready to use"
|
||
rm -f /tmp/ble_pairing_error
|
||
return 0
|
||
fi
|
||
# If not paired, fall through to pairing attempt below
|
||
fi
|
||
|
||
# If check failed or device not paired, handle pairing based on OS
|
||
if [ "$(uname)" = "Darwin" ]; then
|
||
# On macOS, just attempt connection - system will show pairing dialog if needed
|
||
echo ""
|
||
print_info "Attempting to connect to device..."
|
||
print_info "If a pairing dialog appears, enter the PIN displayed on your MeshCore device."
|
||
echo ""
|
||
|
||
rm -f /tmp/ble_pairing_error
|
||
local exit_code=0
|
||
pairing_output=$(run_with_timeout 60 "$python_cmd" "$temp_script" "$device_address" "$device_name" 2>/tmp/ble_pairing_error) || exit_code=$?
|
||
else
|
||
# On Linux/Windows, prompt for PIN and attempt PIN-based pairing
|
||
echo ""
|
||
print_info "Device needs to be paired. You'll need to enter the PIN displayed on your MeshCore device."
|
||
print_info "Make sure your device is powered on and showing the 6-digit PIN."
|
||
echo ""
|
||
|
||
# Get PIN from user
|
||
local pin
|
||
while true; do
|
||
pin=$(prompt_input "Enter the 6-digit PIN displayed on your MeshCore device" "")
|
||
if [[ "$pin" =~ ^[0-9]{6}$ ]]; then
|
||
break
|
||
else
|
||
print_error "Please enter a 6-digit PIN (numbers only)"
|
||
fi
|
||
done
|
||
|
||
# Attempt pairing with PIN
|
||
echo ""
|
||
print_info "Attempting to pair with device (this may take up to 60 seconds)..."
|
||
rm -f /tmp/ble_pairing_error
|
||
local exit_code=0
|
||
pairing_output=$(run_with_timeout 60 "$python_cmd" "$temp_script" "$device_address" "$device_name" "$pin" 2>/tmp/ble_pairing_error) || exit_code=$?
|
||
fi
|
||
|
||
# Check if we got JSON output (script ran and produced output)
|
||
if [ -n "$pairing_output" ]; then
|
||
# Try to parse JSON from the output (may be mixed with other output)
|
||
local pairing_result=$(echo "$pairing_output" | grep -o '{"status"[^}]*}' | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('status', 'unknown'))" 2>/dev/null)
|
||
local pairing_message=$(echo "$pairing_output" | grep -o '{"status"[^}]*}' | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('message', ''))" 2>/dev/null)
|
||
|
||
# If JSON parsing failed, try parsing the whole output as JSON
|
||
if [ -z "$pairing_result" ] || [ "$pairing_result" = "unknown" ]; then
|
||
pairing_result=$(echo "$pairing_output" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('status', 'unknown'))" 2>/dev/null)
|
||
pairing_message=$(echo "$pairing_output" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('message', ''))" 2>/dev/null)
|
||
fi
|
||
|
||
if [ "$pairing_result" = "paired" ]; then
|
||
print_success "BLE pairing successful! Device is now ready to use."
|
||
rm -f /tmp/ble_pairing_error
|
||
return 0
|
||
elif [ "$pairing_result" = "pairing_failed" ] || [ "$pairing_result" = "error" ] || [ "$pairing_result" = "not_paired" ]; then
|
||
# Script ran but pairing failed or device not paired
|
||
if [ "$(uname)" = "Darwin" ] && [ "$pairing_result" = "not_paired" ]; then
|
||
# On macOS, if not paired, the system dialog should have appeared
|
||
# Try one more time to see if user completed pairing via dialog
|
||
print_info "Checking if pairing was completed via system dialog..."
|
||
sleep 2
|
||
rm -f /tmp/ble_pairing_error
|
||
if retry_output=$(run_with_timeout 15 "$python_cmd" "$temp_script" "$device_address" "$device_name" 2>/tmp/ble_pairing_error); then
|
||
local retry_status=$(echo "$retry_output" | grep -o '{"status"[^}]*}' | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('status', 'unknown'))" 2>/dev/null)
|
||
if [ "$retry_status" = "paired" ]; then
|
||
print_success "BLE pairing successful! Device is now ready to use."
|
||
rm -f /tmp/ble_pairing_error
|
||
return 0
|
||
fi
|
||
fi
|
||
print_error "BLE pairing failed or not completed"
|
||
print_info "Please ensure you completed the pairing dialog if it appeared, or try again."
|
||
else
|
||
# Linux/Windows or other error - show the actual error message from JSON
|
||
print_error "BLE pairing failed"
|
||
if [ -n "$pairing_message" ] && [ "$pairing_message" != "None" ]; then
|
||
print_info "Error: $pairing_message"
|
||
fi
|
||
fi
|
||
if [ -f /tmp/ble_pairing_error ]; then
|
||
local error_msg=$(cat /tmp/ble_pairing_error | tail -20) # Last 20 lines to avoid too much output
|
||
if [ -n "$error_msg" ]; then
|
||
print_info "Details: $error_msg"
|
||
fi
|
||
rm -f /tmp/ble_pairing_error
|
||
fi
|
||
return 1
|
||
else
|
||
# JSON parsed but status is unknown or unexpected
|
||
print_error "BLE pairing failed"
|
||
if [ -n "$pairing_message" ] && [ "$pairing_message" != "None" ]; then
|
||
print_info "Error: $pairing_message"
|
||
fi
|
||
if [ -f /tmp/ble_pairing_error ]; then
|
||
local error_msg=$(cat /tmp/ble_pairing_error | tail -20)
|
||
if [ -n "$error_msg" ]; then
|
||
print_info "Details: $error_msg"
|
||
fi
|
||
rm -f /tmp/ble_pairing_error
|
||
fi
|
||
return 1
|
||
fi
|
||
else
|
||
# No output - script may have failed immediately or timed out
|
||
# Check exit code to determine if it was a timeout
|
||
if [ "$exit_code" -eq 124 ]; then
|
||
# Exit code 124 means timeout command killed the process
|
||
print_error "BLE pairing timed out"
|
||
print_info "The pairing process took too long. The device may be busy or not responding."
|
||
print_info "Make sure:"
|
||
print_info " • The device is powered on and showing the PIN"
|
||
print_info " • The device is in pairing mode"
|
||
print_info " • You entered the correct 6-digit PIN"
|
||
print_info " • The device is within range"
|
||
else
|
||
# Script failed immediately or with an error
|
||
# Check if we can extract JSON from stderr (script might have output JSON to stderr)
|
||
local json_from_stderr=""
|
||
if [ -f /tmp/ble_pairing_error ]; then
|
||
# Try to extract JSON from stderr
|
||
json_from_stderr=$(cat /tmp/ble_pairing_error | grep -o '{"status"[^}]*}' | head -1)
|
||
fi
|
||
|
||
if [ -n "$json_from_stderr" ]; then
|
||
# Found JSON in stderr - parse it
|
||
local pairing_result=$(echo "$json_from_stderr" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('status', 'unknown'))" 2>/dev/null)
|
||
local pairing_message=$(echo "$json_from_stderr" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('message', ''))" 2>/dev/null)
|
||
|
||
if [ "$pairing_result" = "pairing_failed" ] || [ "$pairing_result" = "error" ]; then
|
||
print_error "BLE pairing failed"
|
||
if [ -n "$pairing_message" ] && [ "$pairing_message" != "None" ]; then
|
||
print_info "Error: $pairing_message"
|
||
fi
|
||
rm -f /tmp/ble_pairing_error
|
||
return 1
|
||
elif [ "$pairing_result" = "timeout" ]; then
|
||
print_error "BLE pairing timed out"
|
||
if [ -n "$pairing_message" ] && [ "$pairing_message" != "None" ]; then
|
||
print_info "Error: $pairing_message"
|
||
fi
|
||
rm -f /tmp/ble_pairing_error
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# No JSON found - check error message for specific patterns
|
||
if [ -f /tmp/ble_pairing_error ]; then
|
||
local error_msg=$(cat /tmp/ble_pairing_error | tail -30) # Last 30 lines
|
||
|
||
# Check for actual timeout errors (not just the word "timeout" in debug messages)
|
||
if [[ "$error_msg" == *"Connection timed out"* ]] || \
|
||
[[ "$error_msg" == *"Pairing connection timed out"* ]] || \
|
||
[[ "$error_msg" == *"timed out"* ]] && [[ "$error_msg" != *"timeout set to"* ]] && \
|
||
[[ "$error_msg" != *"Connection timeout set"* ]]; then
|
||
print_error "BLE pairing timed out"
|
||
print_info "The pairing process took too long. The device may be busy or not responding."
|
||
print_info "Make sure:"
|
||
print_info " • The device is powered on and showing the PIN"
|
||
print_info " • The device is in pairing mode"
|
||
print_info " • You entered the correct 6-digit PIN"
|
||
print_info " • The device is within range"
|
||
elif [[ "$error_msg" == *"Terminated"* ]] && [ "$exit_code" -eq 124 ]; then
|
||
# Only show timeout if exit code is 124 (actual timeout)
|
||
print_error "BLE pairing timed out"
|
||
print_info "The pairing process took too long. The device may be busy or not responding."
|
||
print_info "Make sure:"
|
||
print_info " • The device is powered on and showing the PIN"
|
||
print_info " • The device is in pairing mode"
|
||
print_info " • You entered the correct 6-digit PIN"
|
||
print_info " • The device is within range"
|
||
elif [[ "$error_msg" == *"Device not found"* ]] || \
|
||
[[ "$error_msg" == *"Device not available"* ]] || \
|
||
[[ "$error_msg" == *"not found or not in range"* ]] || \
|
||
[[ "$error_msg" == *"not available or not in pairing mode"* ]] || \
|
||
([[ "$error_msg" == *"not available"* ]] && [[ "$error_msg" != *"Device info not available yet"* ]]); then
|
||
print_error "Device not found or not available"
|
||
print_info "Make sure your MeshCore device is:"
|
||
print_info " • Powered on and within range"
|
||
print_info " • In pairing mode"
|
||
print_info " • Not connected to another device"
|
||
elif [[ "$error_msg" == *"Pairing failed"* ]] || [[ "$error_msg" == *"pairing_failed"* ]] || \
|
||
[[ "$error_msg" == *"Authentication failed"* ]] || [[ "$error_msg" == *"PIN may be incorrect"* ]]; then
|
||
print_error "BLE pairing failed"
|
||
print_info "Error details:"
|
||
echo "$error_msg" | grep -iE "pairing|failed|error|incorrect|expired" | head -5 | while read line; do
|
||
print_info " $line"
|
||
done
|
||
else
|
||
print_error "Failed to pair with device"
|
||
if [ -n "$error_msg" ]; then
|
||
print_info "Error details:"
|
||
echo "$error_msg" | tail -10 | while read line; do
|
||
print_info " $line"
|
||
done
|
||
fi
|
||
fi
|
||
rm -f /tmp/ble_pairing_error
|
||
else
|
||
print_error "Failed to pair with device - no error details available (exit code: $exit_code)"
|
||
fi
|
||
fi
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Select connection type and configure device
|
||
select_connection_type() {
|
||
echo ""
|
||
print_header "Device Connection Configuration"
|
||
echo ""
|
||
print_info "How would you like to connect to your MeshCore device?"
|
||
echo ""
|
||
echo " 1) Bluetooth Low Energy (BLE) - Recommended for T1000 devices"
|
||
echo " • Wireless connection"
|
||
echo " • Works with MeshCore T1000e and compatible devices"
|
||
echo ""
|
||
echo " 2) Serial Connection - For devices with USB/serial interface"
|
||
echo " • Direct USB or serial cable connection"
|
||
echo " • More reliable for continuous operation"
|
||
echo ""
|
||
echo " 3) TCP Connection - For network-connected devices"
|
||
echo " • Connect to your node over the network"
|
||
echo " • Works with ser2net or other TCP-to-serial bridges"
|
||
echo ""
|
||
|
||
# Read existing connection type and map to default choice number
|
||
EXISTING_CONNECTION_TYPE=$(read_env_value "PACKETCAPTURE_CONNECTION_TYPE")
|
||
DEFAULT_CHOICE="1" # Default to BLE
|
||
if [ -n "$EXISTING_CONNECTION_TYPE" ]; then
|
||
case "$EXISTING_CONNECTION_TYPE" in
|
||
"ble")
|
||
DEFAULT_CHOICE="1"
|
||
;;
|
||
"serial")
|
||
DEFAULT_CHOICE="2"
|
||
;;
|
||
"tcp")
|
||
DEFAULT_CHOICE="3"
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
while true; do
|
||
local choice=$(prompt_input "Select connection type [1-3]" "$DEFAULT_CHOICE")
|
||
case $choice in
|
||
1)
|
||
CONNECTION_TYPE="ble"
|
||
print_info "Selected: Bluetooth Low Energy (BLE)"
|
||
echo ""
|
||
|
||
if prompt_yes_no "Would you like to scan for nearby BLE devices?" "y"; then
|
||
while true; do
|
||
if scan_ble_devices; then
|
||
# Device selected, now handle pairing
|
||
if handle_ble_pairing "$SELECTED_BLE_DEVICE" "$SELECTED_BLE_NAME"; then
|
||
print_success "BLE device configured and paired: $SELECTED_BLE_NAME ($SELECTED_BLE_DEVICE)"
|
||
break
|
||
else
|
||
print_error "BLE pairing failed. Please try selecting a different device or check your device."
|
||
continue
|
||
fi
|
||
elif [ $? -eq 2 ]; then
|
||
# Rescan requested, continue the loop
|
||
continue
|
||
else
|
||
# Fallback to manual entry - use existing values as defaults
|
||
print_info "BLE scanning failed or no devices found. Please enter device details manually."
|
||
EXISTING_BLE_DEVICE=$(read_env_value "PACKETCAPTURE_BLE_DEVICE")
|
||
EXISTING_BLE_NAME=$(read_env_value "PACKETCAPTURE_BLE_NAME")
|
||
SELECTED_BLE_DEVICE=$(prompt_input "Enter BLE device MAC address" "$EXISTING_BLE_DEVICE")
|
||
SELECTED_BLE_NAME=$(prompt_input "Enter device name (optional)" "$EXISTING_BLE_NAME")
|
||
if [ -n "$SELECTED_BLE_DEVICE" ]; then
|
||
# Handle pairing for manually entered device
|
||
if handle_ble_pairing "$SELECTED_BLE_DEVICE" "$SELECTED_BLE_NAME"; then
|
||
print_success "BLE device configured and paired: $SELECTED_BLE_NAME ($SELECTED_BLE_DEVICE)"
|
||
break
|
||
else
|
||
print_error "BLE pairing failed. Please check your device and try again."
|
||
continue
|
||
fi
|
||
else
|
||
print_error "No BLE device configured"
|
||
continue
|
||
fi
|
||
fi
|
||
done
|
||
else
|
||
# Manual entry without scanning - use existing values as defaults
|
||
EXISTING_BLE_DEVICE=$(read_env_value "PACKETCAPTURE_BLE_DEVICE")
|
||
EXISTING_BLE_NAME=$(read_env_value "PACKETCAPTURE_BLE_NAME")
|
||
SELECTED_BLE_DEVICE=$(prompt_input "Enter BLE device MAC address" "$EXISTING_BLE_DEVICE")
|
||
SELECTED_BLE_NAME=$(prompt_input "Enter device name (optional)" "$EXISTING_BLE_NAME")
|
||
if [ -n "$SELECTED_BLE_DEVICE" ]; then
|
||
# Handle pairing for manually entered device
|
||
if handle_ble_pairing "$SELECTED_BLE_DEVICE" "$SELECTED_BLE_NAME"; then
|
||
print_success "BLE device configured and paired: $SELECTED_BLE_NAME ($SELECTED_BLE_DEVICE)"
|
||
else
|
||
print_error "BLE pairing failed. Please check your device and try again."
|
||
continue
|
||
fi
|
||
else
|
||
print_error "No BLE device configured"
|
||
continue
|
||
fi
|
||
fi
|
||
break
|
||
;;
|
||
2)
|
||
CONNECTION_TYPE="serial"
|
||
print_info "Selected: Serial Connection"
|
||
echo ""
|
||
select_serial_device
|
||
break
|
||
;;
|
||
3)
|
||
CONNECTION_TYPE="tcp"
|
||
print_info "Selected: TCP Connection"
|
||
echo ""
|
||
configure_tcp_connection
|
||
break
|
||
;;
|
||
*)
|
||
print_error "Invalid choice. Please enter 1, 2, or 3"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# Interactive device selection
|
||
# Sets SELECTED_SERIAL_DEVICE variable
|
||
select_serial_device() {
|
||
local devices=()
|
||
# Use readarray instead of mapfile for better compatibility
|
||
if command -v readarray >/dev/null 2>&1; then
|
||
readarray -t devices < <(detect_serial_devices)
|
||
else
|
||
# Fallback for systems without readarray
|
||
while IFS= read -r line; do
|
||
devices+=("$line")
|
||
done < <(detect_serial_devices)
|
||
fi
|
||
|
||
echo ""
|
||
print_header "Serial Device Selection"
|
||
echo ""
|
||
|
||
if [ ${#devices[@]} -eq 0 ]; then
|
||
print_warning "No serial devices detected"
|
||
echo ""
|
||
echo " 1) Enter path manually"
|
||
echo ""
|
||
local choice=$(prompt_input "Select option [1]" "1")
|
||
|
||
# Read existing serial device from install directory's .env.local as default
|
||
EXISTING_SERIAL_DEVICE=$(read_env_value "PACKETCAPTURE_SERIAL_PORTS")
|
||
SERIAL_DEVICE_DEFAULT="${EXISTING_SERIAL_DEVICE:-/dev/ttyACM0}"
|
||
|
||
SELECTED_SERIAL_DEVICE=$(prompt_input "Enter serial device path" "$SERIAL_DEVICE_DEFAULT")
|
||
return
|
||
fi
|
||
|
||
if [ ${#devices[@]} -eq 1 ]; then
|
||
print_info "Found 1 serial device:"
|
||
else
|
||
print_info "Found ${#devices[@]} serial devices:"
|
||
fi
|
||
echo ""
|
||
|
||
local i=1
|
||
for device in "${devices[@]}"; do
|
||
# Try to get device info
|
||
local info=""
|
||
if [ "$(uname)" = "Darwin" ]; then
|
||
# macOS: device name is usually descriptive
|
||
info="$device"
|
||
else
|
||
# Linux: show both by-id path and resolved device
|
||
if [[ "$device" == /dev/serial/by-id/* ]]; then
|
||
local resolved=$(readlink -f "$device" 2>/dev/null)
|
||
info="$device -> $resolved"
|
||
else
|
||
info="$device"
|
||
fi
|
||
fi
|
||
echo " $i) $info"
|
||
((i++))
|
||
done
|
||
|
||
echo " $i) Enter path manually"
|
||
echo ""
|
||
|
||
while true; do
|
||
local choice=$(prompt_input "Select device [1-$i]" "1")
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le $i ]; then
|
||
if [ "$choice" -eq $i ]; then
|
||
# Manual entry - use existing value as default
|
||
EXISTING_SERIAL_DEVICE=$(read_env_value "PACKETCAPTURE_SERIAL_PORTS")
|
||
SERIAL_DEVICE_DEFAULT="${EXISTING_SERIAL_DEVICE:-/dev/ttyACM0}"
|
||
SELECTED_SERIAL_DEVICE=$(prompt_input "Enter serial device path" "$SERIAL_DEVICE_DEFAULT")
|
||
return
|
||
else
|
||
# Selected from list
|
||
SELECTED_SERIAL_DEVICE="${devices[$((choice-1))]}"
|
||
return
|
||
fi
|
||
else
|
||
print_error "Invalid selection. Please enter a number between 1 and $i"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# Configure TCP connection
|
||
configure_tcp_connection() {
|
||
echo ""
|
||
print_header "TCP Connection Configuration"
|
||
echo ""
|
||
print_info "TCP connections work with ser2net or other TCP-to-serial bridges"
|
||
print_info "This allows you to access serial devices over the network"
|
||
echo ""
|
||
|
||
# Read existing values from install directory's .env.local as defaults
|
||
EXISTING_TCP_HOST=$(read_env_value "PACKETCAPTURE_TCP_HOST")
|
||
EXISTING_TCP_PORT=$(read_env_value "PACKETCAPTURE_TCP_PORT")
|
||
|
||
# Use existing values as defaults, or fall back to standard defaults
|
||
TCP_HOST_DEFAULT="${EXISTING_TCP_HOST:-localhost}"
|
||
TCP_PORT_DEFAULT="${EXISTING_TCP_PORT:-5000}"
|
||
|
||
TCP_HOST=$(prompt_input "TCP host/address" "$TCP_HOST_DEFAULT")
|
||
TCP_PORT=$(prompt_input "TCP port" "$TCP_PORT_DEFAULT")
|
||
|
||
# Validate port number
|
||
if ! [[ "$TCP_PORT" =~ ^[0-9]+$ ]] || [ "$TCP_PORT" -lt 1 ] || [ "$TCP_PORT" -gt 65535 ]; then
|
||
print_error "Invalid port number. Using default port 5000"
|
||
TCP_PORT="5000"
|
||
fi
|
||
|
||
print_success "TCP connection configured: $TCP_HOST:$TCP_PORT"
|
||
echo ""
|
||
}
|
||
|
||
prompt_yes_no() {
|
||
local prompt="$1"
|
||
local default="${2:-n}"
|
||
local response
|
||
|
||
if [ "$default" = "y" ]; then
|
||
prompt="$prompt [Y/n]: "
|
||
else
|
||
prompt="$prompt [y/N]: "
|
||
fi
|
||
|
||
# Read from /dev/tty to work when stdin is piped
|
||
read -p "$prompt" response </dev/tty
|
||
response=${response:-$default}
|
||
|
||
case "$response" in
|
||
[yY][eE][sS]|[yY]) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
prompt_input() {
|
||
local prompt="$1"
|
||
local default="$2"
|
||
local response
|
||
|
||
# Read from /dev/tty to work when stdin is piped
|
||
if [ -n "$default" ]; then
|
||
read -p "$prompt [$default]: " response </dev/tty
|
||
echo "${response:-$default}"
|
||
else
|
||
read -p "$prompt: " response </dev/tty
|
||
echo "$response"
|
||
fi
|
||
}
|
||
|
||
# Helper function to read a value from .env.local in the install directory
|
||
# Also checks backup files if the main file doesn't exist, doesn't have the value, or has placeholder values
|
||
read_env_value() {
|
||
local key="$1"
|
||
local value=""
|
||
|
||
# Ensure we use the install directory, not the working directory
|
||
if [ -z "$INSTALL_DIR" ]; then
|
||
echo ""
|
||
return
|
||
fi
|
||
local env_file="$INSTALL_DIR/.env.local"
|
||
|
||
# First try the main .env.local file
|
||
if [ -f "$env_file" ]; then
|
||
value=$(grep "^${key}=" "$env_file" 2>/dev/null | cut -d'=' -f2- | sed 's/^"//;s/"$//')
|
||
# If we found a value and it's not a placeholder (XXX, empty, etc.), use it
|
||
if [ -n "$value" ] && [ "$value" != "XXX" ]; then
|
||
echo "$value"
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# If not found or placeholder value, check backup files (most recent first)
|
||
# Backup files are named .env.local.backup-YYYYMMDD-HHMMSS
|
||
local backup_file=$(ls -t "$INSTALL_DIR"/.env.local.backup-* 2>/dev/null | head -1)
|
||
if [ -n "$backup_file" ] && [ -f "$backup_file" ]; then
|
||
value=$(grep "^${key}=" "$backup_file" 2>/dev/null | cut -d'=' -f2- | sed 's/^"//;s/"$//')
|
||
# Only return if we have a non-placeholder value
|
||
if [ -n "$value" ] && [ "$value" != "XXX" ]; then
|
||
echo "$value"
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# If we got here, return the value from main file (even if XXX) or empty
|
||
# This preserves the behavior for cases where we want to know if it's XXX
|
||
echo "$value"
|
||
}
|
||
|
||
# Configure JWT token options (owner public key and client agent)
|
||
configure_jwt_options() {
|
||
ENV_LOCAL="$INSTALL_DIR/.env.local"
|
||
|
||
# Read existing values as defaults
|
||
EXISTING_OWNER_KEY=$(read_env_value "PACKETCAPTURE_OWNER_PUBLIC_KEY")
|
||
EXISTING_OWNER_EMAIL=$(read_env_value "PACKETCAPTURE_OWNER_EMAIL")
|
||
|
||
echo ""
|
||
print_header "JWT Token Configuration (Optional)"
|
||
echo ""
|
||
print_info "You can optionally configure owner information for Let's Mesh Analyzer:"
|
||
echo ""
|
||
print_info "1. Owner Public Key: The public key of the owner of the MQTT observer"
|
||
print_info " (64 hex characters, same length as repeater public key)"
|
||
echo ""
|
||
print_info "2. Owner Email: Email address of the observer owner"
|
||
echo ""
|
||
|
||
# Prompt for owner public key
|
||
if prompt_yes_no "Would you like to configure an owner public key?" "n"; then
|
||
while true; do
|
||
owner_key=$(prompt_input "Enter owner public key (64 hex characters)" "$EXISTING_OWNER_KEY")
|
||
owner_key=$(echo "$owner_key" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
|
||
|
||
if [ -z "$owner_key" ]; then
|
||
print_warning "Owner public key cannot be empty"
|
||
if ! prompt_yes_no "Skip owner public key configuration?" "y"; then
|
||
continue
|
||
else
|
||
break
|
||
fi
|
||
elif [ ${#owner_key} -ne 64 ]; then
|
||
print_error "Owner public key must be exactly 64 hex characters (you entered ${#owner_key})"
|
||
if ! prompt_yes_no "Try again?" "y"; then
|
||
break
|
||
fi
|
||
elif ! [[ "$owner_key" =~ ^[0-9A-F]{64}$ ]]; then
|
||
print_error "Owner public key must contain only hexadecimal characters (0-9, A-F)"
|
||
if ! prompt_yes_no "Try again?" "y"; then
|
||
break
|
||
fi
|
||
else
|
||
echo "PACKETCAPTURE_OWNER_PUBLIC_KEY=$owner_key" >> "$ENV_LOCAL"
|
||
print_success "Owner public key configured"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Prompt for owner email
|
||
echo ""
|
||
if prompt_yes_no "Would you like to configure an owner email for Let's Mesh Analyzer?" "n"; then
|
||
while true; do
|
||
email=$(prompt_input "Enter owner email address" "$EXISTING_OWNER_EMAIL")
|
||
email=$(echo "$email" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
||
|
||
if [ -z "$email" ]; then
|
||
print_warning "Email cannot be empty"
|
||
if ! prompt_yes_no "Skip email configuration?" "y"; then
|
||
continue
|
||
else
|
||
break
|
||
fi
|
||
else
|
||
# Validate email format using a simple regex check
|
||
if echo "$email" | grep -qE '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
|
||
echo "PACKETCAPTURE_OWNER_EMAIL=$email" >> "$ENV_LOCAL"
|
||
print_success "Owner email configured: $email"
|
||
break
|
||
else
|
||
print_error "Invalid email format. Please enter a valid email address (e.g., user@example.com)"
|
||
if ! prompt_yes_no "Try again?" "y"; then
|
||
break
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Client agent is automatically set to the build string (same as status messages)
|
||
# No user configuration needed
|
||
}
|
||
|
||
# Configure MQTT topics for a broker
|
||
configure_mqtt_topics() {
|
||
local BROKER_NUM=$1
|
||
local USE_DEFAULT_ONLY=${2:-false} # Optional second parameter: if true, skip prompt and use default
|
||
ENV_LOCAL="$INSTALL_DIR/.env.local"
|
||
|
||
echo ""
|
||
print_header "MQTT Topic Configuration for Broker $BROKER_NUM"
|
||
echo ""
|
||
print_info "MQTT topics define where different types of data are published."
|
||
print_info "You can use template variables: {IATA}, {IATA_lower}, {PUBLIC_KEY}"
|
||
echo ""
|
||
|
||
# If USE_DEFAULT_ONLY is true (e.g., for LetsMesh), skip prompt and use default pattern
|
||
if [ "$USE_DEFAULT_ONLY" = "true" ]; then
|
||
echo "" >> "$ENV_LOCAL"
|
||
echo "# MQTT Topics for Broker $BROKER_NUM - Default Pattern" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_STATUS=meshcore/{IATA}/{PUBLIC_KEY}/status" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_PACKETS=meshcore/{IATA}/{PUBLIC_KEY}/packets" >> "$ENV_LOCAL"
|
||
print_success "Default pattern topics configured"
|
||
return 0
|
||
fi
|
||
|
||
# Topic options
|
||
echo "Choose topic configuration:"
|
||
echo " 1) Default pattern (meshcore/{IATA}/{PUBLIC_KEY}/status, meshcore/{IATA}/{PUBLIC_KEY}/packets)"
|
||
echo " 2) Classic pattern (meshcore/status, meshcore/packets, meshcore/raw)"
|
||
echo " 3) Custom topics (enter your own)"
|
||
echo ""
|
||
|
||
local topic_choice=$(prompt_input "Select topic configuration [1-3]" "1")
|
||
|
||
case "$topic_choice" in
|
||
1)
|
||
# Default pattern (IATA + PUBLIC_KEY)
|
||
echo "" >> "$ENV_LOCAL"
|
||
echo "# MQTT Topics for Broker $BROKER_NUM - Default Pattern" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_STATUS=meshcore/{IATA}/{PUBLIC_KEY}/status" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_PACKETS=meshcore/{IATA}/{PUBLIC_KEY}/packets" >> "$ENV_LOCAL"
|
||
print_success "Default pattern topics configured"
|
||
;;
|
||
2)
|
||
# Classic pattern (simple meshcore topics, needed for map.w0z.is)
|
||
echo "" >> "$ENV_LOCAL"
|
||
echo "# MQTT Topics for Broker $BROKER_NUM - Classic Pattern" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_STATUS=meshcore/status" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_PACKETS=meshcore/packets" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_RAW=meshcore/raw" >> "$ENV_LOCAL"
|
||
print_success "Classic pattern topics configured"
|
||
;;
|
||
3)
|
||
# Custom topics
|
||
echo ""
|
||
print_info "Enter custom topic paths (use {IATA}, {IATA_lower}, {PUBLIC_KEY} for templates)"
|
||
print_info "You can also manually edit the .env.local file after installation to customize topics"
|
||
echo ""
|
||
|
||
# Read existing topic values from install directory's .env.local as defaults
|
||
EXISTING_STATUS_TOPIC=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_STATUS")
|
||
EXISTING_PACKETS_TOPIC=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_PACKETS")
|
||
|
||
STATUS_TOPIC_DEFAULT="${EXISTING_STATUS_TOPIC:-meshcore/{IATA}/{PUBLIC_KEY}/status}"
|
||
PACKETS_TOPIC_DEFAULT="${EXISTING_PACKETS_TOPIC:-meshcore/{IATA}/{PUBLIC_KEY}/packets}"
|
||
|
||
local status_topic=$(prompt_input "Status topic" "$STATUS_TOPIC_DEFAULT")
|
||
local packets_topic=$(prompt_input "Packets topic" "$PACKETS_TOPIC_DEFAULT")
|
||
|
||
echo "" >> "$ENV_LOCAL"
|
||
echo "# MQTT Topics for Broker $BROKER_NUM - Custom" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_STATUS=$status_topic" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_PACKETS=$packets_topic" >> "$ENV_LOCAL"
|
||
print_success "Custom topics configured"
|
||
;;
|
||
*)
|
||
print_error "Invalid choice, using default pattern"
|
||
echo "" >> "$ENV_LOCAL"
|
||
echo "# MQTT Topics for Broker $BROKER_NUM - Default Pattern" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_STATUS=meshcore/{IATA}/{PUBLIC_KEY}/status" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOPIC_PACKETS=meshcore/{IATA}/{PUBLIC_KEY}/packets" >> "$ENV_LOCAL"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Configure MQTT brokers only (skip device configuration)
|
||
configure_mqtt_brokers_only() {
|
||
ENV_LOCAL="$INSTALL_DIR/.env.local"
|
||
|
||
# Always prompt for IATA (allows changing during reconfiguration)
|
||
# Get existing IATA from config (including backup files) to use as default
|
||
EXISTING_IATA=$(read_env_value "PACKETCAPTURE_IATA")
|
||
EXISTING_IATA=$(echo "$EXISTING_IATA" | tr -d '[:space:]')
|
||
# Clear default if it's XXX or empty
|
||
if [ -z "$EXISTING_IATA" ] || [ "$EXISTING_IATA" = "XXX" ]; then
|
||
EXISTING_IATA=""
|
||
fi
|
||
|
||
echo ""
|
||
print_info "IATA code is a 3-letter airport code identifying your geographic region"
|
||
print_info "Example: SEA (Seattle), LAX (Los Angeles), NYC (New York), LON (London)"
|
||
echo ""
|
||
|
||
IATA=""
|
||
while [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; do
|
||
IATA=$(prompt_input "Enter your IATA code (3 letters)" "$EXISTING_IATA")
|
||
IATA=$(echo "$IATA" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
|
||
|
||
if [ -z "$IATA" ]; then
|
||
print_error "IATA code cannot be empty"
|
||
elif [ "$IATA" = "XXX" ]; then
|
||
print_error "Please enter your actual IATA code, not XXX"
|
||
elif [ ${#IATA} -ne 3 ]; then
|
||
print_warning "IATA code should be 3 letters, you entered: $IATA"
|
||
if ! prompt_yes_no "Use '$IATA' anyway?" "n"; then
|
||
IATA="XXX" # Reset to force re-prompt
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Update IATA in config
|
||
if grep -q "^PACKETCAPTURE_IATA=" "$ENV_LOCAL" 2>/dev/null; then
|
||
sed -i.bak "s/^PACKETCAPTURE_IATA=.*/PACKETCAPTURE_IATA=$IATA/" "$ENV_LOCAL"
|
||
rm -f "$ENV_LOCAL.bak"
|
||
else
|
||
echo "PACKETCAPTURE_IATA=$IATA" >> "$ENV_LOCAL"
|
||
fi
|
||
echo ""
|
||
print_success "IATA code set to: $IATA"
|
||
echo ""
|
||
|
||
echo ""
|
||
print_header "MQTT Broker Configuration"
|
||
echo ""
|
||
print_info "Enable the LetsMesh.net Packet Analyzer (US + EU servers) brokers?"
|
||
echo " • Real-time packet analysis and visualization"
|
||
echo " • Network health monitoring"
|
||
echo " • Redundant servers: mqtt-us-v1.letsmesh.net + mqtt-eu-v1.letsmesh.net"
|
||
echo " • Uses device signing (Python signing as fallback)"
|
||
echo ""
|
||
|
||
if prompt_yes_no "Enable LetsMesh Packet Analyzer with redundancy?" "y"; then
|
||
cat >> "$ENV_LOCAL" << EOF
|
||
|
||
# MQTT Broker 1 - LetsMesh.net Packet Analyzer (US)
|
||
PACKETCAPTURE_MQTT1_ENABLED=true
|
||
PACKETCAPTURE_MQTT1_SERVER=mqtt-us-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT1_PORT=443
|
||
PACKETCAPTURE_MQTT1_TRANSPORT=websockets
|
||
PACKETCAPTURE_MQTT1_USE_TLS=true
|
||
PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN=true
|
||
PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE=mqtt-us-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT1_KEEPALIVE=120
|
||
|
||
# MQTT Broker 2 - LetsMesh.net Packet Analyzer (EU)
|
||
PACKETCAPTURE_MQTT2_ENABLED=true
|
||
PACKETCAPTURE_MQTT2_SERVER=mqtt-eu-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT2_PORT=443
|
||
PACKETCAPTURE_MQTT2_TRANSPORT=websockets
|
||
PACKETCAPTURE_MQTT2_USE_TLS=true
|
||
PACKETCAPTURE_MQTT2_USE_AUTH_TOKEN=true
|
||
PACKETCAPTURE_MQTT2_TOKEN_AUDIENCE=mqtt-eu-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT2_KEEPALIVE=120
|
||
EOF
|
||
print_success "LetsMesh Packet Analyzer enabled with redundancy"
|
||
|
||
# Configure JWT options (owner public key and client agent)
|
||
configure_jwt_options
|
||
|
||
# Configure topics for LetsMesh (use default pattern only)
|
||
configure_mqtt_topics 1 true
|
||
configure_mqtt_topics 2 true
|
||
|
||
if prompt_yes_no "Would you like to configure additional MQTT brokers?" "n"; then
|
||
configure_additional_brokers
|
||
fi
|
||
else
|
||
# User declined LetsMesh, ask if they want to configure a custom broker
|
||
if prompt_yes_no "Would you like to configure a custom MQTT broker?" "y"; then
|
||
configure_custom_broker 1
|
||
|
||
if prompt_yes_no "Would you like to configure additional MQTT brokers?" "n"; then
|
||
configure_additional_brokers
|
||
fi
|
||
else
|
||
print_warning "No MQTT brokers configured - you'll need to edit .env.local manually"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Configure MQTT brokers
|
||
configure_mqtt_brokers() {
|
||
ENV_LOCAL="$INSTALL_DIR/.env.local"
|
||
|
||
# Ensure .env.local exists with update source info
|
||
if [ ! -f "$ENV_LOCAL" ]; then
|
||
# Interactive device selection
|
||
select_connection_type
|
||
|
||
cat > "$ENV_LOCAL" << EOF
|
||
# MeshCore Packet Capture Configuration
|
||
# This file contains your local overrides to the defaults in .env
|
||
|
||
# Update source (configured by installer)
|
||
PACKETCAPTURE_UPDATE_REPO=$REPO
|
||
PACKETCAPTURE_UPDATE_BRANCH=$BRANCH
|
||
|
||
# Connection Configuration
|
||
PACKETCAPTURE_CONNECTION_TYPE=$CONNECTION_TYPE
|
||
EOF
|
||
|
||
# Add device-specific configuration
|
||
case $CONNECTION_TYPE in
|
||
"ble")
|
||
echo "PACKETCAPTURE_BLE_DEVICE=$SELECTED_BLE_DEVICE" >> "$ENV_LOCAL"
|
||
if [ -n "$SELECTED_BLE_NAME" ]; then
|
||
echo "PACKETCAPTURE_BLE_NAME=$SELECTED_BLE_NAME" >> "$ENV_LOCAL"
|
||
fi
|
||
;;
|
||
"serial")
|
||
echo "PACKETCAPTURE_SERIAL_PORTS=$SELECTED_SERIAL_DEVICE" >> "$ENV_LOCAL"
|
||
;;
|
||
"tcp")
|
||
echo "PACKETCAPTURE_TCP_HOST=$TCP_HOST" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_TCP_PORT=$TCP_PORT" >> "$ENV_LOCAL"
|
||
;;
|
||
esac
|
||
|
||
cat >> "$ENV_LOCAL" << EOF
|
||
|
||
# Location Code
|
||
PACKETCAPTURE_IATA=XXX
|
||
|
||
# Advert Settings
|
||
PACKETCAPTURE_ADVERT_INTERVAL_HOURS=11
|
||
|
||
# Packet Type Filtering (comma-separated list of packet type numbers to upload to MQTT)
|
||
# Leave commented out to upload all packet types
|
||
# Example: PACKETCAPTURE_UPLOAD_PACKET_TYPES=2,4 (upload only TXT_MSG and ADVERT)
|
||
#PACKETCAPTURE_UPLOAD_PACKET_TYPES=
|
||
|
||
# Logging Settings
|
||
PACKETCAPTURE_LOG_LEVEL=INFO
|
||
EOF
|
||
fi
|
||
|
||
# Always prompt for IATA (allows changing during reconfiguration)
|
||
# Get existing IATA from config (including backup files) to use as default
|
||
EXISTING_IATA=$(read_env_value "PACKETCAPTURE_IATA")
|
||
EXISTING_IATA=$(echo "$EXISTING_IATA" | tr -d '[:space:]')
|
||
# Clear default if it's XXX or empty
|
||
if [ -z "$EXISTING_IATA" ] || [ "$EXISTING_IATA" = "XXX" ]; then
|
||
EXISTING_IATA=""
|
||
fi
|
||
|
||
echo ""
|
||
print_info "IATA code is a 3-letter airport code identifying your geographic region"
|
||
print_info "Example: SEA (Seattle), LAX (Los Angeles), NYC (New York), LON (London)"
|
||
echo ""
|
||
|
||
IATA=""
|
||
while [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; do
|
||
IATA=$(prompt_input "Enter your IATA code (3 letters)" "$EXISTING_IATA")
|
||
IATA=$(echo "$IATA" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
|
||
|
||
if [ -z "$IATA" ]; then
|
||
print_error "IATA code cannot be empty"
|
||
elif [ "$IATA" = "XXX" ]; then
|
||
print_error "Please enter your actual IATA code, not XXX"
|
||
elif [ ${#IATA} -ne 3 ]; then
|
||
print_warning "IATA code should be 3 letters, you entered: $IATA"
|
||
if ! prompt_yes_no "Use '$IATA' anyway?" "n"; then
|
||
IATA="XXX" # Reset to force re-prompt
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Update IATA in config
|
||
if grep -q "^PACKETCAPTURE_IATA=" "$ENV_LOCAL" 2>/dev/null; then
|
||
sed -i.bak "s/^PACKETCAPTURE_IATA=.*/PACKETCAPTURE_IATA=$IATA/" "$ENV_LOCAL"
|
||
rm -f "$ENV_LOCAL.bak"
|
||
else
|
||
echo "PACKETCAPTURE_IATA=$IATA" >> "$ENV_LOCAL"
|
||
fi
|
||
echo ""
|
||
print_success "IATA code set to: $IATA"
|
||
echo ""
|
||
|
||
# Configure JWT options (owner public key and email) - global settings
|
||
if ! grep -q "^PACKETCAPTURE_OWNER_PUBLIC_KEY=" "$ENV_LOCAL" 2>/dev/null && ! grep -q "^PACKETCAPTURE_OWNER_EMAIL=" "$ENV_LOCAL" 2>/dev/null; then
|
||
configure_jwt_options
|
||
fi
|
||
|
||
echo ""
|
||
print_header "MQTT Broker Configuration"
|
||
echo ""
|
||
print_info "Enable the LetsMesh.net Packet Analyzer (US + EU servers) for redundancy?"
|
||
echo " • Real-time packet analysis and visualization"
|
||
echo " • Network health monitoring"
|
||
echo " • Redundant servers: mqtt-us-v1.letsmesh.net + mqtt-eu-v1.letsmesh.net"
|
||
echo " • Uses device signing (Python signing as fallback)"
|
||
echo ""
|
||
|
||
if prompt_yes_no "Enable LetsMesh Packet Analyzer with redundancy?" "y"; then
|
||
cat >> "$ENV_LOCAL" << EOF
|
||
|
||
# MQTT Broker 1 - LetsMesh.net Packet Analyzer (US)
|
||
PACKETCAPTURE_MQTT1_ENABLED=true
|
||
PACKETCAPTURE_MQTT1_SERVER=mqtt-us-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT1_PORT=443
|
||
PACKETCAPTURE_MQTT1_TRANSPORT=websockets
|
||
PACKETCAPTURE_MQTT1_USE_TLS=true
|
||
PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN=true
|
||
PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE=mqtt-us-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT1_KEEPALIVE=120
|
||
|
||
# MQTT Broker 2 - LetsMesh.net Packet Analyzer (EU)
|
||
PACKETCAPTURE_MQTT2_ENABLED=true
|
||
PACKETCAPTURE_MQTT2_SERVER=mqtt-eu-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT2_PORT=443
|
||
PACKETCAPTURE_MQTT2_TRANSPORT=websockets
|
||
PACKETCAPTURE_MQTT2_USE_TLS=true
|
||
PACKETCAPTURE_MQTT2_USE_AUTH_TOKEN=true
|
||
PACKETCAPTURE_MQTT2_TOKEN_AUDIENCE=mqtt-eu-v1.letsmesh.net
|
||
PACKETCAPTURE_MQTT2_KEEPALIVE=120
|
||
EOF
|
||
print_success "LetsMesh Packet Analyzer brokers enabled"
|
||
|
||
# Configure topics for LetsMesh (use default pattern only)
|
||
configure_mqtt_topics 1 true
|
||
configure_mqtt_topics 2 true
|
||
|
||
if prompt_yes_no "Would you like to configure additional MQTT brokers?" "n"; then
|
||
configure_additional_brokers
|
||
fi
|
||
else
|
||
# User declined LetsMesh, ask if they want to configure a custom broker
|
||
if prompt_yes_no "Would you like to configure a custom MQTT broker?" "y"; then
|
||
configure_custom_broker 1
|
||
|
||
if prompt_yes_no "Would you like to configure additional MQTT brokers?" "n"; then
|
||
configure_additional_brokers
|
||
fi
|
||
else
|
||
print_warning "No MQTT brokers configured - you'll need to edit .env.local manually"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Configure additional brokers (starting from MQTT2)
|
||
configure_additional_brokers() {
|
||
# Find next available broker number
|
||
NEXT_BROKER=2
|
||
while grep -q "^PACKETCAPTURE_MQTT${NEXT_BROKER}_ENABLED=" "$INSTALL_DIR/.env.local" 2>/dev/null; do
|
||
NEXT_BROKER=$((NEXT_BROKER + 1))
|
||
done
|
||
|
||
NUM_ADDITIONAL=$(prompt_input "How many additional brokers?" "1")
|
||
|
||
for i in $(seq 1 $NUM_ADDITIONAL); do
|
||
BROKER_NUM=$((NEXT_BROKER + i - 1))
|
||
configure_custom_broker $BROKER_NUM
|
||
done
|
||
}
|
||
|
||
# Configure a single custom MQTT broker
|
||
configure_custom_broker() {
|
||
local BROKER_NUM=$1
|
||
ENV_LOCAL="$INSTALL_DIR/.env.local"
|
||
|
||
echo ""
|
||
print_header "Configuring MQTT Broker $BROKER_NUM"
|
||
|
||
# Read existing values from install directory's .env.local as defaults
|
||
EXISTING_SERVER=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_SERVER")
|
||
EXISTING_PORT=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_PORT")
|
||
EXISTING_TOKEN_AUDIENCE=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_TOKEN_AUDIENCE")
|
||
EXISTING_USERNAME=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_USERNAME")
|
||
EXISTING_PASSWORD=$(read_env_value "PACKETCAPTURE_MQTT${BROKER_NUM}_PASSWORD")
|
||
|
||
SERVER=$(prompt_input "Server hostname/IP" "$EXISTING_SERVER")
|
||
if [ -z "$SERVER" ]; then
|
||
print_warning "Server hostname required - skipping broker $BROKER_NUM"
|
||
return
|
||
fi
|
||
|
||
echo "" >> "$ENV_LOCAL"
|
||
echo "# MQTT Broker $BROKER_NUM" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_ENABLED=true" >> "$ENV_LOCAL"
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_SERVER=$SERVER" >> "$ENV_LOCAL"
|
||
|
||
PORT_DEFAULT="${EXISTING_PORT:-1883}"
|
||
PORT=$(prompt_input "Port" "$PORT_DEFAULT")
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_PORT=$PORT" >> "$ENV_LOCAL"
|
||
|
||
# Transport
|
||
if prompt_yes_no "Use WebSockets transport?" "n"; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TRANSPORT=websockets" >> "$ENV_LOCAL"
|
||
fi
|
||
|
||
# TLS
|
||
if prompt_yes_no "Use TLS/SSL encryption?" "n"; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_USE_TLS=true" >> "$ENV_LOCAL"
|
||
|
||
if ! prompt_yes_no "Verify TLS certificates?" "y"; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TLS_VERIFY=false" >> "$ENV_LOCAL"
|
||
fi
|
||
fi
|
||
|
||
# Authentication
|
||
echo ""
|
||
print_info "Authentication method:"
|
||
echo " 1) Username/Password"
|
||
echo " 2) MeshCore Auth Token"
|
||
echo " 3) None (anonymous)"
|
||
AUTH_TYPE=$(prompt_input "Choose authentication method [1-3]" "1")
|
||
|
||
if [ "$AUTH_TYPE" = "2" ]; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_USE_AUTH_TOKEN=true" >> "$ENV_LOCAL"
|
||
TOKEN_AUDIENCE=$(prompt_input "Token audience (optional)" "$EXISTING_TOKEN_AUDIENCE")
|
||
if [ -n "$TOKEN_AUDIENCE" ]; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOKEN_AUDIENCE=$TOKEN_AUDIENCE" >> "$ENV_LOCAL"
|
||
fi
|
||
fi
|
||
|
||
if [ "$AUTH_TYPE" = "1" ]; then
|
||
USERNAME=$(prompt_input "Username" "$EXISTING_USERNAME")
|
||
if [ -n "$USERNAME" ]; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_USERNAME=$USERNAME" >> "$ENV_LOCAL"
|
||
PASSWORD=$(prompt_input "Password" "$EXISTING_PASSWORD")
|
||
if [ -n "$PASSWORD" ]; then
|
||
echo "PACKETCAPTURE_MQTT${BROKER_NUM}_PASSWORD=$PASSWORD" >> "$ENV_LOCAL"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
print_success "Broker $BROKER_NUM configured"
|
||
|
||
# Configure topics for this broker
|
||
configure_mqtt_topics $BROKER_NUM
|
||
}
|
||
|
||
# Check for old installations
|
||
check_old_installation() {
|
||
# Check for old systemd service
|
||
if [ -f /etc/systemd/system/meshcore-capture.service ]; then
|
||
local working_dir=$(grep "WorkingDirectory=" /etc/systemd/system/meshcore-capture.service 2>/dev/null | cut -d'=' -f2)
|
||
|
||
if [ -n "$working_dir" ] && [ "$working_dir" != "$HOME/.meshcore-packet-capture" ]; then
|
||
echo ""
|
||
print_warning "Old meshcore-capture systemd service detected at: $working_dir"
|
||
echo ""
|
||
|
||
if prompt_yes_no "Would you like to stop and remove the old service?" "y"; then
|
||
if sudo systemctl stop meshcore-capture.service && sudo systemctl disable meshcore-capture.service && sudo rm -f /etc/systemd/system/meshcore-capture.service && sudo systemctl daemon-reload; then
|
||
print_success "Old service removed"
|
||
else
|
||
print_error "Failed to remove old service - please remove manually"
|
||
fi
|
||
else
|
||
print_warning "Old service left in place - may conflict with new installation"
|
||
fi
|
||
echo ""
|
||
fi
|
||
fi
|
||
|
||
# Check for launchd on macOS
|
||
if [ "$(uname)" = "Darwin" ]; then
|
||
local plist_file="$HOME/Library/LaunchAgents/com.meshcore.packet-capture.plist"
|
||
if [ -f "$plist_file" ] && ! grep -q "$HOME/.meshcore-packet-capture" "$plist_file" 2>/dev/null; then
|
||
echo ""
|
||
print_warning "Old meshcore-capture launchd service detected"
|
||
echo ""
|
||
|
||
if prompt_yes_no "Would you like to unload and remove the old service?" "y"; then
|
||
launchctl unload "$plist_file" 2>/dev/null || true
|
||
rm -f "$plist_file"
|
||
print_success "Old service removed"
|
||
else
|
||
print_warning "Old service left in place - may conflict with new installation"
|
||
fi
|
||
echo ""
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Main installation function
|
||
main() {
|
||
print_header "MeshCore Packet Capture Installer v${SCRIPT_VERSION}"
|
||
|
||
echo "This installer will help you set up MeshCore Packet Capture."
|
||
echo ""
|
||
|
||
# Check for old installations and offer to clean up
|
||
check_old_installation
|
||
|
||
# Determine installation directory
|
||
DEFAULT_INSTALL_DIR="$HOME/.meshcore-packet-capture"
|
||
INSTALL_DIR=$(prompt_input "Installation directory" "$DEFAULT_INSTALL_DIR")
|
||
INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}" # Expand tilde
|
||
|
||
print_info "Installation directory: $INSTALL_DIR"
|
||
|
||
# Check if directory exists
|
||
UPDATING_EXISTING=false
|
||
if [ -d "$INSTALL_DIR" ]; then
|
||
if prompt_yes_no "Directory already exists. Reinstall/update?" "n"; then
|
||
print_info "Updating existing installation..."
|
||
UPDATING_EXISTING=true
|
||
else
|
||
print_error "Installation cancelled."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# Create installation directory
|
||
mkdir -p "$INSTALL_DIR"
|
||
cd "$INSTALL_DIR"
|
||
|
||
# Download or copy files
|
||
print_header "Installing Files"
|
||
|
||
if [ -n "${LOCAL_INSTALL}" ]; then
|
||
# Local install for testing
|
||
print_info "Installing from local directory: ${LOCAL_INSTALL}"
|
||
cp "${LOCAL_INSTALL}/packet_capture.py" "$INSTALL_DIR/"
|
||
cp "${LOCAL_INSTALL}/auth_token.py" "$INSTALL_DIR/"
|
||
cp "${LOCAL_INSTALL}/enums.py" "$INSTALL_DIR/"
|
||
cp "${LOCAL_INSTALL}/ble_pairing_helper.py" "$INSTALL_DIR/"
|
||
cp "${LOCAL_INSTALL}/requirements.txt" "$INSTALL_DIR/"
|
||
# meshcore_py no longer needed - using PyPI version
|
||
if [ -f "${LOCAL_INSTALL}/.env" ]; then
|
||
cp "${LOCAL_INSTALL}/.env" "$INSTALL_DIR/"
|
||
fi
|
||
if [ -f "${LOCAL_INSTALL}/.env.local" ]; then
|
||
print_warning ".env.local found in source - copying as .env.local.example"
|
||
cp "${LOCAL_INSTALL}/.env.local" "$INSTALL_DIR/.env.local.example"
|
||
fi
|
||
chmod +x "$INSTALL_DIR/packet_capture.py"
|
||
|
||
# Create version info file
|
||
create_version_info
|
||
|
||
print_success "Files copied from local directory"
|
||
else
|
||
# Download from GitHub
|
||
print_info "Downloading from GitHub ($REPO @ $BRANCH)..."
|
||
|
||
BASE_URL="https://raw.githubusercontent.com/$REPO/$BRANCH"
|
||
|
||
# Download to temp directory first for verification
|
||
TMP_DIR=$(mktemp -d)
|
||
trap "rm -rf $TMP_DIR" EXIT
|
||
|
||
print_info "Downloading packet_capture.py..."
|
||
if ! curl -fsSL --retry 3 --retry-delay 2 "$BASE_URL/packet_capture.py" -o "$TMP_DIR/packet_capture.py"; then
|
||
print_error "Failed to download packet_capture.py from $REPO/$BRANCH"
|
||
print_error "Please verify the repository and branch exist"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Downloading auth_token.py..."
|
||
if ! curl -fsSL --retry 3 --retry-delay 2 "$BASE_URL/auth_token.py" -o "$TMP_DIR/auth_token.py"; then
|
||
print_error "Failed to download auth_token.py"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Downloading enums.py..."
|
||
if ! curl -fsSL --retry 3 --retry-delay 2 "$BASE_URL/enums.py" -o "$TMP_DIR/enums.py"; then
|
||
print_error "Failed to download enums.py"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Downloading ble_pairing_helper.py..."
|
||
if ! curl -fsSL --retry 3 --retry-delay 2 "$BASE_URL/ble_pairing_helper.py" -o "$TMP_DIR/ble_pairing_helper.py"; then
|
||
print_error "Failed to download ble_pairing_helper.py"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Downloading requirements.txt..."
|
||
if ! curl -fsSL --retry 3 --retry-delay 2 "$BASE_URL/requirements.txt" -o "$TMP_DIR/requirements.txt"; then
|
||
print_error "Failed to download requirements.txt"
|
||
exit 1
|
||
fi
|
||
|
||
# meshcore_py no longer needed - using PyPI version
|
||
|
||
# Verify Python syntax before installing
|
||
print_info "Verifying Python syntax..."
|
||
if ! python3 -m py_compile "$TMP_DIR/packet_capture.py" 2>/dev/null; then
|
||
print_error "Downloaded packet_capture.py has syntax errors"
|
||
print_error "The repository may be in an inconsistent state"
|
||
exit 1
|
||
fi
|
||
if ! python3 -m py_compile "$TMP_DIR/ble_pairing_helper.py" 2>/dev/null; then
|
||
print_error "Downloaded ble_pairing_helper.py has syntax errors"
|
||
print_error "The repository may be in an inconsistent state"
|
||
exit 1
|
||
fi
|
||
|
||
# All downloads successful and verified, now install
|
||
mv "$TMP_DIR/packet_capture.py" "$INSTALL_DIR/packet_capture.py"
|
||
mv "$TMP_DIR/auth_token.py" "$INSTALL_DIR/auth_token.py"
|
||
mv "$TMP_DIR/enums.py" "$INSTALL_DIR/enums.py"
|
||
mv "$TMP_DIR/ble_pairing_helper.py" "$INSTALL_DIR/ble_pairing_helper.py"
|
||
mv "$TMP_DIR/requirements.txt" "$INSTALL_DIR/requirements.txt"
|
||
# meshcore_py no longer needed - using PyPI version
|
||
|
||
chmod +x "$INSTALL_DIR/packet_capture.py"
|
||
|
||
# Create version info file
|
||
create_version_info
|
||
|
||
print_success "Files downloaded and verified"
|
||
fi
|
||
|
||
# Check Python
|
||
print_header "Checking Dependencies"
|
||
|
||
if ! command -v python3 &> /dev/null; then
|
||
print_error "Python 3 is not installed. Please install Python 3 and try again."
|
||
exit 1
|
||
fi
|
||
print_success "Python 3 found: $(python3 --version)"
|
||
|
||
# Set up virtual environment
|
||
print_info "Setting up Python virtual environment..."
|
||
if [ ! -d "$INSTALL_DIR/venv" ]; then
|
||
python3 -m venv "$INSTALL_DIR/venv"
|
||
print_success "Virtual environment created"
|
||
else
|
||
print_success "Using existing virtual environment"
|
||
fi
|
||
|
||
# Install Python dependencies
|
||
print_info "Installing Python dependencies..."
|
||
source "$INSTALL_DIR/venv/bin/activate"
|
||
pip install --quiet --upgrade pip
|
||
pip install --quiet --upgrade -r "$INSTALL_DIR/requirements.txt"
|
||
if ! check_meshcore_version "$INSTALL_DIR/venv/bin/python3" "dependency installation validation"; then
|
||
print_error "Installed meshcore version is incompatible with multi-byte path support"
|
||
print_error "Please ensure meshcore>=$MIN_MESHCORE_VERSION is available and rerun the installer"
|
||
print_error "Manual update command: \"$INSTALL_DIR/venv/bin/python3\" -m pip install --upgrade \"meshcore>=$MIN_MESHCORE_VERSION\""
|
||
exit 1
|
||
fi
|
||
create_runtime_launcher
|
||
print_success "Python dependencies installed"
|
||
|
||
# Configuration
|
||
print_header "Configuration"
|
||
|
||
# Check for existing config.ini and offer migration
|
||
if [ -f "$INSTALL_DIR/config.ini" ] && [ ! -f "$INSTALL_DIR/.env.local" ]; then
|
||
print_info "Found existing config.ini file"
|
||
if prompt_yes_no "Would you like to migrate your config.ini to the new .env.local format?" "y"; then
|
||
print_info "Migrating config.ini to .env.local..."
|
||
if python3 "$INSTALL_DIR/migrate_config.py"; then
|
||
print_success "Configuration migrated successfully"
|
||
print_info "You can now remove config.ini if everything works correctly"
|
||
else
|
||
print_error "Migration failed, continuing with manual configuration"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Check if config URL was provided
|
||
if [ -n "$CONFIG_URL" ]; then
|
||
print_info "Downloading configuration from: $CONFIG_URL"
|
||
if curl -fsSL "$CONFIG_URL" -o "$INSTALL_DIR/.env.local"; then
|
||
print_success "Configuration downloaded successfully"
|
||
|
||
# Convert MCTOMQTT_ prefixes to PACKETCAPTURE_ for compatibility
|
||
if grep -q "MCTOMQTT_" "$INSTALL_DIR/.env.local"; then
|
||
print_info "Converting MCTOMQTT_ prefixes to PACKETCAPTURE_ for compatibility..."
|
||
sed -i.bak 's/^MCTOMQTT_/PACKETCAPTURE_/g' "$INSTALL_DIR/.env.local"
|
||
rm -f "$INSTALL_DIR/.env.local.bak"
|
||
print_success "Configuration converted successfully"
|
||
fi
|
||
|
||
# Show what was downloaded
|
||
echo ""
|
||
print_info "Downloaded configuration:"
|
||
cat "$INSTALL_DIR/.env.local" | grep -v '^#' | grep -v '^$' | head -20
|
||
if [ $(cat "$INSTALL_DIR/.env.local" | grep -v '^#' | grep -v '^$' | wc -l) -gt 20 ]; then
|
||
echo "..."
|
||
fi
|
||
echo ""
|
||
|
||
if prompt_yes_no "Use this configuration?" "y"; then
|
||
print_success "Using downloaded configuration"
|
||
|
||
# Always prompt for IATA
|
||
echo ""
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
print_warning "IATA CODE REQUIRED"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
print_info "IATA code is a 3-letter airport code and should match an airport near the reporting location"
|
||
print_info "Example: SEA (Seattle), LAX (Los Angeles), NYC (New York), LON (London)"
|
||
echo ""
|
||
|
||
# Use existing IATA as default if available and not XXX
|
||
# Read again to get value from backup if main file has XXX
|
||
EXISTING_IATA=$(read_env_value "PACKETCAPTURE_IATA")
|
||
EXISTING_IATA=$(echo "$EXISTING_IATA" | tr -d '[:space:]')
|
||
if [ -z "$EXISTING_IATA" ] || [ "$EXISTING_IATA" = "XXX" ]; then
|
||
EXISTING_IATA=""
|
||
fi
|
||
|
||
IATA=""
|
||
while [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; do
|
||
IATA=$(prompt_input "Enter your IATA code (3 letters)" "$EXISTING_IATA")
|
||
IATA=$(echo "$IATA" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
|
||
|
||
if [ -z "$IATA" ]; then
|
||
print_error "IATA code cannot be empty"
|
||
elif [ "$IATA" = "XXX" ]; then
|
||
print_error "Please enter your actual IATA code, not XXX"
|
||
elif [ ${#IATA} -ne 3 ]; then
|
||
print_warning "IATA code should be 3 letters, you entered: $IATA"
|
||
if ! prompt_yes_no "Use '$IATA' anyway?" "n"; then
|
||
IATA=""
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Update IATA in config
|
||
if grep -q "^PACKETCAPTURE_IATA=" "$INSTALL_DIR/.env.local"; then
|
||
sed -i.bak "s/^PACKETCAPTURE_IATA=.*/PACKETCAPTURE_IATA=$IATA/" "$INSTALL_DIR/.env.local"
|
||
rm -f "$INSTALL_DIR/.env.local.bak"
|
||
else
|
||
echo "PACKETCAPTURE_IATA=$IATA" >> "$INSTALL_DIR/.env.local"
|
||
fi
|
||
echo ""
|
||
print_success "IATA code set to: $IATA"
|
||
echo ""
|
||
|
||
# Check if MQTT1 is already configured and offer additional brokers
|
||
if grep -q "^PACKETCAPTURE_MQTT1_ENABLED=true" "$INSTALL_DIR/.env.local" 2>/dev/null; then
|
||
MQTT1_SERVER=$(grep "^PACKETCAPTURE_MQTT1_SERVER=" "$INSTALL_DIR/.env.local" 2>/dev/null | cut -d'=' -f2)
|
||
echo ""
|
||
print_success "MQTT Broker 1 already configured: $MQTT1_SERVER"
|
||
|
||
if prompt_yes_no "Would you like to configure additional MQTT brokers?" "n"; then
|
||
configure_additional_brokers
|
||
fi
|
||
else
|
||
# No MQTT configured, offer options
|
||
configure_mqtt_brokers
|
||
fi
|
||
else
|
||
rm -f "$INSTALL_DIR/.env.local"
|
||
configure_mqtt_brokers
|
||
fi
|
||
else
|
||
print_error "Failed to download configuration from URL"
|
||
if prompt_yes_no "Continue with interactive configuration?" "y"; then
|
||
configure_mqtt_brokers
|
||
else
|
||
exit 1
|
||
fi
|
||
fi
|
||
elif [ "$UPDATING_EXISTING" = true ] && [ -f "$INSTALL_DIR/.env.local" ]; then
|
||
if prompt_yes_no "Existing configuration found. Reconfigure?" "n"; then
|
||
# Back up existing config before reconfiguring
|
||
cp "$INSTALL_DIR/.env.local" "$INSTALL_DIR/.env.local.backup-$(date +%Y%m%d-%H%M%S)"
|
||
rm -f "$INSTALL_DIR/.env.local"
|
||
configure_mqtt_brokers
|
||
else
|
||
print_info "Keeping existing configuration"
|
||
# Check if MQTT brokers are already configured
|
||
if [ -f "$INSTALL_DIR/.env.local" ] && grep -q "^PACKETCAPTURE_MQTT[1-4]_ENABLED=true" "$INSTALL_DIR/.env.local" 2>/dev/null; then
|
||
print_info "MQTT brokers already configured - skipping MQTT configuration"
|
||
else
|
||
# Still need to configure MQTT brokers if not already configured
|
||
configure_mqtt_brokers_only
|
||
fi
|
||
fi
|
||
elif [ ! -f "$INSTALL_DIR/.env.local" ]; then
|
||
configure_mqtt_brokers
|
||
fi
|
||
|
||
# Installation method selection
|
||
print_header "Installation Method"
|
||
|
||
echo "Choose your preferred installation method:"
|
||
echo ""
|
||
echo " 1) System Service (recommended for production)"
|
||
echo " • Runs automatically on boot"
|
||
echo " • Managed by systemd (Linux) or launchd (macOS)"
|
||
echo " • Automatic restart on failure"
|
||
echo ""
|
||
echo " 2) Docker Container (recommended for development/testing)"
|
||
echo " • Isolated environment"
|
||
echo " • Easy to update and manage"
|
||
echo " • Works on Linux, macOS, and Windows"
|
||
echo ""
|
||
echo " 3) Manual installation only"
|
||
echo " • No automatic startup"
|
||
echo " • Run manually when needed"
|
||
echo ""
|
||
|
||
INSTALL_METHOD=$(prompt_input "Choose installation method [1-3]" "1")
|
||
|
||
case "$INSTALL_METHOD" in
|
||
1)
|
||
install_system_service
|
||
;;
|
||
2)
|
||
install_docker
|
||
;;
|
||
3)
|
||
print_info "Manual installation complete"
|
||
print_info "To run manually: cd $INSTALL_DIR && ./venv/bin/python3 packet_capture.py"
|
||
;;
|
||
*)
|
||
print_error "Invalid selection"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# Final summary
|
||
print_header "Installation Complete!"
|
||
echo "Installation directory: $INSTALL_DIR"
|
||
echo ""
|
||
echo "Configuration file: $INSTALL_DIR/.env.local"
|
||
echo ""
|
||
|
||
if [ "$SERVICE_INSTALLED" = true ]; then
|
||
case "$SYSTEM_TYPE" in
|
||
systemd)
|
||
echo "Service management:"
|
||
echo " Start: sudo systemctl start meshcore-capture"
|
||
echo " Stop: sudo systemctl stop meshcore-capture"
|
||
echo " Status: sudo systemctl status meshcore-capture"
|
||
echo " Logs: sudo journalctl -u meshcore-capture -f"
|
||
echo ""
|
||
echo "Resource monitoring:"
|
||
echo " CPU usage: sudo systemctl show meshcore-capture --property=CPUUsageNSec"
|
||
echo " Memory: sudo systemctl show meshcore-capture --property=MemoryCurrent"
|
||
echo " Tasks: sudo systemctl show meshcore-capture --property=TasksCurrent"
|
||
echo ""
|
||
echo "Resource limits (prevent runaway processes):"
|
||
echo " Memory: 512MB limit"
|
||
echo " CPU: 50% limit"
|
||
echo " Tasks: 200 max"
|
||
echo ""
|
||
echo "Service failure handling:"
|
||
echo " Max failures: 3 within 5 minutes"
|
||
echo " Critical threshold: 5 total failures"
|
||
echo " Auto-restart: systemd will restart on persistent failures"
|
||
;;
|
||
launchd)
|
||
echo "Service management:"
|
||
echo " Start: launchctl start com.meshcore.packet-capture"
|
||
echo " Stop: launchctl stop com.meshcore.packet-capture"
|
||
echo " Status: launchctl list | grep packet-capture"
|
||
echo " Logs: tail -f ~/Library/Logs/meshcore-capture.log"
|
||
;;
|
||
esac
|
||
elif [ "$DOCKER_INSTALLED" = true ]; then
|
||
echo "Docker management:"
|
||
echo " Start: $COMPOSE_CMD -f $INSTALL_DIR/docker-compose.yml up -d"
|
||
echo " Stop: $COMPOSE_CMD -f $INSTALL_DIR/docker-compose.yml down"
|
||
echo " Logs: $COMPOSE_CMD -f $INSTALL_DIR/docker-compose.yml logs -f"
|
||
echo " Status: $COMPOSE_CMD -f $INSTALL_DIR/docker-compose.yml ps"
|
||
echo ""
|
||
echo "Resource monitoring:"
|
||
echo " Stats: docker stats meshcore-packet-capture"
|
||
echo " Top: docker exec meshcore-packet-capture top"
|
||
echo ""
|
||
echo "Resource limits (prevent runaway processes):"
|
||
echo " Memory: 512MB limit, 128MB reserved"
|
||
echo " CPU: 50% limit, 10% reserved"
|
||
echo ""
|
||
echo "Service failure handling:"
|
||
echo " Max failures: 3 within 5 minutes"
|
||
echo " Critical threshold: 5 total failures"
|
||
echo " Auto-restart: Docker will restart on persistent failures"
|
||
else
|
||
echo "Manual run: cd $INSTALL_DIR && ./venv/bin/python3 packet_capture.py"
|
||
fi
|
||
|
||
echo ""
|
||
print_success "Installation complete!"
|
||
}
|
||
|
||
# Detect system type
|
||
detect_system_type() {
|
||
if command -v systemctl &> /dev/null; then
|
||
echo "systemd"
|
||
elif [ "$(uname)" = "Darwin" ]; then
|
||
echo "launchd"
|
||
else
|
||
echo "unknown"
|
||
fi
|
||
}
|
||
|
||
# Install system service
|
||
install_system_service() {
|
||
SYSTEM_TYPE=$(detect_system_type)
|
||
print_info "Detected system type: $SYSTEM_TYPE"
|
||
|
||
case "$SYSTEM_TYPE" in
|
||
systemd)
|
||
install_systemd_service
|
||
;;
|
||
launchd)
|
||
install_launchd_service
|
||
;;
|
||
*)
|
||
print_error "Unsupported system type: $SYSTEM_TYPE"
|
||
print_info "You'll need to manually configure the service"
|
||
SERVICE_INSTALLED=false
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Check and add user to dialout group for serial port access (Linux only)
|
||
check_dialout_group() {
|
||
# Only check on Linux systems
|
||
if [ "$(uname)" != "Linux" ]; then
|
||
return 0
|
||
fi
|
||
|
||
# Only needed for serial connections
|
||
local connection_type=$(read_env_value "PACKETCAPTURE_CONNECTION_TYPE")
|
||
if [ "$connection_type" != "serial" ]; then
|
||
return 0
|
||
fi
|
||
|
||
local current_user=$(whoami)
|
||
|
||
# Check if user is already in dialout group
|
||
if groups "$current_user" | grep -q "\bdialout\b"; then
|
||
return 0
|
||
fi
|
||
|
||
# Check if dialout group exists
|
||
if ! getent group dialout >/dev/null 2>&1; then
|
||
print_warning "dialout group not found on this system"
|
||
print_info "Serial port access may require manual configuration"
|
||
return 0
|
||
fi
|
||
|
||
# Prompt user with clear disclosure
|
||
echo ""
|
||
print_header "Serial Port Access Configuration"
|
||
echo ""
|
||
print_info "To access serial ports (like /dev/ttyUSB0), your user account needs"
|
||
print_info "to be a member of the 'dialout' group."
|
||
echo ""
|
||
print_info "This will allow the packet capture service to communicate with"
|
||
print_info "your MeshCore device via serial connection."
|
||
echo ""
|
||
print_warning "Action required: Adding user '$current_user' to dialout group"
|
||
print_info "This requires administrator privileges (sudo)."
|
||
echo ""
|
||
|
||
if prompt_yes_no "Add user to dialout group now?" "y"; then
|
||
if sudo usermod -a -G dialout "$current_user"; then
|
||
print_success "User added to dialout group"
|
||
print_warning "IMPORTANT: You will need to log out and log back in for this change to take effect"
|
||
print_info "Alternatively, you can run 'newgrp dialout' in a new terminal after installation"
|
||
print_info "The service will work correctly after you log out/in or restart your session"
|
||
else
|
||
print_error "Failed to add user to dialout group"
|
||
print_info "You can add yourself manually with: sudo usermod -a -G dialout $current_user"
|
||
print_info "Then log out and log back in, or run: newgrp dialout"
|
||
fi
|
||
else
|
||
print_warning "Skipping dialout group addition"
|
||
print_info "If you encounter permission errors accessing serial ports, you can add"
|
||
print_info "yourself to the dialout group later with:"
|
||
print_info " sudo usermod -a -G dialout $current_user"
|
||
print_info "Then log out and log back in, or run: newgrp dialout"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
# Install systemd service (Linux)
|
||
install_systemd_service() {
|
||
print_info "Installing systemd service..."
|
||
|
||
# Check and add user to dialout group if needed (before service installation)
|
||
check_dialout_group
|
||
|
||
local service_file="/tmp/meshcore-capture.service"
|
||
local current_user=$(whoami)
|
||
|
||
# Build PATH (legacy decoder path is opt-in only)
|
||
local service_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||
if [ "$ENABLE_LEGACY_DECODER_PATH" = "true" ] && command -v meshcore-decoder &> /dev/null; then
|
||
local decoder_dir=$(dirname "$(which meshcore-decoder)")
|
||
service_path="${decoder_dir}:${service_path}"
|
||
print_info "Legacy meshcore-decoder path enabled: $decoder_dir"
|
||
elif command -v meshcore-decoder &> /dev/null; then
|
||
print_info "meshcore-decoder found but not added to PATH (set PACKETCAPTURE_ENABLE_LEGACY_DECODER_PATH=true to enable)"
|
||
fi
|
||
|
||
cat > "$service_file" << EOF
|
||
[Unit]
|
||
Description=MeshCore Packet Capture
|
||
After=time-sync.target network.target
|
||
Wants=time-sync.target
|
||
|
||
[Service]
|
||
User=$current_user
|
||
WorkingDirectory=$INSTALL_DIR
|
||
Environment="PATH=$service_path"
|
||
Environment="PYTHON_BIN=$INSTALL_DIR/venv/bin/python3"
|
||
Environment="MIN_MESHCORE_VERSION=$MIN_MESHCORE_VERSION"
|
||
Environment="PACKETCAPTURE_MAX_ACTIVE_TASKS=50"
|
||
Environment="PACKETCAPTURE_JWT_CIRCUIT_BREAKER_FAILURES=3"
|
||
Environment="PACKETCAPTURE_JWT_CIRCUIT_BREAKER_TIMEOUT=180"
|
||
Environment="PACKETCAPTURE_MQTT_RETRY_DELAY_MAX=300"
|
||
Environment="PACKETCAPTURE_MQTT_RETRY_BACKOFF_MULTIPLIER=2.0"
|
||
Environment="PACKETCAPTURE_MQTT_RETRY_JITTER=true"
|
||
Environment="PACKETCAPTURE_CONNECTION_RETRY_DELAY_MAX=300"
|
||
Environment="PACKETCAPTURE_CONNECTION_RETRY_BACKOFF_MULTIPLIER=2.0"
|
||
Environment="PACKETCAPTURE_CONNECTION_RETRY_JITTER=true"
|
||
|
||
# Service failure tracking (let systemd restart on persistent failures)
|
||
Environment="PACKETCAPTURE_MAX_SERVICE_FAILURES=3"
|
||
Environment="PACKETCAPTURE_SERVICE_FAILURE_WINDOW=300"
|
||
Environment="PACKETCAPTURE_CRITICAL_FAILURE_THRESHOLD=5"
|
||
Environment="PACKETCAPTURE_MAX_CONSECUTIVE_FAILURES=3"
|
||
ExecStart=$INSTALL_DIR/start_packet_capture.sh
|
||
ExecStop=/bin/bash -c 'if [ -f $INSTALL_DIR/.env.local ] && grep -q "PACKETCAPTURE_CONNECTION_TYPE=ble" $INSTALL_DIR/.env.local; then BLE_DEVICE=\$(grep "PACKETCAPTURE_BLE_DEVICE=" $INSTALL_DIR/.env.local | cut -d= -f2); if [ -n "\$BLE_DEVICE" ] && command -v bluetoothctl >/dev/null 2>&1; then echo "Disconnecting BLE device \$BLE_DEVICE..."; bluetoothctl disconnect "\$BLE_DEVICE" 2>/dev/null || true; sleep 2; fi; fi'
|
||
KillMode=process
|
||
Restart=on-failure
|
||
RestartSec=10
|
||
Type=exec
|
||
|
||
# Resource limits to prevent runaway processes
|
||
MemoryMax=512M
|
||
CPUQuota=50%
|
||
TasksMax=200
|
||
|
||
# Logging
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=meshcore-capture
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
print_info "Service file created. Installing (requires sudo)..."
|
||
|
||
if sudo cp "$service_file" /etc/systemd/system/meshcore-capture.service; then
|
||
sudo systemctl daemon-reload
|
||
|
||
if prompt_yes_no "Enable service to start on boot?" "y"; then
|
||
sudo systemctl enable meshcore-capture.service
|
||
print_success "Service enabled"
|
||
fi
|
||
|
||
if prompt_yes_no "Start service now?" "y"; then
|
||
sudo systemctl start meshcore-capture.service
|
||
|
||
print_info "Waiting for service to start..."
|
||
sleep 3
|
||
|
||
# Check if service is actually running and connected
|
||
print_info "Checking service health..."
|
||
sleep 2
|
||
|
||
if sudo systemctl is-active --quiet meshcore-capture.service; then
|
||
# Check logs for successful MQTT connection
|
||
if sudo journalctl -u meshcore-capture.service --since "10 seconds ago" | grep -q "Connected to.*MQTT broker"; then
|
||
print_success "Service started and connected to MQTT successfully"
|
||
echo ""
|
||
print_info "Recent logs:"
|
||
sudo journalctl -u meshcore-capture.service -n 10 --no-pager
|
||
else
|
||
print_warning "Service started but may not be connected to MQTT yet"
|
||
echo ""
|
||
print_info "Recent logs:"
|
||
sudo journalctl -u meshcore-capture.service -n 15 --no-pager
|
||
echo ""
|
||
print_warning "Check logs with: sudo journalctl -u meshcore-capture -f"
|
||
fi
|
||
else
|
||
print_error "Service failed to start"
|
||
echo ""
|
||
sudo systemctl status meshcore-capture.service --no-pager || true
|
||
fi
|
||
fi
|
||
|
||
SERVICE_INSTALLED=true
|
||
print_success "Systemd service installed"
|
||
else
|
||
print_error "Failed to install service (sudo required)"
|
||
SERVICE_INSTALLED=false
|
||
fi
|
||
|
||
rm -f "$service_file"
|
||
}
|
||
|
||
# Install launchd service (macOS)
|
||
install_launchd_service() {
|
||
print_info "Installing launchd service..."
|
||
|
||
local plist_file="$HOME/Library/LaunchAgents/com.meshcore.packet-capture.plist"
|
||
mkdir -p "$HOME/Library/LaunchAgents"
|
||
|
||
# Build comprehensive PATH that includes Node.js (legacy decoder path is opt-in)
|
||
# LaunchAgents don't inherit shell PATH, so we must explicitly set it
|
||
local service_path=""
|
||
|
||
# Add nvm Node.js paths if nvm is installed
|
||
if [ -d "$HOME/.nvm" ]; then
|
||
# Find the active Node.js version (check for default alias or latest LTS)
|
||
local nvm_node_path=""
|
||
if [ -d "$HOME/.nvm/versions/node" ]; then
|
||
# Try to find the default alias first
|
||
if [ -f "$HOME/.nvm/alias/default" ]; then
|
||
local default_version=$(cat "$HOME/.nvm/alias/default" 2>/dev/null)
|
||
if [ -d "$HOME/.nvm/versions/node/$default_version" ]; then
|
||
nvm_node_path="$HOME/.nvm/versions/node/$default_version/bin"
|
||
fi
|
||
fi
|
||
# If no default, try to find the latest LTS or latest version
|
||
if [ -z "$nvm_node_path" ]; then
|
||
local latest_node=$(ls -t "$HOME/.nvm/versions/node" 2>/dev/null | head -1)
|
||
if [ -n "$latest_node" ]; then
|
||
nvm_node_path="$HOME/.nvm/versions/node/$latest_node/bin"
|
||
fi
|
||
fi
|
||
fi
|
||
if [ -n "$nvm_node_path" ] && [ -d "$nvm_node_path" ]; then
|
||
service_path="${nvm_node_path}:"
|
||
print_info "Including nvm Node.js path: $nvm_node_path"
|
||
fi
|
||
fi
|
||
|
||
# Add Homebrew paths (for Apple Silicon and Intel Macs)
|
||
if [ -d "/opt/homebrew/bin" ]; then
|
||
service_path="${service_path}/opt/homebrew/bin:"
|
||
fi
|
||
if [ -d "/usr/local/bin" ]; then
|
||
service_path="${service_path}/usr/local/bin:"
|
||
fi
|
||
|
||
# Add npm global bin directory (common locations)
|
||
if [ -d "$HOME/.npm-global/bin" ]; then
|
||
service_path="${service_path}$HOME/.npm-global/bin:"
|
||
fi
|
||
# npm config get prefix can tell us where global packages are installed
|
||
if command -v npm &> /dev/null; then
|
||
local npm_prefix=$(npm config get prefix 2>/dev/null)
|
||
if [ -n "$npm_prefix" ] && [ -d "$npm_prefix/bin" ] && [ "$npm_prefix" != "/usr" ]; then
|
||
service_path="${service_path}$npm_prefix/bin:"
|
||
fi
|
||
fi
|
||
|
||
# Add Node.js directory if node is available (covers various installation methods)
|
||
if command -v node &> /dev/null; then
|
||
local node_dir=$(dirname "$(which node)")
|
||
if [ -n "$node_dir" ] && [ "$node_dir" != "." ] && [[ "$service_path" != *"$node_dir"* ]]; then
|
||
service_path="${service_path}${node_dir}:"
|
||
print_info "Including Node.js path: $node_dir"
|
||
fi
|
||
fi
|
||
|
||
# Add meshcore-decoder directory only when explicitly enabled
|
||
if [ "$ENABLE_LEGACY_DECODER_PATH" = "true" ] && command -v meshcore-decoder &> /dev/null; then
|
||
local decoder_dir=$(dirname "$(which meshcore-decoder)")
|
||
if [ -n "$decoder_dir" ] && [ "$decoder_dir" != "." ] && [[ "$service_path" != *"$decoder_dir"* ]]; then
|
||
service_path="${service_path}${decoder_dir}:"
|
||
print_info "Legacy meshcore-decoder path enabled: $decoder_dir"
|
||
fi
|
||
elif command -v meshcore-decoder &> /dev/null; then
|
||
print_info "meshcore-decoder found but not added to PATH (set PACKETCAPTURE_ENABLE_LEGACY_DECODER_PATH=true to enable)"
|
||
fi
|
||
|
||
# Add standard system paths
|
||
service_path="${service_path}/usr/bin:/bin:/usr/sbin:/sbin"
|
||
|
||
# Clean up any double colons
|
||
service_path=$(echo "$service_path" | sed 's/::*/:/g' | sed 's/^://' | sed 's/:$//')
|
||
|
||
print_info "Service PATH will include: $service_path"
|
||
|
||
cat > "$plist_file" << EOF
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>Label</key>
|
||
<string>com.meshcore.packet-capture</string>
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>$INSTALL_DIR/start_packet_capture.sh</string>
|
||
</array>
|
||
<key>WorkingDirectory</key>
|
||
<string>$INSTALL_DIR</string>
|
||
<key>EnvironmentVariables</key>
|
||
<dict>
|
||
<key>PATH</key>
|
||
<string>$service_path</string>
|
||
<key>PYTHON_BIN</key>
|
||
<string>$INSTALL_DIR/venv/bin/python3</string>
|
||
<key>MIN_MESHCORE_VERSION</key>
|
||
<string>$MIN_MESHCORE_VERSION</string>
|
||
</dict>
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
<key>KeepAlive</key>
|
||
<true/>
|
||
<key>StandardOutPath</key>
|
||
<string>$HOME/Library/Logs/meshcore-capture.log</string>
|
||
<key>StandardErrorPath</key>
|
||
<string>$HOME/Library/Logs/meshcore-capture-error.log</string>
|
||
</dict>
|
||
</plist>
|
||
EOF
|
||
|
||
if prompt_yes_no "Load service now?" "y"; then
|
||
launchctl load "$plist_file"
|
||
print_success "Service loaded"
|
||
fi
|
||
|
||
SERVICE_INSTALLED=true
|
||
print_success "Launchd service installed"
|
||
}
|
||
|
||
# Install Docker
|
||
install_docker() {
|
||
print_info "Installing Docker configuration..."
|
||
|
||
# Check if Docker is available
|
||
if ! command -v docker &> /dev/null; then
|
||
print_error "Docker is not installed or not available in PATH"
|
||
print_info "Please install Docker first: https://docs.docker.com/get-docker/"
|
||
exit 1
|
||
fi
|
||
|
||
# Determine docker compose command (prefer modern 'docker compose')
|
||
if docker compose version &> /dev/null; then
|
||
COMPOSE_CMD="docker compose"
|
||
elif command -v docker-compose &> /dev/null; then
|
||
COMPOSE_CMD="docker-compose"
|
||
else
|
||
print_error "Docker Compose is not installed or not available in PATH"
|
||
print_info "Install Docker Desktop or Compose Plugin: https://docs.docker.com/compose/install/"
|
||
exit 1
|
||
fi
|
||
|
||
print_success "Docker and Compose found (${COMPOSE_CMD})"
|
||
|
||
# Create Docker configuration files
|
||
print_info "Creating Docker configuration..."
|
||
if [ ! -f "$INSTALL_DIR/start_packet_capture.sh" ]; then
|
||
print_info "Creating runtime launcher script for Docker startup checks..."
|
||
create_runtime_launcher
|
||
fi
|
||
|
||
# Create Dockerfile
|
||
cat > "$INSTALL_DIR/Dockerfile" << 'EOF'
|
||
# Use Python 3.11 slim image for smaller size
|
||
FROM python:3.11-slim
|
||
|
||
# Install system dependencies for BLE and serial communication
|
||
RUN apt-get update && apt-get install -y \
|
||
bluez \
|
||
libbluetooth-dev \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
# Set working directory
|
||
WORKDIR /app
|
||
|
||
# Copy requirements first for better Docker layer caching
|
||
COPY requirements.txt .
|
||
|
||
# Install Python dependencies
|
||
RUN pip install --no-cache-dir -r requirements.txt
|
||
|
||
# Copy the entire project
|
||
COPY . .
|
||
|
||
# meshcore is now installed from PyPI via requirements.txt (no local package needed)
|
||
|
||
# Create non-root user for security
|
||
RUN useradd -m -u 1000 meshcore && chown -R meshcore:meshcore /app
|
||
USER meshcore
|
||
|
||
# Create data directory for output files
|
||
RUN mkdir -p /app/data
|
||
|
||
# Set default environment variables
|
||
# Note: These are defaults - override in docker-compose.yml or .env.local
|
||
ENV PACKETCAPTURE_CONNECTION_TYPE=serial
|
||
ENV MIN_MESHCORE_VERSION=2.2.31
|
||
ENV PYTHONUNBUFFERED=1
|
||
ENV PYTHONDONTWRITEBYTECODE=1
|
||
|
||
# Default command
|
||
CMD ["./start_packet_capture.sh"]
|
||
EOF
|
||
|
||
# Create docker-compose.yml
|
||
cat > "$INSTALL_DIR/docker-compose.yml" << EOF
|
||
version: '3.8'
|
||
|
||
services:
|
||
meshcore-capture:
|
||
# Use pre-built image (recommended)
|
||
image: ghcr.io/agessaman/meshcore-packet-capture:latest
|
||
# Or build from source: uncomment the line below and comment out the image line above
|
||
# build: .
|
||
container_name: meshcore-packet-capture
|
||
# Privileged mode configuration:
|
||
# - Set to 'false' for serial connections (default, more secure)
|
||
# - Set to 'true' for BLE connections (required for Bluetooth access)
|
||
# To enable for BLE, change 'false' to 'true' below
|
||
# or create docker-compose.override.yml with: privileged: true
|
||
privileged: false # Change to true for BLE connections
|
||
devices:
|
||
# Mount serial devices using persistent device IDs (recommended)
|
||
# Format: host_path:container_path
|
||
# Find your device ID on the host with: sudo ls -la /dev/serial/by-id/
|
||
# Standard container path is /dev/ttyUSB0 (matches code default, no config needed)
|
||
# Example: /dev/serial/by-id/usb-Heltec_HT-n5262_3D3B4D4A4D776001-if00:/dev/ttyUSB0
|
||
# Uncomment and modify the line below with your device ID:
|
||
# - /dev/serial/by-id/usb-Heltec_HT-n5262_3D3B4D4A4D776001-if00:/dev/ttyUSB0
|
||
|
||
# Alternative: Use numbered devices directly (may change after reboot)
|
||
# - /dev/ttyUSB0:/dev/ttyUSB0
|
||
# - /dev/ttyUSB1:/dev/ttyUSB1
|
||
# - /dev/ttyACM0:/dev/ttyACM0
|
||
volumes:
|
||
# Persistent data storage
|
||
- ./data:/app/data
|
||
# Configuration files (optional - can use environment variables instead)
|
||
# Copy .env.local.example to .env.local and customize for your setup
|
||
- ./.env.local:/app/.env.local:ro
|
||
# Logs directory (optional - uncomment to mount logs separately from data)
|
||
# - ./logs:/app/logs
|
||
# Resource limits to prevent runaway processes
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
memory: 512M
|
||
cpus: '0.5'
|
||
reservations:
|
||
memory: 128M
|
||
cpus: '0.1'
|
||
environment:
|
||
# Connection settings
|
||
- PACKETCAPTURE_CONNECTION_TYPE=serial
|
||
# PACKETCAPTURE_SERIAL_PORTS defaults to /dev/ttyUSB0 (matches standard container path above)
|
||
# Only set this if using a different container path or multiple ports
|
||
# - PACKETCAPTURE_SERIAL_PORTS=/dev/ttyUSB0
|
||
# For BLE connections, change CONNECTION_TYPE to 'ble' and set privileged: true
|
||
# Connection timeout, retries, and health check use defaults (30s, 5 retries, 30s interval)
|
||
# Uncomment to customize:
|
||
# - PACKETCAPTURE_TIMEOUT=30
|
||
# - PACKETCAPTURE_MAX_CONNECTION_RETRIES=5
|
||
# - PACKETCAPTURE_CONNECTION_RETRY_DELAY=5
|
||
# - PACKETCAPTURE_HEALTH_CHECK_INTERVAL=30
|
||
|
||
# MQTT settings - Let'sMesh Analyzer (US and EU servers for redundancy)
|
||
# MQTT Broker 1 - Let'sMesh Analyzer (US)
|
||
- PACKETCAPTURE_MQTT1_ENABLED=true
|
||
- PACKETCAPTURE_MQTT1_SERVER=mqtt-us-v1.letsmesh.net
|
||
- PACKETCAPTURE_MQTT1_PORT=443
|
||
- PACKETCAPTURE_MQTT1_TRANSPORT=websockets
|
||
- PACKETCAPTURE_MQTT1_USE_TLS=true
|
||
- PACKETCAPTURE_MQTT1_USE_AUTH_TOKEN=true
|
||
- PACKETCAPTURE_MQTT1_TOKEN_AUDIENCE=mqtt-us-v1.letsmesh.net
|
||
- PACKETCAPTURE_MQTT1_KEEPALIVE=120
|
||
|
||
# MQTT Broker 2 - Let'sMesh Analyzer (EU)
|
||
- PACKETCAPTURE_MQTT2_ENABLED=true
|
||
- PACKETCAPTURE_MQTT2_SERVER=mqtt-eu-v1.letsmesh.net
|
||
- PACKETCAPTURE_MQTT2_PORT=443
|
||
- PACKETCAPTURE_MQTT2_TRANSPORT=websockets
|
||
- PACKETCAPTURE_MQTT2_USE_TLS=true
|
||
- PACKETCAPTURE_MQTT2_USE_AUTH_TOKEN=true
|
||
- PACKETCAPTURE_MQTT2_TOKEN_AUDIENCE=mqtt-eu-v1.letsmesh.net
|
||
- PACKETCAPTURE_MQTT2_KEEPALIVE=120
|
||
|
||
# Custom MQTT broker (optional - uncomment and configure as needed)
|
||
# - PACKETCAPTURE_MQTT3_ENABLED=true
|
||
# - PACKETCAPTURE_MQTT3_SERVER=your-mqtt-broker
|
||
# - PACKETCAPTURE_MQTT3_PORT=1883
|
||
# - PACKETCAPTURE_MQTT3_USERNAME=your_username
|
||
# - PACKETCAPTURE_MQTT3_PASSWORD=your_password
|
||
# - PACKETCAPTURE_MQTT3_USE_TLS=false
|
||
|
||
# MQTT reconnection settings use defaults (5 retries, 5s delay, exit on fail)
|
||
# Uncomment to customize:
|
||
# - PACKETCAPTURE_MAX_MQTT_RETRIES=5
|
||
# - PACKETCAPTURE_MQTT_RETRY_DELAY=5
|
||
# - PACKETCAPTURE_EXIT_ON_RECONNECT_FAIL=true
|
||
|
||
# Topic settings (when IATA is configured, topics automatically use template format)
|
||
# Template variables: {IATA}, {IATA_lower}, {PUBLIC_KEY}
|
||
# Default when IATA is set: meshcore/{IATA}/{PUBLIC_KEY}/status, etc.
|
||
# Uncomment to override with custom topics:
|
||
# - PACKETCAPTURE_TOPIC_STATUS=meshcore/{IATA}/{PUBLIC_KEY}/status
|
||
# - PACKETCAPTURE_TOPIC_PACKETS=meshcore/{IATA}/{PUBLIC_KEY}/packets
|
||
# - PACKETCAPTURE_TOPIC_RAW=meshcore/{IATA}/{PUBLIC_KEY}/raw
|
||
# - PACKETCAPTURE_TOPIC_DECODED=meshcore/{IATA}/{PUBLIC_KEY}/decoded
|
||
# - PACKETCAPTURE_TOPIC_DEBUG=meshcore/{IATA}/{PUBLIC_KEY}/debug
|
||
|
||
# Device settings
|
||
- PACKETCAPTURE_IATA=XYZ
|
||
# PACKETCAPTURE_ORIGIN is optional - if not set, uses device name from meshcore connection
|
||
# Uncomment and set if you want to override the device name:
|
||
# - PACKETCAPTURE_ORIGIN=Your Custom Name
|
||
|
||
# Advert settings
|
||
# Adverts are used for network discovery, not connection keepalive
|
||
# The connection stays alive through regular packet traffic
|
||
# Default: 11 hours. Set to 0 to disable periodic adverts
|
||
- PACKETCAPTURE_ADVERT_INTERVAL_HOURS=11
|
||
|
||
# RF data settings use default (15.0 seconds timeout)
|
||
# Uncomment to customize:
|
||
# - PACKETCAPTURE_RF_DATA_TIMEOUT=15.0
|
||
networks:
|
||
- meshcore-network
|
||
restart: unless-stopped
|
||
# Uncomment for host networking (may be needed for BLE discovery)
|
||
# network_mode: host
|
||
|
||
networks:
|
||
meshcore-network:
|
||
driver: bridge
|
||
EOF
|
||
|
||
# Create .dockerignore
|
||
cat > "$INSTALL_DIR/.dockerignore" << 'EOF'
|
||
# Python cache files
|
||
__pycache__/
|
||
*.py[cod]
|
||
*$py.class
|
||
*.so
|
||
|
||
# Distribution / packaging
|
||
.Python
|
||
build/
|
||
develop-eggs/
|
||
dist/
|
||
downloads/
|
||
eggs/
|
||
.eggs/
|
||
lib/
|
||
lib64/
|
||
parts/
|
||
sdist/
|
||
var/
|
||
wheels/
|
||
*.egg-info/
|
||
.installed.cfg
|
||
*.egg
|
||
|
||
# Virtual environments
|
||
venv/
|
||
env/
|
||
ENV/
|
||
|
||
# IDE files
|
||
.vscode/
|
||
.idea/
|
||
*.swp
|
||
*.swo
|
||
*~
|
||
|
||
# OS files
|
||
.DS_Store
|
||
Thumbs.db
|
||
|
||
# Git
|
||
.git/
|
||
.gitignore
|
||
|
||
# Docker
|
||
Dockerfile*
|
||
docker-compose*
|
||
.dockerignore
|
||
|
||
# Configuration files (use environment variables instead)
|
||
.env.local
|
||
config.ini
|
||
|
||
# Data and logs
|
||
data/
|
||
*.log
|
||
logs/
|
||
|
||
# Documentation
|
||
README.md
|
||
CLEANUP_SUMMARY.md
|
||
|
||
# Old files
|
||
old/
|
||
EOF
|
||
|
||
print_success "Docker configuration files created"
|
||
|
||
# Build Docker image
|
||
print_info "Building Docker image..."
|
||
if docker build -t meshcore-capture "$INSTALL_DIR"; then
|
||
print_success "Docker image built successfully"
|
||
else
|
||
print_error "Failed to build Docker image"
|
||
exit 1
|
||
fi
|
||
|
||
# Ask if user wants to start the container
|
||
if prompt_yes_no "Start the Docker container now?" "y"; then
|
||
print_info "Starting Docker container..."
|
||
cd "$INSTALL_DIR"
|
||
if $COMPOSE_CMD up -d; then
|
||
print_success "Docker container started"
|
||
|
||
# Wait a moment and check logs
|
||
sleep 3
|
||
print_info "Container logs:"
|
||
$COMPOSE_CMD logs --tail=20
|
||
else
|
||
print_error "Failed to start Docker container"
|
||
print_info "You can start it manually later with: $COMPOSE_CMD up -d"
|
||
fi
|
||
fi
|
||
|
||
DOCKER_INSTALLED=true
|
||
print_success "Docker installation complete"
|
||
}
|
||
|
||
# Run main
|
||
main "$@"
|