Add multi-airline ACARS summarizer for readable datalink dossiers.

Parse common position, OOOI, performance, and ops formats across major carriers while hiding VDL binary fragments and duplicate frames from the dossier feed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-24 22:52:00 -06:00
parent 9ad0a5ffce
commit 8c4619179e
4 changed files with 819 additions and 9 deletions
@@ -0,0 +1,570 @@
"""Heuristics to summarize ACARS/VDL payloads across airlines for dossier display."""
from __future__ import annotations
import re
from typing import Any
# --- shared patterns ---
_ICAO_AIRPORT = re.compile(r"\b([A-Z]{4})\b")
_TAIL = re.compile(r"\b(?:N[0-9A-Z]{3,6}|G-[A-Z]{4,5}|[A-Z]-[A-Z]{4,5}|[A-Z]{2}-[A-Z]{3,4})\b")
# Major carriers — explicit list avoids matching FL280, GS450, etc.
_FLIGHT = re.compile(
r"\b(?:"
r"WN|SWA|UA|UAL|AA|AAL|DL|DAL|AS|ASA|B6|JBU|NK|NKS|F9|FFT|G4|HA|HAL|SY|MX|"
r"FDX|UPS|GTI|ABX|ATN|RCH|CNV|EVAC|SAM|REACH|"
r"BA|BAW|AF|AFR|LH|DLH|KL|KLM|QF|QFA|EK|UAE|QR|QTR|TK|THY|AC|ACA|WS|WJA|"
r"FR|RYR|U2|EZY|VS|VIR|NH|ANA|JL|JAL|CX|CPA|SQ|SIA|NZ|ANZ|"
r"UA|CO|NW|US|HP|TW|VX|AS|QX|OO|YX|MQ|OH|9E|"
r"JT|JSA|VA|VOZ|NZ|QF|EK|ET|MS|SU|LO|SK|AY|IB|UX|TP|TAP"
r")\d{1,5}\b",
re.I,
)
# IATA flight numbers on FI lines and standalone (e.g. UO614, CX889).
_FI_FLIGHT = re.compile(r"\b([A-Z]{2,3}\d{1,4})\b")
_NON_FLIGHT_TOKENS = frozenset(
{"FL", "FT", "GS", "KT", "RW", "NM", "TD", "TO", "ON", "IN", "OF", "AT", "DA", "AA", "AD"}
)
_FI_BLOCK = re.compile(
r"FI\s+([A-Z0-9]{2,5}\d{1,5})"
r"(?:/AN\s+([A-Z0-9\-]+))?"
r"(?:/DA\s+([A-Z]{4}))?"
r"(?:/(?:AA|AD|DS)\s+([A-Z]{4}))?",
re.I,
)
_AC_TYPE = re.compile(
r"\b(?:B\d{3,4}(?:-\d{3}|MAX|ER|LR|F)?|A\d{3,4}(?:-\d{3}|NEO|LR)?|"
r"E\d{3}|MD-\d{2}|DC-\d{2}|B77[0-9LWR]?|B78[79]|A35[09]|A33[0-9]|CRJ\d{2,3}|E\d{3})\b",
re.I,
)
# --- message family patterns ---
_TRACK_HEADER = re.compile(
r"^\+\+86501,([^,]+),([^,]+),(\d{6}),([^,]+),([A-Z]{4}),([A-Z]{4})",
re.I,
)
_POS_HEADER = re.compile(r"^POS(N?\d{4,5}[NS]?\d{4,5}[EW]?)", re.I)
_POS_COORDS = re.compile(r"^N?(\d{4,5})([NS])(\d{4,6})([EW])", re.I)
_WAYPOINT = re.compile(
r"^(?:N)?(\d{1,2}\d{2}\.\d),W(\d{1,3}\d{2}\.\d),(\d{6}),(\d+),",
re.I,
)
_PERF_HEADER = re.compile(
r"^[\w]+,(\d+),([^,]+),(\d{6}),([^,]+),([A-Z]{4}),([A-Z]{4})",
re.I,
)
_PHASE_SNAPSHOT = re.compile(
r"^(\d{2}\.\d{2}\.\d{2}),(CL|CR|DE|TO|LD|ER|GND),[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,"
r"N(\d+)\.(\d+),W(\d+)\.(\d+)",
re.I,
)
_TRAJECTORY_HEADER = re.compile(r"^76401\s*$", re.I)
_TRAJECTORY_ROUTE = re.compile(r"^02E24([A-Z]{4})([A-Z]{4})\s*$", re.I)
_COMPRESSED_WP = re.compile(r"^N(\d{5})W(\d{5})", re.I)
_FPN = re.compile(r"^FPN/?", re.I)
_OOOI_TIMES = re.compile(r"\b(OUT|OFF|ON|IN)\s*(\d{4,6})\b", re.I)
_OOOI_STATUS = re.compile(r"\b(OUT|OFF|ON|IN)\s*,\s*(LO|CL|ON|OF|CLOS)\b", re.I)
_ETA = re.compile(r"\bETA\s+(\d{3,4}Z?)\b", re.I)
_DEP_ARR = re.compile(r"^(DEP|ARR|DLA|ALR)\b", re.I)
_WX = re.compile(r"^(?:WXR?\d*|WX\s|MET\b|/WX\b)", re.I)
_REQ = re.compile(r"^(?:REQ|REQUEST)\b", re.I)
_LDR = re.compile(r"^LDR\d+", re.I)
_PIREP = re.compile(r"^#(?:CFB|DFB)", re.I)
_ATN = re.compile(r"^USADCXA\.AT1\.", re.I)
_CPDLC = re.compile(r"^(?:DM-|UM-|AT1\.|ATC\s)", re.I)
_ENG = re.compile(r"^(?:ENG\d|/ENG|OILTEMP|EGT\b)", re.I)
_DOOR = re.compile(r"^(?:DOOR|CABIN|SMOKE)\b", re.I)
_VDL_FRAME = re.compile(r"^[0-9A-F]{6,8}[A-Z]?\s*$", re.I)
_FRAGMENT = re.compile(r"^[,0\s]+(?:,\d{5,8},\d{5,8},\d{5,8})*$", re.I)
_GARBLED_VDL = re.compile(r"[)Z][A-Z0-9,\-:]{20,}")
_MOSTLY_OPAQUE = re.compile(r"^[0-9A-Fa-f\s.\-+/,]{40,}$")
_FREE_TEXT_POS = re.compile(
r"^POS\s+N?(\d{1,2}\.\d+)\s+([NS])\s+W?(\d{1,3}\.\d+)\s+([EW])\s+FL(\d{3})",
re.I,
)
_CLIMB_REQ = re.compile(r"\b(?:CLIMB|DESCEND|REQUEST)\s+(?:FL)?(\d{2,3})\b", re.I)
_LABEL_HINTS: dict[str, str] = {
"00": "out (gate)",
"01": "off (takeoff)",
"02": "on (landing)",
"03": "in (gate)",
"10": "position",
"15": "waypoint",
"20": "position",
"40": "ops / clearance",
"44": "OOOI + position",
"80": "weather",
"81": "wind",
"B1": "engine 1",
"B2": "engine 2",
"B3": "engine 3",
"B4": "engine 4",
"M1": "maintenance",
"M2": "maintenance",
"M3": "maintenance",
"M4": "maintenance",
"Q0": "position / OOOI",
"H1": "terminal",
"D0": "ATC clearance",
"S1": "system status",
"SA": "system status",
"SB": "system status",
"4T": "met report",
"5Z": "free text",
}
def _result(
summary: str,
*,
kind: str,
readable: bool = True,
hidden: bool = False,
) -> dict[str, Any]:
return {
"summary": summary,
"kind": kind,
"readable": readable,
"hidden": hidden,
}
def _phase_name(code: str) -> str:
return {
"CL": "climb",
"CR": "cruise",
"DE": "descent",
"ER": "en route",
"TO": "takeoff",
"LD": "landed",
"ON": "on ground",
"OF": "off block",
"GND": "on ground",
"LO": "level",
}.get(code.upper(), code.upper() or "unknown")
def _fmt_coords(lat_deg: str, lat_frac: str, lon_deg: str, lon_frac: str) -> str:
return f"{int(lat_deg)}°{lat_frac}'N {int(lon_deg)}°{lon_frac}'W"
def _parse_pos_coords(token: str) -> str | None:
token = token.upper().lstrip("POS")
match = _POS_COORDS.match(token)
if not match:
return None
lat, lat_dir, lon, lon_dir = match.groups()
lat_v = f"{int(lat[:2])}°{lat[2:]}.{lat[4:] if len(lat) > 4 else '0'}'{lat_dir}"
lon_v = f"{int(lon[:3])}°{lon[3:]}.{lon[5:] if len(lon) > 5 else '0'}'{lon_dir}"
return f"{lat_v} {lon_v}"
def _extract_route(raw: str) -> str:
fi = _FI_BLOCK.search(raw)
if fi:
flight, _tail, dep, dest = fi.groups()
parts = [flight.upper()]
if dep and dest:
parts.append(f"{dep}{dest}")
elif dep:
parts.append(f"from {dep}")
elif dest:
parts.append(f"to {dest}")
return " · ".join(parts)
airports = _ICAO_AIRPORT.findall(raw)
# Filter duplicates while preserving order
seen: set[str] = set()
ordered: list[str] = []
for apt in airports:
if apt in seen:
continue
seen.add(apt)
ordered.append(apt)
if len(ordered) >= 2:
return f"{ordered[0]}{ordered[-1]}"
if ordered:
return ordered[0]
return ""
def _extract_flight(raw: str) -> str:
fi = _FI_BLOCK.search(raw)
if fi and fi.group(1):
return fi.group(1).upper()
for match in _FLIGHT.finditer(raw):
return match.group(0).upper()
for match in _FI_FLIGHT.finditer(raw):
token = match.group(1).upper()
prefix = re.match(r"^([A-Z]+)", token)
if prefix and prefix.group(1) not in _NON_FLIGHT_TOKENS:
return token
return ""
def _extract_phase_snapshot(raw: str) -> str | None:
for line in raw.splitlines():
match = _PHASE_SNAPSHOT.match(line.strip())
if not match:
continue
time_s, phase, lat_d, lat_f, lon_d, lon_f = match.groups()
coords = _fmt_coords(lat_d, lat_f, lon_d, lon_f)
return f"{_phase_name(phase)} · {coords} · {time_s}Z"
return None
def _has_aircraft_context(raw: str) -> bool:
head = raw[:160].upper()
if _AC_TYPE.search(head):
return True
if _FLIGHT.search(head):
return True
if _FI_BLOCK.search(head):
return True
return False
def _is_fragment(raw: str) -> bool:
first = raw.splitlines()[0].strip()
if _FRAGMENT.match(first):
return True
if re.match(r"^[,0]{1,12}$", first):
return True
if first.startswith("000000") or first.startswith(",000000"):
return True
if re.match(r"^\d{2}\.\d{2}\.\d{2},", first) and not _has_aircraft_context(raw):
return True
return False
def _summarize_oooi(raw: str, label: str) -> dict[str, Any] | None:
times = _OOOI_TIMES.findall(raw)
statuses = _OOOI_STATUS.findall(raw)
if not times and not statuses and label not in {"00", "01", "02", "03", "44", "Q0"}:
return None
events: list[str] = []
for event, value in times:
events.append(f"{event.upper()} {value}")
for event, status in statuses:
events.append(f"{event.upper()} ({_phase_name(status)})")
if label in {"00", "01", "02", "03"} and not events:
events.append(_LABEL_HINTS[label])
if not events and "ON ,LO" not in raw and "OFF,OFF" not in raw:
return None
route = _extract_route(raw)
flight = _extract_flight(raw)
prefix = "OOOI"
if label in _LABEL_HINTS:
prefix = f"OOOI ({_LABEL_HINTS[label]})"
bits = [prefix]
if flight:
bits.append(flight)
if route:
bits.append(route)
if events:
bits.append(", ".join(events[:4]))
return _result(" · ".join(bits), kind="oooi")
def _summarize_position(raw: str, first_line: str) -> dict[str, Any] | None:
upper = first_line.upper()
pos_token = re.search(r"POSN?\d", raw, re.I)
if not (upper.startswith("POS") or _POS_HEADER.match(first_line) or pos_token):
return None
coord_line = first_line
if pos_token and not upper.startswith("POS"):
coord_line = raw[pos_token.start() :].split(",")[0]
coords = _parse_pos_coords(coord_line)
free = _FREE_TEXT_POS.match(raw)
fl = ""
if free:
lat, lat_dir, lon, lon_dir, fl = free.groups()
coords = f"{lat}°{lat_dir} {lon}°{lon_dir}"
fl = f"FL{fl}"
route = _extract_route(raw)
flight = _extract_flight(raw)
parts = ["Position report"]
if flight:
parts.append(flight)
if route:
parts.append(route)
if coords:
parts.append(coords)
if fl:
parts.append(fl)
elif re.search(r"\bFL?\d{3}\b", raw):
fl_match = re.search(r"\bFL?(\d{2,3})\b", raw)
if fl_match:
parts.append(f"FL{fl_match.group(1)}")
return _result(" · ".join(parts), kind="position")
def _summarize_performance(raw: str, first_line: str) -> dict[str, Any] | None:
match = _PERF_HEADER.match(first_line)
if not match or not _AC_TYPE.search(first_line):
return None
_serial, ac_type, _date, flight, dep, dest = match.groups()
phase_bits = _extract_phase_snapshot(raw) or ""
extra = f" · {phase_bits}" if phase_bits else ""
if "FHP" in raw or "SIN," in raw or "SOU," in raw:
title, kind = "Engine health (FHP)", "engine_health"
elif "OATTO" in raw or "LPACKCL" in raw or "RPACKCL" in raw:
title, kind = "Pack temperature", "pack_temp"
elif "FLAPS" in raw.upper():
title, kind = "Climb performance", "climb_perf"
elif "FRE," in raw or "FEX," in raw:
title, kind = "Fuel/performance snapshot", "fuel_perf"
else:
title, kind = "Flight performance", "performance"
return _result(
f"{title} · {flight} · {ac_type} · {dep}{dest}{extra}",
kind=kind,
)
def _summarize_by_label(label: str, raw: str, first_line: str) -> dict[str, Any] | None:
label_u = label.upper()
hint = _LABEL_HINTS.get(label_u, "")
if label_u in {"B1", "B2", "B3", "B4"} or _ENG.match(first_line):
eng = label_u if label_u.startswith("B") else "Engine"
return _result(f"{eng} data report", kind="engine", readable=bool(hint))
if label_u.startswith("M") and label_u[1:2].isdigit():
return _result(f"Maintenance ({hint or 'system report'})", kind="maintenance")
if label_u in {"80", "81", "4T"} or _WX.match(first_line):
apt = _ICAO_AIRPORT.search(raw)
apt_s = f" · {apt.group(1)}" if apt else ""
return _result(f"Weather report{apt_s}", kind="weather")
if label_u == "D0" or _REQ.match(first_line) or _CLIMB_REQ.search(raw):
climb = _CLIMB_REQ.search(raw)
if climb:
return _result(f"Altitude request · FL{climb.group(1)}", kind="request")
return _result("ATC / ops request", kind="request")
if label_u in {"40", "5Z"} and len(raw) < 200:
text = raw.replace("\n", " · ")[:140]
return _result(f"Ops message · {text}", kind="ops")
return None
def summarize_datalink_message(
*,
label: str = "",
text: str = "",
source_type: str = "",
) -> dict[str, Any]:
"""Return {summary, kind, readable, hidden} for a cached datalink message."""
raw = (text or "").strip()
if not raw:
return _result("", kind="empty", readable=False, hidden=True)
first_line = raw.splitlines()[0].strip()
upper = first_line.upper()
label_u = label.upper()
if _is_fragment(raw):
return _result(
"Split telemetry fragment (part of a longer VDL message)",
kind="fragment",
readable=False,
hidden=True,
)
if _ATN.match(first_line) or _CPDLC.match(first_line):
tail = _TAIL.search(raw)
return _result(
"Datalink protocol / CPDLC header" + (f" · {tail.group(0)}" if tail else ""),
kind="protocol",
readable=False,
hidden=True,
)
if label_u == "37" or (_VDL_FRAME.match(first_line) and len(raw) < 160):
if _GARBLED_VDL.search(raw) or len(raw) < 160:
return _result("VDL binary frame (undecoded)", kind="vdl_binary", readable=False, hidden=True)
# --- structured families (order matters) ---
if _DEP_ARR.match(first_line):
kind_word = first_line.split()[0].upper()
route = _extract_route(raw)
flight = _extract_flight(raw)
title = {"DEP": "Departure", "ARR": "Arrival", "DLA": "Delay", "ALR": "Alert"}.get(
kind_word, kind_word
)
bits = [title]
if flight:
bits.append(flight)
if route:
bits.append(route)
return _result(" · ".join(bits), kind=kind_word.lower())
oooi = _summarize_oooi(raw, label_u)
if oooi:
return oooi
match = _TRACK_HEADER.match(first_line)
if match:
tail, ac_type, _date, flight, dep, dest = match.groups()
lines = [line.strip() for line in raw.splitlines() if line.strip()]
waypoint_lines = [line for line in lines if _WAYPOINT.match(line.lstrip("N"))]
phase = ""
if waypoint_lines:
parts = waypoint_lines[-1].rstrip(",").split(",")
if len(parts) >= 8:
phase = _phase_name(parts[7])
wp_count = len(waypoint_lines) or max(0, len(lines) - 2)
summary = (
f"Track report · {flight} · {tail} ({ac_type}) · {dep}{dest}"
+ (f" · {wp_count} waypoint(s)" + (f" · {phase}" if phase else ""))
)
return _result(summary, kind="track")
pos = _summarize_position(raw, first_line)
if pos:
return pos
if _FPN.match(first_line):
route = _extract_route(raw)
flight = _extract_flight(raw)
bits = ["Flight plan"]
if flight:
bits.append(flight)
if route:
bits.append(route)
return _result(" · ".join(bits), kind="flight_plan")
if _PIREP.match(first_line):
return _result("Pilot report (PIREP)", kind="pirep")
lines = [line.strip() for line in raw.splitlines() if line.strip()]
if _TRAJECTORY_HEADER.match(first_line) or (len(lines) >= 2 and _TRAJECTORY_ROUTE.match(lines[1])):
route = ""
route_match = next((m for line in lines if (m := _TRAJECTORY_ROUTE.match(line))), None)
if route_match:
route = f" · {route_match.group(1)}{route_match.group(2)}"
wp_count = sum(1 for line in lines if _COMPRESSED_WP.match(line))
return _result(
f"Trajectory / ADS report{route}" + (f" · {wp_count} point(s)" if wp_count else ""),
kind="trajectory",
)
perf = _summarize_performance(raw, first_line)
if perf:
return perf
if _LDR.match(first_line):
route = _extract_route(raw)
return _result(f"Load report · {route}" if route else "Load report", kind="load")
if _WX.match(first_line):
route = _extract_route(raw)
return _result(f"Weather request · {route or 'en route'}", kind="weather")
if _DOOR.match(first_line):
return _result("Cabin / door advisory", kind="cabin")
if _WAYPOINT.match(first_line.lstrip("N")):
parts = first_line.lstrip("N").rstrip(",").split(",")
if len(parts) >= 4:
lat, lon, _t, alt = parts[0], parts[1], parts[2], parts[3]
phase = _phase_name(parts[7]) if len(parts) >= 8 else ""
summary = f"Waypoint · {lat},{lon} · alt {alt} ft" + (f" · {phase}" if phase else "")
return _result(summary, kind="waypoint")
label_summary = _summarize_by_label(label_u, raw, first_line)
if label_summary:
return label_summary
flight = _extract_flight(raw)
route = _extract_route(raw)
if flight and route:
return _result(f"Datalink · {flight} · {route}", kind="flight")
eta = _ETA.search(raw)
if eta and flight:
return _result(f"ETA update · {flight} · {eta.group(1)}", kind="eta")
if len(raw) < 100 and not _MOSTLY_OPAQUE.match(raw) and not _GARBLED_VDL.search(raw):
clean = raw.replace("\n", " · ")
if label_u in _LABEL_HINTS:
return _result(f"{_LABEL_HINTS[label_u].title()} · {clean}", kind="short")
return _result(clean, kind="short")
digit_ratio = sum(ch.isdigit() for ch in raw) / max(len(raw), 1)
if digit_ratio > 0.55 or _MOSTLY_OPAQUE.match(raw.replace(" ", "")) or _GARBLED_VDL.search(raw):
return _result(
"Binary / proprietary telemetry (undecoded)",
kind="vdl_binary",
readable=False,
hidden=True,
)
if label_u in _LABEL_HINTS:
return _result(
f"{_LABEL_HINTS[label_u].title()} message",
kind=label_u.lower(),
readable=False,
hidden=False,
)
return _result(
first_line[:100] + ("" if len(first_line) > 100 else ""),
kind="raw",
readable=False,
hidden=False,
)
def prepare_datalink_display(messages: list[dict[str, Any]]) -> dict[str, Any]:
"""Attach summaries and filter noise for dossier display."""
enriched: list[dict[str, Any]] = []
hidden_count = 0
seen_summaries: set[str] = set()
for message in messages:
meta = summarize_datalink_message(
label=str(message.get("label") or ""),
text=str(message.get("text") or ""),
source_type=str(message.get("source_type") or ""),
)
item = {**message, **meta}
if item.get("hidden"):
hidden_count += 1
continue
# Drop back-to-back duplicate summaries (common with multi-part VDL)
sig = f"{item.get('kind')}|{item.get('summary')}"
if sig in seen_summaries and item.get("kind") not in {"short", "ops", "request"}:
hidden_count += 1
continue
seen_summaries.add(sig)
enriched.append(item)
return {
"messages": enriched,
"hidden_count": hidden_count,
"total_count": len(messages),
}
def attach_summaries(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
return prepare_datalink_display(messages)["messages"]
+7 -1
View File
@@ -593,9 +593,15 @@ def lookup_datalink_messages(
if queued_refresh:
_ensure_stagger_worker()
from services.fetchers.acars_summarize import prepare_datalink_display
display = prepare_datalink_display(messages)
return {
"configured": True,
"messages": messages,
"messages": display["messages"],
"hidden_count": display["hidden_count"],
"total_count": display["total_count"],
"last_success_at": last_success_at,
"queued_refresh": queued_refresh,
"priority_scan": queued_refresh,
+182
View File
@@ -0,0 +1,182 @@
from services.fetchers.acars_summarize import prepare_datalink_display, summarize_datalink_message
# --- Southwest (existing) ---
def test_summarize_track_report():
text = """++86501,N8997Q,B7378MAX,260620,WN3743,KMSP,KMDW,0496,SMX34-2502-F320
6
N4432.0,W09305.6,201041,15193,-08.3,310,044,CL,00000,0,"""
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "track"
assert "WN3743" in meta["summary"]
assert "KMSP→KMDW" in meta["summary"]
def test_summarize_sw_performance_cruise():
text = """72740,7852,B737-700,260624,WN0120,KABQ,KDEN,1986,SW2501
18.45.14,CR,1575,28981,280.0,.729,-32.3,-06.5,N3601.3,W10655.7,131240
0.48,FHP,AIR
SIN,-1.42 0.30 0.29"""
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "engine_health"
assert "WN0120" in meta["summary"]
assert "cruise" in meta["summary"]
def test_summarize_sw_climb_performance():
text = """05201,7852,B737-700,260624,WN0120,KABQ,KDEN,1986,SW2501
18.38.08,CL,1149,15631,257.0,.520,000.0,014.5,N3515.4,W10649.4,132800
001.40,001,4100,FLAPS-UP"""
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "climb_perf"
assert "climb" in meta["summary"]
def test_summarize_trajectory():
text = """76401
02E24KABQKDEN
N35112W10679318361096P014343008G000022::I0:9W
N35195W10681118371370P006337009G000022::Q0OXW"""
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "trajectory"
assert "KABQ→KDEN" in meta["summary"]
def test_fragment_hidden():
text = "0000000,00000000,00000000\n18.38.23,16395,250.2,.510,01.07,01.04,00,00000000"
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "fragment"
assert meta["hidden"] is True
def test_vdl_binary_hidden():
text = "014F63N\n)AJQZ)LC0Z0IP-M7O,ZHN3-M,73ZO,UU-ZOS1Z7PPZMSN1ZN"
meta = summarize_datalink_message(label="37", text=text, source_type="vdl")
assert meta["hidden"] is True
# --- United / Delta / American ---
def test_united_free_text_position():
text = "POS N40.123 W074.456 FL350 GS450 1425Z"
meta = summarize_datalink_message(label="Q0", text=text, source_type="vdl")
assert meta["kind"] == "position"
assert "FL350" in meta["summary"]
def test_delta_oooi_out():
text = "OUT 1425 12JAN KATL"
meta = summarize_datalink_message(label="00", text=text, source_type="acars")
assert meta["kind"] == "oooi"
assert "OUT 1425" in meta["summary"]
def test_american_fi_block():
text = "FI AA100/AN N100AA/DA KDFW/AA KLAX OUT 1832 OFF 1845"
meta = summarize_datalink_message(label="44", text=text, source_type="acars")
assert "AA100" in meta["summary"]
assert "KDFW→KLAX" in meta["summary"]
def test_united_performance_a320():
text = """88401,4521,A320-200,260624,UA1234,KORD,KDEN,1200,UA2501
19.10.22,CR,2200,35000,450.0,.820,-45.0,-02.0,N3950.1,W10440.2,125000"""
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "performance"
assert "UA1234" in meta["summary"]
assert "KORD→KDEN" in meta["summary"]
# --- International ---
def test_british_airways_engine():
text = "ENG1 N1 92.5 N2 95.1 EGT 512 FF 2850"
meta = summarize_datalink_message(label="B1", text=text, source_type="satcom")
assert meta["kind"] == "engine"
def test_qantas_fi_position():
text = "FI QF9/AN VH-OQA/DA YSSY/AD EGLL POSN32249E045047,,082806,380,DEBNI"
meta = summarize_datalink_message(label="H1", text=text, source_type="acars")
assert meta["kind"] == "position"
assert "QF9" in meta["summary"]
assert "YSSY→EGLL" in meta["summary"]
def test_lufthansa_weather():
text = "WX 250/045 SAT -42 TB MOD EDDF"
meta = summarize_datalink_message(label="80", text=text, source_type="acars")
assert meta["kind"] == "weather"
assert "EDDF" in meta["summary"]
def test_air_france_request():
text = "REQUEST FL370 DUE TURB"
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "request"
assert "FL370" in meta["summary"]
# --- Cargo / military ---
def test_fedex_flight():
text = "++86501,N123FE,B763,260624,FDX1544,KMEM,KORD,0498,SMX34"
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "track"
assert "FDX1544" in meta["summary"]
def test_military_rch():
text = "POSN3840.5 W07720.1 FL280 RCH123 KADW KDMA"
meta = summarize_datalink_message(label="Q0", text=text, source_type="acars")
assert "RCH123" in meta["summary"]
# --- Ops / misc ---
def test_flight_plan():
text = "FPN/RI:DA:KJFK:AA:EGLL..MERIT:D:MERIT"
meta = summarize_datalink_message(label="H1", text=text, source_type="vdl")
assert meta["kind"] == "flight_plan"
assert "KJFK→EGLL" in meta["summary"]
def test_departure_report():
text = "DEP FI DL456/DA KATL/AD KLAX OUT 1205"
meta = summarize_datalink_message(label="40", text=text, source_type="acars")
assert meta["kind"] == "dep"
assert "DL456" in meta["summary"]
def test_pirep():
text = "#CFB/PIREP MOD TURB FL280 N3845 W09030"
meta = summarize_datalink_message(label="H1", text=text, source_type="acars")
assert meta["kind"] == "pirep"
def test_prepare_filters_hidden_and_dedupes():
messages = [
{"id": 1, "label": "H1", "text": "POSN35259W106517,KABQ,KDEN", "source_type": "vdl"},
{"id": 2, "label": "H1", "text": "0000000,00000000,00000000", "source_type": "vdl"},
{"id": 3, "label": "37", "text": "014F63N\n)AJQZ)LC0Z", "source_type": "vdl"},
{
"id": 4,
"label": "H1",
"text": "72740,7852,B737-700,260624,WN0120,KABQ,KDEN\n18.45.14,CR,1575,28981",
"source_type": "vdl",
},
{
"id": 5,
"label": "H1",
"text": "72740,7852,B737-700,260624,WN0120,KABQ,KDEN\n18.45.14,CR,1575,28981",
"source_type": "vdl",
},
]
display = prepare_datalink_display(messages)
assert display["hidden_count"] == 3
assert len(display["messages"]) == 2
@@ -9,11 +9,49 @@ type DatalinkMessage = {
label?: string;
text?: string;
source_type?: string;
summary?: string;
kind?: string;
readable?: boolean;
};
const PRIORITY_POLL_MS = 3_000;
const PRIORITY_POLL_MAX_MS = 45_000;
function DatalinkMessageRow({ message }: { message: DatalinkMessage }) {
const [showRaw, setShowRaw] = useState(false);
const summary = message.summary?.trim();
const raw = message.text?.trim() || '';
const hasSummary = Boolean(summary);
const showRawBlock = showRaw || (!hasSummary && raw);
return (
<div className="text-[10px] font-mono leading-snug border border-[var(--border-primary)]/60 bg-black/20 px-2 py-1.5">
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-0.5">
<span>{formatDatalinkTime(message.timestamp)}</span>
{message.label ? <span className="text-orange-400/90">{message.label}</span> : null}
{message.source_type ? <span className="truncate">{message.source_type}</span> : null}
</div>
{hasSummary ? (
<div className="text-[var(--text-primary)] break-words">{summary}</div>
) : null}
{hasSummary && raw ? (
<button
type="button"
onClick={() => setShowRaw((value) => !value)}
className="mt-0.5 text-[var(--text-muted)] hover:text-cyan-400/90 underline underline-offset-2"
>
{showRaw ? 'hide raw' : 'show raw'}
</button>
) : null}
{showRawBlock && raw ? (
<div className="mt-0.5 text-[var(--text-muted)] whitespace-pre-wrap break-words text-[9px] leading-relaxed max-h-24 overflow-y-auto">
{raw}
</div>
) : null}
</div>
);
}
function formatDatalinkTime(value?: string): string {
if (!value) return '--:--';
try {
@@ -40,6 +78,8 @@ export default function DatalinkMessagesBlock({
const [hint, setHint] = useState<string | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [priorityScanning, setPriorityScanning] = useState(false);
const [hiddenCount, setHiddenCount] = useState(0);
const [showHidden, setShowHidden] = useState(false);
const pollUntilRef = useRef(0);
const buildParams = useCallback(() => {
@@ -71,6 +111,7 @@ export default function DatalinkMessagesBlock({
const json = await res.json();
setConfigured(Boolean(json.configured));
setMessages(Array.isArray(json.messages) ? json.messages : []);
setHiddenCount(typeof json.hidden_count === 'number' ? json.hidden_count : 0);
setHint(typeof json.hint === 'string' ? json.hint : null);
setLoadError(null);
if (json.priority_scan || json.queued_refresh) {
@@ -163,16 +204,27 @@ export default function DatalinkMessagesBlock({
) : null}
<div className="max-h-36 overflow-y-auto space-y-1.5 pr-1">
{messages.map((message) => (
<div key={message.id} className="text-[10px] font-mono leading-snug border border-[var(--border-primary)]/60 bg-black/20 px-2 py-1.5">
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-0.5">
<span>{formatDatalinkTime(message.timestamp)}</span>
{message.label ? <span className="text-orange-400/90">{message.label}</span> : null}
{message.source_type ? <span className="truncate">{message.source_type}</span> : null}
</div>
<div className="text-[var(--text-primary)] whitespace-pre-wrap break-words">{message.text}</div>
</div>
<DatalinkMessageRow key={message.id} message={message} />
))}
</div>
{hiddenCount > 0 ? (
<p className="text-[9px] font-mono text-[var(--text-muted)] mt-1">
{hiddenCount} binary/fragment message{hiddenCount === 1 ? '' : 's'} hidden.{' '}
<button
type="button"
onClick={() => setShowHidden((value) => !value)}
className="underline underline-offset-2 hover:text-cyan-400/90"
>
{showHidden ? 'Hide note' : 'Why?'}
</button>
{showHidden ? (
<span className="block mt-0.5 text-[var(--text-muted)]/80">
VDL splits long telemetry into many frames. Southwest also uses proprietary formats
that cannot be decoded without airline keys.
</span>
) : null}
</p>
) : null}
</div>
);
}