diff --git a/cyd-bridge/README.md b/cyd-bridge/README.md index 3b9e734..845367b 100644 --- a/cyd-bridge/README.md +++ b/cyd-bridge/README.md @@ -76,7 +76,7 @@ Runtime settings (endpoint, token, probe names, units, polling interval) can be ## Building and flashing -Requires [PlatformIO](https://platformio.org/) installed locally (not available in this cloud environment). +Requires [PlatformIO](https://platformio.org/). ```bash cd cyd-bridge @@ -102,9 +102,18 @@ Before relying on the dashboard, verify the pinout matches your exact board revi 2. Confirm `TFT_CS`, `TFT_DC`, `TFT_RST`, `TFT_BL`, `TOUCH_CS`, and the SPI pins match the silkscreen or schematic of your ESP32-2432S028R. 3. If the screen stays white or touch is inverted, adjust `cfg.offset_rotation` in `DisplayConfig.hpp` or `setRotation()` in `Dashboard::begin()`. +## Memory usage + +With the default partition table the firmware uses approximately: + +- **RAM:** ~18 % (~58 kB of 320 kB) +- **Flash:** ~92 % (~1.2 MB of 1.3 MB) + +There is enough headroom to run, but large future features (e.g. local graphing, OTA) may require switching to a larger app partition or dropping unused NimBLE options. + ## Development notes - LovyanGFX driver/pin configuration lives in `src/display/DisplayConfig.hpp` and is tuned for the ESP32-2432S028R variant. - BLE uses `NimBLE-Arduino` to leave enough RAM for the web server and UI. - All user settings are stored in ESP32 NVS via `Preferences`. -- The firmware has not yet been compiled in this environment due to missing PlatformIO tooling; it should be built locally on your development machine before flashing. +- The firmware compiles successfully in PlatformIO; the remaining work is on-device testing and pinout verification. diff --git a/cyd-bridge/src/api/ApiServer.cpp b/cyd-bridge/src/api/ApiServer.cpp index eda3478..dc8e80f 100644 --- a/cyd-bridge/src/api/ApiServer.cpp +++ b/cyd-bridge/src/api/ApiServer.cpp @@ -43,6 +43,7 @@ void ApiServer::begin(uint16_t port) { server_.on("/api/latest", HTTP_GET, [this]() { handleLatest_(); }); server_.on("/api/settings", HTTP_GET, [this]() { handleGetSettings_(); }); server_.on("/api/settings", HTTP_POST, [this]() { handlePostSettings_(); }); + server_.on("/setup", HTTP_POST, [this]() { handleSetupForm_(); }); server_.onNotFound([this]() { handleNotFound_(); }); server_.begin(port_); @@ -116,8 +117,12 @@ void ApiServer::fillLatestJson_(JsonObject& obj) { } } - // Timestamps are relative for the device; we add ISO-like strings where possible. - obj["readingTime"] = reading.lastUpdateMillis > 0 ? String(reading.lastUpdateMillis) : nullptr; + // Timestamps are relative for the device; we add device millis where possible. + if (reading.lastUpdateMillis > 0) { + obj["readingTime"] = String(reading.lastUpdateMillis); + } else { + obj["readingTime"] = nullptr; + } obj["bridgeTime"] = String(millis()); obj["source"] = "cyd-bridge"; } @@ -220,11 +225,54 @@ void ApiServer::handleNotFound_() { server_.send(404, "application/json", "{\"ok\":false,\"error\":\"Not found\"}"); } +void ApiServer::handleSetupForm_() { + String ssid = server_.arg("ssid"); + String pass = server_.arg("pass"); + String mac = server_.arg("mac"); + + if (ssid.length() == 0 || mac.length() == 0) { + server_.send(400, "text/html", "

Error

SSID and MAC are required.

Go back"); + return; + } + + if (!SettingsManager::isValidMac(mac)) { + server_.send(422, "text/html", "

Error

Invalid MAC address format.

Go back"); + return; + } + + settings_.setWifiSsid(ssid); + settings_.setWifiPass(pass); + settings_.setThermoproMac(mac); + settings_.save(); + + String html = R"rawliteral( + + + + + + Saved + + + +

Settings saved

+

The CYD will reboot and connect to your WiFi network.

+

If it fails to connect, it will reopen this setup network.

+ + +)rawliteral"; + server_.send(200, "text/html", html); + + if (rebootCallback_) { + rebootCallback_(); + } +} + String ApiServer::buildSetupPage_() { String page = htmlHead; page += "

CYD ThermoPro Setup

\n"; page += "

Connect this CYD to your WiFi network and enter your ThermoPro details.

