mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-18 07:55:06 +00:00
337 lines
11 KiB
C++
337 lines
11 KiB
C++
#ifdef ESP_PLATFORM
|
|
#include <sdkconfig.h>
|
|
#endif
|
|
|
|
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
|
|
|
#include <esp_wifi.h> // for WIFI_TASK_CORE_ID
|
|
|
|
#include <tactility/concurrent/dispatcher.h>
|
|
#include <tactility/concurrent/event_group.h>
|
|
#include <tactility/concurrent/thread.h>
|
|
#include <tactility/device.h>
|
|
#include <tactility/driver.h>
|
|
#include <tactility/drivers/esp32_wifi_pinned.h>
|
|
#include <tactility/drivers/wifi.h>
|
|
#include <tactility/log.h>
|
|
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <new>
|
|
|
|
#define TAG "esp32_wifi_pinned"
|
|
|
|
namespace {
|
|
|
|
constexpr uint32_t CALL_DONE_FLAG = 1U;
|
|
constexpr configSTACK_DEPTH_TYPE PINNED_THREAD_STACK_SIZE = 4096;
|
|
|
|
// This driver wraps "espressif,esp32-wifi" (created as a child device) and
|
|
// marshals every call that mutates WiFi state onto a dedicated task pinned to
|
|
// WIFI_TASK_CORE_ID. ESP-IDF's WiFi driver misbehaves (hangs waiting on its
|
|
// internal task semaphore, tripping the watchdog) when those calls originate
|
|
// from an arbitrary caller task/core, so this driver guarantees the correct
|
|
// core regardless of who calls the WifiApi.
|
|
struct Esp32WifiPinnedCtx {
|
|
Device* device = nullptr;
|
|
Device* child = nullptr;
|
|
Thread* thread = nullptr;
|
|
DispatcherHandle_t dispatcher = nullptr;
|
|
volatile bool running = false;
|
|
};
|
|
|
|
#define GET_CTX(device) (static_cast<Esp32WifiPinnedCtx*>(device_get_driver_data(device)))
|
|
|
|
struct MarshalledCall {
|
|
const std::function<void()>* work;
|
|
EventGroupHandle_t done;
|
|
};
|
|
|
|
void marshal_trampoline(void* context) {
|
|
auto* call = static_cast<MarshalledCall*>(context);
|
|
(*call->work)();
|
|
event_group_set(call->done, CALL_DONE_FLAG);
|
|
}
|
|
|
|
/** Runs `work` synchronously on the pinned WiFi task, blocking the caller until it completes. */
|
|
error_t run_on_pinned_thread(Esp32WifiPinnedCtx* ctx, const std::function<void()>& work) {
|
|
if (ctx == nullptr || ctx->thread == nullptr) {
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
|
|
// Avoid deadlocking when already running on the pinned thread (e.g. during device_start()).
|
|
if (thread_get_current() == ctx->thread) {
|
|
work();
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
MarshalledCall call = { .work = &work, .done = nullptr };
|
|
event_group_construct(&call.done);
|
|
|
|
error_t err = dispatcher_dispatch(ctx->dispatcher, &call, marshal_trampoline);
|
|
if (err != ERROR_NONE) {
|
|
event_group_destruct(&call.done);
|
|
return err;
|
|
}
|
|
|
|
event_group_wait(call.done, CALL_DONE_FLAG, false, true, nullptr, portMAX_DELAY);
|
|
event_group_destruct(&call.done);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
void request_stop(void* context) {
|
|
static_cast<Esp32WifiPinnedCtx*>(context)->running = false;
|
|
}
|
|
|
|
int32_t pinned_thread_main(void* context) {
|
|
auto* ctx = static_cast<Esp32WifiPinnedCtx*>(context);
|
|
while (ctx->running) {
|
|
dispatcher_consume(ctx->dispatcher);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ---- Work that must run on the pinned thread (see start_device/stop_device) ----
|
|
|
|
error_t start_child_work(Esp32WifiPinnedCtx* ctx) {
|
|
ctx->child = new(std::nothrow) Device();
|
|
if (ctx->child == nullptr) {
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
std::memset(ctx->child, 0, sizeof(Device));
|
|
ctx->child->parent = ctx->device;
|
|
ctx->child->name = ctx->device->name;
|
|
|
|
error_t err = device_construct_add_start(ctx->child, "espressif,esp32-wifi");
|
|
if (err != ERROR_NONE) {
|
|
LOG_E(TAG, "%s: failed to start child wifi device: %s", ctx->device->name, error_to_string(err));
|
|
delete ctx->child;
|
|
ctx->child = nullptr;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
void stop_child_work(Esp32WifiPinnedCtx* ctx) {
|
|
if (ctx->child == nullptr) {
|
|
return;
|
|
}
|
|
if (device_is_ready(ctx->child)) {
|
|
device_stop(ctx->child);
|
|
}
|
|
if (device_is_added(ctx->child)) {
|
|
device_remove(ctx->child);
|
|
}
|
|
device_destruct(ctx->child);
|
|
delete ctx->child;
|
|
ctx->child = nullptr;
|
|
}
|
|
|
|
// ---- WifiApi: read-only calls forward straight to the child, no core affinity needed ----
|
|
|
|
error_t api_get_radio_state(Device* device, WifiRadioState* state) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_get_radio_state(ctx->child, state);
|
|
}
|
|
|
|
error_t api_get_station_state(Device* device, WifiStationState* state) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_get_station_state(ctx->child, state);
|
|
}
|
|
|
|
error_t api_get_access_point_state(Device* device, WifiAccessPointState* state) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_get_access_point_state(ctx->child, state);
|
|
}
|
|
|
|
bool api_is_scanning(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return false;
|
|
return wifi_is_scanning(ctx->child);
|
|
}
|
|
|
|
error_t api_get_scan_results(Device* device, WifiApRecord* results, size_t* num_results) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_get_scan_results(ctx->child, results, num_results);
|
|
}
|
|
|
|
error_t api_station_get_ipv4_address(Device* device, char* ipv4) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_station_get_ipv4_address(ctx->child, ipv4);
|
|
}
|
|
|
|
error_t api_station_get_target_ssid(Device* device, char* ssid) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_station_get_target_ssid(ctx->child, ssid);
|
|
}
|
|
|
|
error_t api_station_get_rssi(Device* device, int32_t* rssi) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_station_get_rssi(ctx->child, rssi);
|
|
}
|
|
|
|
error_t api_add_event_callback(Device* device, void* callback_context, WifiEventCallback callback) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_add_event_callback(ctx->child, callback_context, callback);
|
|
}
|
|
|
|
error_t api_remove_event_callback(Device* device, WifiEventCallback callback) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
return wifi_remove_event_callback(ctx->child, callback);
|
|
}
|
|
|
|
// ---- WifiApi: state-changing calls are marshalled onto the pinned WiFi task ----
|
|
|
|
error_t api_scan(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
|
|
error_t result = ERROR_NONE;
|
|
Device* child = ctx->child;
|
|
error_t err = run_on_pinned_thread(ctx, [child, &result]() {
|
|
result = wifi_scan(child);
|
|
});
|
|
return err != ERROR_NONE ? err : result;
|
|
}
|
|
|
|
error_t api_station_connect(Device* device, const char* ssid, const char* password, int32_t channel) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
|
|
error_t result = ERROR_NONE;
|
|
Device* child = ctx->child;
|
|
error_t err = run_on_pinned_thread(ctx, [child, ssid, password, channel, &result]() {
|
|
result = wifi_station_connect(child, ssid, password, channel);
|
|
});
|
|
return err != ERROR_NONE ? err : result;
|
|
}
|
|
|
|
error_t api_station_disconnect(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE;
|
|
|
|
error_t result = ERROR_NONE;
|
|
Device* child = ctx->child;
|
|
error_t err = run_on_pinned_thread(ctx, [child, &result]() {
|
|
result = wifi_station_disconnect(child);
|
|
});
|
|
return err != ERROR_NONE ? err : result;
|
|
}
|
|
|
|
const WifiApi esp32_wifi_pinned_api = {
|
|
.get_radio_state = api_get_radio_state,
|
|
.get_station_state = api_get_station_state,
|
|
.get_access_point_state = api_get_access_point_state,
|
|
.is_scanning = api_is_scanning,
|
|
.scan = api_scan,
|
|
.get_scan_results = api_get_scan_results,
|
|
.station_get_ipv4_address = api_station_get_ipv4_address,
|
|
.station_get_target_ssid = api_station_get_target_ssid,
|
|
.station_connect = api_station_connect,
|
|
.station_disconnect = api_station_disconnect,
|
|
.station_get_rssi = api_station_get_rssi,
|
|
.add_event_callback = api_add_event_callback,
|
|
.remove_event_callback = api_remove_event_callback
|
|
};
|
|
|
|
// ---- Driver lifecycle ----
|
|
// Starting/stopping the child (which brings the ESP-IDF WiFi stack up/down)
|
|
// also happens on the pinned thread, since esp_wifi_init/start/stop/deinit
|
|
// are subject to the same core-affinity requirement as the state-changing
|
|
// WifiApi calls above.
|
|
|
|
error_t start_device(Device* device) {
|
|
auto* ctx = new(std::nothrow) Esp32WifiPinnedCtx();
|
|
if (ctx == nullptr) return ERROR_OUT_OF_MEMORY;
|
|
ctx->device = device;
|
|
device_set_driver_data(device, ctx);
|
|
|
|
ctx->dispatcher = dispatcher_alloc();
|
|
ctx->running = true;
|
|
ctx->thread = thread_alloc_full(
|
|
"esp32_wifi_pinned",
|
|
PINNED_THREAD_STACK_SIZE,
|
|
pinned_thread_main,
|
|
ctx,
|
|
WIFI_TASK_CORE_ID
|
|
);
|
|
if (ctx->thread == nullptr || thread_start(ctx->thread) != ERROR_NONE) {
|
|
LOG_E(TAG, "%s: failed to start pinned wifi task", device->name);
|
|
if (ctx->thread != nullptr) thread_free(ctx->thread);
|
|
dispatcher_free(ctx->dispatcher);
|
|
device_set_driver_data(device, nullptr);
|
|
delete ctx;
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
error_t result = ERROR_NONE;
|
|
error_t err = run_on_pinned_thread(ctx, [ctx, &result]() {
|
|
result = start_child_work(ctx);
|
|
});
|
|
if (err == ERROR_NONE) {
|
|
err = result;
|
|
}
|
|
|
|
if (err != ERROR_NONE) {
|
|
dispatcher_dispatch(ctx->dispatcher, ctx, request_stop);
|
|
thread_join(ctx->thread, portMAX_DELAY, 10);
|
|
thread_free(ctx->thread);
|
|
dispatcher_free(ctx->dispatcher);
|
|
device_set_driver_data(device, nullptr);
|
|
delete ctx;
|
|
return err;
|
|
}
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t stop_device(Device* device) {
|
|
auto* ctx = GET_CTX(device);
|
|
if (ctx == nullptr) return ERROR_NONE;
|
|
|
|
error_t err = run_on_pinned_thread(ctx, [ctx]() {
|
|
stop_child_work(ctx);
|
|
});
|
|
if (err != ERROR_NONE) {
|
|
LOG_E(TAG, "%s: failed to stop child wifi device on pinned task: %s", device->name, error_to_string(err));
|
|
}
|
|
|
|
dispatcher_dispatch(ctx->dispatcher, ctx, request_stop);
|
|
thread_join(ctx->thread, portMAX_DELAY, 10);
|
|
thread_free(ctx->thread);
|
|
dispatcher_free(ctx->dispatcher);
|
|
|
|
device_set_driver_data(device, nullptr);
|
|
delete ctx;
|
|
return err;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
extern Module platform_esp32_module;
|
|
|
|
Driver esp32_wifi_pinned_driver = {
|
|
.name = "esp32_wifi_pinned",
|
|
.compatible = (const char*[]) { "espressif,esp32-wifi-pinned", nullptr },
|
|
.start_device = start_device,
|
|
.stop_device = stop_device,
|
|
.api = (const void*)&esp32_wifi_pinned_api,
|
|
.device_type = &WIFI_TYPE,
|
|
.owner = &platform_esp32_module,
|
|
.internal = nullptr
|
|
};
|
|
|
|
} // extern "C"
|
|
|
|
#endif // CONFIG_SOC_WIFI_SUPPORTED or CONFIG_SLAVE_SOC_WIFI_SUPPORTED
|