meshcore-packet-capture/install.sh
agessaman dbc0753237 Add JWT token management and renewal process to PacketCapture
- Implement JWT token storage and renewal logic in packet_capture.py.
- Introduce a background task for periodic JWT renewal.
- Update install.sh to check existing MQTT broker configurations before setup.
- Enhance MQTT broker reconnection logic with renewed tokens.
- Add new environment variables for JWT renewal interval and threshold.
2025-10-14 21:26:55 -07:00

1884 lines
69 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

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

#!/bin/bash
# ============================================================================
# MeshCore Packet Capture - Interactive Installer
# ============================================================================
set -e
SCRIPT_VERSION="1.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 </dev/tty
response=${response:-$default}
case "$response" in
[yY][eE][sS]|[yY]) return 0 ;;
*) return 1 ;;
esac
}
prompt_input() {
local prompt="$1"
local default="$2"
local response
# Read from /dev/tty to work when stdin is piped
if [ -n "$default" ]; then
read -p "$prompt [$default]: " response </dev/tty
echo "${response:-$default}"
else
read -p "$prompt: " response </dev/tty
echo "$response"
fi
}
# Configure MQTT topics for a broker
configure_mqtt_topics() {
local BROKER_NUM=$1
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 ""
# 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)"
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)
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.meshcore.packet-capture</string>
<key>ProgramArguments</key>
<array>
<string>$INSTALL_DIR/venv/bin/python3</string>
<string>$INSTALL_DIR/packet_capture.py</string>
</array>
<key>WorkingDirectory</key>
<string>$INSTALL_DIR</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>$service_path</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/meshcore-capture.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/meshcore-capture-error.log</string>
</dict>
</plist>
EOF
if prompt_yes_no "Load service now?" "y"; then
launchctl load "$plist_file"
print_success "Service loaded"
fi
SERVICE_INSTALLED=true
print_success "Launchd service installed"
}
# Install Docker
install_docker() {
print_info "Installing Docker configuration..."
# Check if Docker is available
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed or not available in PATH"
print_info "Please install Docker first: https://docs.docker.com/get-docker/"
exit 1
fi
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 "$@"