diff --git a/csdr/chain/dump1090.py b/csdr/chain/dump1090.py
index 73cb5309..b298dd3f 100644
--- a/csdr/chain/dump1090.py
+++ b/csdr/chain/dump1090.py
@@ -1,7 +1,8 @@
from pycsdr.modules import Convert
from pycsdr.types import Format
from csdr.chain.demodulator import ServiceDemodulator
-from owrx.adsb.dump1090 import Dump1090Module, RawDeframer, ModeSParser
+from owrx.adsb.dump1090 import Dump1090Module, RawDeframer
+from owrx.adsb.modes import ModeSParser
class Dump1090(ServiceDemodulator):
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
index 817857b2..0d3b8a1b 100644
--- a/htdocs/lib/MessagePanel.js
+++ b/htdocs/lib/MessagePanel.js
@@ -272,6 +272,7 @@ $.fn.pocsagMessagePanel = function() {
AdsbMessagePanel = function(el) {
MessagePanel.call(this, el);
+ this.aircraft = {}
this.initClearTimer();
}
@@ -286,7 +287,11 @@ AdsbMessagePanel.prototype.render = function() {
'
' +
'' +
'| ICAO | ' +
- 'Message | ' +
+ 'Callsign | ' +
+ 'Altitude | ' +
+ 'Speed | ' +
+ 'Track | ' +
+ 'Messages | ' +
'
' +
'' +
'
'
@@ -295,16 +300,64 @@ AdsbMessagePanel.prototype.render = function() {
AdsbMessagePanel.prototype.pushMessage = function(message) {
+ if (!('icao' in message)) return;
+ if (!(message.icao in this.aircraft)) {
+ var el = $("");
+ $(this.el).find('tbody').append(el);
+ this.aircraft[message.icao] = {
+ el: el,
+ messages: 0
+ }
+ }
+ var state = this.aircraft[message.icao];
+ Object.assign(state, message);
+ state.lastSeen = Date.now();
+ state.messages += 1;
+
+ var ifDefined = function(input, formatter) {
+ if (typeof(input) !== 'undefined') {
+ if (formatter) return formatter(input);
+ return input;
+ }
+ return "";
+ }
+
+ state.el.html(
+ '| ' + state.icao + ' | ' +
+ '' + ifDefined(state.identification) + ' | ' +
+ '' + ifDefined(state.altitude) + ' | ' +
+ '' + ifDefined(state.groundspeed, Math.round) + ' | ' +
+ '' + ifDefined(state.groundtrack, Math.round) + ' | ' +
+ '' + state.messages + ' | '
+ );
+
var $b = $(this.el).find('tbody');
- $b.append($(
- '
' +
- ' | ' +
- '' + JSON.stringify(message) + ' | ' +
- '
'
- ));
$b.scrollTop($b[0].scrollHeight);
};
+AdsbMessagePanel.prototype.clearMessages = function(toRemain) {
+ console.info("clearing old aircraft...");
+ var now = Date.now();
+ var me = this;
+ Object.entries(this.aircraft).forEach(function(e) {
+ if (now - e[1].lastSeen > toRemain) {
+ console.info("removing " + e[0]);
+ delete me.aircraft[e[0]];
+ e[1].el.remove();
+ }
+ })
+ console.info("done; tracking " + Object.keys(this.aircraft).length + " aircraft");
+};
+
+AdsbMessagePanel.prototype.initClearTimer = function() {
+ var me = this;
+ if (me.removalInterval) clearInterval(me.removalInterval);
+ me.removalInterval = setInterval(function () {
+ me.clearMessages(30000);
+ }, 15000);
+};
+
+
$.fn.adsbMessagePanel = function () {
if (!this.data('panel')) {
this.data('panel', new AdsbMessagePanel(this));
diff --git a/owrx/adsb/dump1090.py b/owrx/adsb/dump1090.py
index ffd4dd1d..bc88880f 100644
--- a/owrx/adsb/dump1090.py
+++ b/owrx/adsb/dump1090.py
@@ -1,6 +1,6 @@
from pycsdr.modules import ExecModule, Writer, TcpSource
from pycsdr.types import Format
-from csdr.module import LogWriter, ThreadModule, PickleModule
+from csdr.module import LogWriter, ThreadModule
from owrx.socket import getAvailablePort
import time
import pickle
@@ -81,11 +81,3 @@ class RawDeframer(ThreadModule):
return bytes.fromhex(line[1:-1].decode())
else:
logger.warning("invalid raw message: %s", line)
-
-
-class ModeSParser(PickleModule):
- def process(self, input):
- return {
- "mode": "ADSB",
- "df": (input[0] & 0b11111000) >> 3
- }
diff --git a/owrx/adsb/modes.py b/owrx/adsb/modes.py
new file mode 100644
index 00000000..5b3468ec
--- /dev/null
+++ b/owrx/adsb/modes.py
@@ -0,0 +1,94 @@
+from csdr.module import PickleModule
+from math import sqrt, atan2, pi
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+FEET_PER_METER = 3.28084
+
+
+class ModeSParser(PickleModule):
+ def process(self, input):
+ format = (input[0] & 0b11111000) >> 3
+ message = {
+ "mode": "ADSB",
+ "format": format
+ }
+ if format == 17:
+ message["capability"] = input[0] & 0b111
+ message["icao"] = input[1:4].hex()
+ type = (input[4] & 0b11111000) >> 3
+ message["type"] = type
+
+ if type in range(1, 5):
+ # identification message
+ id = [
+ (input[5] & 0b11111100) >> 2,
+ ((input[5] & 0b00000011) << 4) | ((input[6] & 0b11110000) >> 4),
+ ((input[6] & 0b00001111) << 2) | ((input[7] & 0b11000000) >> 6),
+ input[7] & 0b00111111,
+ (input[8] & 0b11111100) >> 2,
+ ((input[8] & 0b00000011) << 4) | ((input[9] & 0b11110000) >> 4),
+ ((input[9] & 0b00001111) << 2) | ((input[10] & 0b11000000) >> 6),
+ input[10] & 0b00111111
+ ]
+
+ message["identification"] = bytes(b + (0x40 if b < 27 else 0) for b in id).decode("ascii")
+
+ elif type in range(5, 9):
+ # surface position
+ pass
+
+ elif type in range(9, 19):
+ # airborne position (w/ baro altitude)
+ q = (input[5] & 0b1)
+ altitude = ((input[5] & 0b11111110) << 3) | ((input[6] & 0b1111) >> 4)
+ if q:
+ message["altitude"] = altitude * 25 - 1000
+ else:
+ # TODO: it's gray encoded
+ message["altitude"] = altitude * 100
+
+ elif type == 19:
+ # airborne velocity
+ subtype = input[4] & 0b111
+ if subtype in range(1, 3):
+ dew = (input[5] & 0b00000100) >> 2
+ vew = ((input[5] & 0b00000011) << 8) | input[6]
+ dns = (input[7] & 0b10000000) >> 7
+ vns = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b1110000000) >> 5)
+ vx = vew - 1
+ if dew:
+ vx *= -1
+ vy = vns - 1
+ if dns:
+ vy *= -1
+ # supersonic
+ if subtype == 2:
+ vx *= 4
+ vy *= 4
+ message["groundspeed"] = sqrt(vx ** 2 + vy ** 2)
+ message["groundtrack"] = (atan2(vx, vy) * 360 / (2 * pi)) % 360
+ else:
+ logger.debug("subtype: %i", subtype)
+
+ elif type in range(20, 23):
+ # airborne position (w/GNSS height)
+ altitude = (input[5] << 4) | ((input[6] & 0b1111) >> 4)
+ message["altitude"] = altitude * FEET_PER_METER
+
+ elif type == 28:
+ # aircraft status
+ pass
+
+ elif type == 29:
+ # target state and status information
+ pass
+
+ elif type == 31:
+ # aircraft operation status
+ pass
+
+ return message