mirror of
https://github.com/pelgraine/Meck.git
synced 2026-10-30 19:52:01 +01:00
Garmin: sensor-menu pairing, no PHY update on that link, fast interval for phones
This commit is contained in:
1 parent
8417c32f3c
commit
4b9ba3a65d
3 files changed
+116
-42
No files matched your search
@@ -11,6 +11,25 @@
|
||||
|
||||
#define ADVERT_RESTART_DELAY 1000 // millis
|
||||
|
||||
#ifdef MECK_BLE_SENSOR_ADVERT
|
||||
// Experiment (2026-10-26): make the node look like a Bluetooth heart-rate
|
||||
// sensor as well as a MeshCore node, so a Garmin watch's Sensors & Accessories
|
||||
// search lists it and the watch firmware runs its own pairing, which can ask
|
||||
// for a PIN (Connect IQ apps cannot). The Heart Rate characteristics carry the
|
||||
// same MITM permissions as UART, so the watch has to pair before it can
|
||||
// subscribe. No heart-rate data is ever sent.
|
||||
//
|
||||
// Advertising packet (31 bytes max): flags, 16-bit Heart Rate service UUID,
|
||||
// appearance "Heart Rate Belt", and the device name (shortened to fit).
|
||||
// Scan response: the 128-bit UART service UUID, so phone apps that filter on
|
||||
// it still find the node.
|
||||
#define SENSOR_SVC_UUID 0x180D // Heart Rate
|
||||
#define SENSOR_CHR_HRM_UUID 0x2A37 // Heart Rate Measurement (notify)
|
||||
#define SENSOR_CHR_LOC_UUID 0x2A38 // Body Sensor Location (read)
|
||||
#define SENSOR_APPEARANCE 0x0341 // Heart Rate Belt
|
||||
#define ADV_NAME_MAX_BYTES 18 // 31 - flags(3) - 16-bit svc(4) - appearance(4) - name header(2)
|
||||
#endif
|
||||
|
||||
// BLE transmit power for connections, advertising and scanning: +9 dBm unless
|
||||
// a build sets another esp_power_level_t level with a flag, for example
|
||||
// -D MECK_BLE_TX_POWER=ESP_PWR_LVL_P15
|
||||
@@ -107,7 +126,52 @@ void SerialBLEInterface::_realBegin() {
|
||||
#endif
|
||||
pRxCharacteristic->setCallbacks(this);
|
||||
|
||||
#ifdef MECK_BLE_SENSOR_ADVERT
|
||||
// Heart Rate service: notify-only measurement plus body sensor location,
|
||||
// with the same link requirement as the UART characteristics (encrypted
|
||||
// under MECK_BLE_JUST_WORKS, authenticated/MITM otherwise).
|
||||
#ifdef MECK_BLE_JUST_WORKS
|
||||
const esp_gatt_perm_t sensorRead = ESP_GATT_PERM_READ_ENCRYPTED;
|
||||
const esp_gatt_perm_t sensorWrite = ESP_GATT_PERM_WRITE_ENCRYPTED;
|
||||
#else
|
||||
const esp_gatt_perm_t sensorRead = ESP_GATT_PERM_READ_ENC_MITM;
|
||||
const esp_gatt_perm_t sensorWrite = ESP_GATT_PERM_WRITE_ENC_MITM;
|
||||
#endif
|
||||
pSensorService = pServer->createService(BLEUUID((uint16_t)SENSOR_SVC_UUID));
|
||||
BLECharacteristic* pHrm = pSensorService->createCharacteristic(BLEUUID((uint16_t)SENSOR_CHR_HRM_UUID), BLECharacteristic::PROPERTY_NOTIFY);
|
||||
pHrm->setAccessPermissions(sensorRead);
|
||||
BLE2902* pHrmCccd = new BLE2902();
|
||||
pHrmCccd->setAccessPermissions(sensorRead | sensorWrite);
|
||||
pHrm->addDescriptor(pHrmCccd);
|
||||
uint8_t hrmInit[2] = { 0x00, 0x00 }; // flags byte + 8-bit value 0
|
||||
pHrm->setValue(hrmInit, 2);
|
||||
BLECharacteristic* pLoc = pSensorService->createCharacteristic(BLEUUID((uint16_t)SENSOR_CHR_LOC_UUID), BLECharacteristic::PROPERTY_READ);
|
||||
pLoc->setAccessPermissions(sensorRead);
|
||||
uint8_t locChest = 1;
|
||||
pLoc->setValue(&locChest, 1);
|
||||
|
||||
// Advertising data: flags, Heart Rate UUID, appearance, name (shortened to
|
||||
// ADV_NAME_MAX_BYTES at a UTF-8 character boundary if needed).
|
||||
BLEAdvertisementData adv;
|
||||
adv.setFlags(ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
|
||||
adv.setCompleteServices(BLEUUID((uint16_t)SENSOR_SVC_UUID));
|
||||
adv.setAppearance(SENSOR_APPEARANCE);
|
||||
size_t nameLen = strlen(_dev_name);
|
||||
if (nameLen <= ADV_NAME_MAX_BYTES) {
|
||||
adv.setName(std::string(_dev_name, nameLen));
|
||||
} else {
|
||||
size_t cut = ADV_NAME_MAX_BYTES;
|
||||
while (cut > 0 && (((uint8_t)_dev_name[cut]) & 0xC0) == 0x80) cut--; // do not split a UTF-8 sequence
|
||||
adv.setShortName(std::string(_dev_name, cut));
|
||||
}
|
||||
BLEAdvertisementData rsp;
|
||||
rsp.setCompleteServices(BLEUUID(SERVICE_UUID));
|
||||
pServer->getAdvertising()->setAdvertisementData(adv);
|
||||
pServer->getAdvertising()->setScanResponseData(rsp);
|
||||
BLE_DEBUG_PRINTLN("sensor advert: HR service + UART in scan response, name bytes=%u", (unsigned)nameLen);
|
||||
#else
|
||||
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
|
||||
#endif
|
||||
}
|
||||
|
||||
// -------- BLESecurityCallbacks methods
|
||||
@@ -166,6 +230,13 @@ void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
|
||||
BLE_DEBUG_PRINTLN(" - Requested fast connection interval (15-20ms)");
|
||||
#endif
|
||||
|
||||
#ifdef MECK_BLE_NO_PHY_UPDATE
|
||||
// A Garmin watch paired through its sensor menu answers the 2M PHY request
|
||||
// in a way the ESP32-S3 controller does not survive (assert in
|
||||
// lld_con.c during the PHY update procedure, 2026-10-26). The watch keeps
|
||||
// a 23-byte MTU anyway, so neither request gains anything: skip both.
|
||||
BLE_DEBUG_PRINTLN(" - PHY/DLE requests skipped (MECK_BLE_NO_PHY_UPDATE)");
|
||||
#else
|
||||
// Request 2M PHY for doubled air data rate (BLE 5.0, supported on ESP32-S3)
|
||||
// Note: ESP-IDF misspells "preferred" as "prefered" in their API
|
||||
esp_ble_gap_set_prefered_phy(_remote_bda,
|
||||
@@ -175,6 +246,7 @@ void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
|
||||
ESP_BLE_GAP_PHY_OPTIONS_NO_PREF);
|
||||
esp_ble_gap_set_pkt_data_len(_remote_bda, 251); // Request DLE (max link-layer PDU)
|
||||
BLE_DEBUG_PRINTLN(" - Requested 2M PHY and DLE (251 bytes)");
|
||||
#endif
|
||||
} else {
|
||||
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
|
||||
_auth_ok = false;
|
||||
@@ -197,7 +269,25 @@ void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
|
||||
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id));
|
||||
uint16_t mtu = pServer->getPeerMTU(param->mtu.conn_id);
|
||||
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", mtu);
|
||||
#ifdef MECK_BLE_SLOW_INTERVAL
|
||||
// The relaxed interval requested at authentication suits a watch, which
|
||||
// never raises its MTU above 23. A peer that negotiates a larger MTU is a
|
||||
// phone about to sync contacts; at 100-200 ms the notify buffer overflows
|
||||
// and contact frames are dropped (2026-10-26), so give phones the fast
|
||||
// interval they have always had.
|
||||
if (mtu > 23) {
|
||||
esp_ble_conn_update_params_t conn_params;
|
||||
memcpy(conn_params.bda, _remote_bda, 6);
|
||||
conn_params.min_int = 12; // 15ms (12 x 1.25ms)
|
||||
conn_params.max_int = 16; // 20ms (16 x 1.25ms)
|
||||
conn_params.latency = 0; // no skipped intervals
|
||||
conn_params.timeout = 400; // 4 seconds supervision timeout
|
||||
esp_ble_gap_update_conn_params(&conn_params);
|
||||
BLE_DEBUG_PRINTLN(" - Large-MTU peer: requested fast connection interval (15-20ms)");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
|
||||
@@ -267,6 +357,9 @@ void SerialBLEInterface::enable() {
|
||||
|
||||
// Start the service
|
||||
pService->start();
|
||||
#ifdef MECK_BLE_SENSOR_ADVERT
|
||||
pSensorService->start();
|
||||
#endif
|
||||
|
||||
// Start advertising
|
||||
|
||||
@@ -289,6 +382,9 @@ void SerialBLEInterface::disable() {
|
||||
pServer->getAdvertising()->stop();
|
||||
pServer->disconnect(last_conn_id);
|
||||
pService->stop();
|
||||
#ifdef MECK_BLE_SENSOR_ADVERT
|
||||
pSensorService->stop();
|
||||
#endif
|
||||
}
|
||||
oldDeviceConnected = deviceConnected = false;
|
||||
adv_restart_time = 0;
|
||||
|
||||
@@ -12,6 +12,12 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
|
||||
BLEServer *pServer;
|
||||
BLEService *pService;
|
||||
BLECharacteristic * pTxCharacteristic;
|
||||
#ifdef MECK_BLE_SENSOR_ADVERT
|
||||
// Experiment: a Heart Rate service alongside UART so a Garmin watch lists
|
||||
// the node under Sensors & Accessories and runs its own (PIN) pairing. See
|
||||
// the .cpp for the advertising layout.
|
||||
BLEService *pSensorService;
|
||||
#endif
|
||||
bool deviceConnected;
|
||||
bool oldDeviceConnected;
|
||||
bool _isEnabled;
|
||||
@@ -74,6 +80,9 @@ public:
|
||||
SerialBLEInterface() {
|
||||
pServer = NULL;
|
||||
pService = NULL;
|
||||
#ifdef MECK_BLE_SENSOR_ADVERT
|
||||
pSensorService = NULL;
|
||||
#endif
|
||||
deviceConnected = false;
|
||||
oldDeviceConnected = false;
|
||||
adv_restart_time = 0;
|
||||
|
||||
@@ -192,10 +192,15 @@ lib_deps =
|
||||
bitbank2/JPEGDEC
|
||||
bitbank2/PNGdec
|
||||
|
||||
; MAX + BLE companion, Garmin test build. Identical to meck_max_ble above
|
||||
; except the five BLE flags are switched on instead of commented out:
|
||||
; MAX + BLE companion, Garmin watch build. meck_max_ble plus the watch flags:
|
||||
; debug logging, just-works pairing, small-MTU frame slicing, the watch
|
||||
; protocol extensions, and the slow connection interval.
|
||||
; protocol extensions, the slow connection interval (watch only; phones get
|
||||
; the fast interval back when they raise the MTU), MECK_BLE_SENSOR_ADVERT (a
|
||||
; Heart Rate service and sensor-style advertising, so the watch pairs with
|
||||
; the node through its own Sensors & Accessories menu and the bond then
|
||||
; serves the Meck watch app; switch the sensor entry off afterwards) and
|
||||
; MECK_BLE_NO_PHY_UPDATE (the 2M PHY request crashed the controller on the
|
||||
; sensor link). Verified on the fenix 7S 2026-10-26.
|
||||
[env:meck_max_ble_garmin]
|
||||
extends = LilyGo_TDeck_Pro_Max
|
||||
build_flags =
|
||||
@@ -213,6 +218,8 @@ build_flags =
|
||||
-D MECK_BLE_SMALL_MTU_SPLIT=1
|
||||
-D MECK_WATCH_EXT=1
|
||||
-D MECK_BLE_SLOW_INTERVAL=1
|
||||
-D MECK_BLE_SENSOR_ADVERT=1
|
||||
-D MECK_BLE_NO_PHY_UPDATE=1
|
||||
-D FIRMWARE_VERSION='"Meck v1.15.1.MAX"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
@@ -265,44 +272,6 @@ lib_deps =
|
||||
bitbank2/PNGdec
|
||||
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Fish build -- meck_max_ble plus the LoRaGotchi DX fish (MECK_LORAFISH,
|
||||
; ui-new/LoRaFish.cpp) in LoRaGotchi's colour mode with the watch's tank
|
||||
; scenery, each colour drawn as one of the home tile borders' grey patterns:
|
||||
; an aquarium home page after the world clock, and LoRaGotchi's menus (stats,
|
||||
; DX visits, options) under Games > LoRaGotchi. The aquarium page redraws back-to-back
|
||||
; (no 800 ms floor) with a full refresh every 5 minutes while it stays on
|
||||
; screen. The fish uses LoRaGotchi's own save files (/lorafish.dat,
|
||||
; /lorafish.cfg, NVS "lfgdx").
|
||||
; Build: pio run -e meck_max_ble_fish
|
||||
; ---------------------------------------------------------------------------
|
||||
[env:meck_max_ble_fish]
|
||||
extends = LilyGo_TDeck_Pro_Max
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro_Max.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=2000
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D MECK_BLE_TX_POWER=ESP_PWR_LVL_P15
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D MECK_WEB_READER=1
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_OTA_UPDATE=1
|
||||
-D MECK_LORAFISH
|
||||
-D FIRMWARE_VERSION='"Meck v1.15.1.MAX.FISH"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro_Max.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
bitbank2/JPEGDEC
|
||||
bitbank2/PNGdec
|
||||
|
||||
; MAX + WiFi companion (WiFi app bridging — no BLE, higher contact limit)
|
||||
; WiFi credentials loaded from SD card (/web/wifi.cfg).
|
||||
; Connect via MeshCore web app, meshcore.js, or Python CLI.
|
||||
|
||||
Reference in new issue
Block a user