\n"; - page += "
\n"; + page += "\n"; page += R"rawliteral( diff --git a/cyd-bridge/src/api/ApiServer.hpp b/cyd-bridge/src/api/ApiServer.hpp index c81da0f..a5eae5f 100644 --- a/cyd-bridge/src/api/ApiServer.hpp +++ b/cyd-bridge/src/api/ApiServer.hpp @@ -2,6 +2,7 @@ #include "settings/SettingsManager.hpp" #include "ble/ThermoproBLE.hpp" +#include #include namespace cyd { @@ -38,6 +39,7 @@ private: void handleLatest_(); void handleGetSettings_(); void handlePostSettings_(); + void handleSetupForm_(); void handleNotFound_(); String buildSetupPage_(); diff --git a/cyd-bridge/src/ble/ThermoproBLE.cpp b/cyd-bridge/src/ble/ThermoproBLE.cpp index e106e2d..8008f41 100644 --- a/cyd-bridge/src/ble/ThermoproBLE.cpp +++ b/cyd-bridge/src/ble/ThermoproBLE.cpp @@ -5,6 +5,12 @@ namespace cyd { ThermoproBLE* ThermoproBLE::instance_ = nullptr; +const NimBLEUUID ThermoproBLE::kServiceUuid = NimBLEUUID("1086FFF0-3343-4817-8BB2-B32206336CE8"); +const NimBLEUUID ThermoproBLE::kNotifyUuid = NimBLEUUID("1086FFF2-3343-4817-8BB2-B32206336CE8"); +const NimBLEUUID ThermoproBLE::kWriteUuid = NimBLEUUID("1086FFF1-3343-4817-8BB2-B32206336CE8"); + +constexpr uint8_t ThermoproBLE::kHandshake[]; + ThermoproBLE::ThermoproBLE() { instance_ = this; } @@ -203,7 +209,7 @@ bool ThermoproBLE::setupConnection_(NimBLEClient* client) { state_ = State::SendingHandshake; stateStartMillis_ = millis(); - if (!writeChar_->write(const_cast(kHandshake), sizeof(kHandshake), true)) { + if (!writeChar_->writeValue(const_cast(kHandshake), sizeof(kHandshake), true)) { Serial.println("ThermoproBLE: handshake write failed"); return false; } diff --git a/cyd-bridge/src/ble/ThermoproBLE.hpp b/cyd-bridge/src/ble/ThermoproBLE.hpp index 7a964cb..6fbc6f9 100644 --- a/cyd-bridge/src/ble/ThermoproBLE.hpp +++ b/cyd-bridge/src/ble/ThermoproBLE.hpp @@ -39,9 +39,9 @@ public: private: // BLE UUIDs (ThermoPro custom service) - static inline const NimBLEUUID kServiceUuid = NimBLEUUID("1086FFF0-3343-4817-8BB2-B32206336CE8"); - static inline const NimBLEUUID kNotifyUuid = NimBLEUUID("1086FFF2-3343-4817-8BB2-B32206336CE8"); - static inline const NimBLEUUID kWriteUuid = NimBLEUUID("1086FFF1-3343-4817-8BB2-B32206336CE8"); + static const NimBLEUUID kServiceUuid; + static const NimBLEUUID kNotifyUuid; + static const NimBLEUUID kWriteUuid; // Static handshake bytes captured from the Android app. static constexpr uint8_t kHandshake[] = { 0x01, 0x09, 0x8a, 0x7a, 0x13, 0xb7, 0x3e, 0xd6, 0x8b, 0x67, 0xc2, 0xa0 }; diff --git a/cyd-bridge/src/display/DisplayConfig.hpp b/cyd-bridge/src/display/DisplayConfig.hpp index 335c306..98e76f8 100644 --- a/cyd-bridge/src/display/DisplayConfig.hpp +++ b/cyd-bridge/src/display/DisplayConfig.hpp @@ -28,7 +28,6 @@ public: // Panel configuration auto cfg = _panel_instance.config(); cfg.pin_cs = GPIO_NUM_15; - cfg.pin_dc = GPIO_NUM_2; cfg.pin_rst = GPIO_NUM_4; cfg.bus_shared = true; cfg.panel_width = 240; diff --git a/cyd-bridge/src/settings/SettingsManager.cpp b/cyd-bridge/src/settings/SettingsManager.cpp index ad5b658..9ca7de5 100644 --- a/cyd-bridge/src/settings/SettingsManager.cpp +++ b/cyd-bridge/src/settings/SettingsManager.cpp @@ -1,6 +1,7 @@ #include "SettingsManager.hpp" #include #include +#include namespace cyd { @@ -75,7 +76,8 @@ bool SettingsManager::load() { settings_.apiEndpoint = doc["apiEndpoint"] | ""; settings_.apiToken = doc["apiToken"] | ""; settings_.pollIntervalSec = doc["pollIntervalSec"] | 15; - settings_.unit = doc["unit"] | "F"; + String unit = doc["unit"] | "F"; + settings_.unit = unit.length() > 0 ? unit.charAt(0) : 'F'; settings_.deviceId = doc["deviceId"] | "cyd-smoker"; JsonArray names = doc["probeNames"].as(); diff --git a/cyd-bridge/src/wifi/WiFiManager.cpp b/cyd-bridge/src/wifi/WiFiManager.cpp index 1fb2468..884ff9a 100644 --- a/cyd-bridge/src/wifi/WiFiManager.cpp +++ b/cyd-bridge/src/wifi/WiFiManager.cpp @@ -1,42 +1,7 @@ #include "WiFiManager.hpp" -#include namespace cyd { -namespace { -constexpr const char* setupHtml = R"rawliteral( - - - - - - CYD ThermoPro Setup - - - -

CYD ThermoPro Setup

-

Enter your WiFi credentials and ThermoPro MAC address, then save.

- - - - - - - -
Format: AA:BB:CC:DD:EE:FF. Use a BLE scanner if you don't know it.
- - - - -)rawliteral"; -} // namespace - WiFiManager::WiFiManager(SettingsManager& settings) : settings_(settings) { } @@ -86,9 +51,8 @@ void WiFiManager::update() { case State::AccessPoint: case State::FallbackToAp: - if (portalRunning_) { + if (apRunning_) { dnsServer_.processNextRequest(); - portalServer_.handleClient(); } break; @@ -144,13 +108,10 @@ void WiFiManager::startAccessPoint_() { IPAddress ip = WiFi.softAPIP(); Serial.printf("WiFiManager: AP '%s' started at %s\n", kApSsid, ip.toString().c_str()); + // DNS redirect: any hostname requested while connected to the AP resolves + // to the CYD. The HTTP server in ApiServer serves the setup page at /. dnsServer_.start(53, "*", ip); - - portalServer_.on("/", HTTP_GET, [this]() { handlePortalRoot_(); }); - portalServer_.on("/setup", HTTP_POST, [this]() { handlePortalSubmit_(); }); - portalServer_.onNotFound([this]() { handlePortalNotFound_(); }); - portalServer_.begin(); - portalRunning_ = true; + apRunning_ = true; if (modeCallback_) { modeCallback_(true); @@ -158,65 +119,9 @@ void WiFiManager::startAccessPoint_() { } void WiFiManager::stopAccessPoint_() { - portalRunning_ = false; - portalServer_.close(); + apRunning_ = false; dnsServer_.stop(); WiFi.softAPdisconnect(true); } -void WiFiManager::handlePortalRoot_() { - portalServer_.send(200, "text/html", setupHtml); -} - -void WiFiManager::handlePortalSubmit_() { - String ssid = portalServer_.arg("ssid"); - String pass = portalServer_.arg("pass"); - String mac = portalServer_.arg("mac"); - - if (ssid.length() == 0 || mac.length() == 0) { - portalServer_.send(400, "text/html", "

