diff --git a/.gitignore b/.gitignore index 081cede..e00e2ac 100644 --- a/.gitignore +++ b/.gitignore @@ -9,12 +9,6 @@ src/generated/web_ui.h data_empty/* !data_empty/.gitkeep -# React -web-dashboard/node_modules/ -web-dashboard/dist/ - -# Build output -data/www/ # OS .DS_Store diff --git a/data/version.txt b/data/version.txt new file mode 100644 index 0000000..67ba65e --- /dev/null +++ b/data/version.txt @@ -0,0 +1 @@ +Built: 2026-06-18T16:25:38.961944Z diff --git a/doc/initial_prompt.md b/doc/initial_prompt.md index e69de29..e5e910a 100644 --- a/doc/initial_prompt.md +++ b/doc/initial_prompt.md @@ -0,0 +1,37 @@ +Agis en tant qu'expert en développement embarqué ESP32, C++ et PlatformIO. Je souhaite que tu rédiges l'architecture logicielle et le code d'un projet d'afficheur de navigation maritime étanche. + +Voici les spécifications matérielles et logicielles à respecter scrupuleusement : + +1. MATÉRIEL & ENVIRONNEMENT : +- Microcontrôleur : Waveshare ESP32-S3-Zero. +- Écran : Waveshare E-Ink 4.2 pouces monochrome (400x300 pixels), positionné à l'horizontale. +- Contrôles : 2 ou 3 boutons tactiles capacitifs (utilisant les broches "Touch" natives de l'ESP32-S3 connectées à des tiges métalliques). +- Environnement : PlatformIO, framework Arduino, plateforme espressif32 (v6.x+). + +2. FONCTIONNALITÉS PRINCIPALES : +- Connexion BLE : L'ESP32-S3 se connecte en mode Client à une station centrale pour recevoir les données de navigation (vent, vitesse, polaires, etc.). Les spécifications exactes du service/caractéristiques BLE sont fournies séparément. +- Interface Web (Mode AP) : Au démarrage (via une combinaison de boutons tactiles ou si le BLE est inaccessible), l'ESP32 active un point d'accès Wi-Fi sécurisé. Il héberge une interface web rudimentaire (HTML/CSS ultra-léger) permettant : + * La configuration des pages/tuiles de l'écran. + * La mise à jour du firmware via OTA (gestion des partitions requise dans platformio.ini). +- Stockage persistant : La configuration des écrans doit être sauvegardée en mémoire non volatile (Preferences ou LittleFS). + +3. AFFICHAGE & INTERFACE GRAPHIQUE : +- Utiliser obligatoirement la bibliothèque 'GxEPD2_BW' pour l'écran E-Ink. +- L'écran de 400x300 est divisé en une grille fixe de 4 "tuiles" paramétrables. +- Chaque tuile doit pouvoir afficher une donnée spécifique reçue en BLE (ex: Vitesse, Angle du Vent, Écart à la polaire) avec une police d'écriture de grande taille et une étiquette. +- Les boutons tactiles permettent de changer de "Page" (profils de 4 tuiles différents) ou de déclencher un rafraîchissement complet de l'écran. + +4. ATTENTES : +- Propose une structure de fichiers propre pour PlatformIO (main.cpp, ble_manager, wifi_manager, display_manager, config_manager). +- Donne la configuration du fichier 'platformio.ini' (incluant le partitionnement personnalisé pour l'OTA et les bibliothèques requises à jour : GxEPD2, etc.). +- Fournis un code propre, modulaire, optimisé pour limiter les rafraîchissements complets de l'e-ink (privilégier le rafraîchissement partiel pour les données numériques). +- au niveau des librairies j'aimerais que tu utilise les dernières versions de : + * zinggjm/GxEPD2 (pour la gestion de l'e-ink display) + * bblanchon/ArduinoJson (si nécessaire pour le parsing json) + * h2zero/NimBLE-Arduino (pour la communication BLE avec la station) + * ESP32Async/ESPAsyncWebServer (pour le serveur async) + * adafruit/Adafruit NeoPixel (pour la gestion de la led de statut, j'aimerais que cela indique le statut ble/wifi) +- La documentation technique doit être faite en anglais. +- Fournis des fichiers complets. + +Merci ! diff --git a/include/BleManager.h b/include/BleManager.h index 0f30b2b..024b996 100644 --- a/include/BleManager.h +++ b/include/BleManager.h @@ -67,10 +67,17 @@ public: void providePasskey(uint32_t pin); private: + // BLE callback trampolines declared in BleManager.cpp + friend void navNotifyCB(NimBLERemoteCharacteristic*, uint8_t*, size_t, bool); + friend void windNotifyCB(NimBLERemoteCharacteristic*, uint8_t*, size_t, bool); + friend void autopilotNotifyCB(NimBLERemoteCharacteristic*, uint8_t*, size_t, bool); + friend void perfNotifyCB(NimBLERemoteCharacteristic*, uint8_t*, size_t, bool); + friend void adminNotifyCB(NimBLERemoteCharacteristic*, uint8_t*, size_t, bool); + // ── NimBLEClientCallbacks ───────────────────────────────────────────────── void onConnect(NimBLEClient* pClient) override; void onDisconnect(NimBLEClient* pClient, int reason) override; - bool onConfirmPasskey(NimBLEConnInfo& connInfo, uint32_t pin) override; + void onConfirmPasskey(NimBLEConnInfo& connInfo, uint32_t pin) override; void onAuthenticationComplete(NimBLEConnInfo& connInfo) override; // ── NimBLEScanCallbacks ─────────────────────────────────────────────────── @@ -102,6 +109,8 @@ private: NimBLEAddress _targetAddr; bool _targetFound = false; bool _doConnect = false; + bool _subscribePending = false; + bool _subscribed = false; uint32_t _reconnectAt = 0; ///< millis() when next reconnect is due AppState::BleStatus _status = AppState::BleStatus::IDLE; diff --git a/include/Config.h b/include/Config.h index 709422a..d2167e8 100644 --- a/include/Config.h +++ b/include/Config.h @@ -26,11 +26,11 @@ // ============================================================================= namespace Pins { // ── E-Ink display ───────────────────────────────────────────────────────── - constexpr uint8_t EPD_CS = 9; ///< Chip Select - constexpr uint8_t EPD_DC = 8; ///< Data/Command - constexpr uint8_t EPD_RST = 7; ///< Reset - constexpr uint8_t EPD_BUSY = 6; ///< Busy signal (active LOW) - constexpr uint8_t EPD_SCK = 10; ///< SPI clock (HSPI SCK) + constexpr uint8_t EPD_CS = 7; ///< Chip Select + constexpr uint8_t EPD_DC = 6; ///< Data/Command + constexpr uint8_t EPD_RST = 5; ///< Reset + constexpr uint8_t EPD_BUSY = 4; ///< Busy signal (active LOW) + constexpr uint8_t EPD_SCK = 12; ///< SPI clock (HSPI SCK) constexpr uint8_t EPD_MOSI = 11; ///< SPI MOSI (HSPI MOSI) // ── Capacitive touch buttons (ESP32-S3 native touch) ───────────────────── @@ -80,9 +80,18 @@ namespace BleUUID { // ============================================================================= namespace BleConfig { constexpr const char* DEVICE_NAME = BLE_DEVICE_NAME; - constexpr uint32_t SCAN_DURATION = 10; ///< seconds per scan window - constexpr uint32_t RECONNECT_MS = 5000; ///< delay before reconnect attempt - constexpr uint32_t STALE_TIMEOUT = 15; ///< seconds before data is marked stale + // Optional fixed BLE address ("aa:bb:cc:dd:ee:ff"). Keep empty to disable. + constexpr const char* TARGET_ADDR = ""; + // Fallback name fragment (case-insensitive) for peripherals that do not + // expose the full local name in every advertisement packet. + constexpr const char* NAME_FALLBACK = "MarineGateway"; + // Some firmwares expose only a 16-bit service UUID in advertising payload. + // 0xFE9F observed in field logs. + constexpr uint16_t FALLBACK_SVC16 = 0xFE9F; + constexpr uint32_t DEFAULT_PASSKEY = 123456; ///< default pairing PIN used if UI has not overridden it + constexpr uint32_t SCAN_DURATION = 10000; ///< scan window in milliseconds + constexpr uint32_t RECONNECT_MS = 5000; ///< delay before reconnect attempt + constexpr uint32_t STALE_TIMEOUT = 15; ///< seconds before data is marked stale } // ============================================================================= diff --git a/include/DisplayManager.h b/include/DisplayManager.h index 664da4a..c95b937 100644 --- a/include/DisplayManager.h +++ b/include/DisplayManager.h @@ -23,10 +23,11 @@ #include #include -// ── Display model — Waveshare 4.2" mono GDEY042T81 (400×300) ───────────────── -// Adjust the model constant if your display differs. -using DisplayType = GxEPD2_BW; +// ── Display model — match the validated reference sketch for the 4.2" panel. +// The generic 420 driver is used here to avoid a controller mismatch that can +// leave the panel completely blank during init. +using DisplayType = GxEPD2_BW; class DisplayManager { public: @@ -70,10 +71,10 @@ public: private: DisplayType _display { - GxEPD2_420_GDEY042T81(Pins::EPD_CS, - Pins::EPD_DC, - Pins::EPD_RST, - Pins::EPD_BUSY) + GxEPD2_420(Pins::EPD_CS, + Pins::EPD_DC, + Pins::EPD_RST, + Pins::EPD_BUSY) }; bool _initialised = false; diff --git a/include/TouchManager.h b/include/TouchManager.h index 7cc3064..07c9798 100644 --- a/include/TouchManager.h +++ b/include/TouchManager.h @@ -35,6 +35,7 @@ public: private: // Per-pad state machine struct PadState { + explicit PadState(uint8_t p = 0) : pin(p) {} uint8_t pin; bool wasDown = false; uint32_t pressedAt = 0; @@ -42,9 +43,9 @@ private: }; PadState _pads[3] = { - {Pins::TOUCH_NEXT}, - {Pins::TOUCH_PREV}, - {Pins::TOUCH_ACTION} + PadState(Pins::TOUCH_NEXT), + PadState(Pins::TOUCH_PREV), + PadState(Pins::TOUCH_ACTION) }; uint32_t _lastDebounce = 0; diff --git a/main.cpp.test b/main.cpp.test new file mode 100644 index 0000000..fc3e412 --- /dev/null +++ b/main.cpp.test @@ -0,0 +1,132 @@ +#include +#include +#include // Pour les labels +#include // Pour les unités +#include // Pour les gros chiffres + +// --- Configuration des Pins (ESP32-S3-Zero) --- +#define EPD_CS 7 +#define EPD_DC 6 +#define EPD_RST 5 +#define EPD_BUSY 4 + +// Driver pour Waveshare 4.2" B&W +GxEPD2_BW display(GxEPD2_420(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY)); + +// --- Variables de simulation --- +float vitesse = 7.42; +int perf = 102; +float vmg = 5.15; +float cible = 7.20; + +int loopCount = 0; + +// --- Helper pour dessiner une "Tuile" d'instrument --- +void dessinerTuile(int x, int y, int w, int h, const char* label) { + // 1. Cadre arrondi extérieur + display.drawRoundRect(x, y, w, h, 8, GxEPD_BLACK); + + // 2. Bandeau noir pour le titre (label) + display.fillRoundRect(x, y, w, 30, 8, GxEPD_BLACK); + display.fillRect(x, y+20, w, 10, GxEPD_BLACK); // Carré pour éviter l'arrondi en bas du bandeau + + // 3. Texte du Label + display.setFont(&FreeSansBold9pt7b); + display.setTextColor(GxEPD_WHITE); + display.setCursor(x + 10, y + 20); + display.print(label); +} + +// --- Dessiner tout le layout fixe --- +void dessinerLayoutFixe() { + display.fillScreen(GxEPD_WHITE); + + // On divise l'écran (400x300) en 4 zones de ~190x140 pixels + dessinerTuile(5, 5, 190, 140, "STW"); + dessinerTuile(205, 5, 190, 140, "POLAR"); + dessinerTuile(5, 155, 190, 140, "VMG"); + dessinerTuile(205, 155, 190, 140, "TARGET"); +} + +void afficherValeur(int x, int y, int width, int height, const char* tvalue, const char* tunit) { + int16_t x1, y1; + uint16_t textW, textH; + display.getTextBounds(tvalue, 0, 0, &x1, &y1, &textW, &textH); + + display.fillRect(x, y, width, height, GxEPD_WHITE); + display.setFont(&FreeSansBold24pt7b); + display.setCursor(x + (width - textW) / 2 - x1, y+55); + display.print(tvalue); + display.setFont(&FreeSansBold12pt7b); + display.setCursor(x, y+5); + display.print(tunit); +} + +// --- Mettre à jour les chiffres uniquement --- +void mettreAJourValeurs() { + + display.fillScreen(GxEPD_WHITE); + + // dessinerLayoutFixe(); + + dessinerTuile(5, 5, 190, 140, "STW"); + dessinerTuile(205, 5, 190, 140, "POLAR"); + dessinerTuile(5, 155, 190, 140, "VMG"); + dessinerTuile(205, 155, 190, 140, "TARGET"); + + display.setTextColor(GxEPD_BLACK); + // --- 1. VITESSE --- + afficherValeur(15, 60, 130, 70, String(vitesse, 1).c_str(), " kt"); + // --- 2. PERFORMANCE --- + afficherValeur(215, 60, 130, 70, String(perf).c_str(), " %"); + // --- 3. VMG --- + afficherValeur(15, 210, 130, 70, String(vmg, 1).c_str(), " kt"); + // --- 4. CIBLE --- + afficherValeur(215, 210, 130, 70, String(cible, 1).c_str(), " kt"); +} + +void setup() { + Serial.begin(115200); + display.init(115200, true, 2, false); + display.setRotation(0); // Mode paysage + + Serial.println("Initialisation du Layout..."); + + display.setFullWindow(); + display.firstPage(); + do { + // dessinerLayoutFixe(); + mettreAJourValeurs(); + } while (display.nextPage()); +} + +void loop() { + delay(3000); // 3 secondes + + // Simulation de variation + vitesse += (random(-5, 6) / 100.0); + perf = random(95, 105); + vmg = vitesse * 0.8; + + loopCount++; + + if (loopCount >= 20) { // Full refresh toutes les minute pour nettoyer + display.setFullWindow(); + display.firstPage(); + do { + // dessinerLayoutFixe(); + mettreAJourValeurs(); + } while (display.nextPage()); + loopCount = 0; + } + else { // Rafraîchissement Partiel + display.setPartialWindow(0, 0, 400, 300); + display.firstPage(); + do { + // dessinerLayoutFixe(); + mettreAJourValeurs(); + } while (display.nextPage()); + } + + display.powerOff(); +} \ No newline at end of file diff --git a/partitions/custom_ota.csv b/partitions/custom_ota.csv index 8cdbb9d..98c96ab 100644 --- a/partitions/custom_ota.csv +++ b/partitions/custom_ota.csv @@ -2,9 +2,18 @@ # Flash size: 8 MB (Waveshare ESP32-S3-Zero has 8 MB flash) # # Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x180000, -app1, app, ota_1, 0x190000, 0x180000, -littlefs, data, spiffs, 0x310000, 0x100000, -coredump, data, coredump, 0x410000, 0x10000, +# nvs, data, nvs, 0x9000, 0x5000, +# otadata, data, ota, 0xe000, 0x2000, +# app0, app, ota_0, 0x10000, 0x180000, +# app1, app, ota_1, 0x190000, 0x180000, +# littlefs, data, spiffs, 0x310000, 0x100000, +# coredump, data, coredump, 0x410000, 0x10000, + + +# Name, Type, SubType, Offset, Size, Flags +nvs,data,nvs,0x9000,0x5000, +otadata,data,ota,0xE000,0x2000, +app0,app,ota_0,0x10000,0x140000, +app1,app,ota_1,0x150000,0x140000, +spiffs,data,spiffs,0x290000,0x160000, +coredump,data,coredump,0x3F0000,0x10000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 976d48a..922be47 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,11 +10,15 @@ default_envs = esp32s3_marine_display ; Common settings shared across environments ; ----------------------------------------------------------------------------- [env] -platform = espressif32 @ ^6.9.0 +platform = espressif32 @ ^6 framework = arduino monitor_speed = 115200 monitor_filters = esp32_exception_decoder, time + +monitor_rts = 0 +monitor_dtr = 0 + ; ----------------------------------------------------------------------------- ; Production environment ; ----------------------------------------------------------------------------- @@ -29,6 +33,7 @@ board_build.partitions = partitions/custom_ota.csv ; Flash mode: QIO for speed, 80 MHz board_build.flash_mode = qio board_build.f_flash = 80000000L +board_upload.flash_size = 4MB ; Enable PSRAM if available on the S3-Zero variant build_flags = @@ -51,7 +56,8 @@ build_flags = -D STATUS_LED_COUNT=1 ; ── Misc ────────────────────────────────────────────────────────────── -D CORE_DEBUG_LEVEL=3 - -DCONFIG_NIMBLE_ENABLED=1 + -D CONFIG_BT_ENABLED=1 + -D CONFIG_BT_NIMBLE_ENABLED=1 ; LittleFS filesystem image (contains web UI assets) board_build.filesystem = littlefs diff --git a/src/BleManager.cpp b/src/BleManager.cpp index 6cd27db..41abaf6 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -11,29 +11,82 @@ #include "BleManager.h" #include +#include +#include +#include +#include // Singleton BleManager bleManager; +namespace { + +bool containsCaseInsensitive(const std::string& haystack, const char* needle) { + if (!needle || needle[0] == '\0' || haystack.empty()) return false; + + std::string h = haystack; + std::string n = needle; + std::transform(h.begin(), h.end(), h.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + std::transform(n.begin(), n.end(), n.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return h.find(n) != std::string::npos; +} + +bool matchesTargetDevice(const NimBLEAdvertisedDevice* device, + const std::string& devName, + bool* outNameExact, + bool* outSvc128, + bool* outAddrMatch, + bool* outNameFallback, + bool* outSvc16) { + const bool nameExact = !devName.empty() && devName == BleConfig::DEVICE_NAME; + + const bool svc128 = + device->isAdvertisingService(NimBLEUUID(BleUUID::NAV_SVC)) || + device->isAdvertisingService(NimBLEUUID(BleUUID::WIND_SVC)) || + device->isAdvertisingService(NimBLEUUID(BleUUID::AUTOPILOT_SVC)) || + device->isAdvertisingService(NimBLEUUID(BleUUID::PERF_SVC)) || + device->isAdvertisingService(NimBLEUUID(BleUUID::ADMIN_SVC)); + + const bool hasTargetAddr = (BleConfig::TARGET_ADDR[0] != '\0'); + const bool addrMatch = + hasTargetAddr && strcasecmp(device->getAddress().toString().c_str(), BleConfig::TARGET_ADDR) == 0; + + const bool nameFallback = containsCaseInsensitive(devName, BleConfig::NAME_FALLBACK); + const bool svc16Fallback = + device->isAdvertisingService(NimBLEUUID(static_cast(BleConfig::FALLBACK_SVC16))); + + if (outNameExact) *outNameExact = nameExact; + if (outSvc128) *outSvc128 = svc128; + if (outAddrMatch) *outAddrMatch = addrMatch; + if (outNameFallback) *outNameFallback = nameFallback; + if (outSvc16) *outSvc16 = svc16Fallback; + + return nameExact || svc128 || addrMatch || nameFallback || svc16Fallback; +} + +} // namespace + // ============================================================================= // Helpers — static notify callback trampolines // NimBLE requires a free function or a capturing lambda; we use lambdas that // capture the singleton pointer. // ============================================================================= -static void navNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { +void navNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { bleManager._onNavNotify(c, d, l, n); } -static void windNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { +void windNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { bleManager._onWindNotify(c, d, l, n); } -static void autopilotNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { +void autopilotNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { bleManager._onAutopilotNotify(c, d, l, n); } -static void perfNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { +void perfNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { bleManager._onPerfNotify(c, d, l, n); } -static void adminNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { +void adminNotifyCB(NimBLERemoteCharacteristic* c, uint8_t* d, size_t l, bool n) { bleManager._onAdminNotify(c, d, l, n); } @@ -49,7 +102,11 @@ void BleManager::begin() { BLE_SM_PAIR_AUTHREQ_BOND); NimBLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); - _status = AppState::BleStatus::IDLE; + // Default to the requested pairing PIN unless the web UI overrides it. + _passkey = BleConfig::DEFAULT_PASSKEY; + _status = AppState::BleStatus::IDLE; + _subscribePending = false; + _subscribed = false; _startScan(); } @@ -58,6 +115,8 @@ void BleManager::update() { if (_doConnect && _targetFound) { _doConnect = false; _status = AppState::BleStatus::CONNECTING; + _subscribePending = false; + _subscribed = false; if (!_connectToServer(_targetAddr)) { Serial.println("[BLE] Connection failed — scheduling reconnect"); _scheduleReconnect(); @@ -65,6 +124,21 @@ void BleManager::update() { return; } + // Perform service discovery + subscriptions outside NimBLE callback context. + if (isConnected() && _subscribePending) { + _subscribePending = false; + Serial.println("[BLE] Running deferred subscription step"); + if (!_subscribeAll()) { + Serial.println("[BLE] Subscription failed — disconnecting"); + if (_pClient && _pClient->isConnected()) { + _pClient->disconnect(); + } + return; + } + _subscribed = true; + Serial.println("[BLE] Subscriptions active"); + } + // Handle scheduled reconnect if (!isConnected() && _reconnectAt > 0 && millis() >= _reconnectAt) { _reconnectAt = 0; @@ -144,11 +218,54 @@ void BleManager::providePasskey(uint32_t pin) { // ============================================================================= void BleManager::onResult(const NimBLEAdvertisedDevice* device) { - Serial.printf("[BLE] Found device: %s name: %s\n", + std::string devName = device->getName(); + const char* namePtr = devName.empty() ? "" : devName.c_str(); + const uint8_t svcCount = device->getServiceUUIDCount(); + const bool connectable = device->isConnectable(); + Serial.printf("[BLE] Found device: %s name: %s svcCount=%u connectable=%d\n", device->getAddress().toString().c_str(), - device->getName().c_str()); + namePtr, + svcCount, + connectable); - if (device->getName() == BleConfig::DEVICE_NAME) { + if (svcCount > 0) { + for (uint8_t i = 0; i < svcCount && i < 3; ++i) { + Serial.printf("[BLE] svc[%u]=%s\n", i, device->getServiceUUID(i).toString().c_str()); + } + } + + bool nameMatches = false; + bool hasExpectedService = false; + bool addrMatches = false; + bool fallbackNameMatches = false; + bool fallbackSvc16Matches = false; + + if (matchesTargetDevice(device, + devName, + &nameMatches, + &hasExpectedService, + &addrMatches, + &fallbackNameMatches, + &fallbackSvc16Matches)) { + if (!connectable) { + Serial.println("[BLE] Target signature matched but advertiser is not connectable — skipping"); + return; + } + + const bool strongMatch = nameMatches || hasExpectedService || addrMatches || fallbackNameMatches; + if (!strongMatch && fallbackSvc16Matches) { + Serial.printf("[BLE] Fallback-only match on 0x%04x — waiting for stronger identifier before connect\n", + BleConfig::FALLBACK_SVC16); + return; + } + + Serial.printf("[BLE] Target match: name=%d svc128=%d addr=%d nameFallback=%d svc16=0x%04x(%d)\n", + nameMatches, + hasExpectedService, + addrMatches, + fallbackNameMatches, + BleConfig::FALLBACK_SVC16, + fallbackSvc16Matches); Serial.println("[BLE] Target found — stopping scan"); NimBLEDevice::getScan()->stop(); _targetAddr = device->getAddress(); @@ -159,6 +276,56 @@ void BleManager::onResult(const NimBLEAdvertisedDevice* device) { } void BleManager::onScanEnd(const NimBLEScanResults& results, int reason) { + if (!_targetFound && !isConnected()) { + const NimBLEAdvertisedDevice* fallbackDev = nullptr; + + for (int i = 0; i < results.getCount(); ++i) { + const NimBLEAdvertisedDevice* dev = results.getDevice(i); + if (!dev) continue; + if (!dev->isConnectable()) continue; + + std::string devName = dev->getName(); + bool nameMatches = false; + bool hasExpectedService = false; + bool addrMatches = false; + bool fallbackNameMatches = false; + bool fallbackSvc16Matches = false; + + if (!matchesTargetDevice(dev, + devName, + &nameMatches, + &hasExpectedService, + &addrMatches, + &fallbackNameMatches, + &fallbackSvc16Matches)) { + continue; + } + + const bool strongMatch = nameMatches || hasExpectedService || addrMatches || fallbackNameMatches; + if (strongMatch) { + Serial.println("[BLE] Scan result matched gateway — connecting"); + _targetAddr = dev->getAddress(); + _targetFound = true; + _doConnect = true; + _status = AppState::BleStatus::CONNECTING; + break; + } + + if (!fallbackDev && fallbackSvc16Matches) { + fallbackDev = dev; + } + } + + if (!_targetFound && fallbackDev) { + Serial.printf("[BLE] No strong identifier found; connecting using fallback UUID 0x%04x\n", + BleConfig::FALLBACK_SVC16); + _targetAddr = fallbackDev->getAddress(); + _targetFound = true; + _doConnect = true; + _status = AppState::BleStatus::CONNECTING; + } + } + if (!_targetFound && !isConnected()) { Serial.println("[BLE] Scan ended without target — retrying"); _scheduleReconnect(); @@ -172,31 +339,39 @@ void BleManager::onScanEnd(const NimBLEScanResults& results, int reason) { void BleManager::onConnect(NimBLEClient* pClient) { Serial.println("[BLE] Connected to Marine Gateway"); _status = AppState::BleStatus::CONNECTED; + _subscribePending = false; + _subscribed = false; } void BleManager::onDisconnect(NimBLEClient* pClient, int reason) { Serial.printf("[BLE] Disconnected, reason=%d\n", reason); _status = AppState::BleStatus::DISCONNECTED; _targetFound = false; + _subscribePending = false; + _subscribed = false; _scheduleReconnect(); } -bool BleManager::onConfirmPasskey(NimBLEConnInfo& connInfo, uint32_t pin) { +void BleManager::onConfirmPasskey(NimBLEConnInfo& connInfo, uint32_t pin) { Serial.printf("[BLE] Gateway requests passkey confirmation. PIN displayed: %06lu\n", (unsigned long)pin); - // Wait up to 30 s for the web UI / Serial to provide the key + + bool accept = false; uint32_t deadline = millis() + 30000; - while (!_passkeyPending && millis() < deadline) { - delay(100); - } + if (_passkeyPending) { + while (_passkeyPending && millis() < deadline) { + delay(100); + } _passkeyPending = false; - bool match = (_passkey == pin); - Serial.printf("[BLE] Passkey match: %s\n", match ? "YES" : "NO"); - return match; } - // Timeout — accept the PIN directly (user must have entered it on the device) - Serial.println("[BLE] Passkey timeout — auto-confirming"); - return true; + + accept = (_passkey == pin); + Serial.printf("[BLE] Passkey match: %s (configured=%06lu, shown=%06lu)\n", + accept ? "YES" : "NO", + (unsigned long)_passkey, + (unsigned long)pin); + + NimBLEDevice::injectConfirmPasskey(connInfo, accept); } void BleManager::onAuthenticationComplete(NimBLEConnInfo& connInfo) { @@ -205,11 +380,8 @@ void BleManager::onAuthenticationComplete(NimBLEConnInfo& connInfo) { _pClient->disconnect(); return; } - Serial.println("[BLE] Authentication complete — subscribing to characteristics"); - if (!_subscribeAll()) { - Serial.println("[BLE] Subscription failed — disconnecting"); - _pClient->disconnect(); - } + Serial.println("[BLE] Authentication complete — scheduling deferred subscriptions"); + _subscribePending = true; } // ============================================================================= @@ -217,10 +389,24 @@ void BleManager::onAuthenticationComplete(NimBLEConnInfo& connInfo) { // ============================================================================= bool BleManager::_connectToServer(const NimBLEAddress& addr) { + if (_pClient) { + if (_pClient->isConnected()) { + _pClient->disconnect(); + } + NimBLEDevice::deleteClient(_pClient); + _pClient = nullptr; + } + + Serial.printf("[BLE] Connecting to %s\n", addr.toString().c_str()); _pClient = NimBLEDevice::createClient(); + if (!_pClient) { + Serial.println("[BLE] createClient() failed"); + return false; + } + _pClient->setClientCallbacks(this, false); _pClient->setConnectionParams(12, 12, 0, 51); // 15 ms interval, 510 ms timeout - _pClient->setConnectTimeout(10); + _pClient->setConnectTimeout(5000); // allow enough time for a real peer to respond if (!_pClient->connect(addr)) { Serial.println("[BLE] connect() failed"); @@ -242,18 +428,22 @@ bool BleManager::_subscribeAll() { struct SubEntry { const char* svcUUID; const char* chrUUID; - notify_callback cb; + NimBLERemoteCharacteristic::notify_callback cb; }; SubEntry entries[] = { - { BleUUID::NAV_SVC, BleUUID::NAV_DATA, navNotifyCB }, - { BleUUID::WIND_SVC, BleUUID::WIND_DATA, windNotifyCB }, + { BleUUID::NAV_SVC, BleUUID::NAV_DATA, navNotifyCB }, + { BleUUID::WIND_SVC, BleUUID::WIND_DATA, windNotifyCB }, { BleUUID::AUTOPILOT_SVC, BleUUID::AUTOPILOT_DATA, autopilotNotifyCB }, - { BleUUID::PERF_SVC, BleUUID::PERF_DATA, perfNotifyCB }, - { BleUUID::ADMIN_SVC, BleUUID::ADMIN_DATA, adminNotifyCB }, + { BleUUID::PERF_SVC, BleUUID::PERF_DATA, perfNotifyCB }, + { BleUUID::ADMIN_SVC, BleUUID::ADMIN_DATA, adminNotifyCB }, }; + uint8_t subscribedCount = 0; + uint8_t attemptedCount = 0; + for (auto& e : entries) { + attemptedCount++; auto* svc = _pClient->getService(e.svcUUID); if (!svc) { Serial.printf("[BLE] Service %s not found\n", e.svcUUID); @@ -269,10 +459,18 @@ bool BleManager::_subscribeAll() { Serial.printf("[BLE] Subscribe failed for %s\n", e.chrUUID); } else { Serial.printf("[BLE] Subscribed to %s\n", e.chrUUID); + subscribedCount++; } + } else { + Serial.printf("[BLE] Characteristic %s does not support notify\n", e.chrUUID); } } - return true; + + Serial.printf("[BLE] Subscription summary: %u/%u characteristics subscribed\n", + subscribedCount, + attemptedCount); + + return subscribedCount > 0; } void BleManager::_startScan() { @@ -284,9 +482,9 @@ void BleManager::_startScan() { pScan->setActiveScan(true); pScan->setInterval(100); pScan->setWindow(99); - pScan->start(BleConfig::SCAN_DURATION, false); + pScan->start(BleConfig::SCAN_DURATION, false, true); - Serial.printf("[BLE] Scanning for \"%s\" (%u s)\n", + Serial.printf("[BLE] Scanning for \"%s\" (%u ms)\n", BleConfig::DEVICE_NAME, BleConfig::SCAN_DURATION); } diff --git a/src/ConfigManager.cpp b/src/ConfigManager.cpp index 245b315..511ae45 100644 --- a/src/ConfigManager.cpp +++ b/src/ConfigManager.cpp @@ -243,17 +243,25 @@ void ConfigManager::_applyDefaults() { _cfg.activePage = 0; strlcpy(_cfg.pages[0].name, "Sailing", sizeof(PageConfig::name)); - _cfg.pages[0].tiles[0] = { DataField::SOG, "SOG" }; - _cfg.pages[0].tiles[1] = { DataField::TWA, "TWA" }; - _cfg.pages[0].tiles[2] = { DataField::TWS, "TWS" }; - _cfg.pages[0].tiles[3] = { DataField::VMG, "VMG" }; + _cfg.pages[0].tiles[0].field = DataField::SOG; + strlcpy(_cfg.pages[0].tiles[0].label, "SOG", sizeof(_cfg.pages[0].tiles[0].label)); + _cfg.pages[0].tiles[1].field = DataField::TWA; + strlcpy(_cfg.pages[0].tiles[1].label, "TWA", sizeof(_cfg.pages[0].tiles[1].label)); + _cfg.pages[0].tiles[2].field = DataField::TWS; + strlcpy(_cfg.pages[0].tiles[2].label, "TWS", sizeof(_cfg.pages[0].tiles[2].label)); + _cfg.pages[0].tiles[3].field = DataField::VMG; + strlcpy(_cfg.pages[0].tiles[3].label, "VMG", sizeof(_cfg.pages[0].tiles[3].label)); // Page 1 — Performance strlcpy(_cfg.pages[1].name, "Performance", sizeof(PageConfig::name)); - _cfg.pages[1].tiles[0] = { DataField::POLAR_PCT, "Polar %" }; - _cfg.pages[1].tiles[1] = { DataField::TARGET_STW, "Tgt STW" }; - _cfg.pages[1].tiles[2] = { DataField::AWA, "AWA" }; - _cfg.pages[1].tiles[3] = { DataField::DEPTH, "Depth" }; + _cfg.pages[1].tiles[0].field = DataField::POLAR_PCT; + strlcpy(_cfg.pages[1].tiles[0].label, "Polar %", sizeof(_cfg.pages[1].tiles[0].label)); + _cfg.pages[1].tiles[1].field = DataField::TARGET_STW; + strlcpy(_cfg.pages[1].tiles[1].label, "Tgt STW", sizeof(_cfg.pages[1].tiles[1].label)); + _cfg.pages[1].tiles[2].field = DataField::AWA; + strlcpy(_cfg.pages[1].tiles[2].label, "AWA", sizeof(_cfg.pages[1].tiles[2].label)); + _cfg.pages[1].tiles[3].field = DataField::DEPTH; + strlcpy(_cfg.pages[1].tiles[3].label, "Depth", sizeof(_cfg.pages[1].tiles[3].label)); strlcpy(_cfg.apSSID, "MarineDisplay", sizeof(_cfg.apSSID)); strlcpy(_cfg.apPassword, "marine123", sizeof(_cfg.apPassword)); diff --git a/src/DisplayManager.cpp b/src/DisplayManager.cpp index 4bda5de..940583e 100644 --- a/src/DisplayManager.cpp +++ b/src/DisplayManager.cpp @@ -14,14 +14,10 @@ */ #include "DisplayManager.h" -#include // Singleton DisplayManager displayManager; -// Custom SPI instance on HSPI so the default SPI remains free for other use -static SPIClass ePaperSpi(HSPI); - // ============================================================================= // Font shorthands // ============================================================================= @@ -34,9 +30,8 @@ static SPIClass ePaperSpi(HSPI); // ============================================================================= void DisplayManager::begin() { - ePaperSpi.begin(Pins::EPD_SCK, -1, Pins::EPD_MOSI, Pins::EPD_CS); - _display.epd2.selectSPI(ePaperSpi, SPISettings(4000000, MSBFIRST, SPI_MODE0)); - _display.init(115200, true, 50, false); + // Use the same init path as the working reference sketch. + _display.init(115200, true, 2, false); _display.setRotation(0); // landscape 400×300 // Blank the value cache