diff --git a/Firmware/GPAD_API/GPAD_API/DFPlayer.cpp b/Firmware/GPAD_API/GPAD_API/DFPlayer.cpp index 0dfe166..5e27f2e 100644 --- a/Firmware/GPAD_API/GPAD_API/DFPlayer.cpp +++ b/Firmware/GPAD_API/GPAD_API/DFPlayer.cpp @@ -1,9 +1,10 @@ #include "DFPlayer.h" #include "gpad_utility.h" +#include "debug_macros.h" #include DFRobotDFPlayerMini dfPlayer; -HardwareSerial mySerial1(2); // Use UART2 +extern HardwareSerial uartSerial2; const int LED_PIN = 13; // Krake const int nDFPlayer_BUSY = 4; // active LOW BUSY pin from DFPlayer @@ -17,27 +18,26 @@ int pausa = 0; void serialSplashDFP() { - Serial.println("==================================="); - Serial.println(DEVICE_UNDER_TEST); - Serial.print(PROG_NAME); - Serial.println(FIRMWARE_VERSION); - Serial.print("Compiled at: "); - Serial.println(F(__DATE__ " " __TIME__)); - Serial.println("==================================="); - Serial.println(); + DBG_PRINTLN(F("===================================")); + DBG_PRINTLN(F(DEVICE_UNDER_TEST)); + DBG_PRINT(F(PROG_NAME)); + DBG_PRINTLN(F(FIRMWARE_VERSION)); + DBG_PRINT(F("Compiled at: ")); + DBG_PRINTLN(F(__DATE__ " " __TIME__)); + DBG_PRINTLN(F("===================================")); } void menu_opcoes() { - Serial.println(); - Serial.println(F("==================================================================================================================================")); - Serial.println(F("Commands:")); - Serial.println(F(" [1-9] select MP3 file")); - Serial.println(F(" [s] stop playback")); - Serial.println(F(" [p] pause/continue")); - Serial.println(F(" [+/-] increase/decrease volume")); - Serial.println(F(" [] previous/next track")); - Serial.println(F("=================================================================================================================================")); + DBG_PRINTLN(F("")); + DBG_PRINTLN(F("==================================================================================================================================")); + DBG_PRINTLN(F("Commands:")); + DBG_PRINTLN(F(" [1-9] select MP3 file")); + DBG_PRINTLN(F(" [s] stop playback")); + DBG_PRINTLN(F(" [p] pause/continue")); + DBG_PRINTLN(F(" [+/-] increase/decrease volume")); + DBG_PRINTLN(F(" [] previous/next track")); + DBG_PRINTLN(F("=================================================================================================================================")); } void checkSerial(void) @@ -49,8 +49,8 @@ void checkSerial(void) if ((command >= '1') && (command <= '9')) { int track = command - '0'; - Serial.print("Playing track: "); - Serial.println(track); + DBG_PRINT(F("Playing track: ")); + DBG_PRINTLN(track); dfPlayer.play(track); menu_opcoes(); } @@ -58,7 +58,7 @@ void checkSerial(void) if (command == 's') { dfPlayer.stop(); - Serial.println("Music stopped."); + DBG_PRINTLN(F("Music stopped.")); menu_opcoes(); } @@ -67,12 +67,12 @@ void checkSerial(void) pausa = !pausa; if (pausa == 0) { - Serial.println("Continue..."); + DBG_PRINTLN(F("Continue...")); dfPlayer.start(); } else { - Serial.println("Music paused."); + DBG_PRINTLN(F("Music paused.")); dfPlayer.pause(); } menu_opcoes(); @@ -81,30 +81,30 @@ void checkSerial(void) if (command == '+') { dfPlayer.volumeUp(); - Serial.print("Current volume: "); - Serial.println(dfPlayer.readVolume()); + DBG_PRINT(F("Current volume: ")); + DBG_PRINTLN(dfPlayer.readVolume()); menu_opcoes(); } if (command == '-') { dfPlayer.volumeDown(); - Serial.print("Current volume: "); - Serial.println(dfPlayer.readVolume()); + DBG_PRINT(F("Current volume: ")); + DBG_PRINTLN(dfPlayer.readVolume()); menu_opcoes(); } if (command == '<') { dfPlayer.previous(); - Serial.println("Previous track."); + DBG_PRINTLN(F("Previous track.")); menu_opcoes(); } if (command == '>') { dfPlayer.next(); - Serial.println("Next track."); + DBG_PRINTLN(F("Next track.")); menu_opcoes(); } } @@ -127,48 +127,48 @@ void setupDFPlayer() { pinMode(nDFPlayer_BUSY, INPUT_PULLUP); - Serial.println("UART2 Begin for DFPlayer"); - mySerial1.begin(BAUD_DFPLAYER, SERIAL_8N1, RXD2, TXD2); + DBG_PRINTLN(F("UART2 Begin for DFPlayer")); + uartSerial2.begin(BAUD_DFPLAYER, SERIAL_8N1, RXD2, TXD2); delayWithYield(1000); // ACK=false is safer for DFPlayer clones and avoids repeated blocking/timeouts. - Serial.println("Begin DFPlayer: ACK=false, doReset=false"); - if (!dfPlayer.begin(mySerial1, false, false)) + DBG_PRINTLN(F("Begin DFPlayer: ACK=false, doReset=false")); + if (!dfPlayer.begin(uartSerial2, false, false)) { - Serial.println("DFPlayer Mini not detected or not responding."); - Serial.println("Check wiring, power, SD card, and file names."); + DBG_PRINTLN(F("DFPlayer Mini not detected or not responding.")); + DBG_PRINTLN(F("Check wiring, power, SD card, and file names.")); isDFPlayerDetected = false; return; } isDFPlayerDetected = true; - Serial.println("DFPlayer Mini detected."); + DBG_PRINTLN(F("DFPlayer Mini detected.")); dfPlayer.setTimeOut(500); delayWithYield(300); // This may return unusual values on clones. Do not disable audio only because of this. int moduleState = dfPlayer.readState(); - Serial.print("DFPlayer state after init: "); - Serial.println(moduleState); + DBG_PRINT(F("DFPlayer state after init: ")); + DBG_PRINTLN(moduleState); if (moduleState > 0) { - Serial.println("Warning: unusual DFPlayer state. Possible clone/module variant, continuing test."); + DBG_PRINTLN(F("Warning: unusual DFPlayer state. Possible clone/module variant, continuing test.")); } dfPlayer.volume(volumeDFPlayer); delayWithYield(300); numberFilesDF = dfPlayer.readFileCounts(); - Serial.print("SD card file count: "); - Serial.println(numberFilesDF); + DBG_PRINT(F("SD card file count: ")); + DBG_PRINTLN(numberFilesDF); if (numberFilesDF <= 0) { - Serial.println("Warning: no audio files detected. Use FAT32 SD card and files like 0001.mp3, 0002.mp3."); + DBG_PRINTLN(F("Warning: no audio files detected. Use FAT32 SD card and files like 0001.mp3, 0002.mp3.")); } - Serial.println("DFPlayer startup test: playing track 1."); + DBG_PRINTLN(F("DFPlayer startup test: playing track 1.")); dfPlayer.play(1); delayWithYield(3000); // Give enough time to hear output without starving the scheduler/WDT. @@ -192,34 +192,35 @@ void displayDFPlayerStats() { if (!isDFPlayerDetected) { - Serial.println("DFPlayer stats unavailable: module not detected."); + DBG_PRINTLN(F("DFPlayer stats unavailable: module not detected.")); return; } - Serial.println("================= DFPlayer Stats ================="); - Serial.print("DFPlayer State: "); - Serial.println(dfPlayer.readState()); + DBG_PRINTLN(F("================= DFPlayer Stats =================")); + DBG_PRINT(F("DFPlayer State: ")); + DBG_PRINTLN(dfPlayer.readState()); - Serial.print("DFPlayer Volume: "); - Serial.println(dfPlayer.readVolume()); + DBG_PRINT(F("DFPlayer Volume: ")); + DBG_PRINTLN(dfPlayer.readVolume()); - Serial.print("DFPlayer EQ: "); - Serial.println(dfPlayer.readEQ()); + DBG_PRINT(F("DFPlayer EQ: ")); + DBG_PRINTLN(dfPlayer.readEQ()); - Serial.print("SD Card File Count: "); + DBG_PRINT(F("SD Card File Count: ")); numberFilesDF = dfPlayer.readFileCounts(); - Serial.println(numberFilesDF); + DBG_PRINTLN(numberFilesDF); - Serial.print("Current File Number: "); - Serial.println(dfPlayer.readCurrentFileNumber()); + DBG_PRINT(F("Current File Number: ")); + DBG_PRINTLN(dfPlayer.readCurrentFileNumber()); - Serial.print("BUSY pin: "); - Serial.println(digitalRead(nDFPlayer_BUSY) == LOW ? "LOW / playing" : "HIGH / idle"); - Serial.println("=================================================="); + DBG_PRINT(F("BUSY pin: ")); + DBG_PRINTLN(digitalRead(nDFPlayer_BUSY) == LOW ? F("LOW / playing") : F("HIGH / idle")); + DBG_PRINTLN(F("==================================================")); } void printDetail(uint8_t type, int value) { +#if (DEBUG_LEVEL > 0) switch (type) { case TimeOut: @@ -281,6 +282,10 @@ void printDetail(uint8_t type, int value) default: break; } +#else + (void)type; + (void)value; +#endif } void dfPlayerUpdate(void) @@ -297,14 +302,14 @@ void playNotBusy() { if (!isDFPlayerDetected) return; - Serial.println("playNotBusy"); + DBG_PRINTLN(F("playNotBusy")); if (digitalRead(nDFPlayer_BUSY) == HIGH) { dfPlayer.next(); } else { - Serial.println("DFPlayer is still busy/playing."); + DBG_PRINTLN(F("DFPlayer is still busy/playing.")); } if (dfPlayer.available()) @@ -319,25 +324,25 @@ void playNotBusyLevel(int level) if (currentlyMuted) { - Serial.println("Muted: skipping DFPlayer playback."); + DBG_PRINTLN(F("Muted: skipping DFPlayer playback.")); return; } if (level <= 0) { - Serial.println("Silent level: skipping DFPlayer playback."); + DBG_PRINTLN(F("Silent level: skipping DFPlayer playback.")); return; } - Serial.println("playNotBusyLevel"); + DBG_PRINTLN(F("playNotBusyLevel")); if (digitalRead(nDFPlayer_BUSY) == HIGH) { dfPlayer.play(level + 1); - Serial.println("Track command sent."); + DBG_PRINTLN(F("Track command sent.")); } else { - Serial.println("DFPlayer is still busy/playing."); + DBG_PRINTLN(F("DFPlayer is still busy/playing.")); } if (dfPlayer.available()) @@ -355,7 +360,7 @@ bool playAlarmLevel(int alarmNumberToPlay) if (currentlyMuted) { - Serial.println("Muted: skipping alarm playback."); + DBG_PRINTLN(F("Muted: skipping alarm playback.")); return false; } @@ -377,20 +382,20 @@ bool playAlarmLevel(int alarmNumberToPlay) if (trackNumber <= 0 || trackNumber > numberFilesDF) { - Serial.print("Invalid DFPlayer track number: "); - Serial.println(trackNumber); + DBG_PRINT(F("Invalid DFPlayer track number: ")); + DBG_PRINTLN(trackNumber); return false; } if (digitalRead(nDFPlayer_BUSY) == HIGH) { - Serial.print("Playing alarm track: "); - Serial.println(trackNumber); + DBG_PRINT(F("Playing alarm track: ")); + DBG_PRINTLN(trackNumber); dfPlayer.play(trackNumber); } else { - Serial.println("Not done playing previous file."); + DBG_PRINTLN(F("Not done playing previous file.")); return false; } diff --git a/Firmware/GPAD_API/GPAD_API/GPAD_API.ino b/Firmware/GPAD_API/GPAD_API/GPAD_API.ino index 8980a0e..700ce4d 100644 --- a/Firmware/GPAD_API/GPAD_API/GPAD_API.ino +++ b/Firmware/GPAD_API/GPAD_API/GPAD_API.ino @@ -59,6 +59,7 @@ #include #include +#include #include // From library https://github.com/knolleary/ @@ -79,6 +80,7 @@ #include "DFPlayer.h" #include "GPAD_menu.h" #include "mqtt_handler.h" +#include "debug_macros.h" AsyncWebServer server(80); AsyncWebSocket ws("/ws"); @@ -196,13 +198,33 @@ const char *DEFAULT_MQTT_BROKER_NAME = "broker.hivemq.com"; const char *mqtt_user = ""; const char *mqtt_password = ""; #else -const char *DEFAULT_MQTT_BROKER_NAME = "public.cloud.shiftr.io"; -const char *mqtt_user = "public"; -const char *mqtt_password = "public"; +const char *DEFAULT_MQTT_BROKER_NAME = "krakepubinv.cloud.shiftr.io"; +const char *mqtt_user = "krakepubinv"; +const char *mqtt_password = "DlDmkWjp4I4kgDcA"; #endif const size_t MQTT_BROKER_MAX_LEN = 64; const char *MQTT_CONFIG_PATH = "/mqtt.json"; char mqtt_broker_name[MQTT_BROKER_MAX_LEN] = {0}; +char mqtt_user_storage[32] = {0}; +char mqtt_password_storage[64] = {0}; +struct BrokerOption +{ + const char *label; + const char *host; + uint16_t port; + const char *username; + const char *password; +}; +const BrokerOption brokerOptions[] = { + {"Public Shiftr", "public.cloud.shiftr.io", 1883, "public", "public"}, + {"Krake PubInv", "krakepubinv.cloud.shiftr.io", 1883, "krakepubinv", "DlDmkWjp4I4kgDcA"}, +}; +const uint8_t BROKER_OPTION_COUNT = sizeof(brokerOptions) / sizeof(brokerOptions[0]); +const uint8_t DEFAULT_BROKER_INDEX = 1; +const uint8_t MQTT_FAILOVER_THRESHOLD = 5; +uint8_t selectedBrokerIndex = DEFAULT_BROKER_INDEX; +uint8_t activeBrokerIndex = DEFAULT_BROKER_INDEX; +uint8_t mqttFailCount = 0; const uint8_t MAX_EXTRA_TOPICS = 4; const size_t MAX_TOPIC_LEN = 64; char subscribe_Extra_Topics[MAX_EXTRA_TOPICS][MAX_TOPIC_LEN]; @@ -219,7 +241,7 @@ const uint8_t MAX_WATCH_TOPICS = 12; char watchedTopics[MAX_WATCH_TOPICS][MAX_TOPIC_LEN]; uint8_t watchedTopicCount = 0; unsigned long wifiResetRequestedAtMs = 0; -const unsigned long MQTT_RECONNECT_INTERVAL_MS = 5000; +const unsigned long MQTT_RECONNECT_INTERVAL_MS = 3000; const uint16_t MQTT_SOCKET_TIMEOUT_SECONDS = 2; unsigned long lastMqttReconnectAttemptMs = 0; bool mqttReconnectRequested = false; @@ -241,11 +263,15 @@ struct TrackedKrake TrackedKrake trackedKrakes[MAX_TRACKED_KRAKES]; String jsonEscape(const String &raw); +const char *mqttStateDescription(int state); bool endsWithAckTopic(const char *topic); bool isAllowedPublishTopic(const String &topic); bool extractJsonString(const String &json, const char *key, String &value, int startPos = 0, int *valueEndPos = nullptr); bool writeMqttConfig(); bool loadMqttConfig(); +void applyActiveMqttBrokerConfig(); +bool selectMqttBrokerOption(uint8_t index); +int8_t findBrokerOptionByHost(const char *host); bool parseCsvIntoTopics(const String &rawTopics, char dest[][MAX_TOPIC_LEN], uint8_t &count, uint8_t maxTopics); String joinTopicsCsv(const char topics[][MAX_TOPIC_LEN], uint8_t count); @@ -285,8 +311,13 @@ unsigned long nextLEDchangee_ms = 5000; // time in ms. //wifi disconnect event void onWiFiDisconnect(WiFiEvent_t event, WiFiEventInfo_t info) { - debugSerial.print("\nWifi Disconnected. Reason: "); +#if (DEBUG > 0) + debugSerial.print(F("\nWifi Disconnected. Reason: ")); debugSerial.println(info.wifi_sta_disconnected.reason); +#else + (void)event; + (void)info; +#endif // OPTION A: Simple Reconnect //WiFi.begin(); @@ -302,14 +333,15 @@ void onWiFiDisconnect(WiFiEvent_t event, WiFiEventInfo_t info) { void serialSplash() { // Serial splash +#if (DEBUG > 0) debugSerial.println(F("===================================")); - debugSerial.println(COMPANY_NAME); - debugSerial.println(MODEL_NAME); + debugSerial.println(F(COMPANY_NAME)); + debugSerial.println(F(MODEL_NAME)); // debugSerial.println(DEVICE_UNDER_TEST); - debugSerial.print(PROG_NAME); - debugSerial.println(FIRMWARE_VERSION); + debugSerial.print(F(PROG_NAME)); + debugSerial.println(F(FIRMWARE_VERSION)); // debugSerial.println(HARDWARE_VERSION); - debugSerial.print("Builtin ESP32 MAC Address: "); + debugSerial.print(F("Builtin ESP32 MAC Address: ")); debugSerial.println(macAddressString); debugSerial.print(F("Alarm Topic: ")); debugSerial.println(subscribe_Alarm_Topic); @@ -317,9 +349,10 @@ void serialSplash() debugSerial.println(mqtt_broker_name); debugSerial.print(F("Compiled at: ")); debugSerial.println(F(__DATE__ " " __TIME__)); // compile date that is used for a unique identifier - debugSerial.println(LICENSE); + debugSerial.println(F(LICENSE)); debugSerial.println(F("===================================")); debugSerial.println(); +#endif } // A periodic message identifying the subscriber (Krake) is on line. @@ -364,12 +397,61 @@ void requestMqttReconnect() { mqttReconnectRequested = true; lastMqttReconnectAttemptMs = 0; + mqttFailCount = 0; if (client.connected()) { client.disconnect(); } } +void applyActiveMqttBrokerConfig() +{ + if (activeBrokerIndex >= BROKER_OPTION_COUNT) + { + activeBrokerIndex = DEFAULT_BROKER_INDEX; + } + + const BrokerOption &broker = brokerOptions[activeBrokerIndex]; + strncpy(mqtt_broker_name, broker.host, MQTT_BROKER_MAX_LEN - 1); + mqtt_broker_name[MQTT_BROKER_MAX_LEN - 1] = '\0'; + mqtt_user = broker.username; + mqtt_password = broker.password; + client.setServer(broker.host, broker.port); + client.setSocketTimeout(MQTT_SOCKET_TIMEOUT_SECONDS); +} + +bool selectMqttBrokerOption(uint8_t index) +{ + if (index >= BROKER_OPTION_COUNT) + { + return false; + } + + selectedBrokerIndex = index; + activeBrokerIndex = index; + applyActiveMqttBrokerConfig(); + writeMqttConfig(); + requestMqttReconnect(); + return true; +} + +int8_t findBrokerOptionByHost(const char *host) +{ + if (host == nullptr || host[0] == '\0') + { + return -1; + } + + for (uint8_t i = 0; i < BROKER_OPTION_COUNT; i++) + { + if (strcmp(host, brokerOptions[i].host) == 0) + { + return static_cast(i); + } + } + return -1; +} + bool reconnect(bool force = false) { if (client.connected()) @@ -390,22 +472,39 @@ bool reconnect(bool force = false) } lastMqttReconnectAttemptMs = now; mqttReconnectRequested = false; + applyActiveMqttBrokerConfig(); char clientId[sizeof(COMPANY_NAME) + MAC_ADDRESS_STRING_LENGTH + 1]; snprintf(clientId, sizeof(clientId), "%s-%s", COMPANY_NAME, macAddressString); char willPayload[DEVICE_ROLE_MAX_LEN + 9]; snprintf(willPayload, sizeof(willPayload), "%s offline", device_role); -#if (DEBUG > 0) debugSerial.print("Attempting MQTT connection at: "); debugSerial.print(millis()); - debugSerial.print("..... "); -#endif + debugSerial.print(" broker="); + debugSerial.print(mqtt_broker_name); + debugSerial.print(" user="); + debugSerial.print((mqtt_user != nullptr && mqtt_user[0] != '\0') ? mqtt_user : ""); + debugSerial.print(" ip="); + debugSerial.print(WiFi.localIP()); + debugSerial.print(" rssi="); + debugSerial.print(WiFi.RSSI()); + debugSerial.print(" clientId="); + debugSerial.print(clientId); + debugSerial.print(" sub="); + debugSerial.print(subscribe_Alarm_Topic); + debugSerial.print(" pub="); + debugSerial.print(publish_Ack_Topic); + debugSerial.print(" ... "); if (client.connect(clientId, mqtt_user, mqtt_password, publish_Ack_Topic, 1, true, willPayload)) { -#if (DEBUG > 0) debugSerial.print("success at: "); debugSerial.println(millis()); -#endif + mqttFailCount = 0; + if (selectedBrokerIndex != activeBrokerIndex) + { + selectedBrokerIndex = activeBrokerIndex; + writeMqttConfig(); + } char onlinePayload[DEVICE_ROLE_MAX_LEN + 8]; snprintf(onlinePayload, sizeof(onlinePayload), "%s online", device_role); queueMqtt(publish_Ack_Topic, onlinePayload, true); @@ -425,14 +524,54 @@ bool reconnect(bool force = false) return true; } -#if (DEBUG > 0) debugSerial.print("failed, rc="); - debugSerial.println(client.state()); -#endif + debugSerial.print(client.state()); + debugSerial.print(" "); + debugSerial.println(mqttStateDescription(client.state())); + mqttFailCount++; + if (mqttFailCount >= MQTT_FAILOVER_THRESHOLD) + { + mqttFailCount = 0; + activeBrokerIndex = (activeBrokerIndex + 1) % BROKER_OPTION_COUNT; + const BrokerOption &nextBroker = brokerOptions[activeBrokerIndex]; + debugSerial.print("MQTT failover to "); + debugSerial.print(nextBroker.label); + debugSerial.print(" host="); + debugSerial.println(nextBroker.host); + } yield(); return false; } +const char *mqttStateDescription(int state) +{ + switch (state) + { + case MQTT_CONNECTION_TIMEOUT: + return "connection timeout"; + case MQTT_CONNECTION_LOST: + return "connection lost"; + case MQTT_CONNECT_FAILED: + return "connect failed"; + case MQTT_DISCONNECTED: + return "disconnected"; + case MQTT_CONNECTED: + return "connected"; + case MQTT_CONNECT_BAD_PROTOCOL: + return "bad protocol"; + case MQTT_CONNECT_BAD_CLIENT_ID: + return "bad client id"; + case MQTT_CONNECT_UNAVAILABLE: + return "server unavailable"; + case MQTT_CONNECT_BAD_CREDENTIALS: + return "bad credentials"; + case MQTT_CONNECT_UNAUTHORIZED: + return "unauthorized"; + default: + return "unknown"; + } +} + bool isManagedSubscribedTopic(const char *topic) { if (strcmp(topic, STATUS_DISCOVERY_TOPIC) == 0) @@ -724,6 +863,10 @@ String trackedKrakesJson() payload += "\",\"watchedTopics\":\""; payload += jsonEscape(joinedWatchedTopics()); payload += "\",\"mqttConnected\":" + String(client.connected() ? "true" : "false") + ","; + payload += "\"mqttState\":" + String(client.state()) + ","; + payload += "\"mqttStateText\":\"" + jsonEscape(String(mqttStateDescription(client.state()))) + "\","; + payload += "\"selectedBrokerIndex\":" + String(selectedBrokerIndex) + ","; + payload += "\"activeBrokerIndex\":" + String(activeBrokerIndex) + ","; payload += "\"broker\":\"" + jsonEscape(String(mqtt_broker_name)) + "\","; payload += "\"subscribeAlarmTopic\":\"" + jsonEscape(String(subscribe_Alarm_Topic)) + "\","; payload += "\"publishAckTopic\":\"" + jsonEscape(String(publish_Ack_Topic)) + "\","; @@ -971,6 +1114,9 @@ bool writeMqttConfig() String payload = "{"; payload += "\"broker\":\"" + jsonEscape(String(mqtt_broker_name)) + "\","; + payload += "\"selectedBrokerIndex\":\"" + String(selectedBrokerIndex) + "\","; + payload += "\"mqttUser\":\"" + jsonEscape(String(mqtt_user != nullptr ? mqtt_user : "")) + "\","; + payload += "\"mqttPassword\":\"" + jsonEscape(String(mqtt_password != nullptr ? mqtt_password : "")) + "\","; payload += "\"subscribeTopic\":\"" + jsonEscape(String(subscribe_Alarm_Topic)) + "\","; payload += "\"publishTopic\":\"" + jsonEscape(String(publish_Ack_Topic)) + "\","; payload += "\"subscribeTopics\":\"" + jsonEscape(joinedExtraTopics()) + "\","; @@ -1003,6 +1149,7 @@ bool loadMqttConfig() file.close(); String value; + bool loadedBrokerIndex = false; if (extractJsonString(content, "broker", value)) { value.trim(); @@ -1012,6 +1159,37 @@ bool loadMqttConfig() } } + if (extractJsonString(content, "selectedBrokerIndex", value)) + { + value.trim(); + const int parsedIndex = value.toInt(); + if (value.length() > 0 && parsedIndex >= 0 && parsedIndex < BROKER_OPTION_COUNT) + { + selectedBrokerIndex = static_cast(parsedIndex); + activeBrokerIndex = selectedBrokerIndex; + loadedBrokerIndex = true; + } + } + + if (extractJsonString(content, "mqttUser", value)) + { + value.trim(); + if (value.length() < sizeof(mqtt_user_storage)) + { + value.toCharArray(mqtt_user_storage, sizeof(mqtt_user_storage)); + mqtt_user = mqtt_user_storage; + } + } + + if (extractJsonString(content, "mqttPassword", value)) + { + if (value.length() < sizeof(mqtt_password_storage)) + { + value.toCharArray(mqtt_password_storage, sizeof(mqtt_password_storage)); + mqtt_password = mqtt_password_storage; + } + } + if (extractJsonString(content, "subscribeTopic", value)) { value.trim(); @@ -1058,6 +1236,16 @@ bool loadMqttConfig() value.toCharArray(device_role, DEVICE_ROLE_MAX_LEN); } } + if (!loadedBrokerIndex) + { + const int8_t matchedIndex = findBrokerOptionByHost(mqtt_broker_name); + if (matchedIndex >= 0) + { + selectedBrokerIndex = static_cast(matchedIndex); + activeBrokerIndex = selectedBrokerIndex; + } + } + applyActiveMqttBrokerConfig(); return true; } @@ -1095,17 +1283,32 @@ bool applyMuteSetting(const String &rawValue) bool applyBrokerSetting(const String &broker) { - if (broker.length() == 0 || broker.length() >= MQTT_BROKER_MAX_LEN) + String normalized = broker; + normalized.trim(); + if (normalized.startsWith("wss://")) + { + normalized.remove(0, 6); + } + else if (normalized.startsWith("ws://")) + { + normalized.remove(0, 5); + } + + if (normalized.length() == 0 || normalized.length() >= MQTT_BROKER_MAX_LEN) { return false; } - broker.toCharArray(mqtt_broker_name, MQTT_BROKER_MAX_LEN); - client.setServer(mqtt_broker_name, 1883); - client.setSocketTimeout(MQTT_SOCKET_TIMEOUT_SECONDS); - requestMqttReconnect(); - writeMqttConfig(); - return true; + for (uint8_t i = 0; i < BROKER_OPTION_COUNT; i++) + { + if (normalized.equalsIgnoreCase(brokerOptions[i].host) || + normalized.equalsIgnoreCase(brokerOptions[i].label)) + { + return selectMqttBrokerOption(i); + } + } + + return false; } bool applyRoleSetting(const String &rawRole) @@ -1312,6 +1515,20 @@ String templateProcessor(const String &var) return WifiOTA::processor(var); } +void sendStaticFile(AsyncWebServerRequest *request, const char *path, const char *contentType) +{ + char gzipPath[64]; + snprintf(gzipPath, sizeof(gzipPath), "%s.gz", path); + if (LittleFS.exists(gzipPath)) + { + AsyncWebServerResponse *response = request->beginResponse(LittleFS, gzipPath, contentType); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + return; + } + request->send(LittleFS, path, contentType); +} + // Elegant OTA Setup void setupOTA() @@ -1325,10 +1542,10 @@ void setupOTA() { request->send(LittleFS, "/monitor.html", "text/html", false, templateProcessor); }); server.on("/device-monitor", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/device-monitor.html", "text/html"); }); + { sendStaticFile(request, "/device-monitor.html", "text/html"); }); server.on("/device-monitor.html", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/device-monitor.html", "text/html"); }); + { sendStaticFile(request, "/device-monitor.html", "text/html"); }); server.on("/debug-logs", HTTP_GET, [](AsyncWebServerRequest *request) { request->redirect("/monitor"); }); @@ -1368,22 +1585,22 @@ void setupOTA() request->send(200, "text/plain", serialLogBuffer); }); server.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/settings.html", "text/html"); }); + { sendStaticFile(request, "/settings.html", "text/html"); }); server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/setup.html", "text/html"); }); + { sendStaticFile(request, "/setup.html", "text/html"); }); server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request) { String ssid; String password; const bool hasStored = wifiManager.loadCredentials(ssid, password); - std::vector credentials; + WifiOTA::Manager::CredentialList credentials; wifiManager.loadCredentialsList(credentials); String payload = "{"; payload += "\"hasStored\":" + String(hasStored ? "true" : "false") + ","; payload += "\"ssid\":\"" + jsonEscape(ssid) + "\","; - payload += "\"count\":" + String(credentials.size()); + payload += "\"count\":" + String(credentials.count); payload += "}"; request->send(200, "application/json", payload); }); @@ -1420,21 +1637,27 @@ void setupOTA() request->send(500, "text/plain", "failed to save wifi.json"); return; } - request->send(200, "text/plain", "wifi.json saved"); }); + request->send(200, "text/plain", "wifi.json saved; restart or reconnect the device to apply"); }); server.on("/manual", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/manual.html", "text/html"); }); + { sendStaticFile(request, "/manual.html", "text/html"); }); server.on("/PMD_GPAD_API", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/PMD_GPAD_API.html", "text/html"); }); + { sendStaticFile(request, "/PMD_GPAD_API.html", "text/html"); }); server.on("/PMD_GPAD_API.html", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send(LittleFS, "/PMD_GPAD_API.html", "text/html"); }); + { sendStaticFile(request, "/PMD_GPAD_API.html", "text/html"); }); server.on("/settings-data", HTTP_GET, [](AsyncWebServerRequest *request) { String payload = "{"; payload += "\"broker\":\"" + jsonEscape(String(mqtt_broker_name)) + "\","; + payload += "\"mqttUser\":\"" + jsonEscape(String(mqtt_user != nullptr ? mqtt_user : "")) + "\","; + payload += "\"mqttConnected\":" + String(client.connected() ? "true" : "false") + ","; + payload += "\"mqttState\":" + String(client.state()) + ","; + payload += "\"mqttStateText\":\"" + jsonEscape(String(mqttStateDescription(client.state()))) + "\","; + payload += "\"selectedBrokerIndex\":" + String(selectedBrokerIndex) + ","; + payload += "\"activeBrokerIndex\":" + String(activeBrokerIndex) + ","; payload += "\"subscribeTopic\":\"" + jsonEscape(String(subscribe_Alarm_Topic)) + "\","; payload += "\"publishTopic\":\"" + jsonEscape(String(publish_Ack_Topic)) + "\","; payload += "\"extraTopics\":\"" + jsonEscape(joinedExtraTopics()) + "\","; @@ -1459,9 +1682,39 @@ void setupOTA() } else { - broker.toCharArray(mqtt_broker_name, MQTT_BROKER_MAX_LEN); - client.setServer(mqtt_broker_name, 1883); - client.setSocketTimeout(MQTT_SOCKET_TIMEOUT_SECONDS); + if (!applyBrokerSetting(broker)) + { + errorMessage += "invalid broker;"; + } + } + } + + if (request->hasParam("mqttUser", true)) + { + String user = request->getParam("mqttUser", true)->value(); + user.trim(); + if (user.length() >= sizeof(mqtt_user_storage)) + { + errorMessage += "invalid mqttUser;"; + } + else + { + user.toCharArray(mqtt_user_storage, sizeof(mqtt_user_storage)); + mqtt_user = mqtt_user_storage; + } + } + + if (request->hasParam("mqttPassword", true)) + { + String password = request->getParam("mqttPassword", true)->value(); + if (password.length() >= sizeof(mqtt_password_storage)) + { + errorMessage += "invalid mqttPassword;"; + } + else + { + password.toCharArray(mqtt_password_storage, sizeof(mqtt_password_storage)); + mqtt_password = mqtt_password_storage; } } @@ -1689,11 +1942,32 @@ void setupOTA() server.serveStatic("/", LittleFS, "/"); server.onNotFound([](AsyncWebServerRequest *request) - { request->send(LittleFS, "/404.html", "text/html"); }); + { sendStaticFile(request, "/404.html", "text/html"); }); // End of ELegant OTA Setup } +void handleWifiConnected() +{ +#if defined HMWK || defined KRAKE + if (!client.connected()) + { + reconnect(true); + } +#endif + + clearLCD(); + IPAddress currentAddress = wifiManager.getAddress(); + splashLCD(wifiManager.getMode(), currentAddress); +} + +void handleWifiApStarted() +{ + clearLCD(); + IPAddress currentAddress = wifiManager.getAddress(); + splashLCD(wifiManager.getMode(), currentAddress); +} + void setup() { pinMode(LED_BUILTIN, OUTPUT); // set the LED pin mode @@ -1757,8 +2031,7 @@ void setup() clearPublishTopics(); clearTrackedKrakes(); clearWatchedTopics(); - client.setServer(mqtt_broker_name, 1883); // Default MQTT port, this is a TCP port. - client.setSocketTimeout(MQTT_SOCKET_TIMEOUT_SECONDS); + applyActiveMqttBrokerConfig(); // PubSubClient uses MQTT over TCP, not WSS. client.setCallback(callback); #if (DEBUG > 0) @@ -1801,8 +2074,7 @@ void setup() publish_Default_Topic[MAX_TOPIC_LEN - 1] = '\0'; loadMqttConfig(); - client.setServer(mqtt_broker_name, 1883); - client.setSocketTimeout(MQTT_SOCKET_TIMEOUT_SECONDS); + applyActiveMqttBrokerConfig(); #if (DEBUG > 1) debugSerial.println("XXXXXXX"); @@ -1821,59 +2093,61 @@ void setup() // req for Wifi Man and OTA #if defined HMWK || defined KRAKE - auto connectedCallback = [&]() - { - if (!client.connected()) - { - reconnect(true); - } - - clearLCD(); - IPAddress currentAddress = wifiManager.getAddress(); - splashLCD(wifiManager.getMode(), currentAddress); - }; - wifiManager.setConnectedCallback(connectedCallback); + wifiManager.setConnectedCallback(handleWifiConnected); #endif - auto apStartedCallback = [&]() - { - clearLCD(); - IPAddress currentAddress = wifiManager.getAddress(); - splashLCD(wifiManager.getMode(), currentAddress); - }; - wifiManager.setApStartedCallback(apStartedCallback); + wifiManager.setApStartedCallback(handleWifiApStarted); wifiManager.connect(setupSsid); +#if (DEBUG > 0) debugSerial.println(F("WiFi Manager connected.")); - debugSerial.println(F("initLiffleFS")); +#endif setupOTA(); +#if (DEBUG > 0) debugSerial.println(F("setupOTA")); +#endif + #if ENABLE_OTA ElegantOTA.begin(&server); + #endif +#if (DEBUG > 0) debugSerial.println(F("ElegantOTA.begin")); +#endif server.begin(); // Start server web socket to render pages +#if (DEBUG > 0) debugSerial.println(F("Start server web socket to render pages")); +#endif initRotator(); +#if (DEBUG > 0) debugSerial.println(F("initRotator")); +#endif splashLCD(wifiManager.getMode(), wifiManager.getAddress()); +#if (DEBUG > 0) debugSerial.println(F("splashLCD")); +#endif + #if ENABLE_DFPLAYER setupDFPlayer(); + #endif +#if (DEBUG > 0) debugSerial.println(F("setupDFPlayer")); +#endif setup_GPAD_menu(); +#if (DEBUG > 0) debugSerial.println(F("setupGPAD_menu")); // Need this to work here: printInstructions(serialport); debugSerial.println(F("Done With Setup!")); +#endif turnOnAllLamps(); digitalWrite(LED_BUILTIN, LOW); // turn the LED off at end of setup } // end of setup() @@ -1905,6 +2179,32 @@ void serviceMqttClient() #endif } +void serviceRuntimeDiagnostics() +{ +#if ENABLE_DEBUG_LOGS + static unsigned long lastDiagnosticsMs = 0; + const unsigned long now = millis(); + if (lastDiagnosticsMs != 0 && (now - lastDiagnosticsMs) < 10000) + { + return; + } + lastDiagnosticsMs = now; + + debugSerial.print(F("diag heap=")); + debugSerial.print(ESP.getFreeHeap()); + debugSerial.print(F(" largest=")); + debugSerial.print(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + debugSerial.print(F(" mqtt=")); + debugSerial.print(client.connected() ? F("connected") : F("disconnected")); + debugSerial.print(F(" state=")); + debugSerial.print(mqttStateDescription(client.state())); + debugSerial.print(F(" ui=")); + debugSerial.print(lcdUiStateName()); + debugSerial.print(F(" enc=")); + debugSerial.println(rotaryEncoderEventCount); +#endif +} + void serviceDeferredReset() { #if defined HMWK || defined KRAKE @@ -1972,8 +2272,6 @@ void loop() if (menu_just_exited) { - lcd.clear(); - lcd.noBacklight(); restoreAlarmLevel(&debugSerial); menu_just_exited = false; } @@ -1987,6 +2285,7 @@ void loop() #if defined HMWK || defined KRAKE publishOnLineMsg(); serviceHeapDiagnostics(); + serviceRuntimeDiagnostics(); wink(); // The builtin LED #endif diff --git a/Firmware/GPAD_API/GPAD_API/GPAD_HAL.cpp b/Firmware/GPAD_API/GPAD_API/GPAD_HAL.cpp index 8cf618d..37c78af 100644 --- a/Firmware/GPAD_API/GPAD_API/GPAD_HAL.cpp +++ b/Firmware/GPAD_API/GPAD_API/GPAD_HAL.cpp @@ -28,9 +28,11 @@ #include "mqtt_handler.h" #include "debug_macros.h" #include +#include #include #include #include +#include using namespace gpad_hal; @@ -175,6 +177,12 @@ extern char macAddressString[13]; extern int muteTimeoutMinutes; extern char currentAlarmId[11]; extern char currentAlarmType[4]; +extern PubSubClient client; +extern char mqtt_broker_name[]; +extern uint8_t selectedBrokerIndex; +extern uint8_t activeBrokerIndex; +extern uint8_t mqttFailCount; +extern const char *mqttStateDescription(int state); // For LCD // #include @@ -227,6 +235,19 @@ namespace const unsigned long ALARM_UI_BURST_SETTLE_MS = 150; const unsigned long DFPLAYER_POLL_INTERVAL_MS = 100; const uint8_t ALARM_UI_BURST_REQUEST_COUNT = 3; + const uint8_t LCD_COLS = 20; + const uint8_t LCD_ROWS = 4; + const uint8_t LCD_STATUS_COL = 16; + const uint8_t LCD_MAIN_WIDTH = LCD_STATUS_COL; + const uint8_t LCD_ALARM_WINDOW_WIDTH = LCD_COLS * 2; + const uint8_t ICON_WIFI = 1; + const uint8_t ICON_BROKER = 2; + const uint8_t ICON_VOLUME = 3; + const uint8_t ICON_MUTE = 4; + const uint8_t ICON_SETTINGS = 5; + const unsigned long LCD_RENDER_MIN_INTERVAL_MS = 150; + const unsigned long LCD_SCROLL_STEP_MS = 400; + const unsigned long LCD_SCROLL_PAUSE_MS = 1500; bool alarmUiUpdatePending = false; bool alarmAudioUpdatePending = false; @@ -235,10 +256,417 @@ namespace unsigned long lastAlarmUiUpdateMs = 0; unsigned long lastAlarmAudioUpdateMs = 0; uint8_t alarmUiPendingRequestCount = 0; + bool lcdDirty = true; + bool alarmActionSelectorActive = false; + uint8_t alarmActionSelection = 0; + uint8_t alarmQueueCount = 0; + unsigned long lastLcdRenderMs = 0; + char alarmDisplayBuffer[128] = ""; + size_t scrollIndex = 0; + unsigned long lastScrollMs = 0; + bool scrollEnabled = false; + enum LcdFocus : uint8_t + { + FOCUS_ALARM_ACTIONS = 0, + FOCUS_WIFI = 1, + FOCUS_BROKER = 2, + FOCUS_MUTE = 3, + FOCUS_SETTINGS = 4, + }; + enum LcdPage : uint8_t + { + PAGE_MAIN = 0, + PAGE_WIFI = 1, + PAGE_BROKER = 2, + PAGE_MUTE = 3, + PAGE_INFO = 4, + PAGE_WIFI_STATUS = 5, + }; + enum LcdUiState : uint8_t + { + MAIN_PAGE = 0, + SETTINGS_MENU = 1, + ICON_MENU = 2, + ALARM_ACTION_SELECT = 3, + ACTION_FEEDBACK = 4, + INFO_PAGE = 5, + }; + LcdFocus lcdFocus = FOCUS_SETTINGS; + LcdPage lcdPage = PAGE_MAIN; + LcdUiState lcdUiState = MAIN_PAGE; + uint8_t lcdPageOption = 0; + char actionFeedbackText[LCD_COLS + 1] = ""; + unsigned long actionFeedbackStartMs = 0; + const unsigned long ACTION_FEEDBACK_DURATION_MS = 2500; + char previousLcdRows[LCD_ROWS][LCD_COLS + 1] = { + " ", + " ", + " ", + " ", + }; + + const char *alarmLevelLabel(AlarmLevel level) + { + switch (level) + { + case informational: + return "INFO"; + case problem: + return "PROB"; + case warning: + return "WARN"; + case critical: + return "CRIT"; + case panic: + return "PANIC"; + case silent: + default: + return "OK"; + } + } + + void markLcdDirty() + { + lcdDirty = true; + alarmUiUpdatePending = true; + lastAlarmUiRequestMs = millis(); + } + + void setLcdUiState(LcdUiState state) + { + lcdUiState = state; + markLcdDirty(); + } + + bool alarmIsActive() + { + return currentLevel != silent; + } + + uint8_t pageOptionCount() + { + switch (lcdPage) + { + case PAGE_WIFI: + return 1; + case PAGE_BROKER: + return 2; + case PAGE_MUTE: + return 3; + case PAGE_MAIN: + default: + return 0; + } + } + + LcdFocus nextFocus(LcdFocus focus, bool clockwise) + { + const uint8_t minFocus = alarmIsActive() ? FOCUS_ALARM_ACTIONS : FOCUS_WIFI; + const uint8_t maxFocus = FOCUS_SETTINGS; + uint8_t value = static_cast(focus); + if (value < minFocus || value > maxFocus) + { + value = alarmIsActive() ? FOCUS_ALARM_ACTIONS : FOCUS_SETTINGS; + } + + if (clockwise) + { + value = (value >= maxFocus) ? minFocus : value + 1; + } + else + { + value = (value <= minFocus) ? maxFocus : value - 1; + } + return static_cast(value); + } + + void clearRowBuffer(char *row) + { + memset(row, ' ', LCD_COLS); + row[LCD_COLS] = '\0'; + } + + void copyText(char *row, uint8_t col, uint8_t width, const char *text) + { + if (row == nullptr || text == nullptr || col >= LCD_COLS) + { + return; + } + + uint8_t written = 0; + while (text[written] != '\0' && written < width && (col + written) < LCD_COLS) + { + row[col + written] = text[written]; + written++; + } + } + + void renderTwoLineMessageWindow(char rows[LCD_ROWS][LCD_COLS + 1], const char *msg, size_t offset) + { + char alarmWindow[LCD_ALARM_WINDOW_WIDTH + 1]; + memset(alarmWindow, ' ', LCD_ALARM_WINDOW_WIDTH); + alarmWindow[LCD_ALARM_WINDOW_WIDTH] = '\0'; + + const size_t len = strlen(msg); + for (size_t i = 0; i < LCD_ALARM_WINDOW_WIDTH; i++) + { + size_t src = offset + i; + if (src < len) + { + alarmWindow[i] = msg[src]; + } + } + + memcpy(rows[1], alarmWindow, LCD_COLS); + rows[1][LCD_COLS] = '\0'; + memcpy(rows[2], alarmWindow + LCD_COLS, LCD_COLS); + rows[2][LCD_COLS] = '\0'; + } + + void formatMain(char *row, const char *fmt, ...) + { + char temp[LCD_MAIN_WIDTH + 1]; + va_list args; + va_start(args, fmt); + vsnprintf(temp, sizeof(temp), fmt, args); + va_end(args); + copyText(row, 0, LCD_MAIN_WIDTH, temp); + } + + void formatFullRow(char *row, const char *fmt, ...) + { + char temp[LCD_COLS + 1]; + va_list args; + va_start(args, fmt); + vsnprintf(temp, sizeof(temp), fmt, args); + va_end(args); + copyText(row, 0, LCD_COLS, temp); + } + + unsigned long remainingMuteMinutes() + { + if (!currentlyMuted || muteTimeoutEndMillis == 0) + { + return 0; + } + + const unsigned long now = millis(); + if ((now - muteTimeoutEndMillis) < 0x80000000UL) + { + return 0; + } + + return ((muteTimeoutEndMillis - now) + 59999UL) / 60000UL; + } + + void currentSsid(char *dest, size_t destLen) + { + if (destLen == 0) + { + return; + } + dest[0] = '\0'; + wifi_ap_record_t apInfo; + if (WiFi.status() == WL_CONNECTED && esp_wifi_sta_get_ap_info(&apInfo) == ESP_OK) + { + snprintf(dest, destLen, "%s", reinterpret_cast(apInfo.ssid)); + return; + } + snprintf(dest, destLen, "%s", "Not connected"); + } + + void ipAddressText(char *dest, size_t destLen) + { + if (destLen == 0) + { + return; + } + IPAddress ip = WiFi.localIP(); + if (WiFi.status() != WL_CONNECTED) + { + ip = WiFi.softAPIP(); + } + snprintf(dest, destLen, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); + } + + const char *mqttStatusText() + { + if (client.connected()) + { + return "Connected"; + } + if (WiFi.status() == WL_CONNECTED) + { + return "Waiting"; + } + return "Offline"; + } + + void filterCopy(char *dest, size_t destLen, const char *src) + { + if (destLen == 0) + { + return; + } + dest[0] = '\0'; + if (src == nullptr) + { + return; + } + + size_t out = 0; + for (size_t in = 0; src[in] != '\0' && out < (destLen - 1); in++) + { + const char c = src[in]; + if (isPrintable(c) || c == ' ') + { + dest[out++] = c; + } + } + dest[out] = '\0'; + } + + void installLcdIcons() + { + byte wifiIcon[8] = { + B00000, + B01110, + B10001, + B00100, + B01010, + B00000, + B00100, + B00000, + }; + byte brokerIcon[8] = { + B11111, + B10001, + B10101, + B10001, + B10101, + B10001, + B11111, + B00000, + }; + byte volumeIcon[8] = { + B00001, + B00011, + B01111, + B01111, + B01111, + B00011, + B00001, + B00000, + }; + byte muteIcon[8] = { + B10001, + B01010, + B00100, + B01010, + B10001, + B00000, + B11111, + B00000, + }; + byte settingsIcon[8] = { + B00100, + B10101, + B01110, + B11111, + B01110, + B10101, + B00100, + B00000, + }; + + Real_lcd.createChar(ICON_WIFI, wifiIcon); + Real_lcd.createChar(ICON_BROKER, brokerIcon); + Real_lcd.createChar(ICON_VOLUME, volumeIcon); + Real_lcd.createChar(ICON_MUTE, muteIcon); + Real_lcd.createChar(ICON_SETTINGS, settingsIcon); + } + + void writeStatusIcons(char rows[LCD_ROWS][LCD_COLS + 1]) + { + rows[0][LCD_STATUS_COL] = ICON_WIFI; + rows[0][LCD_STATUS_COL + 1] = ICON_BROKER; + rows[0][LCD_STATUS_COL + 2] = currentlyMuted ? ICON_MUTE : ICON_VOLUME; + rows[0][LCD_STATUS_COL + 3] = ICON_SETTINGS; + + // The compact status cluster lives on row 0; lower rows keep all 20 cells + // available for alarm details and the settings/action prompts. + } + + void writeRowToLcd(uint8_t row, const char *text) + { + lcd.setCursor(0, row); + for (uint8_t col = 0; col < LCD_COLS; col++) + { + lcd.write(static_cast(text[col])); + } + } + + void renderRows(char rows[LCD_ROWS][LCD_COLS + 1]) + { + const unsigned long now = millis(); + if (!lcdDirty && lastLcdRenderMs != 0 && (now - lastLcdRenderMs) < LCD_RENDER_MIN_INTERVAL_MS) + { + return; + } + + bool changed = false; + for (uint8_t row = 0; row < LCD_ROWS; row++) + { + rows[row][LCD_COLS] = '\0'; + if (memcmp(previousLcdRows[row], rows[row], LCD_COLS) != 0) + { + writeRowToLcd(row, rows[row]); + memcpy(previousLcdRows[row], rows[row], LCD_COLS); + previousLcdRows[row][LCD_COLS] = '\0'; + changed = true; + } + } + + if (changed) + { + lastLcdRenderMs = now; + } + + lcd.noBlink(); + lcd.noCursor(); + if (!running_menu) + { + if (lcdUiState == ALARM_ACTION_SELECT) + { + const uint8_t actionCols[3] = {0, 4, 12}; + lcd.setCursor(actionCols[alarmActionSelection], 3); + lcd.cursor(); + lcd.blink(); + } + else if (lcdUiState == MAIN_PAGE) + { + if (lcdFocus >= FOCUS_WIFI && lcdFocus <= FOCUS_SETTINGS) + { + lcd.setCursor(LCD_STATUS_COL + static_cast(lcdFocus) - FOCUS_WIFI, 0); + lcd.cursor(); + lcd.blink(); + } + } + else if (lcdUiState == ICON_MENU) + { + const uint8_t optionRow = (lcdPageOption + 2 >= LCD_ROWS) ? (LCD_ROWS - 1) : (lcdPageOption + 2); + lcd.setCursor(0, optionRow); + lcd.cursor(); + lcd.blink(); + } + } + lcdDirty = false; + } void markAlarmUiAudioPending(bool includeAudio = true) { alarmUiUpdatePending = true; + lcdDirty = true; if (includeAudio) { alarmAudioUpdatePending = true; @@ -399,7 +827,15 @@ void encoderSwitchCallback(byte buttonEvent) // annunciateAlarmLevel(local_ptr_to_serial); // printAlarmState(local_ptr_to_serial); - registerRotaryEncoderPress(); + if (running_menu) + { + registerRotaryEncoderPress(); + } + else if (!alarmActionSelectorHandlePress()) + { + setLcdUiState(SETTINGS_MENU); + reset_menu_navigation(); + } break; case onRelease: // Do nothing... @@ -415,6 +851,14 @@ void encoderSwitchCallback(byte buttonEvent) DBG_PRINT(F("ENCODER_SWITCH Button Long Pressed For ")); DBG_PRINT(longPressTime); DBG_PRINTLN(F("ms")); + alarmActionSelectorActive = false; + lcdPage = PAGE_MAIN; + lcdFocus = FOCUS_SETTINGS; + setLcdUiState(SETTINGS_MENU); + if (!running_menu) + { + reset_menu_navigation(); + } break; // onMultiHit is indicated when you hit the button @@ -504,6 +948,7 @@ void GPAD_HAL_setup(Stream *serialport, wifi_mode_t wifiMode, IPAddress &deviceI Wire.begin(); Real_lcd.init(); + installLcdIcons(); #if (DEBUG > 0) @@ -835,7 +1280,9 @@ void serviceAlarmUiAudio(Stream *serialport) if (isDue(now, lastDfPlayerPollMs, DFPLAYER_POLL_INTERVAL_MS)) { lastDfPlayerPollMs = now; +#if ENABLE_DFPLAYER dfPlayerUpdate(); +#endif } if (!alarmUiUpdatePending && !alarmAudioUpdatePending) @@ -899,9 +1346,321 @@ void GPAD_HAL_loop() #endif muteTimeoutWatchdog(local_ptr_to_serial); + static unsigned long lastDashboardRefreshMs = 0; + const unsigned long now = millis(); + if (lcdUiState == ACTION_FEEDBACK && (now - actionFeedbackStartMs) >= ACTION_FEEDBACK_DURATION_MS) + { + actionFeedbackText[0] = '\0'; + setLcdUiState(MAIN_PAGE); + } + if (!running_menu && !alarmUiUpdatePending && isDue(now, lastDashboardRefreshMs, ALARM_UI_MIN_INTERVAL_MS)) + { + lastDashboardRefreshMs = now; + alarmUiUpdatePending = true; + lastAlarmUiRequestMs = now; + } serviceAlarmUiAudio(local_ptr_to_serial); } +bool isAlarmActionSelectorActive() +{ + return lcdUiState == ALARM_ACTION_SELECT; +} + +const char *lcdUiStateName() +{ + switch (lcdUiState) + { + case MAIN_PAGE: + return "MAIN_PAGE"; + case SETTINGS_MENU: + return "SETTINGS_MENU"; + case ICON_MENU: + return "ICON_MENU"; + case ALARM_ACTION_SELECT: + return "ALARM_ACTION_SELECT"; + case ACTION_FEEDBACK: + return "ACTION_FEEDBACK"; + case INFO_PAGE: + return "INFO_PAGE"; + default: + return "UNKNOWN"; + } +} + +void resetLcdUiToMainPage() +{ + lcdPage = PAGE_MAIN; + lcdPageOption = 0; + alarmActionSelectorActive = false; + actionFeedbackText[0] = '\0'; + if (alarmIsActive()) + { + lcdFocus = FOCUS_ALARM_ACTIONS; + alarmActionSelection = 0; + } + else + { + lcdFocus = FOCUS_SETTINGS; + } + setLcdUiState(MAIN_PAGE); +} + +void showAlarmActions() +{ + if (!alarmIsActive()) + { + return; + } + lcdPage = PAGE_MAIN; + alarmActionSelectorActive = true; + lcdFocus = FOCUS_ALARM_ACTIONS; + if (lcdUiState != ALARM_ACTION_SELECT) + { + alarmActionSelection = 0; + } + setLcdUiState(ALARM_ACTION_SELECT); +} + +void showActionFeedback(const char *msg) +{ + snprintf(actionFeedbackText, sizeof(actionFeedbackText), "%s", msg != nullptr ? msg : ""); + actionFeedbackStartMs = millis(); + alarmActionSelectorActive = false; + setLcdUiState(ACTION_FEEDBACK); +} + +void showInfoPage() +{ + lcdPage = PAGE_INFO; + lcdPageOption = 0; + alarmActionSelectorActive = false; + setLcdUiState(INFO_PAGE); + requestAlarmRefresh(local_ptr_to_serial, false); +} + +void showWifiStatusPage() +{ + lcdPage = PAGE_WIFI_STATUS; + lcdPageOption = 0; + alarmActionSelectorActive = false; + setLcdUiState(INFO_PAGE); + requestAlarmRefresh(local_ptr_to_serial, false); +} + +void executeSelectedAlarmAction() +{ + const uint8_t selectedAction = alarmActionSelection; + alarmActionSelection = 0; + alarmActionSelectorActive = false; + executeAlarmAction(selectedAction); + switch (selectedAction) + { + case 0: + showActionFeedback("Alarm acknowledged"); + break; + case 1: + showActionFeedback("Alarm dismissed"); + break; + case 2: + showActionFeedback("Alarm shelved"); + break; + default: + break; + } +} + +bool alarmActionSelectorHandleRotation(bool clockwise) +{ + if (lcdUiState == ICON_MENU) + { + const uint8_t count = pageOptionCount(); + if (count > 0) + { + if (clockwise) + { + lcdPageOption = (lcdPageOption + 1) % count; + } + else + { + lcdPageOption = (lcdPageOption == 0) ? (count - 1) : (lcdPageOption - 1); + } + } + markLcdDirty(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + if (alarmIsActive()) + { + if (lcdUiState == MAIN_PAGE && lcdFocus >= FOCUS_WIFI && lcdFocus <= FOCUS_SETTINGS) + { + lcdFocus = nextFocus(lcdFocus, clockwise); + markLcdDirty(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + const bool wasSelectingAlarmAction = (lcdUiState == ALARM_ACTION_SELECT); + showAlarmActions(); + if (!wasSelectingAlarmAction) + { + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + if (clockwise) + { + if (alarmActionSelection >= 2) + { + alarmActionSelectorActive = false; + lcdFocus = FOCUS_WIFI; + setLcdUiState(MAIN_PAGE); + } + else + { + alarmActionSelection++; + } + } + else + { + if (alarmActionSelection == 0) + { + alarmActionSelectorActive = false; + lcdFocus = FOCUS_SETTINGS; + setLcdUiState(MAIN_PAGE); + } + else + { + alarmActionSelection--; + } + } + markLcdDirty(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + if (currentLevel == silent) + { + lcdFocus = nextFocus(lcdFocus, clockwise); + alarmActionSelectorActive = false; + markLcdDirty(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + return false; +} + +bool alarmActionSelectorHandlePress() +{ + if (lcdUiState == INFO_PAGE) + { + resetLcdUiToMainPage(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + if (lcdUiState == ACTION_FEEDBACK) + { + resetLcdUiToMainPage(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + if (lcdUiState == ICON_MENU) + { + if (lcdPage == PAGE_WIFI) + { + if (lcdPageOption == 0) + { + resetLcdUiToMainPage(); + } + } + else if (lcdPage == PAGE_BROKER) + { + if (lcdPageOption == 0) + { + resetLcdUiToMainPage(); + setLcdUiState(SETTINGS_MENU); + open_settings_menu_at(2); + } + else + { + resetLcdUiToMainPage(); + } + } + else if (lcdPage == PAGE_MUTE) + { + if (lcdPageOption == 0) + { + resetLcdUiToMainPage(); + setLcdUiState(SETTINGS_MENU); + open_settings_menu_at(4); + } + else if (lcdPageOption == 1) + { + if (currentlyMuted) + { + clearMuteTimeout(); + setMuted(false); + } + else + { + setMuteTimeoutMinutes((unsigned long)muteTimeoutMinutes); + } + } + else + { + resetLcdUiToMainPage(); + } + } + markLcdDirty(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; + } + + if (alarmIsActive() && lcdUiState != ALARM_ACTION_SELECT && lcdFocus == FOCUS_ALARM_ACTIONS) + { + return true; + } + + if (lcdFocus == FOCUS_WIFI) + { + lcdPage = PAGE_WIFI; + lcdPageOption = 0; + setLcdUiState(ICON_MENU); + } + else if (lcdFocus == FOCUS_BROKER) + { + lcdPage = PAGE_BROKER; + lcdPageOption = 0; + setLcdUiState(ICON_MENU); + } + else if (lcdFocus == FOCUS_MUTE) + { + lcdPage = PAGE_MUTE; + lcdPageOption = 0; + setLcdUiState(ICON_MENU); + } + else if (lcdFocus == FOCUS_SETTINGS) + { + resetLcdUiToMainPage(); + setLcdUiState(SETTINGS_MENU); + reset_menu_navigation(); + } + else if (lcdUiState != ALARM_ACTION_SELECT) + { + return false; + } + else + { + executeSelectedAlarmAction(); + } + markLcdDirty(); + requestAlarmRefresh(local_ptr_to_serial, false); + return true; +} + /* Assumes LCD has been initilized Turns off Back Light Clears display @@ -911,6 +1670,11 @@ void clearLCD(void) { lcd.noBacklight(); lcd.clear(); + for (uint8_t row = 0; row < LCD_ROWS; row++) + { + clearRowBuffer(previousLcdRows[row]); + } + lcdDirty = true; } // Splash a message so we can tell the LCD is working @@ -952,10 +1716,7 @@ void splashLCD(wifi_mode_t wifiMode, const IPAddress &deviceIp) lcd.setCursor(0, 2); lcd.print(F(__DATE__ " " __TIME__)); - // Line 3 - lcd.setCursor(0, 3); - lcd.print("MAC: "); - lcd.print(macAddressString); + // Leave row 3 empty; normal UI state owns the bottom row. } bool printable(char c) { @@ -991,67 +1752,248 @@ void filter_control_chars(char *msg) } msg[k] = '\0'; } -// TODO: We need to break the message up into strings to render properly -// on the display + +void renderWifiPage(char rows[LCD_ROWS][LCD_COLS + 1]) +{ + char ssid[21]; + char ip[21]; + currentSsid(ssid, sizeof(ssid)); + ipAddressText(ip, sizeof(ip)); + + if (WiFi.status() == WL_CONNECTED) + { + formatFullRow(rows[0], "WiFi:%.15s", ssid); + formatFullRow(rows[1], "IP:%s", ip); + formatFullRow(rows[2], "Open Web UI"); + formatFullRow(rows[3], "%cBack Open Web UI", lcdPageOption == 0 ? '>' : ' '); + } + else + { + formatFullRow(rows[0], "WiFi Setup"); + formatFullRow(rows[1], "AP:Krake-Setup"); + formatFullRow(rows[2], "Go:%s", ip); + formatFullRow(rows[3], "%cBack Open Web UI", lcdPageOption == 0 ? '>' : ' '); + } +} + +void renderBrokerPage(char rows[LCD_ROWS][LCD_COLS + 1]) +{ + formatFullRow(rows[0], "Broker/MQTT"); + formatFullRow(rows[1], "%.20s", mqtt_broker_name); + formatFullRow(rows[2], "%cBroker %s F:%u", + lcdPageOption == 0 ? '>' : ' ', + selectedBrokerIndex == activeBrokerIndex ? "Sel" : "Fail", + mqttFailCount); + formatFullRow(rows[3], "%cBack %.14s", + lcdPageOption == 1 ? '>' : ' ', + client.connected() ? mqttStatusText() : mqttStateDescription(client.state())); +} + +void renderMutePage(char rows[LCD_ROWS][LCD_COLS + 1]) +{ + const unsigned long muteMinutes = remainingMuteMinutes(); + formatFullRow(rows[0], "Mute"); + formatFullRow(rows[1], "Mute set:%lu min", currentlyMuted ? muteMinutes : (unsigned long)muteTimeoutMinutes); + formatFullRow(rows[2], "%cMute settings", lcdPageOption == 0 ? '>' : ' '); + formatFullRow(rows[3], "%c%s %cBack", + lcdPageOption == 1 ? '>' : ' ', + currentlyMuted ? "Unmute" : "Mute now", + lcdPageOption == 2 ? '>' : ' '); +} + +void renderInfoPage(char rows[LCD_ROWS][LCD_COLS + 1]) +{ + char ip[21]; + char ssid[21]; + ipAddressText(ip, sizeof(ip)); + currentSsid(ssid, sizeof(ssid)); + + formatFullRow(rows[0], "Info"); + formatFullRow(rows[1], "IP:%s", ip); + formatFullRow(rows[2], "MAC:%s", macAddressString); + if (ssid[0] != '\0' && strcmp(ssid, "Not connected") != 0) + { + formatFullRow(rows[3], "SSID:%s", ssid); + } + else + { + formatFullRow(rows[3], "Press to go back"); + } +} + +void renderWifiStatusPage(char rows[LCD_ROWS][LCD_COLS + 1]) +{ + char ssid[21]; + char ip[21]; + currentSsid(ssid, sizeof(ssid)); + ipAddressText(ip, sizeof(ip)); + + if (WiFi.status() == WL_CONNECTED) + { + formatFullRow(rows[0], "WiFi:%.15s", ssid); + formatFullRow(rows[1], "IP:%s", ip); + formatFullRow(rows[2], "Open Web UI"); + formatFullRow(rows[3], "Press: Back"); + } + else + { + formatFullRow(rows[0], "WiFi Setup"); + formatFullRow(rows[1], "AP:Krake-Setup"); + formatFullRow(rows[2], "Go:%s", ip); + formatFullRow(rows[3], "Press: Back"); + } +} + void showStatusLCD(AlarmLevel level, bool muted, char *msg) { - lcd.init(); - lcd.clear(); - // Possibly we don't need the backlight if the level is zero! - if (level != 0) + char rows[LCD_ROWS][LCD_COLS + 1]; + for (uint8_t row = 0; row < LCD_ROWS; row++) + { + clearRowBuffer(rows[row]); + } + + if (level != silent) { - // #if (!LIMIT_POWER_DRAW) lcd.backlight(); - // #endif } else { - lcd.noBacklight(); + lcd.backlight(); } - lcd.print("LVL: "); - lcd.print(level); - lcd.print(" - "); - lcd.print(AlarmNames[level]); + alarmQueueCount = (level == silent) ? 0 : 1; + char cleanMsg[MAX_BUFFER_SIZE]; + filterCopy(cleanMsg, sizeof(cleanMsg), msg); - int msgLineStart = 1; - lcd.setCursor(0, msgLineStart); - int len = strlen(AlarmMessageBuffer); - if (len < 9) + if (lcdUiState == INFO_PAGE) { - if (muted) + if (lcdPage == PAGE_WIFI_STATUS) { - lcd.print("MUTED! MSG:"); + renderWifiStatusPage(rows); } else { - lcd.print("MSG: "); + renderInfoPage(rows); } - msgLineStart = 2; } - if (strlen(AlarmMessageBuffer) == 0) + else if (lcdUiState == ICON_MENU && lcdPage == PAGE_WIFI) + { + renderWifiPage(rows); + } + else if (lcdUiState == ICON_MENU && lcdPage == PAGE_BROKER) + { + renderBrokerPage(rows); + } + else if (lcdUiState == ICON_MENU && lcdPage == PAGE_MUTE) { - lcd.print("None."); + renderMutePage(rows); + } + else if (level == silent) + { + if (lcdUiState == ALARM_ACTION_SELECT) + { + lcdUiState = MAIN_PAGE; + } + formatMain(rows[0], "Q:0"); + formatMain(rows[1], "System OK"); + if (currentlyMuted) + { + formatFullRow(rows[2], "Vol:%02d Mute:%lum", volumeDFPlayer, remainingMuteMinutes()); + } + else + { + formatFullRow(rows[2], "Vol:%02d Mute:Off", volumeDFPlayer); + } + alarmActionSelectorActive = false; + if (lcdFocus == FOCUS_ALARM_ACTIONS) + { + lcdFocus = FOCUS_SETTINGS; + } } else { + if (alarmQueueCount > 1) + { + formatMain(rows[0], "Q:+ NEXT"); + } + else + { + formatMain(rows[0], "Q:1"); + } + + char displayText[sizeof(alarmDisplayBuffer)]; + if (currentAlarmType[0] != '\0') + { + snprintf(displayText, sizeof(displayText), "%s %s %s", alarmLevelLabel(level), currentAlarmType, cleanMsg); + } + else + { + snprintf(displayText, sizeof(displayText), "%s %s", alarmLevelLabel(level), cleanMsg); + } + + const size_t displayLen = strlen(displayText); + if (displayLen <= LCD_ALARM_WINDOW_WIDTH) + { + scrollEnabled = false; + scrollIndex = 0; + lastScrollMs = millis(); + renderTwoLineMessageWindow(rows, displayText, 0); + } + else + { + if (!scrollEnabled || strcmp(alarmDisplayBuffer, displayText) != 0) + { + strncpy(alarmDisplayBuffer, displayText, sizeof(alarmDisplayBuffer) - 1); + alarmDisplayBuffer[sizeof(alarmDisplayBuffer) - 1] = '\0'; + scrollIndex = 0; + lastScrollMs = millis(); + scrollEnabled = true; + } + + const unsigned long now = millis(); + const size_t maxScrollIndex = displayLen - LCD_ALARM_WINDOW_WIDTH; + if (scrollIndex > maxScrollIndex) + { + scrollIndex = maxScrollIndex; + } + + const bool atEnd = (scrollIndex == maxScrollIndex); + const unsigned long intervalMs = atEnd ? LCD_SCROLL_PAUSE_MS : LCD_SCROLL_STEP_MS; + if ((now - lastScrollMs) >= intervalMs) + { + if (atEnd) + { + scrollIndex = 0; + } + else + { + scrollIndex++; + } + lastScrollMs = now; + } + + renderTwoLineMessageWindow(rows, alarmDisplayBuffer, scrollIndex); + } - char buffer[21] = {0}; // note space for terminator - // filter unmeaningful characters from msg buffer - filter_control_chars(msg); + if (lcdUiState == ALARM_ACTION_SELECT) + { + formatFullRow(rows[3], "Ack Dismiss Shelve"); + } - size_t len = strlen(msg); // doesn't count terminator - size_t blen = sizeof(buffer) - 1; // doesn't count terminator - size_t i = 0; - // the actual loop that enumerates your buffer - for (i = 0; i < (len / blen + 1) && i + msgLineStart < 4; ++i) + if (lcdUiState == ACTION_FEEDBACK) { - memcpy(buffer, msg + (i * blen), blen); - local_ptr_to_serial->println(buffer); - lcd.setCursor(0, i + msgLineStart); - lcd.print(buffer); + formatFullRow(rows[3], "%s", actionFeedbackText); } } + + if (level == silent && lcdUiState == ACTION_FEEDBACK) + { + formatFullRow(rows[3], "%s", actionFeedbackText); + } + + (void)muted; + writeStatusIcons(rows); + renderRows(rows); } // This operation is idempotent if there is no change in the abstract state. diff --git a/Firmware/GPAD_API/GPAD_API/GPAD_HAL.h b/Firmware/GPAD_API/GPAD_API/GPAD_HAL.h index 2254a28..89fc70e 100644 --- a/Firmware/GPAD_API/GPAD_API/GPAD_HAL.h +++ b/Firmware/GPAD_API/GPAD_API/GPAD_HAL.h @@ -223,7 +223,28 @@ public : _LCD->write(b); if (_cursorRow < LCD_ROWS && _cursorCol < LCD_COLS) { - _lcdMirror[_cursorRow][_cursorCol] = static_cast(b); + char mirrorChar = static_cast(b); + switch (b) + { + case 1: + mirrorChar = 'W'; + break; + case 2: + mirrorChar = 'B'; + break; + case 3: + mirrorChar = 'V'; + break; + case 4: + mirrorChar = 'M'; + break; + case 5: + mirrorChar = 'S'; + break; + default: + break; + } + _lcdMirror[_cursorRow][_cursorCol] = mirrorChar; } if (_cursorCol < LCD_COLS) @@ -309,6 +330,16 @@ void requestAlarmRefresh(Stream *serialport, bool includeAudio = true); void unchanged_anunicateAlarmLevel(Stream *serialport); void annunciateAlarmLevel(Stream *serialport); void serviceAlarmUiAudio(Stream *serialport); +bool alarmActionSelectorHandleRotation(bool clockwise); +bool alarmActionSelectorHandlePress(); +bool isAlarmActionSelectorActive(); +void resetLcdUiToMainPage(); +void showAlarmActions(); +void executeSelectedAlarmAction(); +void showActionFeedback(const char *msg); +void showInfoPage(); +void showWifiStatusPage(); +const char *lcdUiStateName(); void clearLCD(void); void splashLCD(wifi_mode_t wifiMode, const IPAddress &deviceIp); diff --git a/Firmware/GPAD_API/GPAD_API/GPAD_menu.cpp b/Firmware/GPAD_API/GPAD_API/GPAD_menu.cpp index 7d434b4..a9031e3 100644 --- a/Firmware/GPAD_API/GPAD_API/GPAD_menu.cpp +++ b/Firmware/GPAD_API/GPAD_API/GPAD_menu.cpp @@ -18,39 +18,43 @@ extern char currentAlarmId[11]; extern bool running_menu; extern bool menu_just_exited; extern unsigned long muteTimeoutEndMillis; +extern bool selectMqttBrokerOption(uint8_t index); #define LEDPIN 12 #define MAX_DEPTH 2 +void reset_menu_navigation(); + +void returnToMainPage() +{ + resetLcdUiToMainPage(); + running_menu = false; + menu_just_exited = false; + requestAlarmRefresh(&Serial, false); + Menu::doExit(); +} + result action1(eventMask e) { if (e == eventMask::enterEvent) { - DBG_PRINTLN(F("Yes, I will take that action #1 !")); + DBG_PRINTLN(F("Acknowledging alarm")); } - //char onLineMsg[32] = "Acknowledging!"; - // publishAck(&client, onLineMsg); publishGPAPResponse(&client, "a", currentAlarmId); DBG_PRINT(F("GPAP response queued for ID: ")); DBG_PRINTLN(currentAlarmId); - lcd.clear(); - lcd.setCursor(0, 0); - lcd.print("Acknowledged!"); - lcd.setCursor(0, 1); - lcd.print("Alarm still active"); + requestAlarmRefresh(&Serial, false); return proceed; } result action2(eventMask e) { if (e == eventMask::enterEvent) { - DBG_PRINTLN(F("Yes, I will take that action #2 !")); + DBG_PRINTLN(F("Dismissing alarm")); } char emptyMsg[] = ""; alarm(silent, emptyMsg, &Serial); // sets currentLevel=0, clears AlarmMessageBuffer requestAlarmRefresh(&Serial); // coalesces LCD/audio updates from loop() - //char onLineMsg[32] = "Dismissed!"; - // publishAck(&client, onLineMsg); publishGPAPResponse(&client, "d", currentAlarmId); DBG_PRINT(F("GPAP response queued for ID: ")); DBG_PRINTLN(currentAlarmId); @@ -60,13 +64,11 @@ result action3(eventMask e) { if (e == eventMask::enterEvent) { - DBG_PRINTLN(F("Yes, I will take that action #3 !")); + DBG_PRINTLN(F("Shelving alarm")); } char emptyMsg[] = ""; alarm(silent, emptyMsg, &Serial); requestAlarmRefresh(&Serial); - //char onLineMsg[32] = "Shelved!"; - // publishAck(&client, onLineMsg); publishGPAPResponse(&client, "s", currentAlarmId); DBG_PRINT(F("GPAP response queued for ID: ")); DBG_PRINTLN(currentAlarmId); @@ -76,7 +78,7 @@ result action4(eventMask e) { if (e == eventMask::enterEvent) { - DBG_PRINTLN(F("Yes, I will take that action #3 !")); + DBG_PRINTLN(F("Saving volume")); } DBG_PRINT(F("volume value: ")); DBG_PRINTLN(volumeDFPlayer); @@ -86,9 +88,16 @@ result action4(eventMask e) result action5(eventMask e) { DBG_PRINTLN(F("exiting menu")); - running_menu = false; - menu_just_exited = true; - Menu::doExit(); + returnToMainPage(); + return proceed; +} + +result actionBack(eventMask e) +{ + if (e == eventMask::enterEvent) + { + return quit; + } return proceed; } @@ -153,16 +162,6 @@ result actionComSaveAndExit(eventMask e) setComPortFlowControl(comFlowControlIndex == 1 ? COM_FLOW_RTS_CTS : COM_FLOW_OFF); applyComPortConfig(&Serial); - lcd.clear(); - lcd.setCursor(0, 0); - lcd.print("COM saved"); - lcd.setCursor(0, 1); - lcd.print(comBaudRate); - lcd.print(" "); - lcd.print(kSerialFormats[comSerialFormatIndex]); - lcd.setCursor(0, 2); - lcd.print("Flow:"); - lcd.print(kFlowControlModes[comFlowControlIndex]); return quit; } @@ -186,14 +185,81 @@ result actionMuteTimeout(eventMask e) DBG_PRINT(F("Mute timeout set: ")); DBG_PRINT(muteTimeoutMinutes); DBG_PRINTLN(F(" min")); + requestAlarmRefresh(&Serial); + } + return proceed; +} + +result actionMuteNow(eventMask e) +{ + if (e == eventMask::enterEvent) + { setMuteTimeoutMinutes((unsigned long)muteTimeoutMinutes); requestAlarmRefresh(&Serial); - lcd.clear(); - lcd.setCursor(0, 0); - lcd.print("Muted for:"); - lcd.setCursor(0, 1); - lcd.print(muteTimeoutMinutes); - lcd.print(" minute(s)"); + } + return proceed; +} + +result actionUnmuteNow(eventMask e) +{ + if (e == eventMask::enterEvent) + { + clearMuteTimeout(); + setMuted(false); + requestAlarmRefresh(&Serial); + } + return proceed; +} + +result actionWifiStatus(eventMask e) +{ + if (e == eventMask::enterEvent) + { + showWifiStatusPage(); + running_menu = false; + Menu::doExit(); + } + return proceed; +} + +bool selectBroker(uint8_t index) +{ + const bool selected = selectMqttBrokerOption(index); + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print(selected ? "Broker selected" : "Broker failed"); + lcd.setCursor(0, 1); + lcd.print(selected ? "Connecting..." : "Try again"); + return selected; +} + +result actionBrokerPublic(eventMask e) +{ + if (e == eventMask::enterEvent) + { + selectBroker(0); + } + return proceed; +} + +result actionBrokerKrake(eventMask e) +{ + if (e == eventMask::enterEvent) + { + selectBroker(1); + } + return proceed; +} + +result actionInfo(eventMask e) +{ + if (e == eventMask::enterEvent) + { + running_menu = false; + menu_just_exited = false; + Menu::doExit(); + showInfoPage(); } return proceed; } @@ -203,25 +269,43 @@ MENU(comSetupMenu, "COM Setup", Menu::doNothing, Menu::noEvent, Menu::wrapStyle, FIELD(comBaudRate, "Baud Rate", "", 1200, 115200, 9600, 1, Menu::doNothing, anyEvent, noStyle), FIELD(comSerialFormatIndex, "Serial Format", "", 0, 0, 0, 1, Menu::doNothing, anyEvent, noStyle), FIELD(comFlowControlIndex, "Flow Control", "", 0, 1, 0, 1, Menu::doNothing, anyEvent, noStyle), - OP("Save & Exit", actionComSaveAndExit, enterEvent), - OP("Exit (No Save)", actionComExitNoSave, enterEvent) + OP("Save", actionComSaveAndExit, enterEvent), + OP("Back", actionComExitNoSave, enterEvent) ); -MENU(resetConfirmMenu, "Reset", Menu::doNothing, Menu::noEvent, Menu::noStyle, - OP("Yes - Reset", actionResetConfirm, enterEvent), - OP("No - Cancel", Menu::doNothing, Menu::noEvent) +MENU(resetConfirmMenu, "Reset Device", Menu::doNothing, Menu::noEvent, Menu::noStyle, + OP("Confirm Reset", actionResetConfirm, enterEvent), + OP("Back", actionBack, enterEvent) ); +MENU(wifiMenu, "WiFi", Menu::doNothing, Menu::noEvent, Menu::wrapStyle, + OP("Status / Web UI", actionWifiStatus, enterEvent), + OP("Back", actionBack, enterEvent) +); -MENU(mainMenu, "Krake Menu", Menu::doNothing, Menu::noEvent, Menu::wrapStyle, - OP("Acknowledge", action1, enterEvent), - OP("Dismiss", action2, enterEvent), - OP("Shelve", action3, enterEvent), - FIELD(volumeDFPlayer, "Volume", "%", 0, 30, 10, 1, action4, anyEvent, wrapStyle), - FIELD(muteTimeoutMinutes, "Mute Time", "min", 1, 60, 5, 1, actionMuteTimeout, enterEvent, wrapStyle), +MENU(brokerMenu, "Broker", Menu::doNothing, Menu::noEvent, Menu::wrapStyle, + OP("Krake PubInv", actionBrokerKrake, enterEvent), + OP("Public Shiftr", actionBrokerPublic, enterEvent), + OP("Back", actionBack, enterEvent) +); + +MENU(muteMenu, "Mute Duration", Menu::doNothing, Menu::noEvent, Menu::wrapStyle, + FIELD(muteTimeoutMinutes, "Set Duration", "min", 1, 60, 5, 1, actionMuteTimeout, enterEvent, wrapStyle), + OP("Mute Now", actionMuteNow, enterEvent), + OP("Unmute", actionUnmuteNow, enterEvent), + OP("Back", actionBack, enterEvent) +); + + +MENU(mainMenu, "Settings", Menu::doNothing, Menu::noEvent, Menu::wrapStyle, + OP("Info", actionInfo, enterEvent), + SUBMENU(wifiMenu), + SUBMENU(brokerMenu), + FIELD(volumeDFPlayer, "Volume", "%", 1, 30, 20, 1, action4, enterEvent, wrapStyle), + SUBMENU(muteMenu), SUBMENU(comSetupMenu), SUBMENU(resetConfirmMenu), - OP("Exit Menu", action5, enterEvent) + OP("Back", action5, enterEvent) ); RotaryEventIn reIn( @@ -248,11 +332,8 @@ void registerRotationEvent(bool CW) { DBG_PRINT(F("CW: ")); DBG_PRINTLN(CW); - // Note: Rob believes it is more "natural" for clockwise to mean "up". - // Apparently, whoever wrote the "MENU_INPUTS" believes the opposite, - // so I am changing this hear to reverse the sense. - reIn.registerEvent(CW ? RotaryEventIn::EventType::ROTARY_CCW - : RotaryEventIn::EventType::ROTARY_CW); + reIn.registerEvent(CW ? RotaryEventIn::EventType::ROTARY_CW + : RotaryEventIn::EventType::ROTARY_CCW); } void registerRotaryEncoderPress() @@ -275,9 +356,13 @@ void poll_GPAD_menu() void navigate_to_n_and_execute(int n) { - DBG_PRINTLN(F("moving to zero and executing!")); - nav.doNav(navCmd(idxCmd, n)); // hilite second option - // nav.doNav(navCmd(enterCmd)); //execute option + nav.doNav(navCmd(idxCmd, n)); +} + +void open_settings_menu_at(int n) +{ + reset_menu_navigation(); + navigate_to_n_and_execute(n); } void reset_menu_navigation() @@ -285,3 +370,21 @@ void reset_menu_navigation() running_menu = true; nav.reset(); } + +void executeAlarmAction(uint8_t actionIndex) +{ + switch (actionIndex) + { + case 0: + action1(eventMask::enterEvent); + break; + case 1: + action2(eventMask::enterEvent); + break; + case 2: + action3(eventMask::enterEvent); + break; + default: + break; + } +} diff --git a/Firmware/GPAD_API/GPAD_API/GPAD_menu.h b/Firmware/GPAD_API/GPAD_API/GPAD_menu.h index 3b93b72..b08cba8 100644 --- a/Firmware/GPAD_API/GPAD_API/GPAD_menu.h +++ b/Firmware/GPAD_API/GPAD_API/GPAD_menu.h @@ -6,10 +6,13 @@ void setup_GPAD_menu(); void poll_GPAD_menu(); void navigate_to_n_and_execute(int n); +void open_settings_menu_at(int n); void registerRotationEvent(bool CW); void registerRotaryEncoderPress(); void reset_menu_navigation(); +void executeAlarmAction(uint8_t actionIndex); + #endif diff --git a/Firmware/GPAD_API/GPAD_API/InterruptRotator.cpp b/Firmware/GPAD_API/GPAD_API/InterruptRotator.cpp index 88c989e..a958664 100644 --- a/Firmware/GPAD_API/GPAD_API/InterruptRotator.cpp +++ b/Firmware/GPAD_API/GPAD_API/InterruptRotator.cpp @@ -1,7 +1,10 @@ #include "InterruptRotator.h" +#include "GPAD_HAL.h" #include "GPAD_menu.h" +#include "debug_macros.h" static RotaryEncoder *encoder = nullptr; +volatile unsigned long rotaryEncoderEventCount = 0; // This global variable represents the state of the menu; // we are either running the menu (true) or displaying other @@ -12,7 +15,7 @@ void initRotator() { // Serial.begin(115200); // while (!Serial); - Serial.println("InterruptRotator example for the RotaryEncoder library."); + DBG_PRINTLN(F("InterruptRotator init")); encoder = new RotaryEncoder(PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::TWO03); @@ -29,14 +32,11 @@ void updateRotator() if (pos != newPos) { + rotaryEncoderEventCount++; - Serial.print("pos: "); - Serial.print(newPos); - Serial.print(" dir: "); - // If we have rotated the encoder, then we enter the menu... - if (!running_menu) - reset_menu_navigation(); - + DBG_PRINT(F("pos: ")); + DBG_PRINT(newPos); + DBG_PRINT(F(" dir: ")); int d = (int)(encoder->getDirection()); // Serial.println(d); @@ -46,6 +46,14 @@ void updateRotator() CW = true; else CW = false; + + if (!running_menu) + { + alarmActionSelectorHandleRotation(CW); + pos = newPos; + return; + } + // Serial.print("d : "); // Serial.println(d); // Serial.println((int) RotaryEncoder::Direction::CLOCKWISE); diff --git a/Firmware/GPAD_API/GPAD_API/InterruptRotator.h b/Firmware/GPAD_API/GPAD_API/InterruptRotator.h index a092c7d..57d363a 100644 --- a/Firmware/GPAD_API/GPAD_API/InterruptRotator.h +++ b/Firmware/GPAD_API/GPAD_API/InterruptRotator.h @@ -18,4 +18,6 @@ void initRotator(); void updateRotator(); void IRAM_ATTR checkPositionISR(); +extern volatile unsigned long rotaryEncoderEventCount; + #endif // INTERRUPT_ROTATOR_H diff --git a/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.cpp b/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.cpp index b28ab2d..3354f23 100644 --- a/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.cpp +++ b/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.cpp @@ -4,7 +4,6 @@ namespace { const char *WIFI_CREDENTIALS_PATH = "/wifi.json"; - constexpr size_t MAX_SAVED_WIFI_NETWORKS = 20; String jsonEscape(const String &value) { @@ -92,7 +91,7 @@ int WiFiLed = 2; // Modify based on actual LED pin using namespace WifiOTA; Manager::Manager(WiFiClass &wifi, Print &print) - : wifi(wifi), print(print) + : wifi(wifi), print(print), connectedCallback(nullptr), apStartedCallback(nullptr) { } @@ -107,21 +106,23 @@ Manager::~Manager() {} void Manager::connect(const char *const accessPointSsid) { - std::vector credentials; + CredentialList credentials; if (this->loadCredentialsList(credentials)) { - for (size_t index = 0; index < credentials.size(); ++index) + for (size_t index = 0; index < credentials.count; ++index) { - this->print.print("Trying saved WiFi "); +#if (DEBUG_LEVEL > 0) + this->print.print(F("Trying saved WiFi ")); this->print.print(index + 1); - this->print.print("/"); - this->print.print(credentials.size()); - this->print.print(": "); - this->print.println(credentials[index].ssid); + this->print.print(F("/")); + this->print.print(credentials.count); + this->print.print(F(": ")); + this->print.println(credentials.items[index].ssid); +#endif - if (this->connectStoredCredentials(credentials[index].ssid, credentials[index].password)) + if (this->connectStoredCredentials(credentials.items[index].ssid, credentials.items[index].password)) { - this->print.println("Connected using stored wifi.json credentials."); + this->print.println(F("Connected using stored wifi.json credentials.")); this->ipSet(); return; } @@ -171,16 +172,16 @@ void Manager::startPortal(const char *const accessPointSsid) if (!connectSuccess) { - this->print.println("WiFiManager portal completed without connection."); + this->print.println(F("WiFiManager portal completed without connection.")); } } -void Manager::setConnectedCallback(std::function callback) +void Manager::setConnectedCallback(Callback callback) { this->connectedCallback = callback; } -void Manager::setApStartedCallback(std::function callback) +void Manager::setApStartedCallback(Callback callback) { this->apStartedCallback = callback; } @@ -190,6 +191,22 @@ wifi_mode_t Manager::getMode() return this->wifi.getMode(); } +void Manager::startConfigPortal(const char *const accessPointSsid, unsigned long timeoutSeconds) +{ + this->wifiManager.setConfigPortalTimeout(timeoutSeconds); + this->startPortal(accessPointSsid); +} + +bool Manager::forgetSavedCredentials() +{ + this->wifiManager.resetSettings(); + if (LittleFS.exists(WIFI_CREDENTIALS_PATH)) + { + return LittleFS.remove(WIFI_CREDENTIALS_PATH); + } + return true; +} + IPAddress Manager::getAddress() { switch (this->getMode()) @@ -211,47 +228,43 @@ bool Manager::saveCredentials(const String &ssid, const String &password) trimmedPassword.trim(); if (trimmedSsid.length() == 0 || trimmedPassword.length() == 0) { - this->print.println("SSID and password are required."); + this->print.println(F("SSID and password are required.")); return false; } - std::vector credentials; + CredentialList credentials; this->loadCredentialsList(credentials); - std::vector updated; - updated.reserve(credentials.size() + 1); + CredentialList updated; + updated.count = 0; - updated.push_back({trimmedSsid, trimmedPassword}); - for (size_t i = 0; i < credentials.size(); ++i) + updated.items[updated.count++] = {trimmedSsid, trimmedPassword}; + for (size_t i = 0; i < credentials.count; ++i) { - if (credentials[i].ssid != trimmedSsid) + if (credentials.items[i].ssid != trimmedSsid && updated.count < MAX_SAVED_WIFI_NETWORKS) { - updated.push_back(credentials[i]); - } - if (updated.size() >= MAX_SAVED_WIFI_NETWORKS) - { - break; + updated.items[updated.count++] = credentials.items[i]; } } File file = LittleFS.open(WIFI_CREDENTIALS_PATH, "w"); if (!file) { - this->print.println("Failed to open wifi.json for writing."); + this->print.println(F("Failed to open wifi.json for writing.")); return false; } String payload = "{\"networks\":["; - for (size_t i = 0; i < updated.size(); ++i) + for (size_t i = 0; i < updated.count; ++i) { if (i > 0) { payload += ","; } payload += "{\"ssid\":\""; - payload += jsonEscape(updated[i].ssid); + payload += jsonEscape(updated.items[i].ssid); payload += "\",\"password\":\""; - payload += jsonEscape(updated[i].password); + payload += jsonEscape(updated.items[i].password); payload += "\"}"; } payload += "]}"; @@ -263,20 +276,20 @@ bool Manager::saveCredentials(const String &ssid, const String &password) bool Manager::loadCredentials(String &ssid, String &password) { - std::vector credentials; - if (!this->loadCredentialsList(credentials) || credentials.empty()) + CredentialList credentials; + if (!this->loadCredentialsList(credentials) || credentials.count == 0) { return false; } - ssid = credentials[0].ssid; - password = credentials[0].password; + ssid = credentials.items[0].ssid; + password = credentials.items[0].password; return true; } -bool Manager::loadCredentialsList(std::vector &credentials) +bool Manager::loadCredentialsList(CredentialList &credentials) { - credentials.clear(); + credentials.count = 0; if (!LittleFS.exists(WIFI_CREDENTIALS_PATH)) { return false; @@ -285,7 +298,7 @@ bool Manager::loadCredentialsList(std::vector &credentials) File file = LittleFS.open(WIFI_CREDENTIALS_PATH, "r"); if (!file) { - this->print.println("Failed to open wifi.json for reading."); + this->print.println(F("Failed to open wifi.json for reading.")); return false; } @@ -293,7 +306,7 @@ bool Manager::loadCredentialsList(std::vector &credentials) file.close(); int searchPos = 0; - while (credentials.size() < MAX_SAVED_WIFI_NETWORKS) + while (credentials.count < MAX_SAVED_WIFI_NETWORKS) { const int ssidKeyPos = content.indexOf("\"ssid\"", searchPos); if (ssidKeyPos < 0) @@ -326,9 +339,9 @@ bool Manager::loadCredentialsList(std::vector &credentials) if (loadedSsid.length() > 0 && loadedPassword.length() > 0) { bool alreadyExists = false; - for (size_t i = 0; i < credentials.size(); ++i) + for (size_t i = 0; i < credentials.count; ++i) { - if (credentials[i].ssid == loadedSsid) + if (credentials.items[i].ssid == loadedSsid) { alreadyExists = true; break; @@ -337,14 +350,14 @@ bool Manager::loadCredentialsList(std::vector &credentials) if (!alreadyExists) { - credentials.push_back({loadedSsid, loadedPassword}); + credentials.items[credentials.count++] = {loadedSsid, loadedPassword}; } } searchPos = passwordEndPos + 1; } - if (!credentials.empty()) + if (credentials.count > 0) { return true; } @@ -354,7 +367,7 @@ bool Manager::loadCredentialsList(std::vector &credentials) String legacyPassword; if (!extractJsonString(content, "ssid", legacySsid) || !extractJsonString(content, "password", legacyPassword)) { - this->print.println("wifi.json missing required keys."); + this->print.println(F("wifi.json missing required keys.")); return false; } @@ -362,18 +375,20 @@ bool Manager::loadCredentialsList(std::vector &credentials) legacyPassword.trim(); if (legacySsid.length() == 0 || legacyPassword.length() == 0) { - this->print.println("wifi.json has invalid SSID/password."); + this->print.println(F("wifi.json has invalid SSID/password.")); return false; } - credentials.push_back({legacySsid, legacyPassword}); + credentials.items[credentials.count++] = {legacySsid, legacyPassword}; return true; } bool Manager::connectStoredCredentials(const String &ssid, const String &password, unsigned long timeoutMs) { - this->print.print("Attempting connection from wifi.json SSID: "); +#if (DEBUG_LEVEL > 0) + this->print.print(F("Attempting connection from wifi.json SSID: ")); this->print.println(ssid); +#endif this->wifi.begin(ssid.c_str(), password.c_str()); const unsigned long startMs = millis(); @@ -386,27 +401,31 @@ bool Manager::connectStoredCredentials(const String &ssid, const String &passwor delay(250); } - this->print.println("Stored wifi.json credentials failed to connect."); + this->print.println(F("Stored wifi.json credentials failed to connect.")); this->wifi.disconnect(true, false); return false; } void Manager::ssidSaved() { - this->print.print("Network Saved with SSID: "); +#if (DEBUG_LEVEL > 0) + this->print.print(F("Network Saved with SSID: ")); this->print.print(this->wifi.SSID()); - this->print.print("\n"); + this->print.print(F("\n")); +#endif } void Manager::ipSet() { - this->print.print("Connected to Network: "); +#if (DEBUG_LEVEL > 0) + this->print.print(F("Connected to Network: ")); this->print.print(this->wifi.SSID()); - this->print.print("\n"); + this->print.print(F("\n")); - this->print.print("Obtained IP Address: "); + this->print.print(F("Obtained IP Address: ")); this->wifi.localIP().printTo(Serial); - this->print.print("\n"); + this->print.print(F("\n")); +#endif if (this->connectedCallback) { @@ -416,9 +435,11 @@ void Manager::ipSet() void Manager::apStarted() { - this->print.print("AP Has Started: "); +#if (DEBUG_LEVEL > 0) + this->print.print(F("AP Has Started: ")); this->wifi.softAPIP().printTo(this->print); - this->print.print("\n"); + this->print.print(F("\n")); +#endif if (this->apStartedCallback) { @@ -430,7 +451,7 @@ void WifiOTA::initLittleFS() { if (!LittleFS.begin(true)) { - Serial.println("An error occurred while mounting LittleFS."); + Serial.println(F("An error occurred while mounting LittleFS.")); } else { diff --git a/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.h b/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.h index d694dc5..0888759 100644 --- a/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.h +++ b/Firmware/GPAD_API/GPAD_API/WiFiManagerOTA.h @@ -4,7 +4,6 @@ #include #include #include -#include extern const char *DEFAULT_SSID; extern String ledState; @@ -20,26 +19,35 @@ namespace WifiOTA String ssid; String password; }; + static const size_t MAX_SAVED_WIFI_NETWORKS = 20; + struct CredentialList + { + Credential items[MAX_SAVED_WIFI_NETWORKS]; + size_t count; + }; + typedef void (*Callback)(); Manager(WiFiClass &wifi, Print &print); ~Manager(); void initialize(); void connect(const char *const accessPointSsid); - void setConnectedCallback(std::function callBack); - void setApStartedCallback(std::function callback); + void setConnectedCallback(Callback callBack); + void setApStartedCallback(Callback callback); wifi_mode_t getMode(); IPAddress getAddress(); + void startConfigPortal(const char *const accessPointSsid, unsigned long timeoutSeconds); + bool forgetSavedCredentials(); bool saveCredentials(const String &ssid, const String &password); bool loadCredentials(String &ssid, String &password); - bool loadCredentialsList(std::vector &credentials); + bool loadCredentialsList(CredentialList &credentials); private: WiFiClass &wifi; Print &print; WiFiManager wifiManager; - std::function connectedCallback; - std::function apStartedCallback; + Callback connectedCallback; + Callback apStartedCallback; void ssidSaved(); void ipSet(); diff --git a/Firmware/GPAD_API/GPAD_API/debug_macros.h b/Firmware/GPAD_API/GPAD_API/debug_macros.h index 5240d21..4846a8e 100644 --- a/Firmware/GPAD_API/GPAD_API/debug_macros.h +++ b/Firmware/GPAD_API/GPAD_API/debug_macros.h @@ -3,18 +3,36 @@ #include -#ifndef GPAD_DEBUG -#define GPAD_DEBUG 0 +#ifndef DEBUG_LEVEL +#define DEBUG_LEVEL 0 +#endif + +#ifndef ENABLE_LCD_UI +#define ENABLE_LCD_UI 1 +#endif + +#ifndef ENABLE_DEBUG_LOGS +#define ENABLE_DEBUG_LOGS 0 +#endif + +#ifndef ENABLE_DFPLAYER +#define ENABLE_DFPLAYER 1 #endif -#if (GPAD_DEBUG > 0) -#define DBG_PRINT(x) Serial.print(x) -#define DBG_PRINTLN(x) Serial.println(x) -#define DBG_PRINTF(...) Serial.printf(__VA_ARGS__) -#else -#define DBG_PRINT(x) do { } while (0) -#define DBG_PRINTLN(x) do { } while (0) -#define DBG_PRINTF(...) do { } while (0) +#ifndef ENABLE_COM_SETUP +#define ENABLE_COM_SETUP 1 #endif +#ifndef ENABLE_OTA +#define ENABLE_OTA 1 +#endif + +#ifndef GPAD_DEBUG +#define GPAD_DEBUG DEBUG_LEVEL +#endif + +#define DBG_PRINT(x) do { if (ENABLE_DEBUG_LOGS || (GPAD_DEBUG > 0)) { Serial.print(x); } } while (0) +#define DBG_PRINTLN(x) do { if (ENABLE_DEBUG_LOGS || (GPAD_DEBUG > 0)) { Serial.println(x); } } while (0) +#define DBG_PRINTF(...) do { if (ENABLE_DEBUG_LOGS || (GPAD_DEBUG > 0)) { Serial.printf(__VA_ARGS__); } } while (0) + #endif diff --git a/Firmware/GPAD_API/data/404.html.gz b/Firmware/GPAD_API/data/404.html.gz new file mode 100644 index 0000000..7aa04cd Binary files /dev/null and b/Firmware/GPAD_API/data/404.html.gz differ diff --git a/Firmware/GPAD_API/data/Electrical_testHistory.html.gz b/Firmware/GPAD_API/data/Electrical_testHistory.html.gz new file mode 100644 index 0000000..5be2df9 Binary files /dev/null and b/Firmware/GPAD_API/data/Electrical_testHistory.html.gz differ diff --git a/Firmware/GPAD_API/data/GDT_TrackHistory.html.gz b/Firmware/GPAD_API/data/GDT_TrackHistory.html.gz new file mode 100644 index 0000000..336d935 Binary files /dev/null and b/Firmware/GPAD_API/data/GDT_TrackHistory.html.gz differ diff --git a/Firmware/GPAD_API/data/PMD_GPAD_API.html.gz b/Firmware/GPAD_API/data/PMD_GPAD_API.html.gz new file mode 100644 index 0000000..090b90d Binary files /dev/null and b/Firmware/GPAD_API/data/PMD_GPAD_API.html.gz differ diff --git a/Firmware/GPAD_API/data/device-monitor.html.gz b/Firmware/GPAD_API/data/device-monitor.html.gz new file mode 100644 index 0000000..0d67d5f Binary files /dev/null and b/Firmware/GPAD_API/data/device-monitor.html.gz differ diff --git a/Firmware/GPAD_API/data/index.html.gz b/Firmware/GPAD_API/data/index.html.gz new file mode 100644 index 0000000..f907636 Binary files /dev/null and b/Firmware/GPAD_API/data/index.html.gz differ diff --git a/Firmware/GPAD_API/data/js/404.js.gz b/Firmware/GPAD_API/data/js/404.js.gz new file mode 100644 index 0000000..447e6c1 Binary files /dev/null and b/Firmware/GPAD_API/data/js/404.js.gz differ diff --git a/Firmware/GPAD_API/data/js/common.js.gz b/Firmware/GPAD_API/data/js/common.js.gz new file mode 100644 index 0000000..45b45eb Binary files /dev/null and b/Firmware/GPAD_API/data/js/common.js.gz differ diff --git a/Firmware/GPAD_API/data/js/device-monitor.js.gz b/Firmware/GPAD_API/data/js/device-monitor.js.gz new file mode 100644 index 0000000..5be162e Binary files /dev/null and b/Firmware/GPAD_API/data/js/device-monitor.js.gz differ diff --git a/Firmware/GPAD_API/data/js/home.js.gz b/Firmware/GPAD_API/data/js/home.js.gz new file mode 100644 index 0000000..af40576 Binary files /dev/null and b/Firmware/GPAD_API/data/js/home.js.gz differ diff --git a/Firmware/GPAD_API/data/js/manual.js.gz b/Firmware/GPAD_API/data/js/manual.js.gz new file mode 100644 index 0000000..dacdfc7 Binary files /dev/null and b/Firmware/GPAD_API/data/js/manual.js.gz differ diff --git a/Firmware/GPAD_API/data/js/monitor.js.gz b/Firmware/GPAD_API/data/js/monitor.js.gz new file mode 100644 index 0000000..a11d744 Binary files /dev/null and b/Firmware/GPAD_API/data/js/monitor.js.gz differ diff --git a/Firmware/GPAD_API/data/js/pmd.js.gz b/Firmware/GPAD_API/data/js/pmd.js.gz new file mode 100644 index 0000000..e225aa4 Binary files /dev/null and b/Firmware/GPAD_API/data/js/pmd.js.gz differ diff --git a/Firmware/GPAD_API/data/js/settings.js b/Firmware/GPAD_API/data/js/settings.js index 22b6adf..949d5a7 100644 --- a/Firmware/GPAD_API/data/js/settings.js +++ b/Firmware/GPAD_API/data/js/settings.js @@ -20,7 +20,7 @@ const password = getInputValue('password'); if (!ssid) return KrakeUI.showMessage('SSID is required.', true); if (!password || !password.trim()) return KrakeUI.showMessage('Password is required.', true); - try { await KrakeUI.postForm('/wifi', { ssid, password }); KrakeUI.showMessage('WiFi credentials saved. Device will retry all saved networks on boot.'); await loadWifi(); } + try { await KrakeUI.postForm('/wifi', { ssid, password }); KrakeUI.showMessage('WiFi credentials saved. Restart or reconnect KRAKE to apply the new network.'); await loadWifi(); } catch (e) { KrakeUI.showMessage('Failed to save WiFi: ' + e.message, true); } } async function refreshSettings() { diff --git a/Firmware/GPAD_API/data/js/settings.js.gz b/Firmware/GPAD_API/data/js/settings.js.gz new file mode 100644 index 0000000..268f4b3 Binary files /dev/null and b/Firmware/GPAD_API/data/js/settings.js.gz differ diff --git a/Firmware/GPAD_API/data/manual.html.gz b/Firmware/GPAD_API/data/manual.html.gz new file mode 100644 index 0000000..7d459b9 Binary files /dev/null and b/Firmware/GPAD_API/data/manual.html.gz differ diff --git a/Firmware/GPAD_API/data/monitor.html.gz b/Firmware/GPAD_API/data/monitor.html.gz new file mode 100644 index 0000000..9e6070c Binary files /dev/null and b/Firmware/GPAD_API/data/monitor.html.gz differ diff --git a/Firmware/GPAD_API/data/settings.html.gz b/Firmware/GPAD_API/data/settings.html.gz new file mode 100644 index 0000000..697159a Binary files /dev/null and b/Firmware/GPAD_API/data/settings.html.gz differ diff --git a/Firmware/GPAD_API/data/style.css.gz b/Firmware/GPAD_API/data/style.css.gz index 7a9e01a..e03dedc 100644 Binary files a/Firmware/GPAD_API/data/style.css.gz and b/Firmware/GPAD_API/data/style.css.gz differ diff --git a/Firmware/GPAD_API/platformio.ini b/Firmware/GPAD_API/platformio.ini index 1026e1e..069f225 100644 --- a/Firmware/GPAD_API/platformio.ini +++ b/Firmware/GPAD_API/platformio.ini @@ -20,6 +20,11 @@ build_flags = -std=gnu++11 extra_scripts = pre:pre_extra_script.py build_flags = + -Os + -ffunction-sections + -fdata-sections + -Wl,--gc-sections + -DDEBUG_LEVEL=0 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1 platform = espressif32@^6.12.0 board = esp32dev @@ -40,4 +45,4 @@ lib_deps = marcoschwartz/LiquidCrystal_I2C@^1.1.4 lib_ignore = mocking -test_ignore = native/* \ No newline at end of file +test_ignore = native/*