Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions esp32/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
135 changes: 135 additions & 0 deletions esp32/ble-scan.h
Original file line number Diff line number Diff line change
@@ -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 <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <set>
#include <string>

#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<std::string> 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())
11 changes: 10 additions & 1 deletion esp32/builtins.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) \
Expand Down
17 changes: 17 additions & 0 deletions esp32/optional/ble-scan/ble-scan.fs
Original file line number Diff line number Diff line change
@@ -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
139 changes: 139 additions & 0 deletions esp32/optional/ble-scan/ble-scan.h
Original file line number Diff line number Diff line change
@@ -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 <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <set>
#include <string>

#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<std::string> 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"
4 changes: 4 additions & 0 deletions esp32/optionals.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions esp32/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,5 @@
OPTIONAL_SPI_FLASH_VOCABULARY \
OPTIONAL_HTTP_CLIENT_VOCABULARY \
OPTIONAL_ESPNOW_VOCABULARY \
OPTIONAL_BLE_SCAN_VOCABULARY \
USER_VOCABULARIES
2 changes: 2 additions & 0 deletions esp32/print-builtins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"

Expand Down
1 change: 1 addition & 0 deletions esp32/sim_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down