feat(ble)!: stream observed advertisements

This commit is contained in:
2026-08-08 13:58:19 -04:00
parent ddcdb52bb2
commit cf1831bf93
7 changed files with 83 additions and 50 deletions
+1 -1
View File
@@ -28,4 +28,4 @@ Firmware commits dirty display content and owns panel refresh policy. Binding an
marked generated. Callbacks are fields on the table an app returns, so a `@lua-app` block generates
a class an app composes (`---@class PaintApp : App, TouchHandlers`) rather than global functions. Each `@lua-module`/`@lua-augment` directive sits immediately above the
`luaL_Reg` table it describes, which is how one source declares several namespaces. This repository owns portable `lua/lib/` modules; shared UI owns theme application and persistence. Use `ble` for BLE/GATT
and reserve `bt` for a future Classic Bluetooth contract.
and reserve `bt` for a future Classic Bluetooth contract. `ble.observe(filter)` returns a reusable cursor iterator that retrieves one buffered advert at a time; runtime teardown ends the observation.
+4 -13
View File
@@ -2,12 +2,7 @@
-- Generated from native/src/bindings/core/ble.cpp. Do not edit.
---@class BleObservation
---@field address string
---@field name string
---@field rssi integer
---@field payload string Raw advertisement bytes.
---@field lastSeenMs integer sys.getMillis() at last sighting.
---@alias BleObservationIterator fun(state: nil, after: string|nil): address: string|nil, name: string|nil, rssi: integer|nil, payload: string|nil, lastSeenMs: integer|nil
---@class BleFilter
---@field services? string[] Service-data UUIDs to keep.
@@ -29,23 +24,19 @@ function ble.deinit() end
---@return boolean
function ble.isInitialized() end
---Starts passively observing advertisements, coalesced per device.
---Starts passively observing advertisements, coalesced per device. The returned iterator is reusable; observation stops with its Lua state.
---@param filter? BleFilter Keep only matching adverts; nil keeps all.
---@return true? ok
---@return BleObservationIterator? observations
---@return string? error
function ble.observe(filter) end
---Stops observing and clears the snapshot.
---Stops observing and clears the buffered devices.
function ble.unobserve() end
---Whether advertisement observation is running.
---@return boolean
function ble.isObserving() end
---The current snapshot of observed devices.
---@return BleObservation[] devices
function ble.observed() end
---Connects to a peripheral.
---@param address string
---@return true? ok
+2 -1
View File
@@ -275,7 +275,8 @@ public:
virtual Status observe(const BleFilter& filter) = 0;
virtual void unobserve() = 0;
virtual bool isObserving() const = 0;
virtual void observed(std::vector<BleObservation>& out) = 0;
virtual bool nextObservation(const std::string* after,
BleObservation& out) = 0;
virtual Status connect(const std::string& address) = 0;
virtual void disconnect() = 0;
virtual bool isConnected() const = 0;
+39 -29
View File
@@ -1,10 +1,7 @@
// @lua-module ble BleLib
// @lua-preamble ---@class BleObservation
// @lua-preamble ---@field address string
// @lua-preamble ---@field name string
// @lua-preamble ---@field rssi integer
// @lua-preamble ---@field payload string Raw advertisement bytes.
// @lua-preamble ---@field lastSeenMs integer sys.getMillis() at last sighting.
// @lua-preamble ---@alias BleObservationIterator fun(state: nil, after:
// string|nil): address: string|nil, name: string|nil, rssi: integer|nil,
// payload: string|nil, lastSeenMs: integer|nil
// @lua-preamble
// @lua-preamble ---@class BleFilter
// @lua-preamble ---@field services? string[] Service-data UUIDs to keep.
@@ -66,13 +63,45 @@ void readIntArray(lua_State* state, int index, const char* key,
lua_pop(state, 1);
}
int nextObservation(lua_State* state) {
BleProvider& provider = Runtime::from(state)->ble();
if (!provider.isObserving())
return 0;
std::string after;
const std::string* cursor = nullptr;
if (!lua_isnoneornil(state, 2)) {
after = checkString(state, 2);
cursor = &after;
}
BleObservation observation;
if (!provider.nextObservation(cursor, observation))
return 0;
pushString(state, observation.address);
pushString(state, observation.name);
lua_pushinteger(state, observation.rssi);
pushString(state, observation.payload);
lua_pushinteger(state, observation.lastSeenMs);
return 5;
}
int observe(lua_State* state) {
BleProvider& provider = Runtime::from(state)->ble();
if (provider.isObserving())
return pushError(state, "already observing");
BleFilter filter;
if (lua_istable(state, 1)) {
readStringArray(state, 1, "services", filter.services);
readIntArray(state, 1, "manufacturers", filter.manufacturers);
}
return pushStatus(state, Runtime::from(state)->ble().observe(filter));
const Status status = provider.observe(filter);
if (!status.ok)
return pushError(state, status.error);
lua_pushcfunction(state, nextObservation);
return 1;
}
int unobserve(lua_State* state) {
@@ -85,23 +114,6 @@ int isObserving(lua_State* state) {
return 1;
}
int observed(lua_State* state) {
std::vector<BleObservation> devices;
Runtime::from(state)->ble().observed(devices);
lua_createtable(state, static_cast<int>(devices.size()), 0);
for (size_t at = 0; at < devices.size(); at++) {
lua_createtable(state, 0, 5);
setField(state, "address", devices[at].address);
setField(state, "name", devices[at].name);
setField(state, "rssi", devices[at].rssi);
setField(state, "payload", devices[at].payload);
setField(state, "lastSeenMs", devices[at].lastSeenMs);
lua_rawseti(state, -2, static_cast<lua_Integer>(at + 1));
}
return 1;
}
int connect(lua_State* state) {
const std::string address = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->ble().connect(address));
@@ -161,18 +173,16 @@ const luaL_Reg FUNCTIONS[] = {
// @return boolean
{"isInitialized", isInitialized},
// --- Starts passively observing advertisements, coalesced per device.
// The returned iterator is reusable; observation stops with its Lua state.
// @param filter BleFilter|nil Keep only matching adverts; nil keeps all.
// @return true|nil ok
// @return BleObservationIterator|nil observations
// @return string|nil error
{"observe", observe},
// --- Stops observing and clears the snapshot.
// --- Stops observing and clears the buffered devices.
{"unobserve", unobserve},
// --- Whether advertisement observation is running.
// @return boolean
{"isObserving", isObserving},
// --- The current snapshot of observed devices.
// @return BleObservation[] devices
{"observed", observed},
// --- Connects to a peripheral.
// @param address string
// @return true|nil ok
+1
View File
@@ -105,6 +105,7 @@ void Runtime::close() {
if (!state_)
return;
cancelAllTimers();
providers_.ble->unobserve();
lua_close(state_);
state_ = nullptr;
mainRef_ = 0;
+10 -3
View File
@@ -294,9 +294,16 @@ struct Ble : BleProvider {
}
void unobserve() override { observing = false; }
bool isObserving() const override { return observing; }
void observed(std::vector<BleObservation>& out) override {
const BleObservation device = {"aa:bb", "tag", -60, "payload", 1234};
out.push_back(device);
bool nextObservation(const std::string* after, BleObservation& out) override {
if (!after) {
out = {"aa:bb", "tag", -60, "payload", 1234};
return true;
}
if (*after == "aa:bb") {
out = {"cc:dd", "tag 2", -70, "payload 2", 2345};
return true;
}
return false;
}
Status connect(const std::string&) override { return Status::success(); }
void disconnect() override {}
+26 -3
View File
@@ -121,10 +121,28 @@ int main() {
run(state, "assert(not ble.isInitialized())\n"
"assert(ble.init())\n"
"assert(ble.isInitialized())\n"
"assert(ble.observe({ services = { '181A' } }))\n"
"observations = assert(ble.observe({ services = { '181A' } }))\n"
"assert(ble.isObserving())\n"
"assert(ble.observed()[1].address == 'aa:bb')\n"
"assert(ble.observed()[1].payload == 'payload')\n"
"local function checkObservations()\n"
" local count = 0\n"
" for address, name, rssi, payload, lastSeenMs in observations do\n"
" count = count + 1\n"
" if address == 'aa:bb' then\n"
" assert(name == 'tag' and rssi == -60)\n"
" assert(payload == 'payload' and lastSeenMs == 1234)\n"
" else\n"
" assert(address == 'cc:dd' and name == 'tag 2' and rssi == -70)\n"
" assert(payload == 'payload 2' and lastSeenMs == 2345)\n"
" end\n"
" end\n"
" assert(count == 2)\n"
"end\n"
"checkObservations()\n"
"checkObservations()\n"
"local duplicate, err = ble.observe()\n"
"assert(duplicate == nil and err == 'already observing')\n"
"ble.unobserve()\n"
"assert(not ble.isObserving())\n"
"assert(#ble.read('svc', 'chr') == 3)\n"
"assert(ble.write('svc', 'chr', 'x\\0y'))");
assert(bench.ble.filter.services.size() == 1);
@@ -427,6 +445,11 @@ int main() {
assert(host.log.message.find("refusing to start") == 0);
}
run(state, "observations = assert(ble.observe())");
assert(bench.ble.observing);
runtime.close();
assert(!bench.ble.observing);
// Missing core providers leave no Lua state behind.
esp32lua::Providers incomplete = bench.providers();
incomplete.fs = nullptr;