Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #include <ESP8266WiFi.h>
- #include <ESP8266WebServer.h>
- #include <ESP8266HTTPClient.h>
- #include <ArduinoJson.h>
- // WiFi credentials
- const char* ssid = "YOUR_SSID_HERE";
- const char* password = "YOUR_PASSWORD_HERE";
- // Web server on port 80
- ESP8266WebServer server(80);
- // Coordinates (Lon,Lat)
- const float LAT = ######; // Replace "######" with your LAT
- const float LON = ######; // Replace "######" with your LON
- int RADIUS = 2; // Modifiable (in nautical miles)
- // Update intervals
- const unsigned long UPDATE_INTERVAL = 8000; // 8 seconds for API update
- unsigned long lastUpdate = 0;
- unsigned long lastDataTime = 0;
- float seenTime = 0;
- float initialSeenTime = 0;
- // Aircraft data storage
- String currentCallsign = "";
- String currentType = "";
- String currentHex = "N/A";
- float currentAlt = 0;
- float currentSpeed = 0;
- float currentDirection = 0;
- float currentDistance = 0;
- bool hasAircraftData = false;
- // Aircraft counter
- unsigned long lastResetTime = 0;
- int aircraftCount = 0;
- String seenCallsigns[50]; //Max Count is 50 due to RAM limitations on the ESP
- int seenCallsignsIndex = 0;
- const unsigned long DAY_LENGTH = 86400000; // 24 hours
- // LED configuration
- const int LED_PIN = 2; // Built-in blue LED on most ESP8266 boards (GPIO2, D4)
- unsigned long lastFadeUpdate = 0;
- const unsigned long FADE_INTERVAL = 20; // Update fade every 20ms for smooth effect
- int fadeValue = 0; // Current PWM value (0-1023)
- bool fadeDirection = true; // true = increasing, false = decreasing
- // HTML for the web interface (Home Assistant style)
- const char* htmlPage = R"rawliteral(
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Aircraft Tracker</title>
- <style>
- body {
- background: #1c2526;
- color: #e0e0e0;
- font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- margin: 0;
- padding: 0.5rem;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- min-height: 100vh;
- box-sizing: border-box;
- }
- .card {
- background-color: #212121;
- border-radius: 0.5rem;
- padding: 1rem;
- width: 100%;
- max-width: 600px;
- min-width: 280px;
- box-sizing: border-box;
- box-shadow: 0 0.0625rem 0.1875rem rgba(0,0,0,0.3);
- position: relative; /* For absolute positioning of api-status */
- }
- .card-header {
- font-size: 1rem;
- color: #03a9f4;
- margin: 0 0 0.75rem 0;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
- .data-grid {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: 0.5rem;
- font-size: 0.875rem;
- }
- .data-label {
- color: #b0b0b0;
- font-weight: bold;
- padding-right: 0.5rem;
- }
- .data-value {
- color: #e0e0e0;
- }
- .settings {
- margin-top: 0.75rem;
- padding-top: 0.75rem;
- border-top: 0.0625rem solid #424242;
- }
- .settings-row {
- display: flex;
- gap: 0.5rem;
- flex-wrap: wrap;
- align-items: center;
- }
- label {
- color: #b0b0b0;
- font-size: 0.875rem;
- font-weight: bold;
- }
- input[type="number"] {
- background-color: #424242;
- color: #e0e0e0;
- border: 0.0625rem solid #616161;
- border-radius: 0.25rem;
- padding: 0.25rem;
- font-size: 0.875rem;
- width: 3.75rem;
- box-sizing: border-box;
- }
- button {
- background-color: #03a9f4;
- color: #fff;
- border: none;
- border-radius: 0.25rem;
- padding: 0.375rem 0.75rem;
- cursor: pointer;
- font-size: 0.875rem;
- transition: background-color 0.2s;
- }
- button:hover {
- background-color: #0288d1;
- }
- .track-button {
- background-color: #4caf50;
- width: 6.25rem;
- }
- .track-button:hover {
- background-color: #43a047;
- }
- .track-button:disabled {
- background-color: #616161;
- cursor: not-allowed;
- }
- #status {
- margin-top: 0.5rem;
- font-size: 0.75rem;
- color: #757575;
- text-align: center;
- }
- .loading-spinner {
- display: inline-block;
- width: 16px;
- height: 16px;
- border: 2px solid #03a9f4;
- border-radius: 50%;
- border-top-color: transparent;
- animation: spin 1s linear infinite;
- vertical-align: middle;
- }
- @keyframes spin {
- to {
- transform: rotate(360deg);
- }
- }
- .success-icon {
- color: #4caf50;
- vertical-align: middle;
- }
- .api-status {
- position: absolute;
- bottom: 1rem;
- right: 1rem;
- display: flex;
- align-items: center;
- gap: 0.25rem;
- font-size: 0.875rem;
- }
- .api-label {
- color: rgba(176, 176, 176, 0.5); /* Semi-transparent #b0b0b0 */
- font-weight: bold;
- }
- @media (max-width: 400px) {
- .card {
- padding: 0.75rem;
- min-width: 200px;
- }
- .card-header {
- font-size: 0.875rem;
- }
- .data-grid {
- font-size: 0.75rem;
- gap: 0.375rem;
- }
- input[type="number"], button {
- font-size: 0.75rem;
- padding: 0.2rem;
- }
- .track-button {
- width: 4.5rem;
- }
- input[type="number"] {
- width: 3rem;
- }
- .api-status {
- bottom: 0.75rem;
- right: 0.75rem;
- font-size: 0.75rem;
- }
- .loading-spinner {
- width: 12px;
- height: 12px;
- border-width: 2px;
- }
- }
- @media (min-width: 600px) {
- .card {
- padding: 1.25rem;
- }
- .card-header {
- font-size: 1.125rem;
- }
- .data-grid {
- font-size: 1rem;
- }
- input[type="number"], button {
- font-size: 1rem;
- padding: 0.375rem;
- }
- .api-status {
- bottom: 1.25rem;
- right: 1.25rem;
- }
- }
- </style>
- </head>
- <body>
- <div class="card">
- <div class="card-header">
- <span>Aircraft Tracker</span>
- <span id="status-icon"></span>
- </div>
- <div class="data-grid" id="aircraft-data">
- <span class="data-label">Callsign:</span>
- <span class="data-value" id="callsign">N/A</span>
- <span class="data-label">Type:</span>
- <span class="data-value" id="type">N/A</span>
- <span class="data-label">Altitude:</span>
- <span class="data-value" id="altitude">0 ft</span>
- <span class="data-label">Speed:</span>
- <span class="data-value" id="speed">0 mph</span>
- <span class="data-label">Direction:</span>
- <span class="data-value" id="direction">0°</span>
- <span class="data-label">Distance:</span>
- <span class="data-value" id="distance">0 NM</span>
- <span class="data-label">Last Seen:</span>
- <span class="data-value" id="seen">0 s ago</span>
- <span class="data-label">Today:</span>
- <span class="data-value" id="count">0</span>
- </div>
- <div class="settings">
- <form id="settings-form">
- <div class="settings-row">
- <label for="radius">Radius (nm):</label>
- <input type="number" id="radius" name="radius" min="1" max="50" value="%RADIUS%">
- <button type="submit">Apply</button>
- <button type="button" id="track-button" class="track-button" disabled>Track</button>
- </div>
- </form>
- <div id="status"></div>
- </div>
- <div class="api-status">
- <span class="api-label">API:</span>
- <span id="loading-status"></span>
- </div>
- </div>
- <script>
- const statusIcon = document.getElementById('status-icon');
- const loadingStatus = document.getElementById('loading-status');
- function setLoadingState(isLoading) {
- if (isLoading) {
- loadingStatus.innerHTML = '<span class="loading-spinner"></span>';
- } else {
- loadingStatus.innerHTML = '<span class="success-icon">✅</span>';
- }
- }
- function fetchData() {
- setLoadingState(true);
- fetch('/data')
- .then(response => response.json())
- .then(data => {
- setLoadingState(false);
- statusIcon.textContent = data.hasData ? '🛩️: Aircraft Detected' : '🚫 No Aircraft Detected';
- document.getElementById('callsign').textContent = data.callsign || 'N/A';
- document.getElementById('type').textContent = data.type || 'N/A';
- document.getElementById('altitude').textContent = data.altitude + ' ft';
- document.getElementById('speed').textContent = Math.round(data.speed * 1.15078) + ' mph';
- document.getElementById('direction').textContent = data.direction + '°';
- document.getElementById('distance').textContent = data.distance.toFixed(1) + ' NM';
- document.getElementById('seen').textContent = data.seen.toFixed(0) + ' s ago';
- document.getElementById('count').textContent = data.count;
- const trackButton = document.getElementById('track-button');
- if (data.hasData && data.hex && data.hex !== 'N/A') {
- trackButton.disabled = false;
- trackButton.onclick = () => {
- window.open(`https://globe.adsbexchange.com/?icao=${data.hex}`, '_blank');
- };
- } else {
- trackButton.disabled = true;
- trackButton.onclick = null;
- }
- })
- .catch(error => {
- setLoadingState(false);
- console.error('Error fetching data:', error);
- });
- }
- setInterval(fetchData, 2000);
- fetchData();
- document.getElementById('settings-form').addEventListener('submit', function(e) {
- e.preventDefault();
- const radius = document.getElementById('radius').value;
- fetch('/settings', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: `radius=${radius}`
- })
- .then(response => response.text())
- .then(text => {
- document.getElementById('status').textContent = 'Settings updated';
- setTimeout(() => document.getElementById('status').textContent = '', 3000);
- })
- .catch(error => {
- document.getElementById('status').textContent = 'Error updating settings';
- console.error('Error:', error);
- });
- });
- </script>
- </body>
- </html>
- )rawliteral";
- // Function to infer aircraft type
- String getAircraftType(String model, String callsign) {
- model.toUpperCase();
- callsign.toUpperCase();
- String callsignPrefix = callsign.length() >= 3 ? callsign.substring(0, 3) : callsign;
- if (model.startsWith("B737") || model.startsWith("B747") || model.startsWith("B757") ||
- model.startsWith("B767") || model.startsWith("B777") || model.startsWith("B787") ||
- model.startsWith("A220") || model.startsWith("A320") || model.startsWith("A330") ||
- model.startsWith("A350") || model.startsWith("A380") || model.startsWith("E170") ||
- model.startsWith("E190") || model.startsWith("CRJ") || model.startsWith("Q400") ||
- model.startsWith("ATR") || model.startsWith("B38M")) {
- return "Passenger";
- }
- if (callsignPrefix == "RYR" || callsignPrefix == "BAW" || callsignPrefix == "SHT" ||
- callsignPrefix == "EZY" || callsignPrefix == "EJU" || callsignPrefix == "EXS" ||
- callsignPrefix == "TOM" || callsignPrefix == "VIR" || callsignPrefix == "WUK" ||
- callsignPrefix == "LOG" || callsignPrefix == "AUR" || callsignPrefix == "UAL" ||
- callsignPrefix == "AAL" || callsignPrefix == "DAL" || callsignPrefix == "SWA" ||
- callsignPrefix == "QFA" || callsignPrefix == "ANZ" || callsignPrefix == "SIA" ||
- callsignPrefix == "JAL" || callsignPrefix == "CPA" || callsignPrefix == "UAE" ||
- callsignPrefix == "QTR" || callsignPrefix == "ETD" || callsignPrefix == "AFR" ||
- callsignPrefix == "DLH" || callsignPrefix == "EFW" || callsignPrefix == "KLM" ||
- callsignPrefix == "IBE" || callsignPrefix == "AZA" || callsignPrefix == "SWR" ||
- callsignPrefix == "THY" || callsignPrefix == "ETR" || callsignPrefix == "SAA" ||
- callsignPrefix == "KQA" || callsignPrefix == "TAM" || callsignPrefix == "ARG" ||
- callsignPrefix == "AVA" || callsignPrefix == "BCI" || callsignPrefix == "TAP") {
- return "Passenger";
- }
- if (model.startsWith("C130") || model.startsWith("C17") || model.startsWith("F16") ||
- model.startsWith("F18") || model.startsWith("F35") || model.startsWith("KC135") ||
- model.startsWith("H60") || model.startsWith("CH47") || model.startsWith("MQ9") ||
- model.startsWith("P8") || model.startsWith("A400") || model.startsWith("EUFI") ||
- callsign.startsWith("RRR") || callsign.startsWith("NAVY") || callsign.startsWith("ASCOT") ||
- callsign.startsWith("VANDAL") || callsign.startsWith("TARTAN") ||
- callsignPrefix == "RFR" || callsignPrefix == "NOH" || callsignPrefix == "KIN" ||
- callsignPrefix == "LCS" || callsignPrefix == "WAD" || callsignPrefix == "KRF" ||
- callsignPrefix == "KRH" || callsignPrefix == "NVY" || callsignPrefix == "AIO" ||
- callsignPrefix == "PAT" || callsignPrefix == "CNV" || callsignPrefix == "CGX" ||
- callsignPrefix == "RFF" || callsignPrefix == "CHD" || callsignPrefix == "TTF" ||
- callsignPrefix == "BRK") {
- return "Military";
- }
- if (model.startsWith("EC35") || model.startsWith("H135") || model.startsWith("EC45") ||
- model.startsWith("AS355") || callsign.startsWith("NPAS") || callsign.indexOf("POL") >= 0) {
- return "Police";
- }
- if (model.startsWith("BE20") || model.startsWith("PC12") || model.startsWith("EC45") ||
- model.startsWith("H145") || callsign.indexOf("MED") >= 0 || callsign.indexOf("AMB") >= 0 ||
- callsign.startsWith("HELI") || callsign.indexOf("AIRE") >= 0 ||
- callsignPrefix == "HLE" || callsignPrefix == "FLYDOC" || callsign.startsWith("MEDEVAC") ||
- callsign.startsWith("EVAC") || callsign.startsWith("HOSP") || callsignPrefix == "RED") {
- return "Ambulance";
- }
- if (model.startsWith("S92") || model.startsWith("AW189") || callsign.startsWith("G-") ||
- callsign.indexOf("RESCUE") >= 0 || callsign.indexOf("COAST") >= 0 ||
- callsign.startsWith("COASTGUARD") || callsignPrefix == "RES") {
- return "Coast Guard";
- }
- if (model.startsWith("B74F") || model.startsWith("B75F") || model.startsWith("AN12") ||
- model.startsWith("C130") || callsign.indexOf("FEDEX") >= 0 || callsign.indexOf("UPS") >= 0 ||
- callsign.indexOf("DHL") >= 0 || callsignPrefix == "GTI") {
- return "Cargo";
- }
- if (model.startsWith("GLF") || model.startsWith("CL60") || model.startsWith("C25") ||
- model.startsWith("LJ") || model.startsWith("F2TH") || model.startsWith("FA7X")) {
- return "Private Jet";
- }
- if (model.startsWith("C172") || model.startsWith("C152") || model.startsWith("P28A") ||
- model.startsWith("SR22") || model.startsWith("DA40") || model.startsWith("PA46")) {
- return "General";
- }
- return "Unknown";
- }
- void setup() {
- // Initialize LED pin
- pinMode(LED_PIN, OUTPUT);
- analogWriteRange(1023); // Set PWM range to 1023
- digitalWrite(LED_PIN, HIGH); // Ensure LED is off (active-low)
- Serial.begin(115200);
- Serial.println("Setup started...");
- // Connect to WiFi
- Serial.println("Connecting to WiFi...");
- WiFi.begin(ssid, password);
- int attempts = 0;
- while (WiFi.status() != WL_CONNECTED && attempts < 20) {
- delay(500);
- Serial.print(".");
- attempts++;
- }
- if (WiFi.status() == WL_CONNECTED) {
- Serial.println("\nWiFi connected");
- Serial.print("IP Address: ");
- Serial.println(WiFi.localIP());
- } else {
- Serial.println("\nWiFi failed, restarting...");
- delay(1000);
- ESP.restart();
- }
- // Setup web server routes
- server.on("/", HTTP_GET, []() {
- String page = htmlPage;
- page.replace("%RADIUS%", String(RADIUS));
- page.replace("%STATUS_ICON%", hasAircraftData ? "🛫" : "—");
- server.send(200, "text/html", page);
- });
- server.on("/data", HTTP_GET, []() {
- DynamicJsonDocument doc(256);
- doc["hasData"] = hasAircraftData;
- doc["callsign"] = currentCallsign;
- doc["type"] = currentType;
- doc["altitude"] = currentAlt;
- doc["speed"] = currentSpeed;
- doc["direction"] = currentDirection;
- doc["distance"] = currentDistance;
- doc["seen"] = seenTime;
- doc["count"] = aircraftCount;
- doc["hex"] = currentHex;
- String json;
- serializeJson(doc, json);
- server.send(200, "application/json", json);
- });
- server.on("/settings", HTTP_POST, []() {
- String response = "Settings updated";
- if (server.hasArg("radius")) {
- int newRadius = server.arg("radius").toInt();
- if (newRadius >= 1 && newRadius <= 50) {
- RADIUS = newRadius;
- Serial.println("Radius updated to: " + String(RADIUS));
- } else {
- response = "Invalid radius value";
- Serial.println("Invalid radius: " + server.arg("radius"));
- }
- }
- server.send(200, "text/plain", response);
- });
- server.begin();
- Serial.println("Web server started");
- // Initialize reset time
- lastResetTime = millis();
- }
- void updateMainDisplay() {
- if (WiFi.status() != WL_CONNECTED) {
- hasAircraftData = false;
- return;
- }
- WiFiClientSecure client;
- client.setInsecure();
- HTTPClient http;
- Serial.println("Fetching API data...");
- char url[128];
- snprintf(url, sizeof(url), "https://api.adsb.lol/v2/lat/%.4f/lon/%.4f/dist/%d", LAT, LON, RADIUS);
- Serial.print("URL: ");
- Serial.println(url);
- if (http.begin(client, url)) {
- int httpCode = http.GET();
- Serial.print("HTTP Code: ");
- Serial.println(httpCode);
- if (httpCode == HTTP_CODE_OK) {
- WiFiClient *stream = http.getStreamPtr();
- StaticJsonDocument<200> filter;
- filter["ac"][0]["flight"] = true;
- filter["ac"][0]["alt_baro"] = true;
- filter["ac"][0]["gs"] = true;
- filter["ac"][0]["track"] = true;
- filter["ac"][0]["lat"] = true;
- filter["ac"][0]["lon"] = true;
- filter["ac"][0]["seen"] = true;
- filter["ac"][0]["t"] = true;
- filter["ac"][0]["hex"] = true;
- DynamicJsonDocument doc(3072);
- DeserializationError error = deserializeJson(doc, *stream, DeserializationOption::Filter(filter));
- if (error) {
- Serial.println(F("JSON parse error: ") + String(error.c_str()));
- hasAircraftData = false;
- } else {
- JsonArray aircraftList = doc["ac"];
- if (aircraftList.size() > 0) {
- float min_distance = 9999.0;
- JsonObject closest_aircraft;
- for (JsonObject aircraft : aircraftList) {
- float aircraft_lat = aircraft["lat"] | 0.0;
- float aircraft_lon = aircraft["lon"] | 0.0;
- float delta_lat = aircraft_lat - LAT;
- float delta_lon = (aircraft_lon - LON) * cos(LAT * PI / 180.0);
- float distance = sqrt(delta_lat * delta_lat + delta_lon * delta_lon) * 60.0;
- if (distance < min_distance) {
- min_distance = distance;
- closest_aircraft = aircraft;
- }
- }
- String newCallsign = closest_aircraft["flight"] | "N/A";
- newCallsign.trim();
- bool isUnique = true;
- for (int i = 0; i < seenCallsignsIndex; i++) {
- if (seenCallsigns[i] == newCallsign) {
- isUnique = false;
- break;
- }
- }
- if (isUnique && newCallsign != "N/A" && seenCallsignsIndex < 50) {
- seenCallsigns[seenCallsignsIndex++] = newCallsign;
- aircraftCount++;
- Serial.print("New aircraft detected. Total today: ");
- Serial.println(aircraftCount);
- }
- currentCallsign = newCallsign;
- String aircraftModel = closest_aircraft["t"] | "Unknown";
- currentType = getAircraftType(aircraftModel, currentCallsign);
- currentAlt = closest_aircraft["alt_baro"] | 0;
- currentSpeed = closest_aircraft["gs"] | 0.0;
- currentDirection = closest_aircraft["track"] | 0.0;
- currentDistance = min_distance;
- float apiSeenTime = closest_aircraft["seen"] | 0.0;
- currentHex = closest_aircraft["hex"] | "N/A";
- lastDataTime = millis();
- initialSeenTime = apiSeenTime;
- seenTime = apiSeenTime;
- hasAircraftData = true;
- Serial.println("Aircraft data updated");
- Serial.print("Free Heap: ");
- Serial.println(ESP.getFreeHeap());
- } else {
- Serial.println("No aircraft found");
- hasAircraftData = false;
- currentHex = "N/A";
- }
- }
- } else {
- hasAircraftData = false;
- currentHex = "N/A";
- }
- http.end();
- } else {
- hasAircraftData = false;
- currentHex = "N/A";
- }
- }
- void updateLED() {
- if (hasAircraftData) {
- // Fade in and out
- unsigned long currentTime = millis();
- if (currentTime - lastFadeUpdate >= FADE_INTERVAL) {
- if (fadeDirection) {
- fadeValue += 10; // Increase brightness
- if (fadeValue >= 1023) {
- fadeValue = 1023;
- fadeDirection = false;
- }
- } else {
- fadeValue -= 10; // Decrease brightness
- if (fadeValue <= 0) {
- fadeValue = 0;
- fadeDirection = true;
- }
- }
- // Apply PWM (invert for active-low LED)
- analogWrite(LED_PIN, 1023 - fadeValue);
- lastFadeUpdate = currentTime;
- }
- } else {
- // Turn LED off
- fadeValue = 0;
- digitalWrite(LED_PIN, HIGH); // Active-low, so HIGH is off
- }
- }
- void loop() {
- server.handleClient();
- unsigned long currentTime = millis();
- // Reset counter if 24 hours have passed
- if (currentTime - lastResetTime >= DAY_LENGTH) {
- aircraftCount = 0;
- seenCallsignsIndex = 0;
- for (int i = 0; i < 50; i++) {
- seenCallsigns[i] = "";
- }
- lastResetTime = currentTime;
- Serial.println("Daily aircraft counter reset");
- }
- // Update seen time every second
- if (currentTime - lastUpdate >= 1000) {
- if (lastDataTime > 0) {
- seenTime = initialSeenTime + (currentTime - lastDataTime) / 1000.0;
- } else {
- seenTime = 0;
- }
- }
- // Update main data every 8 seconds
- if (currentTime - lastUpdate >= UPDATE_INTERVAL) {
- updateMainDisplay();
- lastUpdate = currentTime;
- Serial.println("Main data updated");
- }
- // Update LED state
- updateLED();
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement