mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-20 12:25:06 +00:00
482 lines
16 KiB
C++
482 lines
16 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Mock Bluetooth driver for the simulator: fakes a radio with a fixed list of nearby peers
|
|
// instead of talking to real hardware. Actions succeed instantly, there's no real scan, pairing
|
|
// or connection delay.
|
|
|
|
#include <tactility/concurrent/mutex.h>
|
|
#include <tactility/device.h>
|
|
#include <tactility/driver.h>
|
|
#include <tactility/drivers/bluetooth.h>
|
|
#include <tactility/log.h>
|
|
|
|
#include <cstring>
|
|
#include <new>
|
|
|
|
#define TAG "mock_bluetooth"
|
|
|
|
namespace {
|
|
|
|
struct MockPeerFixture {
|
|
BtAddr addr;
|
|
const char* name;
|
|
int8_t rssi;
|
|
};
|
|
|
|
constexpr MockPeerFixture MOCK_PEERS[] = {
|
|
{ { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }, "Mock Keyboard", -45 },
|
|
{ { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }, "Mock Gamepad", -60 },
|
|
{ { 0x02, 0x00, 0x00, 0x00, 0x00, 0x03 }, "Mock Speaker", -75 },
|
|
};
|
|
constexpr size_t MOCK_PEER_COUNT = sizeof(MOCK_PEERS) / sizeof(MOCK_PEERS[0]);
|
|
constexpr const char* MOCK_DEFAULT_NAME = "Tactility";
|
|
|
|
struct MockPeerState {
|
|
bool paired = false;
|
|
/** Bit per BtProfileId */
|
|
uint8_t connectedProfiles = 0;
|
|
};
|
|
|
|
struct MockBluetoothCtx {
|
|
Device* device = nullptr;
|
|
|
|
Mutex mutex {};
|
|
BtRadioState radioState = BT_RADIO_STATE_OFF;
|
|
bool scanning = false;
|
|
bool hidHostActive = false;
|
|
char deviceName[BLE_DEVICE_NAME_MAX + 1] = {};
|
|
MockPeerState peers[MOCK_PEER_COUNT];
|
|
|
|
Mutex subscriptionsMutex {};
|
|
BtEventSubscription* subscriptions = nullptr;
|
|
};
|
|
|
|
#define GET_CTX(device) (static_cast<MockBluetoothCtx*>(device_get_driver_data(device)))
|
|
|
|
void fireEvent(MockBluetoothCtx* ctx, const BtEvent& event) {
|
|
mutex_lock(&ctx->subscriptionsMutex);
|
|
for (BtEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->internal.next) {
|
|
mutex_lock(&sub->internal.ring_mutex);
|
|
if (sub->internal.count < BT_EVENT_QUEUE_CAPACITY) {
|
|
uint8_t tail = (sub->internal.head + sub->internal.count) % BT_EVENT_QUEUE_CAPACITY;
|
|
sub->internal.queue[tail] = event;
|
|
sub->internal.count++;
|
|
}
|
|
mutex_unlock(&sub->internal.ring_mutex);
|
|
task_event_group_signal(sub->internal.event_group, sub->bit);
|
|
}
|
|
mutex_unlock(&ctx->subscriptionsMutex);
|
|
}
|
|
|
|
// Returns the index of the peer, or MOCK_PEER_COUNT when the address isn't a mock peer
|
|
size_t findPeer(const BtAddr addr) {
|
|
for (size_t i = 0; i < MOCK_PEER_COUNT; i++) {
|
|
if (memcmp(MOCK_PEERS[i].addr, addr, BT_ADDR_LEN) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return MOCK_PEER_COUNT;
|
|
}
|
|
|
|
// Caller holds ctx->mutex
|
|
void fillPeerRecord(const MockBluetoothCtx* ctx, size_t index, BtPeerRecord* record) {
|
|
memset(record, 0, sizeof(*record));
|
|
memcpy(record->addr, MOCK_PEERS[index].addr, BT_ADDR_LEN);
|
|
strncpy(record->name, MOCK_PEERS[index].name, BT_NAME_MAX);
|
|
record->rssi = MOCK_PEERS[index].rssi;
|
|
record->paired = ctx->peers[index].paired;
|
|
record->connected = ctx->peers[index].connectedProfiles != 0;
|
|
}
|
|
|
|
void fireRadioState(MockBluetoothCtx* ctx, BtRadioState state) {
|
|
BtEvent event = {};
|
|
event.type = BT_EVENT_RADIO_STATE_CHANGED;
|
|
event.radio_state = state;
|
|
fireEvent(ctx, event);
|
|
}
|
|
|
|
void fireProfileState(MockBluetoothCtx* ctx, size_t index, BtProfileId profile, BtProfileState state) {
|
|
BtEvent event = {};
|
|
event.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
|
memcpy(event.profile_state.addr, MOCK_PEERS[index].addr, BT_ADDR_LEN);
|
|
event.profile_state.profile = profile;
|
|
event.profile_state.state = state;
|
|
fireEvent(ctx, event);
|
|
}
|
|
|
|
void fireConnectState(MockBluetoothCtx* ctx, size_t index) {
|
|
BtEvent event = {};
|
|
event.type = BT_EVENT_CONNECT_STATE_CHANGED;
|
|
mutex_lock(&ctx->mutex);
|
|
fillPeerRecord(ctx, index, &event.peer);
|
|
mutex_unlock(&ctx->mutex);
|
|
fireEvent(ctx, event);
|
|
}
|
|
|
|
// ---- BluetoothApi ----
|
|
|
|
error_t apiGetRadioState(Device* device, BtRadioState* state) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || state == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
mutex_lock(&ctx->mutex);
|
|
*state = ctx->radioState;
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiSetRadioEnabled(Device* device, bool enabled) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
|
|
|
mutex_lock(&ctx->mutex);
|
|
const bool changed = enabled != (ctx->radioState == BT_RADIO_STATE_ON);
|
|
ctx->radioState = enabled ? BT_RADIO_STATE_ON : BT_RADIO_STATE_OFF;
|
|
bool disconnected[MOCK_PEER_COUNT] = {};
|
|
if (!enabled) {
|
|
ctx->scanning = false;
|
|
for (size_t i = 0; i < MOCK_PEER_COUNT; i++) {
|
|
disconnected[i] = ctx->peers[i].connectedProfiles != 0;
|
|
ctx->peers[i].connectedProfiles = 0;
|
|
}
|
|
}
|
|
mutex_unlock(&ctx->mutex);
|
|
if (!changed) return ERROR_NONE;
|
|
|
|
for (size_t i = 0; i < MOCK_PEER_COUNT; i++) {
|
|
if (disconnected[i]) fireConnectState(ctx, i);
|
|
}
|
|
fireRadioState(ctx, enabled ? BT_RADIO_STATE_ON : BT_RADIO_STATE_OFF);
|
|
|
|
LOG_I(TAG, "Bluetooth radio %s (mock)", enabled ? "on" : "off");
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiScanStart(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
|
|
|
mutex_lock(&ctx->mutex);
|
|
if (ctx->radioState != BT_RADIO_STATE_ON || ctx->scanning) {
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
ctx->scanning = true;
|
|
mutex_unlock(&ctx->mutex);
|
|
|
|
BtEvent started_event = {};
|
|
started_event.type = BT_EVENT_SCAN_STARTED;
|
|
fireEvent(ctx, started_event);
|
|
|
|
for (size_t i = 0; i < MOCK_PEER_COUNT; i++) {
|
|
BtEvent found_event = {};
|
|
found_event.type = BT_EVENT_PEER_FOUND;
|
|
mutex_lock(&ctx->mutex);
|
|
fillPeerRecord(ctx, i, &found_event.peer);
|
|
mutex_unlock(&ctx->mutex);
|
|
fireEvent(ctx, found_event);
|
|
}
|
|
|
|
// No real radio, so the "scan" completes instantly.
|
|
mutex_lock(&ctx->mutex);
|
|
ctx->scanning = false;
|
|
mutex_unlock(&ctx->mutex);
|
|
|
|
BtEvent finished_event = {};
|
|
finished_event.type = BT_EVENT_SCAN_FINISHED;
|
|
fireEvent(ctx, finished_event);
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiScanStop(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return ERROR_INVALID_STATE;
|
|
mutex_lock(&ctx->mutex);
|
|
ctx->scanning = false;
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
bool apiIsScanning(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return false;
|
|
mutex_lock(&ctx->mutex);
|
|
const bool scanning = ctx->scanning;
|
|
mutex_unlock(&ctx->mutex);
|
|
return scanning;
|
|
}
|
|
|
|
error_t apiPair(Device* device, const BtAddr addr) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
const size_t index = findPeer(addr);
|
|
mutex_lock(&ctx->mutex);
|
|
if (ctx->radioState != BT_RADIO_STATE_ON) {
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
if (index == MOCK_PEER_COUNT) {
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
ctx->peers[index].paired = true;
|
|
mutex_unlock(&ctx->mutex);
|
|
|
|
BtEvent event = {};
|
|
event.type = BT_EVENT_PAIR_RESULT;
|
|
memcpy(event.pair_result.addr, addr, BT_ADDR_LEN);
|
|
event.pair_result.result = BT_PAIR_RESULT_SUCCESS;
|
|
event.pair_result.profile = BT_PROFILE_HID_HOST;
|
|
fireEvent(ctx, event);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiUnpair(Device* device, const BtAddr addr) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
const size_t index = findPeer(addr);
|
|
if (index == MOCK_PEER_COUNT) return ERROR_NOT_FOUND;
|
|
|
|
mutex_lock(&ctx->mutex);
|
|
const bool was_connected = ctx->peers[index].connectedProfiles != 0;
|
|
ctx->peers[index] = {};
|
|
mutex_unlock(&ctx->mutex);
|
|
|
|
if (was_connected) fireConnectState(ctx, index);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiGetPairedPeers(Device* device, BtPeerRecord* out, size_t* count) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || count == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
mutex_lock(&ctx->mutex);
|
|
size_t paired_count = 0;
|
|
for (size_t i = 0; i < MOCK_PEER_COUNT; i++) {
|
|
if (!ctx->peers[i].paired) continue;
|
|
if (out != nullptr && paired_count < *count) {
|
|
fillPeerRecord(ctx, i, &out[paired_count]);
|
|
}
|
|
paired_count++;
|
|
}
|
|
mutex_unlock(&ctx->mutex);
|
|
|
|
// With a buffer, the count is the number of records written
|
|
*count = out != nullptr && paired_count > *count ? *count : paired_count;
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiConnect(Device* device, const BtAddr addr, BtProfileId profile) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
const size_t index = findPeer(addr);
|
|
mutex_lock(&ctx->mutex);
|
|
if (ctx->radioState != BT_RADIO_STATE_ON) {
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
if (index == MOCK_PEER_COUNT) {
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_NOT_FOUND;
|
|
}
|
|
ctx->peers[index].paired = true;
|
|
ctx->peers[index].connectedProfiles |= 1 << profile;
|
|
mutex_unlock(&ctx->mutex);
|
|
|
|
// No real peer to negotiate with, so the mock always succeeds instantly.
|
|
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_CONNECTING);
|
|
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_CONNECTED);
|
|
fireConnectState(ctx, index);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiDisconnect(Device* device, const BtAddr addr, BtProfileId profile) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || addr == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
const size_t index = findPeer(addr);
|
|
if (index == MOCK_PEER_COUNT) return ERROR_NOT_FOUND;
|
|
|
|
mutex_lock(&ctx->mutex);
|
|
const bool was_connected = (ctx->peers[index].connectedProfiles & (1 << profile)) != 0;
|
|
ctx->peers[index].connectedProfiles &= ~(1 << profile);
|
|
mutex_unlock(&ctx->mutex);
|
|
if (!was_connected) return ERROR_NONE;
|
|
|
|
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_DISCONNECTING);
|
|
fireProfileState(ctx, index, profile, BT_PROFILE_STATE_IDLE);
|
|
fireConnectState(ctx, index);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiEventSubscribe(Device* device, BtEventSubscription* sub, TaskEventGroup* event_group) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || sub == nullptr || event_group == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
uint32_t bit;
|
|
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
|
|
if (claim_result != ERROR_NONE) {
|
|
return claim_result;
|
|
}
|
|
|
|
mutex_lock(&ctx->subscriptionsMutex);
|
|
|
|
// Avoid cyclic subscription list that would loop forever
|
|
for (BtEventSubscription* existing = ctx->subscriptions; existing != nullptr; existing = existing->internal.next) {
|
|
if (existing == sub) {
|
|
mutex_unlock(&ctx->subscriptionsMutex);
|
|
task_event_group_release_bit(event_group, bit);
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
}
|
|
|
|
sub->internal.event_group = event_group;
|
|
sub->bit = bit;
|
|
sub->internal.next = ctx->subscriptions;
|
|
ctx->subscriptions = sub;
|
|
mutex_unlock(&ctx->subscriptionsMutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiEventUnsubscribe(Device* device, BtEventSubscription* sub) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || sub == nullptr) return ERROR_INVALID_ARGUMENT;
|
|
|
|
error_t result = ERROR_NOT_FOUND;
|
|
mutex_lock(&ctx->subscriptionsMutex);
|
|
for (BtEventSubscription** link = &ctx->subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
|
if (*link == sub) {
|
|
*link = sub->internal.next;
|
|
result = ERROR_NONE;
|
|
break;
|
|
}
|
|
}
|
|
mutex_unlock(&ctx->subscriptionsMutex);
|
|
|
|
if (result == ERROR_NONE) {
|
|
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
error_t apiSetDeviceName(Device* device, const char* name) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || name == nullptr || strlen(name) > BLE_DEVICE_NAME_MAX) return ERROR_INVALID_ARGUMENT;
|
|
mutex_lock(&ctx->mutex);
|
|
strncpy(ctx->deviceName, name, BLE_DEVICE_NAME_MAX);
|
|
ctx->deviceName[BLE_DEVICE_NAME_MAX] = '\0';
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t apiGetDeviceName(Device* device, char* buf, size_t buf_len) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || buf == nullptr || buf_len == 0) return ERROR_INVALID_ARGUMENT;
|
|
mutex_lock(&ctx->mutex);
|
|
strncpy(buf, ctx->deviceName, buf_len - 1);
|
|
buf[buf_len - 1] = '\0';
|
|
mutex_unlock(&ctx->mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
void apiSetHidHostActive(Device* device, bool active) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return;
|
|
mutex_lock(&ctx->mutex);
|
|
ctx->hidHostActive = active;
|
|
mutex_unlock(&ctx->mutex);
|
|
}
|
|
|
|
void apiFireEvent(Device* device, BtEvent event) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx != nullptr) fireEvent(ctx, event);
|
|
}
|
|
|
|
const BluetoothApi mock_bluetooth_api = {
|
|
.get_radio_state = apiGetRadioState,
|
|
.set_radio_enabled = apiSetRadioEnabled,
|
|
.scan_start = apiScanStart,
|
|
.scan_stop = apiScanStop,
|
|
.is_scanning = apiIsScanning,
|
|
.pair = apiPair,
|
|
.unpair = apiUnpair,
|
|
.get_paired_peers = apiGetPairedPeers,
|
|
.connect = apiConnect,
|
|
.disconnect = apiDisconnect,
|
|
.event_subscribe = apiEventSubscribe,
|
|
.event_unsubscribe = apiEventUnsubscribe,
|
|
.set_device_name = apiSetDeviceName,
|
|
.get_device_name = apiGetDeviceName,
|
|
.set_hid_host_active = apiSetHidHostActive,
|
|
.fire_event = apiFireEvent,
|
|
};
|
|
|
|
// ---- Driver lifecycle ----
|
|
// startDevice()/stopDevice() only allocate/free this driver's bookkeeping (subscriber list
|
|
// included), so event subscribers can stay subscribed across radio on/off toggles.
|
|
|
|
error_t startDevice(Device* device) {
|
|
auto* ctx = new(std::nothrow) MockBluetoothCtx();
|
|
if (ctx == nullptr) return ERROR_OUT_OF_MEMORY;
|
|
|
|
ctx->device = device;
|
|
strncpy(ctx->deviceName, MOCK_DEFAULT_NAME, BLE_DEVICE_NAME_MAX);
|
|
mutex_construct(&ctx->mutex);
|
|
mutex_construct(&ctx->subscriptionsMutex);
|
|
|
|
device_set_driver_data(device, ctx);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t stopDevice(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return ERROR_NONE;
|
|
|
|
if (ctx->radioState == BT_RADIO_STATE_ON) {
|
|
apiSetRadioEnabled(device, false);
|
|
}
|
|
|
|
// Release any subscribers that never unsubscribed: device_stop() doesn't wait for apps still
|
|
// using this device, so a later bluetooth_event_unsubscribe() would find no ctx and skip
|
|
// releasing the bit.
|
|
mutex_lock(&ctx->subscriptionsMutex);
|
|
for (BtEventSubscription* sub = ctx->subscriptions; sub != nullptr;) {
|
|
BtEventSubscription* next = sub->internal.next;
|
|
// Force-close, don't destruct: a concurrent bluetooth_event_poll() may hold/await this
|
|
// same lock (see BtEventSubscription::internal::closed).
|
|
mutex_lock(&sub->internal.ring_mutex);
|
|
sub->internal.closed = true;
|
|
mutex_unlock(&sub->internal.ring_mutex);
|
|
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
|
sub = next;
|
|
}
|
|
ctx->subscriptions = nullptr;
|
|
mutex_unlock(&ctx->subscriptionsMutex);
|
|
|
|
device_set_driver_data(device, nullptr);
|
|
mutex_destruct(&ctx->subscriptionsMutex);
|
|
mutex_destruct(&ctx->mutex);
|
|
delete ctx;
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
Driver mock_bluetooth_driver = {
|
|
.name = "mock_bluetooth",
|
|
.compatible = (const char*[]) { "mock,bluetooth", nullptr },
|
|
.start_device = startDevice,
|
|
.stop_device = stopDevice,
|
|
.api = (const void*)&mock_bluetooth_api,
|
|
.device_type = &BLUETOOTH_TYPE,
|
|
.internal = nullptr
|
|
};
|
|
|
|
} // extern "C"
|