From a2f3381c2ba711de86ee19f2a69e19ce8e208e5b Mon Sep 17 00:00:00 2001 From: Tim Hayward Date: Tue, 5 May 2026 19:11:17 -0600 Subject: [PATCH] Add optional BLE scan module for ESP32forth. Adds non-blocking BLE scanning words (ble-init, ble-scan-start, ble-scanning?, ble-scan-stop, ble-count, ble-addr, ble-rssi, ble-name, ble-new?, ble-forget) as both a direct-include header and a full optional module wired into the optionals.fs boot sequence. Co-Authored-By: Claude Sonnet 4.6 --- esp32/BUILD | 3 + esp32/ble-scan.h | 135 +++++++++++++++++++++++++++ esp32/builtins.h | 11 ++- esp32/optional/ble-scan/ble-scan.fs | 17 ++++ esp32/optional/ble-scan/ble-scan.h | 139 ++++++++++++++++++++++++++++ esp32/optionals.fs | 4 + esp32/options.h | 1 + esp32/print-builtins.cpp | 2 + esp32/sim_main.cpp | 1 + 9 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 esp32/ble-scan.h create mode 100644 esp32/optional/ble-scan/ble-scan.fs create mode 100644 esp32/optional/ble-scan/ble-scan.h diff --git a/esp32/BUILD b/esp32/BUILD index 46633ae..cdf79b4 100644 --- a/esp32/BUILD +++ b/esp32/BUILD @@ -74,6 +74,9 @@ ESP32_ZIP_FILES += [ [('spi-flash', '$src/esp32/optional/spi-flash/spi-flash.fs')], very_optional=True), Esp32Optional('espnow', '$src/esp32/optional/espnow/espnow.h', [('espnow', '$src/esp32/optional/espnow/espnow.fs')]), + Esp32Optional('ble-scan', '$src/esp32/optional/ble-scan/ble-scan.h', + [('ble-scan', '$src/esp32/optional/ble-scan/ble-scan.fs')], + very_optional=True), ] # Zip it. diff --git a/esp32/ble-scan.h b/esp32/ble-scan.h new file mode 100644 index 0000000..8db8650 --- /dev/null +++ b/esp32/ble-scan.h @@ -0,0 +1,135 @@ +// Copyright 2025 Bradley D. Nelson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + * ESP32forth BLE Scanner v{{VERSION}} + * Revision: {{REVISION}} + * + * Non-blocking BLE scan words for ESP32forth. + * + * Usage: + * ble-init ( -- ) initialize once at startup + * 5 ble-scan-start ( secs -- ) start scan, returns immediately + * ble-scanning? ( -- flag ) -1 while scanning, 0 when done + * ble-scan-stop ( -- ) stop scan early + * ble-count ( -- n ) number of devices found so far + * 0 ble-addr ( i -- addr ) null-terminated MAC string + * 0 ble-rssi ( i -- n ) RSSI in dBm + * 0 ble-name ( i -- addr ) null-terminated name (empty if none) + * 0 ble-new? ( i -- flag ) -1 if first time seen this boot + * ble-forget ( -- ) clear seen history + * + * Example — print all devices from a 5-second scan: + * : .ble-device ( i -- ) + * dup ble-addr type ." " dup ble-rssi . dup ble-new? if ." [NEW] " then + * ble-name dup if type else drop then cr ; + * : ble-scan + * 5 ble-scan-start + * begin ble-scanning? while 100 ms repeat + * ble-count 0 do i .ble-device loop ; + */ + +#include +#include +#include +#include +#include + +#define BLE_SCAN_MAX_RESULTS 64 + +struct BleScanResult { + char addr[18]; // "aa:bb:cc:dd:ee:ff\0" + char name[33]; // up to 32 chars + null + int rssi; + bool isNew; +}; + +static BLEScan* g_ble_scan = nullptr; +static BleScanResult g_ble_results[BLE_SCAN_MAX_RESULTS]; +static volatile int g_ble_count = 0; +static TaskHandle_t g_ble_task = nullptr; +static uint32_t g_ble_duration = 5; +static std::set g_ble_seen; + +class BleScanCallbacks : public BLEAdvertisedDeviceCallbacks { + void onResult(BLEAdvertisedDevice dev) override { + int i = g_ble_count; + if (i >= BLE_SCAN_MAX_RESULTS) return; + String addrStr = dev.getAddress().toString(); + strncpy(g_ble_results[i].addr, addrStr.c_str(), 17); + g_ble_results[i].addr[17] = '\0'; + if (dev.haveName()) { + strncpy(g_ble_results[i].name, dev.getName().c_str(), 32); + g_ble_results[i].name[32] = '\0'; + } else { + g_ble_results[i].name[0] = '\0'; + } + g_ble_results[i].rssi = dev.getRSSI(); + std::string addr(g_ble_results[i].addr); + g_ble_results[i].isNew = (g_ble_seen.find(addr) == g_ble_seen.end()); + if (g_ble_results[i].isNew) g_ble_seen.insert(addr); + g_ble_count = i + 1; // write count last so readers see complete record + } +}; + +static BleScanCallbacks* g_ble_callbacks = nullptr; + +static void bleScanTask(void* param) { + g_ble_scan->start(g_ble_duration, false); + g_ble_task = nullptr; + vTaskDelete(nullptr); +} + +#define OPTIONAL_BLE_SCAN_VOCABULARY V(ble) + +#define OPTIONAL_BLE_SCAN_SUPPORT \ + XV(ble, "ble-init", ble_init, { \ + BLEDevice::init(""); \ + g_ble_scan = BLEDevice::getScan(); \ + g_ble_callbacks = new BleScanCallbacks(); \ + g_ble_scan->setAdvertisedDeviceCallbacks(g_ble_callbacks, false); \ + g_ble_scan->setActiveScan(true); \ + g_ble_scan->setInterval(100); \ + g_ble_scan->setWindow(99); \ + }) \ + XV(ble, "ble-scan-start", ble_scan_start, { \ + g_ble_duration = (uint32_t) n0; DROP; \ + if (g_ble_task) { g_ble_scan->stop(); vTaskDelay(10); } \ + g_ble_count = 0; \ + g_ble_scan->clearResults(); \ + xTaskCreate(bleScanTask, "blescan", 4096, nullptr, 1, &g_ble_task); \ + }) \ + XV(ble, "ble-scanning?", ble_scanning, PUSH(g_ble_task != nullptr ? -1 : 0)) \ + XV(ble, "ble-scan-stop", ble_scan_stop, { \ + g_ble_scan->stop(); \ + vTaskDelay(pdMS_TO_TICKS(50)); \ + }) \ + XV(ble, "ble-count", ble_count, PUSH(g_ble_count)) \ + XV(ble, "ble-addr", ble_addr, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count) ? (cell_t) g_ble_results[i].addr : (cell_t) ""; \ + }) \ + XV(ble, "ble-rssi", ble_rssi, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count) ? g_ble_results[i].rssi : 0; \ + }) \ + XV(ble, "ble-name", ble_name, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count) ? (cell_t) g_ble_results[i].name : (cell_t) ""; \ + }) \ + XV(ble, "ble-new?", ble_new, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count && g_ble_results[i].isNew) ? -1 : 0; \ + }) \ + XV(ble, "ble-forget", ble_forget, g_ble_seen.clear()) diff --git a/esp32/builtins.h b/esp32/builtins.h index eb453c7..ba99fc0 100644 --- a/esp32/builtins.h +++ b/esp32/builtins.h @@ -100,6 +100,14 @@ # define OPTIONAL_ESPNOW_SUPPORT # endif +// Hook to pull in optional BLE scan support. +# if __has_include("ble-scan.h") +# include "ble-scan.h" +# else +# define OPTIONAL_BLE_SCAN_VOCABULARY +# define OPTIONAL_BLE_SCAN_SUPPORT +# endif + static cell_t ResizeFile(cell_t fd, cell_t size); #endif @@ -138,7 +146,8 @@ static cell_t ResizeFile(cell_t fd, cell_t size); OPTIONAL_SERIAL_BLUETOOTH_SUPPORT \ OPTIONAL_SPI_FLASH_SUPPORT \ OPTIONAL_HTTP_CLIENT_SUPPORT \ - OPTIONAL_ESPNOW_SUPPORT + OPTIONAL_ESPNOW_SUPPORT \ + OPTIONAL_BLE_SCAN_SUPPORT #define REQUIRED_MEMORY_SUPPORT \ YV(internals, MALLOC, SET malloc(n0)) \ diff --git a/esp32/optional/ble-scan/ble-scan.fs b/esp32/optional/ble-scan/ble-scan.fs new file mode 100644 index 0000000..4854e66 --- /dev/null +++ b/esp32/optional/ble-scan/ble-scan.fs @@ -0,0 +1,17 @@ +\ Copyright 2025 Bradley D. Nelson +\ +\ Licensed under the Apache License, Version 2.0 (the "License"); +\ you may not use this file except in compliance with the License. +\ You may obtain a copy of the License at +\ +\ http://www.apache.org/licenses/LICENSE-2.0 +\ +\ Unless required by applicable law or agreed to in writing, software +\ distributed under the License is distributed on an "AS IS" BASIS, +\ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +\ See the License for the specific language governing permissions and +\ limitations under the License. + +vocabulary ble ble definitions +transfer ble-builtins +forth definitions diff --git a/esp32/optional/ble-scan/ble-scan.h b/esp32/optional/ble-scan/ble-scan.h new file mode 100644 index 0000000..088bee0 --- /dev/null +++ b/esp32/optional/ble-scan/ble-scan.h @@ -0,0 +1,139 @@ +// Copyright 2025 Bradley D. Nelson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + * ESP32forth BLE Scanner v{{VERSION}} + * Revision: {{REVISION}} + * + * Non-blocking BLE scan words for ESP32forth. + * + * Usage: + * ble-init ( -- ) initialize once at startup + * 5 ble-scan-start ( secs -- ) start scan, returns immediately + * ble-scanning? ( -- flag ) -1 while scanning, 0 when done + * ble-scan-stop ( -- ) stop scan early + * ble-count ( -- n ) number of devices found so far + * 0 ble-addr ( i -- addr ) null-terminated MAC string + * 0 ble-rssi ( i -- n ) RSSI in dBm + * 0 ble-name ( i -- addr ) null-terminated name (empty if none) + * 0 ble-new? ( i -- flag ) -1 if first time seen this boot + * ble-forget ( -- ) clear seen history + * + * Example — print all devices from a 5-second scan: + * : .ble-device ( i -- ) + * dup ble-addr type ." " dup ble-rssi . dup ble-new? if ." [NEW] " then + * ble-name dup if type else drop then cr ; + * : ble-scan + * 5 ble-scan-start + * begin ble-scanning? while 100 ms repeat + * ble-count 0 do i .ble-device loop ; + */ + +#include +#include +#include +#include +#include + +#define BLE_SCAN_MAX_RESULTS 64 + +struct BleScanResult { + char addr[18]; // "aa:bb:cc:dd:ee:ff\0" + char name[33]; // up to 32 chars + null + int rssi; + bool isNew; +}; + +static BLEScan* g_ble_scan = nullptr; +static BleScanResult g_ble_results[BLE_SCAN_MAX_RESULTS]; +static volatile int g_ble_count = 0; +static TaskHandle_t g_ble_task = nullptr; +static uint32_t g_ble_duration = 5; +static std::set g_ble_seen; + +class BleScanCallbacks : public BLEAdvertisedDeviceCallbacks { + void onResult(BLEAdvertisedDevice dev) override { + int i = g_ble_count; + if (i >= BLE_SCAN_MAX_RESULTS) return; + String addrStr = dev.getAddress().toString(); + strncpy(g_ble_results[i].addr, addrStr.c_str(), 17); + g_ble_results[i].addr[17] = '\0'; + if (dev.haveName()) { + strncpy(g_ble_results[i].name, dev.getName().c_str(), 32); + g_ble_results[i].name[32] = '\0'; + } else { + g_ble_results[i].name[0] = '\0'; + } + g_ble_results[i].rssi = dev.getRSSI(); + std::string addr(g_ble_results[i].addr); + g_ble_results[i].isNew = (g_ble_seen.find(addr) == g_ble_seen.end()); + if (g_ble_results[i].isNew) g_ble_seen.insert(addr); + g_ble_count = i + 1; // write count last so readers see complete record + } +}; + +static BleScanCallbacks* g_ble_callbacks = nullptr; + +static void bleScanTask(void* param) { + g_ble_scan->start(g_ble_duration, false); + g_ble_task = nullptr; + vTaskDelete(nullptr); +} + +#define OPTIONAL_BLE_SCAN_VOCABULARY V(ble) + +#define OPTIONAL_BLE_SCAN_SUPPORT \ + XV(internals, "ble-scan-source", BLE_SCAN_SOURCE, \ + PUSH ble_scan_source; PUSH sizeof(ble_scan_source) - 1) \ + XV(ble, "ble-init", ble_init, { \ + BLEDevice::init(""); \ + g_ble_scan = BLEDevice::getScan(); \ + g_ble_callbacks = new BleScanCallbacks(); \ + g_ble_scan->setAdvertisedDeviceCallbacks(g_ble_callbacks, false); \ + g_ble_scan->setActiveScan(true); \ + g_ble_scan->setInterval(100); \ + g_ble_scan->setWindow(99); \ + }) \ + XV(ble, "ble-scan-start", ble_scan_start, { \ + g_ble_duration = (uint32_t) n0; DROP; \ + if (g_ble_task) { g_ble_scan->stop(); vTaskDelay(10); } \ + g_ble_count = 0; \ + g_ble_scan->clearResults(); \ + xTaskCreate(bleScanTask, "blescan", 4096, nullptr, 1, &g_ble_task); \ + }) \ + XV(ble, "ble-scanning?", ble_scanning, PUSH(g_ble_task != nullptr ? -1 : 0)) \ + XV(ble, "ble-scan-stop", ble_scan_stop, { \ + g_ble_scan->stop(); \ + vTaskDelay(pdMS_TO_TICKS(50)); \ + }) \ + XV(ble, "ble-count", ble_count, PUSH(g_ble_count)) \ + XV(ble, "ble-addr", ble_addr, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count) ? (cell_t) g_ble_results[i].addr : (cell_t) ""; \ + }) \ + XV(ble, "ble-rssi", ble_rssi, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count) ? g_ble_results[i].rssi : 0; \ + }) \ + XV(ble, "ble-name", ble_name, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count) ? (cell_t) g_ble_results[i].name : (cell_t) ""; \ + }) \ + XV(ble, "ble-new?", ble_new, { \ + int i = n0; \ + n0 = (i >= 0 && i < g_ble_count && g_ble_results[i].isNew) ? -1 : 0; \ + }) \ + XV(ble, "ble-forget", ble_forget, g_ble_seen.clear()) + +#include "gen/esp32_ble-scan.h" diff --git a/esp32/optionals.fs b/esp32/optionals.fs index a19dbc1..226e9a5 100644 --- a/esp32/optionals.fs +++ b/esp32/optionals.fs @@ -57,3 +57,7 @@ internals DEFINED? HTTPClient-builtins [IF] internals DEFINED? espnow-source [IF] espnow-source evaluate [THEN] forth + +internals DEFINED? ble-scan-source [IF] + ble-scan-source evaluate +[THEN] forth diff --git a/esp32/options.h b/esp32/options.h index 24b54f7..75173bb 100644 --- a/esp32/options.h +++ b/esp32/options.h @@ -103,4 +103,5 @@ OPTIONAL_SPI_FLASH_VOCABULARY \ OPTIONAL_HTTP_CLIENT_VOCABULARY \ OPTIONAL_ESPNOW_VOCABULARY \ + OPTIONAL_BLE_SCAN_VOCABULARY \ USER_VOCABULARIES diff --git a/esp32/print-builtins.cpp b/esp32/print-builtins.cpp index adfd6ce..ece5fdb 100644 --- a/esp32/print-builtins.cpp +++ b/esp32/print-builtins.cpp @@ -33,6 +33,7 @@ #define OPTIONAL_SPI_FLASH_SUPPORT #define OPTIONAL_HTTP_CLIENT_SUPPORT #define OPTIONAL_ESPNOW_SUPPORT +#define OPTIONAL_BLE_SCAN_SUPPORT #define OPTIONAL_BLUETOOTH_VOCABULARY #define OPTIONAL_CAMERA_VOCABULARY @@ -42,6 +43,7 @@ #define OPTIONAL_SPI_FLASH_VOCABULARY #define OPTIONAL_HTTP_CLIENT_VOCABULARY #define OPTIONAL_ESPNOW_VOCABULARY +#define OPTIONAL_BLE_SCAN_VOCABULARY #include "builtins.h" diff --git a/esp32/sim_main.cpp b/esp32/sim_main.cpp index 0f4cfa9..0d57149 100644 --- a/esp32/sim_main.cpp +++ b/esp32/sim_main.cpp @@ -30,6 +30,7 @@ #define OPTIONAL_SPI_FLASH_VOCABULARY #define OPTIONAL_HTTP_CLIENT_VOCABULARY #define OPTIONAL_ESPNOW_VOCABULARY +#define OPTIONAL_BLE_SCAN_VOCABULARY static cell_t *simulated(cell_t *sp, const char *op);