diff --git a/TEF6686_ESP32.ino b/TEF6686_ESP32.ino index 08c14c2..58286a3 100644 --- a/TEF6686_ESP32.ino +++ b/TEF6686_ESP32.ino @@ -8,6 +8,8 @@ #include #include // https://github.com/ohmytime/TFT_eSPI_DynamicSpeed/archive/refs/heads/master.zip (please then edit the User_Setup.h as described in the Wiki) #include // https://github.com/bbx10/Hash_tng/archive/refs/heads/master.zip +#include +#include #include "src/WiFiConnect.h" #include "src/WiFiConnectParam.h" #include "src/FONT16.h" @@ -442,6 +444,8 @@ WiFiConnect wc; WiFiServer Server(7373); WiFiClient RemoteClient; WiFiUDP Udp; +WebServer webserver(80); + hw_timer_t *timScreensaver = NULL; byte screensaver_IRQ = OFF; @@ -636,6 +640,9 @@ void setup() { tft.init(); tft.initDMA(); + webserver.on("/", handleRoot); + webserver.on("/downloadCSV", HTTP_GET, handleDownloadCSV); + doTheme(); if (displayflip == 0) { @@ -672,6 +679,9 @@ void setup() { tft.setSwapBytes(true); tft.fillScreen(BackgroundColor); + SPIFFS.begin(); + if (!SPIFFS.exists("/logbook.csv")) handleCreateNewLogbook(); + FrequencySprite.createSprite(200, 50); FrequencySprite.setTextDatum(TR_DATUM); FrequencySprite.setSwapBytes(true); @@ -918,6 +928,8 @@ void setup() { } void loop() { + if (wifi) webserver.handleClient(); + if (hardwaremodel == PORTABLE_TOUCH_ILI9341 && touch_detect) { if (tft.getTouchRawZ() > 100) { // Check if the touch is active uint16_t x, y; @@ -5298,3 +5310,278 @@ void toggleiMSEQ() { } } } + +void handleRoot() { + // Attempt to open the CSV file stored in SPIFFS (File System) + fs::File file = SPIFFS.open("/logbook.csv", "r"); + if (!file) { + // If the file could not be opened, send an error message to the browser + webserver.send(500, "text/plain", "Failed to open logbook"); + return; // Exit the function if the file cannot be opened + } + + // Start building the HTML page to send to the browser + String html = ""; + html += ""; + html += ""; + + // Add CSS styling for a modern, dark-themed look + html += ""; + + html += ""; + + // Add the logo image at the top of the page + html += "\"FMdx"; + + // Add a header with a dynamic title from the language array (replace with actual language logic) + html += "

" + String(myLanguage[language][286]) + "

