fix: CYD BLE connection via service-UUID scan and clean connect flow
- Scan for ThermoPro by advertised service UUID instead of relying only on MAC, solving resolvable-private-address issues after phone pairing. - Defer connect until after the scan is fully stopped to avoid NimBLE race. - Add connect cleanup, state-name diagnostics, and main-loop retry. - Switch display config to LGFX_AUTODETECT for correct CYD pinout. - Verified: CYD connects to ThermoPro and receives temperature notifications.
This commit is contained in:
@@ -19,17 +19,30 @@ void ThermoproBLE::setAddress(const String& mac) {
|
||||
address_ = mac;
|
||||
}
|
||||
|
||||
bool ThermoproBLE::init(const String& deviceName) {
|
||||
if (address_.length() == 0) {
|
||||
Serial.println("ThermoproBLE: no MAC address configured");
|
||||
return false;
|
||||
const char* ThermoproBLE::getStateName() const {
|
||||
switch (state_) {
|
||||
case State::Idle: return "idle";
|
||||
case State::Scanning: return "scanning";
|
||||
case State::Connecting: return "connecting";
|
||||
case State::Discovering: return "discovering";
|
||||
case State::EnablingNotify: return "enabling-notify";
|
||||
case State::SendingHandshake: return "sending-handshake";
|
||||
case State::Running: return "running";
|
||||
case State::Reconnecting: return "reconnecting";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
bool ThermoproBLE::init(const String& deviceName) {
|
||||
NimBLEDevice::init(std::string(deviceName.c_str()));
|
||||
// We do not need a security/bonded pairing for the ThermoPro protocol.
|
||||
NimBLEDevice::setSecurityAuth(false, false, false);
|
||||
|
||||
Serial.printf("ThermoproBLE: BLE initialized as '%s', target %s\n", deviceName.c_str(), address_.c_str());
|
||||
if (address_.length() > 0) {
|
||||
Serial.printf("ThermoproBLE: BLE initialized as '%s', target MAC %s\n", deviceName.c_str(), address_.c_str());
|
||||
} else {
|
||||
Serial.printf("ThermoproBLE: BLE initialized as '%s', will scan for ThermoPro service UUID\n", deviceName.c_str());
|
||||
}
|
||||
state_ = State::Idle;
|
||||
return true;
|
||||
}
|
||||
@@ -57,16 +70,56 @@ void ThermoproBLE::update() {
|
||||
break;
|
||||
|
||||
case State::Scanning:
|
||||
// If we found a candidate during the scan, stop scanning and connect.
|
||||
if (pendingConnectFound_) {
|
||||
if (millis() - pendingConnectSeenMillis_ >= 200) {
|
||||
NimBLEDevice::getScan()->stop();
|
||||
state_ = State::Connecting;
|
||||
stateStartMillis_ = millis();
|
||||
connectToAddress_(pendingConnectAddress_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (millis() - stateStartMillis_ > 25000) {
|
||||
Serial.println("ThermoproBLE: scan timeout, forcing reconnect");
|
||||
NimBLEDevice::getScan()->stop();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
}
|
||||
break;
|
||||
|
||||
case State::Connecting:
|
||||
if (client_ && client_->isConnected()) {
|
||||
Serial.println("ThermoproBLE: connected to server");
|
||||
if (!setupConnection_(client_)) {
|
||||
Serial.println("ThermoproBLE: setup failed");
|
||||
cleanupClient_();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
break;
|
||||
}
|
||||
state_ = State::Running;
|
||||
stateStartMillis_ = millis();
|
||||
lastActivityMillis_ = millis();
|
||||
reconnectFailures_ = 0;
|
||||
reconnectDelayMs_ = 5000;
|
||||
break;
|
||||
}
|
||||
if (millis() - stateStartMillis_ > 25000) {
|
||||
Serial.println("ThermoproBLE: connect timeout");
|
||||
cleanupClient_();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
}
|
||||
break;
|
||||
|
||||
case State::Discovering:
|
||||
case State::EnablingNotify:
|
||||
case State::SendingHandshake:
|
||||
// Time out stuck states and restart the reconnect cycle.
|
||||
if (millis() - stateStartMillis_ > 15000) {
|
||||
Serial.printf("ThermoproBLE: state timeout in state %d, forcing reconnect\n", static_cast<int>(state_));
|
||||
if (client_ && client_->isConnected()) {
|
||||
client_->disconnect();
|
||||
}
|
||||
Serial.printf("ThermoproBLE: setup state timeout in state %d\n", static_cast<int>(state_));
|
||||
cleanupClient_();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
}
|
||||
@@ -75,14 +128,14 @@ void ThermoproBLE::update() {
|
||||
case State::Running: {
|
||||
if (!client_ || !client_->isConnected()) {
|
||||
Serial.println("ThermoproBLE: connection lost, reconnecting");
|
||||
cleanupClient_();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
break;
|
||||
}
|
||||
// If we haven't seen any notification in 30 seconds, reconnect.
|
||||
if (millis() - lastActivityMillis_ > 30000) {
|
||||
Serial.println("ThermoproBLE: no notifications for 30s, reconnecting");
|
||||
client_->disconnect();
|
||||
cleanupClient_();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
}
|
||||
@@ -99,9 +152,7 @@ void ThermoproBLE::update() {
|
||||
}
|
||||
|
||||
void ThermoproBLE::disconnect() {
|
||||
if (client_ && client_->isConnected()) {
|
||||
client_->disconnect();
|
||||
}
|
||||
cleanupClient_();
|
||||
NimBLEDevice::deinit(true);
|
||||
setConnected_(false);
|
||||
state_ = State::Idle;
|
||||
@@ -111,22 +162,34 @@ void ThermoproBLE::ScanCallbacks::onResult(NimBLEAdvertisedDevice* device) {
|
||||
if (ThermoproBLE::instance_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* self = ThermoproBLE::instance_;
|
||||
String addr(device->getAddress().toString().c_str());
|
||||
String name(device->getName().c_str());
|
||||
|
||||
bool matchesAddress = addr.equalsIgnoreCase(ThermoproBLE::instance_->address_);
|
||||
bool matchesAddress = !self->address_.isEmpty()
|
||||
&& addr.equalsIgnoreCase(self->address_);
|
||||
bool matchesName = isThermoproDevice_(name);
|
||||
bool matchesService = device->isAdvertisingService(ThermoproBLE::kServiceUuid);
|
||||
|
||||
if (matchesAddress) {
|
||||
Serial.printf("ThermoproBLE: found target device %s (%s)\n", name.c_str(), addr.c_str());
|
||||
NimBLEDevice::getScan()->stop();
|
||||
connectToServer_(device, ThermoproBLE::instance_);
|
||||
} else if (matchesName) {
|
||||
Serial.printf("ThermoproBLE: discovered ThermoPro %s (%s)\n", name.c_str(), addr.c_str());
|
||||
// Verbose scan logging during bringup. Print every device address + name.
|
||||
Serial.printf("[ble-scan] %s rssi=%d name='%s' service_match=%s\n",
|
||||
addr.c_str(), device->getRSSI(), name.c_str(),
|
||||
matchesService ? "yes" : "no");
|
||||
|
||||
if (matchesAddress || matchesName || matchesService) {
|
||||
Serial.printf("ThermoproBLE: discovered candidate %s (%s), addrType=%d\n",
|
||||
name.c_str(), addr.c_str(), device->getAddress().getType());
|
||||
// Record the address for a connect initiated from update() once the
|
||||
// scan has been stopped cleanly. Don't connect from inside the callback.
|
||||
self->pendingConnectAddress_ = device->getAddress();
|
||||
self->pendingConnectFound_ = true;
|
||||
self->pendingConnectSeenMillis_ = millis();
|
||||
}
|
||||
}
|
||||
|
||||
bool ThermoproBLE::scanAndConnect_() {
|
||||
pendingConnectFound_ = false;
|
||||
|
||||
NimBLEScan* scanner = NimBLEDevice::getScan();
|
||||
if (!callbacksRegistered_) {
|
||||
scanner->setAdvertisedDeviceCallbacks(&scanCallbacks_, false);
|
||||
@@ -139,46 +202,48 @@ bool ThermoproBLE::scanAndConnect_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ThermoproBLE::connectToServer_(NimBLEAdvertisedDevice* device, void* userData) {
|
||||
auto* self = static_cast<ThermoproBLE*>(userData);
|
||||
if (!self) {
|
||||
bool ThermoproBLE::connectToAddress_(const NimBLEAddress& address) {
|
||||
state_ = State::Connecting;
|
||||
stateStartMillis_ = millis();
|
||||
|
||||
bool addrTypeRandom = (address.getType() == BLE_ADDR_RANDOM);
|
||||
Serial.printf("ThermoproBLE: connecting to %s, addrType=%s\n",
|
||||
address.toString().c_str(), addrTypeRandom ? "random" : "public");
|
||||
|
||||
// The scan must already be stopped before this is called.
|
||||
delay(300);
|
||||
|
||||
client_ = NimBLEDevice::createClient();
|
||||
// Conservative connection parameters.
|
||||
client_->setConnectionParams(12, 12, 0, 160);
|
||||
client_->setConnectTimeout(20);
|
||||
|
||||
bool ok = client_->connect(address, false);
|
||||
if (!ok) {
|
||||
Serial.println("ThermoproBLE: connect call rejected immediately");
|
||||
cleanupClient_();
|
||||
setConnected_(false);
|
||||
startReconnectDelay_();
|
||||
return false;
|
||||
}
|
||||
|
||||
self->state_ = State::Connecting;
|
||||
self->stateStartMillis_ = millis();
|
||||
|
||||
self->client_ = NimBLEDevice::createClient();
|
||||
self->client_->setConnectionParams(12, 12, 0, 51);
|
||||
self->client_->setConnectTimeout(10);
|
||||
|
||||
if (!self->client_->connect(device)) {
|
||||
Serial.println("ThermoproBLE: connect failed");
|
||||
NimBLEDevice::deleteClient(self->client_);
|
||||
self->client_ = nullptr;
|
||||
self->setConnected_(false);
|
||||
self->startReconnectDelay_();
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.println("ThermoproBLE: connected to server");
|
||||
if (!self->setupConnection_(self->client_)) {
|
||||
Serial.println("ThermoproBLE: setup failed");
|
||||
NimBLEDevice::deleteClient(self->client_);
|
||||
self->client_ = nullptr;
|
||||
self->setConnected_(false);
|
||||
self->startReconnectDelay_();
|
||||
return false;
|
||||
}
|
||||
|
||||
self->state_ = State::Running;
|
||||
self->stateStartMillis_ = millis();
|
||||
self->lastActivityMillis_ = millis();
|
||||
self->reconnectFailures_ = 0;
|
||||
self->reconnectDelayMs_ = 5000;
|
||||
Serial.println("ThermoproBLE: connect call accepted");
|
||||
return true;
|
||||
}
|
||||
|
||||
void ThermoproBLE::cleanupClient_() {
|
||||
if (client_) {
|
||||
if (client_->isConnected()) {
|
||||
client_->disconnect();
|
||||
delay(100);
|
||||
}
|
||||
NimBLEDevice::deleteClient(client_);
|
||||
client_ = nullptr;
|
||||
}
|
||||
notifyChar_ = nullptr;
|
||||
writeChar_ = nullptr;
|
||||
}
|
||||
|
||||
bool ThermoproBLE::setupConnection_(NimBLEClient* client) {
|
||||
state_ = State::Discovering;
|
||||
stateStartMillis_ = millis();
|
||||
|
||||
@@ -34,6 +34,9 @@ public:
|
||||
// same core/loop in Arduino by default.
|
||||
ThermoproReading getReading() const { return reading_; }
|
||||
|
||||
// Human-readable current connection state name for diagnostics.
|
||||
const char* getStateName() const;
|
||||
|
||||
// Register a callback invoked whenever the reading state changes.
|
||||
void onStateChange(StateChangeCallback cb) { stateCallback_ = cb; }
|
||||
|
||||
@@ -72,9 +75,16 @@ private:
|
||||
unsigned long reconnectDelayMs_ = 5000;
|
||||
bool callbacksRegistered_ = false;
|
||||
|
||||
// Address discovered during a scan that we want to connect to.
|
||||
// The callback records it; update() initiates the connect after stopping the scan.
|
||||
NimBLEAddress pendingConnectAddress_;
|
||||
bool pendingConnectFound_ = false;
|
||||
unsigned long pendingConnectSeenMillis_ = 0;
|
||||
|
||||
bool scanAndConnect_();
|
||||
static bool connectToServer_(NimBLEAdvertisedDevice* device, void* userData);
|
||||
bool connectToAddress_(const NimBLEAddress& address);
|
||||
bool setupConnection_(NimBLEClient* client);
|
||||
void cleanupClient_();
|
||||
void setConnected_(bool connected);
|
||||
void startReconnectDelay_();
|
||||
|
||||
|
||||
@@ -9,8 +9,18 @@ Dashboard::Dashboard(CYD_Display& display, SettingsManager& settings, ThermoproB
|
||||
}
|
||||
|
||||
void Dashboard::begin() {
|
||||
display_.init();
|
||||
display_.initBacklight();
|
||||
Serial.println("Dashboard: initializing display with LGFX_AUTODETECT...");
|
||||
|
||||
if (!display_.init()) {
|
||||
Serial.println("ERROR: display init failed");
|
||||
} else {
|
||||
Serial.println("Dashboard: display init OK");
|
||||
}
|
||||
|
||||
// Backlight on GPIO 21, active HIGH.
|
||||
pinMode(GPIO_NUM_21, OUTPUT);
|
||||
digitalWrite(GPIO_NUM_21, HIGH);
|
||||
|
||||
display_.setRotation(0); // Portrait 240x320
|
||||
display_.fillScreen(TFT_BLACK);
|
||||
display_.setTextDatum(middle_center);
|
||||
|
||||
@@ -1,89 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
// LovyanGFX configuration for the ESP32-2432S028R "Cheap Yellow Display".
|
||||
// This board uses:
|
||||
// - ESP32-WROOM-32
|
||||
// - 320x240 TFT driven by ILI9341
|
||||
// - Resistive touch panel on XPT2046 (shared SPI bus with display)
|
||||
//
|
||||
// Pinout (ESP32-2432S028R variant):
|
||||
// SDO/MISO GPIO 19
|
||||
// LED GPIO 21 (backlight, active high)
|
||||
// SCK GPIO 18
|
||||
// SDI/MOSI GPIO 23
|
||||
// DC GPIO 2
|
||||
// RESET GPIO 4
|
||||
// CS GPIO 15
|
||||
// TOUCH CS GPIO 5
|
||||
// TOUCH IRQ GPIO - (not used by default)
|
||||
// We use LovyanGFX's built-in autodetection for the CYD board. The autodetect
|
||||
// table knows the correct SPI buses, pins, panel driver, and touch controller
|
||||
// for the common ILI9341 and ST7789 variants.
|
||||
//
|
||||
// Reference:
|
||||
// https://github.com/embedded-kiddie/Arduino-CYD-2432S028R
|
||||
// https://github.com/lovyan03/LovyanGFX/issues/693
|
||||
|
||||
#define LGFX_USE_V1
|
||||
#define LGFX_AUTODETECT
|
||||
#include <LovyanGFX.hpp>
|
||||
#include <LGFX_AUTODETECT.hpp>
|
||||
|
||||
namespace cyd {
|
||||
|
||||
class CYD_Display : public lgfx::LGFX_Device {
|
||||
public:
|
||||
CYD_Display() {
|
||||
// Panel configuration
|
||||
auto cfg = _panel_instance.config();
|
||||
cfg.pin_cs = GPIO_NUM_15;
|
||||
cfg.pin_rst = GPIO_NUM_4;
|
||||
cfg.bus_shared = true;
|
||||
cfg.panel_width = 240;
|
||||
cfg.panel_height = 320;
|
||||
cfg.offset_x = 0;
|
||||
cfg.offset_y = 0;
|
||||
cfg.offset_rotation = 0;
|
||||
cfg.dummy_read_pixel = 8;
|
||||
cfg.dummy_read_bits = 1;
|
||||
cfg.readable = true;
|
||||
cfg.invert = false;
|
||||
cfg.rgb_order = false;
|
||||
cfg.dlen_16bit = false;
|
||||
_panel_instance.config(cfg);
|
||||
|
||||
// SPI bus configuration
|
||||
auto bus_cfg = _bus_instance.config();
|
||||
bus_cfg.spi_host = HSPI_HOST;
|
||||
bus_cfg.spi_mode = 0;
|
||||
bus_cfg.freq_write = 40000000;
|
||||
bus_cfg.freq_read = 16000000;
|
||||
bus_cfg.pin_sclk = GPIO_NUM_18;
|
||||
bus_cfg.pin_mosi = GPIO_NUM_23;
|
||||
bus_cfg.pin_miso = GPIO_NUM_19;
|
||||
bus_cfg.pin_dc = GPIO_NUM_2;
|
||||
_bus_instance.config(bus_cfg);
|
||||
_panel_instance.setBus(&_bus_instance);
|
||||
|
||||
// Touch panel configuration (XPT2046)
|
||||
auto touch_cfg = _touch_instance.config();
|
||||
touch_cfg.x_min = 0;
|
||||
touch_cfg.x_max = 239;
|
||||
touch_cfg.y_min = 0;
|
||||
touch_cfg.y_max = 319;
|
||||
touch_cfg.pin_int = -1;
|
||||
touch_cfg.bus_shared = true;
|
||||
touch_cfg.offset_rotation = 0;
|
||||
touch_cfg.spi_host = HSPI_HOST;
|
||||
touch_cfg.freq = 1000000;
|
||||
touch_cfg.pin_sclk = GPIO_NUM_18;
|
||||
touch_cfg.pin_mosi = GPIO_NUM_23;
|
||||
touch_cfg.pin_miso = GPIO_NUM_19;
|
||||
touch_cfg.pin_cs = GPIO_NUM_5;
|
||||
_touch_instance.config(touch_cfg);
|
||||
_panel_instance.setTouch(&_touch_instance);
|
||||
}
|
||||
|
||||
void initBacklight() {
|
||||
pinMode(GPIO_NUM_21, OUTPUT);
|
||||
digitalWrite(GPIO_NUM_21, HIGH);
|
||||
}
|
||||
|
||||
private:
|
||||
lgfx::Bus_SPI _bus_instance;
|
||||
lgfx::Panel_ILI9341 _panel_instance;
|
||||
lgfx::Touch_XPT2046 _touch_instance;
|
||||
};
|
||||
// With LGFX_AUTODETECT we can simply use the stock LGFX class.
|
||||
using CYD_Display = lgfx::LGFX;
|
||||
|
||||
} // namespace cyd
|
||||
|
||||
+20
-4
@@ -35,6 +35,7 @@ enum class AppState {
|
||||
AppState appState = AppState::Init;
|
||||
unsigned long lastHeapLog = 0;
|
||||
unsigned long lastDashboardUpdate = 0;
|
||||
unsigned long lastBleStartAttempt = 0;
|
||||
|
||||
void requestReboot() {
|
||||
delay(500);
|
||||
@@ -102,11 +103,12 @@ void loop() {
|
||||
// Heartbeat logging
|
||||
if (millis() - lastHeapLog >= 10000) {
|
||||
lastHeapLog = millis();
|
||||
Serial.printf("[heartbeat] state=%d free_heap=%d wifi=%s ble=%s\n",
|
||||
Serial.printf("[heartbeat] state=%d free_heap=%d wifi=%s ble=%s/%s\n",
|
||||
static_cast<int>(appState),
|
||||
ESP.getFreeHeap(),
|
||||
wifi.isConnected() ? "connected" : "not-connected",
|
||||
bleClient.getReading().connected ? "connected" : "disconnected");
|
||||
bleClient.getReading().connected ? "connected" : "disconnected",
|
||||
bleClient.getStateName());
|
||||
}
|
||||
|
||||
switch (appState) {
|
||||
@@ -144,13 +146,27 @@ void loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Start BLE if not already trying.
|
||||
ThermoproReading reading = bleClient.getReading();
|
||||
if (!settings.hasThermoproMac()) {
|
||||
dashboard.showMessage("No MAC configured", "Open setup at", wifi.getIpAddress());
|
||||
break;
|
||||
}
|
||||
|
||||
// Proactively (re)start BLE connect if it is not doing anything.
|
||||
if (!bleClient.getReading().connected) {
|
||||
const char* state = bleClient.getStateName();
|
||||
if (strcmp(state, "idle") == 0 || strcmp(state, "reconnecting") == 0) {
|
||||
if (millis() - lastBleStartAttempt >= 5000) {
|
||||
lastBleStartAttempt = millis();
|
||||
Serial.printf("Main: requesting BLE connect to %s (state=%s)\n",
|
||||
settings.getThermoproMac().c_str(), state);
|
||||
bleClient.setAddress(settings.getThermoproMac());
|
||||
if (!bleClient.startConnect()) {
|
||||
Serial.println("Main: BLE startConnect returned false");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard refresh
|
||||
if (millis() - lastDashboardUpdate >= 1000) {
|
||||
lastDashboardUpdate = millis();
|
||||
|
||||
Reference in New Issue
Block a user