# @name FlightRadar # @desc Nearest 3 aircraft via adsb.fi # @version 1.5-lite # @config lat text "Latitude" default="31.2304" maxlen=16 # @config lon text "Longitude" default="121.4737" maxlen=16 # @config radius number "Radius" default=100 min=5 max=450 step=5 unit=km # @config refresh number "Refresh Interval" default=30 min=15 max=300 unit=s # @config page number "Page Duration" default=3 min=2 max=8 unit=s # @config overhead number "Overhead Distance" default=5 min=1 max=30 unit=km # @config alert bool "Overhead Alert" default=true # @config emergency bool "Emergency Alert" default=true # @config alertsec number "Notification Duration" default=8 min=3 max=30 unit=s # @config tint color "Aircraft Color" default=#33AAFF class FlightRadar var url var ticks var busy var pages var count var status var error var shown var page_ms var color var nhex var nflight var ndist var nalt var nspd var ntrk var ndir var last_over var last_emg var opts def init() var km = store.get("radius") if km == nil km = 100 end if km < 5 km = 5 end if km > 463 km = 463 end var nm = int(km / 1.852) if nm < 1 nm = 1 end if nm > 250 nm = 250 end self.url = "https://opendata.adsb.fi/api/v3/lat/" + store.get("lat") + "/lon/" + store.get("lon") + "/dist/" + str(nm) self.ticks = 0 self.busy = false self.pages = [] self.count = 0 self.status = "SCAN..." self.error = false self.shown = now_ms() self.page_ms = store.get("page") * 1000 self.color = store.get("tint") if self.color == nil self.color = 0x33AAFF end self.nhex = nil self.nflight = nil self.ndist = nil self.nalt = nil self.nspd = nil self.ntrk = nil self.ndir = "" self.last_over = nil self.last_emg = nil self.opts = { "mode":"wrap", "speed":75, "holdMs":500 } shared.set("active", false) shared.set("count", 0) shared.set("status", "scan") end def dir(v) if v == nil return "" end if v >= 337.5 || v < 22.5 return "N" end if v < 67.5 return "NE" end if v < 112.5 return "E" end if v < 157.5 return "SE" end if v < 202.5 return "S" end if v < 247.5 return "SW" end if v < 292.5 return "W" end return "NW" end def emg(s) if s == "7500" return "HIJACK" end if s == "7600" return "RADIO" end if s == "7700" return "EMERG" end return nil end def km1(v) var n = int(v * 10 + 0.5) return str(int(n / 10)) + "." + str(n % 10) + "KM" end def on_body(body, st) self.busy = false if st == 0 self.error = true self.status = "NET ERR" shared.set("status", "network") return end if st != 200 self.error = true self.status = "HTTP " + str(st) shared.set("status", self.status) return end if body == nil self.pages = [] self.count = 0 self.status = "NO FLT" self.error = false self.nhex = nil self.last_over = nil self.last_emg = nil shared.set("active", false) shared.set("count", 0) shared.set("status", "empty") return end var ps = re.matchall( "[{][^{}]*\"hex\":\"[A-Za-z0-9]+\"[^{}]*[}]", body ) if ps == nil || size(ps) == 0 self.pages = [] self.count = 0 self.status = "NO FLT" self.error = false shared.set("active", false) shared.set("count", 0) shared.set("status", "empty") return end var bo = [nil,nil,nil] var bd = [999999.0,999999.0,999999.0] var fb = [] var ehex = nil var eflight = nil var esq = nil for p : ps if size(fb) < 3 fb.push(p) end var q = re.search( "\"squawk\":\"([0-9]+)\"", p ) if q != nil && self.emg(q[1]) != nil && esq == nil esq = q[1] var eh = re.search( "\"hex\":\"([A-Za-z0-9]+)", p ) if eh != nil ehex = eh[1] end var ef = re.search( "\"flight\":\"[ ]*([A-Za-z0-9]+)", p ) if ef != nil eflight = ef[1] else eflight = ehex end end var md = re.search( "\"dst\":([0-9.]+)", p ) if md != nil var d = num(md[1]) if d != nil if d < bd[0] bd[2] = bd[1] bo[2] = bo[1] bd[1] = bd[0] bo[1] = bo[0] bd[0] = d bo[0] = p elif d < bd[1] bd[2] = bd[1] bo[2] = bo[1] bd[1] = d bo[1] = p elif d < bd[2] bd[2] = d bo[2] = p end end end end if bo[0] == nil var j = 0 while j < size(fb) && j < 3 bo[j] = fb[j] bd[j] = -1 j += 1 end end var out = [] var c = 0 var i = 0 self.nhex = nil self.nflight = nil self.ndist = nil self.nalt = nil self.nspd = nil self.ntrk = nil self.ndir = "" while i < 3 var p = bo[i] if p != nil var mh = re.search( "\"hex\":\"([A-Za-z0-9]+)", p ) if mh != nil var hx = mh[1] var mf = re.search( "\"flight\":\"[ ]*([A-Za-z0-9]+)", p ) var fl = hx if mf != nil fl = mf[1] end var dist = nil if bd[i] >= 0 dist = bd[i] * 1.852 end var alt = nil var ma = re.search( "\"alt_baro\":([-0-9.]+)", p ) if ma != nil alt = num(ma[1]) else ma = re.search( "\"alt_geom\":([-0-9.]+)", p ) if ma != nil alt = num(ma[1]) end end var sp = nil var ms = re.search( "\"gs\":([0-9.]+)", p ) if ms != nil sp = num(ms[1]) end var tr = nil var mt = re.search( "\"track\":([0-9.]+)", p ) if mt != nil tr = num(mt[1]) end var di = self.dir(tr) # Page 1: flight + distance var p1 = str(i + 1) + " " + fl if dist != nil p1 = p1 + " " + self.km1(dist) end # Page 2: altitude + speed + direction var p2 = str(i + 1) if alt != nil if alt >= 1000 p2 = p2 + " " + str( int( (alt + 500) / 1000 ) ) + "K" else p2 = p2 + " " + str(int(alt)) + "FT" end end if sp != nil p2 = p2 + " " + str(int(sp)) + "KT" end if di != "" p2 = p2 + " " + di end out.push(p1) out.push(p2) c += 1 # Nearest aircraft if i == 0 self.nhex = hx self.nflight = fl self.ndist = dist self.nalt = alt self.nspd = sp self.ntrk = tr self.ndir = di end end end i += 1 end self.pages = out self.count = c self.error = false if c > 0 self.status = "LIVE" shared.set("status", "ok") else self.status = "NO FLT" shared.set("status", "empty") end shared.set("active", c > 0) shared.set("count", c) # Shared: nearest aircraft if self.nhex != nil shared.set( "flight", self.nflight ) shared.set( "hex", self.nhex ) if self.ndist != nil shared.set( "distance_km", self.ndist ) else shared.set( "distance_km", -1 ) end if self.nalt != nil shared.set( "altitude_ft", self.nalt ) else shared.set( "altitude_ft", -1 ) end if self.nspd != nil shared.set( "speed_kt", self.nspd ) else shared.set( "speed_kt", -1 ) end if self.ntrk != nil shared.set( "heading_deg", self.ntrk ) else shared.set( "heading_deg", -1 ) end shared.set( "direction", self.ndir ) end # ========================================== # Overhead aircraft alert # ========================================== if store.get("alert") && self.nhex != nil && self.ndist != nil && self.ndist <= store.get("overhead") if self.nhex != self.last_over notify({ "text": "OVERHEAD " + self.nflight + " " + self.km1(self.ndist), "textColor": 0xFFFF00, "durationMs": store.get("alertsec") * 1000, "wakeup": true, "stack": false }) self.last_over = self.nhex end elif self.ndist == nil || self.ndist > store.get("overhead") self.last_over = nil end # ========================================== # 7500 / 7600 / 7700 # ========================================== if esq != nil && store.get("emergency") var eid = ehex + ":" + esq if eid != self.last_emg notify({ "text": self.emg(esq) + " " + esq + " " + eflight, "textColor": 0xFF0000, "durationMs": store.get("alertsec") * 1000, "wakeup": true, "stack": false }) self.last_emg = eid end else self.last_emg = nil end end # ============================================ # Periodically fetch aircraft data # ============================================ def loop() if self.ticks <= 0 var r = store.get("refresh") if r == nil || r < 15 r = 30 end self.ticks = r if !self.busy self.busy = true if self.count == 0 && !self.error self.status = "SCAN..." end http.get( self.url, / b, st -> self.on_body(b, st), { "find": "\"ac\":[", "keep": 8192, "headers":{ "User-Agent": "AWTRIX-NG/FlightRadar", "Accept": "application/json" } } ) end end self.ticks -= 1 end def on_show() self.shown = now_ms() end # Show all aircraft pages before switching to the next app def duration() var n = size(self.pages) if n < 1 return self.page_ms end return self.page_ms * n end # ============================================ # Aircraft icon # ============================================ def draw_plane() var x = int(now_ms() / 500) % 2 # Fuselage line( x + 3, 0, x + 3, 7, self.color ) # Wings line( x, 3, x + 6, 3, self.color ) pixel( x + 1, 2, self.color ) pixel( x + 5, 2, self.color ) # Tail line( x + 2, 6, x + 4, 6, self.color ) # Red flashing light if now_ms() % 1000 < 150 pixel( x + 6, 3, 0xFF0000 ) end end # ============================================ # Rendering # ============================================ def draw() clear() self.draw_plane() if self.error pixel( width() - 1, 0, 0xFF0000 ) end var label = self.status var col = 0x666666 var n = size(self.pages) if n > 0 var p = int( (now_ms() - self.shown) / self.page_ms ) % n label = self.pages[p] col = settings.get("textColor") if col == nil col = 0xFFFFFF end elif self.error col = 0xFF4444 end scroll_text( 8, 6, width() - 8, label, col, self.opts ) end end return FlightRadar()