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
706 changes: 706 additions & 0 deletions docs/ble-shim.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions src/NimBLEAttValue.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

// Shim for NimBLE's NimBLEAttValue -- the value of one GATT attribute.
//
// The real class is a small heap buffer with a capacity policy and a pile of
// conversion helpers. The firmware uses three things from it: it takes one by
// value out of getValue(), then reads data() and size(). So that is what this
// is: a vector with those names on it.
//
// Returning by value is deliberate, not an oversight of the shim. The real
// getValue() copies too, and the firmware's transfer path is written around
// that copy costing one malloc/memcpy/free per chunk.

#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>

class NimBLEAttValue {
public:
NimBLEAttValue() = default;
NimBLEAttValue(const uint8_t *data, size_t len) {
if (data != nullptr && len > 0) m_value.assign(data, data + len);
}
explicit NimBLEAttValue(std::vector<uint8_t> value)
: m_value(std::move(value)) {}

const uint8_t *data() const { return m_value.data(); }
size_t size() const { return m_value.size(); }
size_t length() const { return m_value.size(); }
bool empty() const { return m_value.empty(); }

const uint8_t *begin() const { return m_value.data(); }
const uint8_t *end() const { return m_value.data() + m_value.size(); }
uint8_t operator[](size_t i) const { return m_value[i]; }

private:
std::vector<uint8_t> m_value;
};
98 changes: 98 additions & 0 deletions src/NimBLECharacteristic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#pragma once

// Shim for NimBLE's NimBLECharacteristic and its callback interface.
//
// The characteristic is a value plus a subscription plus a callback pointer.
// All three live in this object; SimBleGatt is a friend and owns the mutex
// that guards them, because the reader thread, the host thread and the
// activity thread all touch them.
//
// **Callback dispatch is never inline.** onWrite, onStatus and onSubscribe are
// called by SimBleGatt's host thread, never by the client op that caused them
// and never by indicate(). See docs/ble-shim.md, "Threading model".

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

#include "NimBLEAttValue.h"
#include "NimBLEConnInfo.h"

// Real NimBLE spells these as a namespace of constants, so a firmware
// expression like `WRITE | NOTIFY | INDICATE` is an integer. Values are
// NimBLE's own, so a props number in an emitted `gatt` event means the same
// thing here as in a NimBLE header.
//
// Read off the pinned NimBLE host, not from memory -- `BLE_GATT_CHR_F_*` in
// nimble/host/include/host/ble_gatt.h:130-145. Only the four the firmware uses
// are declared. In particular 0x0001 is **broadcast**, not read: read is
// 0x0002 (ble_gatt.h:133), and standard GATT agrees, so this is not a NimBLE
// quirk. A shim carrying broadcast's bit under the name READ would tell a
// client "broadcast" wherever the device meant "read".
namespace NIMBLE_PROPERTY {
static constexpr uint32_t READ = 0x0002; // ble_gatt.h:133
static constexpr uint32_t WRITE = 0x0008; // ble_gatt.h:139
static constexpr uint32_t NOTIFY = 0x0010; // ble_gatt.h:142
static constexpr uint32_t INDICATE = 0x0020; // ble_gatt.h:145
} // namespace NIMBLE_PROPERTY

class NimBLECharacteristic;
class SimBleGatt;

class NimBLECharacteristicCallbacks {
public:
virtual ~NimBLECharacteristicCallbacks() = default;

// A central wrote this characteristic. Read the bytes with getValue().
virtual void onWrite(NimBLECharacteristic *, NimBLEConnInfo &) {}

// One indication finished. `code` is BLE_HS_EDONE when the peer confirmed.
// Fires once per accepted indicate(), out of band, on the host thread.
virtual void onStatus(NimBLECharacteristic *, NimBLEConnInfo &, int) {}

// A central changed its subscription. bit0 is notify, bit1 is indicate.
// Never fired for a disconnect -- NimBLE does not, so neither does this.
virtual void onSubscribe(NimBLECharacteristic *, NimBLEConnInfo &,
uint16_t) {}
};

class NimBLECharacteristic {
public:
NimBLECharacteristic(const char *uuid, uint32_t properties)
: m_uuid(uuid != nullptr ? uuid : ""), m_properties(properties) {}

void setCallbacks(NimBLECharacteristicCallbacks *callbacks);

// A copy of the last value a central wrote. Returned by value, same as the
// real API.
NimBLEAttValue getValue();

// Puts `len` bytes into the connection's single pending indication slot.
//
// **True means the slot accepted the payload, not that the peer got it.**
// The confirm arrives later through onStatus. A second call before that
// confirm overwrites the first and still returns true -- measured on real
// hardware, and the shim reproduces it rather than queueing politely. The
// overwrite emits a `clobber` event so it is observable instead of silent.
//
// False means the real stack would have refused: stack down, no central
// connected, nobody subscribed to this characteristic, this characteristic
// cannot notify or indicate, or an empty payload.
bool indicate(const uint8_t *data, size_t len);

// Shim-only accessors. Not part of the NimBLE API; the emitted `gatt` event
// and the self-test read them.
const std::string &shimUuid() const { return m_uuid; }
uint32_t shimProperties() const { return m_properties; }

private:
friend class SimBleGatt;

std::string m_uuid;
uint32_t m_properties = 0;
NimBLECharacteristicCallbacks *m_callbacks = nullptr;
// Guarded by SimBleGatt's mutex.
std::vector<uint8_t> m_value;
uint16_t m_subValue = 0;
};
37 changes: 37 additions & 0 deletions src/NimBLEConnInfo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

