diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5990d9c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/build_and_release.yml b/.github/workflows/build_and_release.yml index b06bc8a..6afe03e 100644 --- a/.github/workflows/build_and_release.yml +++ b/.github/workflows/build_and_release.yml @@ -60,6 +60,6 @@ jobs: uses: ncipollo/release-action@v1 with: allowUpdates: true - artifacts: "firmware/firmware*.bin" + artifacts: "firmware/*.bin" bodyFile: "CHANGELOG.md" removeArtifacts: true diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 5c50504..25f94c6 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -41,19 +41,21 @@ jobs: python-version: "3.x" - name: Install PlatformIO run: pip install platformio + - name: Build file system image for ${{ inputs.platform }} + run: pio run --target buildfs -e ${{ inputs.platform }} - name: Build for ${{ inputs.platform }} run: pio run -e ${{ inputs.platform }} -t mergebin #- name: Display firmware files # run: ls -la .pio/build/${{ inputs.platform }}/firmware*.bin - name: Rename and move firmware files run: | + ls -la .pio/build/${{ inputs.platform }}/*.bin mv .pio/build/${{ inputs.platform }}/firmware.bin firmware_${{ inputs.platform }}_ota.bin - if [ "${{ inputs.platform }}" != "esp8266dev" ]; then - mv .pio/build/${{ inputs.platform }}/firmware_factory.bin firmware_${{ inputs.platform }}_factory.bin - fi - ls -la firmware*.bin + mv .pio/build/${{ inputs.platform }}/firmware_factory.bin firmware_${{ inputs.platform }}_factory.bin + mv .pio/build/${{ inputs.platform }}/spiffs.bin spiffs_${{ inputs.platform }}.bin + ls -la *.bin - name: Archive Firmware uses: actions/upload-artifact@v4 with: name: firmware_${{ inputs.platform }} - path: firmware_${{ inputs.platform }}*.bin + path: "*_${{ inputs.platform }}*.bin" diff --git a/.gitignore b/.gitignore index beaaf8a..8293886 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .vscode/launch.json .vscode/ipch .vscode/extensions.json +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index fe753f2..e2ab4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -**Prereq for New Web UI** +**New Web UI** -* Move settings to Preferences -* Updated OTA to support flashing file system -* Bug fixes +***Breaking change*** *You should deploy v1.0.9 if migrating from an ealier version, this will ensure your settings (mqtt server, spa name, etc) are migrated.* + +* Changed settings to be stored in Preferences +* New Web UI diff --git a/README.md b/README.md index 8dbc1e8..dac6661 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,7 @@ -# SpaNet MQTT ESP32 bridge +# eSpa -SpaNet serial to mqtt bridge, including HomeAssitant autodiscovery. +## Introduction -Developed for the ESP32 Dev board but should work on any ESP32 platform. By default uses UART2 for communications with the SpaNet controller. - -Discussion on this (and other) SpaNet projects can be found here https://discord.com/channels/967940763595980900/967940763595980903 - -## Configuration -On first boot or whenever the enable key is press the board will enter hotspot mode. Connect to the hotspot to configure wifi & mqtt settings. - -## Firmware updates - -Firmware updates can be pushed to http://ipaddress/fota - -## Logging - -Debug / log functionality is available by telneting to the device's ip address - - -## Circuit -To keep things as simple as possible, off the shelf modules have been used. -NOTE: The resistors on the RX/TX pins are recommended but optional. - -Circuit - -Assembly -Components -Wiring +The [eSpa project](https://espa.diy) is an open source community for spa home automation, built around [firmware](https://espa.diy/firmware.html) (found in this GitHub repo) and a simple [hardware design](https://espa.diy/hardware.html) that you can build yourself, or [purchase pre-assembled](https://store.espa.diy/). +Learn more at the [eSpa website](https://espa.diy), and [join us on Discord](https://discord.gg/faK8Ag4wHn). diff --git a/data/www/espa.js b/data/www/espa.js new file mode 100644 index 0000000..edc804c --- /dev/null +++ b/data/www/espa.js @@ -0,0 +1,537 @@ +/************************************************************************************************ + * + * Utility Methods + * + ***********************************************************************************************/ +function confirmAction(url) { + if (confirm('Are you sure?')) { + window.location.href = url; + } +} + +function confirmFunction(func) { + if (confirm('Are you sure?')) { + func(); + } +} + +function parseVersion(version) { + const match = version.match(/^v([\d.]+)/); + if (!match) return null; + return match[1].split('.').map(Number); +} + +function compareVersions(current, latest) { + if (!current) return -1; + if (!latest) return 1; + for (let i = 0; i < Math.max(current.length, latest.length); i++) { + const a = current[i] || 0; + const b = latest[i] || 0; + if (a < b) return -1; + if (a > b) return 1; + } + return 0; +} + +// Copy to clipboard functionality +$('#copyToClipboardButton').click(function () { + copyToClipboard('#infoModelPre'); +}); + +function copyToClipboard(element) { + const text = $(element).text(); + navigator.clipboard.writeText(text).then(() => { + //alert('Copied to clipboard: ' + text); + }).catch(err => { + console.error('Failed to copy: ', err); + }); +} + +function reboot(message) { + $.ajax({ + url: '/reboot', + type: 'GET', + success: () => showAlert(message, 'alert-success', 'Reboot'), + error: () => showAlert('Failed to initiate reboot.', 'alert-danger', 'Error'), + complete: () => setTimeout(() => location.href = '/', 2000) + }); +} + + +/************************************************************************************************ + * + * Status + * + ***********************************************************************************************/ + +let fetchStatusFailed = false; + +function fetchStatus() { + fetch('/json') + .then(response => response.json()) + .then(value_json => { + if (fetchStatusFailed) { + clearAlert(); + fetchStatusFailed = false; + } + updateStatusElement('status_state', value_json.status.state); + updateStatusElement('temperatures_water', value_json.temperatures.water + "\u00B0C"); + updateStatusElement('temperatures_setPoint', value_json.temperatures.setPoint); + updateStatusElement('status_controller', value_json.status.controller); + updateStatusElement('status_firmware', value_json.status.firmware); + updateStatusElement('status_serial', value_json.status.serial); + updateStatusElement('status_siInitialised', value_json.status.siInitialised); + updateStatusElement('status_mqtt', value_json.status.mqtt); + updateStatusElement('espa_model', value_json.eSpa.model); + updateStatusElement('espa_build', value_json.eSpa.update.installed_version); + }) + .catch(error => { + console.error('Error fetching status:', error); + showAlert('Error connecting to the spa. If this persists, take a look at our troubleshooting docs.', 'alert-danger', "Error"); + fetchStatusFailed = true; + handleStatusError('status_state'); + handleStatusError('temperatures_water'); + handleStatusError('temperatures_setPoint'); + handleStatusError('status_controller'); + handleStatusError('status_firmware'); + handleStatusError('status_serial'); + handleStatusError('status_siInitialised'); + handleStatusError('status_mqtt'); + handleStatusError('espa_model'); + handleStatusError('espa_build'); + }); +} + +function updateStatusElement(elementId, value) { + const element = document.getElementById(elementId); + element.classList.remove('badge', 'text-bg-warning', 'text-bg-danger'); + if (element instanceof HTMLInputElement) { + element.value = value; + } else { + element.textContent = value; + } +} + +function handleStatusError(elementId) { + const element = document.getElementById(elementId); + element.classList.remove('text-bg-warning'); + element.classList.add('text-bg-danger'); + if (element instanceof HTMLInputElement) { + element.value = ''; + } else { + element.textContent = 'Failed to load'; + } +} + +function clearAlert() { + const pageAlert = $('#page-alert'); + const pageAlertParent = $('.page-alert-parent'); + pageAlert.removeClass(function (index, className) { + return (className.match(/(^|\s)alert-\S+/g) || []).join(' '); + }).text(''); + pageAlertParent.hide(); +} + +window.onload = function () { + fetchStatus(); + loadFotaData(); + setInterval(fetchStatus, 10000); +} + + +/************************************************************************************************ + * + * Updating eSpa configuration + * + ***********************************************************************************************/ + +function updateTempSetPoint() { + const temperatures_setPoint = document.getElementById('temperatures_setPoint').value; + fetch('/set', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'temperatures_setPoint=' + temperatures_setPoint + }) + .then(response => response.text()) + .then(result => console.log(result)) + .catch(error => console.error('Error setting temperature:', error)); +} + +function sendCurrentTime() { + const status_datetime = new Date(Date() + " UTC").toISOString().slice(0, 19).replace("T", " "); + fetch('/set', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'status_datetime=' + status_datetime + }) + .then(response => response.text()) + .then(result => console.log(result)) + .catch(error => console.error('Error setting datetime:', error)); +} + +// Retrieving and updating the configured settings, so they can be displayed in the modal popup +function loadConfig() { + $('#configErrorAlert').hide(); + fetch('/json/config') + .then(response => response.json()) + .then(data => { + document.getElementById('spaName').value = data.spaName; + document.getElementById('mqttServer').value = data.mqttServer; + document.getElementById('mqttPort').value = data.mqttPort; + document.getElementById('mqttUsername').value = data.mqttUsername; + document.getElementById('mqttPassword').value = data.mqttPassword; + document.getElementById('updateFrequency').value = data.updateFrequency; + + // Enable form fields and save button + $('#config_form input').prop('disabled', false); + $('#saveConfigButton').prop('disabled', false); + }) + .catch(error => { + console.error('Error loading config:', error); + $('#configErrorAlert').text('Error loading configuration. Please try again.').show(); + + // Make form fields read-only and disable save button + $('#config_form input').prop('disabled', true); + $('#saveConfigButton').prop('disabled', true); + }); +} + +// Configuration modal +$(document).ready(function () { + // configuration settings modal + $('#configLink').click(function (event) { + event.preventDefault(); + $('#configModal').modal('show'); + }); + + // Load configuration when the config modal is shown + $('#configModal').on('shown.bs.modal', function () { + loadConfig(); + }); + + // Handle form submission when the save button is clicked + $('#saveConfigButton').click(function () { + submitConfigForm(); + }); + + function submitConfigForm() { + $.ajax({ + url: '/config', + type: 'POST', + data: $('#config_form').serialize(), + success: function () { + showAlert('Configuration updated successfully!', 'alert-success', 'Success'); + loadConfig(); + $('#configModal').modal('hide'); + }, + error: function () { + $('#configErrorAlert').text('Error updating configuration. Please try again.').show(); + } + }); + } + + $('#config_form').submit(function (e) { + e.preventDefault(); + submitConfigForm(); + }); +}); + + +/************************************************************************************************ + * + * OTA Update Support + * + ***********************************************************************************************/ + +$(document).ready(function () { + $('#progressDiv').hide(); + $('#localInstallButton').prop('disabled', true); + $('#localUpdate').show(); + document.getElementById('updateForm').reset(); + + // Delegate event listener for dynamically added #fotaLink + $(document).on('click', '#fotaLink', function (event) { + event.preventDefault(); + $('#fotaModal').modal('show'); + // loadFotaData(); + }); + + // Enable the local install button when a file is selected + $('#fsFile').change(updateLocalInstallButton); + $('#appFile').change(updateLocalInstallButton); + function updateLocalInstallButton () { + if ($('#fsFile').val() || $('#appFile').val()) { + $('#localInstallButton').prop('disabled', false); + } else { + $('#localInstallButton').prop('disabled', true); + } + }; + + // Handle local install button click + $('#localInstallButton').click(async function () { + const appFile = $('#appFile')[0].files[0]; + const fsFile = $('#fsFile')[0].files[0]; + let appSuccess = false, fsSuccess = false; + + if (!appFile && !fsFile) { + showAlert('Please select either an application or filesystem update file.', 'alert-danger', 'Error'); + console.error('No files selected for upload.'); + return; + } + + let totalFiles = 1; + if (appFile && fsFile) totalFiles = 2; + let fileNum = 0; + // Upload application file if provided + if (appFile) { + const appData = new FormData(); + appData.append('updateType', 'application'); + appData.append('update', appFile); + fileNum++; + $('#msg').html(`

