mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 17:18:11 -07:00
Adding Vessels
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
INTERCEPT is a web-based Signal Intelligence (SIGINT) platform providing a unified Flask interface for software-defined radio (SDR) tools. It supports pager decoding, 433MHz sensors, ADS-B aircraft tracking, ACARS messaging, WiFi/Bluetooth scanning, and satellite tracking.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
### Setup and Running
|
||||||
|
```bash
|
||||||
|
# Initial setup (installs dependencies and configures SDR tools)
|
||||||
|
./setup.sh
|
||||||
|
|
||||||
|
# Run the application (requires sudo for SDR/network access)
|
||||||
|
sudo -E venv/bin/python intercept.py
|
||||||
|
|
||||||
|
# Or activate venv first
|
||||||
|
source venv/bin/activate
|
||||||
|
sudo -E python intercept.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/test_bluetooth.py
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
pytest --cov=routes --cov=utils
|
||||||
|
|
||||||
|
# Run a specific test
|
||||||
|
pytest tests/test_bluetooth.py::test_function_name -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting and Formatting
|
||||||
|
```bash
|
||||||
|
# Lint with ruff
|
||||||
|
ruff check .
|
||||||
|
|
||||||
|
# Auto-fix linting issues
|
||||||
|
ruff check --fix .
|
||||||
|
|
||||||
|
# Format with black
|
||||||
|
black .
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
mypy .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
- `intercept.py` - Main entry point script
|
||||||
|
- `app.py` - Flask application initialization, global state management, process lifecycle, SSE streaming infrastructure
|
||||||
|
|
||||||
|
### Route Blueprints (routes/)
|
||||||
|
Each signal type has its own Flask blueprint:
|
||||||
|
- `pager.py` - POCSAG/FLEX decoding via rtl_fm + multimon-ng
|
||||||
|
- `sensor.py` - 433MHz IoT sensors via rtl_433
|
||||||
|
- `adsb.py` - Aircraft tracking via dump1090 (SBS protocol on port 30003)
|
||||||
|
- `acars.py` - Aircraft datalink messages via acarsdec
|
||||||
|
- `wifi.py`, `wifi_v2.py` - WiFi scanning (legacy and unified APIs)
|
||||||
|
- `bluetooth.py`, `bluetooth_v2.py` - Bluetooth scanning (legacy and unified APIs)
|
||||||
|
- `satellite.py` - Pass prediction using TLE data
|
||||||
|
- `aprs.py` - Amateur packet radio via direwolf
|
||||||
|
- `rtlamr.py` - Utility meter reading
|
||||||
|
|
||||||
|
### Core Utilities (utils/)
|
||||||
|
|
||||||
|
**SDR Abstraction Layer** (`utils/sdr/`):
|
||||||
|
- `SDRFactory` with factory pattern for multiple SDR types (RTL-SDR, LimeSDR, HackRF, Airspy, SDRPlay)
|
||||||
|
- Each type has a `CommandBuilder` for generating CLI commands
|
||||||
|
|
||||||
|
**Bluetooth Module** (`utils/bluetooth/`):
|
||||||
|
- Multi-backend: DBus/BlueZ primary, fallback for systems without BlueZ
|
||||||
|
- `aggregator.py` - Merges observations across time
|
||||||
|
- `tracker_signatures.py` - 47K+ known tracker fingerprints (AirTag, Tile, SmartTag)
|
||||||
|
- `heuristics.py` - Behavioral analysis for device classification
|
||||||
|
|
||||||
|
**TSCM (Counter-Surveillance)** (`utils/tscm/`):
|
||||||
|
- `baseline.py` - Snapshot "normal" RF environment
|
||||||
|
- `detector.py` - Compare current scan to baseline, flag anomalies
|
||||||
|
- `device_identity.py` - Track devices despite MAC randomization
|
||||||
|
- `correlation.py` - Cross-reference Bluetooth and WiFi observations
|
||||||
|
|
||||||
|
**WiFi Utilities** (`utils/wifi/`):
|
||||||
|
- Platform-agnostic scanner with parsers for airodump-ng, nmcli, iw, iwlist, airport (macOS)
|
||||||
|
- `channel_analyzer.py` - Frequency band analysis
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
**Server-Sent Events (SSE)**: All real-time features stream via SSE endpoints (`/stream_pager`, `/stream_sensor`, etc.). Pattern uses `queue.Queue` with timeout and keepalive messages.
|
||||||
|
|
||||||
|
**Process Management**: External decoders run as subprocesses with output threads feeding queues. Use `safe_terminate()` for cleanup. Global locks prevent race conditions.
|
||||||
|
|
||||||
|
**Data Stores**: `DataStore` class with TTL-based automatic cleanup (WiFi: 10min, Bluetooth: 5min, Aircraft: 5min).
|
||||||
|
|
||||||
|
**Input Validation**: Centralized in `utils/validation.py` - always validate frequencies, gains, device indices before spawning processes.
|
||||||
|
|
||||||
|
### External Tool Integrations
|
||||||
|
|
||||||
|
| Tool | Purpose | Integration |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| rtl_fm | FM demodulation | Subprocess, pipes to multimon-ng |
|
||||||
|
| multimon-ng | Pager decoding | Reads from rtl_fm stdout |
|
||||||
|
| rtl_433 | 433MHz sensors | JSON output parsing |
|
||||||
|
| dump1090 | ADS-B decoding | SBS protocol socket (port 30003) |
|
||||||
|
| acarsdec | ACARS messages | Output parsing |
|
||||||
|
| airmon-ng/airodump-ng | WiFi scanning | Monitor mode, CSV parsing |
|
||||||
|
| bluetoothctl/hcitool | Bluetooth | Fallback when DBus unavailable |
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- `config.py` - Environment variable support with `INTERCEPT_` prefix
|
||||||
|
- Database: SQLite in `instance/` directory for settings, baselines, history
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
Tests use pytest with extensive mocking of external tools. Key fixtures in `tests/conftest.py`. Mock subprocess calls when testing decoder integration.
|
||||||
@@ -36,6 +36,7 @@ from utils.constants import (
|
|||||||
MAX_AIRCRAFT_AGE_SECONDS,
|
MAX_AIRCRAFT_AGE_SECONDS,
|
||||||
MAX_WIFI_NETWORK_AGE_SECONDS,
|
MAX_WIFI_NETWORK_AGE_SECONDS,
|
||||||
MAX_BT_DEVICE_AGE_SECONDS,
|
MAX_BT_DEVICE_AGE_SECONDS,
|
||||||
|
MAX_VESSEL_AGE_SECONDS,
|
||||||
QUEUE_MAX_SIZE,
|
QUEUE_MAX_SIZE,
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
@@ -139,6 +140,11 @@ rtlamr_process = None
|
|||||||
rtlamr_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
rtlamr_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
rtlamr_lock = threading.Lock()
|
rtlamr_lock = threading.Lock()
|
||||||
|
|
||||||
|
# AIS vessel tracking
|
||||||
|
ais_process = None
|
||||||
|
ais_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
|
ais_lock = threading.Lock()
|
||||||
|
|
||||||
# TSCM (Technical Surveillance Countermeasures)
|
# TSCM (Technical Surveillance Countermeasures)
|
||||||
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
tscm_lock = threading.Lock()
|
tscm_lock = threading.Lock()
|
||||||
@@ -166,6 +172,9 @@ bt_services = {} # MAC -> list of services (not auto-cleaned, user-requested
|
|||||||
# Aircraft (ADS-B) state - using DataStore for automatic cleanup
|
# Aircraft (ADS-B) state - using DataStore for automatic cleanup
|
||||||
adsb_aircraft = DataStore(max_age_seconds=MAX_AIRCRAFT_AGE_SECONDS, name='adsb_aircraft')
|
adsb_aircraft = DataStore(max_age_seconds=MAX_AIRCRAFT_AGE_SECONDS, name='adsb_aircraft')
|
||||||
|
|
||||||
|
# Vessel (AIS) state - using DataStore for automatic cleanup
|
||||||
|
ais_vessels = DataStore(max_age_seconds=MAX_VESSEL_AGE_SECONDS, name='ais_vessels')
|
||||||
|
|
||||||
# Satellite state
|
# Satellite state
|
||||||
satellite_passes = [] # Predicted satellite passes (not auto-cleaned, calculated)
|
satellite_passes = [] # Predicted satellite passes (not auto-cleaned, calculated)
|
||||||
|
|
||||||
@@ -175,6 +184,7 @@ cleanup_manager.register(wifi_clients)
|
|||||||
cleanup_manager.register(bt_devices)
|
cleanup_manager.register(bt_devices)
|
||||||
cleanup_manager.register(bt_beacons)
|
cleanup_manager.register(bt_beacons)
|
||||||
cleanup_manager.register(adsb_aircraft)
|
cleanup_manager.register(adsb_aircraft)
|
||||||
|
cleanup_manager.register(ais_vessels)
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -501,6 +511,7 @@ def health_check() -> Response:
|
|||||||
'pager': current_process is not None and (current_process.poll() is None if current_process else False),
|
'pager': current_process is not None and (current_process.poll() is None if current_process else False),
|
||||||
'sensor': sensor_process is not None and (sensor_process.poll() is None if sensor_process else False),
|
'sensor': sensor_process is not None and (sensor_process.poll() is None if sensor_process else False),
|
||||||
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
|
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
|
||||||
|
'ais': ais_process is not None and (ais_process.poll() is None if ais_process else False),
|
||||||
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
|
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
|
||||||
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
|
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
|
||||||
'wifi': wifi_process is not None and (wifi_process.poll() is None if wifi_process else False),
|
'wifi': wifi_process is not None and (wifi_process.poll() is None if wifi_process else False),
|
||||||
@@ -508,6 +519,7 @@ def health_check() -> Response:
|
|||||||
},
|
},
|
||||||
'data': {
|
'data': {
|
||||||
'aircraft_count': len(adsb_aircraft),
|
'aircraft_count': len(adsb_aircraft),
|
||||||
|
'vessel_count': len(ais_vessels),
|
||||||
'wifi_networks_count': len(wifi_networks),
|
'wifi_networks_count': len(wifi_networks),
|
||||||
'wifi_clients_count': len(wifi_clients),
|
'wifi_clients_count': len(wifi_clients),
|
||||||
'bt_devices_count': len(bt_devices),
|
'bt_devices_count': len(bt_devices),
|
||||||
@@ -518,17 +530,18 @@ def health_check() -> Response:
|
|||||||
@app.route('/killall', methods=['POST'])
|
@app.route('/killall', methods=['POST'])
|
||||||
def kill_all() -> Response:
|
def kill_all() -> Response:
|
||||||
"""Kill all decoder and WiFi processes."""
|
"""Kill all decoder and WiFi processes."""
|
||||||
global current_process, sensor_process, wifi_process, adsb_process, acars_process
|
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
||||||
global aprs_process, aprs_rtl_process
|
global aprs_process, aprs_rtl_process
|
||||||
|
|
||||||
# Import adsb module to reset its state
|
# Import adsb and ais modules to reset their state
|
||||||
from routes import adsb as adsb_module
|
from routes import adsb as adsb_module
|
||||||
|
from routes import ais as ais_module
|
||||||
|
|
||||||
killed = []
|
killed = []
|
||||||
processes_to_kill = [
|
processes_to_kill = [
|
||||||
'rtl_fm', 'multimon-ng', 'rtl_433',
|
'rtl_fm', 'multimon-ng', 'rtl_433',
|
||||||
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
||||||
'dump1090', 'acarsdec', 'direwolf'
|
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher'
|
||||||
]
|
]
|
||||||
|
|
||||||
for proc in processes_to_kill:
|
for proc in processes_to_kill:
|
||||||
@@ -553,6 +566,11 @@ def kill_all() -> Response:
|
|||||||
adsb_process = None
|
adsb_process = None
|
||||||
adsb_module.adsb_using_service = False
|
adsb_module.adsb_using_service = False
|
||||||
|
|
||||||
|
# Reset AIS state
|
||||||
|
with ais_lock:
|
||||||
|
ais_process = None
|
||||||
|
ais_module.ais_running = False
|
||||||
|
|
||||||
# Reset ACARS state
|
# Reset ACARS state
|
||||||
with acars_lock:
|
with acars_lock:
|
||||||
acars_process = None
|
acars_process = None
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ def register_blueprints(app):
|
|||||||
from .bluetooth import bluetooth_bp
|
from .bluetooth import bluetooth_bp
|
||||||
from .bluetooth_v2 import bluetooth_v2_bp
|
from .bluetooth_v2 import bluetooth_v2_bp
|
||||||
from .adsb import adsb_bp
|
from .adsb import adsb_bp
|
||||||
|
from .ais import ais_bp
|
||||||
from .acars import acars_bp
|
from .acars import acars_bp
|
||||||
from .aprs import aprs_bp
|
from .aprs import aprs_bp
|
||||||
from .satellite import satellite_bp
|
from .satellite import satellite_bp
|
||||||
@@ -27,6 +28,7 @@ def register_blueprints(app):
|
|||||||
app.register_blueprint(bluetooth_bp)
|
app.register_blueprint(bluetooth_bp)
|
||||||
app.register_blueprint(bluetooth_v2_bp) # New unified Bluetooth API
|
app.register_blueprint(bluetooth_v2_bp) # New unified Bluetooth API
|
||||||
app.register_blueprint(adsb_bp)
|
app.register_blueprint(adsb_bp)
|
||||||
|
app.register_blueprint(ais_bp)
|
||||||
app.register_blueprint(acars_bp)
|
app.register_blueprint(acars_bp)
|
||||||
app.register_blueprint(aprs_bp)
|
app.register_blueprint(aprs_bp)
|
||||||
app.register_blueprint(satellite_bp)
|
app.register_blueprint(satellite_bp)
|
||||||
|
|||||||
+480
@@ -0,0 +1,480 @@
|
|||||||
|
"""AIS vessel tracking routes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request, Response, render_template
|
||||||
|
|
||||||
|
import app as app_module
|
||||||
|
from utils.logging import get_logger
|
||||||
|
from utils.validation import validate_device_index, validate_gain
|
||||||
|
from utils.sse import format_sse
|
||||||
|
from utils.sdr import SDRFactory, SDRType
|
||||||
|
from utils.constants import (
|
||||||
|
AIS_TCP_PORT,
|
||||||
|
AIS_TERMINATE_TIMEOUT,
|
||||||
|
AIS_SOCKET_TIMEOUT,
|
||||||
|
AIS_RECONNECT_DELAY,
|
||||||
|
AIS_UPDATE_INTERVAL,
|
||||||
|
SOCKET_BUFFER_SIZE,
|
||||||
|
SSE_KEEPALIVE_INTERVAL,
|
||||||
|
SSE_QUEUE_TIMEOUT,
|
||||||
|
SOCKET_CONNECT_TIMEOUT,
|
||||||
|
PROCESS_TERMINATE_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = get_logger('intercept.ais')
|
||||||
|
|
||||||
|
ais_bp = Blueprint('ais', __name__, url_prefix='/ais')
|
||||||
|
|
||||||
|
# Track AIS state
|
||||||
|
ais_running = False
|
||||||
|
ais_connected = False
|
||||||
|
ais_messages_received = 0
|
||||||
|
ais_last_message_time = None
|
||||||
|
ais_active_device = None
|
||||||
|
_ais_error_logged = False
|
||||||
|
|
||||||
|
# Common installation paths for AIS-catcher
|
||||||
|
AIS_CATCHER_PATHS = [
|
||||||
|
'/usr/local/bin/AIS-catcher',
|
||||||
|
'/usr/bin/AIS-catcher',
|
||||||
|
'/opt/homebrew/bin/AIS-catcher',
|
||||||
|
'/opt/homebrew/bin/aiscatcher',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_ais_catcher():
|
||||||
|
"""Find AIS-catcher binary, checking PATH and common locations."""
|
||||||
|
# First try PATH
|
||||||
|
for name in ['AIS-catcher', 'aiscatcher']:
|
||||||
|
path = shutil.which(name)
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
# Check common installation paths
|
||||||
|
for path in AIS_CATCHER_PATHS:
|
||||||
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ais_stream(port: int):
|
||||||
|
"""Parse JSON data from AIS-catcher TCP server."""
|
||||||
|
global ais_running, ais_connected, ais_messages_received, ais_last_message_time, _ais_error_logged
|
||||||
|
|
||||||
|
logger.info(f"AIS stream parser started, connecting to localhost:{port}")
|
||||||
|
ais_connected = False
|
||||||
|
ais_messages_received = 0
|
||||||
|
_ais_error_logged = False
|
||||||
|
|
||||||
|
while ais_running:
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(AIS_SOCKET_TIMEOUT)
|
||||||
|
sock.connect(('localhost', port))
|
||||||
|
ais_connected = True
|
||||||
|
_ais_error_logged = False
|
||||||
|
logger.info("Connected to AIS-catcher TCP server")
|
||||||
|
|
||||||
|
buffer = ""
|
||||||
|
last_update = time.time()
|
||||||
|
pending_updates = set()
|
||||||
|
|
||||||
|
while ais_running:
|
||||||
|
try:
|
||||||
|
data = sock.recv(SOCKET_BUFFER_SIZE).decode('utf-8', errors='ignore')
|
||||||
|
if not data:
|
||||||
|
logger.warning("AIS connection closed (no data)")
|
||||||
|
break
|
||||||
|
buffer += data
|
||||||
|
|
||||||
|
while '\n' in buffer:
|
||||||
|
line, buffer = buffer.split('\n', 1)
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = json.loads(line)
|
||||||
|
vessel = process_ais_message(msg)
|
||||||
|
if vessel:
|
||||||
|
mmsi = vessel.get('mmsi')
|
||||||
|
if mmsi:
|
||||||
|
app_module.ais_vessels.set(mmsi, vessel)
|
||||||
|
pending_updates.add(mmsi)
|
||||||
|
ais_messages_received += 1
|
||||||
|
ais_last_message_time = time.time()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
if ais_messages_received < 5:
|
||||||
|
logger.debug(f"Invalid JSON: {line[:100]}")
|
||||||
|
|
||||||
|
# Batch updates
|
||||||
|
now = time.time()
|
||||||
|
if now - last_update >= AIS_UPDATE_INTERVAL:
|
||||||
|
for mmsi in pending_updates:
|
||||||
|
if mmsi in app_module.ais_vessels:
|
||||||
|
try:
|
||||||
|
app_module.ais_queue.put_nowait({
|
||||||
|
'type': 'vessel',
|
||||||
|
**app_module.ais_vessels[mmsi]
|
||||||
|
})
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
pending_updates.clear()
|
||||||
|
last_update = now
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sock.close()
|
||||||
|
ais_connected = False
|
||||||
|
except OSError as e:
|
||||||
|
ais_connected = False
|
||||||
|
if not _ais_error_logged:
|
||||||
|
logger.warning(f"AIS connection error: {e}, reconnecting...")
|
||||||
|
_ais_error_logged = True
|
||||||
|
time.sleep(AIS_RECONNECT_DELAY)
|
||||||
|
|
||||||
|
ais_connected = False
|
||||||
|
logger.info("AIS stream parser stopped")
|
||||||
|
|
||||||
|
|
||||||
|
def process_ais_message(msg: dict) -> dict | None:
|
||||||
|
"""Process AIS-catcher JSON message and extract vessel data."""
|
||||||
|
# AIS-catcher outputs different message types
|
||||||
|
# We're interested in position reports and static data
|
||||||
|
|
||||||
|
mmsi = msg.get('mmsi')
|
||||||
|
if not mmsi:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mmsi = str(mmsi)
|
||||||
|
|
||||||
|
# Get existing vessel data or create new
|
||||||
|
vessel = app_module.ais_vessels.get(mmsi) or {'mmsi': mmsi}
|
||||||
|
|
||||||
|
# Extract common fields
|
||||||
|
if 'lat' in msg and 'lon' in msg:
|
||||||
|
try:
|
||||||
|
lat = float(msg['lat'])
|
||||||
|
lon = float(msg['lon'])
|
||||||
|
# Validate coordinates (AIS uses 181 for unavailable)
|
||||||
|
if -90 <= lat <= 90 and -180 <= lon <= 180:
|
||||||
|
vessel['lat'] = lat
|
||||||
|
vessel['lon'] = lon
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Speed over ground (knots)
|
||||||
|
if 'speed' in msg:
|
||||||
|
try:
|
||||||
|
speed = float(msg['speed'])
|
||||||
|
if speed < 102.3: # 102.3 = not available
|
||||||
|
vessel['speed'] = round(speed, 1)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Course over ground (degrees)
|
||||||
|
if 'course' in msg:
|
||||||
|
try:
|
||||||
|
course = float(msg['course'])
|
||||||
|
if course < 360: # 360 = not available
|
||||||
|
vessel['course'] = round(course, 1)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# True heading (degrees)
|
||||||
|
if 'heading' in msg:
|
||||||
|
try:
|
||||||
|
heading = int(msg['heading'])
|
||||||
|
if heading < 511: # 511 = not available
|
||||||
|
vessel['heading'] = heading
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Navigation status
|
||||||
|
if 'status' in msg:
|
||||||
|
vessel['nav_status'] = msg['status']
|
||||||
|
if 'status_text' in msg:
|
||||||
|
vessel['nav_status_text'] = msg['status_text']
|
||||||
|
|
||||||
|
# Vessel name (from Type 5 or Type 24 messages)
|
||||||
|
if 'shipname' in msg:
|
||||||
|
name = msg['shipname'].strip().strip('@')
|
||||||
|
if name:
|
||||||
|
vessel['name'] = name
|
||||||
|
|
||||||
|
# Callsign
|
||||||
|
if 'callsign' in msg:
|
||||||
|
callsign = msg['callsign'].strip().strip('@')
|
||||||
|
if callsign:
|
||||||
|
vessel['callsign'] = callsign
|
||||||
|
|
||||||
|
# Ship type
|
||||||
|
if 'shiptype' in msg:
|
||||||
|
vessel['ship_type'] = msg['shiptype']
|
||||||
|
if 'shiptype_text' in msg:
|
||||||
|
vessel['ship_type_text'] = msg['shiptype_text']
|
||||||
|
|
||||||
|
# Destination
|
||||||
|
if 'destination' in msg:
|
||||||
|
dest = msg['destination'].strip().strip('@')
|
||||||
|
if dest:
|
||||||
|
vessel['destination'] = dest
|
||||||
|
|
||||||
|
# ETA
|
||||||
|
if 'eta' in msg:
|
||||||
|
vessel['eta'] = msg['eta']
|
||||||
|
|
||||||
|
# Dimensions
|
||||||
|
if 'to_bow' in msg and 'to_stern' in msg:
|
||||||
|
try:
|
||||||
|
length = int(msg['to_bow']) + int(msg['to_stern'])
|
||||||
|
if length > 0:
|
||||||
|
vessel['length'] = length
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'to_port' in msg and 'to_starboard' in msg:
|
||||||
|
try:
|
||||||
|
width = int(msg['to_port']) + int(msg['to_starboard'])
|
||||||
|
if width > 0:
|
||||||
|
vessel['width'] = width
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Draught
|
||||||
|
if 'draught' in msg:
|
||||||
|
try:
|
||||||
|
draught = float(msg['draught'])
|
||||||
|
if draught > 0:
|
||||||
|
vessel['draught'] = draught
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Rate of turn
|
||||||
|
if 'turn' in msg:
|
||||||
|
try:
|
||||||
|
turn = float(msg['turn'])
|
||||||
|
if -127 <= turn <= 127: # Valid range
|
||||||
|
vessel['rate_of_turn'] = turn
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Message type for debugging
|
||||||
|
if 'type' in msg:
|
||||||
|
vessel['last_msg_type'] = msg['type']
|
||||||
|
|
||||||
|
# Timestamp
|
||||||
|
vessel['last_seen'] = time.time()
|
||||||
|
|
||||||
|
return vessel
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/tools')
|
||||||
|
def check_ais_tools():
|
||||||
|
"""Check for AIS decoding tools and hardware."""
|
||||||
|
has_ais_catcher = find_ais_catcher() is not None
|
||||||
|
|
||||||
|
# Check what SDR hardware is detected
|
||||||
|
devices = SDRFactory.detect_devices()
|
||||||
|
has_rtlsdr = any(d.sdr_type == SDRType.RTL_SDR for d in devices)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'ais_catcher': has_ais_catcher,
|
||||||
|
'ais_catcher_path': find_ais_catcher(),
|
||||||
|
'has_rtlsdr': has_rtlsdr,
|
||||||
|
'device_count': len(devices)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/status')
|
||||||
|
def ais_status():
|
||||||
|
"""Get AIS tracking status for debugging."""
|
||||||
|
process_running = False
|
||||||
|
if app_module.ais_process:
|
||||||
|
process_running = app_module.ais_process.poll() is None
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'tracking_active': ais_running,
|
||||||
|
'active_device': ais_active_device,
|
||||||
|
'connected': ais_connected,
|
||||||
|
'messages_received': ais_messages_received,
|
||||||
|
'last_message_time': ais_last_message_time,
|
||||||
|
'vessel_count': len(app_module.ais_vessels),
|
||||||
|
'vessels': dict(app_module.ais_vessels),
|
||||||
|
'queue_size': app_module.ais_queue.qsize(),
|
||||||
|
'ais_catcher_path': find_ais_catcher(),
|
||||||
|
'process_running': process_running
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/start', methods=['POST'])
|
||||||
|
def start_ais():
|
||||||
|
"""Start AIS tracking."""
|
||||||
|
global ais_running, ais_active_device
|
||||||
|
|
||||||
|
with app_module.ais_lock:
|
||||||
|
if ais_running:
|
||||||
|
return jsonify({'status': 'already_running', 'message': 'AIS tracking already active'}), 409
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
try:
|
||||||
|
gain = int(validate_gain(data.get('gain', '40')))
|
||||||
|
device = validate_device_index(data.get('device', '0'))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
# Find AIS-catcher
|
||||||
|
ais_catcher_path = find_ais_catcher()
|
||||||
|
if not ais_catcher_path:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'AIS-catcher not found. Install from https://github.com/jvde-github/AIS-catcher/releases'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Get SDR type from request
|
||||||
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
|
try:
|
||||||
|
sdr_type = SDRType(sdr_type_str)
|
||||||
|
except ValueError:
|
||||||
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
|
||||||
|
# Kill any existing process
|
||||||
|
if app_module.ais_process:
|
||||||
|
try:
|
||||||
|
pgid = os.getpgid(app_module.ais_process.pid)
|
||||||
|
os.killpg(pgid, 15)
|
||||||
|
app_module.ais_process.wait(timeout=PROCESS_TERMINATE_TIMEOUT)
|
||||||
|
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
|
||||||
|
try:
|
||||||
|
pgid = os.getpgid(app_module.ais_process.pid)
|
||||||
|
os.killpg(pgid, 9)
|
||||||
|
except (ProcessLookupError, OSError):
|
||||||
|
pass
|
||||||
|
app_module.ais_process = None
|
||||||
|
logger.info("Killed existing AIS process")
|
||||||
|
|
||||||
|
# Build command using SDR abstraction
|
||||||
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
|
|
||||||
|
bias_t = data.get('bias_t', False)
|
||||||
|
tcp_port = AIS_TCP_PORT
|
||||||
|
|
||||||
|
cmd = builder.build_ais_command(
|
||||||
|
device=sdr_device,
|
||||||
|
gain=float(gain),
|
||||||
|
bias_t=bias_t,
|
||||||
|
tcp_port=tcp_port
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the found AIS-catcher path
|
||||||
|
cmd[0] = ais_catcher_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Starting AIS-catcher with device {device}: {' '.join(cmd)}")
|
||||||
|
app_module.ais_process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for process to start
|
||||||
|
time.sleep(2.0)
|
||||||
|
|
||||||
|
if app_module.ais_process.poll() is not None:
|
||||||
|
stderr_output = ''
|
||||||
|
if app_module.ais_process.stderr:
|
||||||
|
try:
|
||||||
|
stderr_output = app_module.ais_process.stderr.read().decode('utf-8', errors='ignore').strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
error_msg = 'AIS-catcher failed to start. Check SDR device connection.'
|
||||||
|
if stderr_output:
|
||||||
|
error_msg += f' Error: {stderr_output[:200]}'
|
||||||
|
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||||
|
|
||||||
|
ais_running = True
|
||||||
|
ais_active_device = device
|
||||||
|
|
||||||
|
# Start TCP parser thread
|
||||||
|
thread = threading.Thread(target=parse_ais_stream, args=(tcp_port,), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'started',
|
||||||
|
'message': 'AIS tracking started',
|
||||||
|
'device': device,
|
||||||
|
'port': tcp_port
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start AIS-catcher: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/stop', methods=['POST'])
|
||||||
|
def stop_ais():
|
||||||
|
"""Stop AIS tracking."""
|
||||||
|
global ais_running, ais_active_device
|
||||||
|
|
||||||
|
with app_module.ais_lock:
|
||||||
|
if app_module.ais_process:
|
||||||
|
try:
|
||||||
|
pgid = os.getpgid(app_module.ais_process.pid)
|
||||||
|
os.killpg(pgid, 15)
|
||||||
|
app_module.ais_process.wait(timeout=AIS_TERMINATE_TIMEOUT)
|
||||||
|
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
|
||||||
|
try:
|
||||||
|
pgid = os.getpgid(app_module.ais_process.pid)
|
||||||
|
os.killpg(pgid, 9)
|
||||||
|
except (ProcessLookupError, OSError):
|
||||||
|
pass
|
||||||
|
app_module.ais_process = None
|
||||||
|
logger.info("AIS process stopped")
|
||||||
|
ais_running = False
|
||||||
|
ais_active_device = None
|
||||||
|
|
||||||
|
app_module.ais_vessels.clear()
|
||||||
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/stream')
|
||||||
|
def stream_ais():
|
||||||
|
"""SSE stream for AIS vessels."""
|
||||||
|
def generate() -> Generator[str, None, None]:
|
||||||
|
last_keepalive = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
msg = app_module.ais_queue.get(timeout=SSE_QUEUE_TIMEOUT)
|
||||||
|
last_keepalive = time.time()
|
||||||
|
yield format_sse(msg)
|
||||||
|
except queue.Empty:
|
||||||
|
now = time.time()
|
||||||
|
if now - last_keepalive >= SSE_KEEPALIVE_INTERVAL:
|
||||||
|
yield format_sse({'type': 'keepalive'})
|
||||||
|
last_keepalive = now
|
||||||
|
|
||||||
|
response = Response(generate(), mimetype='text/event-stream')
|
||||||
|
response.headers['Cache-Control'] = 'no-cache'
|
||||||
|
response.headers['X-Accel-Buffering'] = 'no'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/dashboard')
|
||||||
|
def ais_dashboard():
|
||||||
|
"""Popout AIS dashboard."""
|
||||||
|
return render_template('ais_dashboard.html')
|
||||||
@@ -203,6 +203,7 @@ check_tools() {
|
|||||||
check_optional "rtlamr" "Utility meter decoder (requires Go)" rtlamr
|
check_optional "rtlamr" "Utility meter decoder (requires Go)" rtlamr
|
||||||
check_required "dump1090" "ADS-B decoder" dump1090
|
check_required "dump1090" "ADS-B decoder" dump1090
|
||||||
check_required "acarsdec" "ACARS decoder" acarsdec
|
check_required "acarsdec" "ACARS decoder" acarsdec
|
||||||
|
check_required "AIS-catcher" "AIS vessel decoder" AIS-catcher aiscatcher
|
||||||
|
|
||||||
echo
|
echo
|
||||||
info "GPS:"
|
info "GPS:"
|
||||||
@@ -412,7 +413,7 @@ install_multimon_ng_from_source_macos() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_macos_packages() {
|
install_macos_packages() {
|
||||||
TOTAL_STEPS=13
|
TOTAL_STEPS=14
|
||||||
CURRENT_STEP=0
|
CURRENT_STEP=0
|
||||||
|
|
||||||
progress "Checking Homebrew"
|
progress "Checking Homebrew"
|
||||||
@@ -458,6 +459,13 @@ install_macos_packages() {
|
|||||||
progress "Installing acarsdec"
|
progress "Installing acarsdec"
|
||||||
(brew_install acarsdec) || warn "acarsdec not available via Homebrew"
|
(brew_install acarsdec) || warn "acarsdec not available via Homebrew"
|
||||||
|
|
||||||
|
progress "Installing AIS-catcher"
|
||||||
|
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
|
||||||
|
(brew_install aiscatcher) || warn "AIS-catcher not available via Homebrew"
|
||||||
|
else
|
||||||
|
ok "AIS-catcher already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing aircrack-ng"
|
progress "Installing aircrack-ng"
|
||||||
brew_install aircrack-ng
|
brew_install aircrack-ng
|
||||||
|
|
||||||
@@ -577,6 +585,34 @@ install_acarsdec_from_source_debian() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_aiscatcher_from_source_debian() {
|
||||||
|
info "AIS-catcher not available via APT. Building from source..."
|
||||||
|
|
||||||
|
apt_install build-essential git cmake pkg-config \
|
||||||
|
librtlsdr-dev libusb-1.0-0-dev libcurl4-openssl-dev zlib1g-dev
|
||||||
|
|
||||||
|
# Run in subshell to isolate EXIT trap
|
||||||
|
(
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT
|
||||||
|
|
||||||
|
info "Cloning AIS-catcher..."
|
||||||
|
git clone --depth 1 https://github.com/jvde-github/AIS-catcher.git "$tmp_dir/AIS-catcher" >/dev/null 2>&1 \
|
||||||
|
|| { warn "Failed to clone AIS-catcher"; exit 1; }
|
||||||
|
|
||||||
|
cd "$tmp_dir/AIS-catcher"
|
||||||
|
mkdir -p build && cd build
|
||||||
|
|
||||||
|
info "Compiling AIS-catcher..."
|
||||||
|
if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then
|
||||||
|
$SUDO install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher
|
||||||
|
ok "AIS-catcher installed successfully."
|
||||||
|
else
|
||||||
|
warn "Failed to build AIS-catcher from source. AIS vessel tracking will not be available."
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
install_rtlsdr_blog_drivers_debian() {
|
install_rtlsdr_blog_drivers_debian() {
|
||||||
# The RTL-SDR Blog drivers provide better support for:
|
# The RTL-SDR Blog drivers provide better support for:
|
||||||
# - RTL-SDR Blog V4 (R828D tuner)
|
# - RTL-SDR Blog V4 (R828D tuner)
|
||||||
@@ -684,7 +720,7 @@ install_debian_packages() {
|
|||||||
export NEEDRESTART_MODE=a
|
export NEEDRESTART_MODE=a
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TOTAL_STEPS=18
|
TOTAL_STEPS=19
|
||||||
CURRENT_STEP=0
|
CURRENT_STEP=0
|
||||||
|
|
||||||
progress "Updating APT package lists"
|
progress "Updating APT package lists"
|
||||||
@@ -815,6 +851,13 @@ install_debian_packages() {
|
|||||||
fi
|
fi
|
||||||
cmd_exists acarsdec || install_acarsdec_from_source_debian
|
cmd_exists acarsdec || install_acarsdec_from_source_debian
|
||||||
|
|
||||||
|
progress "Installing AIS-catcher"
|
||||||
|
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
|
||||||
|
install_aiscatcher_from_source_debian
|
||||||
|
else
|
||||||
|
ok "AIS-catcher already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Configuring udev rules"
|
progress "Configuring udev rules"
|
||||||
setup_udev_rules_debian
|
setup_udev_rules_debian
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -95,6 +95,11 @@
|
|||||||
<span class="mode-name">Aircraft</span>
|
<span class="mode-name">Aircraft</span>
|
||||||
<span class="mode-desc">ADS-B tracking</span>
|
<span class="mode-desc">ADS-B tracking</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/ais/dashboard" class="mode-card" style="text-decoration: none;">
|
||||||
|
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-8l-4-6H6L2 12v8z"/><path d="M12 4v4"/><path d="M8 4v2"/><path d="M16 4v2"/></svg></span>
|
||||||
|
<span class="mode-name">Vessels</span>
|
||||||
|
<span class="mode-desc">AIS ship tracking</span>
|
||||||
|
</a>
|
||||||
<button class="mode-card" onclick="selectMode('wifi')">
|
<button class="mode-card" onclick="selectMode('wifi')">
|
||||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
||||||
<span class="mode-name">WiFi</span>
|
<span class="mode-name">WiFi</span>
|
||||||
@@ -271,6 +276,7 @@
|
|||||||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span><span class="nav-label">433MHz</span></button>
|
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span><span class="nav-label">433MHz</span></button>
|
||||||
<button class="mode-nav-btn" onclick="switchMode('rtlamr')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span><span class="nav-label">Meters</span></button>
|
<button class="mode-nav-btn" onclick="switchMode('rtlamr')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span><span class="nav-label">Meters</span></button>
|
||||||
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span><span class="nav-label">Aircraft</span></a>
|
<a href="/adsb/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span><span class="nav-label">Aircraft</span></a>
|
||||||
|
<a href="/ais/dashboard" class="mode-nav-btn" style="text-decoration: none;"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-8l-4-6H6L2 12v8z"/><path d="M12 4v4"/><path d="M8 4v2"/><path d="M16 4v2"/></svg></span><span class="nav-label">Vessels</span></a>
|
||||||
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span><span class="nav-label">APRS</span></button>
|
<button class="mode-nav-btn" onclick="switchMode('aprs')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span><span class="nav-label">APRS</span></button>
|
||||||
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span><span class="nav-label">Satellite</span></button>
|
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span><span class="nav-label">Satellite</span></button>
|
||||||
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span><span class="nav-label">Listening Post</span></button>
|
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span><span class="nav-label">Listening Post</span></button>
|
||||||
@@ -330,6 +336,7 @@
|
|||||||
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span> 433MHz</button>
|
<button class="mobile-nav-btn" data-mode="sensor" onclick="switchMode('sensor')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span> 433MHz</button>
|
||||||
<button class="mobile-nav-btn" data-mode="rtlamr" onclick="switchMode('rtlamr')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span> Meters</button>
|
<button class="mobile-nav-btn" data-mode="rtlamr" onclick="switchMode('rtlamr')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span> Meters</button>
|
||||||
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span> Aircraft</a>
|
<a href="/adsb/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span> Aircraft</a>
|
||||||
|
<a href="/ais/dashboard" class="mobile-nav-btn" style="text-decoration: none;"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-8l-4-6H6L2 12v8z"/><path d="M12 4v4"/></svg></span> Vessels</a>
|
||||||
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span> APRS</button>
|
<button class="mobile-nav-btn" data-mode="aprs" onclick="switchMode('aprs')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span> APRS</button>
|
||||||
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg></span> WiFi</button>
|
<button class="mobile-nav-btn" data-mode="wifi" onclick="switchMode('wifi')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg></span> WiFi</button>
|
||||||
<button class="mobile-nav-btn" data-mode="bluetooth" onclick="switchMode('bluetooth')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span> BT</button>
|
<button class="mobile-nav-btn" data-mode="bluetooth" onclick="switchMode('bluetooth')"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span> BT</button>
|
||||||
@@ -446,6 +453,8 @@
|
|||||||
|
|
||||||
{% include 'partials/modes/tscm.html' %}
|
{% include 'partials/modes/tscm.html' %}
|
||||||
|
|
||||||
|
{% include 'partials/modes/ais.html' %}
|
||||||
|
|
||||||
<button class="preset-btn" onclick="killAll()"
|
<button class="preset-btn" onclick="killAll()"
|
||||||
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
|
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
|
||||||
Kill All Processes
|
Kill All Processes
|
||||||
@@ -2046,6 +2055,7 @@
|
|||||||
document.getElementById('listeningPostMode')?.classList.toggle('active', mode === 'listening');
|
document.getElementById('listeningPostMode')?.classList.toggle('active', mode === 'listening');
|
||||||
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
||||||
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
||||||
|
document.getElementById('aisMode')?.classList.toggle('active', mode === 'ais');
|
||||||
const pagerStats = document.getElementById('pagerStats');
|
const pagerStats = document.getElementById('pagerStats');
|
||||||
const sensorStats = document.getElementById('sensorStats');
|
const sensorStats = document.getElementById('sensorStats');
|
||||||
const satelliteStats = document.getElementById('satelliteStats');
|
const satelliteStats = document.getElementById('satelliteStats');
|
||||||
@@ -2075,7 +2085,8 @@
|
|||||||
'bluetooth': 'BLUETOOTH',
|
'bluetooth': 'BLUETOOTH',
|
||||||
'listening': 'LISTENING POST',
|
'listening': 'LISTENING POST',
|
||||||
'aprs': 'APRS',
|
'aprs': 'APRS',
|
||||||
'tscm': 'TSCM'
|
'tscm': 'TSCM',
|
||||||
|
'ais': 'AIS VESSELS'
|
||||||
};
|
};
|
||||||
const activeModeIndicator = document.getElementById('activeModeIndicator');
|
const activeModeIndicator = document.getElementById('activeModeIndicator');
|
||||||
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
|
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
|
||||||
@@ -2108,7 +2119,8 @@
|
|||||||
'bluetooth': 'Bluetooth Scanner',
|
'bluetooth': 'Bluetooth Scanner',
|
||||||
'listening': 'Listening Post',
|
'listening': 'Listening Post',
|
||||||
'aprs': 'APRS Tracker',
|
'aprs': 'APRS Tracker',
|
||||||
'tscm': 'TSCM Counter-Surveillance'
|
'tscm': 'TSCM Counter-Surveillance',
|
||||||
|
'ais': 'AIS Vessel Tracker'
|
||||||
};
|
};
|
||||||
const outputTitle = document.getElementById('outputTitle');
|
const outputTitle = document.getElementById('outputTitle');
|
||||||
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
|
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
|
||||||
@@ -10495,6 +10507,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - Opens ADS-B
|
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - Opens ADS-B
|
||||||
Dashboard</span></div>
|
Dashboard</span></div>
|
||||||
|
<div class="icon-item"><span class="icon">🚢</span><span class="desc">Vessels - Opens AIS
|
||||||
|
Dashboard</span></div>
|
||||||
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio
|
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio
|
||||||
tracking</span></div>
|
tracking</span></div>
|
||||||
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellite - Pass
|
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellite - Pass
|
||||||
@@ -10671,6 +10685,7 @@
|
|||||||
<li><strong>Utility Meters:</strong> RTL-SDR, rtl_tcp, rtlamr</li>
|
<li><strong>Utility Meters:</strong> RTL-SDR, rtl_tcp, rtlamr</li>
|
||||||
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
|
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
|
||||||
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
|
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
|
||||||
|
<li><strong>Vessels (AIS):</strong> RTL-SDR, AIS-catcher</li>
|
||||||
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
|
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
|
||||||
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
|
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
|
||||||
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<!-- AIS VESSEL TRACKING MODE -->
|
||||||
|
<div id="aisMode" class="mode-content" style="display: none;">
|
||||||
|
<div class="section">
|
||||||
|
<h3>AIS Vessel Tracking</h3>
|
||||||
|
<div class="info-text" style="margin-bottom: 15px;">
|
||||||
|
Track ships and vessels via AIS (Automatic Identification System) on 161.975 / 162.025 MHz.
|
||||||
|
</div>
|
||||||
|
<a href="/ais/dashboard" target="_blank" class="run-btn" style="display: inline-block; text-decoration: none; text-align: center; margin-bottom: 15px;">
|
||||||
|
Open AIS Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Settings</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gain (dB, 0 = auto)</label>
|
||||||
|
<input type="number" id="aisGainInput" value="40" min="0" max="50" placeholder="0-50">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Status</h3>
|
||||||
|
<div id="aisStatusDisplay" class="info-text">
|
||||||
|
<p>Status: <span id="aisStatusText" style="color: var(--accent-yellow);">Standby</span></p>
|
||||||
|
<p>Vessels: <span id="aisVesselCount">0</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="run-btn" id="startAisBtn" onclick="startAisTracking()">
|
||||||
|
Start AIS Tracking
|
||||||
|
</button>
|
||||||
|
<button class="stop-btn" id="stopAisBtn" onclick="stopAisTracking()" style="display: none;">
|
||||||
|
Stop AIS Tracking
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let aisEventSource = null;
|
||||||
|
let aisVessels = {};
|
||||||
|
|
||||||
|
function startAisTracking() {
|
||||||
|
const gain = document.getElementById('aisGainInput').value || '40';
|
||||||
|
const device = document.getElementById('deviceSelect')?.value || '0';
|
||||||
|
|
||||||
|
fetch('/ais/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ device, gain })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'started' || data.status === 'already_running') {
|
||||||
|
document.getElementById('startAisBtn').style.display = 'none';
|
||||||
|
document.getElementById('stopAisBtn').style.display = 'block';
|
||||||
|
document.getElementById('aisStatusText').textContent = 'Tracking';
|
||||||
|
document.getElementById('aisStatusText').style.color = 'var(--accent-green)';
|
||||||
|
startAisSSE();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Failed to start AIS tracking');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('Error: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAisTracking() {
|
||||||
|
fetch('/ais/stop', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
document.getElementById('startAisBtn').style.display = 'block';
|
||||||
|
document.getElementById('stopAisBtn').style.display = 'none';
|
||||||
|
document.getElementById('aisStatusText').textContent = 'Standby';
|
||||||
|
document.getElementById('aisStatusText').style.color = 'var(--accent-yellow)';
|
||||||
|
document.getElementById('aisVesselCount').textContent = '0';
|
||||||
|
if (aisEventSource) {
|
||||||
|
aisEventSource.close();
|
||||||
|
aisEventSource = null;
|
||||||
|
}
|
||||||
|
aisVessels = {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAisSSE() {
|
||||||
|
if (aisEventSource) aisEventSource.close();
|
||||||
|
|
||||||
|
aisEventSource = new EventSource('/ais/stream');
|
||||||
|
aisEventSource.onmessage = function(e) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'vessel') {
|
||||||
|
aisVessels[data.mmsi] = data;
|
||||||
|
document.getElementById('aisVesselCount').textContent = Object.keys(aisVessels).length;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
aisEventSource.onerror = function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById('stopAisBtn').style.display === 'block') {
|
||||||
|
startAisSSE();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check initial status
|
||||||
|
fetch('/ais/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.tracking_active) {
|
||||||
|
document.getElementById('startAisBtn').style.display = 'none';
|
||||||
|
document.getElementById('stopAisBtn').style.display = 'block';
|
||||||
|
document.getElementById('aisStatusText').textContent = 'Tracking';
|
||||||
|
document.getElementById('aisStatusText').style.color = 'var(--accent-green)';
|
||||||
|
document.getElementById('aisVesselCount').textContent = data.vessel_count || 0;
|
||||||
|
startAisSSE();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
</script>
|
||||||
@@ -203,6 +203,32 @@ SBS_RECONNECT_DELAY = 2.0
|
|||||||
# Default pager log file
|
# Default pager log file
|
||||||
DEFAULT_PAGER_LOG_FILE = 'pager_messages.log'
|
DEFAULT_PAGER_LOG_FILE = 'pager_messages.log'
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# AIS (Vessel Tracking)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# AIS-catcher TCP server port
|
||||||
|
AIS_TCP_PORT = 10110
|
||||||
|
|
||||||
|
# AIS stream update interval
|
||||||
|
AIS_UPDATE_INTERVAL = 0.5
|
||||||
|
|
||||||
|
# AIS reconnect delay on error
|
||||||
|
AIS_RECONNECT_DELAY = 2.0
|
||||||
|
|
||||||
|
# AIS socket timeout
|
||||||
|
AIS_SOCKET_TIMEOUT = 5
|
||||||
|
|
||||||
|
# AIS frequencies (MHz)
|
||||||
|
AIS_FREQUENCIES = [161.975, 162.025]
|
||||||
|
|
||||||
|
# Maximum age for vessel data before cleanup
|
||||||
|
MAX_VESSEL_AGE_SECONDS = 600 # 10 minutes
|
||||||
|
|
||||||
|
# AIS process termination timeout
|
||||||
|
AIS_TERMINATE_TIMEOUT = 5
|
||||||
|
|
||||||
# WiFi capture temp path prefix
|
# WiFi capture temp path prefix
|
||||||
WIFI_CAPTURE_PATH_PREFIX = '/tmp/intercept_wifi'
|
WIFI_CAPTURE_PATH_PREFIX = '/tmp/intercept_wifi'
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,20 @@ TOOL_DEPENDENCIES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'ais': {
|
||||||
|
'name': 'Vessel Tracking (AIS)',
|
||||||
|
'tools': {
|
||||||
|
'AIS-catcher': {
|
||||||
|
'required': True,
|
||||||
|
'description': 'AIS receiver and decoder',
|
||||||
|
'install': {
|
||||||
|
'apt': 'Download .deb from https://github.com/jvde-github/AIS-catcher/releases',
|
||||||
|
'brew': 'brew install aiscatcher',
|
||||||
|
'manual': 'https://github.com/jvde-github/AIS-catcher/releases'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
'aprs': {
|
'aprs': {
|
||||||
'name': 'APRS Tracking',
|
'name': 'APRS Tracking',
|
||||||
'tools': {
|
'tools': {
|
||||||
|
|||||||
@@ -155,6 +155,36 @@ class AirspyCommandBuilder(CommandBuilder):
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
def build_ais_command(
|
||||||
|
self,
|
||||||
|
device: SDRDevice,
|
||||||
|
gain: Optional[float] = None,
|
||||||
|
bias_t: bool = False,
|
||||||
|
tcp_port: int = 10110
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Build AIS-catcher command for AIS vessel tracking with Airspy.
|
||||||
|
|
||||||
|
Uses AIS-catcher with SoapySDR support.
|
||||||
|
"""
|
||||||
|
device_str = self._build_device_string(device)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'AIS-catcher',
|
||||||
|
'-d', f'soapysdr -d {device_str}',
|
||||||
|
'-S', str(tcp_port),
|
||||||
|
'-o', '5',
|
||||||
|
'-q',
|
||||||
|
]
|
||||||
|
|
||||||
|
if gain is not None and gain > 0:
|
||||||
|
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
def get_capabilities(self) -> SDRCapabilities:
|
def get_capabilities(self) -> SDRCapabilities:
|
||||||
"""Return Airspy capabilities."""
|
"""Return Airspy capabilities."""
|
||||||
return self.CAPABILITIES
|
return self.CAPABILITIES
|
||||||
|
|||||||
@@ -159,6 +159,28 @@ class CommandBuilder(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build_ais_command(
|
||||||
|
self,
|
||||||
|
device: SDRDevice,
|
||||||
|
gain: Optional[float] = None,
|
||||||
|
bias_t: bool = False,
|
||||||
|
tcp_port: int = 10110
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Build AIS decoder command for vessel tracking.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device: The SDR device to use
|
||||||
|
gain: Gain in dB (None for auto)
|
||||||
|
bias_t: Enable bias-T power (for active antennas)
|
||||||
|
tcp_port: TCP port for JSON output server
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Command as list of strings for subprocess
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_capabilities(self) -> SDRCapabilities:
|
def get_capabilities(self) -> SDRCapabilities:
|
||||||
"""Return hardware capabilities for this SDR type."""
|
"""Return hardware capabilities for this SDR type."""
|
||||||
|
|||||||
@@ -155,6 +155,36 @@ class HackRFCommandBuilder(CommandBuilder):
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
def build_ais_command(
|
||||||
|
self,
|
||||||
|
device: SDRDevice,
|
||||||
|
gain: Optional[float] = None,
|
||||||
|
bias_t: bool = False,
|
||||||
|
tcp_port: int = 10110
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Build AIS-catcher command for AIS vessel tracking with HackRF.
|
||||||
|
|
||||||
|
Uses AIS-catcher with SoapySDR support.
|
||||||
|
"""
|
||||||
|
device_str = self._build_device_string(device)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'AIS-catcher',
|
||||||
|
'-d', f'soapysdr -d {device_str}',
|
||||||
|
'-S', str(tcp_port),
|
||||||
|
'-o', '5',
|
||||||
|
'-q',
|
||||||
|
]
|
||||||
|
|
||||||
|
if gain is not None and gain > 0:
|
||||||
|
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
def get_capabilities(self) -> SDRCapabilities:
|
def get_capabilities(self) -> SDRCapabilities:
|
||||||
"""Return HackRF capabilities."""
|
"""Return HackRF capabilities."""
|
||||||
return self.CAPABILITIES
|
return self.CAPABILITIES
|
||||||
|
|||||||
@@ -134,6 +134,34 @@ class LimeSDRCommandBuilder(CommandBuilder):
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
def build_ais_command(
|
||||||
|
self,
|
||||||
|
device: SDRDevice,
|
||||||
|
gain: Optional[float] = None,
|
||||||
|
bias_t: bool = False,
|
||||||
|
tcp_port: int = 10110
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Build AIS-catcher command for AIS vessel tracking with LimeSDR.
|
||||||
|
|
||||||
|
Uses AIS-catcher with SoapySDR support.
|
||||||
|
Note: LimeSDR does not support bias-T, parameter is ignored.
|
||||||
|
"""
|
||||||
|
device_str = self._build_device_string(device)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'AIS-catcher',
|
||||||
|
'-d', f'soapysdr -d {device_str}',
|
||||||
|
'-S', str(tcp_port),
|
||||||
|
'-o', '5',
|
||||||
|
'-q',
|
||||||
|
]
|
||||||
|
|
||||||
|
if gain is not None and gain > 0:
|
||||||
|
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
def get_capabilities(self) -> SDRCapabilities:
|
def get_capabilities(self) -> SDRCapabilities:
|
||||||
"""Return LimeSDR capabilities."""
|
"""Return LimeSDR capabilities."""
|
||||||
return self.CAPABILITIES
|
return self.CAPABILITIES
|
||||||
|
|||||||
@@ -157,6 +157,41 @@ class RTLSDRCommandBuilder(CommandBuilder):
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
def build_ais_command(
|
||||||
|
self,
|
||||||
|
device: SDRDevice,
|
||||||
|
gain: Optional[float] = None,
|
||||||
|
bias_t: bool = False,
|
||||||
|
tcp_port: int = 10110
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Build AIS-catcher command for AIS vessel tracking.
|
||||||
|
|
||||||
|
Uses AIS-catcher with TCP JSON output for real-time vessel data.
|
||||||
|
AIS operates on 161.975 MHz and 162.025 MHz (handled automatically).
|
||||||
|
"""
|
||||||
|
if device.is_network:
|
||||||
|
raise ValueError(
|
||||||
|
"AIS-catcher does not support rtl_tcp. "
|
||||||
|
"For remote AIS, run AIS-catcher on the remote machine."
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'AIS-catcher',
|
||||||
|
'-d', str(device.index),
|
||||||
|
'-S', str(tcp_port), # TCP server with JSON output
|
||||||
|
'-o', '5', # JSON output format
|
||||||
|
'-q', # Quiet mode (less console output)
|
||||||
|
]
|
||||||
|
|
||||||
|
if gain is not None and gain > 0:
|
||||||
|
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
def get_capabilities(self) -> SDRCapabilities:
|
def get_capabilities(self) -> SDRCapabilities:
|
||||||
"""Return RTL-SDR capabilities."""
|
"""Return RTL-SDR capabilities."""
|
||||||
return self.CAPABILITIES
|
return self.CAPABILITIES
|
||||||
|
|||||||
@@ -133,6 +133,36 @@ class SDRPlayCommandBuilder(CommandBuilder):
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
def build_ais_command(
|
||||||
|
self,
|
||||||
|
device: SDRDevice,
|
||||||
|
gain: Optional[float] = None,
|
||||||
|
bias_t: bool = False,
|
||||||
|
tcp_port: int = 10110
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
Build AIS-catcher command for AIS vessel tracking with SDRPlay.
|
||||||
|
|
||||||
|
Uses AIS-catcher with SoapySDR support.
|
||||||
|
"""
|
||||||
|
device_str = self._build_device_string(device)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'AIS-catcher',
|
||||||
|
'-d', f'soapysdr -d {device_str}',
|
||||||
|
'-S', str(tcp_port),
|
||||||
|
'-o', '5',
|
||||||
|
'-q',
|
||||||
|
]
|
||||||
|
|
||||||
|
if gain is not None and gain > 0:
|
||||||
|
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
||||||
|
|
||||||
|
if bias_t:
|
||||||
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
def get_capabilities(self) -> SDRCapabilities:
|
def get_capabilities(self) -> SDRCapabilities:
|
||||||
"""Return SDRPlay capabilities."""
|
"""Return SDRPlay capabilities."""
|
||||||
return self.CAPABILITIES
|
return self.CAPABILITIES
|
||||||
|
|||||||
Reference in New Issue
Block a user