Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The same portal runs on the setup AP and on the device’s LAN IP while connecte
| **Latitude / Longitude** | Radar center and ADS-B query position (defaults in `config.h` until set) |
| **Display distances in miles** | Ring scale label in **mi** instead of **km** (e.g. `6mi` vs `10km`) |
| **Show airport runways** | Major-airport runway overlay on the radar (off to hide) |
| **Show flight destination** | Adds a green destination line (e.g. `to LHR`) to aircraft tags (on by default) |

After a reset, the device reboots and shows the setup screen immediately (no “Connecting” loop on stale credentials).

Expand Down Expand Up @@ -82,6 +83,17 @@ Preset and miles/km choice persist across reboot (`planeradar` NVS namespace).

As range decreases (or aircraft approach), targets move inward; beyond-ring dots become full symbols when they cross the outer ring.

### Flight destination

ADS-B doesn't broadcast where a flight is going, so the firmware looks up each aircraft's callsign on [adsbdb.com](https://www.adsbdb.com/) (`https://api.adsbdb.com/v0/callsign/<callsign>`, free, no key) and adds a fourth, green line to its tag: `to Amsterdam`. City names are shown when they fit (up to `kRouteMaxCityChars`, 15) and are folded to plain ASCII (`Zürich` → `Zurich`) because the tag font has no accented letters; otherwise it falls back to the airport code (`to AMS`; ICAO if there is no IATA).

- Only aircraft **inside the outer ring** (the ones that get a tag) are looked up, nearest first, at most `kRouteLookupsPerCycle` (2) per ADS-B cycle, on the same background task as the ADS-B fetch.
- Results are cached (48 callsigns; found 2 h, unknown 30 min, errors 30 s), so a plane is looked up once, not every refresh.
- Not every callsign has a route (general aviation, military, charters, or flights missing from the database); those just show no destination line.
- In `config.h`: `kRouteUseCityNames = false` shows airport codes instead of city names, and `kRouteShowOrigin = true` adds a `from Thessaloniki` line above the destination.
- The serial log prints the full route and heap headroom, e.g. `route: TRA91T Thessaloniki to Amsterdam (SKG>AMS) [heap free 112000, lowest 98000]`. On a Super Mini, if "lowest" drops under ~30000, tell the maintainer: the extra TLS session is tight on the C3.
- Callsigns are sent to adsbdb.com. Untick **Show flight destination** in the setup portal to turn lookups off.

### ADS-B

- Source: `https://opendata.adsb.fi/api/v3/`
Expand All @@ -101,6 +113,7 @@ Edit **`include/config.h`** for hardware and behavior:
| Display SPI | pins, `kDisplayInvert`, `kDisplayRgbOrder`, `kDisplaySpiWriteHz` |
| Default location | `kDefaultRadarLat`, `kDefaultRadarLon` (until portal overrides) |
| ADS-B | `kAdsbFetchIntervalMs`, `kAdsbShowGroundAircraft` |
| Destination | `kRouteLookupsPerCycle`, `kRouteUseCityNames`, `kRouteMaxCityChars`, `kRouteShowOrigin` |

Range presets: `include/ui/radar_range.h` (`kRangePresets`).

Expand All @@ -125,6 +138,8 @@ include/
wifi_setup.h
radar_location.h
adsb_client.h
route_client.h
ascii_fold.h
data/
ui_font.vlw — embedded smooth UI font (Noto Sans Bold)
scripts/
Expand Down
5 changes: 5 additions & 0 deletions include/config.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <cstddef>
#include <cstdint>

#include <driver/gpio.h>
Expand Down Expand Up @@ -56,6 +57,10 @@ constexpr unsigned long kAdsbFetchIntervalMs = 5000;
constexpr unsigned long kRadarRedrawIntervalMs = 250;
/** Legacy scale unused — fetch uses radar::fetchRadiusKm() to screen edge. */
constexpr float kAdsbFetchRadiusScale = 1.0f;
constexpr size_t kRouteLookupsPerCycle = 2;
constexpr bool kRouteUseCityNames = true;
constexpr size_t kRouteMaxCityChars = 15;
constexpr bool kRouteShowOrigin = false;
/** false = hide aircraft with alt_baro "ground"; true = show them too. */
constexpr bool kAdsbShowGroundAircraft = false;

Expand Down
1 change: 1 addition & 0 deletions include/services/adsb_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct Aircraft {
* Added to the elapsed time when dead-reckoning so the drawn position
* reflects the estimated current location, not the already-stale fix. */
uint32_t pos_age_ms;
bool has_flight_callsign;
char callsign[9];
char type[5];
char alt[12];
Expand Down
9 changes: 9 additions & 0 deletions include/services/ascii_fold.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <cstddef>

namespace services {

bool foldToAscii(const char* in, char* out, size_t out_len);

} // namespace services
24 changes: 24 additions & 0 deletions include/services/route_client.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <cstddef>

namespace services::route {

struct Airport {
char code[5];
char city[24];
};

struct Route {
Airport origin;
Airport destination;
};

void init();

bool lookup(const char* callsign, Route* out);

size_t fetchPending(double center_lat, double center_lon, float max_dist_km,
size_t max_lookups);

} // namespace services::route
2 changes: 2 additions & 0 deletions include/ui/radar_range.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ float fetchRadiusKm();

bool useMiles();
bool showRunways();
bool showDestination();
/** WiFi portal checkbox: "T" = miles, otherwise km. */
void saveMilesFromPortal(const char* checkbox_value);
void saveRunwaysFromPortal(const char* checkbox_value);
void saveDestinationFromPortal(const char* checkbox_value);
void formatRing3Label(char* buf, size_t len, float ring3_km, bool use_miles);
void formatCurrentRing3Label(char* buf, size_t len);
/** Reset distance units to km (e.g. with WiFi credential wipe). */
Expand Down
4 changes: 4 additions & 0 deletions include/ui/radar_theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ constexpr uint8_t kTagTypeB = 0;
constexpr uint8_t kTagAltR = 90;
constexpr uint8_t kTagAltG = 200;
constexpr uint8_t kTagAltB = 255;
constexpr uint8_t kTagRouteR = 110;
constexpr uint8_t kTagRouteG = 230;
constexpr uint8_t kTagRouteB = 110;
constexpr uint8_t kRunwayR = 56;
constexpr uint8_t kRunwayG = 150;
constexpr uint8_t kRunwayB = 170;
Expand All @@ -97,6 +100,7 @@ extern uint16_t kColorAircraft;
extern uint16_t kColorTrackVector;
extern uint16_t kColorTagType;
extern uint16_t kColorTagAltitude;
extern uint16_t kColorTagRoute;
extern uint16_t kColorRunway;
extern uint16_t kColorRunwayLabel;

Expand Down
7 changes: 7 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "hardware/display.h"
#include "services/adsb_client.h"
#include "services/radar_location.h"
#include "services/route_client.h"
#include "services/wifi_setup.h"
#include "ui/radar_display.h"
#include "ui/radar_range.h"
Expand Down Expand Up @@ -59,6 +60,11 @@ void adsbFetchTask(void*) {
services::adsb::fetchUpdate(services::location::lat(),
services::location::lon(),
ui::radar::fetchRadiusKm());
if (ui::radar::showDestination()) {
services::route::fetchPending(
services::location::lat(), services::location::lon(),
ui::radar::rangeCurrent().outer_km, config::kRouteLookupsPerCycle);
}
}
vTaskDelay(pdMS_TO_TICKS(config::kAdsbFetchIntervalMs));
}
Expand All @@ -80,6 +86,7 @@ void setup() {
services::location::init();
ui::radar::rangeInit();
services::adsb::init();
services::route::init();

if (wifiSetupConnect()) {
showRadarIfConnected();
Expand Down
1 change: 1 addition & 0 deletions src/services/adsb_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ void formatAltitudeTag(const JsonObject& plane, char* out, size_t out_len) {

void fillTagFields(Aircraft* ac, const JsonObject& plane) {
copyJsonStringTrimmed(plane, "flight", ac->callsign, sizeof(ac->callsign));
ac->has_flight_callsign = ac->callsign[0] != '\0';
if (ac->callsign[0] == '\0') {
copyJsonStringTrimmed(plane, "hex", ac->callsign, sizeof(ac->callsign));
}
Expand Down
83 changes: 83 additions & 0 deletions src/services/ascii_fold.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#include "services/ascii_fold.h"

#include <cstdint>

namespace services {

namespace {

constexpr uint32_t kFoldFirst = 0xC0;
constexpr uint32_t kFoldLast = 0x17F;
const char* const kFoldTable[] = {
"A", "A", "A", "A", "A", "A", "AE", "C",
"E", "E", "E", "E", "I", "I", "I", "I",
"D", "N", "O", "O", "O", "O", "O", "",
"O", "U", "U", "U", "U", "Y", "Th", "ss",
"a", "a", "a", "a", "a", "a", "ae", "c",
"e", "e", "e", "e", "i", "i", "i", "i",
"d", "n", "o", "o", "o", "o", "o", "",
"o", "u", "u", "u", "u", "y", "th", "y",
"A", "a", "A", "a", "A", "a", "C", "c",
"C", "c", "C", "c", "C", "c", "D", "d",
"D", "d", "E", "e", "E", "e", "E", "e",
"E", "e", "E", "e", "G", "g", "G", "g",
"G", "g", "G", "g", "H", "h", "H", "h",
"I", "i", "I", "i", "I", "i", "I", "i",
"I", "i", "IJ", "ij", "J", "j", "K", "k",
"k", "L", "l", "L", "l", "L", "l", "L",
"l", "L", "l", "N", "n", "N", "n", "N",
"n", "n", "N", "n", "O", "o", "O", "o",
"O", "o", "OE", "oe", "R", "r", "R", "r",
"R", "r", "S", "s", "S", "s", "S", "s",
"S", "s", "T", "t", "T", "t", "T", "t",
"U", "u", "U", "u", "U", "u", "U", "u",
"U", "u", "U", "u", "W", "w", "Y", "y",
"Y", "Z", "z", "Z", "z", "Z", "z", "s",
};

} // namespace

bool foldToAscii(const char* in, char* out, size_t out_len) {
if (out == nullptr || out_len == 0) {
return false;
}
out[0] = '\0';
if (in == nullptr) {
return false;
}

size_t o = 0;
const unsigned char* p = reinterpret_cast<const unsigned char*>(in);
while (*p != '\0') {
uint32_t cp = 0;
if (*p < 0x80) {
cp = *p++;
} else if ((*p & 0xE0) == 0xC0 && (p[1] & 0xC0) == 0x80) {
cp = (static_cast<uint32_t>(*p & 0x1F) << 6) | (p[1] & 0x3F);
p += 2;
} else {
out[0] = '\0';
return false;
}

char single[2] = {0, 0};
const char* rep = nullptr;
if (cp >= 0x20 && cp < 0x7F) {
single[0] = static_cast<char>(cp);
rep = single;
} else if (cp >= kFoldFirst && cp <= kFoldLast) {
rep = kFoldTable[cp - kFoldFirst];
}
if (rep == nullptr || rep[0] == '\0') {
out[0] = '\0';
return false;
}
for (; *rep != '\0' && o + 1 < out_len; ++rep) {
out[o++] = *rep;
}
}
out[o] = '\0';
return o > 0;
}

} // namespace services
Loading