diff --git a/src/rf_mapper/__main__.py b/src/rf_mapper/__main__.py index 1aac27b..9f52c12 100644 --- a/src/rf_mapper/__main__.py +++ b/src/rf_mapper/__main__.py @@ -169,6 +169,9 @@ Note: Requires sudo for WiFi/Bluetooth scanning. web_parser.add_argument('--profile-requests', action='store_true', help='Enable profiling') web_parser.add_argument('--log-requests', action='store_true', help='Log requests') + # Check-termux command + subparsers.add_parser('check-termux', help='Check Termux/Android prerequisites') + # Config command config_parser = subparsers.add_parser('config', help='Show/edit configuration') config_parser.add_argument( @@ -209,6 +212,8 @@ Note: Requires sudo for WiFi/Bluetooth scanning. run_status(data_dir) elif args.command == 'config': run_config(args, config) + elif args.command == 'check-termux': + run_check_termux() elif args.command == 'web': run_web_deprecated(args, config, data_dir) else: @@ -436,6 +441,19 @@ Home Assistant: """) +def run_check_termux(): + """Check Termux/Android prerequisites""" + from .termux import is_termux, check_termux_prerequisites + + if not is_termux(): + print("Not running in Termux/Android environment.") + print("This check is only relevant when running on Android via Termux.") + sys.exit(0) + + success = check_termux_prerequisites(verbose=True) + sys.exit(0 if success else 1) + + def get_pid_file(data_dir: Path) -> Path: """Get path to PID file""" return data_dir / "rf-mapper.pid" @@ -534,6 +552,16 @@ def run_start(args, config: Config, data_dir: Path): import subprocess import time + # Check Termux prerequisites if running on Android + from .termux import is_termux, check_termux_prerequisites, setup_termux_signal_handlers + + if is_termux(): + if not check_termux_prerequisites(verbose=True): + print("Exiting due to missing prerequisites.") + sys.exit(1) + # Set up signal handlers for graceful shutdown + setup_termux_signal_handlers() + host = args.host or config.web.host port = args.port or config.web.port debug = getattr(args, 'debug', False) diff --git a/src/rf_mapper/termux.py b/src/rf_mapper/termux.py new file mode 100644 index 0000000..3ce68ca --- /dev/null +++ b/src/rf_mapper/termux.py @@ -0,0 +1,226 @@ +"""Termux/Android environment detection and prerequisite checks.""" + +import os +import subprocess +import sys +from pathlib import Path + + +def is_termux() -> bool: + """Detect if running in Termux on Android.""" + # Check for Termux environment variable + if os.environ.get("TERMUX_VERSION"): + return True + + # Check for Termux-specific paths + termux_paths = [ + "/data/data/com.termux", + Path.home() / ".termux", + Path("/data/data/com.termux/files/usr"), + ] + + for path in termux_paths: + if Path(path).exists(): + return True + + # Check PREFIX environment variable (Termux sets this) + prefix = os.environ.get("PREFIX", "") + if "/com.termux/" in prefix: + return True + + return False + + +def check_termux_api_installed() -> tuple[bool, str]: + """Check if termux-api package and Termux:API app are installed.""" + # Check for termux-api command + try: + result = subprocess.run( + ["which", "termux-location"], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode != 0: + return False, "termux-api package not installed. Run: pkg install termux-api" + except (subprocess.TimeoutExpired, FileNotFoundError): + return False, "termux-api package not installed. Run: pkg install termux-api" + + # Test if Termux:API app is working (quick test with battery status) + try: + result = subprocess.run( + ["termux-battery-status"], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode != 0 or "error" in result.stderr.lower(): + return False, "Termux:API app not installed or not granted permissions" + except subprocess.TimeoutExpired: + return False, "Termux:API app not responding. Install from F-Droid and grant permissions" + except FileNotFoundError: + return False, "termux-api package not installed. Run: pkg install termux-api" + + return True, "OK" + + +def check_location_enabled() -> tuple[bool, str]: + """Check if location services are accessible via termux-api.""" + try: + # Use termux-location with a short timeout + result = subprocess.run( + ["termux-location", "-p", "passive", "-r", "once"], + capture_output=True, + text=True, + timeout=15 + ) + + output = result.stdout.strip() + + # Check for common error patterns + if not output: + return False, "Location services not responding. Enable GPS/Location in Android settings" + + if "null" in output.lower() and "latitude" not in output.lower(): + return False, "Location unavailable. Enable GPS and grant Termux:API location permission" + + # Try to parse as JSON to verify it's valid location data + import json + try: + data = json.loads(output) + if data.get("latitude") is not None: + return True, f"OK (lat: {data.get('latitude'):.4f})" + except json.JSONDecodeError: + pass + + # Location might still be initializing + return True, "OK (location services accessible)" + + except subprocess.TimeoutExpired: + return False, "Location request timed out. Enable GPS in Android settings" + except FileNotFoundError: + return False, "termux-location not found. Run: pkg install termux-api" + + +def check_wake_lock() -> tuple[bool, str]: + """Check if wake lock can be acquired.""" + try: + # Try to acquire wake lock + result = subprocess.run( + ["termux-wake-lock"], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode != 0: + return False, f"Failed to acquire wake lock: {result.stderr.strip()}" + + return True, "OK (wake lock acquired)" + + except subprocess.TimeoutExpired: + return False, "Wake lock request timed out" + except FileNotFoundError: + return False, "termux-wake-lock not found. Run: pkg install termux-api" + + +def check_termux_boot() -> tuple[bool, str]: + """Check if Termux:Boot is available for auto-start capability.""" + boot_dir = Path.home() / ".termux" / "boot" + + if boot_dir.exists(): + return True, "OK (boot directory exists)" + + # Not critical, just informational + return True, "Termux:Boot not configured (optional for auto-start)" + + +def check_termux_prerequisites(verbose: bool = True) -> bool: + """ + Check all Termux prerequisites for RF Mapper. + + Returns True if all required checks pass, False otherwise. + Prints status messages if verbose=True. + """ + if not is_termux(): + # Not running in Termux, skip checks + return True + + if verbose: + print("\n" + "=" * 50) + print("TERMUX ENVIRONMENT DETECTED") + print("Checking prerequisites...") + print("=" * 50) + + all_ok = True + checks = [ + ("Termux:API package", check_termux_api_installed, True), # Required + ("Location services", check_location_enabled, True), # Required + ("Wake lock", check_wake_lock, True), # Required + ("Termux:Boot", check_termux_boot, False), # Optional + ] + + for name, check_func, required in checks: + try: + ok, message = check_func() + status = "✓" if ok else ("✗" if required else "⚠") + + if verbose: + print(f" {status} {name}: {message}") + + if not ok and required: + all_ok = False + + except Exception as e: + if verbose: + print(f" ✗ {name}: Error - {e}") + if required: + all_ok = False + + if verbose: + print("=" * 50) + + if all_ok: + print("All prerequisites met. Starting RF Mapper...") + else: + print("\nREQUIRED PREREQUISITES NOT MET") + print("\nTo fix:") + print(" 1. Install Termux:API from F-Droid") + print(" (NOT from Play Store - versions must match)") + print(" 2. Run: pkg install termux-api") + print(" 3. Enable Location in Android Settings") + print(" 4. Grant Termux:API location permission") + print(" 5. Run: termux-wake-lock") + print("\nFor auto-start on boot:") + print(" 1. Install Termux:Boot from F-Droid") + print(" 2. mkdir -p ~/.termux/boot") + print(" 3. Create boot script: ~/.termux/boot/start-rf-mapper.sh") + print("") + + return all_ok + + +def setup_termux_signal_handlers(): + """Set up signal handlers for graceful shutdown in Termux.""" + import signal + + def handle_signal(signum, frame): + """Release wake lock and exit gracefully.""" + try: + subprocess.run(["termux-wake-unlock"], capture_output=True, timeout=5) + except Exception: + pass + sys.exit(0) + + signal.signal(signal.SIGTERM, handle_signal) + signal.signal(signal.SIGINT, handle_signal) + + +if __name__ == "__main__": + # Allow running as standalone check + if is_termux(): + success = check_termux_prerequisites(verbose=True) + sys.exit(0 if success else 1) + else: + print("Not running in Termux environment") + sys.exit(0)