2026-10-12 20:29:29 +02:00

599 lines
21 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/radio.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/time.h>
#include <algorithm>
#include <cstring>
#include <vector>
#define RADIO_API(device) ((const struct RadioApi*)device_get_driver(device)->api)
struct RadioEventMutex {
Mutex handle {};
RadioEventMutex() { mutex_construct(&handle); }
~RadioEventMutex() { mutex_destruct(&handle); }
};
// Each event kind gets its own intrusive singly-linked subscription list, keyed by device, and
// its own coarse-grained mutex. Notifying a subscriber never invokes caller code (just a struct
// copy and a task_event_group_signal), so there is no reentrancy concern requiring a
// snapshot-then-unlock dance.
static RadioStateEventSubscription* radio_state_event_subscriptions = nullptr;
static RadioEventMutex radio_state_event_subscriptions_mutex;
static RadioRxEventSubscription* radio_rx_event_subscriptions = nullptr;
static RadioEventMutex radio_rx_event_subscriptions_mutex;
static RadioTxEventSubscription* radio_tx_event_subscriptions = nullptr;
static RadioEventMutex radio_tx_event_subscriptions_mutex;
// Kernel-side per-device state that drivers don't need to know about
struct RadioDeviceState {
Device* device;
char owner[RADIO_OWNER_NAME_MAX_LENGTH + 1];
RadioRegion region;
};
static std::vector<RadioDeviceState> radio_device_states;
static RadioEventMutex radio_device_states_mutex;
static RadioDeviceState* find_device_state(Device* device) {
for (auto& state : radio_device_states) {
if (state.device == device) {
return &state;
}
}
return nullptr;
}
static RadioDeviceState& get_or_add_device_state(Device* device) {
auto* state = find_device_state(device);
if (state != nullptr) {
return *state;
}
radio_device_states.push_back(RadioDeviceState { .device = device, .owner = {}, .region = RADIO_REGION_UNSET });
return radio_device_states.back();
}
static void remove_device_state_if_unused(Device* device) {
std::erase_if(radio_device_states, [device](const RadioDeviceState& state) {
return state.device == device && state.owner[0] == '\0' && state.region == RADIO_REGION_UNSET;
});
}
static constexpr RadioRegionBand REGION_BANDS_EU_433[] = { { 433050000, 434790000, 10 } };
static constexpr RadioRegionBand REGION_BANDS_EU_868[] = {
{ 863000000, 869400000, 14 },
{ 869400000, 869650000, 27 },
{ 869650000, 870000000, 14 },
};
static constexpr RadioRegionBand REGION_BANDS_US_915[] = { { 902000000, 928000000, 30 } };
static constexpr RadioRegionBand REGION_BANDS_AU_915[] = { { 915000000, 928000000, 30 } };
static constexpr RadioRegionBand REGION_BANDS_NZ_865[] = { { 864000000, 868000000, 36 } };
static constexpr RadioRegionBand REGION_BANDS_IN_865[] = { { 865000000, 867000000, 30 } };
static constexpr RadioRegionBand REGION_BANDS_RU_868[] = { { 868700000, 869200000, 20 } };
static constexpr RadioRegionBand REGION_BANDS_CN_470[] = { { 470000000, 510000000, 19 } };
static constexpr RadioRegionBand REGION_BANDS_JP_920[] = { { 920800000, 927800000, 16 } };
struct RadioRegionInfo {
const char* name;
const RadioRegionBand* bands;
size_t band_count;
};
#define REGION_INFO(name, bands) { name, bands, sizeof(bands) / sizeof(bands[0]) }
static constexpr RadioRegionInfo REGION_INFOS[RADIO_REGION_COUNT] = {
{ "UNSET", nullptr, 0 },
REGION_INFO("EU_433", REGION_BANDS_EU_433),
REGION_INFO("EU_868", REGION_BANDS_EU_868),
REGION_INFO("US_915", REGION_BANDS_US_915),
REGION_INFO("AU_915", REGION_BANDS_AU_915),
REGION_INFO("NZ_865", REGION_BANDS_NZ_865),
REGION_INFO("IN_865", REGION_BANDS_IN_865),
REGION_INFO("RU_868", REGION_BANDS_RU_868),
REGION_INFO("CN_470", REGION_BANDS_CN_470),
REGION_INFO("JP_920", REGION_BANDS_JP_920),
};
static bool is_in_any_band(enum RadioRegion region, int32_t frequency) {
const RadioRegionBand* bands;
const size_t count = radio_get_region_bands(region, &bands);
for (size_t i = 0; i < count; i++) {
if (frequency >= bands[i].frequency_min && frequency <= bands[i].frequency_max) {
return true;
}
}
return false;
}
static int8_t get_highest_power(enum RadioRegion region) {
const RadioRegionBand* bands;
const size_t count = radio_get_region_bands(region, &bands);
int8_t highest = INT8_MIN;
for (size_t i = 0; i < count; i++) {
highest = std::max(highest, bands[i].power_max);
}
return highest;
}
/** Check the configured frequency, bandwidth and power against a single band of the region */
static error_t check_configuration_in_region(Device* device, enum RadioRegion region) {
int32_t frequency;
if (RADIO_API(device)->get_parameter(device, RADIO_PARAMETER_FREQUENCY, &frequency) != ERROR_NONE) {
// A frequency that can't be read can't be verified
return ERROR_OUT_OF_RANGE;
}
int32_t bandwidth = 0;
RADIO_API(device)->get_parameter(device, RADIO_PARAMETER_BANDWIDTH, &bandwidth);
const int32_t half_bandwidth = bandwidth / 2;
int32_t power;
const bool has_power = RADIO_API(device)->get_parameter(device, RADIO_PARAMETER_POWER, &power) == ERROR_NONE;
const RadioRegionBand* bands;
const size_t count = radio_get_region_bands(region, &bands);
for (size_t i = 0; i < count; i++) {
if ((frequency - half_bandwidth) >= bands[i].frequency_min &&
(frequency + half_bandwidth) <= bands[i].frequency_max &&
(!has_power || power <= bands[i].power_max)) {
return ERROR_NONE;
}
}
return ERROR_OUT_OF_RANGE;
}
extern "C" {
// region State events
error_t radio_state_event_subscribe(RadioStateEventSubscription* sub, TaskEventGroup* event_group, Device* device) {
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(&radio_state_event_subscriptions_mutex.handle);
// Avoid cyclic subscription list that would loop forever
for (RadioStateEventSubscription* existing = radio_state_event_subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&radio_state_event_subscriptions_mutex.handle);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->bit = bit;
sub->internal.device = device;
sub->internal.event_group = event_group;
sub->internal.head = 0;
sub->internal.count = 0;
sub->internal.next = radio_state_event_subscriptions;
radio_state_event_subscriptions = sub;
mutex_unlock(&radio_state_event_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t radio_state_event_unsubscribe(RadioStateEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&radio_state_event_subscriptions_mutex.handle);
for (RadioStateEventSubscription** link = &radio_state_event_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
if (*link == sub) {
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&radio_state_event_subscriptions_mutex.handle);
if (result == ERROR_NONE) {
task_event_group_release_bit(sub->internal.event_group, sub->bit);
}
return result;
}
error_t radio_state_event_emit(Device* device, enum RadioState state) {
RadioStateEvent stamped_event { .timestamp = get_micros_since_boot(), .state = state };
error_t result = ERROR_NOT_FOUND;
mutex_lock(&radio_state_event_subscriptions_mutex.handle);
for (RadioStateEventSubscription* sub = radio_state_event_subscriptions; sub != nullptr; sub = sub->internal.next) {
if (sub->internal.device != device) {
continue;
}
if (sub->internal.count >= RADIO_STATE_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->internal.head + sub->internal.count) % RADIO_STATE_EVENT_QUEUE_CAPACITY;
sub->internal.queue[tail] = stamped_event;
sub->internal.count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
task_event_group_signal(sub->internal.event_group, sub->bit);
}
mutex_unlock(&radio_state_event_subscriptions_mutex.handle);
return result;
}
error_t radio_state_event_poll(RadioStateEventSubscription* sub, RadioStateEvent* out_event) {
mutex_lock(&radio_state_event_subscriptions_mutex.handle);
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->internal.queue[sub->internal.head];
sub->internal.head = (sub->internal.head + 1) % RADIO_STATE_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&radio_state_event_subscriptions_mutex.handle);
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
}
// endregion
// region RX events
error_t radio_rx_event_subscribe(RadioRxEventSubscription* sub, TaskEventGroup* event_group, Device* device) {
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(&radio_rx_event_subscriptions_mutex.handle);
// Avoid cyclic subscription list that would loop forever
for (RadioRxEventSubscription* existing = radio_rx_event_subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&radio_rx_event_subscriptions_mutex.handle);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->bit = bit;
sub->internal.device = device;
sub->internal.event_group = event_group;
sub->internal.head = 0;
sub->internal.count = 0;
sub->internal.next = radio_rx_event_subscriptions;
radio_rx_event_subscriptions = sub;
mutex_unlock(&radio_rx_event_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t radio_rx_event_unsubscribe(RadioRxEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&radio_rx_event_subscriptions_mutex.handle);
for (RadioRxEventSubscription** link = &radio_rx_event_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
if (*link == sub) {
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&radio_rx_event_subscriptions_mutex.handle);
if (result == ERROR_NONE) {
task_event_group_release_bit(sub->internal.event_group, sub->bit);
}
return result;
}
error_t radio_rx_event_emit(Device* device, const uint8_t* data, size_t length, float rssi, float snr) {
RadioRxEvent stamped_event {};
stamped_event.timestamp = get_micros_since_boot();
stamped_event.length = std::min(length, sizeof(stamped_event.data));
std::memcpy(stamped_event.data, data, stamped_event.length);
stamped_event.rssi = rssi;
stamped_event.snr = snr;
error_t result = ERROR_NOT_FOUND;
mutex_lock(&radio_rx_event_subscriptions_mutex.handle);
for (RadioRxEventSubscription* sub = radio_rx_event_subscriptions; sub != nullptr; sub = sub->internal.next) {
if (sub->internal.device != device) {
continue;
}
if (sub->internal.count >= RADIO_RX_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->internal.head + sub->internal.count) % RADIO_RX_EVENT_QUEUE_CAPACITY;
sub->internal.queue[tail] = stamped_event;
sub->internal.count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
task_event_group_signal(sub->internal.event_group, sub->bit);
}
mutex_unlock(&radio_rx_event_subscriptions_mutex.handle);
return result;
}
error_t radio_rx_event_poll(RadioRxEventSubscription* sub, RadioRxEvent* out_event) {
mutex_lock(&radio_rx_event_subscriptions_mutex.handle);
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->internal.queue[sub->internal.head];
sub->internal.head = (sub->internal.head + 1) % RADIO_RX_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&radio_rx_event_subscriptions_mutex.handle);
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
}
// endregion
// region TX events
error_t radio_tx_event_subscribe(RadioTxEventSubscription* sub, TaskEventGroup* event_group, Device* device) {
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(&radio_tx_event_subscriptions_mutex.handle);
// Avoid cyclic subscription list that would loop forever
for (RadioTxEventSubscription* existing = radio_tx_event_subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&radio_tx_event_subscriptions_mutex.handle);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->bit = bit;
sub->internal.device = device;
sub->internal.event_group = event_group;
sub->internal.head = 0;
sub->internal.count = 0;
sub->internal.next = radio_tx_event_subscriptions;
radio_tx_event_subscriptions = sub;
mutex_unlock(&radio_tx_event_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t radio_tx_event_unsubscribe(RadioTxEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&radio_tx_event_subscriptions_mutex.handle);
for (RadioTxEventSubscription** link = &radio_tx_event_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
if (*link == sub) {
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&radio_tx_event_subscriptions_mutex.handle);
if (result == ERROR_NONE) {
task_event_group_release_bit(sub->internal.event_group, sub->bit);
}
return result;
}
error_t radio_tx_event_emit(Device* device, RadioTxId id, enum RadioTransmissionState state) {
RadioTxEvent stamped_event { .timestamp = get_micros_since_boot(), .id = id, .state = state };
error_t result = ERROR_NOT_FOUND;
mutex_lock(&radio_tx_event_subscriptions_mutex.handle);
for (RadioTxEventSubscription* sub = radio_tx_event_subscriptions; sub != nullptr; sub = sub->internal.next) {
if (sub->internal.device != device) {
continue;
}
if (sub->internal.count >= RADIO_TX_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->internal.head + sub->internal.count) % RADIO_TX_EVENT_QUEUE_CAPACITY;
sub->internal.queue[tail] = stamped_event;
sub->internal.count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
task_event_group_signal(sub->internal.event_group, sub->bit);
}
mutex_unlock(&radio_tx_event_subscriptions_mutex.handle);
return result;
}
error_t radio_tx_event_poll(RadioTxEventSubscription* sub, RadioTxEvent* out_event) {
mutex_lock(&radio_tx_event_subscriptions_mutex.handle);
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->internal.queue[sub->internal.head];
sub->internal.head = (sub->internal.head + 1) % RADIO_TX_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&radio_tx_event_subscriptions_mutex.handle);
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
}
// endregion
error_t radio_get_state(struct Device* device, enum RadioState* state) {
return RADIO_API(device)->get_radio_state(device, state);
}
error_t radio_set_enabled(struct Device* device, bool enabled) {
if (enabled) {
const auto region = radio_get_region(device);
if (region != RADIO_REGION_UNSET) {
const error_t result = check_configuration_in_region(device, region);
if (result != ERROR_NONE) {
return result;
}
}
}
return RADIO_API(device)->set_enabled(device, enabled);
}
error_t radio_set_modulation(struct Device* device, enum RadioModulation modulation) {
return RADIO_API(device)->set_modulation(device, modulation);
}
error_t radio_get_modulation(struct Device* device, enum RadioModulation* modulation) {
return RADIO_API(device)->get_modulation(device, modulation);
}
bool radio_can_transmit(struct Device* device, enum RadioModulation modulation) {
return RADIO_API(device)->can_transmit(device, modulation);
}
bool radio_can_receive(struct Device* device, enum RadioModulation modulation) {
return RADIO_API(device)->can_receive(device, modulation);
}
error_t radio_set_parameter(struct Device* device, enum RadioParameter parameter, int32_t value) {
const auto region = radio_get_region(device);
if (region != RADIO_REGION_UNSET) {
if (parameter == RADIO_PARAMETER_FREQUENCY && !is_in_any_band(region, value)) {
return ERROR_OUT_OF_RANGE;
}
if (parameter == RADIO_PARAMETER_POWER && value > get_highest_power(region)) {
return ERROR_OUT_OF_RANGE;
}
}
return RADIO_API(device)->set_parameter(device, parameter, value);
}
error_t radio_get_parameter(struct Device* device, enum RadioParameter parameter, int32_t* value) {
return RADIO_API(device)->get_parameter(device, parameter, value);
}
error_t radio_transmit(struct Device* device, const uint8_t* data, size_t length, RadioTxId* id) {
return RADIO_API(device)->transmit(device, data, length, id);
}
error_t radio_get_rssi(struct Device* device, float* rssi) {
const auto* api = RADIO_API(device);
if (api->get_rssi == nullptr) return ERROR_NOT_SUPPORTED;
return api->get_rssi(device, rssi);
}
error_t radio_is_receiving(struct Device* device, bool* receiving) {
const auto* api = RADIO_API(device);
if (api->is_receiving == nullptr) return ERROR_NOT_SUPPORTED;
return api->is_receiving(device, receiving);
}
// region Ownership
error_t radio_acquire(struct Device* device, const char* owner) {
mutex_lock(&radio_device_states_mutex.handle);
auto& state = get_or_add_device_state(device);
error_t result;
if (state.owner[0] == '\0') {
std::strncpy(state.owner, owner, RADIO_OWNER_NAME_MAX_LENGTH);
state.owner[RADIO_OWNER_NAME_MAX_LENGTH] = '\0';
result = ERROR_NONE;
} else if (std::strncmp(state.owner, owner, RADIO_OWNER_NAME_MAX_LENGTH) == 0) {
result = ERROR_NONE;
} else {
result = ERROR_RESOURCE_BUSY;
}
mutex_unlock(&radio_device_states_mutex.handle);
return result;
}
error_t radio_release(struct Device* device) {
mutex_lock(&radio_device_states_mutex.handle);
auto* state = find_device_state(device);
error_t result = ERROR_INVALID_STATE;
if (state != nullptr && state->owner[0] != '\0') {
state->owner[0] = '\0';
remove_device_state_if_unused(device);
result = ERROR_NONE;
}
mutex_unlock(&radio_device_states_mutex.handle);
return result;
}
error_t radio_get_owner(struct Device* device, char* owner, size_t owner_size) {
mutex_lock(&radio_device_states_mutex.handle);
auto* state = find_device_state(device);
error_t result = ERROR_NOT_FOUND;
if (state != nullptr && state->owner[0] != '\0' && owner_size > 0) {
std::strncpy(owner, state->owner, owner_size - 1);
owner[owner_size - 1] = '\0';
result = ERROR_NONE;
}
mutex_unlock(&radio_device_states_mutex.handle);
return result;
}
// endregion
// region Regulatory region
error_t radio_set_region(struct Device* device, enum RadioRegion region) {
if (region < RADIO_REGION_UNSET || region >= RADIO_REGION_COUNT) {
return ERROR_INVALID_ARGUMENT;
}
mutex_lock(&radio_device_states_mutex.handle);
get_or_add_device_state(device).region = region;
remove_device_state_if_unused(device);
mutex_unlock(&radio_device_states_mutex.handle);
return ERROR_NONE;
}
enum RadioRegion radio_get_region(struct Device* device) {
mutex_lock(&radio_device_states_mutex.handle);
auto* state = find_device_state(device);
const auto region = (state != nullptr) ? state->region : RADIO_REGION_UNSET;
mutex_unlock(&radio_device_states_mutex.handle);
return region;
}
size_t radio_get_region_bands(enum RadioRegion region, const struct RadioRegionBand** bands) {
if (region <= RADIO_REGION_UNSET || region >= RADIO_REGION_COUNT) {
*bands = nullptr;
return 0;
}
*bands = REGION_INFOS[region].bands;
return REGION_INFOS[region].band_count;
}
const char* radio_region_to_string(enum RadioRegion region) {
if (region < RADIO_REGION_UNSET || region >= RADIO_REGION_COUNT) {
return "unknown";
}
return REGION_INFOS[region].name;
}
// endregion
const struct DeviceType RADIO_TYPE = {
.name = "radio"
};
} // extern "C"