Devices & Components
CrowPanel 7.0" -HMI ESP32 Display 800x480
Software & Tools
Arduino IDE
Project description
Code
Code
cpp
...
1// Realtime Airplane Radar UI - STEP 4 2// CrowPanel 7.0" ESP32-S3 800x480 RGB 3// Real aircraft data + background API update + sweep reveal effect 4// by micemk May, 2026 5 6#define LGFX_USE_V1 7#include <LovyanGFX.hpp> 8#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp> 9#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp> 10#include <WiFi.h> 11#include <HTTPClient.h> 12#include <WiFiClientSecure.h> 13#include <ArduinoJson.h> 14#include <Wire.h> 15#include <math.h> 16#include "time.h" 17 18const char* ssid = "*************"; 19const char* password = "*************"; 20 21// Center location 22const double HOME_LAT = 41.1171; // for Ohrid 23const double HOME_LON = 20.8016; // for Ohrid 24 25// Radar range 26const float RADAR_RANGE_KM = 100.0; 27const int API_RADIUS_NM = 54; // about 100 km 28 29class LGFX : public lgfx::LGFX_Device { 30public: 31 lgfx::Bus_RGB _bus; 32 lgfx::Panel_RGB _panel; 33 lgfx::Light_PWM _light; 34 35 LGFX(void) { 36 { 37 auto cfg = _panel.config(); 38 cfg.memory_width = 800; 39 cfg.memory_height = 480; 40 cfg.panel_width = 800; 41 cfg.panel_height = 480; 42 _panel.config(cfg); 43 } 44 45 { 46 auto cfg = _bus.config(); 47 cfg.panel = &_panel; 48 49 cfg.pin_d0 = GPIO_NUM_15; 50 cfg.pin_d1 = GPIO_NUM_7; 51 cfg.pin_d2 = GPIO_NUM_6; 52 cfg.pin_d3 = GPIO_NUM_5; 53 cfg.pin_d4 = GPIO_NUM_4; 54 cfg.pin_d5 = GPIO_NUM_9; 55 cfg.pin_d6 = GPIO_NUM_46; 56 cfg.pin_d7 = GPIO_NUM_3; 57 cfg.pin_d8 = GPIO_NUM_8; 58 cfg.pin_d9 = GPIO_NUM_16; 59 cfg.pin_d10 = GPIO_NUM_1; 60 cfg.pin_d11 = GPIO_NUM_14; 61 cfg.pin_d12 = GPIO_NUM_21; 62 cfg.pin_d13 = GPIO_NUM_47; 63 cfg.pin_d14 = GPIO_NUM_48; 64 cfg.pin_d15 = GPIO_NUM_45; 65 66 cfg.pin_henable = GPIO_NUM_41; 67 cfg.pin_vsync = GPIO_NUM_40; 68 cfg.pin_hsync = GPIO_NUM_39; 69 cfg.pin_pclk = GPIO_NUM_0; 70 71 cfg.freq_write = 12000000; 72 73 cfg.hsync_front_porch = 40; 74 cfg.hsync_pulse_width = 48; 75 cfg.hsync_back_porch = 40; 76 cfg.vsync_front_porch = 1; 77 cfg.vsync_pulse_width = 31; 78 cfg.vsync_back_porch = 13; 79 80 cfg.pclk_active_neg = 1; 81 cfg.de_idle_high = 0; 82 cfg.pclk_idle_high = 0; 83 84 _bus.config(cfg); 85 _panel.setBus(&_bus); 86 } 87 88 { 89 auto cfg = _light.config(); 90 cfg.pin_bl = GPIO_NUM_2; 91 cfg.freq = 44100; 92 cfg.pwm_channel = 7; 93 _light.config(cfg); 94 _panel.setLight(&_light); 95 } 96 97 setPanel(&_panel); 98 } 99}; 100 101LGFX lcd; 102LGFX_Sprite radarCanvas(&lcd); 103 104// ---------- LAYOUT ---------- 105const int MARGIN = 5; 106 107const int RADAR_X = MARGIN; 108const int RADAR_Y = MARGIN; 109const int RADAR_W = 470; 110const int RADAR_H = 470; 111 112const int INFO_X = 485; 113const int INFO_Y = MARGIN; 114const int INFO_W = 310; 115const int INFO_H = 470; 116 117const int CX = 240; 118const int CY = 240; 119 120const float X_CORR = 0.93; 121const int R = 212; 122 123const int OUTER_GAP = 3; 124const int OUTER_RING_THICKNESS = 3; 125 126// ---------- SWEEP ---------- 127float sweepAngle = 0; 128unsigned long lastSweepUpdate = 0; 129 130const int TRAIL_WIDTH = 60; 131const int SWEEP_SPEED = 25; 132 133unsigned long lastClockUpdate = 0; 134 135// ---------- COLORS ---------- 136uint16_t greenBright; 137uint16_t green; 138uint16_t greenDim; 139uint16_t greenDark; 140uint16_t panelBg; 141 142// ---------- AIRCRAFT ---------- 143struct AircraftPoint { 144 bool valid; 145 float distanceKm; 146 float bearingDeg; 147 int altitudeFt; 148 float speedKt; 149 float trackDeg; 150 char flight[12]; 151 char category[5]; 152 char typeCode[8]; 153}; 154 155const int MAX_PLANES = 40; 156 157AircraftPoint planes[MAX_PLANES]; 158AircraftPoint pendingPlanes[MAX_PLANES]; 159 160int planeCount = 0; 161int pendingPlaneCount = 0; 162 163bool apiOk = false; 164bool pendingApiOk = false; 165bool fetchInProgress = false; 166bool pendingDataReady = false; 167 168// ---------- HELPERS ---------- 169int rxCorr(int radius) { 170 return (int)(radius * X_CORR); 171} 172 173double deg2radD(double deg) { 174 return deg * M_PI / 180.0; 175} 176 177double rad2degD(double rad) { 178 return rad * 180.0 / M_PI; 179} 180 181float angleDiffBehindSweep(float sweep, float targetBearing) { 182 float d = sweep - targetBearing; 183 while (d < 0) d += 360; 184 while (d >= 360) d -= 360; 185 return d; 186} 187 188float distanceKm(double lat1, double lon1, double lat2, double lon2) { 189 const double Rearth = 6371.0; 190 191 double dLat = deg2radD(lat2 - lat1); 192 double dLon = deg2radD(lon2 - lon1); 193 194 double a = 195 sin(dLat / 2) * sin(dLat / 2) + 196 cos(deg2radD(lat1)) * cos(deg2radD(lat2)) * 197 sin(dLon / 2) * sin(dLon / 2); 198 199 double c = 2 * atan2(sqrt(a), sqrt(1 - a)); 200 return Rearth * c; 201} 202 203float bearingDeg(double lat1, double lon1, double lat2, double lon2) { 204 double y = sin(deg2radD(lon2 - lon1)) * cos(deg2radD(lat2)); 205 double x = 206 cos(deg2radD(lat1)) * sin(deg2radD(lat2)) - 207 sin(deg2radD(lat1)) * cos(deg2radD(lat2)) * cos(deg2radD(lon2 - lon1)); 208 209 double brng = rad2degD(atan2(y, x)); 210 if (brng < 0) brng += 360.0; 211 return brng; 212} 213 214void canvasCorrectedCircle(int cx, int cy, int r, uint16_t col) { 215 int lastX = cx + rxCorr(r); 216 int lastY = cy; 217 218 for (int a = 1; a <= 360; a++) { 219 float rad = a * DEG_TO_RAD; 220 int x = cx + cos(rad) * rxCorr(r); 221 int y = cy + sin(rad) * r; 222 223 radarCanvas.drawLine(lastX, lastY, x, y, col); 224 225 lastX = x; 226 lastY = y; 227 } 228} 229 230void canvasCorrectedRadialLine(int angleDeg, int r1, int r2, uint16_t col) { 231 float rad = (angleDeg - 90) * DEG_TO_RAD; 232 233 int x1 = CX + cos(rad) * rxCorr(r1); 234 int y1 = CY + sin(rad) * r1; 235 int x2 = CX + cos(rad) * rxCorr(r2); 236 int y2 = CY + sin(rad) * r2; 237 238 radarCanvas.drawLine(x1, y1, x2, y2, col); 239} 240 241// ---------- API ---------- 242void fetchAircraftData() { 243 if (WiFi.status() != WL_CONNECTED) { 244 pendingApiOk = false; 245 pendingPlaneCount = 0; 246 pendingDataReady = true; 247 return; 248 } 249 250 String url = "https://api.airplanes.live/v2/point/"; 251 url += String(HOME_LAT, 4); 252 url += "/"; 253 url += String(HOME_LON, 4); 254 url += "/"; 255 url += String(API_RADIUS_NM); 256 257 WiFiClientSecure client; 258 client.setInsecure(); 259 260 HTTPClient http; 261 http.setTimeout(12000); 262 http.addHeader("User-Agent", "ESP32-AirRadar/1.0"); 263 264 if (!http.begin(client, url)) { 265 pendingApiOk = false; 266 pendingDataReady = true; 267 return; 268 } 269 270 int code = http.GET(); 271 272 if (code != 200) { 273 Serial.printf("API HTTP error: %d\n", code); 274 http.end(); 275 pendingApiOk = false; 276 pendingDataReady = true; 277 return; 278 } 279 280 String payload = http.getString(); 281 http.end(); 282 283 DynamicJsonDocument doc(65536); 284 DeserializationError err = deserializeJson(doc, payload); 285 286 if (err) { 287 Serial.print("JSON error: "); 288 Serial.println(err.c_str()); 289 pendingApiOk = false; 290 pendingDataReady = true; 291 return; 292 } 293 294 AircraftPoint tempPlanes[MAX_PLANES]; 295 int tempCount = 0; 296 297 JsonArray arr = doc["ac"].as<JsonArray>(); 298 if (arr.isNull()) { 299 arr = doc["aircraft"].as<JsonArray>(); 300 } 301 302 for (JsonObject ac : arr) { 303 if (tempCount >= MAX_PLANES) break; 304 305 if (!ac["lat"].is<float>() || !ac["lon"].is<float>()) continue; 306 307 double lat = ac["lat"]; 308 double lon = ac["lon"]; 309 310 float d = distanceKm(HOME_LAT, HOME_LON, lat, lon); 311 if (d > RADAR_RANGE_KM) continue; 312 313 AircraftPoint &p = tempPlanes[tempCount]; 314 315 p.valid = true; 316 p.distanceKm = d; 317 p.bearingDeg = bearingDeg(HOME_LAT, HOME_LON, lat, lon); 318 319 if (ac["alt_baro"].is<int>()) p.altitudeFt = ac["alt_baro"]; 320 else if (ac["alt_geom"].is<int>()) p.altitudeFt = ac["alt_geom"]; 321 else p.altitudeFt = -1; 322 323 p.speedKt = ac["gs"] | 0.0; 324 p.trackDeg = ac["track"] | 0.0; 325 326 const char* fl = ac["flight"] | ""; 327 strncpy(p.flight, fl, sizeof(p.flight) - 1); 328 p.flight[sizeof(p.flight) - 1] = '\0'; 329 330 const char* cat = ac["category"] | ""; 331 strncpy(p.category, cat, sizeof(p.category) - 1); 332 p.category[sizeof(p.category) - 1] = '\0'; 333 334 const char* typ = ac["t"] | ""; 335 strncpy(p.typeCode, typ, sizeof(p.typeCode) - 1); 336 p.typeCode[sizeof(p.typeCode) - 1] = '\0'; 337 338 tempCount++; 339 } 340 341 noInterrupts(); 342 343 pendingPlaneCount = tempCount; 344 345 for (int i = 0; i < tempCount; i++) { 346 pendingPlanes[i] = tempPlanes[i]; 347 } 348 349 pendingApiOk = true; 350 pendingDataReady = true; 351 352 interrupts(); 353 354 Serial.printf("Background aircraft found: %d\n", tempCount); 355} 356 357void aircraftFetchTask(void *parameter) { 358 fetchAircraftData(); 359 fetchInProgress = false; 360 vTaskDelete(NULL); 361} 362 363void startAircraftFetchBackground() { 364 if (fetchInProgress) return; 365 366 fetchInProgress = true; 367 368 xTaskCreatePinnedToCore( 369 aircraftFetchTask, 370 "aircraftFetchTask", 371 12000, 372 NULL, 373 1, 374 NULL, 375 0 376 ); 377} 378 379void applyPendingAircraftData() { 380 if (!pendingDataReady) return; 381 382 noInterrupts(); 383 384 planeCount = pendingPlaneCount; 385 386 for (int i = 0; i < planeCount; i++) { 387 planes[i] = pendingPlanes[i]; 388 } 389 390 apiOk = pendingApiOk; 391 pendingDataReady = false; 392 393 interrupts(); 394 395 Serial.printf("Applied aircraft data: %d\n", planeCount); 396} 397 398// ---------- RADAR CANVAS DRAW ---------- 399void drawGridToCanvas() { 400 radarCanvas.fillSprite(TFT_BLACK); 401 402 for (int d = -240; d <= 240; d += 40) { 403 int x = CX + rxCorr(d); 404 if (x >= RADAR_X && x <= RADAR_X + RADAR_W) { 405 radarCanvas.drawFastVLine(x, RADAR_Y, RADAR_H, greenDark); 406 } 407 } 408 409 for (int d = -240; d <= 240; d += 40) { 410 int y = CY + d; 411 if (y >= RADAR_Y && y <= RADAR_Y + RADAR_H) { 412 radarCanvas.drawFastHLine(RADAR_X, y, RADAR_W, greenDark); 413 } 414 } 415 416 radarCanvas.drawRect(RADAR_X, RADAR_Y, RADAR_W, RADAR_H, green); 417} 418 419void drawSweepToCanvas() { 420 for (float i = TRAIL_WIDTH; i >= 0; i -= 0.5) { 421 float a = sweepAngle - i; 422 if (a < 0) a += 360; 423 424 float intensity = (float)(TRAIL_WIDTH - i) / TRAIL_WIDTH; 425 426 uint8_t g = 6 + intensity * 120; 427 uint16_t col = radarCanvas.color565(0, g, 0); 428 429 float rad = (a - 90) * DEG_TO_RAD; 430 431 int x = CX + cos(rad) * rxCorr(R); 432 int y = CY + sin(rad) * R; 433 434 radarCanvas.drawLine(CX, CY, x, y, col); 435 } 436 437 float rad = (sweepAngle - 90) * DEG_TO_RAD; 438 int sx = CX + cos(rad) * rxCorr(R); 439 int sy = CY + sin(rad) * R; 440 441 radarCanvas.drawLine(CX, CY, sx, sy, greenBright); 442 radarCanvas.drawLine(CX + 1, CY, sx + 1, sy, greenBright); 443} 444 445void drawRadarCirclesToCanvas() { 446 canvasCorrectedCircle(CX, CY, 35, green); 447 canvasCorrectedCircle(CX, CY, 70, green); 448 canvasCorrectedCircle(CX, CY, 110, green); 449 canvasCorrectedCircle(CX, CY, 150, green); 450 canvasCorrectedCircle(CX, CY, 190, green); 451 canvasCorrectedCircle(CX, CY, R, greenBright); 452 453 for (int i = 0; i < OUTER_RING_THICKNESS; i++) { 454 canvasCorrectedCircle(CX, CY, R + OUTER_GAP + 1 + i, greenBright); 455 } 456} 457 458void drawCrossLinesToCanvas() { 459 radarCanvas.drawFastHLine(CX - rxCorr(R), CY, rxCorr(R) * 2, greenBright); 460 radarCanvas.drawFastVLine(CX, CY - R, R * 2, greenBright); 461 462 for (int d = 20; d < R; d += 40) { 463 radarCanvas.drawFastVLine(CX + rxCorr(d), CY - 7, 14, greenBright); 464 radarCanvas.drawFastVLine(CX - rxCorr(d), CY - 7, 14, greenBright); 465 466 radarCanvas.drawFastHLine(CX - 7, CY + d, 14, greenBright); 467 radarCanvas.drawFastHLine(CX - 7, CY - d, 14, greenBright); 468 } 469 470 for (int a = 30; a < 360; a += 30) { 471 if (a == 90 || a == 180 || a == 270) continue; 472 canvasCorrectedRadialLine(a, 0, R, greenDim); 473 } 474} 475 476void drawDistanceLabelsToCanvas() { 477 radarCanvas.setTextDatum(middle_center); 478 radarCanvas.setTextSize(1); 479 radarCanvas.setTextColor(greenBright, TFT_BLACK); 480 481 int radii[] = {35, 70, 110, 150, 190}; 482 int km[] = {20, 40, 60, 80, 100}; 483 484 const int LABEL_OFFSET = 9; 485 486 for (int i = 0; i < 5; i++) { 487 radarCanvas.drawString(String(km[i]) + "km", CX + 18, CY - radii[i] - LABEL_OFFSET); 488 radarCanvas.drawString(String(km[i]) + "km", CX + 18, CY + radii[i] + LABEL_OFFSET); 489 } 490 491 radarCanvas.drawString("0", CX + 15, CY + 12); 492} 493 494void drawAngleScaleToCanvas() { 495 for (int a = 0; a < 360; a++) { 496 int len = 4; 497 if (a % 10 == 0) len = 13; 498 else if (a % 5 == 0) len = 8; 499 500 canvasCorrectedRadialLine(a, R - len, R, greenBright); 501 } 502 503 radarCanvas.setTextColor(greenBright, TFT_BLACK); 504 radarCanvas.setTextSize(2); 505 radarCanvas.setTextDatum(middle_center); 506 507 for (int a = 0; a < 360; a += 30) { 508 float rad = (a - 90) * DEG_TO_RAD; 509 510 int tx = CX + cos(rad) * rxCorr(R - 28); 511 int ty = CY + sin(rad) * (R - 28); 512 513 radarCanvas.drawNumber(a, tx, ty); 514 } 515} 516 517void drawAircraftTargetsToCanvas() { 518 for (int i = 0; i < planeCount; i++) { 519 if (!planes[i].valid) continue; 520 521 float ageAngle = angleDiffBehindSweep(sweepAngle, planes[i].bearingDeg); 522 523 if (ageAngle > 260) continue; 524 525 float fade = 1.0; 526 if (ageAngle > 200) { 527 fade = 1.0 - ((ageAngle - 200.0) / 60.0); 528 if (fade < 0.20) fade = 0.20; 529 } 530 531 float rr = (planes[i].distanceKm / RADAR_RANGE_KM) * R; 532 float rad = (planes[i].bearingDeg - 90) * DEG_TO_RAD; 533 534 int x = CX + cos(rad) * rxCorr(rr); 535 int y = CY + sin(rad) * rr; 536 537 uint8_t r = 0; 538 uint8_t g = 255 * fade; 539 uint8_t b = 35 * fade; 540 541 if (planes[i].altitudeFt >= 30000) { 542 r = 0; g = 180 * fade; b = 255 * fade; 543 } else if (planes[i].altitudeFt >= 10000) { 544 r = 0; g = 255 * fade; b = 35 * fade; 545 } else if (planes[i].altitudeFt >= 0) { 546 r = 255 * fade; g = 220 * fade; b = 0; 547 } else { 548 r = 255 * fade; g = 40 * fade; b = 40 * fade; 549 } 550 551 uint16_t col = radarCanvas.color565(r, g, b); 552 553 int size = 4; 554 if (planes[i].altitudeFt > 30000) size = 5; 555 if (planes[i].distanceKm < 20) size = 6; 556 557 radarCanvas.fillCircle(x, y, size, col); 558 radarCanvas.drawCircle(x, y, size + 2, greenDim); 559 560 if (planes[i].trackDeg >= 0) { 561 float tr = (planes[i].trackDeg - 90) * DEG_TO_RAD; 562 int x2 = x + cos(tr) * 12; 563 int y2 = y + sin(tr) * 12; 564 radarCanvas.drawLine(x, y, x2, y2, col); 565 } 566 567 // Callsign label only when sweep has just passed the aircraft 568 if (ageAngle >= 0 && ageAngle < 35) { 569 radarCanvas.setTextDatum(top_left); 570 radarCanvas.setTextSize(1); 571 radarCanvas.setTextColor(TFT_WHITE, TFT_BLACK); 572 573 String label = String(planes[i].flight); 574 label.trim(); 575 if (label.length() == 0) label = "UNKNOWN"; 576 577 radarCanvas.drawString(label, x + 9, y - 12); 578 579 // if (planes[i].altitudeFt >= 0) { 580 // radarCanvas.setTextColor(greenBright, TFT_BLACK); 581 // radarCanvas.drawString(String(planes[i].altitudeFt) + "ft", x + 9, y + 2); 582 // } 583 } 584 } 585 586 // Top-left status - larger 587 radarCanvas.setTextDatum(top_left); 588 radarCanvas.setTextSize(2); 589 590 if (apiOk) { 591 radarCanvas.setTextColor(green, TFT_BLACK); 592 radarCanvas.drawString("REAL ADS-B", 12, 10); 593 594 radarCanvas.setTextColor(greenBright, TFT_BLACK); 595 radarCanvas.setTextSize(2); 596 radarCanvas.drawString("AC: " + String(planeCount), 12, 32); 597 } else { 598 radarCanvas.setTextColor(radarCanvas.color565(255, 80, 80), TFT_BLACK); 599 radarCanvas.drawString("NO API", 14, 14); 600 } 601 602 if (fetchInProgress) { 603 radarCanvas.setTextSize(2); 604 radarCanvas.setTextColor(greenDim, TFT_BLACK); 605 radarCanvas.drawString("UPDATING", 10, 54); 606 } 607 608 // 3D-like compass bottom-left 609 // 3D-like compass bottom-left - smaller 610 int ccx = 50; 611 int ccy = 427; 612 uint16_t compassBright = greenBright; 613 uint16_t compassDim = greenDim; 614 uint16_t compassDark = greenDark; 615 616 radarCanvas.setTextDatum(middle_center); 617 618 // smaller outer ring 619 radarCanvas.drawCircle(ccx, ccy, 17, compassDim); 620 radarCanvas.drawCircle(ccx, ccy, 18, compassDark); 621 622 // N triangle - bright 623 radarCanvas.fillTriangle(ccx, ccy - 16, ccx - 5, ccy - 4, ccx + 5, ccy - 4, compassBright); 624 radarCanvas.drawTriangle(ccx, ccy - 16, ccx - 5, ccy - 4, ccx + 5, ccy - 4, TFT_WHITE); 625 626 // S triangle 627 radarCanvas.fillTriangle(ccx, ccy + 16, ccx - 5, ccy + 4, ccx + 5, ccy + 4, compassDim); 628 629 // W triangle 630 radarCanvas.fillTriangle(ccx - 16, ccy, ccx - 4, ccy - 5, ccx - 4, ccy + 5, compassDim); 631 632 // E triangle 633 radarCanvas.fillTriangle(ccx + 16, ccy, ccx + 4, ccy - 5, ccx + 4, ccy + 5, compassDim); 634 635 // center point 636 radarCanvas.fillCircle(ccx, ccy, 2, compassBright); 637 radarCanvas.drawCircle(ccx, ccy, 4, compassDim); 638 639 // smaller letters 640 radarCanvas.setTextSize(1); 641 radarCanvas.setTextColor(compassBright, TFT_BLACK); 642 radarCanvas.drawString("N", ccx, ccy - 29); 643 644 radarCanvas.setTextColor(compassDim, TFT_BLACK); 645 radarCanvas.drawString("S", ccx, ccy + 29); 646 radarCanvas.drawString("W", ccx - 29, ccy - 3); 647 radarCanvas.drawString("E", ccx + 29, ccy - 3); 648} 649 650void drawRangeBarsToCanvas() { 651 // 5 bars: 0-20, 20-40, 40-60, 60-80, 80-100 km 652 const int bins = 5; 653 int counts[bins] = {0}; 654 655 for (int i = 0; i < planeCount; i++) { 656 if (!planes[i].valid) continue; 657 658 int bin = (int)(planes[i].distanceKm / 20.0); 659 if (bin < 0) bin = 0; 660 if (bin >= bins) bin = bins - 1; 661 662 counts[bin]++; 663 } 664 665 const int startX = 395; 666 const int baseY = 62; 667 const int barW = 10; 668 const int barGap = 14; 669 670 const int tickH = 3; 671 const int tickGap = 2; 672 const int maxTicks = 8; 673 674 radarCanvas.setTextDatum(top_left); 675 radarCanvas.setTextSize(1); 676 radarCanvas.setTextColor(green, TFT_BLACK); 677 radarCanvas.drawString("RANGE AC", startX - 0, 12); 678 679 for (int b = 0; b < bins; b++) { 680 int x = startX + b * barGap; 681 int c = counts[b]; 682 if (c > maxTicks) c = maxTicks; 683 684 for (int t = 0; t < c; t++) { 685 int y = baseY - t * (tickH + tickGap); 686 radarCanvas.fillRect(x, y, barW, tickH, greenBright); 687 } 688 689 // faint zero baseline 690 radarCanvas.drawFastHLine(x, baseY + 7, barW, greenDark); 691 } 692 693 // labels only 20 / 60 / 100 694 radarCanvas.setTextColor(green, TFT_BLACK); 695 radarCanvas.drawString("20", startX - 1, baseY + 14); 696 radarCanvas.drawString("60", startX + 2 * barGap - 1, baseY + 14); 697 radarCanvas.drawString("100", startX + 4 * barGap - 4, baseY + 14); 698} 699 700void drawSweepHeadingIndicatorToCanvas() { 701 int hx = 430; 702 int hy = 420; 703 704 int angleInt = (int)sweepAngle; 705 if (angleInt < 0) angleInt += 360; 706 if (angleInt >= 360) angleInt -= 360; 707 708 radarCanvas.setTextDatum(middle_center); 709 710 // small dial 711 radarCanvas.drawCircle(hx, hy, 28, greenDark); 712 radarCanvas.drawCircle(hx, hy, 29, greenDim); 713 714 // rotating pointer triangle 715 float a = (sweepAngle - 90) * DEG_TO_RAD; 716 717 int tipX = hx + cos(a) * 23; 718 int tipY = hy + sin(a) * 23; 719 720 float leftA = a + 2.45; 721 float rightA = a - 2.45; 722 723 int leftX = hx + cos(leftA) * 10; 724 int leftY = hy + sin(leftA) * 10; 725 int rightX = hx + cos(rightA) * 10; 726 int rightY = hy + sin(rightA) * 10; 727 728 radarCanvas.fillTriangle(tipX, tipY, leftX, leftY, rightX, rightY, greenBright); 729 radarCanvas.drawLine(hx, hy, tipX, tipY, TFT_WHITE); 730 731 radarCanvas.fillCircle(hx, hy, 3, greenBright); 732 733 // degree number 734 radarCanvas.setTextSize(2); 735 radarCanvas.setTextColor(greenBright, TFT_BLACK); 736 737 char buf[8]; 738 sprintf(buf, "%03d", angleInt); 739 radarCanvas.drawString(buf, hx, hy + 42); 740 741 radarCanvas.setTextSize(1); 742 radarCanvas.setTextColor(green, TFT_BLACK); 743 radarCanvas.drawString("SWEEP", hx, hy - 38); 744} 745 746void drawRadarFrameToCanvas() { 747 drawGridToCanvas(); 748 drawSweepToCanvas(); 749 drawRadarCirclesToCanvas(); 750 drawCrossLinesToCanvas(); 751 drawDistanceLabelsToCanvas(); 752 drawAngleScaleToCanvas(); 753 drawAircraftTargetsToCanvas(); 754 drawRangeBarsToCanvas(); 755 drawSweepHeadingIndicatorToCanvas(); 756} 757 758void drawTimePanel() { 759 int boxX = INFO_X + 18; 760 int boxY = INFO_Y + 392; 761 int boxW = INFO_W - 36; 762 int boxH = 68; 763 764 lcd.fillRect(boxX, boxY, boxW, boxH, TFT_BLACK); 765 766 lcd.drawFastHLine(INFO_X + 10, boxY - 17, INFO_W - 20, greenDark); 767 768 lcd.setTextDatum(top_left); 769 lcd.setTextSize(2); 770 lcd.setTextColor(greenBright, TFT_BLACK); 771 lcd.drawString("TIME:", boxX+110, boxY-7); 772 773 struct tm timeinfo; 774 if (getLocalTime(&timeinfo)) { 775 char timeStr[12]; 776 sprintf(timeStr, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); 777 778 lcd.setTextSize(5); 779 lcd.setTextColor(TFT_YELLOW, TFT_BLACK); 780 lcd.drawString(timeStr, boxX+20, boxY + 22); 781 } else { 782 lcd.setTextSize(4); 783 lcd.setTextColor(TFT_YELLOW, TFT_BLACK); 784 lcd.drawString("--:--:--", boxX+20, boxY + 22); 785 } 786} 787 788void drawLocationPanel() { 789 int boxX = INFO_X + 18; 790 int boxY = INFO_Y + 305; // како што кажа 791 int boxW = INFO_W - 36; 792 int boxH = 70; 793 794 lcd.fillRect(boxX, boxY, boxW, boxH, TFT_BLACK); 795 796 // 🔹 хоризонтална линија над TIME (ова е болдираната линија што ти фали) 797 lcd.drawFastHLine(INFO_X + 10, boxY - 10, INFO_W - 20, greenDark); 798 799 // OHRID - центрирано 800 lcd.setTextDatum(top_center); 801 lcd.setTextSize(2); 802 lcd.setTextColor(greenBright, TFT_BLACK); 803 lcd.drawString("OHRID", boxX + boxW / 2, boxY); 804 805 // Lat / Long во ЕДЕН ред 806 lcd.setTextDatum(top_center); 807 lcd.setTextSize(2); 808 lcd.setTextColor(TFT_YELLOW, TFT_BLACK); 809 810 String coord = String(HOME_LAT, 4) + " / " + String(HOME_LON, 4); 811 lcd.drawString(coord, boxX + boxW / 2, boxY + 32); 812} 813 814void drawDetectedAircraftPanel() { 815 int topY = INFO_Y + 10; // почнува најгоре 816 int bottomY = INFO_Y + 295; // до пред OHRID 817 int boxX = INFO_X + 10; 818 int boxW = INFO_W - 20; 819 int boxH = bottomY - topY; 820 821 lcd.fillRect(boxX, topY, boxW, boxH, TFT_BLACK); 822 823 lcd.setTextDatum(top_center); 824 lcd.setTextSize(2); 825 lcd.setTextColor(greenBright, TFT_BLACK); 826 lcd.drawString("DETECTED AIRCRAFTS", INFO_X + INFO_W / 2, topY + 2); 827 828 const int MAX_ROWS = 10; 829 int idx[MAX_ROWS]; 830 for (int i = 0; i < MAX_ROWS; i++) idx[i] = -1; 831 832 // најди 8 најблиски авиони 833 for (int i = 0; i < planeCount; i++) { 834 if (!planes[i].valid) continue; 835 836 for (int j = 0; j < MAX_ROWS; j++) { 837 if (idx[j] == -1 || planes[i].distanceKm < planes[idx[j]].distanceKm) { 838 for (int k = MAX_ROWS - 1; k > j; k--) idx[k] = idx[k - 1]; 839 idx[j] = i; 840 break; 841 } 842 } 843 } 844 845 lcd.setTextDatum(top_left); 846 lcd.setTextSize(1); 847 848 int y = topY + 34; 849 850 for (int row = 0; row < MAX_ROWS; row++) { 851 if (idx[row] == -1) break; 852 853 AircraftPoint &p = planes[idx[row]]; 854 855 String call = String(p.flight); 856 call.trim(); 857 if (call.length() == 0) call = "UNKNOWN"; 858 859 uint16_t dotCol = greenBright; 860 if (p.altitudeFt >= 30000) dotCol = lcd.color565(0, 180, 255); 861 else if (p.altitudeFt >= 10000) dotCol = lcd.color565(0, 255, 35); 862 else if (p.altitudeFt >= 0) dotCol = lcd.color565(255, 220, 0); 863 else dotCol = lcd.color565(255, 40, 40); 864 865 lcd.setTextColor(TFT_YELLOW, TFT_BLACK); 866 lcd.drawString(call.substring(0, 7), boxX + 2, y); 867 868 lcd.setTextColor(TFT_WHITE, TFT_BLACK); 869 lcd.drawString(String((int)p.distanceKm) + "km", boxX + 76, y); 870 871 if (p.altitudeFt >= 0) lcd.drawString(String(p.altitudeFt) + "ft", boxX + 118, y); 872 else lcd.drawString("---ft", boxX + 118, y); 873 874 lcd.drawString(String((int)p.speedKt) + "kt", boxX + 188, y); 875 876 lcd.setTextColor(green, TFT_BLACK); 877 878 String typ = String(p.typeCode); 879typ.trim(); 880if (typ.length() == 0) typ = String(p.category); 881 882lcd.drawString(typ.substring(0, 5), boxX + 235, y); 883 884 lcd.fillCircle(boxX + 272, y + 5, 4, dotCol); 885 886 lcd.drawFastHLine(boxX, y + 17, boxW, greenDark); 887 888 y += 26; 889 } 890 891 if (planeCount == 0) { 892 lcd.setTextDatum(top_center); 893 lcd.setTextSize(1); 894 lcd.setTextColor(greenDim, TFT_BLACK); 895 lcd.drawString("NO AIRCRAFT IN RANGE", INFO_X + INFO_W / 2, topY + 52); 896 } 897} 898// ---------- RIGHT PANEL ---------- 899 900void drawRightInfoPanel() { 901 lcd.fillRect(INFO_X, INFO_Y, INFO_W, INFO_H, TFT_BLACK); 902 lcd.drawRect(INFO_X, INFO_Y, INFO_W, INFO_H, green); 903 904 lcd.setTextDatum(top_left); 905 906// lcd.setTextSize(2); 907 // lcd.setTextColor(greenBright, TFT_BLACK); 908 // lcd.drawString("AIR TRAFFIC RADAR", INFO_X + 18, INFO_Y + 18); 909 910// lcd.drawFastHLine(INFO_X + 10, INFO_Y + 55, INFO_W - 20, greenDark); 911 912 drawDetectedAircraftPanel(); 913 drawLocationPanel(); 914 drawTimePanel(); 915} 916 917// ---------- SETUP ---------- 918void setup() { 919 Serial.begin(115200); 920 921 Wire.begin(19, 20); 922 Wire.beginTransmission(0x18); 923 Wire.write(0x01); 924 Wire.write(0x3B); 925 Wire.endTransmission(); 926 927 lcd.init(); 928 lcd.setBrightness(180); 929 930 greenBright = lcd.color565(0, 255, 35); 931 green = lcd.color565(0, 180, 25); 932 greenDim = lcd.color565(0, 90, 18); 933 greenDark = lcd.color565(0, 45, 10); 934 panelBg = lcd.color565(0, 20, 0); 935 936 radarCanvas.setPsram(true); 937 radarCanvas.setColorDepth(16); 938 939 if (!radarCanvas.createSprite(480, 480)) { 940 Serial.println("Radar sprite create failed!"); 941 } 942 943 lcd.fillScreen(TFT_BLACK); 944 945 lcd.setTextColor(TFT_WHITE, TFT_BLACK); 946 lcd.setTextSize(2); 947 lcd.setTextDatum(middle_center); 948 lcd.drawString("Connecting WiFi...", 240, 240); 949 950 WiFi.begin(ssid, password); 951 952 unsigned long wifiStart = millis(); 953 while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 15000) { 954 delay(300); 955 } 956 957 lcd.fillScreen(TFT_BLACK); 958 959 if (WiFi.status() == WL_CONNECTED) { 960 Serial.println("WiFi connected"); 961 962 configTzTime("CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", "time.nist.gov"); 963 964 fetchAircraftData(); 965 applyPendingAircraftData(); 966 startAircraftFetchBackground(); 967 968 } else { 969 Serial.println("WiFi failed"); 970 apiOk = false; 971 } 972 973 drawRadarFrameToCanvas(); 974 radarCanvas.pushSprite(0, 0); 975 976 drawRightInfoPanel(); 977} 978 979// ---------- LOOP ---------- 980void loop() { 981 if (millis() - lastSweepUpdate > SWEEP_SPEED) { 982 lastSweepUpdate = millis(); 983 984 drawRadarFrameToCanvas(); 985 radarCanvas.pushSprite(0, 0); 986 987 sweepAngle += 2; 988 989 if (sweepAngle >= 360) { 990 sweepAngle -= 360; 991 992 applyPendingAircraftData(); 993 drawDetectedAircraftPanel(); 994 startAircraftFetchBackground(); 995 } 996 } 997 998 if (millis() - lastClockUpdate > 1000) { 999 lastClockUpdate = millis(); 1000 drawTimePanel(); 1001 } 1002}
Comments
Only logged in users can leave comments