forked from brokosz/PlaneRadar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflight_data.h
More file actions
280 lines (239 loc) · 10.9 KB
/
flight_data.h
File metadata and controls
280 lines (239 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#pragma once
#include <Arduino.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <math.h>
#include "settings.h"
#define MAX_FLIGHTS 40
struct Flight {
char flight_num[12]; // "AA2341" or callsign
char origin[5]; // IATA "ORD" (--- if unavailable)
char dest[5]; // IATA "LAX" (--- if unavailable)
char aircraft[8]; // ICAO type "B738"
char registration[12]; // "N12345"
char callsign[12]; // same as flight_num
float lat;
float lon;
int altitude_ft;
int speed_kts;
int heading;
int vert_speed_fpm;
float distance_mi;
};
Flight g_flights[MAX_FLIGHTS];
int g_flight_count = 0;
unsigned long g_last_fetch_ms = 0;
// ── Haversine distance (miles) ───────────────────────────────────────────────
static float haversine_mi(float lat1, float lon1, float lat2, float lon2) {
const float R = 3958.8f;
float dlat = (lat2 - lat1) * (float)M_PI / 180.0f;
float dlon = (lon2 - lon1) * (float)M_PI / 180.0f;
float a = sinf(dlat / 2) * sinf(dlat / 2)
+ cosf(lat1 * (float)M_PI / 180.0f)
* cosf(lat2 * (float)M_PI / 180.0f)
* sinf(dlon / 2) * sinf(dlon / 2);
return R * 2.0f * atan2f(sqrtf(a), sqrtf(1.0f - a));
}
static int cmp_distance(const void *a, const void *b) {
float da = ((const Flight *)a)->distance_mi;
float db = ((const Flight *)b)->distance_mi;
return (da > db) - (da < db);
}
// ── ArduinoJson filter for readsb "ac" objects ───────────────────────────────
// Only the fields we actually use are kept; everything else is discarded
// during streaming. Reduces peak heap by ~70% on large responses.
static void make_readsb_filter(JsonDocument &f) {
f["ac"][0]["hex"] = true;
f["ac"][0]["flight"] = true;
f["ac"][0]["r"] = true;
f["ac"][0]["t"] = true;
f["ac"][0]["lat"] = true;
f["ac"][0]["lon"] = true;
f["ac"][0]["alt_baro"] = true;
f["ac"][0]["gs"] = true;
f["ac"][0]["track"] = true;
f["ac"][0]["baro_rate"] = true;
}
// ── Shared parser for readsb-format "ac" arrays (airplanes.live + adsb.fi) ───
static int parse_readsb_ac(JsonArray ac, float ref_lat, float ref_lon) {
int count = 0;
for (JsonObject a : ac) {
if (count >= MAX_FLIGHTS) break;
if (!a["alt_baro"].is<int>()) continue; // "ground" string → skip
int alt = a["alt_baro"].as<int>();
if (alt <= 0) continue;
if (a["lat"].isNull() || a["lon"].isNull()) continue;
Flight &f = g_flights[count];
f.lat = a["lat"].as<float>();
f.lon = a["lon"].as<float>();
f.altitude_ft = alt;
f.speed_kts = a["gs"].as<int>();
f.heading = a["track"].as<int>();
f.vert_speed_fpm = a["baro_rate"].as<int>();
const char *cs = a["flight"] | "";
strncpy(f.flight_num, cs, sizeof(f.flight_num) - 1);
f.flight_num[sizeof(f.flight_num) - 1] = '\0';
for (int i = strlen(f.flight_num) - 1; i >= 0 && f.flight_num[i] == ' '; i--)
f.flight_num[i] = '\0';
if (strlen(f.flight_num) == 0)
strncpy(f.flight_num, a["hex"] | "N/A", sizeof(f.flight_num) - 1);
// Commercial-only filter: airline callsigns start with 2-3 letters then digits
if (settings.commercial_only) {
const char *fn = f.flight_num;
int letters = 0;
while (fn[letters] && isalpha((unsigned char)fn[letters])) letters++;
bool has_digits = letters >= 2 && letters <= 3 && isdigit((unsigned char)fn[letters]);
if (!has_digits) continue;
}
strncpy(f.callsign, f.flight_num, sizeof(f.callsign) - 1);
strncpy(f.aircraft, a["t"] | "---", sizeof(f.aircraft) - 1);
strncpy(f.registration, a["r"] | "---", sizeof(f.registration) - 1);
strncpy(f.origin, "---", sizeof(f.origin) - 1);
strncpy(f.dest, "---", sizeof(f.dest) - 1);
f.distance_mi = haversine_mi(ref_lat, ref_lon, f.lat, f.lon);
count++;
}
return count;
}
// ── Shared HTTPS fetch + filtered JSON parse for readsb-format APIs ──────────
// Uses getString() so the full response is buffered before parsing.
// On ESP32-S3 with PSRAM, large String allocs (>16 KB) go to external
// SRAM, decoupling the TLS decrypt step from the ArduinoJson parse step
// and reducing peak internal-SRAM demand.
static bool fetch_readsb_url(const char *tag, const char *url,
float ref_lat, float ref_lon) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, url);
http.setTimeout(20000);
int code = http.GET();
Serial.printf("[%s] HTTP %d\n", tag, code);
if (code != 200) { http.end(); return false; }
// getString() fully buffers the response; large payloads land in PSRAM
String payload = http.getString();
http.end();
Serial.printf("[%s] %u bytes received\n", tag, payload.length());
if (payload.isEmpty()) return false;
JsonDocument filter;
make_readsb_filter(filter);
JsonDocument doc;
DeserializationError err = deserializeJson(doc, payload,
DeserializationOption::Filter(filter));
if (err) { Serial.printf("[%s] JSON err: %s\n", tag, err.c_str()); return false; }
JsonArray ac = doc["ac"].as<JsonArray>();
if (ac.isNull()) { Serial.printf("[%s] no ac array\n", tag); return false; }
g_flight_count = parse_readsb_ac(ac, ref_lat, ref_lon);
Serial.printf("[%s] Parsed %d airborne flights\n", tag, g_flight_count);
return g_flight_count > 0;
}
// ── airplanes.live — free community ADS-B aggregator ─────────────────────────
static bool fetch_airplaneslive(float lat, float lon, float radius_mi) {
char url[120];
snprintf(url, sizeof(url),
"https://api.airplanes.live/v2/point/%.4f/%.4f/%.0f",
lat, lon, radius_mi * 0.868976f);
return fetch_readsb_url("APL", url, lat, lon);
}
// ── adsb.fi open data — second community ADS-B source ────────────────────────
static bool fetch_adsbfi(float lat, float lon, float radius_mi) {
char url[120];
snprintf(url, sizeof(url),
"https://opendata.adsb.fi/api/v3/lat/%.4f/lon/%.4f/dist/%.0f",
lat, lon, radius_mi * 0.868976f);
return fetch_readsb_url("ADSBFI", url, lat, lon);
}
// ── OpenSky Network fallback ──────────────────────────────────────────────────
// Anonymous tier: 100 requests/day. Used only when primary fails.
static bool fetch_opensky(float lat, float lon, float radius_mi) {
float dlat = radius_mi / 69.0f;
float dlon = radius_mi / (69.0f * cosf(lat * (float)M_PI / 180.0f));
char url[280];
snprintf(url, sizeof(url),
"https://opensky-network.org/api/states/all"
"?lamin=%.4f&lomin=%.4f&lamax=%.4f&lomax=%.4f",
lat - dlat, lon - dlon,
lat + dlat, lon + dlon);
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, url);
http.setTimeout(15000);
int code = http.GET();
Serial.printf("[OSN] HTTP %d\n", code);
if (code != 200) { http.end(); return false; }
JsonDocument doc;
DeserializationError err = deserializeJson(doc, http.getStream());
http.end();
if (err) {
Serial.printf("[OSN] JSON err: %s\n", err.c_str());
return false;
}
if (!doc["states"].is<JsonArray>()) {
Serial.println("[OSN] states is null or missing (rate-limited?)");
return false;
}
g_flight_count = 0;
for (JsonArray state : doc["states"].as<JsonArray>()) {
if (g_flight_count >= MAX_FLIGHTS) break;
if (state[5].isNull() || state[6].isNull()) continue;
if (state[8].as<bool>()) continue; // on ground
Flight &f = g_flights[g_flight_count];
f.lon = state[5].as<float>();
f.lat = state[6].as<float>();
f.altitude_ft = (int)(state[7].as<float>() * 3.28084f);
f.speed_kts = (int)(state[9].as<float>() * 1.94384f);
f.heading = (int)state[10].as<float>();
f.vert_speed_fpm = (int)((state[11].isNull() ? 0.0f : state[11].as<float>()) * 196.85f);
const char *cs = state[1] | "N/A";
strncpy(f.callsign, cs, sizeof(f.callsign) - 1);
strncpy(f.flight_num, cs, sizeof(f.flight_num) - 1);
for (int i = strlen(f.flight_num) - 1; i >= 0 && f.flight_num[i] == ' '; i--)
f.flight_num[i] = '\0';
strncpy(f.origin, "---", sizeof(f.origin) - 1);
strncpy(f.dest, "---", sizeof(f.dest) - 1);
strncpy(f.aircraft, "---", sizeof(f.aircraft) - 1);
strncpy(f.registration,"---", sizeof(f.registration) - 1);
f.distance_mi = haversine_mi(lat, lon, f.lat, f.lon);
g_flight_count++;
}
Serial.printf("[OSN] Parsed %d flights\n", g_flight_count);
return g_flight_count > 0;
}
// ── Public fetch — airplanes.live → adsb.fi → OpenSky ───────────────────────
inline bool flights_fetch() {
float lat = settings.latitude;
float lon = settings.longitude;
float r = (float)settings.radius_miles;
if (lat == 0.0f && lon == 0.0f) {
Serial.println("[FETCH] No location — skipping");
return false;
}
bool ok = fetch_airplaneslive(lat, lon, r);
if (!ok) {
Serial.println("[FETCH] airplanes.live failed — trying adsb.fi");
ok = fetch_adsbfi(lat, lon, r);
}
if (!ok) {
Serial.println("[FETCH] adsb.fi failed — trying OpenSky");
ok = fetch_opensky(lat, lon, r);
}
if (ok && g_flight_count > 0) {
qsort(g_flights, g_flight_count, sizeof(Flight), cmp_distance);
g_last_fetch_ms = millis();
}
return ok;
}
// ── Display helpers ───────────────────────────────────────────────────────────
// ASCII-only trend indicator — LV_SYMBOL_* glyphs not in LVGL Arduino font build
inline const char *trend_arrow(const Flight &f) {
if (f.vert_speed_fpm > 200) return "+";
if (f.vert_speed_fpm < -200) return "-";
return "=";
}
inline uint32_t trend_color(const Flight &f) {
if (f.vert_speed_fpm > 200) return 0x66bb6a; // green – climbing
if (f.vert_speed_fpm < -200) return 0xef5350; // red – descending
return 0x888888; // grey – level
}