#!/bin/bash # ============================================================================ # MeshCore Packet Capture - Interactive Installer # ============================================================================ set -e SCRIPT_VERSION="1.0.0" DEFAULT_REPO="agessaman/meshcore-packet-capture" DEFAULT_BRANCH="feature/multi-mqtt-integration" # 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" } 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" } # 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) 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 local manual_mac=$(prompt_input "Enter BLE device MAC address" "") local manual_name=$(prompt_input "Enter device name (optional)" "") 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)..." # Debug: Show the actual values being passed 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" # 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 # Check pairing status first (with timeout to prevent hanging) local pairing_output if pairing_output=$(timeout 45 python3 "$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 paired and ready to use" rm -f /tmp/ble_pairing_error return 0 elif [ "$pairing_status" = "not_found" ]; then print_warning "Device not found or not in range" print_info "Make sure your MeshCore device is:" print_info " • Powered on and within range" print_info " • In pairing mode (if not already paired)" print_info " • Not connected to another device" rm -f /tmp/ble_pairing_error return 1 elif [ "$pairing_status" = "timeout" ]; then print_warning "Connection timed out" print_info "The device may be busy or not responding. Please try again." print_info "If the device shows as connected, try disconnecting it first." rm -f /tmp/ble_pairing_error return 1 elif [ "$pairing_status" = "not_paired" ]; then print_info "Device requires pairing. You'll need to enter the PIN displayed on your MeshCore device." 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 (with timeout to prevent hanging) echo "" print_info "Attempting to pair with device..." if pairing_output=$(timeout 60 python3 "$temp_script" "$device_address" "$device_name" "$pin" 2>/tmp/ble_pairing_error); then local pairing_result=$(echo "$pairing_output" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['status'])" 2>/dev/null) if [ "$pairing_result" = "paired" ]; then print_success "BLE pairing successful! Device is now ready to use." rm -f /tmp/ble_pairing_error return 0 else print_error "BLE pairing failed" if [ -f /tmp/ble_pairing_error ]; then local error_msg=$(cat /tmp/ble_pairing_error) if [ -n "$error_msg" ]; then print_info "Error details: $error_msg" fi rm -f /tmp/ble_pairing_error fi return 1 fi else # Check if it was a timeout if [ -f /tmp/ble_pairing_error ]; then local error_msg=$(cat /tmp/ble_pairing_error) if [[ "$error_msg" == *"timeout"* ]] || [[ "$error_msg" == *"Terminated"* ]]; 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 "Please try again or check if the device is in pairing mode." else print_error "Failed to attempt BLE pairing" if [ -n "$error_msg" ]; then print_info "Error details: $error_msg" fi fi rm -f /tmp/ble_pairing_error else print_error "Failed to attempt BLE pairing" fi return 1 fi else print_error "Failed to check pairing status" if [ -f /tmp/ble_pairing_error ]; then local error_msg=$(cat /tmp/ble_pairing_error) if [ -n "$error_msg" ]; then print_info "Error details: $error_msg" fi rm -f /tmp/ble_pairing_error fi return 1 fi else # Check if it was a timeout if [ -f /tmp/ble_pairing_error ]; then local error_msg=$(cat /tmp/ble_pairing_error) if [[ "$error_msg" == *"timeout"* ]] || [[ "$error_msg" == *"Terminated"* ]]; then print_error "BLE pairing check timed out" print_info "The device may be busy or not responding. Please try again." else print_error "Failed to check BLE pairing status" if [ -n "$error_msg" ]; then print_info "Error details: $error_msg" fi fi rm -f /tmp/ble_pairing_error else print_error "Failed to check BLE pairing status" 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 "" while true; do local choice=$(prompt_input "Select connection type [1-3]" "1") 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 print_info "BLE scanning failed or no devices found. Please enter device details manually." SELECTED_BLE_DEVICE=$(prompt_input "Enter BLE device MAC address" "") SELECTED_BLE_NAME=$(prompt_input "Enter device name (optional)" "") 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 SELECTED_BLE_DEVICE=$(prompt_input "Enter BLE device MAC address" "") SELECTED_BLE_NAME=$(prompt_input "Enter device name (optional)" "") 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") SELECTED_SERIAL_DEVICE=$(prompt_input "Enter serial device path" "/dev/ttyACM0") 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 SELECTED_SERIAL_DEVICE=$(prompt_input "Enter serial device path" "/dev/ttyACM0") 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 "" TCP_HOST=$(prompt_input "TCP host/address" "localhost") TCP_PORT=$(prompt_input "TCP port" "5000") # 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 > "$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) 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" 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 "" local status_topic=$(prompt_input "Status topic" "meshcore/{IATA}/{PUBLIC_KEY}/status") local packets_topic=$(prompt_input "Packets topic" "meshcore/{IATA}/{PUBLIC_KEY}/packets") 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" # Get IATA from existing config IATA=$(grep "^PACKETCAPTURE_IATA=" "$ENV_LOCAL" 2>/dev/null | cut -d'=' -f2) # Always prompt for IATA if it's XXX or empty if [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; then 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 "" while [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; do IATA=$(prompt_input "Enter your IATA code (3 letters)" "") 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 sed -i.bak "s/^PACKETCAPTURE_IATA=.*/PACKETCAPTURE_IATA=$IATA/" "$ENV_LOCAL" rm -f "$ENV_LOCAL.bak" echo "" print_success "IATA code set to: $IATA" echo "" fi echo "" print_header "MQTT Broker Configuration" echo "" print_info "Enable the LetsMesh.net Packet Analyzer (mqtt-us-v1.letsmesh.net) broker?" echo " • Real-time packet analysis and visualization" echo " • Network health monitoring" echo " • Requires meshcore-decoder for authentication" echo "" if [ "$DECODER_AVAILABLE" = true ]; then if prompt_yes_no "Enable LetsMesh Packet Analyzer?" "y"; then cat >> "$ENV_LOCAL" << EOF # MQTT Broker 1 - LetsMesh.net Packet Analyzer 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 EOF print_success "LetsMesh Packet Analyzer enabled" # Configure topics for LetsMesh configure_mqtt_topics 1 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 else # No decoder available, can't use LetsMesh print_warning "meshcore-decoder not available - cannot use LetsMesh auth token authentication" if prompt_yes_no "Would you like to configure a custom MQTT broker with username/password?" "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 EOF fi # Get IATA from existing config IATA=$(grep "^PACKETCAPTURE_IATA=" "$ENV_LOCAL" 2>/dev/null | cut -d'=' -f2) # Always prompt for IATA if it's XXX or empty if [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; then 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 "" while [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; do IATA=$(prompt_input "Enter your IATA code (3 letters)" "") 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 sed -i.bak "s/^PACKETCAPTURE_IATA=.*/PACKETCAPTURE_IATA=$IATA/" "$ENV_LOCAL" rm -f "$ENV_LOCAL.bak" echo "" print_success "IATA code set to: $IATA" echo "" fi echo "" print_header "MQTT Broker Configuration" echo "" print_info "Enable the LetsMesh.net Packet Analyzer (mqtt-us-v1.letsmesh.net) broker?" echo " • Real-time packet analysis and visualization" echo " • Network health monitoring" echo " • Requires meshcore-decoder for authentication" echo "" if [ "$DECODER_AVAILABLE" = true ]; then if prompt_yes_no "Enable LetsMesh Packet Analyzer?" "y"; then cat >> "$ENV_LOCAL" << EOF # MQTT Broker 1 - LetsMesh.net Packet Analyzer 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 EOF print_success "LetsMesh Packet Analyzer enabled" # Configure topics for LetsMesh configure_mqtt_topics 1 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 else # No decoder available, can't use LetsMesh print_warning "meshcore-decoder not available - cannot use LetsMesh auth token authentication" if prompt_yes_no "Would you like to configure a custom MQTT broker with username/password?" "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" SERVER=$(prompt_input "Server hostname/IP") 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=$(prompt_input "Port" "1883") 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 (requires meshcore-decoder)" echo " 3) None (anonymous)" AUTH_TYPE=$(prompt_input "Choose authentication method [1-3]" "1") if [ "$AUTH_TYPE" = "2" ]; then if [ "$DECODER_AVAILABLE" = false ]; then print_error "meshcore-decoder not available - using username/password instead" AUTH_TYPE=1 else echo "PACKETCAPTURE_MQTT${BROKER_NUM}_USE_AUTH_TOKEN=true" >> "$ENV_LOCAL" TOKEN_AUDIENCE=$(prompt_input "Token audience (optional)" "") if [ -n "$TOKEN_AUDIENCE" ]; then echo "PACKETCAPTURE_MQTT${BROKER_NUM}_TOKEN_AUDIENCE=$TOKEN_AUDIENCE" >> "$ENV_LOCAL" fi fi fi if [ "$AUTH_TYPE" = "1" ]; then USERNAME=$(prompt_input "Username" "") if [ -n "$USERNAME" ]; then echo "PACKETCAPTURE_MQTT${BROKER_NUM}_USERNAME=$USERNAME" >> "$ENV_LOCAL" PASSWORD=$(prompt_input "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" 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" 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 -r "$INSTALL_DIR/requirements.txt" # meshcore is now installed from PyPI via requirements.txt print_success "Python dependencies installed" # Check for meshcore-decoder (optional) if command -v meshcore-decoder &> /dev/null; then print_success "meshcore-decoder found: $(which meshcore-decoder)" DECODER_AVAILABLE=true else print_warning "meshcore-decoder not found (required for auth token authentication)" if prompt_yes_no "Would you like instructions to install it now?" "y"; then echo "" echo "To install meshcore-decoder, run:" echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash" echo " # Restart your shell or run: source ~/.bashrc (or ~/.zshrc)" echo " nvm install --lts" echo " npm install -g @michaelhart/meshcore-decoder" echo "" if prompt_yes_no "Continue without meshcore-decoder (you can install it later)?" "y"; then DECODER_AVAILABLE=false else exit 1 fi else DECODER_AVAILABLE=false fi fi # 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 "" # Try to extract existing IATA from config EXISTING_IATA=$(grep "^PACKETCAPTURE_IATA=" "$INSTALL_DIR/.env.local" 2>/dev/null | cut -d'=' -f2) IATA="" while [ -z "$IATA" ] || [ "$IATA" = "XXX" ]; do if [ -n "$EXISTING_IATA" ] && [ "$EXISTING_IATA" != "XXX" ]; then IATA=$(prompt_input "Enter your IATA code" "$EXISTING_IATA") else IATA=$(prompt_input "Enter your IATA code (3 letters)" "") fi 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 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" ;; 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: docker-compose -f $INSTALL_DIR/docker-compose.yml up -d" echo " Stop: docker-compose -f $INSTALL_DIR/docker-compose.yml down" echo " Logs: docker-compose -f $INSTALL_DIR/docker-compose.yml logs -f" echo " Status: docker-compose -f $INSTALL_DIR/docker-compose.yml ps" 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 } # Install systemd service (Linux) install_systemd_service() { print_info "Installing systemd service..." local service_file="/tmp/meshcore-capture.service" local current_user=$(whoami) # Build PATH with meshcore-decoder if available 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" 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 [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 PATH with meshcore-decoder if available local service_path="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" if command -v meshcore-decoder &> /dev/null; then local decoder_dir=$(dirname "$(which meshcore-decoder)") service_path="${decoder_dir}:${service_path}" fi 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 if ! command -v docker-compose &> /dev/null; then print_error "Docker Compose is not installed or not available in PATH" print_info "Please install Docker Compose first: https://docs.docker.com/compose/install/" exit 1 fi print_success "Docker and Docker Compose found" # 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 . . # Install the local meshcore package in development mode RUN pip install -e ./meshcore_py # 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 ENV PACKETCAPTURE_CONNECTION_TYPE=ble ENV PACKETCAPTURE_TIMEOUT=30 ENV PACKETCAPTURE_MAX_CONNECTION_RETRIES=5 ENV PACKETCAPTURE_CONNECTION_RETRY_DELAY=5 ENV PACKETCAPTURE_HEALTH_CHECK_INTERVAL=30 # 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: build: . container_name: meshcore-packet-capture privileged: true # Required for BLE access and device communication devices: # Mount serial devices (uncomment and modify as needed) - /dev/ttyUSB0:/dev/ttyUSB0 - /dev/ttyUSB1:/dev/ttyUSB1 - /dev/ttyACM0:/dev/ttyACM0 volumes: # Persistent data storage - ./data:/app/data # Configuration files - ./.env.local:/app/.env.local:ro environment: # Connection settings - PACKETCAPTURE_CONNECTION_TYPE=ble - PACKETCAPTURE_TIMEOUT=30 - PACKETCAPTURE_MAX_CONNECTION_RETRIES=5 - PACKETCAPTURE_CONNECTION_RETRY_DELAY=5 - PACKETCAPTURE_HEALTH_CHECK_INTERVAL=30 # MQTT settings (configure as needed) - PACKETCAPTURE_MQTT1_ENABLED=true - PACKETCAPTURE_MQTT1_SERVER=localhost - PACKETCAPTURE_MQTT1_PORT=1883 - PACKETCAPTURE_MQTT1_USERNAME= - PACKETCAPTURE_MQTT1_PASSWORD= - PACKETCAPTURE_MQTT1_USE_TLS=false # MQTT reconnection settings - PACKETCAPTURE_MAX_MQTT_RETRIES=5 - PACKETCAPTURE_MQTT_RETRY_DELAY=5 - PACKETCAPTURE_EXIT_ON_RECONNECT_FAIL=true # Topic settings - PACKETCAPTURE_TOPIC_STATUS=meshcore/status - PACKETCAPTURE_TOPIC_PACKETS=meshcore/packets - PACKETCAPTURE_TOPIC_RAW=meshcore/raw - PACKETCAPTURE_TOPIC_DECODED=meshcore/decoded - PACKETCAPTURE_TOPIC_DEBUG=meshcore/debug # Device settings - PACKETCAPTURE_IATA=LOC - PACKETCAPTURE_ORIGIN=PacketCapture Docker # Advert settings - PACKETCAPTURE_ADVERT_INTERVAL_HOURS=11 # RF data settings - PACKETCAPTURE_RF_DATA_TIMEOUT=15.0 # JWT token renewal settings - PACKETCAPTURE_JWT_RENEWAL_INTERVAL=3600 - PACKETCAPTURE_JWT_RENEWAL_THRESHOLD=300 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 docker-compose up -d; then print_success "Docker container started" # Wait a moment and check logs sleep 3 print_info "Container logs:" docker-compose logs --tail=20 else print_error "Failed to start Docker container" print_info "You can start it manually later with: docker-compose up -d" fi fi DOCKER_INSTALLED=true print_success "Docker installation complete" } # Run main main "$@"