Uploading file ${fileNum} of ${totalFiles} - Application update.

`); + appSuccess = await uploadFileAsync(appData, '/fota', fileNum, totalFiles); + } + + // Upload filesystem file if provided + if (fsFile) { + const fsData = new FormData(); + fsData.append('updateType', 'filesystem'); + fsData.append('update', fsFile); + fileNum++; + $('#msg').html(`

Uploading file ${fileNum} of ${totalFiles} - File system update.

`); + fsSuccess = await uploadFileAsync(fsData, '/fota', fileNum, totalFiles); + } + + // Trigger reboot only if all provided uploads were successful + if ((!appFile || appSuccess) && (!fsFile || fsSuccess)) { + $('#fotaModal').modal('hide'); + setTimeout(() => reboot('The firmware has been updated successfully. The spa will now restart to apply the changes.'), 500); + } else { + showAlert('One or more uploads failed.', 'alert-danger', 'Error'); + } + + document.getElementById('updateForm').reset(); + }); + + async function uploadFileAsync(data, url, fileNum, totalFiles) { + let percentMultipler = 1 / totalFiles; + let startPercent = percentMultipler * 100 * (fileNum - 1); + return new Promise((resolve) => { + $.ajax({ + url, + type: 'POST', + data, + contentType: false, + processData: false, + xhr: function () { + $('#progressDiv').show(); + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('progress', function(evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + percentComplete = parseInt((percentComplete * 100 * percentMultipler) + startPercent); + $('#progressBar').css('width', percentComplete + '%').attr('aria-valuenow', percentComplete).text(percentComplete + '%'); + } + }, false); + return xhr; + }, + success: function (data) { + showAlert('The firmware has been uploaded.', 'alert-success', 'Firmware uploaded'); + resolve(true); + }, + error: function () { + showAlert('The firmware update failed. Please try again.', 'alert-danger', 'Error'); + resolve(false); + } + }); + }); + } +} + + // Handle remote update installation + /* + $('#remoteInstallButton').click(function (event) { + event.preventDefault(); + var selectedVersion = $('#firmware-select').val(); + if (selectedVersion) { + $.ajax({ + url: '/install', + type: 'POST', + data: { version: selectedVersion }, + success: function (data) { + showAlert('The firmware has been updated successfully. The spa will now restart to apply the changes.', 'alert-success', 'Firmware updated'); + $('#fotaModal').modal('hide'); + }, + error: function () { + showAlert('The firmware update failed. Please try again.', 'alert-danger', 'Error'); + } + }); + } + }); + + // Show/hide update sections based on selected update method + $('#updateMethod').change(function () { + var selectedMethod = $(this).val(); + if (selectedMethod === 'remote') { + $('#remoteUpdate').show(); + $('#localUpdate').hide(); + } else if (selectedMethod === 'local') { + $('#remoteUpdate').hide(); + $('#localUpdate').show(); + } else { + $('#remoteUpdate').hide(); + $('#localUpdate').hide(); + } + }); + */ +); + +function loadFotaData() { + fetch('/json') + .then(response => response.json()) + .then(value_json => { + document.getElementById('espa_model').innerText = value_json.eSpa.model; + document.getElementById('installedVersion').innerText = value_json.eSpa.update.installed_version; + }) + .catch(error => console.error('Error fetching FOTA data:', error)); + + $.ajax({ + url: 'https://api.github.com/repos/wayne-love/ESPySpa/releases', + type: 'GET', + success: function (data) { + document.getElementById('lastestRelease').innerText = data[0].tag_name; + + // Populate the select dropdown with all releases + const firmwareSelect = document.getElementById('firmware-select'); + firmwareSelect.innerHTML = ''; // Clear existing options + + // Add default disabled option + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.text = 'Select a version'; + defaultOption.disabled = true; + defaultOption.selected = true; + firmwareSelect.appendChild(defaultOption); + + // Add release options + data.forEach(release => { + const option = document.createElement('option'); + option.value = release.tag_name; + option.text = release.tag_name; + firmwareSelect.appendChild(option); + }); + + // Enable the install button when a valid choice is selected + firmwareSelect.addEventListener('change', function () { + if (firmwareSelect.value) { + $('#remoteInstallButton').prop('disabled', false); + } else { + $('#remoteInstallButton').prop('disabled', true); + } + }); + + // Check for new version + const latestVersion = parseVersion(data[0].tag_name); + const currentVersion = parseVersion(document.getElementById('installedVersion').innerText); + const comparison = compareVersions(currentVersion, latestVersion); + if (comparison < 0) { + showAlert(`There is a new eSpa release available - it's version ${data[0].tag_name}. You can update now.`, 'alert-primary', "New eSpa release!"); + } + }, + error: function () { + showAlert('Failed to fetch eSpa release information. If this persists, take a look at our troubleshooting docs.', 'alert-danger', "Error"); + } + }); +} + + +/************************************************************************************************ + * + * Status models (for JSON and Spa response) + * + ***********************************************************************************************/ + +$(document).ready(function () { + // JSON dump modal + $('#jsonLink').click(function (event) { + event.preventDefault(); + fetch('/json').then(response => response.json()).then(data => { + $('#infoModalTitle').html("Spa JSON"); + $('#infoModalBody').html('
' + JSON.stringify(data, null, 2) + '
'); + $('#infoModal').modal('show'); + }) + .catch(error => { + console.error('Error fetching JSON:', error); + showAlert('Error connecting to the spa. If this persists, take a look at our troubleshooting docs.', 'alert-danger', "Error"); + }); + }); + + // spa status modal + $('#statusLink').click(function (event) { + event.preventDefault(); + fetch('/status').then(response => response.text()).then(data => { + $('#infoModalTitle').html("Spa Status"); + $('#infoModalBody').html('
' + data + '
'); + $('#infoModal').modal('show'); + }) + .catch(error => { + console.error('Error fetching status:', error); + showAlert('Error connecting to the spa. If this persists, take a look at our troubleshooting docs.', 'alert-danger', "Error"); + }); + }); +}); + + +/************************************************************************************************ + * + * Front page alerts + * + ***********************************************************************************************/ + +function showAlert(message, alertClass, title = '') { + const pageAlert = $('#page-alert'); + const pageAlertParent = $('.page-alert-parent'); + + // Clear existing alert classes and set the new class + pageAlert.removeClass(function (index, className) { + return (className.match(/(^|\s)alert-\S+/g) || []).join(' '); + }).addClass(alertClass); + + // Construct the alert content + let alertContent = ''; + if (title) { + alertContent += `

