mirror of
https://github.com/smittix/intercept.git
synced 2026-07-05 16:18:12 -07:00
feat(export): AIS UDP NMEA forward and JSON export endpoints for AIS/ADS-B
AIS: - New optional NMEA UDP forwarding via AIS-catcher's -u flag, configurable from the AIS sidebar (host + port). Lets OpenCPN and other NMEA tools receive live vessel data directly. All SDR builders updated. - New GET /ais/vessels endpoint — clean JSON snapshot of tracked vessels for REST integration ADS-B: - New GET /adsb/aircraft endpoint — JSON snapshot of all tracked aircraft, with optional ?icao= and ?military=true filters. Response includes a reminder that port 30003 (SBS) is already available for tools like Virtual Radar Server and OpenCPN's AIS/target plugin. Closes #90
This commit is contained in:
@@ -803,6 +803,41 @@ def adsb_status():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@adsb_bp.route('/aircraft')
|
||||||
|
def adsb_aircraft_export():
|
||||||
|
"""Export current ADS-B aircraft data as JSON.
|
||||||
|
|
||||||
|
Returns a snapshot of all tracked aircraft suitable for integration
|
||||||
|
with external tools. For SBS (BaseStation) format, connect directly
|
||||||
|
to port 30003 which dump1090 exposes natively.
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
icao: Filter to a specific ICAO hex code (optional)
|
||||||
|
military: 'true' to return only military aircraft (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with aircraft list and metadata.
|
||||||
|
"""
|
||||||
|
aircraft = dict(app_module.adsb_aircraft)
|
||||||
|
|
||||||
|
icao_filter = request.args.get('icao', '').upper()
|
||||||
|
if icao_filter:
|
||||||
|
aircraft = {k: v for k, v in aircraft.items() if k.upper() == icao_filter}
|
||||||
|
|
||||||
|
if request.args.get('military') == 'true':
|
||||||
|
try:
|
||||||
|
from utils.military_icao import is_military_icao
|
||||||
|
aircraft = {k: v for k, v in aircraft.items() if is_military_icao(k)}
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'count': len(aircraft),
|
||||||
|
'aircraft': list(aircraft.values()),
|
||||||
|
'sbs_port': 30003, # dump1090 SBS stream for tools like Virtual Radar Server
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@adsb_bp.route('/session')
|
@adsb_bp.route('/session')
|
||||||
def adsb_session():
|
def adsb_session():
|
||||||
"""Get ADS-B session status and uptime."""
|
"""Get ADS-B session status and uptime."""
|
||||||
|
|||||||
+39
-1
@@ -408,11 +408,24 @@ def start_ais():
|
|||||||
bias_t = data.get('bias_t', False)
|
bias_t = data.get('bias_t', False)
|
||||||
tcp_port = AIS_TCP_PORT
|
tcp_port = AIS_TCP_PORT
|
||||||
|
|
||||||
|
# Optional UDP NMEA forwarding (e.g. for OpenCPN on port 10110)
|
||||||
|
udp_host = data.get('udp_host') or None
|
||||||
|
udp_port = None
|
||||||
|
if udp_host:
|
||||||
|
try:
|
||||||
|
udp_port = int(data.get('udp_port', 10110))
|
||||||
|
if not 1 <= udp_port <= 65535:
|
||||||
|
raise ValueError
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return api_error('Invalid udp_port (1-65535)', 400)
|
||||||
|
|
||||||
cmd = builder.build_ais_command(
|
cmd = builder.build_ais_command(
|
||||||
device=sdr_device,
|
device=sdr_device,
|
||||||
gain=float(gain),
|
gain=float(gain),
|
||||||
bias_t=bias_t,
|
bias_t=bias_t,
|
||||||
tcp_port=tcp_port
|
tcp_port=tcp_port,
|
||||||
|
udp_host=udp_host,
|
||||||
|
udp_port=udp_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use the found AIS-catcher path
|
# Use the found AIS-catcher path
|
||||||
@@ -535,6 +548,31 @@ def get_vessel_dsc(mmsi: str):
|
|||||||
return api_success(data={'mmsi': mmsi, 'dsc_messages': matches})
|
return api_success(data={'mmsi': mmsi, 'dsc_messages': matches})
|
||||||
|
|
||||||
|
|
||||||
|
@ais_bp.route('/vessels')
|
||||||
|
def ais_vessels():
|
||||||
|
"""Export current AIS vessel data as JSON.
|
||||||
|
|
||||||
|
Returns a snapshot of all tracked vessels suitable for integration
|
||||||
|
with external tools (OpenCPN, ship tracking apps, etc.).
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
mmsi: Filter to a specific MMSI (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with vessel list and metadata.
|
||||||
|
"""
|
||||||
|
vessels = dict(app_module.ais_vessels)
|
||||||
|
|
||||||
|
mmsi_filter = request.args.get('mmsi')
|
||||||
|
if mmsi_filter:
|
||||||
|
vessels = {k: v for k, v in vessels.items() if str(k) == str(mmsi_filter)}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'count': len(vessels),
|
||||||
|
'vessels': list(vessels.values()),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@ais_bp.route('/dashboard')
|
@ais_bp.route('/dashboard')
|
||||||
def ais_dashboard():
|
def ais_dashboard():
|
||||||
"""Popout AIS dashboard."""
|
"""Popout AIS dashboard."""
|
||||||
|
|||||||
@@ -18,6 +18,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>NMEA UDP Forward</h3>
|
||||||
|
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 8px;">
|
||||||
|
Forward NMEA 0183 sentences to an external app (e.g. OpenCPN). Leave host blank to disable.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<div style="flex: 2;">
|
||||||
|
<label style="font-size: 10px; color: var(--text-dim);">Host</label>
|
||||||
|
<input type="text" id="aisUdpHost" placeholder="e.g. 192.168.1.10" style="width: 100%;">
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<label style="font-size: 10px; color: var(--text-dim);">Port</label>
|
||||||
|
<input type="number" id="aisUdpPort" value="10110" min="1" max="65535" style="width: 100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Status</h3>
|
<h3>Status</h3>
|
||||||
<div id="aisStatusDisplay" class="info-text">
|
<div id="aisStatusDisplay" class="info-text">
|
||||||
@@ -110,11 +127,22 @@
|
|||||||
function startAisTracking() {
|
function startAisTracking() {
|
||||||
const gain = document.getElementById('aisGainInput').value || '40';
|
const gain = document.getElementById('aisGainInput').value || '40';
|
||||||
const device = document.getElementById('deviceSelect')?.value || '0';
|
const device = document.getElementById('deviceSelect')?.value || '0';
|
||||||
|
const udpHost = document.getElementById('aisUdpHost').value.trim();
|
||||||
|
const udpPort = parseInt(document.getElementById('aisUdpPort').value) || 10110;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
device, gain,
|
||||||
|
bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false,
|
||||||
|
};
|
||||||
|
if (udpHost) {
|
||||||
|
body.udp_host = udpHost;
|
||||||
|
body.udp_port = udpPort;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/ais/start', {
|
fetch('/ais/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ device, gain, bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false })
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|||||||
+6
-1
@@ -161,7 +161,9 @@ class AirspyCommandBuilder(CommandBuilder):
|
|||||||
device: SDRDevice,
|
device: SDRDevice,
|
||||||
gain: float | None = None,
|
gain: float | None = None,
|
||||||
bias_t: bool = False,
|
bias_t: bool = False,
|
||||||
tcp_port: int = 10110
|
tcp_port: int = 10110,
|
||||||
|
udp_host: str | None = None,
|
||||||
|
udp_port: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build AIS-catcher command for AIS vessel tracking with Airspy.
|
Build AIS-catcher command for AIS vessel tracking with Airspy.
|
||||||
@@ -184,6 +186,9 @@ class AirspyCommandBuilder(CommandBuilder):
|
|||||||
if bias_t:
|
if bias_t:
|
||||||
cmd.extend(['-gr', 'biastee', '1'])
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
if udp_host and udp_port:
|
||||||
|
cmd.extend(['-u', udp_host, str(udp_port)])
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def build_iq_capture_command(
|
def build_iq_capture_command(
|
||||||
|
|||||||
+5
-1
@@ -165,7 +165,9 @@ class CommandBuilder(ABC):
|
|||||||
device: SDRDevice,
|
device: SDRDevice,
|
||||||
gain: float | None = None,
|
gain: float | None = None,
|
||||||
bias_t: bool = False,
|
bias_t: bool = False,
|
||||||
tcp_port: int = 10110
|
tcp_port: int = 10110,
|
||||||
|
udp_host: str | None = None,
|
||||||
|
udp_port: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build AIS decoder command for vessel tracking.
|
Build AIS decoder command for vessel tracking.
|
||||||
@@ -175,6 +177,8 @@ class CommandBuilder(ABC):
|
|||||||
gain: Gain in dB (None for auto)
|
gain: Gain in dB (None for auto)
|
||||||
bias_t: Enable bias-T power (for active antennas)
|
bias_t: Enable bias-T power (for active antennas)
|
||||||
tcp_port: TCP port for JSON output server
|
tcp_port: TCP port for JSON output server
|
||||||
|
udp_host: Optional host to forward NMEA 0183 sentences via UDP
|
||||||
|
udp_port: UDP port for NMEA forwarding (required if udp_host set)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Command as list of strings for subprocess
|
Command as list of strings for subprocess
|
||||||
|
|||||||
+6
-1
@@ -161,7 +161,9 @@ class HackRFCommandBuilder(CommandBuilder):
|
|||||||
device: SDRDevice,
|
device: SDRDevice,
|
||||||
gain: float | None = None,
|
gain: float | None = None,
|
||||||
bias_t: bool = False,
|
bias_t: bool = False,
|
||||||
tcp_port: int = 10110
|
tcp_port: int = 10110,
|
||||||
|
udp_host: str | None = None,
|
||||||
|
udp_port: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build AIS-catcher command for AIS vessel tracking with HackRF.
|
Build AIS-catcher command for AIS vessel tracking with HackRF.
|
||||||
@@ -184,6 +186,9 @@ class HackRFCommandBuilder(CommandBuilder):
|
|||||||
if bias_t:
|
if bias_t:
|
||||||
cmd.extend(['-gr', 'biastee', '1'])
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
if udp_host and udp_port:
|
||||||
|
cmd.extend(['-u', udp_host, str(udp_port)])
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def build_iq_capture_command(
|
def build_iq_capture_command(
|
||||||
|
|||||||
@@ -140,7 +140,9 @@ class LimeSDRCommandBuilder(CommandBuilder):
|
|||||||
device: SDRDevice,
|
device: SDRDevice,
|
||||||
gain: float | None = None,
|
gain: float | None = None,
|
||||||
bias_t: bool = False,
|
bias_t: bool = False,
|
||||||
tcp_port: int = 10110
|
tcp_port: int = 10110,
|
||||||
|
udp_host: str | None = None,
|
||||||
|
udp_port: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build AIS-catcher command for AIS vessel tracking with LimeSDR.
|
Build AIS-catcher command for AIS vessel tracking with LimeSDR.
|
||||||
@@ -161,6 +163,9 @@ class LimeSDRCommandBuilder(CommandBuilder):
|
|||||||
if gain is not None and gain > 0:
|
if gain is not None and gain > 0:
|
||||||
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
cmd.extend(['-gr', 'tuner', str(int(gain))])
|
||||||
|
|
||||||
|
if udp_host and udp_port:
|
||||||
|
cmd.extend(['-u', udp_host, str(udp_port)])
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def build_iq_capture_command(
|
def build_iq_capture_command(
|
||||||
|
|||||||
+6
-1
@@ -281,7 +281,9 @@ class RTLSDRCommandBuilder(CommandBuilder):
|
|||||||
device: SDRDevice,
|
device: SDRDevice,
|
||||||
gain: float | None = None,
|
gain: float | None = None,
|
||||||
bias_t: bool = False,
|
bias_t: bool = False,
|
||||||
tcp_port: int = 10110
|
tcp_port: int = 10110,
|
||||||
|
udp_host: str | None = None,
|
||||||
|
udp_port: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build AIS-catcher command for AIS vessel tracking.
|
Build AIS-catcher command for AIS vessel tracking.
|
||||||
@@ -308,6 +310,9 @@ class RTLSDRCommandBuilder(CommandBuilder):
|
|||||||
if bias_t:
|
if bias_t:
|
||||||
cmd.extend(['-gr', 'BIASTEE', 'on'])
|
cmd.extend(['-gr', 'BIASTEE', 'on'])
|
||||||
|
|
||||||
|
if udp_host and udp_port:
|
||||||
|
cmd.extend(['-u', udp_host, str(udp_port)])
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def build_iq_capture_command(
|
def build_iq_capture_command(
|
||||||
|
|||||||
@@ -139,7 +139,9 @@ class SDRPlayCommandBuilder(CommandBuilder):
|
|||||||
device: SDRDevice,
|
device: SDRDevice,
|
||||||
gain: float | None = None,
|
gain: float | None = None,
|
||||||
bias_t: bool = False,
|
bias_t: bool = False,
|
||||||
tcp_port: int = 10110
|
tcp_port: int = 10110,
|
||||||
|
udp_host: str | None = None,
|
||||||
|
udp_port: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Build AIS-catcher command for AIS vessel tracking with SDRPlay.
|
Build AIS-catcher command for AIS vessel tracking with SDRPlay.
|
||||||
@@ -162,6 +164,9 @@ class SDRPlayCommandBuilder(CommandBuilder):
|
|||||||
if bias_t:
|
if bias_t:
|
||||||
cmd.extend(['-gr', 'biastee', '1'])
|
cmd.extend(['-gr', 'biastee', '1'])
|
||||||
|
|
||||||
|
if udp_host and udp_port:
|
||||||
|
cmd.extend(['-u', udp_host, str(udp_port)])
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def build_iq_capture_command(
|
def build_iq_capture_command(
|
||||||
|
|||||||
Reference in New Issue
Block a user