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() { '' + '' + '' + - '' + + '' + + '' + + '' + + '' + + '' + '' + '' + '
ICAOMessageCallsignAltitudeSpeedTrackMessages
' @@ -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