${title}

`; + } + alertContent += message; + + // Set the alert content and show the alert + pageAlert.html(alertContent); + pageAlertParent.show(); +} + + +/************************************************************************************************ + * + * Light / Dark Mode Switch + * + ***********************************************************************************************/ + +document.addEventListener('DOMContentLoaded', (event) => { + const htmlElement = document.documentElement; + const switchElement = document.getElementById('darkModeSwitch'); + + // Set the default theme to dark if no setting is found in local storage + const currentTheme = localStorage.getItem('bsTheme') || 'dark'; + htmlElement.setAttribute('data-bs-theme', currentTheme); + switchElement.checked = currentTheme === 'dark'; + + switchElement.addEventListener('change', function () { + if (this.checked) { + htmlElement.setAttribute('data-bs-theme', 'dark'); + localStorage.setItem('bsTheme', 'dark'); + } else { + htmlElement.setAttribute('data-bs-theme', 'light'); + localStorage.setItem('bsTheme', 'light'); + } + }); +}); \ No newline at end of file diff --git a/data/www/favicon.ico b/data/www/favicon.ico new file mode 100644 index 0000000..a4b5997 Binary files /dev/null and b/data/www/favicon.ico differ diff --git a/data/www/images/heart.svg b/data/www/images/heart.svg new file mode 100644 index 0000000..43bda91 --- /dev/null +++ b/data/www/images/heart.svg @@ -0,0 +1,11 @@ + +Created with Fabric.js 1.7.22 + + + + + + + + + \ No newline at end of file diff --git a/data/www/images/logo_eSpa_64px.png b/data/www/images/logo_eSpa_64px.png new file mode 100644 index 0000000..5f674fa Binary files /dev/null and b/data/www/images/logo_eSpa_64px.png differ diff --git a/data/www/index.htm b/data/www/index.htm new file mode 100644 index 0000000..c6f1756 --- /dev/null +++ b/data/www/index.htm @@ -0,0 +1,308 @@ + + + + + + + + + + + eSpa + + + + + +
+ + + + + +
+
+

Status

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Spa status:Loading...
Spa temperature:Loading...
Spa controller:Loading...
Spa controller firmware:Loading... +
Spa serial number:Loading...
Spa interface initialised:Loading...
MQTT status:Loading...
eSpa Model:Loading...
eSpa Build:Loading...
+
+
+ + +
+
+

Controls

+
+
+
+
+ + + + + +
Set Temperature: + +
+
+
+ + + +

Built with heart by the eSpa Team

+
+
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/www/styles.css b/data/www/styles.css new file mode 100644 index 0000000..239a853 --- /dev/null +++ b/data/www/styles.css @@ -0,0 +1,72 @@ +/* input[type=file]::file-selector-button, input[type="submit"], a, button { + padding: 7px 15px; + border: none; + background: #007BFF; + color: white; + text-decoration: none; + border-radius: 5px; + margin-top: 5px; + display: inline-block; + font-size: 16px; + font-family: Arial, sans-serif; + cursor: pointer; + text-align: center; +} +input[type=file]::file-selector-button:hover, input[type="submit"]:hover, a:hover, button:hover { + background-color: #0056b3; +} +table, td, th { + border: 1px solid; + padding: 5px; +} +table { + border-collapse: collapse; +} */ + +h1, h2, h3 { + text-align: left; + font-weight: 300; + padding-top: 20px; + display: flex; + align-items: center; +} + +.footer { + font-size: 10px; + text-align: center; +} + +/* .navbar-dark .navbar-nav .nav-link { + color: white; +} */ + +/* html[data-bs-theme="light"] .dropdown-menu { + background-color: white; +} */ +/* +html[data-bs-theme="dark"] .dropdown-menu { + background-color: #0069d9; +} */ + +.modal-content { + border: none; +} + +/* .modal-header { + background-color: #0069d9; + color: white; +} */ + +.modal-header .modal-title { + font-weight: 400; +} + +/* .modal-header .close { + color: white; +} */ + +input[type="submit"]:disabled, button:disabled { + background: #cccccc; + color: #666666; + cursor: not-allowed; +} diff --git a/lib/Config/Config.cpp b/lib/Config/Config.cpp index ebe866c..e35bfe8 100644 --- a/lib/Config/Config.cpp +++ b/lib/Config/Config.cpp @@ -27,11 +27,7 @@ bool Config::readConfig() { preferences.end(); return true; } else { - debugI("Preferences not found, checking for config file"); - if (readConfigFile()) { - writeConfig(); - return true; - } + debugI("Preferences not found."); } return false; } @@ -51,73 +47,3 @@ void Config::writeConfig() { debugE("Failed to open Preferences for writing"); } } - -// Read config from file and populate settings -bool Config::readConfigFile() { - debugI("Reading config file"); - if (!LittleFS.begin()) { - debugW("Failed to mount file system, formatting"); - return false; - } - File configFile = LittleFS.open("/config.json", "r"); - if (!configFile) { - debugW("Config file not found"); - LittleFS.end(); - return false; - } else { - size_t size = configFile.size(); - std::unique_ptr buf(new char[size]); - configFile.readBytes(buf.get(), size); - - JsonDocument json; - auto deserializeError = deserializeJson(json, buf.get()); - serializeJson(json, Serial); - - if (!deserializeError) { - debugI("Parsed JSON"); - - if (json["mqtt_server"].is()) MqttServer.setValue(json["mqtt_server"].as()); - if (json["mqtt_port"].is()) MqttPort.setValue(json["mqtt_port"].as()); - if (json["mqtt_username"].is()) MqttUsername.setValue(json["mqtt_username"].as()); - if (json["mqtt_password"].is()) MqttPassword.setValue(json["mqtt_password"].as()); - if (json["spa_name"].is()) SpaName.setValue(json["spa_name"].as()); - if (json["update_frequency"].is()) UpdateFrequency.setValue(json["update_frequency"].as()); - } else { - debugW("Failed to parse config file"); - LittleFS.end(); - return false; - } - configFile.close(); - } - - LittleFS.end(); - return true; -} - -// Write configuration to file (for backup purposes or debugging) -void Config::writeConfigFile() { - debugI("Updating config file"); - JsonDocument json; - - if (!LittleFS.begin()) { - debugW("Failed to mount file system, formatting"); - return; - } - - json["mqtt_server"] = MqttServer.getValue(); - json["mqtt_port"] = MqttPort.getValue(); - json["mqtt_username"] = MqttUsername.getValue(); - json["mqtt_password"] = MqttPassword.getValue(); - json["spa_name"] = SpaName.getValue(); - json["update_frequency"] = UpdateFrequency.getValue(); - - File configFile = LittleFS.open("/config.json", "w"); - if (!configFile) { - debugE("Failed to open config file for writing"); - } else { - serializeJson(json, configFile); - configFile.close(); - debugI("Config file updated"); - } - LittleFS.end(); -} diff --git a/lib/Config/Config.h b/lib/Config/Config.h index 2da8446..c7f7538 100644 --- a/lib/Config/Config.h +++ b/lib/Config/Config.h @@ -5,7 +5,6 @@ #include #include #include -#include extern RemoteDebug Debug; @@ -86,9 +85,6 @@ class Config : public ControllerConfig { void setCallback(void (*callback)(const char*, T)) { Setting::setCallback(callback); } -private: - bool readConfigFile(); // Read configuration from file - void writeConfigFile(); // Write configuration to file }; diff --git a/lib/MultiBlinker/MultiBlinker.cpp b/lib/MultiBlinker/MultiBlinker.cpp index 084e89a..e2fe3bf 100644 --- a/lib/MultiBlinker/MultiBlinker.cpp +++ b/lib/MultiBlinker/MultiBlinker.cpp @@ -2,23 +2,23 @@ // Define the on/off times for each state (-1 to 15) const LEDPattern LED_PATTERNS[17] = { - {2000, 2000}, //KNIGHT_RIDER - {UINT_MAX, 0}, // STATE_NONE: Always off - {100, 100}, // STATE_WIFI_NOT_CONNECTED - {0, 0}, // Reserved - {0, 0}, // Reserved - {500, 500}, // STATE_MQTT_NOT_CONNECTED - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, 0}, // Reserved - {0, UINT_MAX} // STATE_STARTED_WIFI_AP: Always on + {2000, 2000}, //KNIGHT_RIDER + {UINT_MAX, 0}, // STATE_NONE: Always off + {100, 100}, // STATE_WIFI_NOT_CONNECTED + {1000, 1000}, // STATE_WAITING_FOR_SPA + {0, 0}, // Reserved + {500, 500}, // STATE_MQTT_NOT_CONNECTED + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, 0}, // Reserved + {0, UINT_MAX} // STATE_STARTED_WIFI_AP: Always on }; MultiBlinker::MultiBlinker(int led1, int led2, int led3, int led4) { diff --git a/lib/MultiBlinker/MultiBlinker.h b/lib/MultiBlinker/MultiBlinker.h index bf93774..56710e8 100644 --- a/lib/MultiBlinker/MultiBlinker.h +++ b/lib/MultiBlinker/MultiBlinker.h @@ -19,8 +19,9 @@ extern RemoteDebug Debug; const int KNIGHT_RIDER = -1; // Knight Rider animation or 2000ms blink const int STATE_NONE = 0; // ON: (nothing) const int STATE_STARTED_WIFI_AP = 15; // ON: ALL or solid on -const int STATE_WIFI_NOT_CONNECTED = 1; // ON: 4 or 100ms blink -const int STATE_MQTT_NOT_CONNECTED = 4; // ON: 2 or 500ms blink +const int STATE_WIFI_NOT_CONNECTED = 1; // ON: LED 4 or 100ms blink +const int STATE_WAITING_FOR_SPA = 2; // ON: LED 3 or 1000ms blink +const int STATE_MQTT_NOT_CONNECTED = 4; // ON: LED 2 or 500ms blink const int MULTI_BLINKER_INTERVAL = 100; diff --git a/lib/SpaUtils/SpaUtils.cpp b/lib/SpaUtils/SpaUtils.cpp index fd479ab..4117ee4 100644 --- a/lib/SpaUtils/SpaUtils.cpp +++ b/lib/SpaUtils/SpaUtils.cpp @@ -146,10 +146,16 @@ bool generateStatusJson(SpaInterface &si, MQTTClientWrapper &mqttClient, String json["status"]["state"] = si.getStatus(); json["status"]["spaMode"] = si.getMode(); json["status"]["controller"] = si.getModel(); + String firmware = si.getSVER().substring(3); + firmware.replace(' ', '.'); + json["status"]["firmware"] = firmware; json["status"]["serial"] = si.getSerialNo1() + "-" + si.getSerialNo2(); json["status"]["siInitialised"] = si.isInitialised()?"true":"false"; json["status"]["mqtt"] = mqttClient.connected()?"connected":"disconnected"; + json["eSpa"]["model"] = xstr(PIOENV); + json["eSpa"]["update"]["installed_version"] = xstr(BUILD_INFO); + json["heatpump"]["mode"] = si.HPMPStrings[si.getHPMP()]; json["heatpump"]["auxheat"] = si.getHELE()==0? "OFF" : "ON"; diff --git a/lib/SpaUtils/SpaUtils.h b/lib/SpaUtils/SpaUtils.h index 39bb8a0..24fcac9 100644 --- a/lib/SpaUtils/SpaUtils.h +++ b/lib/SpaUtils/SpaUtils.h @@ -11,6 +11,10 @@ #include #include "MQTTClientWrapper.h" +//define stringify function +#define xstr(a) str(a) +#define str(a) #a + extern RemoteDebug Debug; String convertToTime(int data); diff --git a/lib/WebUI/WebUI.cpp b/lib/WebUI/WebUI.cpp index 5222acb..d460efd 100644 --- a/lib/WebUI/WebUI.cpp +++ b/lib/WebUI/WebUI.cpp @@ -14,70 +14,46 @@ const char * WebUI::getError() { return Update.errorString(); } - void WebUI::begin() { - - server.reset(new WebServer(80)); - - server->on("/", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - server->send(200, "text/html", WebUI::indexPageTemplate); - }); - - server->on("/json", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - String json; - if (generateStatusJson(*_spa, *_mqttClient, json, true)) { - server->send(200, "text/json", json.c_str()); - } else { - server->send(200, "text/text", "Error generating json"); - } - }); - - server->on("/reboot", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->send(200, "text/html", WebUI::rebootPage); + server.on("/reboot", HTTP_GET, [&](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + request->send(200, "text/plain", "Rebooting ESP..."); debugD("Rebooting..."); delay(200); - server->client().stop(); ESP.restart(); }); - server->on("/styles.css", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->send(200, "text/css", WebUI::styleSheet); + server.on("/fota", HTTP_GET, [&](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + request->send(200, "text/html", fotaPage); }); - server->on("/fota", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - server->send(200, "text/html", WebUI::fotaPage); + server.on("/config", HTTP_GET, [&](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + request->send(SPIFFS, "/www/config.htm"); }); - server->on("/fota", HTTP_POST, [&]() { - debugD("uri: %s", server->uri().c_str()); + server.on("/fota", HTTP_POST, [this](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); if (Update.hasError()) { - server->sendHeader("Connection", "close"); - server->send(200, F("text/plain"), String(F("Update error: ")) + String(getError())); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", String("Update error: ") + String(this->getError())); + response->addHeader("Connection", "close"); + request->send(response); } else { - server->client().setNoDelay(true); - server->sendHeader("Connection", "close"); - server->send(200, "text/plain", "OK"); + request->client()->setNoDelay(true); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "OK"); + response->addHeader("Connection", "close"); + request->send(response); } - }, [&]() { - debugD("uri: %s", server->uri().c_str()); - HTTPUpload& upload = server->upload(); - if (upload.status == UPLOAD_FILE_START) { + }, [this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + if (index == 0) { static int updateType = U_FLASH; // Default to firmware update - if (server->hasArg("updateType")) { - String type = server->arg("updateType"); + if (request->hasArg("updateType")) { + String type = request->arg("updateType"); if (type == "filesystem") { updateType = U_SPIFFS; debugD("Filesystem update selected."); - if (!SPIFFS.begin()) SPIFFS.format(); } else if (type == "application") { updateType = U_FLASH; debugD("Application (firmware) update selected."); @@ -90,45 +66,39 @@ void WebUI::begin() { debugD("No update type specified. Defaulting to application update."); } - debugD("Update: %s", upload.filename.c_str()); - if (!Update.begin(UPDATE_SIZE_UNKNOWN, updateType)) { //start with max available size - debugD("Update Error: %s",getError()); - } - } else if (upload.status == UPLOAD_FILE_WRITE) { - /* flashing firmware to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { - debugD("Update Error: %s",getError()); + debugD("Update: %s", filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN, updateType)) { // start with max available size + debugD("Update Error: %s", this->getError()); } - } else if (upload.status == UPLOAD_FILE_END) { - if (Update.end(true)) { //true to set the size to the current progress - debugD("Update Success: %u\n", upload.totalSize); + } + if (Update.write(data, len) != len) { + debugD("Update Error: %s", this->getError()); + } + if (final) { + if (Update.end(true)) { // true to set the size to the current progress + debugD("Update Success: %u\n", index + len); } else { - debugD("Update Error: %s",getError()); + debugD("Update Error: %s", this->getError()); } } }); - server->on("/config", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - server->send(200, "text/html", WebUI::configPageTemplate); - }); - - server->on("/config", HTTP_POST, [&]() { - debugD("uri: %s", server->uri().c_str()); - if (server->hasArg("spaName")) _config->SpaName.setValue(server->arg("spaName")); - if (server->hasArg("mqttServer")) _config->MqttServer.setValue(server->arg("mqttServer")); - if (server->hasArg("mqttPort")) _config->MqttPort.setValue(server->arg("mqttPort").toInt()); - if (server->hasArg("mqttUsername")) _config->MqttUsername.setValue(server->arg("mqttUsername")); - if (server->hasArg("mqttPassword")) _config->MqttPassword.setValue(server->arg("mqttPassword")); - if (server->hasArg("updateFrequency")) _config->UpdateFrequency.setValue(server->arg("updateFrequency").toInt()); + server.on("/config", HTTP_POST, [this](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + if (request->hasParam("spaName", true)) _config->SpaName.setValue(request->getParam("spaName", true)->value()); + if (request->hasParam("mqttServer", true)) _config->MqttServer.setValue(request->getParam("mqttServer", true)->value()); + if (request->hasParam("mqttPort", true)) _config->MqttPort.setValue(request->getParam("mqttPort", true)->value().toInt()); + if (request->hasParam("mqttUsername", true)) _config->MqttUsername.setValue(request->getParam("mqttUsername", true)->value()); + if (request->hasParam("mqttPassword", true)) _config->MqttPassword.setValue(request->getParam("mqttPassword", true)->value()); + if (request->hasParam("updateFrequency", true)) _config->UpdateFrequency.setValue(request->getParam("updateFrequency", true)->value().toInt()); _config->writeConfig(); - server->sendHeader("Connection", "close"); - server->send(200, "text/plain", "Updated"); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Updated"); + response->addHeader("Connection", "close"); + request->send(response); }); - server->on("/json/config", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); + server.on("/json/config", HTTP_GET, [this](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); String configJson = "{"; configJson += "\"spaName\":\"" + _config->SpaName.getValue() + "\","; configJson += "\"mqttServer\":\"" + _config->MqttServer.getValue() + "\","; @@ -137,54 +107,74 @@ void WebUI::begin() { configJson += "\"mqttPassword\":\"" + _config->MqttPassword.getValue() + "\","; configJson += "\"updateFrequency\":" + String(_config->UpdateFrequency.getValue()); configJson += "}"; - server->send(200, "application/json", configJson); + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", configJson); + response->addHeader("Connection", "close"); + request->send(response); }); - server->on("/set", HTTP_POST, [&]() { - //In theory with minor modification, we can reuse mqttCallback here - //for (uint8_t i = 0; i < server->args(); i++) updateSpaSetting("set/" + server->argName(0), server->arg(0)); - if (server->hasArg("temperatures_setPoint")) { - float newTemperature = server->arg("temperatures_setPoint").toFloat(); - _spa->setSTMP(int(newTemperature*10)); - server->send(200, "text/plain", "Temperature updated"); + server.on("/json", HTTP_GET, [&](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + String json; + AsyncWebServerResponse *response; + if (generateStatusJson(*_spa, *_mqttClient, json, true)) { + response = request->beginResponse(200, "application/json", json); + } else { + response = request->beginResponse(200, "text/plain", "Error generating json"); } - else if (server->hasArg("status_datetime")) { - String p = server->arg("status_datetime"); + response->addHeader("Connection", "close"); + request->send(response); + }); + + // Handle /set endpoint (POST) + server.on("/set", HTTP_POST, [this](AsyncWebServerRequest *request) { + // In theory with minor modification, we can reuse mqttCallback here + // for (uint8_t i = 0; i < request->params(); i++) updateSpaSetting("set/" + request->getParam(i)->name(), request->getParam(i)->value()); + if (request->hasParam("temperatures_setPoint", true)) { + float newTemperature = request->getParam("temperatures_setPoint", true)->value().toFloat(); + _spa->setSTMP(int(newTemperature * 10)); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Temperature updated"); + response->addHeader("Connection", "close"); + request->send(response); + } else if (request->hasParam("status_datetime", true)) { + String p = request->getParam("status_datetime", true)->value(); tmElements_t tm; - tm.Year=CalendarYrToTm(p.substring(0,4).toInt()); - tm.Month=p.substring(5,7).toInt(); - tm.Day=p.substring(8,10).toInt(); - tm.Hour=p.substring(11,13).toInt(); - tm.Minute=p.substring(14,16).toInt(); - tm.Second=p.substring(17).toInt(); + tm.Year = CalendarYrToTm(p.substring(0, 4).toInt()); + tm.Month = p.substring(5, 7).toInt(); + tm.Day = p.substring(8, 10).toInt(); + tm.Hour = p.substring(11, 13).toInt(); + tm.Minute = p.substring(14, 16).toInt(); + tm.Second = p.substring(17).toInt(); _spa->setSpaTime(makeTime(tm)); - server->send(200, "text/plain", "Date/Time updated"); - } - else { - server->send(400, "text/plain", "Invalid temperature value"); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Date/Time updated"); + response->addHeader("Connection", "close"); + request->send(response); + } else { + AsyncWebServerResponse *response = request->beginResponse(400, "text/plain", "Invalid temperature value"); + response->addHeader("Connection", "close"); + request->send(response); } }); - server->on("/wifi-manager", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - server->send(200, "text/plain", "WiFi Manager launching, connect to ESP WiFi..."); + // Handle /wifi-manager endpoint (GET) + server.on("/wifi-manager", HTTP_GET, [this](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "WiFi Manager launching, connect to ESP WiFi..."); + response->addHeader("Connection", "close"); + request->send(response); if (_wifiManagerCallback != nullptr) { _wifiManagerCallback(); } }); - server->on("/json.html", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - server->send(200, "text/html", WebUI::jsonHTMLTemplate); + server.on("/status", HTTP_GET, [this](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", _spa->statusResponse.getValue()); + response->addHeader("Connection", "close"); + request->send(response); }); - server->on("/status", HTTP_GET, [&]() { - debugD("uri: %s", server->uri().c_str()); - server->sendHeader("Connection", "close"); - server->send(200, "text/plain", _spa->statusResponse.getValue()); - }); + // As a fallback we try to load from /www any requested URL + server.serveStatic("/", SPIFFS, "/www/"); - server->begin(); + server.begin(); initialised = true; } \ No newline at end of file diff --git a/lib/WebUI/WebUI.h b/lib/WebUI/WebUI.h index b3253b6..7063065 100644 --- a/lib/WebUI/WebUI.h +++ b/lib/WebUI/WebUI.h @@ -3,23 +3,18 @@ #include #include -#include #include #include "SpaInterface.h" #include "SpaUtils.h" #include "Config.h" #include "MQTTClientWrapper.h" - -//define stringify function -#define xstr(a) str(a) -#define str(a) #a +#include "ESPAsyncWebServer.h" extern RemoteDebug Debug; class WebUI { public: - std::unique_ptr server; WebUI(SpaInterface *spa, Config *config, MQTTClientWrapper *mqttClient); /// @brief Set the function to be called when properties have been updated. @@ -29,6 +24,7 @@ class WebUI { bool initialised = false; private: + AsyncWebServer server{80}; SpaInterface *_spa; Config *_config; MQTTClientWrapper *_mqttClient; @@ -36,454 +32,120 @@ class WebUI { const char* getError(); -static constexpr const char *indexPageTemplate PROGMEM = -R"( - - - - - - - - -

ESP32 Spa Controller

- - - - - - - - - -
Spa status:Loading...
Spa temperature:Loading...
Set Temperature: -
Spa controller:Loading...
Spa serial number:Loading...
Spa interface initialised:Loading...
MQTT status:Loading...
Build:)" xstr(BUILD_INFO) R"(
-

Spa JSON HTML

-

Spa JSON

-

Spa Response

-

Send Current Time to Spa

-

Configuration

-

Firmware Update

-

Wi-Fi Manager

-

Reboot ESP

- -)"; - -static constexpr const char *fotaPage PROGMEM = -R"( + // hard-coded FOTA page in case file system gets wiped + static constexpr const char *fotaPage PROGMEM = R"( + - + - Firmware Update

Firmware Update

- -
- - -

-
- - -

- - -
- + + + + + + + + + + + + +
-
progress: 0%
-
+
progress: 0%
+
- -)"; -static constexpr const char *styleSheet PROGMEM = -R"( -input[type=file]::file-selector-button, input[type="submit"], a, button { - padding: 7px 15px; - border: none; - background: #007BFF; - color: white; - text-decoration: none; - border-radius: 5px; - margin-top: 5px; - display: inline-block; - font-size: 16px; - font-family: Arial, sans-serif; - cursor: pointer; - text-align: center; -} -input[type=file]::file-selector-button:hover, input[type="submit"]:hover, a:hover, button:hover { - background-color: #0056b3; -} -table, td, th { - border: 1px solid; - padding: 5px; -} -table { - border-collapse: collapse; -})"; - -static constexpr const char *rebootPage PROGMEM = -R"( - - - - - - -Rebooting - - -

Rebooting ESP...

- -)"; - -static constexpr const char *configPageTemplate PROGMEM = -R"( - - - - - -Configuration - - -

Configuration

-
- - - - - - - -
Spa Name:
MQTT Server:
MQTT Port:
MQTT Username:
MQTT Password:
Poll Frequency (seconds):
- -
-
- - - -)"; - -static constexpr const char *jsonHTMLTemplate PROGMEM = -R"( - - - - - - -JSON Data Table - - -

JSON Data Table

- - - - - - -
- -)"; - + +)"; }; #endif // WEBUI_H diff --git a/merge-bin.py b/merge-bin.py index 459aa68..3b4e5b2 100644 --- a/merge-bin.py +++ b/merge-bin.py @@ -3,6 +3,7 @@ # Adds PlatformIO post-processing to merge all the ESP flash images into a single image. import os +import csv Import("env", "projenv") @@ -20,6 +21,24 @@ def merge_bin_action(source, target, env): if board_config.get("build.mcu", "") == 'esp8266': return + partition_csv = env.get("PARTITIONS_TABLE_CSV") + fs_image_name = env.get("ESP32_FS_IMAGE_NAME") + offset = 0 + print("partition_csv: ", partition_csv) + print("fs_image_name: ", fs_image_name) + + if partition_csv and fs_image_name: + with open(partition_csv, "r") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + if row["# Name"] == fs_image_name: + offset = row[" Offset"] + break + + if offset: + flash_images.append(offset) + flash_images.append("${BUILD_DIR}/%s.bin" % fs_image_name) + merge_cmd = " ".join( [ '"$PYTHONEXE"', diff --git a/platformio.ini b/platformio.ini index d0cdf7a..135f6f0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,11 +10,13 @@ [platformio] default_envs = esp32dev, spa-control-pcb +; data_dir = {$PROJECT_DIR}/data [env:spa-base] framework = arduino monitor_speed = 115200 lib_ldf_mode = deep +board_build.filesystem = spiffs lib_deps = https://github.com/ktos/RemoteDebug.git@^3.0.7 tzapu/WiFiManager@^2.0.17 @@ -22,6 +24,7 @@ lib_deps = bblanchon/ArduinoJson@^7.1.0 links2004/WebSockets@^2.3.6 paulstoffregen/Time@^1.6.1 + https://github.com/me-no-dev/ESPAsyncWebServer.git extra_scripts = pre:get_version.py post:merge-bin.py diff --git a/src/main.cpp b/src/main.cpp index aebfbfd..5f887c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "MultiBlinker.h" @@ -15,9 +16,6 @@ #include "HAAutoDiscovery.h" #include "MQTTClientWrapper.h" -//define stringify function -#define xstr(a) str(a) -#define str(a) #a unsigned long bootStartMillis; // To track when the device started RemoteDebug Debug; @@ -62,9 +60,9 @@ void WMsaveConfigCallback(){ } void startWiFiManager(){ - if (ui.initialised) { - ui.server->stop(); - } + // if (ui.initialised) { + // ui.server_old->stop(); + // } WiFiManager wm; WiFiManagerParameter custom_spa_name("spa_name", "Spa Name", config.SpaName.getValue().c_str(), 40); @@ -555,6 +553,12 @@ void setup() { blinker.setState(STATE_NONE); // start with all LEDs off blinker.start(); + if (SPIFFS.begin()) { + debugD("Mounted SPIFFS"); + } else { + debugE("Error mounting SPIFFS"); + } + debugA("Starting ESP..."); @@ -617,9 +621,9 @@ void loop() { mqttClient.loop(); Debug.handle(); - if (ui.initialised) { - ui.server->handleClient(); - } + // if (ui.initialised) { + // ui.server_old->handleClient(); + // } if (updateMqtt) { debugD("Changing MQTT settings..."); @@ -641,10 +645,12 @@ void loop() { if (delayedStart) { delayedStart = !(bootTime + 10000 < millis()); } else { - si.loop(); - if (si.isInitialised()) { + if (!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..."); @@ -689,7 +695,6 @@ void loop() { mqttPublishStatus(); si.statusResponse.setCallback(mqttPublishStatusString); - } // all systems are go! Start the knight rider animation loop