mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 22:59:59 -07:00
- Fix SSE fanout thread AttributeError when source queue is None during interpreter shutdown by snapshotting to local variable with null guard - Fix branded "i" logo rendering oversized on first page load (FOUC) by adding inline width/height to SVG elements across 10 templates - Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
317 lines
10 KiB
Python
317 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Mock Intercept Agent for development and testing.
|
|
|
|
This provides a simulated agent that generates fake data for testing
|
|
the controller without needing actual SDR hardware.
|
|
|
|
Usage:
|
|
python tests/mock_agent.py [--port 8021] [--name mock-agent-1]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import random
|
|
import string
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timezone
|
|
|
|
from flask import Flask, jsonify, request
|
|
|
|
app = Flask(__name__)
|
|
|
|
# State
|
|
running_modes: set[str] = set()
|
|
start_time = time.time()
|
|
agent_name = "mock-agent-1"
|
|
|
|
# Simulated data generators
|
|
def generate_aircraft() -> list[dict]:
|
|
"""Generate fake ADS-B aircraft data."""
|
|
aircraft = []
|
|
for _ in range(random.randint(3, 10)):
|
|
icao = ''.join(random.choices(string.hexdigits.upper()[:6], k=6))
|
|
callsign = random.choice(['UAL', 'DAL', 'AAL', 'SWA', 'JBU']) + str(random.randint(100, 9999))
|
|
aircraft.append({
|
|
'icao': icao,
|
|
'callsign': callsign,
|
|
'altitude': random.randint(5000, 45000),
|
|
'speed': random.randint(200, 550),
|
|
'heading': random.randint(0, 359),
|
|
'lat': round(40.0 + random.uniform(-2, 2), 4),
|
|
'lon': round(-74.0 + random.uniform(-2, 2), 4),
|
|
'vertical_rate': random.randint(-2000, 2000),
|
|
'squawk': str(random.randint(1000, 7777)),
|
|
'last_seen': datetime.now(timezone.utc).isoformat()
|
|
})
|
|
return aircraft
|
|
|
|
|
|
def generate_sensors() -> list[dict]:
|
|
"""Generate fake 433MHz sensor data."""
|
|
sensors = []
|
|
models = ['Acurite-Tower', 'Oregon-THGR122N', 'LaCrosse-TX141W', 'Ambient-F007TH']
|
|
for _i in range(random.randint(2, 5)):
|
|
sensors.append({
|
|
'time': datetime.now(timezone.utc).isoformat(),
|
|
'model': random.choice(models),
|
|
'id': random.randint(1, 255),
|
|
'channel': random.randint(1, 3),
|
|
'temperature_C': round(random.uniform(-10, 35), 1),
|
|
'humidity': random.randint(20, 95),
|
|
'battery_ok': random.choice([0, 1])
|
|
})
|
|
return sensors
|
|
|
|
|
|
def generate_wifi_networks() -> list[dict]:
|
|
"""Generate fake WiFi network data."""
|
|
networks = []
|
|
ssids = ['HomeNetwork', 'Linksys', 'NETGEAR', 'xfinitywifi', 'ATT-WIFI', 'CoffeeShop-Guest']
|
|
for ssid in random.sample(ssids, random.randint(3, 6)):
|
|
bssid = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(6)])
|
|
networks.append({
|
|
'ssid': ssid,
|
|
'bssid': bssid,
|
|
'channel': random.choice([1, 6, 11, 36, 40, 44, 48]),
|
|
'signal': random.randint(-80, -30),
|
|
'encryption': random.choice(['WPA2', 'WPA3', 'WEP', 'Open']),
|
|
'clients': random.randint(0, 10),
|
|
'last_seen': datetime.now(timezone.utc).isoformat()
|
|
})
|
|
return networks
|
|
|
|
|
|
def generate_bluetooth_devices() -> list[dict]:
|
|
"""Generate fake Bluetooth device data."""
|
|
devices = []
|
|
names = ['iPhone', 'Galaxy S21', 'AirPods', 'Tile Tracker', 'Fitbit', 'Unknown']
|
|
for _ in range(random.randint(2, 8)):
|
|
mac = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(6)])
|
|
devices.append({
|
|
'address': mac,
|
|
'name': random.choice(names),
|
|
'rssi': random.randint(-90, -40),
|
|
'type': random.choice(['LE', 'Classic', 'Dual']),
|
|
'manufacturer': random.choice(['Apple', 'Samsung', 'Unknown']),
|
|
'last_seen': datetime.now(timezone.utc).isoformat()
|
|
})
|
|
return devices
|
|
|
|
|
|
def generate_vessels() -> list[dict]:
|
|
"""Generate fake AIS vessel data."""
|
|
vessels = []
|
|
vessel_names = ['EVERGREEN', 'MAERSK WINNER', 'OOCL HONG KONG', 'MSC GULSUN', 'CMA CGM MARCO POLO']
|
|
for name in random.sample(vessel_names, random.randint(2, 4)):
|
|
mmsi = str(random.randint(200000000, 800000000))
|
|
vessels.append({
|
|
'mmsi': mmsi,
|
|
'name': name,
|
|
'callsign': ''.join(random.choices(string.ascii_uppercase, k=5)),
|
|
'ship_type': random.choice(['Cargo', 'Tanker', 'Passenger', 'Fishing']),
|
|
'lat': round(40.5 + random.uniform(-0.5, 0.5), 4),
|
|
'lon': round(-73.9 + random.uniform(-0.5, 0.5), 4),
|
|
'speed': round(random.uniform(0, 25), 1),
|
|
'course': random.randint(0, 359),
|
|
'destination': random.choice(['NEW YORK', 'NEWARK', 'BALTIMORE', 'BOSTON']),
|
|
'last_seen': datetime.now(timezone.utc).isoformat()
|
|
})
|
|
return vessels
|
|
|
|
|
|
# Data snapshot storage
|
|
data_snapshots: dict[str, list] = {}
|
|
|
|
|
|
def update_data_snapshot(mode: str):
|
|
"""Update data snapshot for a mode."""
|
|
if mode == 'adsb':
|
|
data_snapshots[mode] = generate_aircraft()
|
|
elif mode == 'sensor':
|
|
data_snapshots[mode] = generate_sensors()
|
|
elif mode == 'wifi':
|
|
data_snapshots[mode] = generate_wifi_networks()
|
|
elif mode == 'bluetooth':
|
|
data_snapshots[mode] = generate_bluetooth_devices()
|
|
elif mode == 'ais':
|
|
data_snapshots[mode] = generate_vessels()
|
|
else:
|
|
data_snapshots[mode] = []
|
|
|
|
|
|
# Background data generation threads
|
|
data_threads: dict[str, threading.Event] = {}
|
|
|
|
|
|
def data_generator_loop(mode: str, stop_event: threading.Event):
|
|
"""Background loop to generate data periodically."""
|
|
while not stop_event.is_set():
|
|
update_data_snapshot(mode)
|
|
stop_event.wait(random.uniform(2, 5))
|
|
|
|
|
|
# =============================================================================
|
|
# Routes
|
|
# =============================================================================
|
|
|
|
@app.route('/capabilities')
|
|
def capabilities():
|
|
"""Return mock capabilities."""
|
|
return jsonify({
|
|
'modes': {
|
|
'pager': True,
|
|
'sensor': True,
|
|
'adsb': True,
|
|
'ais': True,
|
|
'acars': True,
|
|
'aprs': True,
|
|
'wifi': True,
|
|
'bluetooth': True,
|
|
'dsc': True,
|
|
'rtlamr': True,
|
|
'tscm': True,
|
|
'satellite': True,
|
|
'listening_post': True
|
|
},
|
|
'devices': [
|
|
{'index': 0, 'name': 'Mock RTL-SDR', 'type': 'rtlsdr', 'serial': 'MOCK001'}
|
|
],
|
|
'agent_version': '1.0.0-mock'
|
|
})
|
|
|
|
|
|
@app.route('/status')
|
|
def status():
|
|
"""Return agent status."""
|
|
return jsonify({
|
|
'running_modes': list(running_modes),
|
|
'uptime': time.time() - start_time,
|
|
'push_enabled': False,
|
|
'push_connected': False
|
|
})
|
|
|
|
|
|
@app.route('/health')
|
|
def health():
|
|
"""Health check."""
|
|
return jsonify({'status': 'healthy', 'version': '1.0.0-mock'})
|
|
|
|
|
|
@app.route('/config', methods=['GET', 'POST'])
|
|
def config():
|
|
"""Config endpoint."""
|
|
if request.method == 'POST':
|
|
return jsonify({'status': 'updated', 'config': {}})
|
|
return jsonify({
|
|
'name': agent_name,
|
|
'port': request.environ.get('SERVER_PORT', 8021),
|
|
'push_enabled': False,
|
|
'modes_enabled': dict.fromkeys(['pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'], True)
|
|
})
|
|
|
|
|
|
@app.route('/<mode>/start', methods=['POST'])
|
|
def start_mode(mode: str):
|
|
"""Start a mode."""
|
|
if mode in running_modes:
|
|
return jsonify({'status': 'error', 'message': f'{mode} already running'}), 409
|
|
|
|
running_modes.add(mode)
|
|
|
|
# Start data generation thread
|
|
stop_event = threading.Event()
|
|
data_threads[mode] = stop_event
|
|
thread = threading.Thread(target=data_generator_loop, args=(mode, stop_event))
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
# Generate initial data
|
|
update_data_snapshot(mode)
|
|
|
|
return jsonify({'status': 'started', 'mode': mode})
|
|
|
|
|
|
@app.route('/<mode>/stop', methods=['POST'])
|
|
def stop_mode(mode: str):
|
|
"""Stop a mode."""
|
|
if mode not in running_modes:
|
|
return jsonify({'status': 'not_running'})
|
|
|
|
running_modes.discard(mode)
|
|
|
|
# Stop data generation thread
|
|
if mode in data_threads:
|
|
data_threads[mode].set()
|
|
del data_threads[mode]
|
|
|
|
# Clear data
|
|
if mode in data_snapshots:
|
|
del data_snapshots[mode]
|
|
|
|
return jsonify({'status': 'stopped', 'mode': mode})
|
|
|
|
|
|
@app.route('/<mode>/status')
|
|
def mode_status(mode: str):
|
|
"""Get mode status."""
|
|
return jsonify({
|
|
'running': mode in running_modes,
|
|
'data_count': len(data_snapshots.get(mode, []))
|
|
})
|
|
|
|
|
|
@app.route('/<mode>/data')
|
|
def mode_data(mode: str):
|
|
"""Get current data snapshot."""
|
|
# Generate fresh data if mode is running but no snapshot exists
|
|
if mode in running_modes and mode not in data_snapshots:
|
|
update_data_snapshot(mode)
|
|
|
|
return jsonify({
|
|
'mode': mode,
|
|
'data': data_snapshots.get(mode, []),
|
|
'timestamp': datetime.now(timezone.utc).isoformat(),
|
|
'agent_name': agent_name
|
|
})
|
|
|
|
|
|
# =============================================================================
|
|
# Main
|
|
# =============================================================================
|
|
|
|
def main():
|
|
global agent_name, start_time
|
|
|
|
parser = argparse.ArgumentParser(description='Mock Intercept Agent')
|
|
parser.add_argument('--port', '-p', type=int, default=8021, help='Port (default: 8021)')
|
|
parser.add_argument('--name', '-n', default='mock-agent-1', help='Agent name')
|
|
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
|
|
|
args = parser.parse_args()
|
|
agent_name = args.name
|
|
start_time = time.time()
|
|
|
|
print("=" * 60)
|
|
print(" MOCK INTERCEPT AGENT")
|
|
print(" For development and testing")
|
|
print("=" * 60)
|
|
print()
|
|
print(f" Agent Name: {agent_name}")
|
|
print(f" Port: {args.port}")
|
|
print()
|
|
print(" Available modes: all (simulated data)")
|
|
print()
|
|
print(f" Listening on http://0.0.0.0:{args.port}")
|
|
print()
|
|
print(" Press Ctrl+C to stop")
|
|
print()
|
|
|
|
app.run(host='0.0.0.0', port=args.port, debug=args.debug)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|