"; + + // Add the "Download CSV" button, which triggers a download action when clicked + html += ""; + + // Add "Go to Bottom" button + html += ""; + + // Add JavaScript for scrolling to the bottom + html += ""; + + // Start the HTML table to display CSV data + html += ""; + + // Read and process the first line (header row) from the CSV file + String header = ""; + if (file.available()) { + header = file.readStringUntil('\n'); // Read the first line containing the headers + html += ""; // Start the header row in the table + int startIndex = 0; + + // Split the header line by commas and create table headers ("; // Add the column as a table header + startIndex = endIndex + 1; // Move to the next column + } + html += ""; // End the header row + } + + // Variable to track if there is any data in the CSV file + bool hasData = false; + int rowCount = 0; // Counter for rows, used for alternating row colors + + // Process the remaining lines (data rows) in the CSV file + while (file.available()) { + String line = file.readStringUntil('\n'); // Read the next line (data row) + if (line.length() > 0) { + hasData = true; // Mark that data rows are present + rowCount++; // Increment the row count + html += ""; // Start a new row in the table + int startIndex = 0; + + // Split the line by commas and create table data cells ("; // Add the cell data to the table + startIndex = endIndex + 1; // Move to the next column + } + html += ""; // End the data row + } + } + + file.close(); // Close the file after reading + + // If no data rows were found, display a "No data available" message in the table + if (!hasData) { + html += ""; + } + + // End the HTML table and body + html += "
) for each column + while (startIndex < header.length()) { + int endIndex = header.indexOf(',', startIndex); + if (endIndex == -1) endIndex = header.length(); // Handle last column (no comma after it) + String column = header.substring(startIndex, endIndex); // Extract the column name + html += "" + column + "
) for each column + while (startIndex < line.length()) { + int endIndex = line.indexOf(',', startIndex); + if (endIndex == -1) endIndex = line.length(); // Handle the last column + String cell = line.substring(startIndex, endIndex); // Extract the cell data + html += "" + cell + "
" + String(myLanguage[language][288]) + "
"; + html += ""; // End the HTML page + + // Send the generated HTML content to the browser with a 200 OK response + webserver.send(200, "text/html", html); +} + +void handleDownloadCSV() { + // Attempt to open the CSV file from SPIFFS in read mode + fs::File file = SPIFFS.open("/logbook.csv", "r"); + + // Check if the file was successfully opened + if (!file) { + // If the file could not be opened, send an error response + webserver.send(500, "text/plain", "Failed to open logbook for download"); + return; // Exit the function if the file cannot be opened + } + + // Set the headers to specify that the response will be a CSV file for download + webserver.sendHeader("Content-Type", "text/csv"); // Set MIME type for CSV files + webserver.sendHeader("Content-Disposition", "attachment; filename=logbook.csv"); // Suggests the file name for download + + // Stream the CSV file content directly to the browser + webserver.streamFile(file, "text/csv"); + + // Close the file after streaming the content to release resources + file.close(); +} + +bool handleCreateNewLogbook() { + // Check if the file "logbook.csv" already exists + if (SPIFFS.exists("/logbook.csv")) { + // If it exists, delete the file + if (!SPIFFS.remove("/logbook.csv")) { + // Return false if the file could not be deleted + return false; + } + } + + // Create a new "logbook.csv" file in write mode + fs::File file = SPIFFS.open("/logbook.csv", "w"); + + // Check if the file was successfully created + if (!file) { + // Return false if file creation fails + return false; + } + + // Write the header to the new CSV file + String header = "Date,Time,Frequency,PI code,Signal,PS,RadioText\n"; + file.print(header); // Ensure that the header is written properly + + // Make sure the data is written before closing the file + file.flush(); // Ensure that everything is written to the file + file.close(); // Close the file after writing + + // Return true if the function runs without problems + return true; +} + +bool addRowToCSV() { + // Check if there is enough free space in SPIFFS (150 bytes or more) + if (SPIFFS.totalBytes() - SPIFFS.usedBytes() < 150) { + return false; // Return false if there is less than 150 bytes free + } + + // Open the logbook.csv file in append mode + fs::File file = SPIFFS.open("/logbook.csv", "a"); + + // Check if the file could not be opened + if (!file) { + return false; // Return false if the file can't be opened + } + + // Get the current date and time from ESP32 (using the built-in time functions) + String currentDateTime = getCurrentDateTime(); // Get the current date and time + + // If time is not available, replace with "-" + if (currentDateTime == "") { + currentDateTime = "-,-"; // Set both date and time to "-" + } + + // Convert frequency to a string format (XX.XX MHz) + int freqInt = (int)frequency; // Assuming frequency is already a float or double, cast it to int + + // Apply the necessary conversion (if any) for frequency + int convertedFreq = (freqInt + ConverterSet * 100) / 100; + String frequencyFormatted = String(convertedFreq) + "." + ((freqInt + ConverterSet * 100) % 100 < 10 ? "0" : "") + String((freqInt + ConverterSet * 100) % 100) + " MHz"; // Add " MHz" + + // Format the signal strength (xx.x with the correct unit) + int SStatusprint = 0; + if (unit == 0) SStatusprint = SStatus; + if (unit == 1) SStatusprint = ((SStatus * 100) + 10875) / 100; + if (unit == 2) SStatusprint = round((float(SStatus) / 10.0 - 10.0 * log10(75) - 90.0) * 10.0); + + // Choose the correct unit suffix for signal based on the `unit` value + String signal = String(SStatusprint / 10) + "." + String(abs(SStatusprint % 10)); + if (unit == 0) { + signal += " dBμV"; // Unit for unit == 0 + } else if (unit == 1) { + signal += " dBf"; // Unit for unit == 1 + } else if (unit == 2) { + signal += " dBm"; // Unit for unit == 2 + } + + // Format the RadioText with enhanced option if available + String radioText = String(radio.rds.stationText + " " + radio.rds.stationText32); + if (radio.rds.hasEnhancedRT) { + radioText += " eRT: " + String(radio.rds.enhancedRTtext); + } + + // Replace commas in the station name and radioText just when adding to the row + String stationName = radio.rds.stationName; + String radioTextModified = radioText; + + stationName.replace(",", " "); // Temporarily replace commas in stationName + radioTextModified.replace(",", " "); // Temporarily replace commas in radioText + + // Create the row data, replacing stationIDtext with picode + String row = currentDateTime + "," + frequencyFormatted + "," + radio.rds.picode + "," + signal + "," + stationName + "," + radioTextModified + "\n"; + + // Write the row to the CSV file + if (file.print(row)) { + // Successfully wrote to the file + file.close(); + return true; // Return true when the row is successfully added + } else { + // Failed to write to the file + file.close(); + return false; // Return false if there was an issue writing + } +} + +String getCurrentDateTime() { + // Check if time has been set + if (!rtcset) { + return "-,-"; // Return placeholder when time is not set + } + + // Use the ESP32's time functions (assuming time is set correctly via RDS) + struct tm timeInfo; + if (!getLocalTime(&timeInfo)) { + return "-,-"; // Return placeholder if time is not available + } + + // Format date-time based on the region + char buf[20]; // Buffer size for formatted date string + + if (radio.rds.region == 1) { + // USA format: MM/DD/YYYY, HH:MM AM/PM + strftime(buf, sizeof(buf), "%m/%d/%Y", &timeInfo); // MM/DD/YYYY format + + // Format time in 12-hour format with AM/PM + int hour = timeInfo.tm_hour; + String ampm = (hour >= 12) ? "PM" : "AM"; + if (hour == 0) hour = 12; // Midnight case + else if (hour > 12) hour -= 12; // Convert PM to 12-hour format + + String timeWithAMPM = String(hour) + ":" + (timeInfo.tm_min < 10 ? "0" : "") + String(timeInfo.tm_min) + " " + ampm; + + // Return final formatted date-time for USA + return String(buf) + "," + timeWithAMPM; + } else { + // European format: DD/MM/YYYY, HH:MM + strftime(buf, sizeof(buf), "%d-%m-%Y", &timeInfo); // DD/MM/YYYY format + String timeEuropean = String(timeInfo.tm_hour) + ":" + (timeInfo.tm_min < 10 ? "0" : "") + String(timeInfo.tm_min); // Add leading 0 for minutes if necessary + return String(buf) + "," + timeEuropean; + } +} diff --git a/src/comms.cpp b/src/comms.cpp index 28045dc..d0416fd 100644 --- a/src/comms.cpp +++ b/src/comms.cpp @@ -675,7 +675,7 @@ void XDRGTKRoutine() { if (scandxmode) cancelDXScan(); if (!XDRScan) BWsetRecall = BWset; XDRScan = true; - Data_Accelerator = true; + Data_Accelerator = true; switch (buff[1]) { case 'a': scanner_start = (atol(buff + 2) + 5) / 10; break; @@ -764,7 +764,7 @@ void XDRGTKRoutine() { } break; } - Data_Accelerator = false; + Data_Accelerator = false; break; case 'W': @@ -909,11 +909,13 @@ void tryWiFi() { if (wc.autoConnect()) { Server.begin(); Udp.begin(9031); + webserver.begin(); remoteip = IPAddress (WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], subnetclient); if (!setupmode) tftPrint(0, myLanguage[language][57], 155, 128, InsignificantColor, InsignificantColorSmooth, 28); } else { if (!setupmode) tftPrint(0, myLanguage[language][56], 155, 128, SignificantColor, SignificantColorSmooth, 28); Server.end(); + webserver.stop(); Udp.stop(); WiFi.mode(WIFI_OFF); wifi = false; diff --git a/src/comms.h b/src/comms.h index 159164b..a61ccc6 100644 --- a/src/comms.h +++ b/src/comms.h @@ -123,6 +123,7 @@ extern WiFiClient RemoteClient; extern WiFiUDP Udp; extern WiFiServer Server; extern WiFiConnect wc; +extern WebServer webserver; void Communication(); void XDRGTKRoutine(); diff --git a/src/language.h b/src/language.h index 3307e46..e92369e 100644 --- a/src/language.h +++ b/src/language.h @@ -5,7 +5,7 @@ // [number of languages][number of texts] -static const char* const myLanguage[18][286] PROGMEM = { +static const char* const myLanguage[18][290] PROGMEM = { { "English", // English "Rotary direction changed", // 1 "Please release button", // 2 @@ -291,7 +291,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Nederlands", // Dutch @@ -579,7 +583,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Druk op aangegeven hoeken", // 282 "om scherm te calibreren", // 283 "Scherm inversie gewijzigd", // 284 - "Selecteer bandbreedte" // 285 + "Selecteer bandbreedte", // 285 + "Jouw logboek", // 286 + "Download logboek", // 287 + "Logboek is leeg", // 288 + "Ga naar einde" // 289 }, { "Polski", // Polish @@ -867,7 +875,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Naciśnij zaznaczony róg", // 282 "Aby skalibrować ekran", // 283 "Inwersja kolorów wyświetlacza", // 284 - "Wybierz szer. pasma" // 285 + "Wybierz szer. pasma", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Hrvatski", // Croatian @@ -1155,7 +1167,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Ελληνικά", // Greek @@ -1443,7 +1459,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Πιέστε την\nεπισημασμένη γωνία", // 282 "Για βαθμονόμηση\nτης οθόνης αφής", // 283 "Εναλλαγή αναστροφής οθόνης", // 284 - "Επιλογή εύρους μπάντας" // 285 + "Επιλογή εύρους μπάντας", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Română", // Romanian @@ -1731,7 +1751,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Deutsch", // German @@ -2019,7 +2043,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Markierte Ecke drücken", // 282 "zum Kalibrieren des Touchscrees", // 283 "Bildschirmumkehrung umschalten", // 284 - "Bandbreite wählen" // 285 + "Bandbreite wählen", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Český", // Czech @@ -2307,7 +2335,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Magyar", // Hungarian @@ -2595,7 +2627,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Nyomd meg a kiemelt sarkot", // 282 "Érintőképernyő kalibrálásához", // 283 "Képernyő inverzió kapcsoló", // 284 - "Sávszélesség választása" // 285 + "Sávszélesség választása", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Français", // French @@ -2883,7 +2919,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Appuyez sur l'angle\nmis en évidence", // 282 "Pour calibrer l'écran tactile", // 283 "Inversion d'affichage bascules", // 284 - "Sélectionner la bande passante" // 285 + "Sélectionner la bande passante", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Български", // Bulgarian @@ -3171,7 +3211,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Русский", // Russian @@ -3459,7 +3503,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Нажмите на подсвеченный угол", // 282 "Калибровка тачскрина", // 283 "Переключение инверсии экрана", // 284 - "Выбрать ширину полосы" // 285 + "Выбрать ширину полосы", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Українська", // Ukranian @@ -3747,7 +3795,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Italiano", // Italian @@ -4035,7 +4087,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Premi l'angolo evidenziato", // 282 "Per calibrare lo schermo touch", // 283 "Inversione colore schermo", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Simplified Chinese", // Simplified Chinese @@ -4323,7 +4379,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Press highlighted corner", // 282 "To calibrate touchscreen", // 283 "Screen inversion toggles", // 284 - "Select Bandwidth" // 285 + "Select Bandwidth", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Norsk", // Norwegian @@ -4611,7 +4671,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Trykk på det uthevede hjørnet", // 282 "For å kalibrere berøringsskjerm", // 283 "Veksle Skjerminversjon", // 284 - "Velg Båndbredde" // 285 + "Velg Båndbredde", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Español", // Spanish @@ -4899,7 +4963,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Presione en el\nángulo resaltado", // 282 "Para calibrar\nla pantalla táctil", // 283 "Cambio de reverso\nde la pantalla", // 284 - "Seleccionar ancho de banda" // 285 + "Seleccionar ancho de banda", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 }, { "Português", // Portuguese @@ -5187,7 +5255,11 @@ static const char* const myLanguage[18][286] PROGMEM = { "Pressione o\ncanto inscrito", // 282 "Para calibração\nda tela de toque", // 283 "Alternar a inversão da tela", // 284 - "Selecione largura de banda" // 285 + "Selecione largura de banda", // 285 + "Your logbook", // 286 + "Download logbook", // 287 + "Logbook is empty", // 288 + "Go to bottom" // 289 } }; -#endif +#endif \ No newline at end of file