feat(ble): replace blocking scan with advertisement observation
The one-shot scan kept only the top six devices by RSSI and dropped the advertisement payload, so a distant beacon lost its slot to nearby phones and its data was unreachable -- wrong on every axis for reading sensors that broadcast in their adverts. Replace it with a continuous observer: observe(filter)/observed()/ unobserve()/isObserving(). BleObservation carries the raw advertisement bytes for Lua to parse, and BleFilter keeps only adverts matching a service-data UUID or manufacturer id.
This commit is contained in:
@@ -246,10 +246,24 @@ public:
|
||||
virtual Status forget() = 0;
|
||||
};
|
||||
|
||||
struct BleDevice {
|
||||
std::string name;
|
||||
// One sighting of an advertising device, coalesced to its latest advert. The
|
||||
// payload is the raw advertisement bytes; parsing the AD structures (and any
|
||||
// beacon format inside them) is the caller's job, so a new sensor format never
|
||||
// touches this seam.
|
||||
struct BleObservation {
|
||||
std::string address;
|
||||
std::string name;
|
||||
int32_t rssi;
|
||||
std::string payload;
|
||||
int32_t lastSeenMs;
|
||||
};
|
||||
|
||||
// Kept adverts must carry one of these service-data UUIDs or one of these
|
||||
// manufacturer ids; an empty filter keeps everything. Applied before an advert
|
||||
// is buffered, so unwanted devices never occupy a slot.
|
||||
struct BleFilter {
|
||||
std::vector<std::string> services;
|
||||
std::vector<int32_t> manufacturers;
|
||||
};
|
||||
|
||||
class BleProvider {
|
||||
@@ -258,7 +272,10 @@ public:
|
||||
virtual Status init(const std::string* name) = 0;
|
||||
virtual void deinit() = 0;
|
||||
virtual bool isInitialized() const = 0;
|
||||
virtual Status scan(int32_t durationMs, std::vector<BleDevice>& devices) = 0;
|
||||
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 Status connect(const std::string& address) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
virtual bool isConnected() const = 0;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// @lua-module ble BleLib
|
||||
// @lua-preamble ---@class BleDevice
|
||||
// @lua-preamble ---@field name string
|
||||
// @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
|
||||
// @lua-preamble ---@class BleFilter
|
||||
// @lua-preamble ---@field services? string[] Service-data UUIDs to keep.
|
||||
// @lua-preamble ---@field manufacturers? integer[] Manufacturer ids to keep.
|
||||
|
||||
#include "../helpers.h"
|
||||
|
||||
@@ -30,21 +36,67 @@ int isInitialized(lua_State* state) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int scan(lua_State* state) {
|
||||
const int32_t durationMs = optionalInt(state, 1, 3000);
|
||||
luaL_argcheck(state, durationMs > 0, 1, "must be positive");
|
||||
void readStringArray(lua_State* state, int index, const char* key,
|
||||
std::vector<std::string>& out) {
|
||||
lua_getfield(state, index, key);
|
||||
if (lua_istable(state, -1)) {
|
||||
const int count = static_cast<int>(lua_rawlen(state, -1));
|
||||
for (int at = 1; at <= count; at++) {
|
||||
lua_rawgeti(state, -1, at);
|
||||
if (lua_type(state, -1) == LUA_TSTRING)
|
||||
out.push_back(lua_tostring(state, -1));
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
|
||||
std::vector<BleDevice> devices;
|
||||
const Status status = Runtime::from(state)->ble().scan(durationMs, devices);
|
||||
if (!status.ok)
|
||||
return pushError(state, status.error);
|
||||
void readIntArray(lua_State* state, int index, const char* key,
|
||||
std::vector<int32_t>& out) {
|
||||
lua_getfield(state, index, key);
|
||||
if (lua_istable(state, -1)) {
|
||||
const int count = static_cast<int>(lua_rawlen(state, -1));
|
||||
for (int at = 1; at <= count; at++) {
|
||||
lua_rawgeti(state, -1, at);
|
||||
if (lua_type(state, -1) == LUA_TNUMBER)
|
||||
out.push_back(static_cast<int32_t>(lua_tointeger(state, -1)));
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
}
|
||||
|
||||
int observe(lua_State* state) {
|
||||
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));
|
||||
}
|
||||
|
||||
int unobserve(lua_State* state) {
|
||||
Runtime::from(state)->ble().unobserve();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int isObserving(lua_State* state) {
|
||||
lua_pushboolean(state, Runtime::from(state)->ble().isObserving());
|
||||
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, 3);
|
||||
setField(state, "name", devices[at].name);
|
||||
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;
|
||||
@@ -108,11 +160,19 @@ const luaL_Reg FUNCTIONS[] = {
|
||||
// --- Whether the BLE stack is running.
|
||||
// @return boolean
|
||||
{"isInitialized", isInitialized},
|
||||
// --- Scans for advertising devices.
|
||||
// @param durationMs integer|nil Defaults to 3000.
|
||||
// @return BleDevice[]|nil devices
|
||||
// --- Starts passively observing advertisements, coalesced per device.
|
||||
// @param filter BleFilter|nil Keep only matching adverts; nil keeps all.
|
||||
// @return true|nil ok
|
||||
// @return string|nil error
|
||||
{"scan", scan},
|
||||
{"observe", observe},
|
||||
// --- Stops observing and clears the snapshot.
|
||||
{"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
|
||||
|
||||
@@ -276,9 +276,10 @@ struct Wifi : WifiProvider {
|
||||
};
|
||||
|
||||
struct Ble : BleProvider {
|
||||
int32_t duration = 0;
|
||||
std::string value;
|
||||
bool initialized = false;
|
||||
bool observing = false;
|
||||
BleFilter filter;
|
||||
|
||||
Status init(const std::string*) override {
|
||||
initialized = true;
|
||||
@@ -286,12 +287,17 @@ struct Ble : BleProvider {
|
||||
}
|
||||
void deinit() override { initialized = false; }
|
||||
bool isInitialized() const override { return initialized; }
|
||||
Status scan(int32_t durationMs, std::vector<BleDevice>& devices) override {
|
||||
duration = durationMs;
|
||||
const BleDevice device = {"tag", "aa:bb", -60};
|
||||
devices.push_back(device);
|
||||
Status observe(const BleFilter& next) override {
|
||||
filter = next;
|
||||
observing = true;
|
||||
return Status::success();
|
||||
}
|
||||
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);
|
||||
}
|
||||
Status connect(const std::string&) override { return Status::success(); }
|
||||
void disconnect() override {}
|
||||
bool isConnected() const override { return true; }
|
||||
|
||||
@@ -121,10 +121,14 @@ int main() {
|
||||
run(state, "assert(not ble.isInitialized())\n"
|
||||
"assert(ble.init())\n"
|
||||
"assert(ble.isInitialized())\n"
|
||||
"assert(ble.scan()[1].address == 'aa:bb')\n"
|
||||
"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"
|
||||
"assert(#ble.read('svc', 'chr') == 3)\n"
|
||||
"assert(ble.write('svc', 'chr', 'x\\0y'))");
|
||||
assert(bench.ble.duration == 3000);
|
||||
assert(bench.ble.filter.services.size() == 1);
|
||||
assert(bench.ble.filter.services[0] == "181A");
|
||||
assert(bench.ble.value.size() == 3);
|
||||
|
||||
run(state, "fired = 0\n"
|
||||
|
||||
Reference in New Issue
Block a user