diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index b834435..5bbe7f8 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -38,7 +38,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.x" + python-version: "3.13" - name: Install PlatformIO run: pip install platformio - name: Update PlatformIO diff --git a/data/www/espa.js b/data/www/espa.js index bd27515..ce7a18c 100644 --- a/data/www/espa.js +++ b/data/www/espa.js @@ -96,6 +96,7 @@ function fetchStatus() { updateStatusElement('status_serial', value_json.status.serial); updateStatusElement('status_siInitialised', value_json.status.siInitialised); updateStatusElement('status_mqtt', value_json.status.mqtt); + updateEspaControlStatus(value_json.status.espaControl); updateStatusElement('espa_model', value_json.eSpa.model); updateStatusElement('espa_build', value_json.eSpa.update.installed_version); }) @@ -111,6 +112,7 @@ function fetchStatus() { handleStatusError('status_serial'); handleStatusError('status_siInitialised'); handleStatusError('status_mqtt'); + handleStatusError('status_espaControl'); handleStatusError('espa_model'); handleStatusError('espa_build'); }); @@ -126,6 +128,37 @@ function updateStatusElement(elementId, value) { } } +function updateEspaControlStatus(status) { + const element = document.getElementById('status_espaControl'); + if (!element) return; + + element.classList.remove('text-bg-success', 'text-bg-danger', 'text-bg-warning', 'text-bg-secondary'); + + switch(status) { + case 'connected': + element.classList.add('text-bg-success'); + element.textContent = 'Connected'; + break; + case 'disconnected': + element.classList.add('text-bg-warning'); + element.textContent = 'Disconnected'; + break; + case 'not_paired': + element.classList.add('text-bg-secondary'); + element.textContent = 'Not Paired'; + break; + case 'disabled': + element.classList.add('text-bg-secondary'); + element.textContent = 'Disabled'; + break; + case 'unavailable': + default: + element.classList.add('text-bg-secondary'); + element.textContent = 'Unavailable'; + break; + } +} + function handleStatusError(elementId) { const element = document.getElementById(elementId); element.classList.remove('text-bg-warning'); diff --git a/data/www/index.htm b/data/www/index.htm index 5c7c8ea..69f490b 100644 --- a/data/www/index.htm +++ b/data/www/index.htm @@ -36,6 +36,7 @@ Wi-Fi Manager Firmware Updater Send Current Time to Spa + Connect to eSpa Control
Reboot eSpa @@ -125,6 +126,10 @@Cloud monitoring and control for your spa 🏊♂️
+ +");
+ }
+ }
+ #endif
}
+#ifdef ENABLE_ESPA_CONTROL
+// Handle serial commands for EspaControl pairing
+void handleSerialCommands() {
+ static String serialBuffer = "";
+
+ while (Serial.available()) {
+ char c = Serial.read();
+
+ if (c == '\n' || c == '\r') {
+ if (serialBuffer.length() > 0) {
+ String cmd = serialBuffer;
+ cmd.trim();
+ serialBuffer = "";
+
+ // Handle commands
+ if (cmd.startsWith("pair ")) {
+ String code = cmd.substring(5);
+ code.trim();
+
+ if (code.length() == 6) {
+ debugI("Submitting pairing code: %s", code.c_str());
+ if (espaControl.submitPairingCode(code)) {
+ debugI("Pairing code submitted successfully");
+ } else {
+ debugE("Failed to submit pairing code");
+ }
+ } else {
+ debugE("Invalid pairing code. Must be 6 digits. Usage: pair 123456");
+ }
+ }
+ else if (cmd == "unpair") {
+ debugI("Clearing pairing token...");
+ espaControl.unpair();
+ debugI("Device unpaired");
+ }
+ else if (cmd == "status") {
+ debugI("=== EspaControl Status ===");
+ debugI("Device ID: %s", espaControl.getDeviceId().c_str());
+
+ PairingState state = espaControl.getPairingState();
+ String stateStr;
+ switch(state) {
+ case NOT_PAIRED: stateStr = "NOT_PAIRED"; break;
+ case CODE_SUBMITTED: stateStr = "CODE_SUBMITTED"; break;
+ case POLLING: stateStr = "POLLING"; break;
+ case PAIRED: stateStr = "PAIRED"; break;
+ case PAIRING_ERROR: stateStr = "PAIRING_ERROR"; break;
+ default: stateStr = "UNKNOWN"; break;
+ }
+ debugI("Pairing State: %s", stateStr.c_str());
+ debugI("Is Paired: %s", espaControl.isPaired() ? "YES" : "NO");
+ debugI("Connected: %s", espaControl.isConnected() ? "YES" : "NO");
+ }
+ else if (cmd == "help") {
+ debugI("=== EspaControl Commands ===");
+ debugI("pair - Submit 6-digit pairing code (e.g., pair 123456)");
+ debugI("unpair - Clear pairing token and reset device");
+ debugI("status - Show current pairing status");
+ debugI("help - Show this help message");
+ }
+ }
+ } else {
+ serialBuffer += c;
+
+ // Prevent buffer overflow
+ if (serialBuffer.length() > 100) {
+ serialBuffer = "";
+ }
+ }
+ }
+}
+#endif
+
void loop() {
checkButton(); // Check if the button is pressed to start Wi-Fi Manager
Debug.handle();
+
+ #ifdef ENABLE_ESPA_CONTROL
+ handleSerialCommands(); // Handle serial commands for pairing
+ #endif
if (setSpaCallbackReady) {
if (spaCallbackProperty == "reboot") {
@@ -727,17 +902,27 @@ void loop() {
if (delayedStart) {
delayedStart = !(bootTime + 10000 < millis());
} else {
- si.loop();
+ // In desktop test mode, skip spa interface entirely
+ if (!desktopTestMode) {
+ si.loop();
+ }
- if (!si.isInitialised()) {
+ if (!desktopTestMode && !si.isInitialised()) {
// set status lights to indicate we are waiting for spa connection before we proceed
blinker.setState(STATE_WAITING_FOR_SPA);
} else {
if ( spaSerialNumber=="" ) {
debugI("Initialising...");
- spaSerialNumber = si.getSerialNo1()+"-"+si.getSerialNo2();
- debugI("Spa serial number is %s",spaSerialNumber.c_str());
+ if (desktopTestMode) {
+ // Use MAC address as serial number in test mode
+ spaSerialNumber = WiFi.macAddress();
+ spaSerialNumber.replace(":", "");
+ debugI("Desktop test mode - using MAC as serial: %s",spaSerialNumber.c_str());
+ } else {
+ spaSerialNumber = si.getSerialNo1()+"-"+si.getSerialNo2();
+ debugI("Spa serial number is %s",spaSerialNumber.c_str());
+ }
mqttBase = String("sn_esp32/") + spaSerialNumber + String("/");
mqttStatusTopic = mqttBase + "status";
@@ -745,7 +930,7 @@ void loop() {
mqttAvailability = mqttBase+"available";
debugI("MQTT base topic is %s",mqttBase.c_str());
}
- if (!mqttClient.connected()) { // MQTT broker reconnect if not connected
+ if (!desktopTestMode && !mqttClient.connected()) { // MQTT broker reconnect if not connected (skip in test mode)
long now=millis();
if (now - mqttLastConnect > 1000) {
blinker.setState(STATE_MQTT_NOT_CONNECTED);
@@ -768,10 +953,9 @@ void loop() {
} else {
debugW("MQTT connection failed");
}
-
}
} else {
- if (!autoDiscoveryPublished) { // This is the setup area, gets called once when communication with Spa and MQTT broker have been established.
+ if (!desktopTestMode && !autoDiscoveryPublished) { // This is the setup area, gets called once when communication with Spa and MQTT broker have been established.
debugI("Publish autodiscovery information");
mqttHaAutoDiscovery();
autoDiscoveryPublished = true;
@@ -780,15 +964,27 @@ void loop() {
si.statusResponse.setCallback(mqttPublishStatusString);
}
+
+ #ifdef ENABLE_ESPA_CONTROL
+ espaControl.loop(); // Process WebSocket events
+ #endif
// all systems are go! Start the knight rider animation loop
blinker.setState(KNIGHT_RIDER);
}
+
+ // In desktop test mode, we're always "ready" after delayed start
+ if (desktopTestMode) {
+ #ifdef ENABLE_ESPA_CONTROL
+ espaControl.loop(); // Process WebSocket events for pairing testing
+ #endif
+ blinker.setState(KNIGHT_RIDER);
+ }
}
}
}
- if (updateMqtt) {
+ if (!desktopTestMode && updateMqtt) {
debugD("Changing MQTT settings...");
mqttClient.disconnect();
mqttClient.setServer(config.MqttServer.getValue(), config.MqttPort.getValue());
@@ -811,5 +1007,7 @@ void loop() {
updateSoftAP = false;
}
- mqttClient.loop();
+ if (!desktopTestMode) {
+ mqttClient.loop();
+ }
}
\ No newline at end of file