From 4f659b75634a60399aa969bc56b50c0fcd467ba8 Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Wed, 7 Oct 2020 22:02:53 -0700 Subject: [PATCH 1/6] Initial check in of HTTPS server for #452 This switches from the espressif web server to esp32_https_server. Both HTTPS and HTTP have been migrated. On board SSL key generation. --- src/PowerFSM.cpp | 2 + src/main.cpp | 1 + src/meshwifi/meshhttp.cpp | 349 ++++++++++++++++++++++++++------------ src/meshwifi/meshhttp.h | 2 + src/meshwifi/meshwifi.cpp | 6 +- 5 files changed, 253 insertions(+), 107 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 14f7b996b..5acc785e7 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -118,6 +118,8 @@ static void powerEnter() { screen.setOn(true); setBluetoothEnable(true); + setCPUFast(true); // Set CPU to 240mhz when we're plugged in to wall power. + DEBUG_MSG("PowerFSM - powerEnter(true)\n"); } static void onEnter() diff --git a/src/main.cpp b/src/main.cpp index 37e9077df..32076a0db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -336,6 +336,7 @@ void setup() // Initialize Wifi initWifi(); + if (!rIf) recordCriticalError(ErrNoRadio); diff --git a/src/meshwifi/meshhttp.cpp b/src/meshwifi/meshhttp.cpp index 9106ab08b..eb4fca952 100644 --- a/src/meshwifi/meshhttp.cpp +++ b/src/meshwifi/meshhttp.cpp @@ -6,29 +6,46 @@ #include #include -WebServer webserver(80); +// Persistant Data Storage +#include +Preferences prefs; -// Maximum number of messages for chat history. Don't make this too big -- it'll use a -// lot of memory! -const uint16_t maxMessages = 50; +/* + Including the esp32_https_server library will trigger a compile time error. I've + tracked it down to a reoccurrance of this bug: + https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57824 + The work around is described here: + https://forums.xilinx.com/t5/Embedded-Development-Tools/Error-with-Standard-Libaries-in-Zynq/td-p/450032 -struct message_t { - char sender[10]; - char message[250]; - int32_t gpsLat; - int32_t gpsLong; - uint32_t time; - bool fromMe; -}; + Long story short is we need "#undef str" before including the esp32_https_server. + - Jm Casler (jm@casler.org) Oct 2020 +*/ +#undef str -struct messages_t { - message_t history[maxMessages]; -}; +// Includes for the https server +// https://github.com/fhessel/esp32_https_server +#include +#include +#include +#include +#include -messages_t messages_history; +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; -String something = ""; -String sender = ""; +SSLCert *cert; +HTTPSServer *secureServer; +HTTPServer *insecureServer; + +// Declare some handler functions for the various URLs on the server +void handleStyleCSS(HTTPRequest *req, HTTPResponse *res); +void handleJSONChatHistoryDummy(HTTPRequest *req, HTTPResponse *res); +void handleHotspot(HTTPRequest *req, HTTPResponse *res); +void handleRoot(HTTPRequest *req, HTTPResponse *res); +void handle404(HTTPRequest *req, HTTPResponse *res); + +bool isWebServerReady = 0; +bool isCertReady = 0; void handleWebResponse() { @@ -36,82 +53,196 @@ void handleWebResponse() return; } - // We're going to handle the DNS responder here so it - // will be ignored by the NRF boards. - handleDNSResponse(); + if (isWebServerReady) { + // We're going to handle the DNS responder here so it + // will be ignored by the NRF boards. + handleDNSResponse(); - webserver.handleClient(); + secureServer->loop(); + insecureServer->loop(); + } +} + +void taskCreateCert(void *parameter) +{ + + prefs.begin("MeshtasticHTTPS", false); + + // Delete the saved certs + if (0) { + DEBUG_MSG("Deleting any saved SSL keys ...\n"); + // prefs.clear(); + prefs.remove("PK"); + prefs.remove("cert"); + } + + size_t pkLen = prefs.getBytesLength("PK"); + size_t certLen = prefs.getBytesLength("cert"); + + DEBUG_MSG("Checking if we have a previously saved SSL Certificate.\n"); + + if (pkLen && certLen) { + DEBUG_MSG("Existing SSL Certificate found!\n"); + } else { + DEBUG_MSG("Creating the certificate. This may take a while. Please wait...\n"); + + cert = new SSLCert(); + //disableCore1WDT(); + int createCertResult = createSelfSignedCert(*cert, KEYSIZE_2048, "CN=meshtastic.local,O=Meshtastic,C=US", + "20190101000000", "20300101000000"); + //enableCore1WDT(); + + if (createCertResult != 0) { + DEBUG_MSG("Creating the certificate failed\n"); + + // Serial.printf("Creating the certificate failed. Error Code = 0x%02X, check SSLCert.hpp for details", + // createCertResult); + // while (true) + // delay(500); + } else { + DEBUG_MSG("Creating the certificate was successful\n"); + + DEBUG_MSG("Created Private Key: %d Bytes\n", cert->getPKLength()); + // for (int i = 0; i < cert->getPKLength(); i++) + // Serial.print(cert->getPKData()[i], HEX); + // Serial.println(); + + DEBUG_MSG("Created Certificate: %d Bytes\n", cert->getCertLength()); + // for (int i = 0; i < cert->getCertLength(); i++) + // Serial.print(cert->getCertData()[i], HEX); + // Serial.println(); + + prefs.putBytes("PK", (uint8_t *)cert->getPKData(), cert->getPKLength()); + prefs.putBytes("cert", (uint8_t *)cert->getCertData(), cert->getCertLength()); + } + } + + isCertReady = 1; + vTaskDelete(NULL); +} + +void createSSLCert() +{ + + if (isWifiAvailable() == 0) { + return; + } + + // Create a new process just to handle creating the cert. + // This is a workaround for Bug: https://github.com/fhessel/esp32_https_server/issues/48 + // jm@casler.org (Oct 2020) + xTaskCreate(taskCreateCert, /* Task function. */ + "createCert", /* String with name of task. */ + 16384, /* Stack size in bytes. */ + NULL, /* Parameter passed as input of the task */ + 16, /* Priority of the task. */ + NULL); /* Task handle. */ + + DEBUG_MSG("Waiting for SSL Cert to be generated.\n"); + if (isCertReady) { + DEBUG_MSG(".\n"); + delayMicroseconds(1000); + } + DEBUG_MSG("SSL Cert Ready!\n"); } void initWebServer() { - webserver.onNotFound(handleNotFound); - webserver.on("/json/chat/send/channel", handleJSONChatHistory); - webserver.on("/json/chat/send/user", handleJSONChatHistory); - webserver.on("/json/chat/history/channel", handleJSONChatHistory); - webserver.on("/json/chat/history/dummy", handleJSONChatHistoryDummy); - webserver.on("/json/chat/history/user", handleJSONChatHistory); - webserver.on("/json/stats", handleJSONChatHistory); - webserver.on("/hotspot-detect.html", handleHotspot); - webserver.on("/css/style.css", handleStyleCSS); - webserver.on("/scripts/script.js", handleScriptsScriptJS); - webserver.on("/", handleRoot); - webserver.begin(); -} + DEBUG_MSG("Initializing Web Server ...\n"); -void handleJSONChatHistory() -{ - int i; + prefs.begin("MeshtasticHTTPS", false); - String out = ""; - out += "{\n"; - out += " \"data\" : {\n"; - out += " \"chat\" : "; - out += "["; - out += "\"" + sender + "\""; - out += ","; - out += "\"" + something + "\""; - out += "]\n"; + size_t pkLen = prefs.getBytesLength("PK"); + size_t certLen = prefs.getBytesLength("cert"); - for (i = 0; i < maxMessages; i++) { - out += "["; - out += "\"" + String(messages_history.history[i].sender) + "\""; - out += ","; - out += "\"" + String(messages_history.history[i].message) + "\""; - out += "]\n"; + DEBUG_MSG("Checking if we have a previously saved SSL Certificate.\n"); + + if (pkLen && certLen) { + + uint8_t *pkBuffer = new uint8_t[pkLen]; + prefs.getBytes("PK", pkBuffer, pkLen); + + uint8_t *certBuffer = new uint8_t[certLen]; + prefs.getBytes("cert", certBuffer, certLen); + + cert = new SSLCert(certBuffer, certLen, pkBuffer, pkLen); + + DEBUG_MSG("Retrieved Private Key: %d Bytes\n", cert->getPKLength()); + // DEBUG_MSG("Retrieved Private Key: " + String(cert->getPKLength()) + " Bytes"); + // for (int i = 0; i < cert->getPKLength(); i++) + // Serial.print(cert->getPKData()[i], HEX); + // Serial.println(); + + DEBUG_MSG("Retrieved Certificate: %d Bytes\n", cert->getCertLength()); + // for (int i = 0; i < cert->getCertLength(); i++) + // Serial.print(cert->getCertData()[i], HEX); + // Serial.println(); + } else { + DEBUG_MSG("Web Server started without SSL keys! How did this happen?\n"); } - out += "\n"; - out += " }\n"; - out += "}\n"; + // We can now use the new certificate to setup our server as usual. + secureServer = new HTTPSServer(cert); + insecureServer = new HTTPServer(); - webserver.send(200, "application/json", out); - return; + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + + ResourceNode *nodeCSS = new ResourceNode("/css/style.css", "GET", &handleStyleCSS); + ResourceNode *nodeJS = new ResourceNode("/scripts/script.js", "GET", &handleJSONChatHistoryDummy); + ResourceNode *nodeHotspot = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); + ResourceNode *nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode *node404 = new ResourceNode("", "GET", &handle404); + + // Secure nodes + secureServer->registerNode(nodeCSS); + secureServer->registerNode(nodeJS); + secureServer->registerNode(nodeHotspot); + secureServer->registerNode(nodeRoot); + secureServer->setDefaultNode(node404); + + // Insecure nodes + insecureServer->registerNode(nodeCSS); + insecureServer->registerNode(nodeJS); + insecureServer->registerNode(nodeHotspot); + insecureServer->registerNode(nodeRoot); + insecureServer->setDefaultNode(node404); + + DEBUG_MSG("Starting Web Server...\n"); + secureServer->start(); + insecureServer->start(); + if (secureServer->isRunning() && insecureServer->isRunning()) { + DEBUG_MSG("Web Server Ready\n"); + isWebServerReady = 1; + } } -void handleNotFound() +void handle404(HTTPRequest *req, HTTPResponse *res) { - String message = ""; - message += "File Not Found\n\n"; - message += "URI: "; - message += webserver.uri(); - message += "\nMethod: "; - message += (webserver.method() == HTTP_GET) ? "GET" : "POST"; - message += "\nArguments: "; - message += webserver.args(); - message += "\n"; - for (uint8_t i = 0; i < webserver.args(); i++) { - message += " " + webserver.argName(i) + ": " + webserver.arg(i) + "\n"; - } - Serial.println(message); - webserver.send(404, "text/plain", message); + // Discard request body, if we received any + // We do this, as this is the default node and may also server POST/PUT requests + req->discardRequestBody(); + + // Set the response status + res->setStatusCode(404); + res->setStatusText("Not Found"); + + // Set content type of the response + res->setHeader("Content-Type", "text/html"); + + // Write a tiny HTTP page + res->println(""); + res->println(""); + res->println("Not Found"); + res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); } /* This supports the Apple Captive Network Assistant (CNA) Portal */ -void handleHotspot() +void handleHotspot(HTTPRequest *req, HTTPResponse *res) { DEBUG_MSG("Hotspot Request\n"); @@ -120,25 +251,14 @@ void handleHotspot() otherwise iOS will have trouble detecting that the connection to the SoftAP worked. */ - String out = ""; - // out += "Success\n"; - out += "\n"; - webserver.send(200, "text/html", out); - return; -} + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "text/html"); -void notifyWebUI() -{ - DEBUG_MSG("************ Got a message! ************\n"); - MeshPacket &mp = devicestate.rx_text_message; - NodeInfo *node = nodeDB.getNode(mp.from); - sender = (node && node->has_user) ? node->user.long_name : "???"; - - static char tempBuf[256]; // mesh.options says this is MeshPacket.encrypted max_size - assert(mp.decoded.which_payload == SubPacket_data_tag); - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.data.payload.bytes); - - something = tempBuf; + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->println(""); + res->println("\n"); } /* @@ -146,7 +266,7 @@ void notifyWebUI() https://tomeko.net/online_tools/cpp_text_escape.php?lang=en */ -void handleRoot() +void handleRoot(HTTPRequest *req, HTTPResponse *res) { String out = ""; @@ -223,11 +343,17 @@ void handleRoot() "\n" "\n" ""; - webserver.send(200, "text/html", out); - return; + + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "text/html"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->print(out); } -void handleStyleCSS() +void handleStyleCSS(HTTPRequest *req, HTTPResponse *res) { String out = ""; @@ -510,11 +636,16 @@ void handleStyleCSS() " height: 0;\n" "}"; - webserver.send(200, "text/css", out); - return; + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "text/css"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->print(out); } -void handleScriptsScriptJS() +void handleScriptsScriptJS(HTTPRequest *req, HTTPResponse *res) { String out = ""; out += "String.prototype.toHHMMSS = function () {\n" @@ -664,11 +795,16 @@ void handleScriptsScriptJS() "//\tsetTimeout(scrollHistory(),500);\n" "// }"; - webserver.send(200, "text/javascript", out); - return; + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "text/html"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->print(out); } -void handleJSONChatHistoryDummy() +void handleJSONChatHistoryDummy(HTTPRequest *req, HTTPResponse *res) { String out = ""; out += "{\n" @@ -788,6 +924,11 @@ void handleJSONChatHistoryDummy() "\t}\n" "}"; - webserver.send(200, "application/json", out); - return; + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "application/json"); + + // The response implements the Print interface, so you can use it just like + // you would write to Serial etc. + res->print(out); } \ No newline at end of file diff --git a/src/meshwifi/meshhttp.h b/src/meshwifi/meshhttp.h index 42c6644bf..eeddfe9ff 100644 --- a/src/meshwifi/meshhttp.h +++ b/src/meshwifi/meshhttp.h @@ -4,6 +4,8 @@ #include void initWebServer(); +void createSSLCert(); + void handleNotFound(); diff --git a/src/meshwifi/meshwifi.cpp b/src/meshwifi/meshwifi.cpp index b7a8dace4..ed5f5dfc1 100644 --- a/src/meshwifi/meshwifi.cpp +++ b/src/meshwifi/meshwifi.cpp @@ -29,9 +29,6 @@ bool isWifiAvailable() // strcpy(radioConfig.preferences.wifi_password, ""); if (*wifiName && *wifiPsw) { - - // Once every 10 seconds, try to reconnect. - return 1; } else { return 0; @@ -64,6 +61,9 @@ void initWifi() return; } + createSSLCert(); + + if (radioConfig.has_preferences) { const char *wifiName = radioConfig.preferences.wifi_ssid; const char *wifiPsw = radioConfig.preferences.wifi_password; From 6d178ebc9150ea96f8df53c44308f5d9593c0580 Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Wed, 7 Oct 2020 22:18:46 -0700 Subject: [PATCH 2/6] Added esp32_https_server to the meshtastic project --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index b48dc3938..03420d962 100644 --- a/platformio.ini +++ b/platformio.ini @@ -66,6 +66,7 @@ lib_deps = https://github.com/meshtastic/RadioLib.git#1083c2e76f9906c5f80dfec726facebf8413eef0 https://github.com/meshtastic/TinyGPSPlus.git https://github.com/meshtastic/AXP202X_Library.git#8404abb6d4b486748636bc6ad72d2a47baaf5460 + https://github.com/meshtastic/esp32_https_server.git Wire ; explicitly needed here because the AXP202 library forgets to add it SPI From db8faa9fafe6a4c523da12f6a19af00463b6b233 Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Fri, 9 Oct 2020 23:07:37 -0700 Subject: [PATCH 3/6] added powerExit --- src/PowerFSM.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index a464c833e..ba16be4d0 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -125,7 +125,13 @@ static void powerEnter() screen.setOn(true); setBluetoothEnable(true); setCPUFast(true); // Set CPU to 240mhz when we're plugged in to wall power. - DEBUG_MSG("PowerFSM - powerEnter(true)\n"); +} + +static void powerExit() +{ + screen.setOn(true); + setBluetoothEnable(true); + setCPUFast(false); // Set CPU to 80mhz when we're plugged in to wall power. } static void onEnter() @@ -159,6 +165,8 @@ State stateSERIAL(serialEnter, NULL, NULL, "SERIAL"); State stateBOOT(bootEnter, NULL, NULL, "BOOT"); State stateON(onEnter, NULL, NULL, "ON"); State statePOWER(powerEnter, NULL, NULL, "POWER"); +State stateUNPLUG(powerExit, NULL, NULL, "UNPLUG"); + Fsm powerFSM(&stateBOOT); void PowerFSM_setup() @@ -224,6 +232,7 @@ void PowerFSM_setup() powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&statePOWER, &stateUNPLUG, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); } powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); From 5b777219be6e04292d02b3671d8d7f00ae0e2d2b Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Sat, 10 Oct 2020 17:59:32 -0700 Subject: [PATCH 4/6] Enable the RX LNA #466 Enabling the RX LNA --- src/mesh/SX1262Interface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/SX1262Interface.cpp b/src/mesh/SX1262Interface.cpp index b17acd9fe..9d38f122f 100644 --- a/src/mesh/SX1262Interface.cpp +++ b/src/mesh/SX1262Interface.cpp @@ -82,8 +82,8 @@ bool SX1262Interface::reconfigure() assert(err == ERR_NONE); // Hmm - seems to lower SNR when the signal levels are high. Leaving off for now... - // err = lora.setRxGain(true); - // assert(err == ERR_NONE); + err = lora.setRxGain(true); + assert(err == ERR_NONE); err = lora.setSyncWord(syncWord); assert(err == ERR_NONE); From b17a8d7a6acb118b605b381679ad514c109a1890 Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Sat, 10 Oct 2020 21:54:27 -0700 Subject: [PATCH 5/6] Removed powerExit -- it wasn't working --- src/PowerFSM.cpp | 10 +--------- src/graphics/Screen.cpp | 4 ++++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index ba16be4d0..63d05a5a2 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -127,13 +127,6 @@ static void powerEnter() setCPUFast(true); // Set CPU to 240mhz when we're plugged in to wall power. } -static void powerExit() -{ - screen.setOn(true); - setBluetoothEnable(true); - setCPUFast(false); // Set CPU to 80mhz when we're plugged in to wall power. -} - static void onEnter() { screen.setOn(true); @@ -148,6 +141,7 @@ static void onEnter() service.sendNetworkPing(displayedNodeNum, true); // Refresh the currently displayed node lastPingMs = now; } + } static void screenPress() @@ -165,7 +159,6 @@ State stateSERIAL(serialEnter, NULL, NULL, "SERIAL"); State stateBOOT(bootEnter, NULL, NULL, "BOOT"); State stateON(onEnter, NULL, NULL, "ON"); State statePOWER(powerEnter, NULL, NULL, "POWER"); -State stateUNPLUG(powerExit, NULL, NULL, "UNPLUG"); Fsm powerFSM(&stateBOOT); @@ -232,7 +225,6 @@ void PowerFSM_setup() powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); - powerFSM.add_transition(&statePOWER, &stateUNPLUG, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); } powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 2ea03c637..284962015 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1026,6 +1026,10 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat String(days) + "d " + (hours < 10 ? "0" : "") + String(hours) + ":" + (minutes < 10 ? "0" : "") + String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds)); + // Show CPU Frequency. + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("CPU " + String(getCpuFrequencyMhz()) + "MHz"), y + FONT_HEIGHT * 1, + "CPU " + String(getCpuFrequencyMhz()) + "MHz"); + // Line 3 drawGPSAltitude(display, x, y + FONT_HEIGHT * 2, gpsStatus); From d39cc3d57bf99b68aaf7e713620ccea9c33fbf78 Mon Sep 17 00:00:00 2001 From: Jm Casler Date: Sat, 10 Oct 2020 22:06:56 -0700 Subject: [PATCH 6/6] Checking if ESP32 for the frequency display --- src/graphics/Screen.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 284962015..2fcb99b9d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1026,9 +1026,11 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat String(days) + "d " + (hours < 10 ? "0" : "") + String(hours) + ":" + (minutes < 10 ? "0" : "") + String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds)); +#ifndef NO_ESP32 // Show CPU Frequency. display->drawString(x + SCREEN_WIDTH - display->getStringWidth("CPU " + String(getCpuFrequencyMhz()) + "MHz"), y + FONT_HEIGHT * 1, "CPU " + String(getCpuFrequencyMhz()) + "MHz"); +#endif // Line 3 drawGPSAltitude(display, x, y + FONT_HEIGHT * 2, gpsStatus);