Error

SSID and MAC are required.

Go back"); - return; - } - - if (!SettingsManager::isValidMac(mac)) { - portalServer_.send(422, "text/html", "

Error

Invalid MAC address format.

Go back"); - return; - } - - settings_.setWifiSsid(ssid); - settings_.setWifiPass(pass); - settings_.setThermoproMac(mac); - settings_.save(); - - String html = R"rawliteral( - - - - - - Saved - - - -

Settings saved

-

The CYD will reboot and connect to your WiFi network.

-

If it fails to connect, it will reopen this setup network.

- - -)rawliteral"; - portalServer_.send(200, "text/html", html); - - delay(500); - ESP.restart(); -} - -void WiFiManager::handlePortalNotFound_() { - redirectToPortal_(portalServer_); -} - -void WiFiManager::redirectToPortal_(WebServer& server) { - server.sendHeader("Location", "http://" + String(kSetupDomain) + "/", true); - server.send(302, "text/plain", "Redirecting to setup portal..."); -} - } // namespace cyd diff --git a/cyd-bridge/src/wifi/WiFiManager.hpp b/cyd-bridge/src/wifi/WiFiManager.hpp index bc516c8..fa7ee41 100644 --- a/cyd-bridge/src/wifi/WiFiManager.hpp +++ b/cyd-bridge/src/wifi/WiFiManager.hpp @@ -3,7 +3,6 @@ #include "settings/SettingsManager.hpp" #include #include -#include #include namespace cyd { @@ -52,23 +51,17 @@ private: unsigned long connectStartMillis_ = 0; static constexpr unsigned long kConnectTimeoutMs = 20000; - // Captive portal + // Captive portal DNS redirect. The actual HTTP server lives in ApiServer + // so we don't bind port 80 in two places. DNSServer dnsServer_; - WebServer portalServer_{80}; - bool portalRunning_ = false; - unsigned long lastPortalClientMillis_ = 0; + bool apRunning_ = false; static constexpr const char* kApSsid = "CYD-ThermoPro-Setup"; static constexpr const char* kApPass = ""; - static constexpr const char* kSetupDomain = "cyd.setup"; void startStation_(); void startAccessPoint_(); void stopAccessPoint_(); - void handlePortalRoot_(); - void handlePortalSubmit_(); - void handlePortalNotFound_(); - static void redirectToPortal_(WebServer& server); }; } // namespace cyd