fix ble connection , ok !
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Built: 2026-06-18T16:25:38.961944Z
|
||||
@@ -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 !
|
||||
|
||||
+10
-1
@@ -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;
|
||||
|
||||
+17
-8
@@ -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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -23,10 +23,11 @@
|
||||
#include <Fonts/FreeMonoBold12pt7b.h>
|
||||
#include <Fonts/FreeMono9pt7b.h>
|
||||
|
||||
// ── Display model — Waveshare 4.2" mono GDEY042T81 (400×300) ─────────────────
|
||||
// Adjust the model constant if your display differs.
|
||||
using DisplayType = GxEPD2_BW<GxEPD2_420_GDEY042T81,
|
||||
GxEPD2_420_GDEY042T81::HEIGHT>;
|
||||
// ── 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<GxEPD2_420,
|
||||
GxEPD2_420::HEIGHT>;
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
#include <Arduino.h>
|
||||
#include <GxEPD2_BW.h>
|
||||
#include <Fonts/FreeSansBold9pt7b.h> // Pour les labels
|
||||
#include <Fonts/FreeSansBold12pt7b.h> // Pour les unités
|
||||
#include <Fonts/FreeSansBold24pt7b.h> // 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<GxEPD2_420, GxEPD2_420::HEIGHT> 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();
|
||||
}
|
||||
@@ -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,
|
||||
|
+8
-2
@@ -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
|
||||
|
||||
+232
-34
@@ -11,29 +11,82 @@
|
||||
|
||||
#include "BleManager.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <strings.h>
|
||||
|
||||
// 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<char>(std::tolower(c)); });
|
||||
std::transform(n.begin(), n.end(), n.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(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<uint16_t>(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() ? "<none>" : 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);
|
||||
}
|
||||
|
||||
|
||||
+16
-8
@@ -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));
|
||||
|
||||
@@ -14,14 +14,10 @@
|
||||
*/
|
||||
|
||||
#include "DisplayManager.h"
|
||||
#include <SPI.h>
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user