diff --git a/routes/adsb.py b/routes/adsb.py index cf984b5..e582df1 100644 --- a/routes/adsb.py +++ b/routes/adsb.py @@ -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') def adsb_session(): """Get ADS-B session status and uptime.""" diff --git a/routes/ais.py b/routes/ais.py index 847ef36..4d6eed2 100644 --- a/routes/ais.py +++ b/routes/ais.py @@ -408,11 +408,24 @@ def start_ais(): bias_t = data.get('bias_t', False) 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( device=sdr_device, gain=float(gain), 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 @@ -535,6 +548,31 @@ def get_vessel_dsc(mmsi: str): 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') def ais_dashboard(): """Popout AIS dashboard.""" diff --git a/templates/partials/modes/ais.html b/templates/partials/modes/ais.html index dacab19..dbc6083 100644 --- a/templates/partials/modes/ais.html +++ b/templates/partials/modes/ais.html @@ -18,6 +18,23 @@ +
+

NMEA UDP Forward

+

+ Forward NMEA 0183 sentences to an external app (e.g. OpenCPN). Leave host blank to disable. +

+
+
+ + +
+
+ + +
+
+
+

Status

@@ -110,11 +127,22 @@ function startAisTracking() { const gain = document.getElementById('aisGainInput').value || '40'; 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', { method: 'POST', 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(data => { diff --git a/utils/sdr/airspy.py b/utils/sdr/airspy.py index 104fef1..a146f60 100644 --- a/utils/sdr/airspy.py +++ b/utils/sdr/airspy.py @@ -161,7 +161,9 @@ class AirspyCommandBuilder(CommandBuilder): device: SDRDevice, gain: float | None = None, bias_t: bool = False, - tcp_port: int = 10110 + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, ) -> list[str]: """ Build AIS-catcher command for AIS vessel tracking with Airspy. @@ -184,6 +186,9 @@ class AirspyCommandBuilder(CommandBuilder): if bias_t: cmd.extend(['-gr', 'biastee', '1']) + if udp_host and udp_port: + cmd.extend(['-u', udp_host, str(udp_port)]) + return cmd def build_iq_capture_command( diff --git a/utils/sdr/base.py b/utils/sdr/base.py index 6fe012c..0ecca03 100644 --- a/utils/sdr/base.py +++ b/utils/sdr/base.py @@ -165,7 +165,9 @@ class CommandBuilder(ABC): device: SDRDevice, gain: float | None = None, bias_t: bool = False, - tcp_port: int = 10110 + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, ) -> list[str]: """ Build AIS decoder command for vessel tracking. @@ -175,6 +177,8 @@ class CommandBuilder(ABC): gain: Gain in dB (None for auto) bias_t: Enable bias-T power (for active antennas) 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: Command as list of strings for subprocess diff --git a/utils/sdr/hackrf.py b/utils/sdr/hackrf.py index 9db3e74..1274ada 100644 --- a/utils/sdr/hackrf.py +++ b/utils/sdr/hackrf.py @@ -161,7 +161,9 @@ class HackRFCommandBuilder(CommandBuilder): device: SDRDevice, gain: float | None = None, bias_t: bool = False, - tcp_port: int = 10110 + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, ) -> list[str]: """ Build AIS-catcher command for AIS vessel tracking with HackRF. @@ -184,6 +186,9 @@ class HackRFCommandBuilder(CommandBuilder): if bias_t: cmd.extend(['-gr', 'biastee', '1']) + if udp_host and udp_port: + cmd.extend(['-u', udp_host, str(udp_port)]) + return cmd def build_iq_capture_command( diff --git a/utils/sdr/limesdr.py b/utils/sdr/limesdr.py index 5c6c1b8..7b904a6 100644 --- a/utils/sdr/limesdr.py +++ b/utils/sdr/limesdr.py @@ -140,7 +140,9 @@ class LimeSDRCommandBuilder(CommandBuilder): device: SDRDevice, gain: float | None = None, bias_t: bool = False, - tcp_port: int = 10110 + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, ) -> list[str]: """ 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: cmd.extend(['-gr', 'tuner', str(int(gain))]) + if udp_host and udp_port: + cmd.extend(['-u', udp_host, str(udp_port)]) + return cmd def build_iq_capture_command( diff --git a/utils/sdr/rtlsdr.py b/utils/sdr/rtlsdr.py index 80b50e5..9437338 100644 --- a/utils/sdr/rtlsdr.py +++ b/utils/sdr/rtlsdr.py @@ -281,7 +281,9 @@ class RTLSDRCommandBuilder(CommandBuilder): device: SDRDevice, gain: float | None = None, bias_t: bool = False, - tcp_port: int = 10110 + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, ) -> list[str]: """ Build AIS-catcher command for AIS vessel tracking. @@ -308,6 +310,9 @@ class RTLSDRCommandBuilder(CommandBuilder): if bias_t: cmd.extend(['-gr', 'BIASTEE', 'on']) + if udp_host and udp_port: + cmd.extend(['-u', udp_host, str(udp_port)]) + return cmd def build_iq_capture_command( diff --git a/utils/sdr/sdrplay.py b/utils/sdr/sdrplay.py index 1b2c335..6b7293d 100644 --- a/utils/sdr/sdrplay.py +++ b/utils/sdr/sdrplay.py @@ -139,7 +139,9 @@ class SDRPlayCommandBuilder(CommandBuilder): device: SDRDevice, gain: float | None = None, bias_t: bool = False, - tcp_port: int = 10110 + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, ) -> list[str]: """ Build AIS-catcher command for AIS vessel tracking with SDRPlay. @@ -162,6 +164,9 @@ class SDRPlayCommandBuilder(CommandBuilder): if bias_t: cmd.extend(['-gr', 'biastee', '1']) + if udp_host and udp_port: + cmd.extend(['-u', udp_host, str(udp_port)]) + return cmd def build_iq_capture_command(