Files
wiki/decode.py
2026-04-12 21:55:33 +03:00

228 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import subprocess, json, time, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
import pyModeS as pms
aircraft, cpr = {}, {}
HTML = """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ADS-B Радар</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@10/ol.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; color: #e0e0e0; font-family: 'Courier New', monospace; height: 100vh; overflow: hidden; }
#map { width: 100vw; height: 100vh; }
#header {
position: absolute; top: 0; left: 0; right: 0; z-index: 1000;
background: rgba(26,26,46,0.92); border-bottom: 1px solid #2a2a5a;
padding: 8px 16px; display: flex; align-items: center; gap: 16px;
}
#header h1 { font-size: 16px; color: #7eb8f7; letter-spacing: 2px; }
#count { font-size: 13px; color: #aaa; }
#popup {
position: absolute; z-index: 2000;
background: rgba(16,16,36,0.97); border: 1px solid #3a3a7a;
border-radius: 8px; padding: 12px 16px; min-width: 200px;
box-shadow: 0 4px 24px rgba(0,0,0,0.6);
display: none; pointer-events: none;
}
#popup .icao { font-size: 18px; font-weight: bold; color: #7eb8f7; margin-bottom: 6px; }
#popup .row { font-size: 13px; color: #ccc; margin: 2px 0; }
#popup .label { color: #888; margin-right: 6px; }
.ol-attribution { display: none !important; }
</style>
</head>
<body>
<div id="header">
<h1>✈ ADS-B РАДАР</h1>
<span id="count">Самолётов: 0</span>
</div>
<div id="map"></div>
<div id="popup">
<div class="icao" id="p-icao"></div>
<div class="row"><span class="label">Позывной:</span><span id="p-flight">—</span></div>
<div class="row"><span class="label">Высота:</span><span id="p-alt">—</span></div>
<div class="row"><span class="label">Скорость:</span><span id="p-speed">—</span></div>
<div class="row"><span class="label">Курс:</span><span id="p-track">—</span></div>
<div class="row"><span class="label">Пакетов:</span><span id="p-msgs">—</span></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/ol@10/dist/ol.js"></script>
<script>
const map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png'
})
})
],
view: new ol.View({ center: ol.proj.fromLonLat([37.62, 55.75]), zoom: 8 }),
controls: []
});
const vectorSource = new ol.source.Vector();
map.addLayer(new ol.layer.Vector({ source: vectorSource }));
const features = {};
let selectedIcao = null;
let nowTs = Date.now() / 1000;
const popup = document.getElementById('popup');
function freshColor(seen) {
const age = nowTs - (seen || 0);
if (age < 15) return '#00ff88';
if (age < 45) return '#ffcc00';
return '#ff4444';
}
function makeStyle(track, selected, seen) {
const color = selected ? '#ffd700' : freshColor(seen);
return new ol.style.Style({ text: new ol.style.Text({
text: '', font: (selected ? 'bold 26px' : '22px') + ' sans-serif',
rotation: (track || 0) * Math.PI / 180,
fill: new ol.style.Fill({ color }),
stroke: new ol.style.Stroke({ color: '#1a1a2e', width: 2 })
})});
}
function showPopup(ac, pixel) {
selectedIcao = ac.icao;
document.getElementById('p-icao').textContent = ac.icao.toUpperCase();
document.getElementById('p-flight').textContent = ac.flight || '';
document.getElementById('p-alt').textContent = ac.altitude != null ? ac.altitude + ' ft' : '';
document.getElementById('p-speed').textContent = ac.speed != null ? ac.speed + ' кт' : '';
document.getElementById('p-track').textContent = ac.track != null ? ac.track + '°' : '';
document.getElementById('p-msgs').textContent = ac.msgs || '';
let [x, y] = pixel;
if (x + 220 > map.getSize()[0]) x -= 230; else x += 20;
popup.style.left = x + 'px'; popup.style.top = (y - 10) + 'px';
popup.style.display = 'block';
if (features[ac.icao]) features[ac.icao].setStyle(makeStyle(ac.track, true, ac.seen));
}
function hidePopup() {
if (selectedIcao && features[selectedIcao]) {
const d = features[selectedIcao]._data;
features[selectedIcao].setStyle(makeStyle(d?.track, false, d?.seen));
}
selectedIcao = null;
popup.style.display = 'none';
}
map.on('click', e => {
const hit = map.forEachFeatureAtPixel(e.pixel, f => f, { hitTolerance: 12 });
hit?._data ? showPopup(hit._data, e.pixel) : hidePopup();
});
map.on('pointermove', e => {
map.getTargetElement().style.cursor =
map.hasFeatureAtPixel(e.pixel, { hitTolerance: 12 }) ? 'pointer' : '';
});
async function refresh() {
try {
const data = await (await fetch('/aircraft.json')).json();
nowTs = data.now;
const active = new Set();
for (const ac of data.aircraft) {
if (ac.lat == null || ac.lon == null) continue;
active.add(ac.icao);
const coord = ol.proj.fromLonLat([ac.lon, ac.lat]);
const sel = ac.icao === selectedIcao;
if (features[ac.icao]) {
features[ac.icao].getGeometry().setCoordinates(coord);
features[ac.icao].setStyle(makeStyle(ac.track, sel, ac.seen));
features[ac.icao]._data = ac;
} else {
const f = new ol.Feature({ geometry: new ol.geom.Point(coord) });
f.setStyle(makeStyle(ac.track, false, ac.seen)); f._data = ac;
vectorSource.addFeature(f); features[ac.icao] = f;
}
}
for (const icao of Object.keys(features)) {
if (!active.has(icao)) { vectorSource.removeFeature(features[icao]); delete features[icao]; }
}
document.getElementById('count').textContent = 'Самолётов: ' + active.size;
} catch(e) {}
}
refresh(); setInterval(refresh, 3000);
</script>
</body>
</html>"""
def decode():
proc = subprocess.Popen(["rtl_adsb.exe"], stdout=subprocess.PIPE, text=True)
for line in proc.stdout:
line = line.strip()
if not (line.startswith('*') and line.endswith(';')):
continue
msg = line[1:-1]
if len(msg) not in (14, 28):
continue
try:
if pms.df(msg) != 17:
continue
icao = pms.icao(msg)
tc = pms.typecode(msg)
if icao not in aircraft:
aircraft[icao] = {'icao': icao}
aircraft[icao]['seen'] = time.time()
aircraft[icao]['msgs'] = aircraft[icao].get('msgs', 0) + 1
if 1 <= tc <= 4:
cs = pms.adsb.callsign(msg).strip()
if cs: aircraft[icao]['flight'] = cs
elif 9 <= tc <= 18:
alt = pms.adsb.altitude(msg)
if alt: aircraft[icao]['altitude'] = alt
oe = pms.adsb.oe_flag(msg)
cpr.setdefault(icao, {})[oe] = (msg, time.time())
try:
pos = pms.adsb.position_with_ref(msg, 55.75, 37.62)
if pos: aircraft[icao]['lat'], aircraft[icao]['lon'] = pos
except: pass
if 0 in cpr[icao] and 1 in cpr[icao]:
m0,t0 = cpr[icao][0]; m1,t1 = cpr[icao][1]
if abs(t0-t1) < 10:
try:
pos = pms.adsb.position(m0, m1, t0, t1)
if pos: aircraft[icao]['lat'], aircraft[icao]['lon'] = pos
except: pass
elif tc == 19:
v = pms.adsb.velocity(msg)
if v: aircraft[icao]['speed'], aircraft[icao]['track'] = v[0], v[1]
except: pass
class H(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/aircraft.json':
now = time.time()
active = [v for v in aircraft.values() if now - v.get('seen',0) < 120]
body = json.dumps({'now': now, 'aircraft': active}).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(body)
elif self.path == '/':
body = HTML.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, *a): pass
threading.Thread(target=decode, daemon=True).start()
print("Запущен!")
print(" Карта: http://localhost:8080/")
print(" API: http://localhost:8080/aircraft.json")
HTTPServer(('0.0.0.0', 8080), H).serve_forever()