#!/bin/bash # ============================================================================ # MeshCore Packet Capture - Interactive Installer # ============================================================================ set -e SCRIPT_VERSION="1.2" 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}" # 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" </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 meshcore and bleak are available if ! python3 -c "import meshcore, bleak" 2>/dev/null; then print_warning "meshcore or 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 # 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 if dependencies are available (meshcore and bleak) if ! "$python_cmd" -c "import meshcore, bleak" 2>/dev/null; then print_warning "BLE dependencies (meshcore/bleak) not yet installed" 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 # 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/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" # meshcore is now installed from PyPI via requirements.txt and will be upgraded on reinstall 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 (silently includes meshcore-decoder if available for legacy support) local service_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" if command -v meshcore-decoder &> /dev/null; then local decoder_dir=$(dirname "$(which meshcore-decoder)") service_path="${decoder_dir}:${service_path}" 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="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/venv/bin/python3 $INSTALL_DIR/packet_capture.py 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 and meshcore-decoder # 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 if found (silent - for legacy support) if 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}:" fi 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 Label com.meshcore.packet-capture ProgramArguments $INSTALL_DIR/venv/bin/python3 $INSTALL_DIR/packet_capture.py WorkingDirectory $INSTALL_DIR EnvironmentVariables PATH $service_path RunAtLoad KeepAlive StandardOutPath $HOME/Library/Logs/meshcore-capture.log StandardErrorPath $HOME/Library/Logs/meshcore-capture-error.log 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..." # 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 PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 # Default command CMD ["python", "packet_capture.py"] 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=LOC # 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 "$@"