fix ble connection , ok !

This commit is contained in:
feeling001@gmail.com
2026-06-21 11:50:44 +02:00
parent c40d7516b1
commit d44aaa3318
13 changed files with 483 additions and 83 deletions
-6
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
Built: 2026-06-18T16:25:38.961944Z
+37
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
// =============================================================================
+9 -8
View File
@@ -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;
+4 -3
View File
@@ -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
View File
@@ -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();
}
+15 -6
View File
@@ -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,
1 # Marine Display — Custom Partition Table
2 # Flash size: 8 MB (Waveshare ESP32-S3-Zero has 8 MB flash)
3 #
4 # Name, Type, SubType, Offset, Size, Flags
5 nvs, data, nvs, 0x9000, 0x5000, # nvs, data, nvs, 0x9000, 0x5000,
6 otadata, data, ota, 0xe000, 0x2000, # otadata, data, ota, 0xe000, 0x2000,
7 app0, app, ota_0, 0x10000, 0x180000, # app0, app, ota_0, 0x10000, 0x180000,
8 app1, app, ota_1, 0x190000, 0x180000, # app1, app, ota_1, 0x190000, 0x180000,
9 littlefs, data, spiffs, 0x310000, 0x100000, # littlefs, data, spiffs, 0x310000, 0x100000,
10 coredump, data, coredump, 0x410000, 0x10000, # coredump, data, coredump, 0x410000, 0x10000,
11 # Name, Type, SubType, Offset, Size, Flags
12 nvs,data,nvs,0x9000,0x5000,
13 otadata,data,ota,0xE000,0x2000,
14 app0,app,ota_0,0x10000,0x140000,
15 app1,app,ota_1,0x150000,0x140000,
16 spiffs,data,spiffs,0x290000,0x160000,
17 coredump,data,coredump,0x3F0000,0x10000,
18
19
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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));
+2 -7
View File
@@ -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