Files
slate32/src/host/providers_ble.cpp
T
evan 9e58e72bf6 build: add a format target and one shared editor config
clang-format had no config here, so adopting the submodule's puts both repos
on the same pointer alignment. .editorconfig is what lua-language-server reads
on save, which is why the Lua tree had drifted between tabs and two widths.
2026-08-04 13:20:08 -04:00

237 lines
6.7 KiB
C++

#include "providers.h"
#include <Arduino.h>
#include <NimBLEDevice.h>
#include <WiFi.h>
#include "../heaplog.h"
#include <cstdio>
namespace slate {
namespace {
using esp32lua::Status;
constexpr size_t MAX_SCAN_RESULTS = 6;
struct ScanResult {
char name[32];
char address[18];
int32_t rssi;
uint8_t addressType;
};
class ScanCallbacks : public NimBLEScanCallbacks {
public:
void reset() { count = 0; }
size_t size() const { return count; }
const ScanResult& result(size_t index) const { return results[index]; }
void onResult(const NimBLEAdvertisedDevice* device) override {
ScanResult result{};
const std::string name = device->haveName() ? device->getName() : "";
const std::string address = device->getAddress().toString();
std::snprintf(result.name, sizeof(result.name), "%s", name.c_str());
std::snprintf(result.address, sizeof(result.address), "%s",
address.c_str());
result.rssi = device->getRSSI();
result.addressType = device->getAddressType();
size_t at;
if (count < MAX_SCAN_RESULTS) {
at = count++;
} else {
if (result.rssi <= results[count - 1].rssi)
return;
at = count - 1;
}
while (at > 0 && result.rssi > results[at - 1].rssi) {
results[at] = results[at - 1];
at--;
}
results[at] = result;
}
private:
ScanResult results[MAX_SCAN_RESULTS]{};
size_t count = 0;
};
ScanCallbacks scanCallbacks;
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();
NimBLEDevice::deinit(true);
addressTypes.clear();
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::scan(int32_t durationMs,
std::vector<esp32lua::BleDevice>& devices) {
if (!isInitialized())
return Status::failure("BLE is not initialized");
NimBLEScan* scan = NimBLEDevice::getScan();
scanCallbacks.reset();
scan->setActiveScan(false);
scan->setMaxResults(0);
scan->setScanCallbacks(&scanCallbacks);
scan->getResults(static_cast<uint32_t>(durationMs), false);
scan->setScanCallbacks(nullptr);
scan->clearResults();
devices.clear();
addressTypes.clear();
for (size_t at = 0; at < scanCallbacks.size(); at++) {
const ScanResult& result = scanCallbacks.result(at);
devices.push_back({result.name, result.address, result.rssi});
addressTypes[result.address] = result.addressType;
}
return Status::success();
}
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;
const auto found = addressTypes.find(address);
if (found != addressTypes.end())
addressType = found->second;
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<std::string>(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<const uint8_t*>(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