#include "providers.h" #include #include #include #include "../heaplog.h" #include #include #include namespace slate { namespace { using esp32lua::Status; // A distant temp sensor must not lose its slot to a nearer phone, so the map is // bounded by staleness, not RSSI: when full, the least-recently-seen entry is // evicted. Filtering at ingestion keeps unwanted devices out entirely. constexpr size_t MAX_OBSERVED = 32; struct Observation { std::string name; std::string payload; int32_t rssi; int32_t lastSeenMs; uint8_t addressType; }; // onResult fires on the NimBLE host task while observed()/connect() run on the // Lua task, so every touch of the map takes the lock. No Lua is called from // here -- the tree samples the snapshot on its own cadence instead. class Observer : public NimBLEScanCallbacks { public: void setFilter(const esp32lua::BleFilter& next) { std::lock_guard guard(mutex); filter = next; devices.clear(); } void clear() { std::lock_guard guard(mutex); devices.clear(); } void snapshot(std::vector& out) { std::lock_guard guard(mutex); out.clear(); out.reserve(devices.size()); for (const auto& entry : devices) out.push_back({entry.first, entry.second.name, entry.second.rssi, entry.second.payload, entry.second.lastSeenMs}); } bool addressType(const std::string& address, uint8_t& type) { std::lock_guard guard(mutex); const auto found = devices.find(address); if (found == devices.end()) return false; type = found->second.addressType; return true; } void onResult(const NimBLEAdvertisedDevice* device) override { if (!matches(device)) return; Observation entry; entry.name = device->haveName() ? device->getName() : ""; const std::vector& payload = device->getPayload(); entry.payload.assign(payload.begin(), payload.end()); entry.rssi = device->getRSSI(); entry.lastSeenMs = static_cast(millis()); entry.addressType = device->getAddressType(); std::lock_guard guard(mutex); const std::string address = device->getAddress().toString(); if (devices.find(address) == devices.end() && devices.size() >= MAX_OBSERVED) evictStalest(); devices[address] = entry; } private: bool matches(const NimBLEAdvertisedDevice* device) const { if (filter.services.empty() && filter.manufacturers.empty()) return true; for (const std::string& uuid : filter.services) if (!device->getServiceData(NimBLEUUID(uuid.c_str())).empty()) return true; if (!filter.manufacturers.empty() && device->haveManufacturerData()) { const std::string mfg = device->getManufacturerData(); if (mfg.size() >= 2) { const int32_t id = static_cast(mfg[0]) | (static_cast(mfg[1]) << 8); for (int32_t want : filter.manufacturers) if (want == id) return true; } } return false; } void evictStalest() { auto stalest = devices.begin(); for (auto it = devices.begin(); it != devices.end(); ++it) if (it->second.lastSeenMs < stalest->second.lastSeenMs) stalest = it; devices.erase(stalest); } std::mutex mutex; std::map devices; esp32lua::BleFilter filter; }; Observer observer; bool validAddress(const std::string& address) { if (address.length() != 17) return false; for (size_t at = 0; at < address.length(); at++) { if (at % 3 == 2) { if (address[at] != ':') return false; } else if (!((address[at] >= '0' && address[at] <= '9') || (address[at] >= 'a' && address[at] <= 'f') || (address[at] >= 'A' && address[at] <= 'F'))) { return false; } } return true; } } // namespace Status Ble::init(const std::string* name) { if (isInitialized()) return Status::success(); WiFi.mode(WIFI_OFF); logHeap("pre-ble"); if (!NimBLEDevice::init(name ? *name : "Slate32")) return Status::failure("cannot initialize BLE"); logHeap("ble-on"); return Status::success(); } void Ble::deinit() { if (!isInitialized()) return; disconnect(); stopAdvertising(); unobserve(); NimBLEDevice::deinit(true); logHeap("ble-off"); } bool Ble::isInitialized() const { return NimBLEDevice::isInitialized(); } void bleShutdown() { if (!NimBLEDevice::isInitialized()) return; NimBLEDevice::deinit(true); Serial.println("[ble] released for wifi"); } Status Ble::observe(const esp32lua::BleFilter& filter) { if (!isInitialized()) return Status::failure("BLE is not initialized"); observer.setFilter(filter); NimBLEScan* scan = NimBLEDevice::getScan(); scan->setActiveScan(false); scan->setMaxResults(0); // wantDuplicates: a beacon re-advertises its latest reading; drop the filter // and we would see each sensor once and never update its value. scan->setScanCallbacks(&observer, true); scan->setDuplicateFilter(false); if (!scan->isScanning() && !scan->start(0, false)) return Status::failure("cannot start BLE scan"); return Status::success(); } void Ble::unobserve() { if (!isInitialized()) return; NimBLEScan* scan = NimBLEDevice::getScan(); if (scan->isScanning()) scan->stop(); scan->setScanCallbacks(nullptr); observer.clear(); } bool Ble::isObserving() const { return isInitialized() && NimBLEDevice::getScan()->isScanning(); } void Ble::observed(std::vector& out) { observer.snapshot(out); } Status Ble::connect(const std::string& address) { if (!isInitialized()) return Status::failure("BLE is not initialized"); if (!validAddress(address)) return Status::failure("invalid BLE address"); disconnect(); client = NimBLEDevice::createClient(); if (!client) return Status::failure("cannot create BLE client"); uint8_t addressType = BLE_ADDR_PUBLIC; observer.addressType(address, addressType); if (!client->connect(NimBLEAddress(address, addressType))) { NimBLEDevice::deleteClient(client); client = nullptr; return Status::failure("cannot connect to BLE device"); } return Status::success(); } void Ble::disconnect() { if (!client) return; if (client->isConnected()) client->disconnect(); NimBLEDevice::deleteClient(client); client = nullptr; } bool Ble::isConnected() const { return isInitialized() && client && client->isConnected(); } Status Ble::read(const std::string& service, const std::string& characteristic, std::string& value) { if (!isConnected()) return Status::failure("BLE device is not connected"); NimBLERemoteService* remoteService = client->getService(service.c_str()); if (!remoteService) return Status::failure("BLE service not found"); NimBLERemoteCharacteristic* remoteCharacteristic = remoteService->getCharacteristic(characteristic.c_str()); if (!remoteCharacteristic) return Status::failure("BLE characteristic not found"); if (!remoteCharacteristic->canRead()) return Status::failure("BLE characteristic is not readable"); value = static_cast(remoteCharacteristic->readValue()); return Status::success(); } Status Ble::write(const std::string& service, const std::string& characteristic, const std::string& value) { if (!isConnected()) return Status::failure("BLE device is not connected"); NimBLERemoteService* remoteService = client->getService(service.c_str()); if (!remoteService) return Status::failure("BLE service not found"); NimBLERemoteCharacteristic* remoteCharacteristic = remoteService->getCharacteristic(characteristic.c_str()); if (!remoteCharacteristic) return Status::failure("BLE characteristic not found"); if (!remoteCharacteristic->canWrite() && !remoteCharacteristic->canWriteNoResponse()) return Status::failure("BLE characteristic is not writable"); if (!remoteCharacteristic->writeValue( reinterpret_cast(value.data()), value.size(), remoteCharacteristic->canWrite())) return Status::failure("BLE write failed"); return Status::success(); } Status Ble::startAdvertising(const std::string* name) { if (!isInitialized()) return Status::failure("BLE is not initialized"); NimBLEAdvertising* bleAdvertising = NimBLEDevice::getAdvertising(); if (advertising) bleAdvertising->stop(); bleAdvertising->clearData(); if (name && !bleAdvertising->setName(*name)) return Status::failure("BLE advertising name is too long"); if (!bleAdvertising->start()) return Status::failure("cannot start BLE advertising"); advertising = true; return Status::success(); } void Ble::stopAdvertising() { if (!isInitialized() || !advertising) return; NimBLEDevice::getAdvertising()->stop(); advertising = false; } } // namespace slate