diff --git a/src/services/adsb_client.cpp b/src/services/adsb_client.cpp index 84be21bb7..011177944 100644 --- a/src/services/adsb_client.cpp +++ b/src/services/adsb_client.cpp @@ -46,43 +46,66 @@ int performGetWithPoll(HTTPClient& http) { return HTTPC_ERROR_READ_TIMEOUT; } -bool readResponseBodyWithPoll(HTTPClient& http, String& payload) { - WiFiClient* stream = http.getStreamPtr(); - if (stream == nullptr) { - return false; - } - - const int content_length = http.getSize(); - if (content_length > 0) { - payload.reserve(static_cast(content_length + 1)); +/** + * Stream wrapper that keeps the rest of the firmware alive during blocking + * HTTP reads and bounds the whole body transfer with a single deadline. + * + * The response is parsed straight off this stream rather than buffered: a + * busy sector easily exceeds 20 kB, and the heap is too fragmented to hold + * that in one contiguous allocation. + */ +class PollingStream : public Stream { + public: + PollingStream(WiFiClient* source, unsigned long deadline) + : source_(source), deadline_(deadline) {} + + int available() override { return source_->available(); } + int peek() override { return waitForData() ? source_->peek() : -1; } + void flush() override {} + size_t write(uint8_t) override { return 0; } + + int read() override { return waitForData() ? source_->read() : -1; } + + size_t readBytes(char* buffer, size_t length) override { + size_t got = 0; + while (got < length && waitForData()) { + const int n = source_->read(reinterpret_cast(buffer + got), + length - got); + if (n > 0) { + got += static_cast(n); + } + } + return got; } - uint8_t buffer[512]; - const unsigned long deadline = millis() + kRequestTimeoutMs; - while (millis() < deadline) { - pollNetwork(); - const int available = stream->available(); - if (available > 0) { - const int to_read = - available > static_cast(sizeof(buffer)) ? static_cast(sizeof(buffer)) - : available; - const int read_bytes = stream->readBytes(buffer, to_read); - if (read_bytes > 0) { - payload.concat(reinterpret_cast(buffer), - static_cast(read_bytes)); + private: + /** Blocks until a byte is ready, the peer hangs up, or the deadline hits. */ + bool waitForData() { + while (source_->available() <= 0) { + if (millis() >= deadline_) { + return false; } + if (!source_->connected()) { + return source_->available() > 0; + } + pollNetwork(); + delay(1); } - if (content_length > 0 && - static_cast(payload.length()) >= content_length) { - break; - } - if (!http.connected() && stream->available() <= 0) { - break; - } - delay(1); + return true; } - return payload.length() > 0; + WiFiClient* source_; + unsigned long deadline_; +}; + +/** Keeps only the fields the radar actually renders. */ +void buildPlaneFilter(JsonDocument& filter) { + static const char* const kKeys[] = { + "lat", "lon", "true_heading", "mag_heading", "track", "dir", "gs", + "tas", "ias", "alt_baro", "alt_geom", "flight", "hex", "t"}; + for (const char* key : kKeys) { + filter[key] = true; + } } float kmToNauticalMiles(float km) { return km / kKmPerNm; } @@ -232,46 +255,65 @@ bool fetchUpdate(double center_lat, double center_lon, float fetch_radius_km) { return false; } - String payload; - if (!readResponseBodyWithPoll(http, payload)) { - Serial.println("adsb: empty response"); + WiFiClient* source = http.getStreamPtr(); + if (source == nullptr) { + Serial.println("adsb: no response stream"); http.end(); return false; } - http.end(); - JsonDocument doc; - const DeserializationError err = deserializeJson(doc, payload); - if (err) { - Serial.printf("adsb: JSON parse error: %s\n", err.c_str()); - return false; - } + PollingStream stream(source, millis() + kRequestTimeoutMs); + stream.setTimeout(kRequestTimeoutMs); - JsonArray ac = doc["ac"].as(); - if (ac.isNull()) { + // Walk to the start of the "ac" array; two steps so `"ac" : [` also matches. + if (!stream.find("\"ac\"") || !stream.find("[")) { + Serial.println("adsb: no aircraft array in response"); + http.end(); s_aircraft_count = 0; return true; } + JsonDocument filter; + buildPlaneFilter(filter); + size_t n = 0; - for (JsonObject plane : ac) { - if (n >= kMaxAircraft) { - break; - } - if (!plane["lat"].is() || !plane["lon"].is()) { - continue; - } - if (isOnGround(plane) && !config::kAdsbShowGroundAircraft) { - continue; - } + bool ok = true; + if (stream.peek() != ']') { // guard against an empty "ac":[] array + do { + // One aircraft at a time: peak memory stays a few hundred bytes + // instead of the ~20 kB the full document would need. + JsonDocument plane_doc; + const DeserializationError err = deserializeJson( + plane_doc, stream, DeserializationOption::Filter(filter)); + if (err) { + Serial.printf("adsb: JSON parse error: %s\n", err.c_str()); + ok = false; + break; + } - s_aircraft[n].lat = plane["lat"].as(); - s_aircraft[n].lon = plane["lon"].as(); - s_aircraft[n].nose_deg = pickNoseHeading(plane); - s_aircraft[n].track_deg = pickTrackHeading(plane); - s_aircraft[n].gs_knots = pickGroundSpeed(plane); - fillTagFields(&s_aircraft[n], plane); - ++n; + JsonObject plane = plane_doc.as(); + if (n < kMaxAircraft && plane["lat"].is() && + plane["lon"].is() && + (config::kAdsbShowGroundAircraft || !isOnGround(plane))) { + s_aircraft[n].lat = plane["lat"].as(); + s_aircraft[n].lon = plane["lon"].as(); + s_aircraft[n].nose_deg = pickNoseHeading(plane); + s_aircraft[n].track_deg = pickTrackHeading(plane); + s_aircraft[n].gs_knots = pickGroundSpeed(plane); + fillTagFields(&s_aircraft[n], plane); + ++n; + } + + if (n >= kMaxAircraft) { + break; // enough to draw; drop the rest of the body + } + } while (stream.findUntil(",", "]")); + } + + http.end(); + + if (!ok && n == 0) { + return false; } s_aircraft_count = n; diff --git a/src/ui/radar_display.cpp b/src/ui/radar_display.cpp index b0d333997..7d07c83b1 100644 --- a/src/ui/radar_display.cpp +++ b/src/ui/radar_display.cpp @@ -15,8 +15,6 @@ #include "ui/radar_theme.h" #include "ui/runway_overlay.h" -namespace fonts = lgfx::v1::fonts; - namespace ui { namespace radar { @@ -203,8 +201,13 @@ constexpr float kKmPerDeg = 111.0f; void offsetKmFromCenter(float lat, float lon, float* dx_km, float* dy_km, float* dist_km) { - *dx_km = - static_cast(lon - services::location::lon()) * kKmPerDeg; + // A degree of longitude shrinks as cos(latitude); without this an east-west + // offset reads ~38% too far at 43°N, pushing aircraft outside the ring. + const float center_lat = static_cast(services::location::lat()); + const float lon_scale = cosf(center_lat * static_cast(M_PI) / 180.0f); + + *dx_km = static_cast(lon - services::location::lon()) * kKmPerDeg * + lon_scale; *dy_km = static_cast(lat - services::location::lat()) * kKmPerDeg; *dist_km = sqrtf((*dx_km) * (*dx_km) + (*dy_km) * (*dy_km)); diff --git a/src/ui/runway_overlay.cpp b/src/ui/runway_overlay.cpp index 8f4b63127..ee45cfa7c 100644 --- a/src/ui/runway_overlay.cpp +++ b/src/ui/runway_overlay.cpp @@ -11,8 +11,6 @@ #include "ui/radar_range.h" #include "ui/radar_theme.h" -namespace fonts = lgfx::v1::fonts; - namespace ui::runway { namespace { diff --git a/src/ui/status_screens.cpp b/src/ui/status_screens.cpp index c9be33fe8..af3713516 100644 --- a/src/ui/status_screens.cpp +++ b/src/ui/status_screens.cpp @@ -11,8 +11,6 @@ #include "hardware/display.h" #include "hardware/display_font.h" -namespace fonts = lgfx::v1::fonts; - namespace { constexpr int kLineGap = 6;