// Shim for NimBLE's NimBLEConnInfo -- what one connection negotiated.
//
// Every firmware callback in the shim's contract takes one of these by
// reference. The real class wraps ble_gap_conn_desc and exposes peer address,
// bonding state and more; the firmware reads four numbers off it, so those
// four are what exist here.
//
// The interval, latency and timeout are the central's choices, not the
// peripheral's: the client sets them with the `connect` and `connparams` ops
// (docs/ble-shim.md, "Client to simulator"). Interval is in 1.25 ms units,
// timeout in 10 ms units -- the same units the firmware's logs assume.

#include <cstdint>

#include "host/ble_gap.h"

class NimBLEConnInfo {
public:
NimBLEConnInfo() = default;
NimBLEConnInfo(uint16_t handle, uint16_t interval, uint16_t latency,
uint16_t timeout)
: m_handle(handle), m_interval(interval), m_latency(latency),
m_timeout(timeout) {}

uint16_t getConnHandle() const { return m_handle; }
uint16_t getConnInterval() const { return m_interval; }
uint16_t getConnLatency() const { return m_latency; }
uint16_t getConnTimeout() const { return m_timeout; }

private:
uint16_t m_handle = BLE_HS_CONN_HANDLE_NONE;
uint16_t m_interval = 0;
uint16_t m_latency = 0;
uint16_t m_timeout = 0;
};
106 changes: 106 additions & 0 deletions src/NimBLEDevice.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "NimBLEDevice.h"

#include "SimBleGatt.h"

// Every method here is a forwarder. The model, the threads and the wire
// protocol live in SimBleGatt; this file only exists so the firmware's
// #include <NimBLEDevice.h> compiles and links.

// --- NimBLEDevice -----------------------------------------------------------

bool NimBLEDevice::init(const char *deviceName) {
return SimBleGatt::get().init(deviceName);
}

void NimBLEDevice::deinit(bool clearAll) {
SimBleGatt::get().deinit(clearAll);
}

bool NimBLEDevice::isInitialized() { return SimBleGatt::get().initialized(); }

NimBLEServer *NimBLEDevice::createServer() { return SimBleGatt::get().server(); }

NimBLEAdvertising *NimBLEDevice::getAdvertising() {
return SimBleGatt::get().advertising();
}

void NimBLEDevice::setSecurityAuth(bool bonding, bool mitm, bool sc) {
SimBleGatt::get().setSecurityAuth(bonding, mitm, sc);
}

// --- NimBLEServer -----------------------------------------------------------

NimBLEService *NimBLEServer::createService(const char *uuid) {
return SimBleGatt::get().createService(uuid);
}

void NimBLEServer::setCallbacks(NimBLEServerCallbacks *callbacks, bool) {
// The delete flag is ignored: the shim never owns the pointer. The firmware
// passes false and registers a static object, so nothing is lost.
SimBleGatt::get().setServerCallbacks(callbacks);
}

bool NimBLEServer::start() { return SimBleGatt::get().startServer(); }

void NimBLEServer::updateConnParams(uint16_t handle, uint16_t minInterval,
uint16_t maxInterval, uint16_t latency,
uint16_t timeout) {
SimBleGatt::get().requestConnParams(handle, minInterval, maxInterval, latency,
timeout);
}

// --- NimBLEService ----------------------------------------------------------

NimBLECharacteristic *NimBLEService::createCharacteristic(const char *uuid,
uint32_t properties) {
return SimBleGatt::get().createCharacteristic(this, uuid, properties);
}

bool NimBLEService::start() { return true; }

// --- NimBLECharacteristic ---------------------------------------------------

void NimBLECharacteristic::setCallbacks(
NimBLECharacteristicCallbacks *callbacks) {
SimBleGatt::get().setCharacteristicCallbacks(this, callbacks);
}

NimBLEAttValue NimBLECharacteristic::getValue() {
return SimBleGatt::get().characteristicValue(this);
}

bool NimBLECharacteristic::indicate(const uint8_t *data, size_t len) {
return SimBleGatt::get().indicate(this, data, len);
}

// --- NimBLEAdvertising ------------------------------------------------------

bool NimBLEAdvertising::start() { return SimBleGatt::get().advertisingStart(); }

void NimBLEAdvertising::stop() { SimBleGatt::get().advertisingStop(); }

void NimBLEAdvertising::setName(const char *name) {
SimBleGatt::get().setAdvertisingName(name);
}

void NimBLEAdvertising::addServiceUUID(const char *uuid) {
SimBleGatt::get().setAdvertisingServiceUuid(uuid);
}

void NimBLEAdvertising::enableScanResponse(bool enable) {
SimBleGatt::get().setAdvertisingScanResponse(enable);
}

void NimBLEAdvertising::setMinInterval(uint16_t interval) {
SimBleGatt::get().setAdvertisingMinInterval(interval);
}

void NimBLEAdvertising::setMaxInterval(uint16_t interval) {
SimBleGatt::get().setAdvertisingMaxInterval(interval);
}

// --- host/ble_gap.h ---------------------------------------------------------

extern "C" int ble_gap_conn_rssi(uint16_t conn_handle, int8_t *out_rssi) {
return SimBleGatt::get().connRssi(conn_handle, out_rssi);
}
Loading