meshcore-packet-capture/install.sh
agessaman e6f28002cd Enhance packet capture and installation scripts for MeshCore compatibility
- 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.
2026-03-15 08:34:04 -07:00

2671 lines
105 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "$@"