// SPDX-License-Identifier: Apache-2.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include constexpr auto* TAG = "mesh"; namespace { constexpr size_t THREAD_STACK_SIZE = 8192; constexpr size_t MAX_BACKENDS = 4; constexpr size_t MAX_NODES = 128; constexpr size_t TX_QUEUE_LIMIT = 16; constexpr uint64_t TICK_INTERVAL_MS = 1000; constexpr uint64_t FLUSH_MAX_AGE_MS = 30000; constexpr uint64_t DUTY_CYCLE_WINDOW_MS = 3600 * 1000; constexpr int CHANNEL_BUSY_RETRIES = 3; constexpr uint32_t CALL_DONE_BIT = (1 << 0); // 2023-11-14: a system clock before this was never set constexpr int64_t MIN_VALID_WALL_TIME = 1700000000; struct GlobalMutex { Mutex handle {}; GlobalMutex() { mutex_construct(&handle); } ~GlobalMutex() { mutex_destruct(&handle); } }; /** Guards the backend registry, the settings, the event subscriptions and the service pointer with its data */ GlobalMutex globalMutex; void lock() { mutex_lock(&globalMutex.handle); } void unlock() { mutex_unlock(&globalMutex.handle); } /** Serializes callOnServiceThread() callers. Taken before the global lock, never while holding it */ GlobalMutex callMutex; const MeshBackendApi* backends[MAX_BACKENDS] = {}; size_t backendCount = 0; MeshSettings settings {}; bool settingsLoaded = false; MeshEventSubscription* subscriptions = nullptr; uint64_t getMillis() { return get_micros_since_boot() / 1000; } const MeshBackendApi* findBackend(const char* id) { for (size_t i = 0; i < backendCount; i++) { if (strncmp(backends[i]->id, id, MESH_BACKEND_ID_MAX_LENGTH) == 0) { return backends[i]; } } return nullptr; } /** Takes a reference on the first started radio that can send and receive LoRa. @return nullptr when there is none */ Device* findRadio() { Device* found = nullptr; device_for_each_of_type(&RADIO_TYPE, &found, [](Device* device, void* context) { if (!device_is_ready(device) || !radio_can_transmit(device, RADIO_MODULATION_LORA) || !radio_can_receive(device, RADIO_MODULATION_LORA)) { return true; } if (device_get(device) != ERROR_NONE) { return true; } *static_cast(context) = device; return false; }); return found; } /** Requires the global lock and loaded settings */ const MeshBackendApi* getSelectedBackend() { if (settings.backend[0] != '\0') { return findBackend(settings.backend); } return (backendCount > 0) ? backends[0] : nullptr; } size_t getPresetCount(const MeshBackendApi* api) { size_t count = 0; if (api->presets != nullptr) { while (api->presets[count] != nullptr) { count++; } } return count; } /** @return the index of the preset with @a name, or 0 (the default) when there is none */ size_t findPreset(const MeshBackendApi* api, const char* name) { const size_t count = getPresetCount(api); for (size_t i = 0; i < count; i++) { if (strcmp(api->presets[i], name) == 0) { return i; } } return 0; } void ensureSettingsLoaded() { if (!settingsLoaded) { mesh_settings_load(&settings); settingsLoaded = true; } } /** Requires the global lock */ void queueEvent(const MeshEvent& event) { for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) { if (sub->internal.count >= MESH_EVENT_QUEUE_CAPACITY) { continue; } const uint8_t tail = (sub->internal.head + sub->internal.count) % MESH_EVENT_QUEUE_CAPACITY; sub->internal.queue[tail] = event; sub->internal.count++; task_event_group_signal(sub->internal.event_group, sub->bit); } } /** Requires the global lock */ void emitEvent(MeshEventType type, MeshState state, uint32_t seq, MeshDeliveryState delivery) { queueEvent({ .timestamp = get_micros_since_boot(), .type = type, .state = state, .seq = seq, .delivery = delivery, .frame = {}, }); } /** Requires the global lock */ void emitFrame(MeshState state, MeshFrameKind kind, size_t length, float rssi, float snr, const MeshAddress* from, const char* name) { MeshEvent event {}; event.timestamp = get_micros_since_boot(); event.type = MESH_EVENT_FRAME; event.state = state; event.delivery = MESH_DELIVERY_RECEIVED; event.frame.kind = kind; event.frame.length = (uint16_t)length; event.frame.rssi = rssi; event.frame.snr = snr; if (from != nullptr) { event.frame.from = *from; } if (name != nullptr) { strncpy(event.frame.name, name, MESH_NAME_MAX_LENGTH); } queueEvent(event); } struct Frame { tt::std::vector data; uint32_t seq = 0; int busyRetries = 0; }; struct AirtimeRecord { uint64_t timeMs; uint32_t airtimeMs; }; } // namespace /** * The service instance. Fields marked "thread" are only touched by the service thread, the rest is * guarded by the global lock. */ struct MeshHost { MeshState state = MESH_STATE_OFF; MeshStore store; tt::std::vector nodes; char activeBackend[MESH_BACKEND_ID_MAX_LENGTH + 1] = {}; tt::std::deque> calls; // thread Device* radio = nullptr; bool radioAcquired = false; TaskEventGroup group {}; uint32_t stopBit = 0; uint32_t callBit = 0; RadioRxEventSubscription rxSub {}; RadioTxEventSubscription txSub {}; bool rxSubscribed = false; bool txSubscribed = false; Thread* thread = nullptr; std::atomic running { false }; const MeshBackendApi* api = nullptr; void* backend = nullptr; MeshRadioProfile profile {}; RadioRegion appliedRegion = RADIO_REGION_UNSET; tt::std::deque txQueue; bool txInFlight = false; RadioTxId txId = 0; Frame txFrame; tt::std::deque airtime; bool dutyCycleLogged = false; uint64_t lastTickMs = 0; // Synchronous calls EventGroupHandle_t callEvents = nullptr; }; namespace { MeshHost* service = nullptr; /** Requires the global lock */ void setState(MeshHost* host, MeshState state) { if (host->state != state) { LOG_I(TAG, "State: %s -> %s", mesh_state_to_string(host->state), mesh_state_to_string(state)); host->state = state; emitEvent(MESH_EVENT_STATE, state, 0, MESH_DELIVERY_RECEIVED); } } void setStateLocked(MeshHost* host, MeshState state) { lock(); setState(host, state); unlock(); } /** Requires the global lock */ void setDelivery(MeshHost* host, uint32_t seq, MeshDeliveryState delivery) { if (host->store.setDelivery(seq, delivery) == ERROR_NONE) { emitEvent(MESH_EVENT_DELIVERY, host->state, seq, delivery); } } /** Queue work for the service thread. Requires the global lock. */ void post(MeshHost* host, std::function work) { host->calls.push_back(std::move(work)); task_event_group_signal(&host->group, host->callBit); } /** * Run work on the service thread and wait for it. Must be called without the global lock and never from * the service thread. The thread drains all posted work before it exits, so the wait always ends. */ error_t callOnServiceThread(const std::function& work) { mutex_lock(&callMutex.handle); lock(); MeshHost* host = service; if (host == nullptr) { unlock(); mutex_unlock(&callMutex.handle); return ERROR_INVALID_STATE; } event_group_clear(host->callEvents, CALL_DONE_BIT); post(host, [host, &work] { work(host); event_group_set(host->callEvents, CALL_DONE_BIT); }); unlock(); event_group_wait(host->callEvents, CALL_DONE_BIT, false, true, nullptr, portMAX_DELAY); mutex_unlock(&callMutex.handle); return ERROR_NONE; } // region Radio and backend lifecycle (service thread) void radioOff(MeshHost* host) { if (host->radio != nullptr && host->radioAcquired) { radio_set_enabled(host->radio, false); } } void deactivateBackend(MeshHost* host) { if (host->api == nullptr) { return; } const char* id = host->api->id; host->api->destroy(host->backend); host->api = nullptr; host->backend = nullptr; host->txQueue.clear(); host->txInFlight = false; radioOff(host); lock(); // Messages that weren't sent wait for this backend to come back for (uint32_t seq : host->store.findOutgoing(id, MESH_DELIVERY_QUEUED)) { setDelivery(host, seq, MESH_DELIVERY_BLOCKED); } host->nodes.clear(); host->activeBackend[0] = '\0'; unlock(); } bool applyProfile(MeshHost* host) { const auto& profile = host->profile; radio_set_enabled(host->radio, false); if (radio_set_modulation(host->radio, profile.modulation) != ERROR_NONE) { LOG_E(TAG, "Radio doesn't support the backend's modulation"); return false; } const struct { RadioParameter parameter; int32_t value; } parameters[] = { { RADIO_PARAMETER_FREQUENCY, profile.frequency }, { RADIO_PARAMETER_BANDWIDTH, profile.bandwidth }, { RADIO_PARAMETER_SPREADING_FACTOR, profile.spreading_factor }, { RADIO_PARAMETER_CODING_RATE, profile.coding_rate }, { RADIO_PARAMETER_SYNC_WORD, profile.sync_word }, { RADIO_PARAMETER_PREAMBLE_LENGTH, profile.preamble_length }, { RADIO_PARAMETER_POWER, profile.power }, }; for (const auto& entry : parameters) { const error_t result = radio_set_parameter(host->radio, entry.parameter, entry.value); if (result != ERROR_NONE && result != ERROR_NOT_SUPPORTED) { LOG_E(TAG, "Setting radio parameter %d to %d failed: %d", (int)entry.parameter, (int)entry.value, (int)result); return false; } } // Listen-before-talk is optional, so a driver without it is fine radio_set_parameter(host->radio, RADIO_PARAMETER_LBT, 1); if (radio_set_enabled(host->radio, true) != ERROR_NONE) { LOG_E(TAG, "Failed to turn on the radio"); return false; } return true; } void resubmitPending(MeshHost* host); /** (Re)select the backend and configure the radio from the settings and the radio's region */ void configure(MeshHost* host) { deactivateBackend(host); if (host->radio == nullptr) { setStateLocked(host, MESH_STATE_NO_RADIO); return; } if (!host->radioAcquired) { host->radioAcquired = radio_acquire(host->radio, "mesh") == ERROR_NONE; if (!host->radioAcquired) { setStateLocked(host, MESH_STATE_RADIO_BUSY); return; } } lock(); const MeshBackendApi* api = getSelectedBackend(); const size_t preset = (api != nullptr) ? findPreset(api, settings.preset) : 0; unlock(); if (api == nullptr) { setStateLocked(host, MESH_STATE_NO_BACKEND); return; } host->appliedRegion = radio_get_region(host->radio); if (host->appliedRegion == RADIO_REGION_UNSET) { setStateLocked(host, MESH_STATE_NO_REGION); return; } // Set before create() because backends use host functions there host->api = api; void* backend = api->create(host); if (backend == nullptr) { host->api = nullptr; setStateLocked(host, MESH_STATE_ERROR); return; } if (api->get_radio_profile(backend, host->appliedRegion, preset, &host->profile) != ERROR_NONE) { LOG_E(TAG, "Backend %s doesn't support region %s with preset %u", api->id, radio_region_to_string(host->appliedRegion), (unsigned)preset); api->destroy(backend); host->api = nullptr; setStateLocked(host, MESH_STATE_ERROR); return; } host->backend = backend; if (!applyProfile(host)) { deactivateBackend(host); setStateLocked(host, MESH_STATE_ERROR); return; } lock(); strncpy(host->activeBackend, api->id, MESH_BACKEND_ID_MAX_LENGTH); host->activeBackend[MESH_BACKEND_ID_MAX_LENGTH] = '\0'; setState(host, MESH_STATE_ON); unlock(); LOG_I(TAG, "Backend %s active in region %s", api->id, radio_region_to_string(host->appliedRegion)); api->on_start(backend); resubmitPending(host); } void sendStored(MeshHost* host, uint32_t seq) { MeshMessage message; lock(); const bool found = host->store.get(seq, &message) == ERROR_NONE; unlock(); if (!found || message.delivery != MESH_DELIVERY_QUEUED) { return; } if (host->api == nullptr || strcmp(message.backend, host->api->id) != 0) { lock(); setDelivery(host, seq, MESH_DELIVERY_BLOCKED); unlock(); return; } const MeshAddress* to = (message.peer.length > 0) ? &message.peer : nullptr; if (host->api->send_text(host->backend, seq, to, message.channel, message.text) != ERROR_NONE) { lock(); setDelivery(host, seq, MESH_DELIVERY_FAILED); unlock(); } } void resubmitPending(MeshHost* host) { lock(); tt::std::vector pending = host->store.findOutgoing(host->api->id, MESH_DELIVERY_BLOCKED); for (uint32_t seq : pending) { setDelivery(host, seq, MESH_DELIVERY_QUEUED); } unlock(); for (uint32_t seq : pending) { sendStored(host, seq); } } // endregion // region TX (service thread) uint32_t getAirtimeUsedMs(MeshHost* host, uint64_t nowMs) { while (!host->airtime.empty() && nowMs - host->airtime.front().timeMs > DUTY_CYCLE_WINDOW_MS) { host->airtime.pop_front(); } uint32_t used = 0; for (const auto& record : host->airtime) { used += record.airtimeMs; } return used; } void pumpTx(MeshHost* host) { if (host->txInFlight || host->txQueue.empty() || host->api == nullptr) { return; } const auto& frame = host->txQueue.front(); const uint32_t airtimeMs = mesh_airtime_estimate_ms(host->profile, frame.data.size()); const uint32_t dutyCycle = mesh_airtime_duty_cycle_percent(host->appliedRegion, host->profile.frequency); const uint64_t nowMs = getMillis(); if (dutyCycle < 100) { const uint64_t budgetMs = DUTY_CYCLE_WINDOW_MS * dutyCycle / 100; if (getAirtimeUsedMs(host, nowMs) + airtimeMs > budgetMs) { if (!host->dutyCycleLogged) { LOG_W(TAG, "Duty cycle budget used up, holding %u frames", (unsigned)host->txQueue.size()); host->dutyCycleLogged = true; } return; } } host->dutyCycleLogged = false; host->txFrame = std::move(host->txQueue.front()); host->txQueue.pop_front(); if (radio_transmit(host->radio, host->txFrame.data.data(), host->txFrame.data.size(), &host->txId) != ERROR_NONE) { if (host->txFrame.seq != 0) { lock(); setDelivery(host, host->txFrame.seq, MESH_DELIVERY_FAILED); unlock(); } return; } host->txInFlight = true; host->airtime.push_back({ .timeMs = nowMs, .airtimeMs = airtimeMs }); } void onTxEvent(MeshHost* host, const RadioTxEvent& event) { if (!host->txInFlight || event.id != host->txId) { return; } const uint32_t seq = host->txFrame.seq; switch (event.state) { case RADIO_TRANSMISSION_STATE_TRANSMITTED: host->txInFlight = false; if (seq != 0) { lock(); MeshMessage message; if (host->store.get(seq, &message) == ERROR_NONE && message.delivery == MESH_DELIVERY_QUEUED) { setDelivery(host, seq, MESH_DELIVERY_SENT); } unlock(); } break; case RADIO_TRANSMISSION_STATE_CHANNEL_BUSY: host->txInFlight = false; if (host->txFrame.busyRetries < CHANNEL_BUSY_RETRIES) { host->txFrame.busyRetries++; host->txQueue.push_front(std::move(host->txFrame)); break; } [[fallthrough]]; case RADIO_TRANSMISSION_STATE_TIMEOUT: case RADIO_TRANSMISSION_STATE_ERROR: host->txInFlight = false; if (seq != 0) { lock(); setDelivery(host, seq, MESH_DELIVERY_FAILED); unlock(); } break; default: break; } } // endregion // region Service thread /** Takes the first LoRa radio and subscribes to its events. @return false when there is none */ bool attachRadio(MeshHost* host) { host->radio = findRadio(); if (host->radio == nullptr) { return false; } host->rxSubscribed = radio_rx_event_subscribe(&host->rxSub, &host->group, host->radio) == ERROR_NONE; host->txSubscribed = radio_tx_event_subscribe(&host->txSub, &host->group, host->radio) == ERROR_NONE; return true; } void drainRadioEvents(MeshHost* host) { if (host->rxSubscribed) { // Static: RadioRxEvent is large and only used on this thread static RadioRxEvent rxEvent; while (radio_rx_event_poll(&host->rxSub, &rxEvent) == ERROR_NONE) { if (host->api != nullptr) { if (!host->api->on_receive(host->backend, rxEvent.data, rxEvent.length, rxEvent.rssi, rxEvent.snr)) { lock(); emitFrame(host->state, MESH_FRAME_UNREADABLE, rxEvent.length, rxEvent.rssi, rxEvent.snr, nullptr, nullptr); unlock(); } } } } if (host->txSubscribed) { RadioTxEvent txEvent; while (radio_tx_event_poll(&host->txSub, &txEvent) == ERROR_NONE) { onTxEvent(host, txEvent); } } } void runCalls(MeshHost* host) { while (true) { lock(); if (host->calls.empty()) { unlock(); return; } auto work = std::move(host->calls.front()); host->calls.pop_front(); unlock(); work(); } } void tick(MeshHost* host) { // Radios can start after the service, e.g. once the hotplug poller detects an add-on if (host->radio == nullptr && attachRadio(host)) { LOG_I(TAG, "LoRa radio found"); configure(host); } if (host->radio != nullptr && host->radioAcquired && radio_get_region(host->radio) != host->appliedRegion) { LOG_I(TAG, "Region changed, reconfiguring"); configure(host); } RadioState radioState = RADIO_STATE_OFF; if (host->api != nullptr && radio_get_state(host->radio, &radioState) == ERROR_NONE && radioState == RADIO_STATE_ERROR) { LOG_E(TAG, "Radio failed"); deactivateBackend(host); setStateLocked(host, MESH_STATE_ERROR); } if (host->api != nullptr) { host->api->on_tick(host->backend); } lock(); host->store.flushIfNeeded(getMillis(), FLUSH_MAX_AGE_MS); unlock(); } int32_t threadMain(void* context) { auto* host = static_cast(context); configure(host); while (host->running) { uint32_t flags = 0; task_event_group_wait_any(&host->group, &flags, pdMS_TO_TICKS(TICK_INTERVAL_MS)); if (!host->running) { break; } drainRadioEvents(host); runCalls(host); const uint64_t nowMs = getMillis(); if (nowMs - host->lastTickMs >= TICK_INTERVAL_MS) { host->lastTickMs = nowMs; tick(host); } pumpTx(host); } runCalls(host); deactivateBackend(host); return 0; } // endregion // region Service lifecycle void* createService(const ServiceManifest*) { return tt::newObject(); } void destroyService(const ServiceManifest*, void* data) { tt::deleteObject(static_cast(data)); } void releaseResources(MeshHost* host) { if (host->rxSubscribed) { radio_rx_event_unsubscribe(&host->rxSub); host->rxSubscribed = false; } if (host->txSubscribed) { radio_tx_event_unsubscribe(&host->txSub); host->txSubscribed = false; } if (host->radio != nullptr) { if (host->radioAcquired) { radio_release(host->radio); host->radioAcquired = false; } device_put(host->radio); host->radio = nullptr; } task_event_group_destruct(&host->group); event_group_destruct(&host->callEvents); } error_t onStart(ServiceInstance*, void* data) { auto* host = static_cast(data); event_group_construct(&host->callEvents); task_event_group_construct(&host->group); if (task_event_group_claim_bit(&host->group, &host->stopBit) != ERROR_NONE || task_event_group_claim_bit(&host->group, &host->callBit) != ERROR_NONE) { releaseResources(host); return ERROR_RESOURCE; } char storePath[FILE_MAX_PATH_STRING_LENGTH]; if (service_paths_get_user_data_path(MESH_SERVICE_ID, "store", storePath, sizeof(storePath)) != ERROR_NONE || host->store.open(storePath) != ERROR_NONE) { LOG_E(TAG, "Failed to open the message store"); releaseResources(host); return ERROR_RESOURCE; } if (!attachRadio(host)) { LOG_W(TAG, "No LoRa radio found yet"); } lock(); ensureSettingsLoaded(); service = host; unlock(); host->running = true; host->thread = thread_alloc_full("mesh", THREAD_STACK_SIZE, threadMain, host, -1); if (host->thread == nullptr || thread_start(host->thread) != ERROR_NONE) { host->running = false; if (host->thread != nullptr) { thread_free(host->thread); host->thread = nullptr; } lock(); service = nullptr; unlock(); host->store.close(); releaseResources(host); return ERROR_RESOURCE; } return ERROR_NONE; } void onStop(ServiceInstance*, void* data) { auto* host = static_cast(data); // No new work can be posted from here on, and the thread drains what was posted before lock(); service = nullptr; unlock(); host->running = false; task_event_group_signal(&host->group, host->stopBit); thread_join(host->thread, portMAX_DELAY, pdMS_TO_TICKS(10)); thread_free(host->thread); host->thread = nullptr; lock(); host->store.close(); setState(host, MESH_STATE_OFF); unlock(); releaseResources(host); } // endregion } // namespace const ServiceManifest mesh_service_manifest = { .id = MESH_SERVICE_ID, .create_service = createService, .destroy_service = destroyService, .on_start = onStart, .on_stop = onStop, }; bool mesh_service_should_auto_start() { lock(); ensureSettingsLoaded(); const bool enabled = settings.enabled; unlock(); return enabled; } extern "C" { // region Backend registry error_t mesh_backend_register(const MeshBackendApi* api) { lock(); error_t result = ERROR_NONE; if (findBackend(api->id) != nullptr) { result = ERROR_ALREADY_EXISTS; } else if (backendCount >= MAX_BACKENDS) { result = ERROR_RESOURCE; } else { backends[backendCount++] = api; if (service != nullptr && service->state == MESH_STATE_NO_BACKEND) { MeshHost* host = service; post(host, [host] { configure(host); }); } } unlock(); return result; } error_t mesh_backend_unregister(const MeshBackendApi* api) { lock(); error_t result = ERROR_NOT_FOUND; for (size_t i = 0; i < backendCount; i++) { if (backends[i] == api) { backends[i] = backends[--backendCount]; backends[backendCount] = nullptr; result = ERROR_NONE; break; } } MeshHost* host = service; unlock(); // The service may be using it, so wait for the service thread to let go of it if (result == ERROR_NONE && host != nullptr) { callOnServiceThread([api](MeshHost* serviceHost) { if (serviceHost->api == api) { configure(serviceHost); } }); } return result; } // endregion // region Host functions error_t mesh_host_transmit(MeshHost* host, const uint8_t* data, size_t length, uint32_t seq) { if (length == 0 || length > RADIO_RX_MAX_PACKET_LENGTH) { return ERROR_OUT_OF_RANGE; } if (host->txQueue.size() >= TX_QUEUE_LIMIT) { return ERROR_RESOURCE; } host->txQueue.push_back(Frame { .data = tt::std::vector(data, data + length), .seq = seq, .busyRetries = 0 }); return ERROR_NONE; } uint32_t mesh_host_receive_text( MeshHost* host, const MeshAddress* from, uint8_t channel, const char* sender_name, const char* text, float rssi, float snr, int64_t sender_time ) { MeshMessage message {}; strncpy(message.backend, host->api->id, MESH_BACKEND_ID_MAX_LENGTH); message.outgoing = false; message.delivery = MESH_DELIVERY_RECEIVED; if (from != nullptr) { message.peer = *from; } message.channel = channel; if (sender_name != nullptr) { strncpy(message.sender_name, sender_name, MESH_NAME_MAX_LENGTH); } const int64_t now = (int64_t)time(nullptr); if (now >= MIN_VALID_WALL_TIME) { message.wall_time = now; message.time_source = MESH_TIME_SOURCE_SYSTEM; } else if (sender_time >= MIN_VALID_WALL_TIME) { message.wall_time = sender_time; message.time_source = MESH_TIME_SOURCE_MESH; } else { message.time_source = MESH_TIME_SOURCE_NONE; } message.uptime_ms = getMillis(); message.rssi = rssi; message.snr = snr; strncpy(message.text, text, MESH_TEXT_MAX_LENGTH); lock(); uint32_t seq = 0; if (host->store.add(&message) == ERROR_NONE) { seq = message.seq; emitEvent(MESH_EVENT_MESSAGE, host->state, seq, MESH_DELIVERY_RECEIVED); } unlock(); return seq; } void mesh_host_set_delivery(MeshHost* host, uint32_t seq, MeshDeliveryState delivery) { lock(); setDelivery(host, seq, delivery); unlock(); } void mesh_host_node_heard(MeshHost* host, const MeshAddress* address, const char* name, float rssi, float snr) { lock(); MeshNode* node = nullptr; for (auto& candidate : host->nodes) { if (candidate.address.length == address->length && memcmp(candidate.address.bytes, address->bytes, address->length) == 0) { node = &candidate; break; } } if (node == nullptr) { if (host->nodes.size() >= MAX_NODES) { // Replace the node that wasn't heard for the longest time auto oldest = host->nodes.begin(); for (auto iterator = host->nodes.begin(); iterator != host->nodes.end(); ++iterator) { if (iterator->last_heard_ms < oldest->last_heard_ms) { oldest = iterator; } } host->nodes.erase(oldest); } host->nodes.push_back({}); node = &host->nodes.back(); strncpy(node->backend, host->activeBackend, MESH_BACKEND_ID_MAX_LENGTH); node->address = *address; } if (name != nullptr) { strncpy(node->name, name, MESH_NAME_MAX_LENGTH); node->name[MESH_NAME_MAX_LENGTH] = '\0'; } node->last_heard_ms = getMillis(); node->rssi = rssi; node->snr = snr; emitEvent(MESH_EVENT_NODE, host->state, 0, MESH_DELIVERY_RECEIVED); unlock(); } void mesh_host_advert_heard(MeshHost* host, const MeshAddress* address, const char* name, float rssi, float snr) { mesh_host_node_heard(host, address, name, rssi, snr); lock(); emitFrame(host->state, MESH_FRAME_ADVERT, 0, rssi, snr, address, name); unlock(); } error_t mesh_host_get_data_path(MeshHost* host, const char* child_path, char* out_path, size_t out_path_size) { char directory[FILE_MAX_PATH_STRING_LENGTH]; char backendDirectory[16 + MESH_BACKEND_ID_MAX_LENGTH]; snprintf(backendDirectory, sizeof(backendDirectory), "backends/%s", host->api->id); if (service_paths_get_user_data_path(MESH_SERVICE_ID, backendDirectory, directory, sizeof(directory)) != ERROR_NONE) { return ERROR_BUFFER_OVERFLOW; } const error_t made = directory_make(directory, true); if (made != ERROR_NONE && made != ERROR_ALREADY_EXISTS) { return ERROR_RESOURCE; } const int written = snprintf(out_path, out_path_size, "%s/%s", directory, child_path); return (written < 0 || (size_t)written >= out_path_size) ? ERROR_BUFFER_OVERFLOW : ERROR_NONE; } void mesh_host_get_node_name(MeshHost*, char* name, size_t name_size) { lock(); ensureSettingsLoaded(); strncpy(name, settings.node_name, name_size - 1); name[name_size - 1] = '\0'; unlock(); } // endregion // region Public API const char* mesh_state_to_string(MeshState state) { switch (state) { case MESH_STATE_OFF: return "off"; case MESH_STATE_ON: return "on"; case MESH_STATE_NO_RADIO: return "no radio"; case MESH_STATE_RADIO_BUSY: return "radio busy"; case MESH_STATE_NO_REGION: return "no region"; case MESH_STATE_NO_BACKEND: return "no backend"; case MESH_STATE_ERROR: return "error"; default: return "unknown"; } } error_t mesh_set_enabled(bool enabled) { lock(); ensureSettingsLoaded(); settings.enabled = enabled; const MeshSettings copy = settings; unlock(); const error_t saved = mesh_settings_save(©); const error_t result = enabled ? service_manager_start(MESH_SERVICE_ID) : service_manager_stop(MESH_SERVICE_ID); return (result != ERROR_NONE) ? result : saved; } bool mesh_is_enabled(void) { lock(); ensureSettingsLoaded(); const bool enabled = settings.enabled; unlock(); return enabled; } MeshState mesh_get_state(void) { lock(); const MeshState state = (service != nullptr) ? service->state : MESH_STATE_OFF; unlock(); return state; } error_t mesh_set_node_name(const char* name) { if (name == nullptr || name[0] == '\0' || strlen(name) > MESH_NAME_MAX_LENGTH) { return ERROR_INVALID_ARGUMENT; } lock(); ensureSettingsLoaded(); strncpy(settings.node_name, name, MESH_NAME_MAX_LENGTH); settings.node_name[MESH_NAME_MAX_LENGTH] = '\0'; const MeshSettings copy = settings; unlock(); return mesh_settings_save(©); } error_t mesh_get_node_name(char* name, size_t name_size) { if (name_size == 0) { return ERROR_BUFFER_OVERFLOW; } mesh_host_get_node_name(nullptr, name, name_size); return ERROR_NONE; } bool mesh_is_available(void) { Device* radio = findRadio(); if (radio == nullptr) { return false; } device_put(radio); return true; } size_t mesh_get_backend_count(void) { lock(); const size_t count = backendCount; unlock(); return count; } error_t mesh_get_backend_id(size_t index, char* id, size_t id_size) { lock(); error_t result = ERROR_NOT_FOUND; if (index < backendCount && id_size > 0) { strncpy(id, backends[index]->id, id_size - 1); id[id_size - 1] = '\0'; result = ERROR_NONE; } unlock(); return result; } error_t mesh_set_backend(const char* id) { lock(); if (findBackend(id) == nullptr) { unlock(); return ERROR_NOT_FOUND; } ensureSettingsLoaded(); if (strncmp(settings.backend, id, MESH_BACKEND_ID_MAX_LENGTH) != 0) { settings.preset[0] = '\0'; } strncpy(settings.backend, id, MESH_BACKEND_ID_MAX_LENGTH); settings.backend[MESH_BACKEND_ID_MAX_LENGTH] = '\0'; const MeshSettings copy = settings; if (service != nullptr) { MeshHost* host = service; post(host, [host] { configure(host); }); } unlock(); return mesh_settings_save(©); } error_t mesh_get_backend(char* id, size_t id_size) { lock(); ensureSettingsLoaded(); const char* selected = settings.backend; if (service != nullptr && service->activeBackend[0] != '\0') { selected = service->activeBackend; } error_t result = ERROR_NOT_FOUND; if (selected[0] != '\0' && id_size > 0) { strncpy(id, selected, id_size - 1); id[id_size - 1] = '\0'; result = ERROR_NONE; } unlock(); return result; } size_t mesh_get_preset_count(void) { lock(); ensureSettingsLoaded(); const MeshBackendApi* api = getSelectedBackend(); const size_t count = (api != nullptr) ? getPresetCount(api) : 0; unlock(); return count; } error_t mesh_get_preset_name(size_t index, char* name, size_t name_size) { lock(); ensureSettingsLoaded(); const MeshBackendApi* api = getSelectedBackend(); error_t result = ERROR_NOT_FOUND; if (api != nullptr && index < getPresetCount(api) && name_size > 0) { strncpy(name, api->presets[index], name_size - 1); name[name_size - 1] = '\0'; result = ERROR_NONE; } unlock(); return result; } error_t mesh_set_preset(const char* name) { lock(); ensureSettingsLoaded(); const MeshBackendApi* api = getSelectedBackend(); const size_t index = (api != nullptr) ? findPreset(api, name) : 0; if (api == nullptr || index >= getPresetCount(api) || strcmp(api->presets[index], name) != 0) { unlock(); return ERROR_NOT_FOUND; } strncpy(settings.preset, name, MESH_NAME_MAX_LENGTH); settings.preset[MESH_NAME_MAX_LENGTH] = '\0'; const MeshSettings copy = settings; if (service != nullptr) { MeshHost* host = service; post(host, [host] { configure(host); }); } unlock(); return mesh_settings_save(©); } error_t mesh_get_preset(char* name, size_t name_size) { lock(); ensureSettingsLoaded(); const MeshBackendApi* api = getSelectedBackend(); error_t result = ERROR_NOT_FOUND; if (api != nullptr && getPresetCount(api) > 0 && name_size > 0) { strncpy(name, api->presets[findPreset(api, settings.preset)], name_size - 1); name[name_size - 1] = '\0'; result = ERROR_NONE; } unlock(); return result; } error_t mesh_send_text(const MeshAddress* to, uint8_t channel, const char* text, uint32_t* seq) { if (text == nullptr || strlen(text) > MESH_TEXT_MAX_LENGTH) { return ERROR_OUT_OF_RANGE; } lock(); MeshHost* host = service; if (host == nullptr || host->state != MESH_STATE_ON) { unlock(); return ERROR_INVALID_STATE; } MeshMessage message {}; strncpy(message.backend, host->activeBackend, MESH_BACKEND_ID_MAX_LENGTH); message.outgoing = true; message.delivery = MESH_DELIVERY_QUEUED; if (to != nullptr) { message.peer = *to; } message.channel = channel; strncpy(message.sender_name, settings.node_name, MESH_NAME_MAX_LENGTH); const int64_t now = (int64_t)time(nullptr); message.wall_time = (now >= MIN_VALID_WALL_TIME) ? now : 0; message.time_source = (now >= MIN_VALID_WALL_TIME) ? MESH_TIME_SOURCE_SYSTEM : MESH_TIME_SOURCE_NONE; message.uptime_ms = getMillis(); message.snr = NAN; strncpy(message.text, text, MESH_TEXT_MAX_LENGTH); const error_t result = host->store.add(&message); if (result == ERROR_NONE) { emitEvent(MESH_EVENT_MESSAGE, host->state, message.seq, MESH_DELIVERY_QUEUED); const uint32_t messageSeq = message.seq; post(host, [host, messageSeq] { sendStored(host, messageSeq); }); if (seq != nullptr) { *seq = message.seq; } } unlock(); return result; } error_t mesh_cancel(uint32_t seq) { lock(); MeshHost* host = service; if (host == nullptr) { unlock(); return ERROR_INVALID_STATE; } MeshMessage message; if (host->store.get(seq, &message) != ERROR_NONE || !message.outgoing) { unlock(); return ERROR_NOT_FOUND; } if (message.delivery != MESH_DELIVERY_QUEUED && message.delivery != MESH_DELIVERY_BLOCKED) { unlock(); return ERROR_INVALID_STATE; } setDelivery(host, seq, MESH_DELIVERY_CANCELLED); post(host, [host, seq] { std::erase_if(host->txQueue, [seq](const Frame& frame) { return frame.seq == seq; }); }); unlock(); return ERROR_NONE; } error_t mesh_raw_send(const char* backend, const uint8_t* data, size_t length) { if (length == 0 || length > RADIO_RX_MAX_PACKET_LENGTH) { return ERROR_OUT_OF_RANGE; } // The caller's buffers stay valid during this synchronous call error_t result = ERROR_INVALID_STATE; const error_t callResult = callOnServiceThread([&](MeshHost* host) { if (host->api != nullptr && strcmp(backend, host->api->id) == 0) { result = mesh_host_transmit(host, data, length, 0); } }); return (callResult != ERROR_NONE) ? callResult : result; } error_t mesh_message_get(uint32_t seq, MeshMessage* message) { lock(); const error_t result = (service != nullptr) ? service->store.get(seq, message) : ERROR_INVALID_STATE; unlock(); return result; } error_t mesh_history_get(uint32_t before_seq, MeshMessage* messages, size_t max_count, size_t* count) { lock(); error_t result = ERROR_INVALID_STATE; if (service != nullptr) { *count = service->store.getHistory(before_seq, messages, max_count); result = ERROR_NONE; } unlock(); return result; } error_t mesh_get_nodes(MeshNode* nodes, size_t max_count, size_t* count) { lock(); error_t result = ERROR_INVALID_STATE; if (service != nullptr) { *count = std::min(max_count, service->nodes.size()); std::copy_n(service->nodes.begin(), *count, nodes); result = ERROR_NONE; } unlock(); return result; } error_t mesh_get_channels(MeshChannel* channels, size_t max_count, size_t* count) { error_t result = ERROR_INVALID_STATE; const error_t callResult = callOnServiceThread([&](MeshHost* host) { if (host->api == nullptr) { return; } result = (host->api->get_channels != nullptr) ? host->api->get_channels(host->backend, channels, max_count, count) : ERROR_NOT_SUPPORTED; }); return (callResult != ERROR_NONE) ? callResult : result; } error_t mesh_set_channel(const MeshChannel* channel) { error_t result = ERROR_INVALID_STATE; const error_t callResult = callOnServiceThread([&](MeshHost* host) { if (host->api == nullptr) { return; } result = (host->api->set_channel != nullptr) ? host->api->set_channel(host->backend, channel) : ERROR_NOT_SUPPORTED; }); return (callResult != ERROR_NONE) ? callResult : result; } // endregion // region Events error_t mesh_event_subscribe(MeshEventSubscription* sub, TaskEventGroup* event_group) { uint32_t bit; const error_t claimed = task_event_group_claim_bit(event_group, &bit); if (claimed != ERROR_NONE) { return claimed; } lock(); for (auto* existing = subscriptions; existing != nullptr; existing = existing->internal.next) { if (existing == sub) { unlock(); task_event_group_release_bit(event_group, bit); return ERROR_INVALID_STATE; } } sub->bit = bit; sub->internal.event_group = event_group; sub->internal.head = 0; sub->internal.count = 0; sub->internal.next = subscriptions; subscriptions = sub; unlock(); return ERROR_NONE; } error_t mesh_event_unsubscribe(MeshEventSubscription* sub) { error_t result = ERROR_NOT_FOUND; lock(); for (auto** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) { if (*link == sub) { *link = sub->internal.next; result = ERROR_NONE; break; } } unlock(); if (result == ERROR_NONE) { task_event_group_release_bit(sub->internal.event_group, sub->bit); } return result; } error_t mesh_event_poll(MeshEventSubscription* sub, MeshEvent* event) { lock(); const bool hasEvent = sub->internal.count > 0; if (hasEvent) { *event = sub->internal.queue[sub->internal.head]; sub->internal.head = (sub->internal.head + 1) % MESH_EVENT_QUEUE_CAPACITY; sub->internal.count--; } unlock(); return hasEvent ? ERROR_NONE : ERROR_TIMEOUT; } // endregion } // extern "C"