diff --git a/routes/acars.py b/routes/acars.py index db05df0..8976781 100644 --- a/routes/acars.py +++ b/routes/acars.py @@ -443,6 +443,26 @@ def stream_acars() -> Response: return response +@acars_bp.route('/messages') +def get_acars_messages() -> Response: + """Get recent ACARS messages from correlator (for history reload).""" + from utils.flight_correlator import get_flight_correlator + limit = request.args.get('limit', 50, type=int) + limit = max(1, min(limit, 200)) + msgs = get_flight_correlator().get_recent_messages('acars', limit) + return jsonify(msgs) + + +@acars_bp.route('/clear', methods=['POST']) +def clear_acars_messages() -> Response: + """Clear stored ACARS messages and reset counter.""" + global acars_message_count + from utils.flight_correlator import get_flight_correlator + get_flight_correlator().clear_acars() + acars_message_count = 0 + return jsonify({'status': 'cleared'}) + + @acars_bp.route('/frequencies') def get_frequencies() -> Response: """Get default ACARS frequencies.""" diff --git a/routes/vdl2.py b/routes/vdl2.py index ca2200a..637571d 100644 --- a/routes/vdl2.py +++ b/routes/vdl2.py @@ -79,6 +79,22 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) -> data['type'] = 'vdl2' data['timestamp'] = datetime.utcnow().isoformat() + 'Z' + # Enrich embedded ACARS payload with translated label + try: + vdl2_inner = data.get('vdl2', data) + acars_payload = (vdl2_inner.get('avlc') or {}).get('acars') + if acars_payload and acars_payload.get('label'): + from utils.acars_translator import translate_message + translation = translate_message({ + 'label': acars_payload.get('label'), + 'text': acars_payload.get('msg_text', ''), + }) + acars_payload['label_description'] = translation['label_description'] + acars_payload['message_type'] = translation['message_type'] + acars_payload['parsed'] = translation['parsed'] + except Exception: + pass + # Update stats vdl2_message_count += 1 vdl2_last_message_time = time.time() @@ -370,6 +386,26 @@ def stream_vdl2() -> Response: return response +@vdl2_bp.route('/messages') +def get_vdl2_messages() -> Response: + """Get recent VDL2 messages from correlator (for history reload).""" + from utils.flight_correlator import get_flight_correlator + limit = request.args.get('limit', 50, type=int) + limit = max(1, min(limit, 200)) + msgs = get_flight_correlator().get_recent_messages('vdl2', limit) + return jsonify(msgs) + + +@vdl2_bp.route('/clear', methods=['POST']) +def clear_vdl2_messages() -> Response: + """Clear stored VDL2 messages and reset counter.""" + global vdl2_message_count + from utils.flight_correlator import get_flight_correlator + get_flight_correlator().clear_vdl2() + vdl2_message_count = 0 + return jsonify({'status': 'cleared'}) + + @vdl2_bp.route('/frequencies') def get_frequencies() -> Response: """Get default VDL2 frequencies.""" diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index 1d91ef4..61623c8 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -171,9 +171,14 @@
- +
+ + +
@@ -221,9 +226,14 @@
- +
+ + +
@@ -2786,6 +2796,31 @@ sudo make install } function selectAircraft(icao) { + // Toggle: clicking the same aircraft deselects it + if (selectedIcao === icao) { + const prev = selectedIcao; + selectedIcao = null; + // Reset previous marker icon + if (prev && markers[prev] && aircraft[prev]) { + const ac = aircraft[prev]; + const militaryInfo = isMilitaryAircraft(prev, ac.callsign); + const rotation = Math.round((ac.heading || 0) / 5) * 5; + const color = militaryInfo.military ? '#556b2f' : getAltitudeColor(ac.altitude); + const iconType = getAircraftIconType(ac.type_code, militaryInfo.military); + markers[prev].setIcon(createMarkerIcon(rotation, color, iconType, false)); + if (markerState[prev]) markerState[prev].isSelected = false; + } + renderAircraftList(); + showAircraftDetails(null); + updateFlightLookupBtn(); + highlightSidebarMessages(null); + if (acarsMessageTimer) { + clearInterval(acarsMessageTimer); + acarsMessageTimer = null; + } + return; + } + const prevSelected = selectedIcao; selectedIcao = icao; @@ -2915,8 +2950,11 @@ sudo make install
-
Datalink Messages
-
Loading...
+
+ Datalink Messages + +
+
Loading...
`; // Fetch aircraft photo if registration is available @@ -2945,18 +2983,23 @@ sudo make install const container = document.getElementById('aircraftAcarsMessages'); if (!container) return; const msgs = (data.acars || []).concat(data.vdl2 || []); + let newHtml; if (msgs.length === 0) { - container.innerHTML = 'No messages yet'; - return; + newHtml = 'No messages yet'; + } else { + msgs.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || '')); + newHtml = msgs.slice(0, 20).map(renderAcarsCard).join(''); + } + // Only update DOM if content actually changed + if (container.innerHTML !== newHtml) { + container.style.opacity = '0.7'; + setTimeout(() => { + container.innerHTML = newHtml; + container.style.opacity = '1'; + }, 150); } - // Sort newest first - msgs.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || '')); - container.innerHTML = msgs.slice(0, 20).map(renderAcarsCard).join(''); }) - .catch(() => { - const container = document.getElementById('aircraftAcarsMessages'); - if (container) container.innerHTML = ''; - }); + .catch(() => { /* keep existing content on error */ }); } doFetch(); @@ -3532,6 +3575,17 @@ sudo make install document.getElementById('acarsToggleBtn').classList.add('active'); document.getElementById('acarsPanelIndicator').classList.add('active'); startAcarsStream(false); + // Reload message history from backend + fetch('/acars/messages?limit=50') + .then(r => r.json()) + .then(msgs => { + if (!msgs || !msgs.length) return; + // Add oldest first so newest ends up on top + for (let i = msgs.length - 1; i >= 0; i--) { + addAcarsMessage(msgs[i]); + } + }) + .catch(() => {}); } }) .catch(() => {}); @@ -3887,6 +3941,33 @@ sudo make install } } + function clearAcarsMessages() { + fetch('/acars/clear', { method: 'POST' }).catch(() => {}); + const container = document.getElementById('acarsMessages'); + container.innerHTML = '
No ACARS messages
Start ACARS to receive aircraft datalink messages
'; + acarsMessageCount = 0; + document.getElementById('acarsCount').textContent = '0'; + } + + function clearVdl2Messages() { + fetch('/vdl2/clear', { method: 'POST' }).catch(() => {}); + const container = document.getElementById('vdl2Messages'); + container.innerHTML = '
No VDL2 messages
Start VDL2 to receive digital datalink messages
'; + vdl2MessageCount = 0; + document.getElementById('vdl2Count').textContent = '0'; + } + + function clearAircraftMessages() { + const container = document.getElementById('aircraftAcarsMessages'); + if (container) { + container.innerHTML = 'No messages yet'; + } + if (acarsMessageTimer) { + clearInterval(acarsMessageTimer); + acarsMessageTimer = null; + } + } + // Populate ACARS device selector document.addEventListener('DOMContentLoaded', () => { fetch('/devices') @@ -4213,13 +4294,14 @@ sudo make install if (acars.reg) acarsFields.push(['Registration', acars.reg]); if (acars.flight) acarsFields.push(['Flight', acars.flight]); if (acars.mode) acarsFields.push(['Mode', acars.mode]); - if (acars.label) acarsFields.push(['Label', acars.label]); + if (acars.label) acarsFields.push(['Label', acars.label + (acars.label_description ? ' — ' + acars.label_description : '')]); if (acars.sublabel) acarsFields.push(['Sublabel', acars.sublabel]); if (acars.blk_id) acarsFields.push(['Block ID', acars.blk_id]); if (acars.ack) acarsFields.push(['ACK', acars.ack === '!' ? 'NAK' : acars.ack]); if (acars.msg_num) acarsFields.push(['Msg Number', acars.msg_num]); if (acars.msg_num_seq) acarsFields.push(['Msg Seq', acars.msg_num_seq]); if (acars.more != null) acarsFields.push(['More Follows', acars.more ? 'Yes' : 'No']); + if (acars.message_type && acars.message_type !== 'other') acarsFields.push(['Message Type', acars.message_type.replace(/_/g, ' ')]); const acarsHtml = buildSection('ACARS', acarsFields); // XID fields @@ -4352,11 +4434,16 @@ sudo make install (matchedIcao ? ' cursor: pointer;' : ''); const linkIcon = matchedIcao ? '' : ''; + const acarsLabelDesc = acars.label_description || ''; + const acarsMsgType = acars.message_type || 'other'; + const vdl2TypeBadge = typeof getAcarsTypeBadge === 'function' && acars.label ? getAcarsTypeBadge(acarsMsgType) : ''; + msg.innerHTML = `
${escapeHtml(label)}${linkIcon} ${time}
+ ${acarsLabelDesc ? `
${vdl2TypeBadge} ${escapeHtml(acarsLabelDesc)}
` : ''}
${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 50)) + (msgText.length > 50 ? '…' : '') : ''}
@@ -4430,6 +4517,16 @@ sudo make install document.getElementById('vdl2ToggleBtn').classList.add('active'); document.getElementById('vdl2PanelIndicator').classList.add('active'); startVdl2Stream(false); + // Reload message history from backend + fetch('/vdl2/messages?limit=50') + .then(r => r.json()) + .then(msgs => { + if (!msgs || !msgs.length) return; + for (let i = msgs.length - 1; i >= 0; i--) { + addVdl2Message(msgs[i]); + } + }) + .catch(() => {}); } }) .catch(() => {}); diff --git a/utils/flight_correlator.py b/utils/flight_correlator.py index a95b4f4..4e37b84 100644 --- a/utils/flight_correlator.py +++ b/utils/flight_correlator.py @@ -81,6 +81,21 @@ class FlightCorrelator: """Return message without internal correlation fields.""" return {k: v for k, v in msg.items() if not k.startswith('_corr_')} + def get_recent_messages(self, msg_type: str = 'acars', limit: int = 50) -> list[dict]: + """Return the most recent messages (newest first).""" + source = self._acars_messages if msg_type == 'acars' else self._vdl2_messages + msgs = [self._clean_msg(m) for m in source] + msgs.reverse() + return msgs[:limit] + + def clear_acars(self) -> None: + """Clear all stored ACARS messages.""" + self._acars_messages.clear() + + def clear_vdl2(self) -> None: + """Clear all stored VDL2 messages.""" + self._vdl2_messages.clear() + @property def acars_count(self) -> int: return len(self._acars_messages)