diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d9ad760a..a25537784 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ option(AE_FILTRATION "Build aether in filtration mode" Off) option(AE_INSTALL "Install aether" ${AE_ROOT_PORJECT}) option(AE_BUILD_TOOLS "Build tools" ${AE_ROOT_PORJECT}) option(AE_BUILD_EXAMPLES "Build examples" ${AE_ROOT_PORJECT}) +option(AE_BUILD_PREPARED_TEST_IOT "Build Windows prepared LTE IoT example" OFF) option(AE_BUILD_TESTS "Build tests" ${AE_ROOT_PORJECT}) option(AE_BUILD_ANDROID_SMOKE "Build Android NDK smoke shared library and runner" Off) option(AE_ADDRESS_SANITIZE "Enable address sanitizer" Off) @@ -177,6 +178,10 @@ CPMAddPackage( EXCLUDE_FROM_ALL FALSE ) +if(MSVC) + target_compile_options(aether-objects PRIVATE /Zc:preprocessor) +endif() + add_library(${TARGET_NAME} STATIC ) # collect sources add_subdirectory(aether) @@ -225,7 +230,8 @@ if(ESP_PLATFORM) idf::esp_netif idf::nvs_flash idf::spiffs - idf::esp_driver_uart) + idf::esp_driver_uart + idf::esp_driver_gpio) # test if they exists foreach(c ${idf_components}) @@ -358,6 +364,10 @@ if(AE_BUILD_TESTS) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tests ${CMAKE_BINARY_DIR}/tests) endif() +if(AE_BUILD_PREPARED_TEST_IOT) + add_subdirectory(examples/prepared_test_iot) +endif() + if(ANDROID AND AE_BUILD_ANDROID_SMOKE) message(STATUS "Aether builds Android NDK smoke targets!") # Shared-library consumers require PIC for aether and static dependencies. diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 000000000..6e01a9d54 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,151 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ + "msvc_x64_x64" + ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "-j 4", + "ctestCommandArgs": "" + }, + { + "name": "x64-Release", + "generator": "Ninja", + "configurationType": "RelWithDebInfo", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "-j 4", + "ctestCommandArgs": "", + "inheritEnvironments": [ + "msvc_x64_x64" + ], + "variables": [] + }, + { + "name": "prepared-iot-sim7070", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ + "msvc_x64_x64" + ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "-j 4", + "ctestCommandArgs": "-R \"prepared|serial-port|modem-shutdown\" --output-on-failure", + "variables": [ + { + "name": "AE_BUILD_PREPARED_TEST_IOT", + "value": "ON", + "type": "BOOL" + }, + { + "name": "AE_BUILD_EXAMPLES", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "AE_BUILD_TOOLS", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "AE_INSTALL", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "AE_BUILD_TESTS", + "value": "ON", + "type": "BOOL" + }, + { + "name": "AE_DISTILLATION", + "value": "ON", + "type": "BOOL" + }, + { + "name": "AE_FILTRATION", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "CMAKE_EXPORT_COMPILE_COMMANDS", + "value": "ON", + "type": "BOOL" + }, + { + "name": "USER_CONFIG", + "value": "${projectDir}/examples/prepared_test_iot/user_config_sim7070g.h", + "type": "PATH" + } + ] + }, + { + "name": "prepared-iot-thingy91x", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ + "msvc_x64_x64" + ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "-j 4", + "ctestCommandArgs": "-R \"prepared|serial-port|modem-shutdown\" --output-on-failure", + "variables": [ + { + "name": "AE_BUILD_PREPARED_TEST_IOT", + "value": "ON", + "type": "BOOL" + }, + { + "name": "AE_BUILD_EXAMPLES", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "AE_BUILD_TOOLS", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "AE_INSTALL", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "AE_BUILD_TESTS", + "value": "ON", + "type": "BOOL" + }, + { + "name": "AE_DISTILLATION", + "value": "ON", + "type": "BOOL" + }, + { + "name": "AE_FILTRATION", + "value": "OFF", + "type": "BOOL" + }, + { + "name": "CMAKE_EXPORT_COMPILE_COMMANDS", + "value": "ON", + "type": "BOOL" + }, + { + "name": "USER_CONFIG", + "value": "${projectDir}/examples/prepared_test_iot/user_config_thingy91x.h", + "type": "PATH" + } + ] + } + ] +} diff --git a/CppProperties.json b/CppProperties.json new file mode 100644 index 000000000..f598ba59c --- /dev/null +++ b/CppProperties.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + { + "inheritEnvironments": [ + "msvc_x64" + ], + "name": "x64-Debug", + "includePath": [ + "${env.INCLUDE}", + "${workspaceRoot}\\**" + ], + "defines": [ + "WIN32", + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "intelliSenseMode": "windows-msvc-x64" + } + ] +} \ No newline at end of file diff --git a/aether/CMakeLists.txt b/aether/CMakeLists.txt index 586300b84..bb2b36742 100644 --- a/aether/CMakeLists.txt +++ b/aether/CMakeLists.txt @@ -17,6 +17,7 @@ cmake_minimum_required(VERSION 3.16.0) list(APPEND aether_srcs "aether_app.cpp" "aether.cpp" + "escaped_io_stream_trap.cpp" "adapter_registry.cpp" "client_connectivity_policy.cpp" "client.cpp" diff --git a/aether/adapters/adapter.h b/aether/adapters/adapter.h index fa892352f..e2796ca91 100644 --- a/aether/adapters/adapter.h +++ b/aether/adapters/adapter.h @@ -20,11 +20,34 @@ #include #include "aether-objects/obj/obj.h" +#include "aether/actions/action.h" +#include "aether/events/event_subscription.h" #include "aether/events/events.h" #include "aether/access_points/access_point.h" namespace ae { +/// Runtime shutdown operation owned by an adapter, never serialized. +class IAdapterStop : public Action {}; + +/// Completes immediately or follows a driver-owned shutdown action. +class AdapterStop final : public IAdapterStop { + public: + AdapterStop() { Finish(); } + explicit AdapterStop(Action& action) { + if (action.is_finished()) { + Finish(); + return; + } + finished_sub_ = action.finished_event().Subscribe([this]() { + finished_sub_.Reset(); + Finish(); + }); + } + + private: + Subscription finished_sub_; +}; /** * \brief The interface to control network adapter. * It must configure interface and provide list of access points. @@ -46,6 +69,10 @@ class Adapter : public Obj { virtual std::vector access_points() = 0; + // Stop runtime network resources. The adapter owns the returned action and + // keeps it alive until destruction, including after completion. + virtual IAdapterStop& Stop() = 0; + virtual NewAccessPoint::Subscriber new_access_point(); protected: diff --git a/aether/adapters/ethernet.h b/aether/adapters/ethernet.h index b5b5621d6..5b7b0ce90 100644 --- a/aether/adapters/ethernet.h +++ b/aether/adapters/ethernet.h @@ -47,8 +47,11 @@ class EthernetAdapter final : public Adapter { AE_OBJECT_REFLECT(AE_MMBRS(ethernet_access_point_)) std::vector access_points() override; + // This adapter currently has no asynchronous shutdown sequence. + IAdapterStop& Stop() override { return stop_operation_; } private: + AdapterStop stop_operation_; EthernetAccessPoint::ptr ethernet_access_point_; }; } // namespace ae diff --git a/aether/adapters/lora_module_adapter.h b/aether/adapters/lora_module_adapter.h index f1945173e..00b7d67ac 100644 --- a/aether/adapters/lora_module_adapter.h +++ b/aether/adapters/lora_module_adapter.h @@ -24,9 +24,9 @@ # include "aether/events/events.h" -# include "aether/lora_modules/ilora_module_driver.h" -# include "aether/adapters/parent_lora_module.h" # include "aether/access_points/access_point.h" +# include "aether/adapters/parent_lora_module.h" +# include "aether/lora_modules/ilora_module_driver.h" # define LORA_MODULE_TCP_TRANSPORT_ENABLED 1 @@ -47,10 +47,13 @@ class LoraModuleAdapter : public ParentLoraModuleAdapter { AE_OBJECT_REFLECT(AE_MMBRS(access_point_)) std::vector access_points() override; + // This adapter currently has no asynchronous shutdown sequence. + IAdapterStop& Stop() override { return stop_operation_; } ILoraModuleDriver& lora_module_driver(); private: + AdapterStop stop_operation_; bool connected_{false}; std::unique_ptr lora_module_driver_; diff --git a/aether/adapters/modem_adapter.cpp b/aether/adapters/modem_adapter.cpp index 65135994d..671ed30fd 100644 --- a/aether/adapters/modem_adapter.cpp +++ b/aether/adapters/modem_adapter.cpp @@ -64,5 +64,17 @@ IModemDriver& ModemAdapter::modem_driver() { return *modem_driver_; } +IAdapterStop& ModemAdapter::Stop() { + if (!stop_operation_) { + auto* operation = modem_driver_ ? modem_driver_->Stop() : nullptr; + if (operation != nullptr) { + stop_operation_.emplace(*operation); + } else { + stop_operation_.emplace(); + } + } + return *stop_operation_; +} + } // namespace ae #endif diff --git a/aether/adapters/modem_adapter.h b/aether/adapters/modem_adapter.h index b53d31ad3..ee1f7439c 100644 --- a/aether/adapters/modem_adapter.h +++ b/aether/adapters/modem_adapter.h @@ -20,8 +20,9 @@ #include "aether/config.h" #if AE_SUPPORT_MODEMS -# include "aether/adapters/parent_modem.h" +# include # include "aether/access_points/access_point.h" +# include "aether/adapters/parent_modem.h" namespace ae { class IModemDriver; @@ -42,6 +43,7 @@ class ModemAdapter final : public ParentModemAdapter { AE_OBJECT_REFLECT(AE_MMBRS(access_point_)) std::vector access_points() override; + IAdapterStop& Stop() override; IModemDriver& modem_driver(); @@ -50,6 +52,7 @@ class ModemAdapter final : public ParentModemAdapter { std::unique_ptr modem_driver_; AccessPoint::ptr access_point_; + std::optional stop_operation_; }; } // namespace ae diff --git a/aether/adapters/wifi_adapter.h b/aether/adapters/wifi_adapter.h index 79034816b..589b017f0 100644 --- a/aether/adapters/wifi_adapter.h +++ b/aether/adapters/wifi_adapter.h @@ -23,9 +23,9 @@ #if AE_SUPPORT_WIFIS -# include "aether/wifi/wifi_driver.h" -# include "aether/adapters/parent_wifi.h" # include "aether/access_points/wifi_access_point.h" +# include "aether/adapters/parent_wifi.h" +# include "aether/wifi/wifi_driver.h" namespace ae { @@ -42,10 +42,13 @@ class WifiAdapter final : public ParentWifiAdapter { AE_OBJECT_REFLECT(AE_MMBRS(access_points_)) std::vector access_points() override; + // This adapter currently has no asynchronous shutdown sequence. + IAdapterStop& Stop() override { return stop_operation_; } WifiDriver& driver(); private: + AdapterStop stop_operation_; std::unique_ptr wifi_driver_; std::vector access_points_; }; diff --git a/aether/aether.cpp b/aether/aether.cpp index 98f11bd53..a1e5f18fc 100644 --- a/aether/aether.cpp +++ b/aether/aether.cpp @@ -39,6 +39,15 @@ Aether::Aether(ObjProp prop) Aether::~Aether() { AE_TELE_DEBUG(AetherDestroyed); } +void Aether::StopNetworking() { + for (auto& [id, client] : clients_) { + if (client.is_loaded()) { + Ptr loaded = client.Load(); + loaded->StopNetworking(); + } + } +} + AeCtx Aether::ToAeContext() const { static constexpr AeCtxTable ae_table{ [](void* obj) -> Aether& { return *static_cast(obj); }, diff --git a/aether/aether.h b/aether/aether.h index f5d72b9ab..2472b4ac4 100644 --- a/aether/aether.h +++ b/aether/aether.h @@ -73,6 +73,7 @@ class Aether : public Obj { // AeContext protocol AeCtx ToAeContext() const; + void StopNetworking(); ObjPtr CreateClient(ClientConfig const& config, std::string const& client_id); diff --git a/aether/aether_app.cpp b/aether/aether_app.cpp index 1d5bd2884..5b288a79a 100644 --- a/aether/aether_app.cpp +++ b/aether/aether_app.cpp @@ -16,6 +16,7 @@ #include "aether/aether_app.h" +#include #include #include @@ -27,6 +28,7 @@ #include "aether/adapters/wifi_adapter.h" #include "aether/crypto.h" #include "aether/crypto/key.h" +#include "aether/escaped_io_stream_trap.h" #include "aether/global_ids.h" #include "aether/poller/epoll_poller.h" #include "aether/poller/freertos_poller.h" @@ -50,14 +52,15 @@ void AetherAppContext::TelemetryInit() { # if AE_TELE_LOG_CONSOLE && AE_TELE_LOG_TO_STATISTICS // telemetry to both console and statistics tele_statistics_trap_is_set = true; - auto trap = std::make_shared< - ae::tele::ProxyTrap>( - std::make_shared(std::cout), - std::make_shared()); + auto trap = + std::make_shared>( + std::make_shared(std::cout), + std::make_shared()); # elif AE_TELE_LOG_CONSOLE // telemetry to console only - auto trap = std::make_shared(std::cout); + auto trap = std::make_shared(std::cout); # elif AE_TELE_LOG_TO_STATISTICS // telemetry to statistics only tele_statistics_trap_is_set = true; @@ -89,9 +92,8 @@ void AetherAppContext::TeleStatisticsInit( // trap is set to something different # if AE_TELE_LOG_CONSOLE && AE_TELE_LOG_TO_STATISTICS // if proxy trap is used - auto proxy = std::static_pointer_cast< - ae::tele::ProxyTrap>( - trap); + auto proxy = std::static_pointer_cast>(trap); auto const& current_statistics = proxy->second; tele_statistics->trap()->MergeStatistics(*current_statistics); proxy->second = tele_statistics->trap(); @@ -381,6 +383,12 @@ std::unique_ptr AetherApp::Construct(AetherAppContext context) { } AetherApp::~AetherApp() { + if (aether_) { + Exit(); + while (!IsExited()) { + WaitUntil(Update(Now())); + } + } // save aether_ state on exit if (aether_) { aether_.Save(); @@ -391,4 +399,44 @@ AetherApp::~AetherApp() { aether_.Reset(); } +void AetherApp::Exit(int code) { + if (requested_exit_code_) { + return; + } + requested_exit_code_ = code; + // Defer shutdown out of the callback that requested exit. + shutdown_task_ = aether_->task_scheduler->Task([this]() { + aether_->StopNetworking(); + auto registry = aether_->adapter_registry; + if (registry) { + registry.Load(); + stopping_adapters_ = registry->adapters(); + for (auto& adapter : stopping_adapters_) { + adapter.Load(); + stop_actions_.push_back(&adapter->Stop()); + } + } + CheckShutdown(); + }); + if (!shutdown_task_) { + AE_TELED_ERROR("Failed to schedule application shutdown"); + assert(false && "Task allocation failed"); + } +} + +void AetherApp::CheckShutdown() { + for (auto* action : stop_actions_) { + if (!action->is_finished()) { + shutdown_task_ = aether_->task_scheduler->DelayedTask( + [this]() { CheckShutdown(); }, Now() + std::chrono::milliseconds{10}); + if (!shutdown_task_) { + AE_TELED_ERROR("Failed to schedule application shutdown check"); + assert(false && "Task allocation failed"); + } + return; + } + } + exit_code_ = requested_exit_code_; +} + } // namespace ae diff --git a/aether/aether_app.h b/aether/aether_app.h index 4f509313c..05141bae5 100644 --- a/aether/aether_app.h +++ b/aether/aether_app.h @@ -21,6 +21,7 @@ #include #include #include +#include #include "aether-objects/obj/domain.h" #include "aether-objects/ptr/ptr.h" @@ -156,14 +157,9 @@ class AetherApp { ~AetherApp(); - /** - * \brief Mark application as exited - */ - void Exit(int code = 0) { - exit_code_ = code; - // wake up the task thread - aether_->task_scheduler->Task([]() {}); - } + // Stop client connections before adapters, then mark the app as exited. + // Keep updating the scheduler until IsExited() becomes true. + void Exit(int code = 0); bool IsExited() const { return exit_code_.has_value(); } int ExitCode() const { @@ -221,12 +217,17 @@ class AetherApp { private: AetherApp() = default; + void CheckShutdown(); std::unique_ptr domain_facility_; std::unique_ptr domain_; Aether::ptr aether_; std::optional exit_code_; + std::optional requested_exit_code_; + std::vector stopping_adapters_; + std::vector stop_actions_; + TaskSubscription shutdown_task_; }; } // namespace ae diff --git a/aether/api_protocol/api_message.h b/aether/api_protocol/api_message.h index 05251322a..e41c2f95f 100644 --- a/aether/api_protocol/api_message.h +++ b/aether/api_protocol/api_message.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "aether-miscpp/serialization/serialization.h" diff --git a/aether/api_protocol/protocol_context.cpp b/aether/api_protocol/protocol_context.cpp index 37d2f9b5c..c1a6d1b3d 100644 --- a/aether/api_protocol/protocol_context.cpp +++ b/aether/api_protocol/protocol_context.cpp @@ -35,13 +35,15 @@ ProtocolContext::~ProtocolContext() { void ProtocolContext::SetSendResultResponse(RequestId request_id) { auto entry = TakePending(request_id); + auto* p = parser(); + assert(p != nullptr && "Parser shouldn't be null"); + if (entry.response == nullptr) { AE_TELED_DEBUG("No callback for request id {} cancel parse", request_id); - parser()->Cancel(); + p->Cancel(); + return; } - auto* p = parser(); - assert(p != nullptr && "Parser shouldn't be null"); entry.response->OnResult(*p); DestroyPending(entry); } @@ -52,7 +54,10 @@ void ProtocolContext::SetSendErrorResponse(RequestId req_id, auto entry = TakePending(req_id); if (entry.response == nullptr) { AE_TELED_DEBUG("No callback for error with request id {}", req_id); - parser()->Cancel(); + auto* p = parser(); + assert(p != nullptr && "Parser shouldn't be null"); + p->Cancel(); + return; } entry.response->OnError(error_type, static_cast(error_code)); diff --git a/aether/channels/modem_channel.cpp b/aether/channels/modem_channel.cpp index be1b67fcc..07011d1ba 100644 --- a/aether/channels/modem_channel.cpp +++ b/aether/channels/modem_channel.cpp @@ -33,6 +33,7 @@ auto EnsureModemConnected(ModemAccessPoint& access_point) { auto& connect_action = access_point.Connect(); s = connect_action.connection_event().Subscribe( [&](bool is_connected) noexcept { + s.Reset(); if (is_connected) { ex::set_value(std::move(ctx.receiver)); } else { @@ -49,40 +50,36 @@ std::unique_ptr CreateTransport(AeContext const& ae_context, ae_context, access_point.modem_driver(), std::move(endpoint)); } -auto ConnectTransport(std::unique_ptr&& transport) { +} // namespace modem_channel_internal + +TransportBuildSender ModemChannel::ConnectTransport( + std::unique_ptr transport) { return ex::create), ex::set_error_t(int)>( [t{std::move(transport)}, s{Subscription{}}](auto& ctx) mutable noexcept { - if (t->stream_info().link_state == LinkState::kLinked) { - ex::set_value(std::move(ctx.receiver), std::move(t)); - return; - } - - s = t->stream_update_event().Subscribe([&]() { - if (t->stream_info().link_state == LinkState::kLinked) { + auto complete = [&]() noexcept { + auto const state = t->stream_info().link_state; + if (state == LinkState::kLinked) { + // The operation state can outlive completion. Stop observing the + // transport before transferring ownership to the receiver. + s.Reset(); ex::set_value(std::move(ctx.receiver), std::move(t)); - } else if (t->stream_info().link_state == LinkState::kLinkError) { + return true; + } + if (state == LinkState::kLinkError) { + s.Reset(); ex::set_error(std::move(ctx.receiver), 2); + return true; } - }); + return false; + }; + if (complete()) { + return; + } + s = t->stream_update_event().Subscribe(complete); }); } -auto MakeTransportBuilderSender(AeContext const& ae_context, - ModemAccessPoint& access_point, - Endpoint endpoint) { - return EnsureModemConnected(access_point) | - ex::then([ae_context, &access_point, - e{std::move(endpoint)}]() mutable noexcept { - return CreateTransport(ae_context, access_point, std::move(e)); - }) | - ex::let_value([&](std::unique_ptr& t) noexcept { - return ConnectTransport(std::move(t)); - }); -} - -} // namespace modem_channel_internal - ModemChannel::ModemChannel(ObjProp prop, ObjPtr aether, ModemAccessPoint::ptr access_point, Endpoint address) : Channel{prop}, @@ -112,8 +109,15 @@ ModemChannel::ModemChannel(ObjProp prop, ObjPtr aether, TransportBuildSender ModemChannel::TransportBuilder() { auto ap = access_point_.Load(); assert(ap && "Access point is not loaded"); - return modem_channel_internal::MakeTransportBuilderSender(*aether_, *ap, - address); + return modem_channel_internal::EnsureModemConnected(*ap) | + ex::then([context = AeContext{*aether_}, ap, + endpoint = address]() mutable noexcept { + return modem_channel_internal::CreateTransport(context, *ap, + std::move(endpoint)); + }) | + ex::let_value([](std::unique_ptr& transport) noexcept { + return ConnectTransport(std::move(transport)); + }); } Duration ModemChannel::TransportBuildTimeout() const { diff --git a/aether/channels/modem_channel.h b/aether/channels/modem_channel.h index 50741757f..3e408e303 100644 --- a/aether/channels/modem_channel.h +++ b/aether/channels/modem_channel.h @@ -20,6 +20,8 @@ #include "aether/config.h" #if AE_SUPPORT_MODEMS +# include + # include "aether/access_points/modem_access_point.h" # include "aether/channels/channel.h" # include "aether/types/address.h" @@ -46,6 +48,10 @@ class ModemChannel final : public Channel { Endpoint address; private: + friend struct ModemChannelTestAccess; + static TransportBuildSender ConnectTransport( + std::unique_ptr transport); + ObjPtr aether_; ModemAccessPoint::ptr access_point_; }; diff --git a/aether/client.cpp b/aether/client.cpp index cd31717de..bea202dea 100644 --- a/aether/client.cpp +++ b/aether/client.cpp @@ -33,6 +33,21 @@ Client::Client(ObjProp prop, Aether::ptr aether) Client::~Client() = default; +void Client::StopNetworking() { +#if AE_ENABLE_PING + ping_cloud_servers_.reset(); +#endif +#if TELEMETRY_ENABLED + telemetry_.reset(); +#endif + if (cloud_connection_) { + cloud_connection_->Stop(); + } + if (server_connection_manager_) { + server_connection_manager_->Stop(); + } +} + std::string const& Client::id() const { return client_id_; } Uid const& Client::parent_uid() const { return parent_uid_; } Uid const& Client::uid() const { return uid_; } diff --git a/aether/client.h b/aether/client.h index dbf5e6f1d..8f11b2a48 100644 --- a/aether/client.h +++ b/aether/client.h @@ -72,6 +72,8 @@ class Client : public Obj { ephemeral_uid_, master_key_, cloud_, server_keys_, connectivity_policy_, client_cloud_manager_)) void SendTelemetry(); + // Stop runtime connections without changing persisted configuration. + void StopNetworking(); private: ObjPtr aether_; diff --git a/aether/cloud_connections/cloud_server_connections.cpp b/aether/cloud_connections/cloud_server_connections.cpp index 993eb8988..f45b09a24 100644 --- a/aether/cloud_connections/cloud_server_connections.cpp +++ b/aether/cloud_connections/cloud_server_connections.cpp @@ -147,6 +147,25 @@ void CloudServerConnections::InitServerConnections() { NormalizeServerPriorities(); } +void CloudServerConnections::Stop() { + if (stopped_) { + return; + } + stopped_ = true; + defer_sub_.Reset(); + selected_servers_.clear(); + for (auto& entry : server_entries_) { + entry.state_sub.Reset(); + entry.error_sub.Reset(); + entry.quarantine_sub.Reset(); + } + for (auto& entry : server_entries_) { + if (auto* connection = entry.connection.client_connection()) { + connection->server_connection().Stop(); + } + } +} + bool CloudServerConnections::SubscribeToServerState( CloudServerConnection& server_connection) { auto* conn = server_connection.client_connection(); @@ -256,7 +275,7 @@ void CloudServerConnections::ReleaseQuarantinedServer( } void CloudServerConnections::ScheduleReconcileServers() { - if (defer_sub_) { + if (stopped_ || defer_sub_) { return; } defer_sub_ = ae_context_.scheduler().Task([this]() { @@ -271,7 +290,7 @@ void CloudServerConnections::ScheduleReconcileServers() { } void CloudServerConnections::ReconcileServers() { - if (selected_servers_.size() >= max_connections_) { + if (stopped_ || selected_servers_.size() >= max_connections_) { return; } // Vacancy-fill model: keep the current selected list stable and only append diff --git a/aether/cloud_connections/cloud_server_connections.h b/aether/cloud_connections/cloud_server_connections.h index c0bf1acf7..90b59e0d4 100644 --- a/aether/cloud_connections/cloud_server_connections.h +++ b/aether/cloud_connections/cloud_server_connections.h @@ -92,6 +92,8 @@ class CloudServerConnections { * \brief Restream all selected servers */ void Restream(); + // Cancel failover before stopping transports. Keeps published entries alive. + void Stop(); /** * \brief Iterate over servers according to the request policy. @@ -178,6 +180,7 @@ class CloudServerConnections { PtrView cloud_; std::unique_ptr connection_factory_; std::size_t max_connections_; + bool stopped_{}; // Entries are reserved and populated once before connection pointers are // published. Do not grow or rebuild this vector: callbacks retain pointers // to its connections. diff --git a/aether/config.h b/aether/config.h index eacd198e9..5686b56bb 100644 --- a/aether/config.h +++ b/aether/config.h @@ -17,10 +17,6 @@ #ifndef AETHER_CONFIG_H_ #define AETHER_CONFIG_H_ -// Defines Aether's compile-time configuration defaults. USER_CONFIG, when -// defined, is included before these defaults so it can override them. Add new -// options to aether/tele_compile_options.h for telemetry reporting. - // IWYU pragma: begin_exports #include #include @@ -208,17 +204,17 @@ * Also choose one of the supported modem implementations. */ #ifndef AE_SUPPORT_MODEMS -# define AE_SUPPORT_MODEMS 0 +# define AE_SUPPORT_MODEMS 1 #endif // Thingy91x modem implementation is enabled. #ifndef AE_ENABLE_THINGY91X -# define AE_ENABLE_THINGY91X 1 +# define AE_ENABLE_THINGY91X 0 #endif // SIM7070 modem implementation is enabled. #ifndef AE_ENABLE_SIM7070 -# define AE_ENABLE_SIM7070 0 +# define AE_ENABLE_SIM7070 1 #endif // BG95 modem implementation is enabled. @@ -331,7 +327,7 @@ // default value used for ping timeout, until statistics are available #ifndef AE_DEFAULT_RESPONSE_TIMEOUT_MS -# define AE_DEFAULT_RESPONSE_TIMEOUT_MS 5000 +# define AE_DEFAULT_RESPONSE_TIMEOUT_MS 15000 #endif // Is periodic ping messages enabled @@ -356,12 +352,12 @@ // default value for connection modem #ifndef AE_MODEM_CONNECTION_TIMEOUT_MS -# define AE_MODEM_CONNECTION_TIMEOUT_MS 15000 +# define AE_MODEM_CONNECTION_TIMEOUT_MS 60000 #endif // Maximal number of server connections #ifndef AE_CLOUD_MAX_SERVER_CONNECTIONS -# define AE_CLOUD_MAX_SERVER_CONNECTIONS 3 +# define AE_CLOUD_MAX_SERVER_CONNECTIONS 1 #endif // Cloud server quarantine time @@ -371,7 +367,7 @@ // Cloud request per-server timeout in milliseconds #ifndef AE_CLOUD_REQUEST_TIMEOUT_MS -# define AE_CLOUD_REQUEST_TIMEOUT_MS 5000 +# define AE_CLOUD_REQUEST_TIMEOUT_MS 30000 #endif // Telemetry configuration diff --git a/aether/connection_manager/server_connection_manager.cpp b/aether/connection_manager/server_connection_manager.cpp index ff63c88d6..05835be89 100644 --- a/aether/connection_manager/server_connection_manager.cpp +++ b/aether/connection_manager/server_connection_manager.cpp @@ -40,6 +40,9 @@ ServerConnectionManager::GetServerConnectionFactory() { auto ServerConnectionManager::CreateConnection(Ptr const& server) -> std::shared_ptr { + if (stopped_) { + return {}; + } auto in_cache = FindInCache(server->server_id); if (in_cache) { return in_cache; @@ -70,6 +73,16 @@ std::shared_ptr ServerConnectionManager::FindInCache( return nullptr; } +void ServerConnectionManager::Stop() { + stopped_ = true; + server_update_subs_.Reset(); + for (auto& [id, cached] : cached_connections_) { + if (auto connection = cached.lock()) { + connection->server_connection().Stop(); + } + } +} + void ServerConnectionManager::ServerUpdate(ServerId server_id) { auto in_cache = FindInCache(server_id); if (!in_cache) { diff --git a/aether/connection_manager/server_connection_manager.h b/aether/connection_manager/server_connection_manager.h index a0e762453..3da7a9587 100644 --- a/aether/connection_manager/server_connection_manager.h +++ b/aether/connection_manager/server_connection_manager.h @@ -52,6 +52,7 @@ class ServerConnectionManager { Ptr const& server); std::shared_ptr FindInCache(ServerId server_id) const; + void Stop(); private: void ServerUpdate(ServerId server_id); @@ -60,6 +61,7 @@ class ServerConnectionManager { PtrView client_; std::map> cached_connections_; MultiSubscription server_update_subs_; + bool stopped_{}; }; } // namespace ae diff --git a/aether/escaped_io_stream_trap.cpp b/aether/escaped_io_stream_trap.cpp new file mode 100644 index 000000000..d57847b5b --- /dev/null +++ b/aether/escaped_io_stream_trap.cpp @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether/escaped_io_stream_trap.h" + +#include +#include + +namespace ae::tele { +namespace escaped_io_stream_trap_internal { +class EscapedLogLine final : public ILogLine { + public: + explicit EscapedLogLine(ILogLine& log_line) : log_line_{&log_line} {} + + void InvokeTime(TimePoint time) override { log_line_->InvokeTime(time); } + void WriteLevel(Level level) override { log_line_->WriteLevel(level); } + void WriteModule(Module const& module) override { + log_line_->WriteModule(module); + } + void Location(std::string_view file, std::uint32_t line) override { + log_line_->Location(file, line); + } + void TagName(std::string_view name) override { log_line_->TagName(name); } + + void Blob(std::span blob) override { + static constexpr char kHexDigits[] = "0123456789ABCDEF"; + + std::string escaped; + escaped.reserve(blob.size()); + for (auto byte : blob) { + if (byte < 0x20 || byte == 0x7F) { + escaped.append("{0x"); + escaped.push_back(kHexDigits[(byte >> 4) & 0x0F]); + escaped.push_back(kHexDigits[byte & 0x0F]); + escaped.push_back('}'); + } else { + escaped.push_back(static_cast(byte)); + } + } + + log_line_->Blob(std::span{ + reinterpret_cast(escaped.data()), escaped.size()}); + } + + private: + ILogLine* log_line_; +}; + +class EscapedLogCollector final : public ILogCollector { + public: + explicit EscapedLogCollector(ILogCollector& collector) + : collector_{&collector} {} + + void WriteLine(ILogLine& log_line) override { + auto escaped_log_line = EscapedLogLine{log_line}; + collector_->WriteLine(escaped_log_line); + } + + private: + ILogCollector* collector_; +}; +} // namespace escaped_io_stream_trap_internal + +EscapedIoStreamTrap::EscapedIoStreamTrap(std::ostream& stream) + : trap_{stream} {} + +void EscapedIoStreamTrap::AddInvoke(Tag const& tag, std::uint32_t count) { + trap_.AddInvoke(tag, count); +} + +void EscapedIoStreamTrap::AddInvokeDuration(Tag const& tag, Duration duration) { + trap_.AddInvokeDuration(tag, duration); +} + +void EscapedIoStreamTrap::LogLine(Tag const& tag, + ILogCollector& log_collector) { + auto escaped_collector = + escaped_io_stream_trap_internal::EscapedLogCollector{log_collector}; + trap_.LogLine(tag, escaped_collector); +} + +void EscapedIoStreamTrap::WriteEnvData(EnvData const& env_data) { + trap_.WriteEnvData(env_data); +} +} // namespace ae::tele diff --git a/aether/escaped_io_stream_trap.h b/aether/escaped_io_stream_trap.h new file mode 100644 index 000000000..f6fc1116a --- /dev/null +++ b/aether/escaped_io_stream_trap.h @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_ESCAPED_IO_STREAM_TRAP_H_ +#define AETHER_ESCAPED_IO_STREAM_TRAP_H_ + +#include +#include + +#include "aether-tele/itrap.h" +#include "aether-tele/traps/io_stream_traps.h" + +namespace ae::tele { +// Writes log text to a stream while rendering embedded ASCII control +// characters as {0xNN}. The newline terminating the log record is preserved. +class EscapedIoStreamTrap final : public ITrap { + public: + explicit EscapedIoStreamTrap(std::ostream& stream); + + void AddInvoke(Tag const& tag, std::uint32_t count) override; + void AddInvokeDuration(Tag const& tag, Duration duration) override; + void LogLine(Tag const& tag, ILogCollector& log_collector) override; + void WriteEnvData(EnvData const& env_data) override; + + private: + IoStreamTrap trap_; +}; +} // namespace ae::tele + +#endif // AETHER_ESCAPED_IO_STREAM_TRAP_H_ diff --git a/aether/events/event_handler.h b/aether/events/event_handler.h index 22c5c186e..5e1f12b6f 100644 --- a/aether/events/event_handler.h +++ b/aether/events/event_handler.h @@ -17,7 +17,6 @@ #ifndef AETHER_EVENTS_EVENT_HANDLER_H_ #define AETHER_EVENTS_EVENT_HANDLER_H_ -#include #include #include "aether-miscpp/types/small_function.h" diff --git a/aether/executors/with_timeout.h b/aether/executors/with_timeout.h index 9e04ea59a..cd81e41ff 100644 --- a/aether/executors/with_timeout.h +++ b/aether/executors/with_timeout.h @@ -17,8 +17,8 @@ #ifndef AETHER_EXECUTORS_WITH_TIMEOUT_H_ #define AETHER_EXECUTORS_WITH_TIMEOUT_H_ -#include #include +#include #include diff --git a/aether/modems/bg95_at_modem.cpp b/aether/modems/bg95_at_modem.cpp index 1307de119..c2af92fb0 100644 --- a/aether/modems/bg95_at_modem.cpp +++ b/aether/modems/bg95_at_modem.cpp @@ -14,9 +14,16 @@ * limitations under the License. */ +/** + * @file bg95_at_modem.cpp + * @brief BG95 AT command pipelines for registration, sockets, and power + * control. + */ + #include "aether/modems/bg95_at_modem.h" #if AE_SUPPORT_MODEMS && AE_ENABLE_BG95 +# include # include # include "aether-miscpp/misc/override.h" @@ -28,7 +35,9 @@ # include "aether/modems/modems_tele.h" namespace ae { -static constexpr auto kWaitOk = at::Wait{"OK"}; +using namespace std::chrono_literals; + +static const auto kWaitOk = at::Wait{"OK"}; namespace bg95_modem_internal { OpenNetworkOperationImpl::OpenNetworkOperationImpl(AeContext const& ae_context, @@ -50,7 +59,8 @@ OpenNetworkOperationImpl::OpenNetworkOperationImpl(AeContext const& ae_context, auto OpenNetworkOperationImpl::Pipeline() { // BG95 uses QIACT for PDP context activation and QIOPEN for socket connection // AT+QIACT=,, to activate PDP context - // AT+QIOPEN=,,,, to open socket + // AT+QIOPEN=,,,, to open + // socket auto handle = self_->next_connection_index_++; auto protocol_str = [&]() -> std::string_view { @@ -65,27 +75,27 @@ auto OpenNetworkOperationImpl::Pipeline() { // First activate PDP context, then open socket return ex::just() | - at::MakeRequest(at_support_, - "AT+QIACT=1,1,\"0.0.0.0\"", - at::Wait{"+QIACT:", - [](auto&, auto pos) { - std::int32_t context_id{}; - std::int32_t result{}; - if (at_support::ParseResponse(*pos, "+QIACT", - context_id, - result)) { - return result == 1; - } - return false; - }}) | + at::MakeRequest( + at_support_, "AT+QIACT=1,1,\"0.0.0.0\"", + at::Wait{"+QIACT:", + [](auto&, auto pos) { + std::int32_t context_id{}; + std::int32_t result{}; + if (at_support::ParseResponse(*pos, "+QIACT", + context_id, result)) { + return result == 1; + } + return false; + }}) | ex::with_timeout(ae_context_, 30s) | at::MakeRequest(at_support_, - Format(R"(AT+QIOPEN=1,{}, "{}", "{}", {}))", - handle, protocol_str, host_, port_), + Format(R"(AT+QIOPEN=1,{}, "{}", "{}", {}))", handle, + protocol_str, host_, port_), at::Wait{Format("+QIOPEN: {}", handle), [handle](AtBuffer&, auto pos) { std::int32_t result_code{}; - int expected_handle = static_cast(handle); + int expected_handle = + static_cast(handle); if (at_support::ParseResponse( *pos, "+QIOPEN", result_code, expected_handle)) { @@ -221,20 +231,19 @@ ModemStartOperation::ModemStartOperation(AeContext const& ae_context, } auto ModemStartOperation::SetBaudRate(kBaudRate const rate) { - static const std::map - baud_rate_commands_bg95 = { - {kBaudRate::kBaudRate9600, "AT+IPR=9600"}, - {kBaudRate::kBaudRate19200, "AT+IPR=19200"}, - {kBaudRate::kBaudRate38400, "AT+IPR=38400"}, - {kBaudRate::kBaudRate57600, "AT+IPR=57600"}, - {kBaudRate::kBaudRate115200, "AT+IPR=115200"}, - {kBaudRate::kBaudRate230400, "AT+IPR=230400"}, - {kBaudRate::kBaudRate921600, "AT+IPR=921600"}, - {kBaudRate::kBaudRate2900000, "AT+IPR=2900000"}, - {kBaudRate::kBaudRate3000000, "AT+IPR=3000000"}, - {kBaudRate::kBaudRate3200000, "AT+IPR=3200000"}, - {kBaudRate::kBaudRate3684000, "AT+IPR=3684000"}, - {kBaudRate::kBaudRate4000000, "AT+IPR=4000000"}}; + static const std::map baud_rate_commands_bg95 = { + {kBaudRate::kBaudRate9600, "AT+IPR=9600"}, + {kBaudRate::kBaudRate19200, "AT+IPR=19200"}, + {kBaudRate::kBaudRate38400, "AT+IPR=38400"}, + {kBaudRate::kBaudRate57600, "AT+IPR=57600"}, + {kBaudRate::kBaudRate115200, "AT+IPR=115200"}, + {kBaudRate::kBaudRate230400, "AT+IPR=230400"}, + {kBaudRate::kBaudRate921600, "AT+IPR=921600"}, + {kBaudRate::kBaudRate2900000, "AT+IPR=2900000"}, + {kBaudRate::kBaudRate3000000, "AT+IPR=3000000"}, + {kBaudRate::kBaudRate3200000, "AT+IPR=3200000"}, + {kBaudRate::kBaudRate3684000, "AT+IPR=3684000"}, + {kBaudRate::kBaudRate4000000, "AT+IPR=4000000"}}; return ex::let_value([&, rate]() noexcept { auto make_request = [&](std::string&& cmd) noexcept { @@ -256,17 +265,15 @@ auto ModemStartOperation::SetBaudRate(kBaudRate const rate) { auto ModemStartOperation::SetNetMode(kModemMode const modem_mode) { // BG95 specific network mode commands - static const std::map - net_mode_commands_bg95 = { - {kModemMode::kModeAuto, "AT+CNMP=2"}, // Set modem mode Auto - {kModemMode::kModeGSMOnly, "AT+CNMP=13"}, // Set modem mode GSMOnly - {kModemMode::kModeLTEOnly, "AT+CNMP=38"}, // Set modem mode LTEOnly - {kModemMode::kModeGSMLTE, "AT+CNMP=51"}, // Set modem mode GSMLTE - {kModemMode::kModeCatM, "AT+CMNB=1"}, // Set modem mode CatM - {kModemMode::kModeNbIot, "AT+CMNB=2"}, // Set modem mode NbIot - {kModemMode::kModeCatMNbIot, - "AT+CMNB=3"}, // Set modem mode CatMNbIot - }; + static const std::map net_mode_commands_bg95 = { + {kModemMode::kModeAuto, "AT+CNMP=2"}, // Set modem mode Auto + {kModemMode::kModeGSMOnly, "AT+CNMP=13"}, // Set modem mode GSMOnly + {kModemMode::kModeLTEOnly, "AT+CNMP=38"}, // Set modem mode LTEOnly + {kModemMode::kModeGSMLTE, "AT+CNMP=51"}, // Set modem mode GSMLTE + {kModemMode::kModeCatM, "AT+CMNB=1"}, // Set modem mode CatM + {kModemMode::kModeNbIot, "AT+CMNB=2"}, // Set modem mode NbIot + {kModemMode::kModeCatMNbIot, "AT+CMNB=3"}, // Set modem mode CatMNbIot + }; return ex::let_value([&, modem_mode]() noexcept { auto make_request = [&](std::string&& cmd) noexcept { @@ -331,14 +338,14 @@ auto ModemStartOperation::SetupNetwork( ex::with_timeout(ae_context_, 120s) | at::MakeRequest(at_support_, R"(AT+CGDCONT=1,"IP",")" + apn_name + "\"", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | + ex::with_timeout(ae_context_, 180s) | at::MakeRequest(at_support_, "AT+CNCFG=1,0,\"" + apn_name + "\",\"" + apn_user + "\",\"" + apn_pass + "\"," + type, kWaitOk) | - ex::with_timeout(ae_context_, 1s) | + ex::with_timeout(ae_context_, 180s) | at::MakeRequest(at_support_, "AT+CREG=1;+CGREG=1;+CEREG=1", kWaitOk) | - ex::with_timeout(ae_context_, 1s); + ex::with_timeout(ae_context_, 180s); } auto ModemStartOperation::CheckSimStatus() { @@ -380,7 +387,7 @@ auto ModemStartOperation::Pipeline() { }) | CheckSimStatus() | ex::let_value([&]() noexcept { auto setup_sim = [this]() noexcept { - return ex::just() | SetupSim(modem_init_.use_pin); + return ex::just() | SetupSim(modem_init_.pin); }; using res = ex::variant_sender, @@ -426,7 +433,7 @@ void ModemStartOperation::RunPipeline() { } ModemStopOperation::ModemStopOperation(AeContext const& ae_context, - Bg95AtModem& self) + Bg95AtModem& self) : ae_context_{ae_context}, self_{&self}, at_support_{self.at_support_} { RunPipeline(); } @@ -536,8 +543,8 @@ void Bg95AtModem::Init() { ModemOperation* Bg95AtModem::Start() { if (!modem_start_operation_ || modem_start_operation_->is_finished()) { if (started_) { - modem_start_operation_ = std::make_unique< - bg95_modem_internal::ModemStartedAlreadyOperation>(); + modem_start_operation_ = + std::make_unique(); } else { modem_start_operation_ = std::make_unique( @@ -551,12 +558,12 @@ ModemOperation* Bg95AtModem::Start() { ModemOperation* Bg95AtModem::Stop() { if (!modem_stop_operation_ || modem_stop_operation_->is_finished()) { if (!started_) { - modem_stop_operation_ = std::make_unique< - bg95_modem_internal::ModemStoppedAlreadyOperation>(); + modem_stop_operation_ = + std::make_unique(); } else { modem_stop_operation_ = - std::make_unique( - ae_context_, *this); + std::make_unique(ae_context_, + *this); } } @@ -581,8 +588,8 @@ ModemOperation* Bg95AtModem::CloseNetwork(ConnectionIndex connect_index) { return op; } -WriteOperation* Bg95AtModem::WritePacket( - ConnectionIndex connect_index, std::span data) { +WriteOperation* Bg95AtModem::WritePacket(ConnectionIndex connect_index, + std::span data) { if (data.size() > kModemMTU) { assert(false); return nullptr; @@ -596,12 +603,11 @@ Bg95AtModem::DataEvent::Subscriber Bg95AtModem::data_event() { return EventSubscriber{data_event_}; } -ModemOperation* Bg95AtModem::SetPowerSaveParam( - ModemPowerSaveParam const& psp) { +ModemOperation* Bg95AtModem::SetPowerSaveParam(ModemPowerSaveParam const& psp) { if (!modem_set_psp_operation_ || modem_set_psp_operation_->is_finished()) { - modem_set_psp_operation_ = std::make_unique< - bg95_modem_internal::ModemSetPowerSaveParamOperation>(ae_context_, - *this, psp); + modem_set_psp_operation_ = + std::make_unique( + ae_context_, *this, psp); } return modem_set_psp_operation_.get(); @@ -619,17 +625,16 @@ ModemOperation* Bg95AtModem::PowerOff() { // ============================private members=============================== // void Bg95AtModem::SetupPoll() { // BG95 uses +QIURC indication for socket events - poll_listener_.emplace(at_support_.dispatcher(), - "+QIURC: ", [this](auto&, auto pos) { - std::string type; - std::int32_t handle{}; - if (at_support::ParseResponse(*pos, "+QIURC", type, - handle)) { - if (type == "recv") { - PollEvent(handle); - } - } - }); + poll_listener_.emplace( + at_support_.dispatcher(), "+QIURC: ", [this](auto&, auto pos) { + std::string type; + std::int32_t handle{}; + if (at_support::ParseResponse(*pos, "+QIURC", type, handle)) { + if (type == "recv") { + PollEvent(handle); + } + } + }); } void Bg95AtModem::PollEvent(std::int32_t handle) { diff --git a/aether/modems/bg95_at_modem.h b/aether/modems/bg95_at_modem.h index 731f8702b..8dfb20902 100644 --- a/aether/modems/bg95_at_modem.h +++ b/aether/modems/bg95_at_modem.h @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * @file bg95_at_modem.h + * @brief Quectel BG95 AT driver and its queued asynchronous operations. + */ + #ifndef AETHER_MODEMS_BG95_AT_MODEM_H_ #define AETHER_MODEMS_BG95_AT_MODEM_H_ @@ -36,6 +41,9 @@ namespace ae { class Bg95AtModem; namespace bg95_modem_internal { +/** + * @brief Queue a modem socket open request and publish its connection index. + */ class OpenNetworkOperationImpl final : public OpenNetworkOperation { public: explicit OpenNetworkOperationImpl(AeContext const& ae_context, @@ -55,6 +63,9 @@ class OpenNetworkOperationImpl final : public OpenNetworkOperation { std::int32_t handle_{-1}; }; +/** + * @brief Queue closure of a modem socket and publish the outcome. + */ class CloseNetworkOperationImpl final : public ModemOperation { public: explicit CloseNetworkOperationImpl(AeContext const& ae_context, @@ -71,6 +82,9 @@ class CloseNetworkOperationImpl final : public ModemOperation { ConnectionIndex connect_index_; }; +/** + * @brief Queue transmission of borrowed data on a modem socket. + */ class WriteOperationImpl final : public WriteOperation { public: explicit WriteOperationImpl(AeContext const& ae_context, Bg95AtModem& self, @@ -88,11 +102,17 @@ class WriteOperationImpl final : public WriteOperation { std::span data_; }; +/** + * @brief Immediately successful result for an already started modem. + */ class ModemStartedAlreadyOperation final : public ModemOperation { public: explicit ModemStartedAlreadyOperation() { SetResult(Ok{kIgnore}); }; }; +/** + * @brief Configure the modem and establish network service. + */ class ModemStartOperation final : public ModemOperation { public: explicit ModemStartOperation(AeContext const& ae_context, Bg95AtModem& self); @@ -117,11 +137,17 @@ class ModemStartOperation final : public ModemOperation { AtSupport& at_support_; }; +/** + * @brief Immediately successful result for an already stopped modem. + */ class ModemStoppedAlreadyOperation final : public ModemOperation { public: explicit ModemStoppedAlreadyOperation() { SetResult(Ok{kIgnore}); }; }; +/** + * @brief Run the modem-specific network shutdown sequence. + */ class ModemStopOperation final : public ModemOperation { public: explicit ModemStopOperation(AeContext const& ae_context, Bg95AtModem& self); @@ -136,6 +162,9 @@ class ModemStopOperation final : public ModemOperation { }; // Power save parameters not implemented yet - returns success +/** + * @brief Run the modem-specific power-saving configuration operation. + */ class ModemSetPowerSaveParamOperation final : public ModemOperation { public: explicit ModemSetPowerSaveParamOperation(AeContext const& /*ae_context*/, @@ -145,6 +174,9 @@ class ModemSetPowerSaveParamOperation final : public ModemOperation { } }; +/** + * @brief Run the modem-specific power-down command sequence. + */ class ModemPowerOffOperation final : public ModemOperation { public: explicit ModemPowerOffOperation(AeContext const& ae_context, @@ -160,6 +192,15 @@ class ModemPowerOffOperation final : public ModemOperation { }; } // namespace bg95_modem_internal +/** + * @brief Quectel BG95 AT driver and its queued asynchronous operations. + * + * Owns the serial port, AT dispatcher, operation queue, and action pools. + * Construction schedules serial initialization. Call Start() to establish + * network service, and drive the scheduler until Stop() finishes before + * releasing the driver. + * @see IModemDriver + */ class Bg95AtModem final : public IModemDriver { friend class bg95_modem_internal::OpenNetworkOperationImpl; friend class bg95_modem_internal::CloseNetworkOperationImpl; @@ -172,19 +213,54 @@ class Bg95AtModem final : public IModemDriver { static constexpr std::uint16_t kModemMTU{1520}; public: + /** + * @brief Create the driver and schedule serial initialization. + * @param ae_context Non-owning runtime context that must outlive the driver. + * @param poller Poller used by the serial port implementation. + * @param modem_init Configuration copied or moved into the driver. + */ explicit Bg95AtModem(AeContext const& ae_context, IPoller::ptr const& poller, ModemInit modem_init); + /** + * @copydoc IModemDriver::Start + */ ModemOperation* Start() override; + /** + * @brief Deactivate context 1 and send AT+CFUN=0. + * @return A driver-owned stop operation; an already stopped modem succeeds + * immediately. The command pipeline stops on the first reported failure. + * @note This implementation does not issue individual socket-close commands. + */ ModemOperation* Stop() override; + /** + * @copydoc IModemDriver::OpenNetwork + */ OpenNetworkOperation* OpenNetwork(Protocol protocol, std::string const& host, std::uint16_t port) override; + /** + * @copydoc IModemDriver::CloseNetwork + */ ModemOperation* CloseNetwork(ConnectionIndex connect_index) override; + /** + * @copydoc IModemDriver::WritePacket + */ WriteOperation* WritePacket(ConnectionIndex connect_index, std::span data) override; + /** + * @copydoc IModemDriver::data_event + */ DataEvent::Subscriber data_event() override; + /** + * @copydoc IModemDriver::SetPowerSaveParam + * @note Currently returns success without applying the requested parameters. + */ ModemOperation* SetPowerSaveParam(ModemPowerSaveParam const& psp) override; + /** + * @copydoc IModemDriver::PowerOff + * @note Sends AT+CFUN=0 followed by AT+QPOWD. + */ ModemOperation* PowerOff() override; private: @@ -193,16 +269,6 @@ class Bg95AtModem final : public IModemDriver { static constexpr auto kWriteActionPoolCapacity = AE_MODEM_WRITE_ACTION_POOL_CAPACITY; - using OpenNetworkActionPool = - ActionPool; - using CloseNetworkActionPool = - ActionPool; - using WriteActionPool = - ActionPool; - void Init(); void SetupPoll(); void PollEvent(std::int32_t handle); @@ -221,9 +287,15 @@ class Bg95AtModem final : public IModemDriver { std::unique_ptr modem_stop_operation_; std::unique_ptr modem_set_psp_operation_; std::unique_ptr modem_poweroff_operation_; - OpenNetworkActionPool open_network_pool_; - CloseNetworkActionPool close_network_pool_; - WriteActionPool write_pool_; + ActionPool + open_network_pool_; + ActionPool + close_network_pool_; + ActionPool + write_pool_; bool initiated_; bool started_; diff --git a/aether/modems/esp32_modem_boot.h b/aether/modems/esp32_modem_boot.h new file mode 100644 index 000000000..96cf4647d --- /dev/null +++ b/aether/modems/esp32_modem_boot.h @@ -0,0 +1,148 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_MODEMS_ESP32_MODEM_BOOT_H_ +#define AETHER_MODEMS_ESP32_MODEM_BOOT_H_ + +#include "aether/config.h" +#if defined(ESP_PLATFORM) && defined(AE_MODEM_PWR_GPIO) && \ + defined(AE_MODEM_DTR_GPIO) +# include +# include +# include +# include +# include +# include "aether/ae_context.h" +# include "aether/executors/executors.h" +# include "aether/serial_ports/at_support/at_request.h" +# include "aether/tele.h" +# include "driver/gpio.h" + +namespace ae::esp32_modem_boot_internal { +using Sender = ex::AnySender; +inline constexpr auto kPowerPin = static_cast(AE_MODEM_PWR_GPIO); +inline constexpr auto kDtrPin = static_cast(AE_MODEM_DTR_GPIO); + +// The Waveshare HAT inverts PWR through a transistor: high presses PWRKEY. +// Release the key even if the operation is cancelled during the pulse. +struct PowerKeyRelease { + ~PowerKeyRelease() { gpio_set_level(kPowerPin, 0); } +}; + +inline auto Pause(AeContext context, std::chrono::milliseconds delay) { + return ex::create( + [context, delay, task = TaskSubscription{}](auto& ctx) mutable noexcept { + task = context.scheduler().DelayedTask( + [&ctx]() noexcept { ex::set_value(std::move(ctx.receiver)); }, + delay); + if (!task) { + AE_TELED_ERROR("Failed to schedule modem boot delay"); + assert(false && "Task allocation failed"); + } + }); +} + +inline Sender Probe(AeContext context, AtSupport& at) { + return Sender{at::MakeRequest(ex::just(), at, "AT", at::Wait{"OK"}) | + ex::with_timeout(context, std::chrono::seconds{1})}; +} + +inline Sender WaitReady(AeContext context, AtSupport& at, int attempts) { + return Sender{ + Probe(context, at) | + ex::let_error([context, &at, attempts](auto error) -> Sender { + if constexpr (std::is_same_v) { + return Sender{ex::just_error(error)}; + } + if (attempts == 0) { + return Sender{ex::just_error(error)}; + } + return Sender{Pause(context, std::chrono::milliseconds{500}) | + ex::let_value([context, &at, attempts]() noexcept { + return WaitReady(context, at, attempts - 1); + })}; + })}; +} + +inline Sender Pulse(AeContext context) { + return Sender{ex::create( + [context, release = std::unique_ptr{}, + task = TaskSubscription{}](auto& ctx) mutable noexcept { + release = std::make_unique(); + auto error = gpio_set_level(kPowerPin, 1); + if (error != ESP_OK) { + ex::set_error(std::move(ctx.receiver), static_cast(error)); + return; + } + task = context.scheduler().DelayedTask( + [&ctx]() noexcept { + auto error = gpio_set_level(kPowerPin, 0); + if (error != ESP_OK) { + ex::set_error(std::move(ctx.receiver), static_cast(error)); + return; + } + ex::set_value(std::move(ctx.receiver)); + }, + std::chrono::milliseconds{1100}); + if (!task) { + AE_TELED_ERROR("Failed to schedule PWRKEY release"); + gpio_set_level(kPowerPin, 0); + assert(false && "Task allocation failed"); + ex::set_error(std::move(ctx.receiver), + static_cast(ESP_ERR_NO_MEM)); + } + })}; +} + +inline Sender EnsureReady(AeContext context, AtSupport& at) { + return Sender{ + ex::just() | ex::let_value([context, &at]() noexcept -> Sender { + // DTR low keeps the UART awake. Set output latches before + // enabling outputs. + auto error = gpio_set_level(kPowerPin, 0); + if (error == ESP_OK) { + error = gpio_set_level(kDtrPin, 0); + } + if (error == ESP_OK) { + error = gpio_set_direction(kPowerPin, GPIO_MODE_OUTPUT); + } + if (error == ESP_OK) { + error = gpio_set_direction(kDtrPin, GPIO_MODE_OUTPUT); + } + if (error != ESP_OK) { + return Sender{ex::just_error(static_cast(error))}; + } + // A running modem must not receive another power-key pulse on + // ESP restart. + return Sender{ + WaitReady(context, at, 2) | + ex::let_error([context, &at](auto error) noexcept -> Sender { + if constexpr (!std::is_same_v) { + return Sender{ex::just_error(error)}; + } else { + return Sender{Pulse(context) | + ex::let_value([context, &at]() noexcept { + return WaitReady(context, at, 12); + })}; + } + })}; + })}; +} +} // namespace ae::esp32_modem_boot_internal +#endif +#endif // AETHER_MODEMS_ESP32_MODEM_BOOT_H_ diff --git a/aether/modems/imodem_driver.h b/aether/modems/imodem_driver.h index e265daee2..c6e8195a4 100644 --- a/aether/modems/imodem_driver.h +++ b/aether/modems/imodem_driver.h @@ -14,39 +14,63 @@ * limitations under the License. */ +/** + * @file imodem_driver.h + * @brief Asynchronous modem driver interface and operation result types. + */ + #ifndef AETHER_MODEMS_IMODEM_DRIVER_H_ #define AETHER_MODEMS_IMODEM_DRIVER_H_ #include "aether/config.h" #if AE_SUPPORT_MODEMS -# include -# include # include # include +# include +# include +# include "aether-miscpp/meta/ignore_t.h" # include "aether-miscpp/types/result.h" +# include "aether/actions/action.h" # include "aether/events/events.h" +# include "aether/modems/modem_driver_types.h" # include "aether/types/address.h" -# include "aether-miscpp/meta/ignore_t.h" -# include "aether/actions/action.h" # include "aether/types/data_buffer.h" -# include "aether/modems/modem_driver_types.h" namespace ae { +/** + * @brief Driver-specific operation error code. + * + * Interpret the numeric value in the context of the selected modem driver. + */ enum class ModemError : int {}; +/** + * @brief Asynchronous socket opening result: a connection index or an error. + */ class OpenNetworkOperation : public Action { public: // return either connection index or error using ResultType = Result; using ResultEvent = Event; + /** + * @brief Subscribe to the terminal result, emitted before Action::Finish(). + * @note Retain a Subscription to control the callback lifetime. + */ ResultEvent::Subscriber result_event() { return EventSubscriber{result_event_}; } + /** + * @brief Return the stored result, or an empty optional before completion. + */ std::optional const& result() const { return result_; } protected: + /** + * @brief Store and publish the result, then finish the action. + * @warning Completion may destroy this action; do not access it afterwards. + */ void SetResult(ResultType&& res) { result_.emplace(std::move(res)); result_event_.Emit(result_.value()); @@ -58,17 +82,31 @@ class OpenNetworkOperation : public Action { ResultEvent result_event_; }; +/** + * @brief Asynchronous write result: the number of bytes written or an error. + */ class WriteOperation : public Action { public: // return size of bytes written, or error code using ResultType = Result; using ResultEvent = Event; + /** + * @brief Subscribe to the terminal result, emitted before Action::Finish(). + * @note Retain a Subscription to control the callback lifetime. + */ ResultEvent::Subscriber result_event() { return EventSubscriber{result_event_}; } + /** + * @brief Return the stored result, or an empty optional before completion. + */ std::optional const& result() const { return result_; } protected: + /** + * @brief Store and publish the result, then finish the action. + * @warning Completion may destroy this action; do not access it afterwards. + */ void SetResult(ResultType&& res) { result_.emplace(std::move(res)); result_event_.Emit(result_.value()); @@ -80,16 +118,30 @@ class WriteOperation : public Action { ResultEvent result_event_; }; +/** + * @brief Asynchronous control operation with success or error completion. + */ class ModemOperation : public Action { public: using ResultType = Result; using ResultEvent = Event; + /** + * @brief Subscribe to the terminal result, emitted before Action::Finish(). + * @note Retain a Subscription to control the callback lifetime. + */ ResultEvent::Subscriber result_event() { return EventSubscriber{result_event_}; } + /** + * @brief Return the stored result, or an empty optional before completion. + */ std::optional const& result() const { return result_; } protected: + /** + * @brief Store and publish the result, then finish the action. + * @warning Completion may destroy this action; do not access it afterwards. + */ void SetResult(ResultType&& res) { result_.emplace(std::move(res)); result_event_.Emit(result_.value()); @@ -101,27 +153,105 @@ class ModemOperation : public Action { ResultEvent result_event_; }; +/** + * @brief Runtime interface for asynchronous cellular modem control and socket + * I/O. + * + * Returned action pointers are non-owning. The driver owns the operations; + * callers must keep the driver and its scheduler context alive while they run. + * An operation can already be complete when returned: inspect result() before + * subscribing to result_event(). Operation pointers must not be retained after + * the owner reclaims or replaces a completed operation. + * + * Keep driving the scheduler until shutdown completes before destroying the + * driver. Supported settings and shutdown behavior depend on the + * implementation. + */ class IModemDriver { public: using DataEvent = Event; + using ConnectionClosedEvent = Event; + + // Drivers emit this when they observe an unsolicited socket closure. + // Retain a Subscription and defer work that could destroy the driver. + ConnectionClosedEvent::Subscriber connection_closed_event() { + return EventSubscriber{connection_closed_event_}; + } virtual ~IModemDriver() = default; + /** + * @brief Initialize network service and prepare the modem for connections. + * @return A driver-owned operation, or nullptr if the request is rejected. + */ virtual ModemOperation* Start() = 0; + /** + * @brief Request asynchronous shutdown of network service. + * @return A driver-owned shutdown operation, or nullptr if unavailable. + * @note Await completion before releasing the driver. See the concrete driver + * for socket cleanup and restart semantics. + */ virtual ModemOperation* Stop() = 0; + /** + * @brief Open a TCP or UDP connection to a remote endpoint. + * @param protocol Transport protocol to use. + * @param host Remote host name or address supported by the driver. + * @param port Remote port number. + * @return A driver-owned operation yielding a connection index, or nullptr + * if no operation can be created. + */ virtual OpenNetworkOperation* OpenNetwork(Protocol protocol, std::string const& host, std::uint16_t port) = 0; + /** + * @brief Request closure of a modem connection. + * @param connect_index Connection index returned by OpenNetwork(). + * @return A driver-owned operation, or nullptr if no operation is available. + */ virtual ModemOperation* CloseNetwork(ConnectionIndex connect_index) = 0; + /** + * @brief Queue data for transmission on an open connection. + * @param connect_index Destination connection index. + * @param data Borrowed bytes; keep the buffer valid until the operation + * finishes. + * @return A driver-owned write operation, or nullptr if the request is + * rejected. + */ virtual WriteOperation* WritePacket(ConnectionIndex connect_index, std::span data) = 0; + /** + * @brief Subscribe to received data and its originating connection index. + * @note The buffer reference is valid only during the callback; copy data + * that must be retained. Keep a Subscription for the desired callback + * lifetime. + */ virtual DataEvent::Subscriber data_event() = 0; + /** + * @brief Request implementation-specific power-saving settings. + * @param psp Requested radio and power-saving parameters. + * @return A driver-owned operation, or nullptr if the request is rejected. + * @note Some drivers implement this as a successful no-op. + */ virtual ModemOperation* SetPowerSaveParam(ModemPowerSaveParam const& psp) = 0; + /** + * @brief Request the driver's power-down sequence. + * @return A driver-owned operation, or nullptr if the request is rejected. + * @note This is distinct from Stop(); it does not promise socket cleanup or + * physical removal of power. See the concrete implementation. + */ virtual ModemOperation* PowerOff() = 0; + + protected: + void NotifyConnectionClosed(ConnectionIndex connection) { + connection_closed_event_.Emit(connection); + } + + private: + ConnectionClosedEvent connection_closed_event_; }; } /* namespace ae */ diff --git a/aether/modems/modem_driver_types.h b/aether/modems/modem_driver_types.h index e53cc65d7..6bb78b7db 100644 --- a/aether/modems/modem_driver_types.h +++ b/aether/modems/modem_driver_types.h @@ -14,6 +14,12 @@ * limitations under the License. */ +/** + * @file modem_driver_types.h + * @brief Shared modem configuration, radio settings, and connection + * identifiers. + */ + #ifndef AETHER_MODEMS_MODEM_DRIVER_TYPES_H_ #define AETHER_MODEMS_MODEM_DRIVER_TYPES_H_ @@ -26,6 +32,9 @@ # include "aether/serial_ports/serial_port_types.h" namespace ae { +/** + * @brief Named error values used by modem configuration and conversion code. + */ enum class kModemError : std::int8_t { kNoError = 0, kSerialPortError = -1, @@ -56,6 +65,9 @@ enum class kModemError : std::int8_t { kDataLength = -26 }; +/** + * @brief Requested radio access mode; support depends on the modem. + */ enum class kModemMode : std::uint8_t { kModeAuto = 0, kModeGSMOnly = 1, @@ -66,6 +78,9 @@ enum class kModemMode : std::uint8_t { kModeCatMNbIot = 6 }; +/** + * @brief Packet data authentication method. + */ enum class kAuthType : std::uint8_t { kAuthTypeNone = 0, kAuthTypePAP = 1, @@ -82,6 +97,10 @@ enum class kAuthType : std::uint8_t { // 0 1 0 – Value is incremented in multiples of 6 min // 1 1 1 – Value indicates that the timer is deactivated +/** + * @brief Packed requested active-time timer with a five-bit value and unit + * selector. + */ struct kRequestedActiveTimeT3324 { AE_REFLECT_MEMBERS(byte) union { @@ -92,13 +111,13 @@ struct kRequestedActiveTimeT3324 { } bits; }; - // Constructor for convenient initialization + /// @brief Pack the supplied values after masking them to the stored fields. kRequestedActiveTimeT3324(std::uint8_t val = 0, std::uint8_t mult = 0) { bits.Value = (val & 0x1F); bits.Multiplier = (mult & 0x07); } - // Conversion operator to uint8_t for compatibility + /// @brief Return the packed timer byte. operator std::uint8_t() const { return byte; } }; @@ -114,6 +133,9 @@ struct kRequestedActiveTimeT3324 { // 1 0 1 – Value is incremented in multiples of 1 min // 1 1 0 – Value is incremented in multiples of 320 h +/** + * @brief Packed requested periodic tracking-area-update timer. + */ struct kRequestedPeriodicTAUT3412 { AE_REFLECT_MEMBERS(byte) union { @@ -124,16 +146,19 @@ struct kRequestedPeriodicTAUT3412 { } bits; }; - // Constructor for convenient initialization + /// @brief Pack the supplied values after masking them to the stored fields. kRequestedPeriodicTAUT3412(std::uint8_t val = 0, std::uint8_t mult = 0) { bits.Value = (val & 0x1F); bits.Multiplier = (mult & 0x03); } - // Conversion operator to uint8_t for compatibility + /// @brief Return the packed timer byte. operator std::uint8_t() const { return byte; } }; +/** + * @brief eDRX configuration mode passed to the modem. + */ enum class EdrxMode : std::uint8_t { kEdrxDisable = 0, kEdrxEnable = 1, @@ -141,6 +166,9 @@ enum class EdrxMode : std::uint8_t { kEdrxDisableCode = 3 }; +/** + * @brief Radio access technology selector for eDRX. + */ enum class EdrxActTType : std::uint8_t { kEdrxActDisable = 0, kEdrxActEUtranWBS1 = 4, @@ -166,6 +194,9 @@ enum class EdrxActTType : std::uint8_t { // 1 1 1 0 – 5242.88 s4 // 1 1 1 1 – 10485.76 s4 +/** + * @brief Packed requested/provided eDRX values and paging time window. + */ struct kEDrx { AE_REFLECT_MEMBERS(bytes) union { @@ -183,7 +214,7 @@ struct kEDrx { } bits; }; - // Constructor for convenient initialization + /// @brief Pack the supplied values after masking them to the stored fields. kEDrx(std::uint8_t r_edrx = 0, std::uint8_t p_edrx = 0, std::uint8_t ptw_val = 0) { bits.ReqEDRXValue = (r_edrx & 0x0F); @@ -191,12 +222,15 @@ struct kEDrx { bits.PTWValue = (ptw_val & 0x0F); } - // Conversion operator to uint16_t for compatibility + /// @brief Return byte0 in the high byte and byte1 in the low byte. operator std::uint16_t() const { return static_cast((bytes.byte0 << 8) | (bytes.byte1 << 0)); } }; +/** + * @brief Symbolic band identifiers; enum values are not physical band numbers. + */ enum class kModemBand : std::uint8_t { kWCDMA_B1 = 0, kWCDMA_B2 = 1, @@ -235,58 +269,86 @@ enum class kModemBand : std::uint8_t { kINVALID_BAND = 34 }; +/** + * @brief Requested power setting for one radio band. + */ struct BandPower { AE_REFLECT_MEMBERS(band, power) - kModemBand band; - std::uint8_t power; + kModemBand band; ///< Radio band to configure. + std::uint8_t power; ///< Driver-specific transmit-power value. }; // ========================modem init======================================== +/** + * @brief Requested power-saving and radio settings. + * @note Individual drivers may support only a subset or ignore these settings. + */ struct ModemPowerSaveParam { AE_REFLECT_MEMBERS(psm_mode, tau, act, edrx_mode, act_type, edrx_val, rai_mode, bands_mode, bands, modem_mode, power) - std::uint8_t psm_mode; - kRequestedPeriodicTAUT3412 tau; - kRequestedActiveTimeT3324 act; - EdrxMode edrx_mode; - EdrxActTType act_type; - kEDrx edrx_val; - std::uint8_t rai_mode; - std::uint8_t bands_mode; - std::vector bands; - kModemMode modem_mode; - std::vector power; + std::uint8_t psm_mode; ///< Requested power-saving mode. + kRequestedPeriodicTAUT3412 + tau; ///< Requested periodic tracking-area-update timer. + kRequestedActiveTimeT3324 act; ///< Requested active-time timer. + EdrxMode edrx_mode; ///< Requested eDRX mode. + EdrxActTType act_type; ///< Radio access type for eDRX. + kEDrx edrx_val; ///< Requested and provided eDRX timing values. + std::uint8_t rai_mode; ///< Requested release assistance indication mode. + std::uint8_t bands_mode; ///< Driver-specific band-selection mode. + std::vector + bands; ///< Requested band identifiers for the selected driver. + kModemMode modem_mode; ///< Requested radio access mode. + std::vector + power; ///< Requested per-band transmit-power settings. }; +/** + * @brief Base-station information carried in modem configuration. + */ struct ModemBaseStation { AE_REFLECT_MEMBERS(cell_identifier) - std::uint32_t cell_identifier; + std::uint32_t cell_identifier; ///< Cell identifier, when available. }; +/** + * @brief Serial, SIM, network, authentication, and power configuration. + * @note Field support depends on the driver. Supplying credentials or an SSL + * certificate does not imply that the driver implements the corresponding + * feature. + */ struct ModemInit { - AE_REFLECT_MEMBERS(serial_init, psp, bs, pin, use_pin, operator_name, - apn_name, apn_user, apn_pass, modem_mode, auth_type, - use_auth, auth_user, auth_pass, ssl_cert, use_ssl) - SerialInit serial_init; - ModemPowerSaveParam psp; - ModemBaseStation bs; - std::uint16_t pin; - bool use_pin; - kModemMode modem_mode; - std::string operator_code; - std::string operator_name; - std::string apn_name; - std::string apn_user; - std::string apn_pass; - kAuthType auth_type; - bool use_auth; - std::string auth_user; - std::string auth_pass; - std::string ssl_cert; - bool use_ssl; + AE_REFLECT_MEMBERS(serial_init, psp, bs, pin, use_pin, operator_code, + operator_name, apn_name, apn_user, apn_pass, modem_mode, + auth_type, use_auth, auth_user, auth_pass, ssl_cert, + use_ssl) + SerialInit serial_init; ///< Platform serial port configuration. + ModemPowerSaveParam psp; ///< Requested power-saving configuration. + ModemBaseStation bs; ///< Base-station configuration data. + std::uint16_t pin; ///< Numeric SIM PIN used when use_pin is enabled. + bool use_pin; ///< Whether to submit the configured SIM PIN. + kModemMode modem_mode; ///< Requested radio access mode. + std::string operator_code; ///< Numeric operator code; an empty value permits + ///< automatic selection. + std::string operator_name; ///< Operator name, preferred over operator_code + ///< when supported. + std::string apn_name; ///< Access point name for packet data. + std::string apn_user; ///< APN authentication user name. + std::string apn_pass; ///< APN authentication password. + kAuthType auth_type; ///< Requested APN authentication method. + bool use_auth; ///< Optional authentication flag; support is driver-specific. + std::string auth_user; ///< Optional authentication user name. + std::string auth_pass; ///< Optional authentication password. + std::string ssl_cert; ///< Optional SSL certificate configuration. + bool use_ssl; ///< Optional SSL flag; support is driver-specific. }; +/** + * @brief Signed modem-local socket identifier. + */ using ConnectionIndex = std::int8_t; +/** + * @brief Sentinel for an absent or unopened modem connection. + */ static constexpr ConnectionIndex kInvalidConnectionIndex = -1; } // namespace ae #endif diff --git a/aether/modems/modem_factory.cpp b/aether/modems/modem_factory.cpp index 62e773c50..707fa757f 100644 --- a/aether/modems/modem_factory.cpp +++ b/aether/modems/modem_factory.cpp @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * @file modem_factory.cpp + * @brief Compile-time selection and construction of an AT modem driver. + */ + #include "aether/modems/modem_factory.h" #if AE_SUPPORT_MODEMS diff --git a/aether/modems/modem_factory.h b/aether/modems/modem_factory.h index 4822fbfff..b2bfbdc1d 100644 --- a/aether/modems/modem_factory.h +++ b/aether/modems/modem_factory.h @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * @file modem_factory.h + * @brief Factory for the modem driver selected by compile-time configuration. + */ + #ifndef AETHER_MODEMS_MODEM_FACTORY_H_ #define AETHER_MODEMS_MODEM_FACTORY_H_ @@ -27,8 +32,21 @@ # include "aether/modems/imodem_driver.h" namespace ae { +/** + * @brief Construct the modem driver enabled in the build configuration. + */ class ModemDriverFactory { public: + /** + * @brief Create a driver and start its serial initialization. + * @param ae_context Non-owning runtime context that must outlive the driver. + * @param poller Poller used by the platform serial port implementation. + * @param modem_init Serial, SIM, network, and power configuration. + * @return Exclusive ownership of the selected modem driver. + * @note Selection priority is BG95, SIM7070, then Thingy91X. Enabling no + * driver produces a compile-time error; this factory does not detect attached + * hardware. + */ static std::unique_ptr CreateModem(AeContext const& ae_context, IPoller::ptr const& poller, ModemInit modem_init); diff --git a/aether/modems/modems_tele.h b/aether/modems/modems_tele.h index 3abfb597f..e96e56bb0 100644 --- a/aether/modems/modems_tele.h +++ b/aether/modems/modems_tele.h @@ -13,6 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * @file modems_tele.h + * @brief Telemetry include shared by modem driver implementations. + */ #ifndef AETHER_MODEMS_MODEMS_TELE_H_ #define AETHER_MODEMS_MODEMS_TELE_H_ diff --git a/aether/modems/sim7070_at_modem.cpp b/aether/modems/sim7070_at_modem.cpp index 6721c575d..ab381c91e 100644 --- a/aether/modems/sim7070_at_modem.cpp +++ b/aether/modems/sim7070_at_modem.cpp @@ -14,23 +14,141 @@ * limitations under the License. */ +/** + * @file sim7070_at_modem.cpp + * @brief SIM7070 startup retries, socket I/O, and orderly modem shutdown. + */ + #include "aether/modems/sim7070_at_modem.h" #if AE_SUPPORT_MODEMS && AE_ENABLE_SIM7070 +# include +# include +# include # include +# include # include "aether-miscpp/misc/override.h" +# include "aether/clock.h" # include "aether/executors/executors.h" -# include "aether/serial_ports/at_support/at_stage.h" # include "aether/serial_ports/at_support/at_request.h" +# include "aether/serial_ports/at_support/at_stage.h" # include "aether/serial_ports/serial_port_factory.h" +# include "aether/modems/esp32_modem_boot.h" # include "aether/modems/modems_tele.h" namespace ae { -static constexpr auto kWaitOk = at::Wait{"OK"}; +using namespace std::chrono_literals; + +static const auto kWaitOk = at::Wait{"OK"}; namespace sim7070_modem_internal { +using CleanupSender = ex::AnySender; + +/** + * @brief Query SIM readiness with delayed retries after AT command errors. + * @param context Runtime context that must outlive the sender operation. + * @param at_support Dispatcher used for the AT requests. + * @param retries Number of retries allowed after the first failed request. + * @return A sender bounded by a 15-second timeout, including retry delays. + */ +CleanupSender WaitForSim(AeContext const& context, AtSupport& at_support, + int retries) { + return CleanupSender{ + at::MakeRequest(ex::just(), at_support, "AT+CPIN?", kWaitOk, + at::Wait{"+CPIN:"}) | + ex::let_error([&context, &at_support, retries](int error) { + if (retries == 0) { + return CleanupSender{ex::just_error(error)}; + } + // After CFUN=1 the modem can acknowledge the command before its SIM + // interface is ready. Retry the query without blocking the scheduler. + auto pause = ex::create( + [&context, task = TaskSubscription{}](auto& ctx) mutable noexcept { + task = context.scheduler().DelayedTask( + [&ctx]() noexcept { ex::set_value(std::move(ctx.receiver)); }, + Now() + 500ms); + if (!task) { + AE_TELED_ERROR("Failed to schedule SIM readiness check"); + assert(false && "Task allocation failed"); + } + }); + return CleanupSender{ + std::move(pause) | + ex::let_value([&context, &at_support, retries]() noexcept { + return WaitForSim(context, at_support, retries - 1); + })}; + }) | + ex::with_timeout(context, 15s)}; +} + +/** + * @brief Retry operator selection while the modem finishes waking up. + * @param context Runtime context that must outlive the sender operation. + * @param at_support Dispatcher used for the AT requests. + * @param command Complete operator-selection AT command. + * @param retries Number of retries allowed after the first failed request. + * @return A sender bounded by a 180-second timeout, including retry delays. + */ +CleanupSender SelectOperator(AeContext const& context, AtSupport& at_support, + std::string command, int retries) { + return CleanupSender{ + at::MakeRequest(ex::just(), at_support, command, kWaitOk) | + ex::let_error([&context, &at_support, command, retries](int error) { + if (retries == 0) { + return CleanupSender{ex::just_error(error)}; + } + // Operator selection can still fail transiently after CPIN is ready. + auto pause = ex::create( + [&context, task = TaskSubscription{}](auto& ctx) mutable noexcept { + task = context.scheduler().DelayedTask( + [&ctx]() noexcept { ex::set_value(std::move(ctx.receiver)); }, + Now() + 1s); + if (!task) { + AE_TELED_ERROR("Failed to schedule operator selection retry"); + assert(false && "Task allocation failed"); + } + }); + return CleanupSender{ + std::move(pause) | + ex::let_value([&context, &at_support, command, retries]() noexcept { + return SelectOperator(context, at_support, command, retries - 1); + })}; + }) | + ex::with_timeout(context, 180s)}; +} + +/** + * @brief Close stale connections sequentially before completing startup. + * @param ae_context Runtime context that must outlive the sender operation. + * @param at_support Dispatcher used for the AT requests. + * @param stale_connections Connection set consumed by the cleanup sequence. + * @return A sender that completes when cleanup ends or a request fails. + */ +CleanupSender CleanupConnections(AeContext const& ae_context, + AtSupport& at_support, + std::set& stale_connections) { + if (stale_connections.empty()) { + return CleanupSender{ex::just()}; + } + + auto connection = *stale_connections.begin(); + stale_connections.erase(stale_connections.begin()); + AE_TELED_DEBUG("Close stale modem connection {}", connection); + + return CleanupSender{ + at::MakeRequest( + ex::just(), at_support, + "AT+CACLOSE=" + std::to_string(static_cast(connection)), + kWaitOk) | + ex::with_timeout(ae_context, 10s) | + ex::let_value([&ae_context, &at_support, &stale_connections]() noexcept { + return CleanupConnections(ae_context, at_support, stale_connections); + })}; +} + OpenNetworkOperationImpl::OpenNetworkOperationImpl(AeContext const& ae_context, Sim7070AtModem& self, Protocol protocol, @@ -42,14 +160,31 @@ OpenNetworkOperationImpl::OpenNetworkOperationImpl(AeContext const& ae_context, protocol_{protocol}, host_{std::move(host)}, port_{port} { + static constexpr auto kMaxConnectionIndex = std::int32_t{12}; + for (auto index = std::int32_t{0}; index <= kMaxConnectionIndex; ++index) { + auto connection = static_cast(index); + if (!self.connections_.contains(connection) && + !self.opening_connections_.contains(connection)) { + handle_ = index; + self.opening_connections_.emplace(connection); + break; + } + } + AE_TELED_DEBUG("Open {} connection for {}:{}", protocol == Protocol::kTcp ? "tcp" : "udp", host_, port_); RunPipeline(); } auto OpenNetworkOperationImpl::Pipeline() { + using res = ex::AnySender; + if (handle_ < 0) { + AE_TELED_ERROR("No free modem connection index"); + return res{ex::just_error(-6)}; + } + // AT+CAOPEN=,,,,[,] - auto handle = self_->next_connection_index_++; auto protocol_str = [&]() -> std::string_view { if (protocol_ == Protocol::kTcp) { return "TCP"; @@ -60,40 +195,56 @@ auto OpenNetworkOperationImpl::Pipeline() { return "UNKNOWN"; }(); - return ex::just() | - at::MakeRequest(at_support_, - Format(R"(AT+CAOPEN={},0,"{}","{}",{})", handle, - protocol_str, host_, port_), - at::Wait{Format("+CAOPEN: {},0", handle), - [handle](AtBuffer&, auto pos) { - std::int32_t result_code{}; - int expected_handle = - static_cast(handle); - if (at_support::ParseResponse( - *pos, "+CAOPEN", result_code, - expected_handle)) { - return result_code == 0; - } - return false; - }}) | - ex::with_timeout(ae_context_, 10s); + return res{ex::just() | + at::MakeRequest(at_support_, + Format(R"(AT+CAOPEN={},0,"{}","{}",{})", handle_, + protocol_str, host_, port_), + at::Wait{Format("+CAOPEN: {},0", handle_), + [this](AtBuffer&, auto pos) { + std::int32_t response_handle{}; + std::int32_t result_code{}; + if (!at_support::ParseResponse( + *pos, "+CAOPEN", + response_handle, result_code) || + response_handle != handle_) { + return false; + } + open_result_ = result_code; + return true; + }}) | + ex::with_timeout(ae_context_, 10s)}; } void OpenNetworkOperationImpl::RunPipeline() { self_->operation_queue_.Push(at::Stage(ae_context_, [this]() { return Pipeline() | ex::then([this]() noexcept { + self_->opening_connections_.erase( + static_cast(handle_)); + if (open_result_ != 0) { + AE_TELED_ERROR("Open connection {} failed, CAOPEN result {}", + handle_, open_result_); + SetResult(Error{static_cast(open_result_)}); + return; + } AE_TELED_DEBUG("Opened connection {}", handle_); self_->connections_.emplace(static_cast(handle_)); SetResult(Ok{static_cast(handle_)}); }) | ex::upon_error(Override{ [&](ex::TimeoutError) noexcept { + self_->opening_connections_.erase( + static_cast(handle_)); + AE_TELED_ERROR("Open connection {} timeout", handle_); SetResult(Error{static_cast(-2)}); }, [&](std::exception_ptr) noexcept { + self_->opening_connections_.erase( + static_cast(handle_)); SetResult(Error{static_cast(-3)}); }, [&](auto err) noexcept { + self_->opening_connections_.erase( + static_cast(handle_)); SetResult(Error{static_cast(err)}); }, }); @@ -126,6 +277,7 @@ void CloseNetworkOperationImpl::RunPipeline() { self_->operation_queue_.Push(at::Stage(ae_context_, [this]() { return Pipeline() | ex::then([this]() noexcept { self_->connections_.erase(connect_index_); + self_->recv_in_queue_.erase(connect_index_); SetResult(Ok{kIgnore}); }) | ex::upon_error(Override{ @@ -315,27 +467,153 @@ auto ModemStartOperation::SetupNetwork( return "AT+COPS=0"; // Auto }); - return at::MakeRequest(at_support_, std::move(cmd), at::Wait{"OK"}) | - ex::with_timeout(ae_context_, 120s) | - at::MakeRequest(at_support_, - R"(AT+CGDCONT=1,"IP",")" + apn_name + "\"", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, - "AT+CNCFG=0,0,\"" + apn_name + "\",\"" + apn_user + - "\",\"" + apn_pass + "\"," + type, - kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, "AT+CREG=1;+CGREG=1;+CEREG=1", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, - "AT+CNACT=0,1", // Activate the PDP context - kWaitOk) | - ex::with_timeout(ae_context_, 30s); + auto registration = CleanupSender{ + at::MakeRequest(ex::just(), at_support_, "AT+CREG=1;+CGREG=1;+CEREG=1", + kWaitOk) | + ex::with_timeout(ae_context_, 180s) | + at::MakeRequest(at_support_, "AT+CEREG?", kWaitOk, + at::Wait{"+CEREG:", + [this](AtBuffer&, auto pos) { + std::int32_t reporting_mode{}; + std::int32_t registration_status{}; + if (!at_support::ParseResponse( + *pos, "+CEREG", reporting_mode, + registration_status)) { + return false; + } + network_registered_ = + registration_status == 1 || + registration_status == 5; + return true; + }}) | + ex::with_timeout(ae_context_, 1s) | + ex::let_value([this, cmd = std::move(cmd)]() mutable noexcept { + using res = ex::AnySender; + + if (network_registered_) { + AE_TELED_DEBUG("Modem is already registered in the network"); + return res{ex::just()}; + } + return res{SelectOperator(ae_context_, at_support_, std::move(cmd), 5)}; + }) | + ex::let_value([this]() noexcept { + return at::MakeRequest( + ex::just(), at_support_, "AT+CEREG?", kWaitOk, + at::Wait{"+CEREG:", + [this](AtBuffer&, auto pos) { + std::int32_t reporting_mode{}; + std::int32_t registration_status{}; + if (!at_support::ParseResponse( + *pos, "+CEREG", reporting_mode, + registration_status)) { + return false; + } + network_registered_ = registration_status == 1 || + registration_status == 5; + return true; + }}) | + ex::with_timeout(ae_context_, 1s); + }) | + ex::let_value([this]() noexcept { + auto success = []() noexcept { return ex::just(); }; + auto error = []() noexcept { return ex::just_error(-5); }; + using res = ex::variant_sender, + std::invoke_result_t>; + + if (!network_registered_) { + AE_TELED_ERROR("Modem is not registered in the network"); + return res{error()}; + } + return res{success()}; + })}; + auto context = CleanupSender{ + ex::just() | + at::MakeRequest(at_support_, R"(AT+CGDCONT=1,"IP",")" + apn_name + "\"", + kWaitOk) | + ex::with_timeout(ae_context_, 180s) | + at::MakeRequest(at_support_, + "AT+CNCFG=0,0,\"" + apn_name + "\",\"" + apn_user + + "\",\"" + apn_pass + "\"," + type, + kWaitOk) | + ex::with_timeout(ae_context_, 180s) | + at::MakeRequest(at_support_, "AT+CNACT?", kWaitOk, + at::Wait{"+CNACT: 0,", + [this](AtBuffer&, auto pos) { + std::int32_t context{}; + std::int32_t status{}; + if (!at_support::ParseResponse( + *pos, "+CNACT", context, status)) { + return false; + } + pdp_active_ = status == 1; + return true; + }}) | + ex::with_timeout(ae_context_, 1s)}; + auto activation = CleanupSender{ + std::move(registration) | + ex::let_value([context = std::move(context)]() mutable noexcept { + return std::move(context); + }) | + ex::let_value([this]() noexcept { + auto activate = [this]() noexcept { + return at::MakeRequest( + ex::just(), at_support_, + "AT+CNACT=0,1", // Activate the PDP context + kWaitOk, + at::Wait{"+APP PDP: 0,", [this](AtBuffer&, auto pos) { + auto const& response = *pos; + auto line = std::string_view{ + reinterpret_cast(response.data()), + response.size()}; + pdp_active_ = line.find("+APP PDP: 0,ACTIVE") != + std::string_view::npos; + return true; + }}); + }; + using res = ex::AnySender; + + if (pdp_active_) { + AE_TELED_DEBUG("PDP context is already active"); + return res{ex::just()}; + } + return res{activate()}; + }) | + ex::with_timeout(ae_context_, 180s)}; + return std::move(activation) | ex::let_value([this]() noexcept { + stale_connections_.clear(); + socket_state_listener_.emplace( + at_support_.dispatcher(), + "+CASTATE:", [this](AtBuffer&, auto pos) { + std::int32_t connection{}; + std::int32_t state{}; + if (!at_support::ParseResponse(*pos, "+CASTATE", connection, + state)) { + return; + } + if (state == 1 || state == 2) { + stale_connections_.emplace( + static_cast(connection)); + } + }); + return at::MakeRequest(ex::just(), at_support_, "AT+CASTATE?", + kWaitOk) | + ex::with_timeout(ae_context_, 10s); + }) | + ex::let_value([this]() noexcept { + socket_state_listener_.reset(); + return CleanupConnections(ae_context_, at_support_, + stale_connections_); + }) | + ex::then([this]() noexcept { + self_->connections_.clear(); + self_->opening_connections_.clear(); + self_->recv_in_queue_.clear(); + }); } auto ModemStartOperation::CheckSimStatus() { - return at::MakeRequest(at_support_, "AT+CPIN?", kWaitOk) | - ex::with_timeout(ae_context_, 1s); + return WaitForSim(ae_context_, at_support_, 20); } auto ModemStartOperation::SetupSim(std::uint16_t pin) { @@ -370,9 +648,17 @@ auto ModemStartOperation::Pipeline() { ex::set_error(std::move(ctx.receiver), -1); } }) | - CheckSimStatus() | ex::let_value([&]() noexcept { + ex::let_value([this]() noexcept { + // Stop disables the SIM interface along with the radio. Restore + // full functionality before querying the SIM on the next launch. + return at::MakeRequest(ex::just(), at_support_, "AT+CFUN=1,0", + kWaitOk) | + ex::with_timeout(ae_context_, 30s); + }) | + ex::let_value([this]() noexcept { return CheckSimStatus(); }) | + ex::let_value([&]() noexcept { auto setup_sim = [this]() noexcept { - return ex::just() | SetupSim(modem_init_.use_pin); + return ex::just() | SetupSim(modem_init_.pin); }; using res = ex::variant_sender, @@ -383,20 +669,25 @@ auto ModemStartOperation::Pipeline() { } return res{ex::just()}; }) | - at::MakeRequest(at_support_, "AT+CFUN=1,0", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | SetBaudRate(modem_init_.serial_init.baud_rate) | - SetNetMode(modem_init_.modem_mode) | - SetupNetwork(modem_init_.operator_name, modem_init_.operator_code, - modem_init_.apn_name, modem_init_.apn_user, - modem_init_.apn_pass, modem_init_.modem_mode, - modem_init_.auth_type); + SetNetMode(modem_init_.modem_mode) | ex::let_value([this]() noexcept { + // Keep the large network setup type out of the preceding pipeline. + return CleanupSender{SetupNetwork( + modem_init_.operator_name, modem_init_.operator_code, + modem_init_.apn_name, modem_init_.apn_user, modem_init_.apn_pass, + modem_init_.modem_mode, modem_init_.auth_type)}; + }); } void ModemStartOperation::RunPipeline() { // all AT operations must be in operation_queue_ self_->operation_queue_.Push(at::Stage(ae_context_, [this]() noexcept { return Pipeline() | ex::then([&]() noexcept { + if (!pdp_active_) { + AE_TELED_ERROR("PDP context activation failed"); + SetResult(Error{static_cast(-4)}); + return; + } self_->started_ = true; self_->SetupPoll(); SetResult(Ok{kIgnore}); @@ -423,46 +714,99 @@ ModemStopOperation::ModemStopOperation(AeContext const& ae_context, RunPipeline(); } +ex::AnySender ModemStopOperation::CloseSockets() { + if (remaining_.empty()) { + return ex::AnySender{ex::just()}; + } + auto connection = *remaining_.begin(); + remaining_.erase(remaining_.begin()); + return ex::AnySender{ + at::MakeRequest(ex::just(), at_support_, + "AT+CACLOSE=" + std::to_string(connection), kWaitOk) | + ex::with_timeout(ae_context_, 10s) | + ex::then([this, connection]() noexcept { + self_->connections_.erase(connection); + }) | + ex::upon_error([this, connection](auto&&) noexcept { + AE_TELED_ERROR("Sim7070 failed to close socket {} during shutdown", + connection); + failed_ = true; + }) | + ex::let_value([this]() noexcept { return CloseSockets(); })}; +} + auto ModemStopOperation::Pipeline() { - return ex::create( - [&](auto& ctx) noexcept { - if (!self_->started_) { - ex::set_error(std::move(ctx.receiver), -1); - return; + using Sender = ex::AnySender; + // Erase intermediate sender types to keep MSVC template memory bounded. + auto query = + Sender{CloseSockets() | ex::let_value([this]() noexcept { + return at::MakeRequest( + ex::just(), at_support_, "AT+CNACT?", kWaitOk, + at::Wait{"+CNACT: 0,", + [this](AtBuffer&, auto pos) { + std::int32_t context{}; + std::int32_t status{}; + if (!at_support::ParseResponse( + *pos, "+CNACT", context, status)) { + return false; + } + pdp_active_ = status != 0; + return true; + }}) | + ex::with_timeout(ae_context_, 1s) | + ex::upon_error([this](auto&&) noexcept { + // Unknown context state: still attempt deactivation. + AE_TELED_ERROR( + "Sim7070 failed to query context during shutdown"); + failed_ = true; + }); + })}; + auto deactivate = + Sender{std::move(query) | ex::let_value([this]() noexcept { + if (!pdp_active_) { + return Sender{ex::just()}; } - ex::set_value(std::move(ctx.receiver)); - }) | - at::MakeRequest(at_support_, "AT+CNACT=0,0", kWaitOk) | - ex::with_timeout(ae_context_, 10s) | - at::MakeRequest(at_support_, "", at::Wait{"+APP PDP: 0,DEACTIVE"}, - kWaitOk) | - ex::with_timeout(ae_context_, 10s) | - at::MakeRequest(at_support_, "ATZ", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, "AT+CFUN=0", kWaitOk) | - ex::with_timeout(ae_context_, 1s); + // Install both waits before sending: the URC can precede OK. + return Sender{ + at::MakeRequest(ex::just(), at_support_, "AT+CNACT=0,0", + kWaitOk, at::Wait{"+APP PDP: 0,DEACTIVE"}) | + ex::with_timeout(ae_context_, 10s) | + ex::upon_error([this](auto&&) noexcept { + AE_TELED_ERROR("Sim7070 context deactivation failed"); + failed_ = true; + })}; + })}; + return std::move(deactivate) | ex::let_value([this]() noexcept { + // Normal power-off also detaches LTE if PDP cleanup failed. + // CPOWD reports NORMAL POWER DOWN, not OK (AT manual 5.2.1). + return at::MakeRequest(ex::just(), at_support_, "AT+CPOWD=1", + at::Wait{"NORMAL POWER DOWN"}) | + ex::with_timeout(ae_context_, 30s); + }); } void ModemStopOperation::RunPipeline() { self_->operation_queue_.Push(at::Stage(ae_context_, [this]() { - return Pipeline() | ex::then([&]() noexcept { + remaining_ = self_->connections_; + return Pipeline() | + ex::then([this]() noexcept { self_->connections_.clear(); }) | + ex::upon_error([this](auto&&) noexcept { + AE_TELED_ERROR("Sim7070 power-off failed or timed out"); + failed_ = true; + }) | + ex::then([this]() noexcept { self_->started_ = false; + // Stop is terminal: release the port even if the object graph + // retains this driver, and also when deactivation failed. + self_->serial_->Close(); + if (failed_) { + SetResult(Error{static_cast(-1)}); + return; + } SetResult(Ok{kIgnore}); - }) | - ex::upon_error(Override{ - [&](ex::TimeoutError) noexcept { - SetResult(Error{static_cast(-2)}); - }, - [&](std::exception_ptr const&) noexcept { - SetResult(Error{static_cast(-3)}); - }, - [&](auto err) noexcept { - SetResult(Error{static_cast(err)}); - }, }); })); } - ModemSetPowerSaveParamOperation::ModemSetPowerSaveParamOperation( AeContext const& ae_context, Sim7070AtModem& self, ModemPowerSaveParam psp) : ae_context_{ae_context}, self_{&self}, psp_{std::move(psp)} { @@ -497,10 +841,9 @@ ModemPowerOffOperation::ModemPowerOffOperation(AeContext const& ae_context, } auto ModemPowerOffOperation::Pipeline() { - return ex::just() | at::MakeRequest(at_support_, "AT+CFUN=1", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, "AT+CPOWD=1", kWaitOk) | - ex::with_timeout(ae_context_, 5s); + return at::MakeRequest(ex::just(), at_support_, "AT+CPOWD=1", + at::Wait{"NORMAL POWER DOWN"}) | + ex::with_timeout(ae_context_, 30s); } void ModemPowerOffOperation::RunPipeline() { @@ -520,10 +863,16 @@ void ModemPowerOffOperation::RunPipeline() { Sim7070AtModem::Sim7070AtModem(AeContext const& ae_context, IPoller::ptr const& poller, ModemInit modem_init) + : Sim7070AtModem{ae_context, modem_init, + SerialPortFactory::CreatePort(ae_context, poller, + modem_init.serial_init)} {} + +Sim7070AtModem::Sim7070AtModem(AeContext const& ae_context, + ModemInit modem_init, + std::unique_ptr serial) : ae_context_{ae_context}, modem_init_{std::move(modem_init)}, - serial_{SerialPortFactory::CreatePort(ae_context_, poller, - modem_init_.serial_init)}, + serial_{std::move(serial)}, at_support_{*serial_}, operation_queue_{}, open_network_pool_{ae_context_}, @@ -537,10 +886,20 @@ Sim7070AtModem::Sim7070AtModem(AeContext const& ae_context, void Sim7070AtModem::Init() { operation_queue_.Push(at::Stage(ae_context_, [this]() { - return at::MakeRequest(ex::just(), at_support_, "AT", kWaitOk) | - at::MakeRequest(at_support_, "ATE0", kWaitOk) | - at::MakeRequest(at_support_, "AT+CMEE=1", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | ex::then([&]() noexcept { +# if defined(ESP_PLATFORM) && defined(AE_MODEM_PWR_GPIO) && \ + defined(AE_MODEM_DTR_GPIO) + auto ready = + esp32_modem_boot_internal::EnsureReady(ae_context_, at_support_); +# else + auto ready = at::MakeRequest(ex::just(), at_support_, "AT", kWaitOk) | + ex::with_timeout(ae_context_, 1s); +# endif + return std::move(ready) | ex::let_value([this]() noexcept { + return ex::just() | at::MakeRequest(at_support_, "ATE0", kWaitOk) | + at::MakeRequest(at_support_, "AT+CMEE=1", kWaitOk) | + ex::with_timeout(ae_context_, 1s); + }) | + ex::then([&]() noexcept { AE_TELED_INFO("Sim7070AtModem init success"); initiated_ = true; }) | @@ -552,11 +911,17 @@ void Sim7070AtModem::Init() { AE_TELED_ERROR("Sim7070AtModem init failed, with error {}", error); }, + [](std::exception_ptr const&) noexcept { + AE_TELED_ERROR("Sim7070AtModem init failed, with exception"); + }, }); })); } ModemOperation* Sim7070AtModem::Start() { + if (stopping_) { + return nullptr; + } if (!modem_start_operation_ || modem_start_operation_->is_finished()) { if (started_) { modem_start_operation_ = std::make_unique< @@ -572,23 +937,23 @@ ModemOperation* Sim7070AtModem::Start() { } ModemOperation* Sim7070AtModem::Stop() { - if (!modem_stop_operation_ || modem_stop_operation_->is_finished()) { - if (!started_) { - modem_stop_operation_ = std::make_unique< - sim7070_modem_internal::ModemStoppedAlreadyOperation>(); - } else { - modem_stop_operation_ = - std::make_unique( - ae_context_, *this); - } + if (!modem_stop_operation_) { + stopping_ = true; + poll_listener_.reset(); + buffer_full_listener_.reset(); + connection_closed_listener_.reset(); + modem_stop_operation_ = + std::make_unique( + ae_context_, *this); } - return modem_stop_operation_.get(); } - OpenNetworkOperation* Sim7070AtModem::OpenNetwork(Protocol protocol, std::string const& host, std::uint16_t port) { + if (stopping_) { + return nullptr; + } // setup polling on demand if (!poll_listener_) { SetupPoll(); @@ -600,12 +965,18 @@ OpenNetworkOperation* Sim7070AtModem::OpenNetwork(Protocol protocol, } ModemOperation* Sim7070AtModem::CloseNetwork(ConnectionIndex connect_index) { + if (stopping_) { + return modem_stop_operation_.get(); + } auto* op = close_network_pool_.Create(ae_context_, *this, connect_index); return op; } WriteOperation* Sim7070AtModem::WritePacket( ConnectionIndex connect_index, std::span data) { + if (stopping_) { + return nullptr; + } if (data.size() > kModemMTU) { assert(false); return nullptr; @@ -621,6 +992,9 @@ Sim7070AtModem::DataEvent::Subscriber Sim7070AtModem::data_event() { ModemOperation* Sim7070AtModem::SetPowerSaveParam( ModemPowerSaveParam const& psp) { + if (stopping_) { + return nullptr; + } if (!modem_set_psp_operation_ || modem_set_psp_operation_->is_finished()) { modem_set_psp_operation_ = std::make_unique< sim7070_modem_internal::ModemSetPowerSaveParamOperation>(ae_context_, @@ -631,6 +1005,9 @@ ModemOperation* Sim7070AtModem::SetPowerSaveParam( } ModemOperation* Sim7070AtModem::PowerOff() { + if (stopping_) { + return nullptr; + } if (!modem_poweroff_operation_ || modem_poweroff_operation_->is_finished()) { modem_poweroff_operation_ = std::make_unique( @@ -641,32 +1018,71 @@ ModemOperation* Sim7070AtModem::PowerOff() { // ============================private members=============================== // void Sim7070AtModem::SetupPoll() { + if (stopping_) { + return; + } + connection_closed_listener_.emplace( + at_support_.dispatcher(), "+CASTATE:", [this](auto&, auto pos) { + std::int32_t cid{-1}; + std::int32_t state{-1}; + if (!at_support::ParseResponse(*pos, "+CASTATE", cid, state) || + cid < 0 || cid > std::numeric_limits::max() || + state != 0) { + return; + } + auto connection = static_cast(cid); + if (connections_.erase(connection) == 0) { + return; + } + AE_TELED_ERROR("Modem connection {} closed", cid); + NotifyConnectionClosed(connection); + }); poll_listener_.emplace(at_support_.dispatcher(), "+CADATAIND: ", [this](auto&, auto pos) { std::int32_t cid{}; at_support::ParseResponse(*pos, "+CADATAIND", cid); PollEvent(cid); }); + buffer_full_listener_.emplace( + at_support_.dispatcher(), "+CAURC: buffer full,", + [this](auto&, auto pos) { + auto line = std::string_view{reinterpret_cast(pos->data()), + pos->size()}; + auto separator = line.find_last_of(','); + if (separator == std::string_view::npos) { + return; + } + auto cid = FromChars(line.substr(separator + 1)); + if (cid.has_value()) { + PollEvent(*cid); + } + }); } void Sim7070AtModem::PollEvent(std::int32_t handle) { + if (stopping_) { + return; + } auto it = connections_.find(static_cast(handle)); if (it == std::end(connections_)) { AE_TELED_ERROR("Poll unknown handle {}", handle); return; } - if (recv_in_queue_ > 0) { + auto connection = static_cast(handle); + if (!recv_in_queue_.emplace(connection).second) { return; } - recv_in_queue_++; - auto finally = [this]() noexcept { - return ex::then([this]() noexcept { recv_in_queue_--; }) | - ex::upon_error([this]() noexcept { recv_in_queue_--; }); + auto finally = [this, connection]() noexcept { + return ex::then([this, connection]() noexcept { + recv_in_queue_.erase(connection); + }) | + ex::upon_error([this, connection]() noexcept { + recv_in_queue_.erase(connection); + }); }; - operation_queue_.Push(at::Stage(ae_context_, [this, connection{*it}, - finally]() { + operation_queue_.Push(at::Stage(ae_context_, [this, connection, finally]() { auto conn = static_cast(connection); return ex::just() | at::MakeRequest( diff --git a/aether/modems/sim7070_at_modem.h b/aether/modems/sim7070_at_modem.h index 82ebbfeb9..5b4ca778a 100644 --- a/aether/modems/sim7070_at_modem.h +++ b/aether/modems/sim7070_at_modem.h @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * @file sim7070_at_modem.h + * @brief SIM7070 AT driver and its queued asynchronous operations. + */ + #ifndef AETHER_MODEMS_SIM7070_AT_MODEM_H_ #define AETHER_MODEMS_SIM7070_AT_MODEM_H_ @@ -27,6 +32,7 @@ # include "aether/actions/actions_queue.h" # include "aether/actions/repeatable_task.h" # include "aether/ae_context.h" +# include "aether/executors/any_sender.h" # include "aether/poller/poller.h" # include "aether/serial_ports/at_support/at_support.h" # include "aether/serial_ports/iserial_port.h" @@ -37,6 +43,9 @@ namespace ae { class Sim7070AtModem; namespace sim7070_modem_internal { +/** + * @brief Queue a modem socket open request and publish its connection index. + */ class OpenNetworkOperationImpl final : public OpenNetworkOperation { public: explicit OpenNetworkOperationImpl(AeContext const& ae_context, @@ -54,8 +63,12 @@ class OpenNetworkOperationImpl final : public OpenNetworkOperation { std::string host_; std::uint16_t port_; std::int32_t handle_{-1}; + std::int32_t open_result_{-1}; }; +/** + * @brief Queue closure of a modem socket and publish the outcome. + */ class CloseNetworkOperationImpl final : public ModemOperation { public: explicit CloseNetworkOperationImpl(AeContext const& ae_context, @@ -72,6 +85,9 @@ class CloseNetworkOperationImpl final : public ModemOperation { ConnectionIndex connect_index_; }; +/** + * @brief Queue transmission of borrowed data on a modem socket. + */ class WriteOperationImpl final : public WriteOperation { public: explicit WriteOperationImpl(AeContext const& ae_context, Sim7070AtModem& self, @@ -89,11 +105,17 @@ class WriteOperationImpl final : public WriteOperation { std::span data_; }; +/** + * @brief Immediately successful result for an already started modem. + */ class ModemStartedAlreadyOperation final : public ModemOperation { public: explicit ModemStartedAlreadyOperation() { SetResult(Ok{kIgnore}); }; }; +/** + * @brief Configure the modem and establish network service. + */ class ModemStartOperation final : public ModemOperation { public: explicit ModemStartOperation(AeContext const& ae_context, @@ -118,27 +140,36 @@ class ModemStartOperation final : public ModemOperation { Sim7070AtModem* self_; ModemInit modem_init_; AtSupport& at_support_; + bool network_registered_{false}; + bool pdp_active_{false}; + std::set stale_connections_; + std::optional socket_state_listener_; }; -class ModemStoppedAlreadyOperation final : public ModemOperation { - public: - explicit ModemStoppedAlreadyOperation() { SetResult(Ok{kIgnore}); }; -}; - +/** + * @brief Run the modem-specific network shutdown sequence. + */ class ModemStopOperation final : public ModemOperation { public: explicit ModemStopOperation(AeContext const& ae_context, Sim7070AtModem& self); private: + ex::AnySender CloseSockets(); auto Pipeline(); void RunPipeline(); AeContext ae_context_; Sim7070AtModem* self_; AtSupport& at_support_; + std::set remaining_; + bool failed_{false}; + bool pdp_active_{true}; }; +/** + * @brief Run the modem-specific power-saving configuration operation. + */ class ModemSetPowerSaveParamOperation final : public ModemOperation { public: explicit ModemSetPowerSaveParamOperation(AeContext const& ae_context, @@ -154,6 +185,9 @@ class ModemSetPowerSaveParamOperation final : public ModemOperation { ModemPowerSaveParam psp_; }; +/** + * @brief Run the modem-specific power-down command sequence. + */ class ModemPowerOffOperation final : public ModemOperation { public: explicit ModemPowerOffOperation(AeContext const& ae_context, @@ -169,6 +203,15 @@ class ModemPowerOffOperation final : public ModemOperation { }; } // namespace sim7070_modem_internal +/** + * @brief SIM7070 AT driver and its queued asynchronous operations. + * + * Owns the serial port, AT dispatcher, operation queue, and action pools. + * Construction schedules serial initialization. Call Start() to establish + * network service, and drive the scheduler until Stop() finishes before + * releasing the driver. + * @see IModemDriver + */ class Sim7070AtModem final : public IModemDriver { friend class sim7070_modem_internal::OpenNetworkOperationImpl; friend class sim7070_modem_internal::CloseNetworkOperationImpl; @@ -180,37 +223,73 @@ class Sim7070AtModem final : public IModemDriver { static constexpr std::uint16_t kModemMTU{1520}; public: + /** + * @brief Create the driver and schedule serial initialization. + * @param ae_context Non-owning runtime context that must outlive the driver. + * @param poller Poller used by the serial port implementation. + * @param modem_init Configuration copied or moved into the driver. + */ explicit Sim7070AtModem(AeContext const& ae_context, IPoller::ptr const& poller, ModemInit modem_init); + /** + * @copydoc IModemDriver::Start + */ ModemOperation* Start() override; + /** + * @brief Stop polling, close tracked sockets, and power off the modem. + * + * Shutdown is queued after pending operations. Socket cleanup continues after + * individual close failures; the final operation reports cleanup errors. + * The driver deactivates PDP context 0, sends AT+CPOWD=1 and waits up to + * 30 seconds for NORMAL POWER DOWN before closing the serial port. + * @return The same driver-owned stop operation on repeated calls. It remains + * available until driver destruction. + * The serial port is closed before completion, including on shutdown errors. + * @note Stop is terminal for this instance. New start, open, write, + * power-save, and power-off requests are rejected once stopping begins. + */ ModemOperation* Stop() override; + /** + * @copydoc IModemDriver::OpenNetwork + */ OpenNetworkOperation* OpenNetwork(Protocol protocol, std::string const& host, std::uint16_t port) override; + /** + * @copydoc IModemDriver::CloseNetwork + */ ModemOperation* CloseNetwork(ConnectionIndex connect_index) override; + /** + * @copydoc IModemDriver::WritePacket + */ WriteOperation* WritePacket(ConnectionIndex connect_index, std::span data) override; + /** + * @copydoc IModemDriver::data_event + */ DataEvent::Subscriber data_event() override; + /** + * @copydoc IModemDriver::SetPowerSaveParam + * @note Currently returns success without applying the requested parameters. + */ ModemOperation* SetPowerSaveParam(ModemPowerSaveParam const& psp) override; + /** + * @copydoc IModemDriver::PowerOff + * @note Sends AT+CPOWD=1 and waits for NORMAL POWER DOWN (up to 30 seconds). + */ ModemOperation* PowerOff() override; private: + friend struct Sim7070AtModemTestAccess; + Sim7070AtModem(AeContext const& ae_context, ModemInit modem_init, + std::unique_ptr serial); + static constexpr auto kNetworkOpActionPoolCapacity = AE_MODEM_NETWORK_OP_ACTION_POOL_CAPACITY; static constexpr auto kWriteActionPoolCapacity = AE_MODEM_WRITE_ACTION_POOL_CAPACITY; - using OpenNetworkActionPool = - ActionPool; - using CloseNetworkActionPool = - ActionPool; - using WriteActionPool = - ActionPool; - void Init(); void SetupPoll(); void PollEvent(std::int32_t handle); @@ -221,21 +300,30 @@ class Sim7070AtModem final : public IModemDriver { AtSupport at_support_; ActionsQueue operation_queue_; std::set connections_; - ConnectionIndex next_connection_index_{0}; + std::set opening_connections_; DataEvent data_event_; std::optional poll_listener_; + std::optional buffer_full_listener_; + std::optional connection_closed_listener_; std::unique_ptr modem_start_operation_; std::unique_ptr modem_stop_operation_; std::unique_ptr modem_set_psp_operation_; std::unique_ptr modem_poweroff_operation_; - OpenNetworkActionPool open_network_pool_; - CloseNetworkActionPool close_network_pool_; - WriteActionPool write_pool_; + ActionPool + open_network_pool_; + ActionPool + close_network_pool_; + ActionPool + write_pool_; bool initiated_; bool started_; - int recv_in_queue_ = 0; + bool stopping_{false}; + std::set recv_in_queue_; }; } /* namespace ae */ diff --git a/aether/modems/thingy91x_at_modem.cpp b/aether/modems/thingy91x_at_modem.cpp index 7027712eb..d9e731dbe 100644 --- a/aether/modems/thingy91x_at_modem.cpp +++ b/aether/modems/thingy91x_at_modem.cpp @@ -14,23 +14,31 @@ * limitations under the License. */ +/** + * @file thingy91x_at_modem.cpp + * @brief Thingy91X AT command pipelines, socket polling, and shutdown. + */ + #include "aether/modems/thingy91x_at_modem.h" #if AE_SUPPORT_MODEMS && AE_ENABLE_THINGY91X # include +# include # include -# include "aether-miscpp/misc/override.h" # include "aether-miscpp/misc/from_chars.h" +# include "aether-miscpp/misc/override.h" # include "aether/executors/executors.h" +# include "aether/serial_ports/at_support/at_request.h" # include "aether/serial_ports/at_support/at_stage.h" # include "aether/serial_ports/serial_port_factory.h" -# include "aether/serial_ports/at_support/at_request.h" # include "aether/modems/modems_tele.h" namespace ae { -static constexpr auto kWaitOk = at::Wait{"OK"}; +using namespace std::chrono_literals; + +static const auto kWaitOk = at::Wait{"OK"}; namespace thingy91x_modem_internal { OpenNetworkOperationImpl::OpenNetworkOperationImpl(AeContext const& ae_context, @@ -54,43 +62,69 @@ auto OpenNetworkOperationImpl::Pipeline() { return ex::just() | at::MakeRequest(at_support_, "AT#XSOCKET=1," + std::to_string(socket_type) + ",0", + kWaitOk, at::Wait{"#XSOCKET: ", [this](auto&, auto pos) { return at_support::ParseResponse( - *pos, "#XSOCKET:", handle_) + *pos, "#XSOCKET", handle_) .has_value(); }}) | - ex::with_timeout(ae_context_, 2s) | - at::MakeRequest(at_support_, - "AT#XSOCKETSELECT=" + std::to_string(handle_), - kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, "AT#XSOCKETOPT=1,20,30", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest( - at_support_, - "AT#XCONNECT=\"" + host_ + "\"," + std::to_string(port_), - kWaitOk) | - ex::with_timeout(ae_context_, 10s); + ex::with_timeout(ae_context_, 2s) | ex::let_value([this]() noexcept { + return at::MakeRequest(ex::just(), at_support_, + "AT#XSOCKETSELECT=" + std::to_string(handle_), + kWaitOk) | + ex::with_timeout(ae_context_, 1s); + }) | + ex::let_value([this]() noexcept { + return at::MakeRequest(ex::just(), at_support_, + "AT#XSOCKETOPT=1,20,30", kWaitOk) | + ex::with_timeout(ae_context_, 1s); + }) | + ex::let_value([this]() noexcept { + return at::MakeRequest( + ex::just(), at_support_, + "AT#XCONNECT=\"" + host_ + "\"," + std::to_string(port_), + kWaitOk) | + ex::with_timeout(ae_context_, 10s); + }); } void OpenNetworkOperationImpl::RunPipeline() { self_->operation_queue_.Push(at::Stage(ae_context_, [this]() { + auto fail = [this](ModemError error) -> ex::AnySender { + if (handle_ < 0) { + return ex::just() | + ex::then([this, error]() noexcept { SetResult(Error{error}); }); + } + // Roll back within this stage, before another operation selects a socket. + return at::MakeRequest(ex::just(), at_support_, + "AT#XSOCKETSELECT=" + std::to_string(handle_), + kWaitOk) | + ex::with_timeout(ae_context_, 1s) | + at::MakeRequest(at_support_, "AT#XSOCKET=0", kWaitOk) | + ex::with_timeout(ae_context_, 10s) | + ex::upon_error([this](auto&&) noexcept { + AE_TELED_ERROR( + "Failed to close modem socket {} after open error", handle_); + }) | + ex::then([this, error]() noexcept { SetResult(Error{error}); }); + }; return Pipeline() | ex::then([this]() noexcept { AE_TELED_DEBUG("Opened connection {}", handle_); self_->connections_.emplace(static_cast(handle_)); SetResult(Ok{static_cast(handle_)}); }) | - ex::upon_error(Override{ - [&](ex::TimeoutError) noexcept { - SetResult(Error{static_cast(-2)}); + ex::let_error(Override{ + [fail](ex::TimeoutError) noexcept { + AE_TELED_ERROR("Open modem connection timeout"); + return fail(static_cast(-2)); }, - [&](std::exception_ptr) noexcept { - SetResult(Error{static_cast(-3)}); + [fail](std::exception_ptr) noexcept { + return fail(static_cast(-3)); }, - [&](auto err) noexcept { - SetResult(Error{static_cast(err)}); + [fail](auto err) noexcept { + return fail(static_cast(err)); }, }); })); @@ -205,15 +239,24 @@ void WriteOperationImpl::RunPipeline() { })); } +/** + * @brief Immediately successful result for an already started modem. + */ class ModemStartedAlreadyOperation final : public ModemOperation { public: explicit ModemStartedAlreadyOperation() { SetResult(Ok{kIgnore}); } }; +/** + * @brief Configure the modem and establish network service. + */ class ModemStartOperation final : public ModemOperation { public: ModemStartOperation(AeContext const& ae_context, Thingy91xAtModem& self) - : ae_context_{ae_context}, self_{&self}, at_support_{self.at_support_} { + : ae_context_{ae_context}, + self_{&self}, + modem_init_{self.modem_init_}, + at_support_{self.at_support_} { RunPipeline(); } @@ -288,19 +331,21 @@ class ModemStartOperation final : public ModemOperation { }); return at::MakeRequest(at_support_, std::move(cmd), at::Wait{"OK"}) | - ex::with_timeout(ae_context_, std::chrono::seconds{120}) | + ex::with_timeout(ae_context_, 120s) | + at::MakeRequest(at_support_, R"(AT+CEREG=1)", kWaitOk) | + ex::with_timeout(ae_context_, 180s) | at::MakeRequest(at_support_, - R"(AT+CGDCONT=0,"IP",")" + apn_name + "\"", + R"(AT+CGDCONT=1,"IP",")" + apn_name + "\"", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - at::MakeRequest(at_support_, - R"(AT+CEREG=1,"IP",")" + apn_name + "\"", kWaitOk) | - ex::with_timeout(ae_context_, 1s); + ex::with_timeout(ae_context_, 180s); } auto CheckSimStatus() { - return at::MakeRequest(at_support_, "AT+CPIN?", kWaitOk) | - ex::with_timeout(ae_context_, 1s); + // Start the SIM timeout after the preceding network registration finishes. + return ex::let_value([this]() noexcept { + return at::MakeRequest(ex::just(), at_support_, "AT+CPIN?", kWaitOk) | + ex::with_timeout(ae_context_, 1s); + }); } auto SetupSim(std::uint16_t pin) { @@ -340,7 +385,7 @@ class ModemStartOperation final : public ModemOperation { // Disabling full functionality at::MakeRequest(at_support_, "AT+CFUN=0", kWaitOk) | ex::with_timeout(ae_context_, 1s) | - SetNetMode(kModemMode::kModeCatMNbIot) | + SetNetMode(modem_init_.modem_mode) | SetupNetwork(modem_init_.operator_name, modem_init_.operator_code, modem_init_.apn_name, modem_init_.apn_user, modem_init_.apn_pass, modem_init_.modem_mode, @@ -348,11 +393,17 @@ class ModemStartOperation final : public ModemOperation { // Enabling full functionality and waiting for network // registration at::MakeRequest(at_support_, "AT+CFUN=1", kWaitOk, - at::Wait{"+CEREG: 2"}, at::Wait{"+CEREG: 1"}) | - ex::with_timeout(ae_context_, 60s) | CheckSimStatus() | + at::Wait{"+CEREG:", + [](AtBuffer&, auto pos) noexcept { + std::int32_t status{}; + return at_support::ParseResponse( + *pos, "+CEREG", status) && + (status == 1 || status == 5); + }}) | + ex::with_timeout(ae_context_, 180s) | CheckSimStatus() | ex::let_value([&]() noexcept { auto setup_sim = [this]() noexcept { - return ex::just() | SetupSim(modem_init_.use_pin); + return ex::just() | SetupSim(modem_init_.pin); }; using res = ex::variant_sender, @@ -396,100 +447,76 @@ class ModemStartOperation final : public ModemOperation { AtSupport& at_support_; }; -class ModemStoppedAlreadyOperation final : public ModemOperation { - public: - explicit ModemStoppedAlreadyOperation() { SetResult(Ok{kIgnore}); } -}; - +/** + * @brief Run the modem-specific network shutdown sequence. + */ class ModemStopOperation final : public ModemOperation { public: - ModemStopOperation(AeContext const& ae_context, Thingy91xAtModem& self, - std::uint8_t res_mode) - : ae_context_{ae_context}, - self_{&self}, - at_support_{self_->at_support_}, - res_mode_{res_mode} { - RunPipeline(); - } - - private: - auto Pipeline() { - return ex::create( - [&](auto& ctx) noexcept { - // Modem must be started - if (!self_->started_) { - ex::set_error(std::move(ctx.receiver), -1); - return; - } - // Valid reset modes: 0 (hard) or 1 (soft) - if (res_mode_ >= 2) { - ex::set_error(std::move(ctx.receiver), -2); - return; - } - ex::set_value(std::move(ctx.receiver)); - }) | - // Disabling full functionality - at::MakeRequest(at_support_, "AT+CFUN=0", kWaitOk) | - ex::with_timeout(ae_context_, 1s) | - /** - * Performs a factory reset of the modem using the specified reset - * mode. - * This function sends an AT%XFACTORYRESET command with the - * specified mode parameter to perform a factory reset operation on - * the modem. The command execution is verified by checking for "OK" - * response within a 2-second timeout. - * - * @param[in] mode Reset operation mode. Valid values are: - * - 1: Soft reset (preserves some settings) - * - 0: Hard reset (full factory defaults) - * - * @return kModemError indicating operation status: - * - kModemError::kNoError: Reset command executed - * successfully - * - kModemError::kResetMode: Invalid mode parameter (?2) - * - Other kModemError codes: Communication error or missing - * "OK" response - * - * @note - * - The function will return kResetMode error immediately for - * invalid modes (?2) - * - Actual modem behavior depends on modem firmware implementation - * - Hard reset (mode 1) will erase all user configurations - * - Device may reboot after successful execution - * - Uses 2000ms response timeout for command verification - */ - at::MakeRequest(at_support_, - "AT%XFACTORYRESET=" + std::to_string(res_mode_), - kWaitOk) | - ex::with_timeout(ae_context_, 2s); - } - - void RunPipeline() { + ModemStopOperation(AeContext const& ae_context, Thingy91xAtModem& self) + : ae_context_{ae_context}, self_{&self}, at_support_{self.at_support_} { self_->operation_queue_.Push(at::Stage(ae_context_, [this]() { - return Pipeline() | ex::then([&]() noexcept { + // Take the snapshot after all previously queued opens/closes have + // finished. + remaining_ = self_->connections_; + return CloseSockets() | ex::let_value([this]() noexcept { + return at::MakeRequest(ex::just(), at_support_, "AT+CFUN=0", + kWaitOk) | + ex::with_timeout(ae_context_, 30s); + }) | + ex::then([this]() noexcept { self_->connections_.clear(); }) | + ex::upon_error([this](auto&&) noexcept { + AE_TELED_ERROR("Thingy91x network deactivation failed"); + failed_ = true; + }) | + ex::then([this]() noexcept { self_->started_ = false; + // Stop is terminal: release the port even if the object graph + // retains this driver, and also when deactivation failed. + self_->serial_->Close(); + if (failed_) { + SetResult(Error{static_cast(-1)}); + return; + } SetResult(Ok{kIgnore}); - }) | - ex::upon_error(Override{ - [&](ex::TimeoutError) noexcept { - SetResult(Error{static_cast(-2)}); - }, - [&](std::exception_ptr const&) noexcept { - SetResult(Error{static_cast(-3)}); - }, - [&](auto err) noexcept { - SetResult(Error{static_cast(err)}); - }, }); })); } + private: + ex::AnySender CloseSockets() { + if (remaining_.empty()) { + return ex::AnySender{ex::just()}; + } + auto connection = *remaining_.begin(); + remaining_.erase(remaining_.begin()); + return ex::AnySender{ + at::MakeRequest(ex::just(), at_support_, + "AT#XSOCKETSELECT=" + std::to_string(connection), + kWaitOk) | + ex::with_timeout(ae_context_, 1s) | + at::MakeRequest(at_support_, "AT#XSOCKET=0", kWaitOk) | + ex::with_timeout(ae_context_, 10s) | + ex::then([this, connection]() noexcept { + self_->connections_.erase(connection); + }) | + ex::upon_error([this, connection](auto&&) noexcept { + AE_TELED_ERROR("Thingy91x failed to close socket {} during shutdown", + connection); + failed_ = true; + }) | + ex::let_value([this]() noexcept { return CloseSockets(); })}; + } + AeContext ae_context_; Thingy91xAtModem* self_; AtSupport& at_support_; - std::uint8_t res_mode_; + std::set remaining_; + bool failed_{false}; }; +/** + * @brief Run the modem-specific power-saving configuration operation. + */ class ModemSetPowerSaveParamOperation final : public ModemOperation { public: ModemSetPowerSaveParamOperation(AeContext const& ae_context, @@ -664,6 +691,9 @@ class ModemSetPowerSaveParamOperation final : public ModemOperation { ModemPowerSaveParam psp_; }; +/** + * @brief Run the modem-specific power-down command sequence. + */ class ModemPowerOffOperation final : public ModemOperation { public: ModemPowerOffOperation(AeContext const& ae_context, Thingy91xAtModem& self) @@ -700,10 +730,16 @@ class ModemPowerOffOperation final : public ModemOperation { Thingy91xAtModem::Thingy91xAtModem(AeContext const& ae_context, IPoller::ptr const& poller, ModemInit modem_init) + : Thingy91xAtModem{ae_context, modem_init, + SerialPortFactory::CreatePort(ae_context, poller, + modem_init.serial_init)} {} + +Thingy91xAtModem::Thingy91xAtModem(AeContext const& ae_context, + ModemInit modem_init, + std::unique_ptr serial) : ae_context_{ae_context}, modem_init_{std::move(modem_init)}, - serial_{SerialPortFactory::CreatePort(ae_context_, poller, - modem_init_.serial_init)}, + serial_{std::move(serial)}, at_support_{*serial_}, operation_queue_{}, open_network_pool_{ae_context_}, @@ -735,6 +771,9 @@ void Thingy91xAtModem::Init() { } ModemOperation* Thingy91xAtModem::Start() { + if (stopping_) { + return nullptr; + } if (!modem_start_operation_ || modem_start_operation_->is_finished()) { if (started_) { modem_start_operation_ = std::make_unique< @@ -750,23 +789,22 @@ ModemOperation* Thingy91xAtModem::Start() { } ModemOperation* Thingy91xAtModem::Stop() { - if (!modem_stop_operation_ || modem_stop_operation_->is_finished()) { - if (!started_) { - modem_stop_operation_ = std::make_unique< - thingy91x_modem_internal::ModemStoppedAlreadyOperation>(); - } else { - modem_stop_operation_ = - std::make_unique( - ae_context_, *this, 1); - } + if (!modem_stop_operation_) { + stopping_ = true; + poll_listener_.reset(); + poll_task_.reset(); + modem_stop_operation_ = + std::make_unique( + ae_context_, *this); } - return modem_stop_operation_.get(); } - OpenNetworkOperation* Thingy91xAtModem::OpenNetwork(Protocol protocol, std::string const& host, std::uint16_t port) { + if (stopping_) { + return nullptr; + } // setup polling on demund if (!poll_listener_) { SetupPoll(); @@ -778,12 +816,18 @@ OpenNetworkOperation* Thingy91xAtModem::OpenNetwork(Protocol protocol, } ModemOperation* Thingy91xAtModem::CloseNetwork(ConnectionIndex connect_index) { + if (stopping_) { + return modem_stop_operation_.get(); + } auto* op = close_network_pool_.Create(ae_context_, *this, connect_index); return op; } WriteOperation* Thingy91xAtModem::WritePacket( ConnectionIndex connect_index, std::span data) { + if (stopping_) { + return nullptr; + } if (data.size() > kModemMTU) { assert(false); return nullptr; @@ -799,6 +843,9 @@ Thingy91xAtModem::DataEvent::Subscriber Thingy91xAtModem::data_event() { ModemOperation* Thingy91xAtModem::SetPowerSaveParam( ModemPowerSaveParam const& psp) { + if (stopping_) { + return nullptr; + } if (!modem_set_psp_operation_ || modem_set_psp_operation_->is_finished()) { modem_set_psp_operation_ = std::make_unique< thingy91x_modem_internal::ModemSetPowerSaveParamOperation>(ae_context_, @@ -808,6 +855,9 @@ ModemOperation* Thingy91xAtModem::SetPowerSaveParam( } ModemOperation* Thingy91xAtModem::PowerOff() { + if (stopping_) { + return nullptr; + } if (!modem_poweroff_operation_ || modem_poweroff_operation_->is_finished()) { modem_poweroff_operation_ = std::make_unique( @@ -819,6 +869,9 @@ ModemOperation* Thingy91xAtModem::PowerOff() { // ============================private members=============================== // void Thingy91xAtModem::SetupPoll() { + if (stopping_) { + return; + } poll_listener_.emplace( at_support_.dispatcher(), "#XPOLL: ", [this](auto&, auto pos) { // read event flags from POLL answer @@ -865,6 +918,9 @@ void Thingy91xAtModem::SetupPoll() { } void Thingy91xAtModem::PollEvent(std::int32_t handle, std::string_view flags) { + if (stopping_) { + return; + } // get connection index auto it = connections_.find(static_cast(handle)); if (it == std::end(connections_)) { diff --git a/aether/modems/thingy91x_at_modem.h b/aether/modems/thingy91x_at_modem.h index 46648c68d..0f8e44d44 100644 --- a/aether/modems/thingy91x_at_modem.h +++ b/aether/modems/thingy91x_at_modem.h @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * @file thingy91x_at_modem.h + * @brief Thingy91X AT driver and its asynchronous socket operations. + */ + #ifndef AETHER_MODEMS_THINGY91X_AT_MODEM_H_ #define AETHER_MODEMS_THINGY91X_AT_MODEM_H_ @@ -36,6 +41,9 @@ namespace ae { class Thingy91xAtModem; namespace thingy91x_modem_internal { +/** + * @brief Queue a modem socket open request and publish its connection index. + */ class OpenNetworkOperationImpl final : public OpenNetworkOperation { public: OpenNetworkOperationImpl(AeContext const& ae_context, Thingy91xAtModem& self, @@ -55,6 +63,9 @@ class OpenNetworkOperationImpl final : public OpenNetworkOperation { std::int32_t handle_{-1}; }; +/** + * @brief Queue closure of a modem socket and publish the outcome. + */ class CloseNetworkOperationImpl final : public ModemOperation { public: CloseNetworkOperationImpl(AeContext const& ae_context, Thingy91xAtModem& self, @@ -70,6 +81,9 @@ class CloseNetworkOperationImpl final : public ModemOperation { ConnectionIndex connect_index_; }; +/** + * @brief Queue transmission of borrowed data on a modem socket. + */ class WriteOperationImpl final : public WriteOperation { public: WriteOperationImpl(AeContext const& ae_context, Thingy91xAtModem& self, @@ -93,6 +107,15 @@ class ModemSetPowerSaveParamOperation; class ModemPowerOffOperation; } // namespace thingy91x_modem_internal +/** + * @brief Thingy91X AT driver and its asynchronous socket operations. + * + * Owns the serial port, AT dispatcher, operation queue, and action pools. + * Construction schedules serial initialization. Call Start() to establish + * network service, and drive the scheduler until Stop() finishes before + * releasing the driver. + * @see IModemDriver + */ class Thingy91xAtModem final : public IModemDriver { friend class thingy91x_modem_internal::OpenNetworkOperationImpl; friend class thingy91x_modem_internal::CloseNetworkOperationImpl; @@ -105,39 +128,76 @@ class Thingy91xAtModem final : public IModemDriver { static constexpr std::uint16_t kModemMTU{1024}; public: + /** + * @brief Create the driver and schedule serial initialization. + * @param ae_context Non-owning runtime context that must outlive the driver. + * @param poller Poller used by the serial port implementation. + * @param modem_init Configuration copied or moved into the driver. + */ explicit Thingy91xAtModem(AeContext const& ae_context, IPoller::ptr const& poller, ModemInit modem_init); + /** + * @copydoc IModemDriver::Start + */ ModemOperation* Start() override; + /** + * @brief Stop polling, close tracked sockets, and disable network service. + * + * Shutdown is queued after pending operations. Socket cleanup continues after + * individual close failures; the final operation reports cleanup errors. + * The driver selects and closes each socket before sending AT+CFUN=0. + * @return The same driver-owned stop operation on repeated calls. It remains + * available until driver destruction. + * The serial port is closed before completion, including on shutdown errors. + * @note Stop is terminal for this instance. New start, open, write, + * power-save, and power-off requests are rejected once stopping begins. + */ ModemOperation* Stop() override; + /** + * @copydoc IModemDriver::OpenNetwork + */ OpenNetworkOperation* OpenNetwork(ae::Protocol protocol, std::string const& host, std::uint16_t port) override; + /** + * @copydoc IModemDriver::CloseNetwork + */ ModemOperation* CloseNetwork(ConnectionIndex connect_index) override; + /** + * @copydoc IModemDriver::WritePacket + */ WriteOperation* WritePacket(ConnectionIndex connect_index, std::span data) override; + /** + * @copydoc IModemDriver::data_event + */ DataEvent::Subscriber data_event() override; + /** + * @copydoc IModemDriver::SetPowerSaveParam + * @note Applies supported radio and power-saving settings through AT + * commands. + */ ModemOperation* SetPowerSaveParam(ModemPowerSaveParam const& psp) override; + /** + * @copydoc IModemDriver::PowerOff + * @note Sends AT+CFUN=0 to enter minimum functionality; board power remains + * on. + */ ModemOperation* PowerOff() override; private: + friend struct Thingy91xAtModemTestAccess; + Thingy91xAtModem(AeContext const& ae_context, ModemInit modem_init, + std::unique_ptr serial); + static constexpr auto kNetworkOpActionPoolCapacity = AE_MODEM_NETWORK_OP_ACTION_POOL_CAPACITY; static constexpr auto kWriteActionPoolCapacity = AE_MODEM_WRITE_ACTION_POOL_CAPACITY; - using OpenNetworkActionPool = - ActionPool; - using CloseNetworkActionPool = - ActionPool; - using WriteActionPool = - ActionPool; - void Init(); void SetupPoll(); void PollEvent(std::int32_t handle, std::string_view flags); @@ -152,9 +212,15 @@ class Thingy91xAtModem final : public IModemDriver { std::unique_ptr modem_stop_operation_; std::unique_ptr modem_set_psp_operation_; std::unique_ptr modem_poweroff_operation_; - OpenNetworkActionPool open_network_pool_; - CloseNetworkActionPool close_network_pool_; - WriteActionPool write_pool_; + ActionPool + open_network_pool_; + ActionPool + close_network_pool_; + ActionPool + write_pool_; std::set connections_; DataEvent data_event_; @@ -162,6 +228,7 @@ class Thingy91xAtModem final : public IModemDriver { std::optional poll_listener_; bool initiated_; bool started_; + bool stopping_{false}; int poll_in_queue_ = 0; int recv_in_queue_ = 0; }; diff --git a/aether/poller/win_poller.cpp b/aether/poller/win_poller.cpp index f79f8b0be..e0fb7b25d 100644 --- a/aether/poller/win_poller.cpp +++ b/aether/poller/win_poller.cpp @@ -87,8 +87,9 @@ void IoCpPoller::Loop() { if (!GetQueuedCompletionStatus(iocp_, &bytes_transferred, &completion_key, &overlapped, INFINITE)) { auto error = GetLastError(); - if (error == ERROR_CONNECTION_ABORTED) { - // socket closed + if (error == ERROR_CONNECTION_ABORTED || + error == ERROR_OPERATION_ABORTED) { + // Expected completion when a socket closes or serial I/O is canceled. continue; } AE_TELE_ERROR(kWinpollWaitFailed, "GetQueuedCompletionStatus error {}", diff --git a/aether/registration/registration.cpp b/aether/registration/registration.cpp index 06130e4a7..e2baceb6e 100644 --- a/aether/registration/registration.cpp +++ b/aether/registration/registration.cpp @@ -52,7 +52,7 @@ Registration::Registration(AeContext const& ae_context, // TODO: add configuration response_timeout_{std::chrono::seconds(20)}, sign_pk_{Crypto::ptr{ae_context_.aether().crypto} - ->signs_pk_[kDefaultSignatureMethod]} { + -> signs_pk_[kDefaultSignatureMethod]} { AE_TELE_INFO(RegisterStarted); // parent uid must not be empty @@ -109,7 +109,7 @@ auto Registration::GetKeys() { sign_pk_)) { AE_TELE_ERROR(RegisterGetKeysVerificationFailed, "Sign verification failed"); - ex::set_error(std::move(r), 1); + ex::set_error(std::move(r), std::int32_t{1}); return; } server_pub_key_ = std::move(signed_key.key); @@ -162,7 +162,7 @@ auto Registration::RequestPowParams() { AE_TELE_ERROR( RegisterPowParamsVerificationFailed, "Proof of work params sign verification failed"); - ex::set_error(std::move(r), 2); + ex::set_error(std::move(r), std::int32_t{2}); return; } @@ -328,7 +328,17 @@ void Registration::Run() { }) | ex::let_stopped([&]() noexcept { return ex::just_error(1); }) | // perform the rest of the registration process - ex::let_value([&](auto&&) noexcept { return RequestPowParams(); }) | + ex::let_value([&](auto&&) noexcept { + return RequestPowParams() | + ex::with_timeout(ae_context_, response_timeout_) | + ex::let_error(Override{ + [&](ex::TimeoutError) noexcept { + AE_TELED_ERROR( + "Registration proof-of-work response timeout"); + return ex::just_error(-1); + }, + [](auto error) noexcept { return ex::just_error(error); }}); + }) | ex::let_value([&](auto&&) noexcept { return MakeRegistration(); }) | ex::let_value([&](auto&&) noexcept { return ResolveCloud(); }) | ex::let_error(Override{ @@ -345,12 +355,23 @@ void Registration::Run() { return ex::just_error(-1); }}); - waiter_.emplace(ae_context_, std::move(s), - [&](std::optional>&& res) noexcept { - assert(res); - registration_event_.Emit(std::move(res).value()); - Finish(); - }); + waiter_.emplace( + ae_context_, std::move(s), + [&](std::optional>&& res) noexcept { + assert(res); + completed_result_ = std::move(res); + // Leave the incoming-data callback before destroying its + // transport and notifying registration consumers. + completion_sub_ = ae_context_.scheduler().Task([this]() { + root_server_select_stream_.Disconnect(); + registration_event_.Emit(std::move(*completed_result_)); + Finish(); + }); + if (!completion_sub_) { + AE_TELED_ERROR("Failed to schedule registration completion"); + assert(false && "Task allocation failed"); + } + }); } } // namespace ae #endif diff --git a/aether/registration/registration.h b/aether/registration/registration.h index 6b65fa9ac..e2687af45 100644 --- a/aether/registration/registration.h +++ b/aether/registration/registration.h @@ -21,22 +21,23 @@ #if AE_SUPPORT_REGISTRATION -# include # include +# include -# include "aether/types/uid.h" +# include "aether-miscpp/types/small_function.h" # include "aether/crypto/key.h" # include "aether/events/events.h" -# include "aether/types/server_id.h" -# include "aether/types/client_config.h" # include "aether/executors/executors.h" -# include "aether-miscpp/types/small_function.h" +# include "aether/tasks/details/task_subsctiption.h" +# include "aether/types/client_config.h" +# include "aether/types/server_id.h" +# include "aether/types/uid.h" -# include "aether/registration_cloud.h" # include "aether/registration/api/client_reg_api_unsafe.h" # include "aether/registration/api/registration_root_api.h" -# include "aether/registration/root_server_select_stream.h" # include "aether/registration/registration_crypto_provider.h" +# include "aether/registration/root_server_select_stream.h" +# include "aether/registration_cloud.h" namespace ae { class Registration : Action { @@ -83,6 +84,8 @@ class Registration : Action { std::optional> waiter_; + std::optional> completed_result_; + TaskSubscription completion_sub_; RegistrationEvent registration_event_; }; } // namespace ae diff --git a/aether/registration/root_server_select_stream.cpp b/aether/registration/root_server_select_stream.cpp index 6b520b3d6..65b246d72 100644 --- a/aether/registration/root_server_select_stream.cpp +++ b/aether/registration/root_server_select_stream.cpp @@ -57,6 +57,14 @@ void RootServerSelectStream::Restream() { CloudError(); } +void RootServerSelectStream::Disconnect() { + buffer_write_.buffer_on(); + buffer_write_.Drop(); + server_connection_.reset(); + buffer_write_.buffer_off(); + stream_update_event_.Emit(); +} + RootServerSelectStream::ServerChangedEvent::Subscriber RootServerSelectStream::server_changed_event() { return server_changed_event_; diff --git a/aether/registration/root_server_select_stream.h b/aether/registration/root_server_select_stream.h index ad7a0bb6b..78884b639 100644 --- a/aether/registration/root_server_select_stream.h +++ b/aether/registration/root_server_select_stream.h @@ -35,9 +35,9 @@ namespace ae { class Aether; struct RootServerSelectStreamTestAccess; - class RootServerSelectStream final : public ByteIStream { public: + static constexpr auto kBufferCapacity = AE_ROOT_REG_SERVER_BUFFER_CAPACITY; using ServerChangedEvent = Event; using CloudErrorEvent = Event; @@ -49,14 +49,13 @@ class RootServerSelectStream final : public ByteIStream { StreamUpdateEvent::Subscriber stream_update_event() override; OutDataEvent::Subscriber out_data_event() override; void Restream() override; + // Release the registration transport after the operation completes. + void Disconnect(); ServerChangedEvent::Subscriber server_changed_event(); CloudErrorEvent::Subscriber cloud_error_event(); private: - static constexpr auto kBufferCapacity = AE_ROOT_REG_SERVER_BUFFER_CAPACITY; - using Buffer = BufferWrite; - friend struct RootServerSelectStreamTestAccess; WriteAction* OnWrite(DataBuffer&& data); @@ -68,7 +67,7 @@ class RootServerSelectStream final : public ByteIStream { AeContext ae_context_; PtrView cloud_; - Buffer buffer_write_; + BufferWrite buffer_write_; // The next server priority to select. Registration server priorities must be // contiguous starting at zero; gaps are UB. This invariant and Cloud's // strict server collection capacity bound ensure this value never increments diff --git a/aether/serial_ports/at_support/at_buffer.cpp b/aether/serial_ports/at_support/at_buffer.cpp index f38168efe..bfa0d0283 100644 --- a/aether/serial_ports/at_support/at_buffer.cpp +++ b/aether/serial_ports/at_support/at_buffer.cpp @@ -16,6 +16,8 @@ #include "aether/serial_ports/at_support/at_buffer.h" +#include + #include "aether/tele.h" namespace ae { @@ -87,35 +89,85 @@ AtBuffer::iterator AtBuffer::erase(iterator first, iterator last) { void AtBuffer::DataRead(std::span data) { AE_TELED_DEBUG("AtBuffer receives packet {}", data); - auto data_str = - std::string_view{reinterpret_cast(data.data()), data.size()}; - auto start = std::end(data_lines_); - while (!data_str.empty()) { - auto line_end = data_str.find("\r\n"); - if (line_end == std::string_view::npos) { - AE_TELED_ERROR("The line without \\r\\n {}", data_str); - line_end = data_str.size(); - } - // skip \r\n - auto sub = data_str.substr(0, line_end); - // move to next line - data_str.remove_prefix((line_end == data_str.size()) ? data_str.size() - : line_end + 2); + pending_data_.insert(std::end(pending_data_), std::begin(data), + std::end(data)); - if (sub.empty()) { - continue; + auto start = std::end(data_lines_); + auto add_line = [&](std::string_view line) { + if (line.empty()) { + return; } - AE_TELED_DEBUG("AtBuffer adds line {}", sub); + AE_TELED_DEBUG("AtBuffer adds line {}", line); auto it = data_lines_.emplace( std::end(data_lines_), - reinterpret_cast(sub.data()), - reinterpret_cast(sub.data()) + sub.size()); - + reinterpret_cast(line.data()), + reinterpret_cast(line.data()) + line.size()); if (start == std::end(data_lines_)) { start = it; } + }; + + while (!pending_data_.empty()) { + auto data_str = + std::string_view{reinterpret_cast(pending_data_.data()), + pending_data_.size()}; + + if (data_str.starts_with("\r\n")) { + pending_data_.erase(std::begin(pending_data_), + std::begin(pending_data_) + 2); + continue; + } + + // +CARECV carries arbitrary binary data. Its payload may contain CR/LF, + // therefore frame it using the declared byte count instead of looking for + // the first line terminator. + static constexpr auto kCarecvPrefix = std::string_view{"+CARECV: "}; + if (data_str.starts_with(kCarecvPrefix)) { + auto comma = data_str.find(',', kCarecvPrefix.size()); + auto line_end = data_str.find("\r\n", kCarecvPrefix.size()); + if (comma != std::string_view::npos && + (line_end == std::string_view::npos || comma < line_end)) { + std::size_t payload_size{}; + auto size_begin = data_str.data() + kCarecvPrefix.size(); + auto size_end = data_str.data() + comma; + auto [parse_end, parse_error] = + std::from_chars(size_begin, size_end, payload_size); + if (parse_error == std::errc{} && parse_end == size_end) { + auto frame_size = comma + 1 + payload_size; + auto response_size = frame_size + 2; + if (data_str.size() < response_size) { + break; + } + if (data_str.substr(frame_size, 2) == "\r\n") { + add_line(data_str.substr(0, frame_size)); + pending_data_.erase(std::begin(pending_data_), + std::begin(pending_data_) + + static_cast(response_size)); + continue; + } + } + } + } + + auto line_end = data_str.find("\r\n"); + if (line_end == std::string_view::npos) { + // The SIM7070 data-entry prompt is not terminated by CR/LF. + if (data_str == ">" || data_str == "> ") { + add_line(data_str); + pending_data_.clear(); + } + break; + } + + add_line(data_str.substr(0, line_end)); + pending_data_.erase( + std::begin(pending_data_), + std::begin(pending_data_) + static_cast(line_end + 2)); + } + + if (start != std::end(data_lines_)) { + update_event_.Emit(start); } - update_event_.Emit(start); } } // namespace ae diff --git a/aether/serial_ports/at_support/at_buffer.h b/aether/serial_ports/at_support/at_buffer.h index 53395ec76..36efcf9cb 100644 --- a/aether/serial_ports/at_support/at_buffer.h +++ b/aether/serial_ports/at_support/at_buffer.h @@ -17,14 +17,14 @@ #ifndef AETHER_SERIAL_PORTS_AT_SUPPORT_AT_BUFFER_H_ #define AETHER_SERIAL_PORTS_AT_SUPPORT_AT_BUFFER_H_ +#include #include #include -#include #include +#include "aether/events/event_subscription.h" #include "aether/events/events.h" #include "aether/types/data_buffer.h" -#include "aether/events/event_subscription.h" #include "aether/serial_ports/iserial_port.h" @@ -56,6 +56,7 @@ class AtBuffer { std::list data_lines_; UpdateEvent update_event_; Subscription data_read_sub_; + DataBuffer pending_data_; }; } // namespace ae #endif // AETHER_SERIAL_PORTS_AT_SUPPORT_AT_BUFFER_H_ diff --git a/aether/serial_ports/at_support/at_dispatcher.cpp b/aether/serial_ports/at_support/at_dispatcher.cpp index 54d8cd4a9..e530c4879 100644 --- a/aether/serial_ports/at_support/at_dispatcher.cpp +++ b/aether/serial_ports/at_support/at_dispatcher.cpp @@ -18,6 +18,7 @@ #include #include +#include namespace ae { AtDispatcher::AtDispatcher(AtBuffer& buffer) @@ -27,13 +28,14 @@ AtDispatcher::AtDispatcher(AtBuffer& buffer) void AtDispatcher::Listen(std::string command, IAtObserver* observer) { // there should be only one observer for one command use last - observers_.insert_or_assign(std::move(command), observer); + observers_.insert_or_assign( + std::move(command), ObserverEntry{observer, next_generation_++}); } void AtDispatcher::Remove(IAtObserver* observer) { - for (auto& [_, o] : observers_) { - if (o == observer) { - o = nullptr; + for (auto& [_, entry] : observers_) { + if (entry.observer == observer) { + entry.observer = nullptr; } } // remove_guard used to prevent observers_ modification during @@ -45,17 +47,32 @@ void AtDispatcher::Remove(IAtObserver* observer) { void AtDispatcher::BufferUpdate(AtBuffer::iterator pos) { remove_guard_ = true; - for (auto const& [command, observer] : observers_) { - if (observer == nullptr) { + // A callback may remove its listener and synchronously install another one + // for the same command. Only listeners that existed when this buffer update + // started are allowed to observe its data. + auto snapshot = std::vector>{}; + snapshot.reserve(observers_.size()); + for (auto const& entry : observers_) { + snapshot.push_back(entry); + } + + for (auto const& [command, entry] : snapshot) { + if (entry.observer == nullptr) { continue; } auto search = pos; while (search != buffer_->end()) { + auto current = observers_.find(command); + if (current == observers_.end() || + current->second.observer != entry.observer || + current->second.generation != entry.generation) { + break; + } auto res = buffer_->FindPattern(command, search); if (res == buffer_->end()) { break; } - observer->Observe(*buffer_, res); + entry.observer->Observe(*buffer_, res); search = ++res; } } @@ -66,8 +83,9 @@ void AtDispatcher::BufferUpdate(AtBuffer::iterator pos) { void AtDispatcher::CleanupObservers() { remove_guard_ = false; - std::erase_if(observers_, - [](auto const& co) { return co.second == nullptr; }); + std::erase_if(observers_, [](auto const& co) { + return co.second.observer == nullptr; + }); } } // namespace ae diff --git a/aether/serial_ports/at_support/at_dispatcher.h b/aether/serial_ports/at_support/at_dispatcher.h index c866da63e..74382dfdc 100644 --- a/aether/serial_ports/at_support/at_dispatcher.h +++ b/aether/serial_ports/at_support/at_dispatcher.h @@ -18,6 +18,7 @@ #define AETHER_SERIAL_PORTS_AT_SUPPORT_AT_DISPATCHER_H_ #include +#include #include #include "aether/events/event_subscription.h" @@ -41,13 +42,19 @@ class AtDispatcher { void Remove(IAtObserver* observer); private: + struct ObserverEntry { + IAtObserver* observer; + std::size_t generation; + }; + void BufferUpdate(AtBuffer::iterator pos); void CleanupObservers(); AtBuffer* buffer_; - std::map observers_; + std::map observers_; Subscription buffer_sub_; bool remove_guard_ = false; + std::size_t next_generation_ = 0; }; } // namespace ae diff --git a/aether/serial_ports/at_support/at_request.h b/aether/serial_ports/at_support/at_request.h index d430ee159..4ada20c25 100644 --- a/aether/serial_ports/at_support/at_request.h +++ b/aether/serial_ports/at_support/at_request.h @@ -79,10 +79,16 @@ class AtRequestOp { [&](auto&&... ws) { MakeWaits(ListenerArgs{ std::move(ws.trigger), - [&, h_{std::move(ws.handler)}](auto&&... args) mutable noexcept { + [&, matched = false, + h_{std::move(ws.handler)}](auto&&... args) mutable noexcept { + if (completed_ || matched) { + return; + } if (std::invoke(h_, std::forward(args)...)) { + matched = true; // setup set value if all waits triggered if (--waits_counter_ == 0) { + completed_ = true; ex::set_value(std::move(receiver_)); } } @@ -93,7 +99,12 @@ class AtRequestOp { error_listener_.emplace( state_.at_support.dispatcher(), "ERROR", - [&](auto&&...) noexcept { ex::set_error(std::move(receiver_), -1); }); + [&](auto&&...) noexcept { + if (!completed_) { + completed_ = true; + ex::set_error(std::move(receiver_), -1); + } + }); using CommandType = decltype(state_.command); auto res = std::invoke([&]() noexcept { @@ -103,9 +114,11 @@ class AtRequestOp { return std::invoke(state_.command); } }); - if (!res) { + if (!res && !completed_) { + completed_ = true; ex::set_error(std::move(receiver_), res.error()); - } else if (waits_counter_ == 0) { + } else if (waits_counter_ == 0 && !completed_) { + completed_ = true; ex::set_value(std::move(receiver_)); } } @@ -128,6 +141,7 @@ class AtRequestOp { R receiver_; StateType state_; std::size_t waits_counter_ = kWaitsCount; + bool completed_{false}; std::array, kWaitsCount> listeners_; std::optional error_listener_; }; diff --git a/aether/serial_ports/at_support/at_support.cpp b/aether/serial_ports/at_support/at_support.cpp index ffa554733..17d54e2f9 100644 --- a/aether/serial_ports/at_support/at_support.cpp +++ b/aether/serial_ports/at_support/at_support.cpp @@ -51,7 +51,7 @@ Result AtSupport::SendATCommand(std::string_view command) { AE_TELED_DEBUG("AT command: {}", command); - static std::array buffer; + std::array buffer{}; assert(command.size() + 2 < buffer.size()); std::copy(command.begin(), command.end(), buffer.begin()); @@ -59,7 +59,8 @@ Result AtSupport::SendATCommand(std::string_view command) { buffer[command.size() + 1] = '\n'; // TODO: add serial error - serial_->Write(buffer); + serial_->Write( + std::span{buffer.data(), command.size() + 2}); return Ok{command.size()}; } diff --git a/aether/serial_ports/esp32_serial_port.cpp b/aether/serial_ports/esp32_serial_port.cpp index f74db575b..50f8e6a47 100644 --- a/aether/serial_ports/esp32_serial_port.cpp +++ b/aether/serial_ports/esp32_serial_port.cpp @@ -1,225 +1,193 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/serial_ports/esp32_serial_port.h" - -#if ESP32_SERIAL_PORT_ENABLED == 1 - -# include "aether-miscpp/misc/defer.h" -# include "aether/serial_ports/serial_ports_tele.h" - -namespace ae { -Esp32SerialPort::ReadAction::ReadAction(ActionContext action_context, - Esp32SerialPort& serial_port) - : Action{action_context}, serial_port_{&serial_port}, read_event_{} { - if (serial_port_->uart_num_ == UART_NUM_MAX) { - return; - } - auto poller = serial_port_->poller_.Lock(); - assert(poller); - poll_sub_ = - poller->Add({serial_port_->uart_num_}) - .Subscribe( - MethodPtr<&Esp32SerialPort::ReadAction::ReadAction::PollEvent>{ - this}); -} - -UpdateStatus Esp32SerialPort::ReadAction::Update() { - if (read_event_) { - for (auto const& b : buffers_) { - serial_port_->read_event_.Emit(b); - } - buffers_.clear(); - read_event_ = false; - } - return {}; -} - -void Esp32SerialPort::ReadAction::PollEvent(PollerEvent event) { - if (event.descriptor != DescriptorType{serial_port_->uart_num_}) { - return; - } - switch (event.event_type) { - case EventType::kRead: - ReadData(); - break; - default: - break; - } -} - -void Esp32SerialPort::ReadAction::ReadData() { - if (serial_port_->uart_num_ == UART_NUM_MAX) { - AE_TELE_ERROR(kAdapterSerialNotOpen, "Port is not open"); - - return; - } - - size_t length = 0; - esp_err_t err = uart_get_buffered_data_len(serial_port_->uart_num_, &length); - if (err != ESP_OK) { - AE_TELE_ERROR(kAdapterSerialReadFailed, "Read failed {}!", err); - return; - } - - if (length == 0) { - return; - } - - DataBuffer buffer(length); - int bytes_read = - uart_read_bytes(serial_port_->uart_num_, buffer.data(), length, 0); - - // Reading error - if (bytes_read <= 0) { - AE_TELE_ERROR(kAdapterSerialReadFailed, "Read failed, no data!"); - return; - } else { - buffer.resize(bytes_read); - - AE_TELED_DEBUG("Serial data read {} bytes: {}", bytes_read, buffer); - - buffers_.emplace_back(std::move(buffer)); - read_event_ = true; - Action::Trigger(); - } -} - -Esp32SerialPort::Esp32SerialPort(ActionContext action_context, - SerialInit serial_init, - IPoller::ptr const& poller) - : action_context_{action_context}, - serial_init_{std::move(serial_init)}, - poller_{poller}, - uart_num_{OpenPort(serial_init_)}, - read_action_{action_context_, *this} {} - -Esp32SerialPort::~Esp32SerialPort() { Close(); } - -void Esp32SerialPort::Write(DataBuffer const& data) { - if (uart_num_ == UART_NUM_MAX) { - AE_TELE_ERROR(kAdapterSerialNotOpen, "Port is not open"); - return; - } - - int bytes_written = uart_write_bytes(uart_num_, data.data(), data.size()); - if (bytes_written < 0) { - // Recording error - AE_TELE_ERROR(kAdapterSerialWriteFailed, "Write failed!"); - Close(); - } -} - -Esp32SerialPort::DataReadEvent::Subscriber Esp32SerialPort::read_event() { - return EventSubscriber{read_event_}; -} - -bool Esp32SerialPort::IsOpen() { return uart_num_ != UART_NUM_MAX; } - -uart_port_t Esp32SerialPort::OpenPort(SerialInit const& serial_init) { - uart_port_t uart_num{UART_NUM_MAX}; - - if (!GetUartNumber(serial_init.port_name, &uart_num)) { - return UART_NUM_MAX; - } - - auto close_on_exit = ae_defer_at[&] { uart_driver_delete(uart_num); }; - - if (!SetOptions(uart_num, serial_init)) { - return UART_NUM_MAX; - } - - close_on_exit.Reset(); - - return uart_num; -} - -bool Esp32SerialPort::SetOptions(uart_port_t uart_num, - SerialInit const& serial_init) { - uart_config_t uart_config = { - /* baud_rate */ static_cast(serial_init.baud_rate), - /* data_bits */ UART_DATA_8_BITS, - /* parity */ UART_PARITY_DISABLE, - /* stop_bits */ UART_STOP_BITS_1, - /* flow_ctrl */ UART_HW_FLOWCTRL_DISABLE, - /* rx_flow_ctrl_thresh */ 122, - /* source_clk */ UART_SCLK_DEFAULT, - /* flags */ {}}; - - if (uart_param_config(uart_num_, &uart_config) != ESP_OK) { - return false; - } - - if (uart_set_pin(uart_num_, serial_init.tx_io_num, serial_init.rx_io_num, - UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK) { - return false; - } - - if (uart_driver_install(uart_num_, - // Receive buffer size - 4096, - // Transmit Buffer size - 0, - // Event Queue size - 0, - // Event Queue Handler - NULL, - // Flags - 0) != ESP_OK) { - return false; - } - - if (SetupTimeouts() != ESP_OK) { - uart_driver_delete(uart_num_); - return false; - } - - return true; -} - -esp_err_t Esp32SerialPort::SetupTimeouts() { - return uart_set_rx_timeout(uart_num_, 10); // 10 units ? 100 ms -} - -bool Esp32SerialPort::GetUartNumber(const std::string& port_name, - uart_port_t* out_uart_num) { - if (port_name == "UART0" || port_name == "/dev/uart/0") { -# if defined UART_NUM_0 - *out_uart_num = UART_NUM_0; -# endif - return true; - } else if (port_name == "UART1" || port_name == "/dev/uart/1") { -# if defined UART_NUM_1 - *out_uart_num = UART_NUM_1; -# endif - return true; - } else if (port_name == "UART2" || port_name == "/dev/uart/2") { -# if defined UART_NUM_2 - *out_uart_num = UART_NUM_2; -# endif - return true; - } - return false; -} - -void Esp32SerialPort::Close() { - if (uart_num_ != UART_NUM_MAX) { - uart_driver_delete(uart_num_); - } -} -} /* namespace ae */ - -#endif // ESP32_SERIAL_PORT_ENABLED +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether/serial_ports/esp32_serial_port.h" +#if ESP32_SERIAL_PORT_ENABLED == 1 +# include +# include +# include +# include +# include +# include "aether/tele.h" +# include "soc/soc_caps.h" +namespace ae { +namespace esp32_serial_port_internal { +constexpr int kRxBufferSize = 8192; +constexpr int kTxBufferSize = 8192; +constexpr std::size_t kWriteChunkSize = 2048; +constexpr int kEventQueueSize = 32; +constexpr std::size_t kReadChunkSize = 2048; +constexpr auto kMinDataBits = static_cast(kBits::kFiveBits); +constexpr auto kMaxDataBits = static_cast(kBits::kEigthBits); +} // namespace esp32_serial_port_internal + +Esp32SerialPort::Esp32SerialPort(AeContext const& context, + SerialInit const& init) + : context_{context} { + uart_num_ = OpenPort(init, events_); + if (IsOpen()) { + Schedule(); + } +} +Esp32SerialPort::~Esp32SerialPort() { Close(); } +uart_port_t Esp32SerialPort::OpenPort(SerialInit const& init, + QueueHandle_t& events) { + auto port = UART_NUM_MAX; + // UART enum constants are not macros. Exclude the low-power UART. + for (int i = 0; i < SOC_UART_HP_NUM; ++i) { + if (init.port_name == "UART" + std::to_string(i) || + init.port_name == "/dev/uart/" + std::to_string(i)) { + port = static_cast(i); + break; + } + } + auto bits = static_cast(init.byte_size); + if (port == UART_NUM_MAX || bits < esp32_serial_port_internal::kMinDataBits || + bits > esp32_serial_port_internal::kMaxDataBits || + static_cast(init.baud_rate) <= 0 || + (init.parity != kParity::kNoParity && + init.parity != kParity::kOddParity && + init.parity != kParity::kEvenParity) || + static_cast(init.stop_bits) < 1 || + static_cast(init.stop_bits) > 3 || uart_is_driver_installed(port)) { + AE_TELED_ERROR("Invalid or occupied UART: {}", init.port_name); + return UART_NUM_MAX; + } + uart_config_t config{}; + config.baud_rate = static_cast(init.baud_rate); + config.data_bits = static_cast( + bits - esp32_serial_port_internal::kMinDataBits); + config.parity = UART_PARITY_DISABLE; + if (init.parity == kParity::kOddParity) { + config.parity = UART_PARITY_ODD; + } else if (init.parity == kParity::kEvenParity) { + config.parity = UART_PARITY_EVEN; + } + config.stop_bits = static_cast(init.stop_bits); + config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + config.source_clk = UART_SCLK_DEFAULT; + if (uart_param_config(port, &config) != ESP_OK || + uart_set_pin(port, init.tx_io_num, init.rx_io_num, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE) != ESP_OK || + uart_driver_install(port, esp32_serial_port_internal::kRxBufferSize, + esp32_serial_port_internal::kTxBufferSize, + esp32_serial_port_internal::kEventQueueSize, &events, + 0) != ESP_OK) { + AE_TELED_ERROR("Failed to configure UART: {}", init.port_name); + return UART_NUM_MAX; + } + return port; +} +void Esp32SerialPort::Schedule() { + task_ = context_.scheduler().DelayedTask([this] { Poll(); }, + std::chrono::milliseconds{2}); + if (!task_) { + AE_TELED_ERROR("Failed to schedule UART polling"); + assert(false && "Task allocation failed"); + Close(); + } +} +void Esp32SerialPort::Poll() { + uart_event_t event{}; + while (xQueueReceive(events_, &event, 0) == pdTRUE) { + if (event.type == UART_FIFO_OVF || event.type == UART_BUFFER_FULL || + event.type == UART_FRAME_ERR || event.type == UART_PARITY_ERR) { + AE_TELED_ERROR("UART receive error: {}", static_cast(event.type)); + Close(); + return; + } + } + FlushWrite(); + if (!IsOpen()) { + return; + } + std::array buffer{}; + auto count = uart_read_bytes(uart_num_, buffer.data(), buffer.size(), 0); + if (count < 0) { + AE_TELED_ERROR("UART read failed"); + Close(); + return; + } + // A subscriber can close the port, so schedule before emitting. + Schedule(); + if (count > 0) { + read_event_.Emit({buffer.data(), static_cast(count)}); + } +} +void Esp32SerialPort::Write(std::span data) { + if (!IsOpen()) { + AE_TELED_ERROR("UART is closed"); + return; + } + constexpr std::size_t kMaxPendingBytes = 16384; + if (data.size() > kMaxPendingBytes - pending_write_.size()) { + AE_TELED_ERROR("UART transmit queue overflow"); + Close(); + return; + } + pending_write_.insert(pending_write_.end(), data.begin(), data.end()); + FlushWrite(); +} +void Esp32SerialPort::FlushWrite() { + if (pending_write_.empty()) { + return; + } + // Only enqueue into an idle driver. A bounded chunk fits in the TX ring, + // so uart_write_bytes does not wait for space while the scheduler is running. + auto ready = uart_wait_tx_done(uart_num_, 0); + if (ready == ESP_ERR_TIMEOUT) { + return; + } + if (ready != ESP_OK) { + AE_TELED_ERROR("UART TX status failed: {}", ready); + Close(); + return; + } + auto size = std::min(pending_write_.size(), + esp32_serial_port_internal::kWriteChunkSize); + std::size_t free_size{}; + if (uart_get_tx_buffer_free_size(uart_num_, &free_size) != ESP_OK) { + AE_TELED_ERROR("UART TX buffer query failed"); + Close(); + return; + } + if (free_size < size) { + return; + } + auto count = uart_write_bytes(uart_num_, pending_write_.data(), size); + if (count < 0 || static_cast(count) != size) { + AE_TELED_ERROR("UART buffered write accepted {} of {} bytes", count, size); + Close(); + return; + } + AE_TELED_DEBUG("UART TX buffer accepted {} bytes", count); + pending_write_.erase(pending_write_.begin(), pending_write_.begin() + count); +} +Esp32SerialPort::DataReadEvent::Subscriber Esp32SerialPort::read_event() { + return EventSubscriber{read_event_}; +} +bool Esp32SerialPort::IsOpen() { return uart_num_ != UART_NUM_MAX; } +void Esp32SerialPort::Close() { + task_.Reset(); + if (IsOpen()) { + uart_driver_delete(uart_num_); + uart_num_ = UART_NUM_MAX; + events_ = nullptr; + } + pending_write_.clear(); +} +} // namespace ae +#endif diff --git a/aether/serial_ports/esp32_serial_port.h b/aether/serial_ports/esp32_serial_port.h index 428f26636..5d9de48d1 100644 --- a/aether/serial_ports/esp32_serial_port.h +++ b/aether/serial_ports/esp32_serial_port.h @@ -1,87 +1,53 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_SERIAL_PORTS_ESP32_SERIAL_PORT_H_ -#define AETHER_SERIAL_PORTS_ESP32_SERIAL_PORT_H_ - -// FIXME: add implementation for esp32 serial port -#if 0 && defined(ESP_PLATFORM) - -# include -# include -# include - -# include "driver/uart.h" - -# include "aether-objects/ptr/ptr_view.h" -# include "aether/actions/action_context.h" - -# include "aether/poller/poller.h" -# include "aether/serial_ports/iserial_port.h" -# include "aether/serial_ports/serial_port_types.h" - -# define ESP32_SERIAL_PORT_ENABLED 1 - -namespace ae { -class Esp32SerialPort : public ISerialPort { - class ReadAction final : public Action { - public: - ReadAction(ActionContext action_context, Esp32SerialPort& serial_port); - - UpdateStatus Update(); - - private: - void PollEvent(PollerEvent event); - void ReadData(); - - Esp32SerialPort* serial_port_; - Subscription poll_sub_; - std::list buffers_; - std::atomic_bool read_event_; - }; - - public: - Esp32SerialPort(ActionContext action_context, SerialInit serial_init, - IPoller::ptr const& poller); - ~Esp32SerialPort() override; - - void Write(DataBuffer const& data) override; - - DataReadEvent::Subscriber read_event() override; - - bool IsOpen() override; - - private: - uart_port_t OpenPort(SerialInit const& serial_init); - bool SetOptions(uart_port_t uart_num, SerialInit const& serial_init); - esp_err_t SetupTimeouts(); - bool GetUartNumber(const std::string& port_name, uart_port_t* out_uart_num); - - void Close(); - - ActionContext action_context_; - SerialInit serial_init_; - PtrView poller_; - uart_port_t uart_num_; - - DataReadEvent read_event_; - - ActionPtr read_action_; -}; -} /* namespace ae */ - -#endif // ESP_PLATFORM -#endif // AETHER_SERIAL_PORTS_ESP32_SERIAL_PORT_H_ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SERIAL_PORTS_ESP32_SERIAL_PORT_H_ +#define AETHER_SERIAL_PORTS_ESP32_SERIAL_PORT_H_ +#if defined(ESP_PLATFORM) +# define ESP32_SERIAL_PORT_ENABLED 1 +# include +# include +# include +# include "aether/ae_context.h" +# include "aether/serial_ports/iserial_port.h" +# include "aether/serial_ports/serial_port_types.h" +# include "driver/uart.h" +namespace ae { +// All calls, including destruction, belong to the AeContext scheduler thread. +class Esp32SerialPort final : public ISerialPort { + public: + explicit Esp32SerialPort(AeContext const& context, SerialInit const& init); + ~Esp32SerialPort() override; + void Write(std::span data) override; + DataReadEvent::Subscriber read_event() override; + bool IsOpen() override; + void Close() override; + + private: + static uart_port_t OpenPort(SerialInit const& init, QueueHandle_t& events); + void Schedule(); + void Poll(); + void FlushWrite(); + AeContext context_; + uart_port_t uart_num_{UART_NUM_MAX}; + QueueHandle_t events_{}; + std::vector pending_write_; + DataReadEvent read_event_; + TaskSubscription task_; +}; +} // namespace ae +#endif +#endif // AETHER_SERIAL_PORTS_ESP32_SERIAL_PORT_H_ diff --git a/aether/serial_ports/iserial_port.h b/aether/serial_ports/iserial_port.h index 90b66841c..0c5da985a 100644 --- a/aether/serial_ports/iserial_port.h +++ b/aether/serial_ports/iserial_port.h @@ -17,8 +17,8 @@ #ifndef AETHER_SERIAL_PORTS_ISERIAL_PORT_H_ #define AETHER_SERIAL_PORTS_ISERIAL_PORT_H_ -#include #include +#include #include "aether/events/events.h" @@ -36,6 +36,13 @@ class ISerialPort { * \brief Check if the serial port is open. */ virtual bool IsOpen() = 0; + /** + * \brief Release the port and stop read notifications. Safe to call + * again. + * Call on the scheduler thread, after all protocol operations + * have stopped. + */ + virtual void Close() = 0; /** * \brief Write amount of data. */ diff --git a/aether/serial_ports/serial_port_factory.cpp b/aether/serial_ports/serial_port_factory.cpp index 659d5c001..622027f08 100644 --- a/aether/serial_ports/serial_port_factory.cpp +++ b/aether/serial_ports/serial_port_factory.cpp @@ -17,9 +17,9 @@ #include "aether/serial_ports/serial_port_factory.h" // IWYU pragma: begin_keeps -#include "aether/serial_ports/win_serial_port.h" #include "aether/serial_ports/esp32_serial_port.h" #include "aether/serial_ports/unix_serial_port.h" +#include "aether/serial_ports/win_serial_port.h" // IWYU pragma: end_keeps namespace ae { @@ -30,7 +30,7 @@ std::unique_ptr SerialPortFactory::CreatePort( #if WIN_SERIAL_PORT_ENABLED == 1 return std::make_unique(ae_context, serial_init, poller); #elif ESP32_SERIAL_PORT_ENABLED == 1 - return std::make_unique(ae_context, serial_init, poller); + return std::make_unique(ae_context, serial_init); #elif UNIX_SERIAL_PORT_ENABLED == 1 return std::make_unique(ae_context, serial_init, poller); #else diff --git a/aether/serial_ports/unix_serial_port.cpp b/aether/serial_ports/unix_serial_port.cpp index 3f4bb9174..beaa88747 100644 --- a/aether/serial_ports/unix_serial_port.cpp +++ b/aether/serial_ports/unix_serial_port.cpp @@ -19,8 +19,8 @@ #if defined UNIX_SERIAL_PORT_ENABLED # include -# include # include +# include # include "aether-miscpp/misc/defer.h" # include "aether/serial_ports/serial_ports_tele.h" @@ -36,8 +36,11 @@ UnixSerialPort::UnixSerialPort(AeContext const& ae_context, poller_fd_.Events(EventType::kRead | EventType::kError); } -UnixSerialPort::~UnixSerialPort() { +UnixSerialPort::~UnixSerialPort() { Close(); } + +void UnixSerialPort::Close() { auto fd = poller_fd_.Remove(); + scheduler_sub_.Reset(); if (*fd != kInvalidDescriptor) { close(*fd); } diff --git a/aether/serial_ports/unix_serial_port.h b/aether/serial_ports/unix_serial_port.h index 8130f87bc..45e077c01 100644 --- a/aether/serial_ports/unix_serial_port.h +++ b/aether/serial_ports/unix_serial_port.h @@ -22,16 +22,16 @@ # define UNIX_SERIAL_PORT_ENABLED 1 +# include # include # include -# include # include "aether/ae_context.h" # include "aether/poller/poller.h" -# include "aether/types/data_buffer.h" # include "aether/poller/unix_poller.h" # include "aether/serial_ports/iserial_port.h" # include "aether/serial_ports/serial_port_types.h" +# include "aether/types/data_buffer.h" namespace ae { class UnixSerialPort final : public ISerialPort { @@ -45,6 +45,7 @@ class UnixSerialPort final : public ISerialPort { DataReadEvent::Subscriber read_event() override; bool IsOpen() override; + void Close() override; private: static int OpenPort(SerialInit const& serial_init); @@ -54,8 +55,6 @@ class UnixSerialPort final : public ISerialPort { void ReadData(DescriptorType fd); void EmitData(); - void Close(); - AeContext ae_context_; SerialInit serial_init_; UnixPolledFd poller_fd_; diff --git a/aether/serial_ports/win_serial_port.cpp b/aether/serial_ports/win_serial_port.cpp index 4c73b0073..8d33f7731 100644 --- a/aether/serial_ports/win_serial_port.cpp +++ b/aether/serial_ports/win_serial_port.cpp @@ -31,6 +31,13 @@ WinSerialPort::WinSerialPort(AeContext const& ae_context, fd_{OpenPort(serial_init_)}, read_buffer_(kReadBufSize) { if (fd_ != INVALID_HANDLE_VALUE) { + overlapped_rd_.hEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr); + if (overlapped_rd_.hEvent == nullptr) { + AE_TELED_ERROR("Unable to create serial read event {}", GetLastError()); + CloseHandle(fd_); + fd_ = INVALID_HANDLE_VALUE; + return; + } poller_->Add({fd_}, MethodPtr<&WinSerialPort::PollEvent>{this}); RequestRead(); } @@ -39,51 +46,52 @@ WinSerialPort::WinSerialPort(AeContext const& ae_context, WinSerialPort::~WinSerialPort() { Close(); } void WinSerialPort::Write(std::span data) { - DWORD dwWrite, dwErr, dwRes; - BOOL fSuccess{FALSE}; + auto lock = std::lock_guard{fd_lock_}; + if (fd_ == INVALID_HANDLE_VALUE) { + AE_TELE_ERROR(kAdapterSerialNotOpen, "Port is not open"); + return; + } + + DWORD bytes_written{0}; + bool success{false}; - overlapped_wr_.hEvent = CreateEventA(NULL, true, true, NULL); + overlapped_wr_ = {}; + overlapped_wr_.hEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr); if (overlapped_wr_.hEvent == NULL) { - dwErr = GetLastError(); - AE_TELED_DEBUG("Error write to serial port {}", dwErr); + AE_TELED_ERROR("Unable to create serial write event {}", GetLastError()); return; } - fSuccess = WriteFile(fd_, data.data(), static_cast(data.size()), - &dwWrite, &overlapped_wr_); - dwErr = GetLastError(); - if (!fSuccess) { - if (dwErr != ERROR_IO_PENDING) { - AE_TELED_ERROR("WriteFile error {}", dwErr); + auto write_result = + WriteFile(fd_, data.data(), static_cast(data.size()), + &bytes_written, &overlapped_wr_); + if (write_result != FALSE) { + success = true; + } else { + auto error = GetLastError(); + if (error != ERROR_IO_PENDING) { + AE_TELED_ERROR("WriteFile error {}", error); } else { - dwRes = WaitForSingleObject(overlapped_wr_.hEvent, INFINITE); - dwErr = GetLastError(); - switch (dwRes) { - case WAIT_OBJECT_0: - if (!GetOverlappedResult(fd_, &overlapped_wr_, &dwWrite, FALSE)) { - dwErr = GetLastError(); - AE_TELED_ERROR("GetOverlappedResult error {}", dwErr); - } else { - fSuccess = FALSE; - } - break; - default: - fSuccess = TRUE; - break; + auto wait_result = WaitForSingleObject(overlapped_wr_.hEvent, INFINITE); + if (wait_result == WAIT_OBJECT_0) { + success = GetOverlappedResult(fd_, &overlapped_wr_, &bytes_written, + FALSE) != FALSE; + if (!success) { + AE_TELED_ERROR("GetOverlappedResult error {}", GetLastError()); + } + } else { + AE_TELED_ERROR("Wait for serial write error {}", GetLastError()); } } - } else { - fSuccess = FALSE; } - if (overlapped_wr_.hEvent != NULL) CloseHandle(overlapped_wr_.hEvent); + CloseHandle(overlapped_wr_.hEvent); + overlapped_wr_.hEvent = nullptr; - if (fSuccess == TRUE) { - AE_TELED_ERROR("Write to com port filed!"); - } else { - if (dwWrite > 0) { - AE_TELED_DEBUG("Serial data write {} bytes", dwWrite); - } + if (!success) { + AE_TELED_ERROR("Write to COM port failed"); + } else if (bytes_written > 0) { + AE_TELED_DEBUG("Serial data write {} bytes", bytes_written); } } @@ -93,67 +101,81 @@ WinSerialPort::DataReadEvent::Subscriber WinSerialPort::read_event() { bool WinSerialPort::IsOpen() { return fd_ != INVALID_HANDLE_VALUE; } -void WinSerialPort::PollEvent(LPOVERLAPPED) { HandleRead(); } +void WinSerialPort::PollEvent(LPOVERLAPPED overlapped) { + if (overlapped == &overlapped_rd_) { + HandleRead(); + } +} void WinSerialPort::RequestRead() { - DWORD dwErr, dwRead; - BOOL fRes; - + auto lock = std::lock_guard{fd_lock_}; if (fd_ == INVALID_HANDLE_VALUE) { - AE_TELE_ERROR(kAdapterSerialNotOpen, "Port is not open"); return; } - auto lock = std::lock_guard{fd_lock_}; - - fRes = ::ReadFile(fd_, read_buffer_.data(), - static_cast(read_buffer_.size()), &dwRead, - &overlapped_rd_); - if (!fRes) { - dwErr = GetLastError(); - if (dwErr != ERROR_IO_PENDING) { - AE_TELED_ERROR("Read err {}", dwErr); + DWORD bytes_read{0}; + ResetEvent(overlapped_rd_.hEvent); + auto read_result = ::ReadFile(fd_, read_buffer_.data(), + static_cast(read_buffer_.size()), + &bytes_read, &overlapped_rd_); + if (read_result == FALSE) { + auto error = GetLastError(); + if (error != ERROR_IO_PENDING) { + AE_TELED_ERROR("Read err {}", error); return; } } + read_pending_ = true; } void WinSerialPort::HandleRead() { - DWORD dwErr; - DWORD dwRead; - - auto lock = std::scoped_lock{fd_lock_}; - - if (!::GetOverlappedResult(fd_, &overlapped_rd_, &dwRead, FALSE)) { - dwErr = GetLastError(); - AE_TELED_ERROR("GetOverlappedResult err {}", dwErr); - return; - } + bool schedule_emit{false}; + { + auto lock = std::scoped_lock{fd_lock_}; + if (fd_ == INVALID_HANDLE_VALUE) { + return; + } - if (dwRead > 0) { - DataBuffer data(static_cast(dwRead)); - std::copy(read_buffer_.begin(), read_buffer_.begin() + dwRead, - data.begin()); - AE_TELED_DEBUG("Serial data read {} bytes", data.size()); + DWORD bytes_read{0}; + read_pending_ = false; + if (!::GetOverlappedResult(fd_, &overlapped_rd_, &bytes_read, FALSE)) { + auto error = GetLastError(); + if (error != ERROR_OPERATION_ABORTED) { + AE_TELED_ERROR("GetOverlappedResult err {}", error); + } + return; + } - buffers_.emplace_back(std::move(data)); + if (bytes_read > 0) { + DataBuffer data(static_cast(bytes_read)); + std::copy(read_buffer_.begin(), read_buffer_.begin() + bytes_read, + data.begin()); + AE_TELED_DEBUG("Serial data read {} bytes", data.size()); - if (!read_flag_.exchange(true)) { - scheduler_sub_ = ae_context_.scheduler().Task([this]() noexcept { - auto lock = std::scoped_lock{fd_lock_}; - EmitData(); - read_flag_ = false; - }); + buffers_.emplace_back(std::move(data)); + schedule_emit = !read_flag_.exchange(true); } } + + if (schedule_emit) { + scheduler_sub_ = + ae_context_.scheduler().Task([this]() noexcept { EmitData(); }); + } + RequestRead(); } void WinSerialPort::EmitData() { - for (auto const& b : buffers_) { + std::list buffers; + { + auto lock = std::scoped_lock{fd_lock_}; + buffers.swap(buffers_); + read_flag_ = false; + } + + for (auto const& b : buffers) { read_event_.Emit(b); } - buffers_.clear(); } void* WinSerialPort::OpenPort(SerialInit const& serial_init) { @@ -164,8 +186,9 @@ void* WinSerialPort::OpenPort(SerialInit const& serial_init) { OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if (fd == INVALID_HANDLE_VALUE) { - AE_TELE_ERROR(kAdapterSerialNotOpen, "Failed to open port: {}", - GetLastError()); + auto const error = GetLastError(); + AE_TELE_ERROR(kAdapterSerialNotOpen, "Failed to open port '{}': error {}", + full_name, error); return INVALID_HANDLE_VALUE; } @@ -216,9 +239,39 @@ bool WinSerialPort::SetOptions(void* fd, SerialInit const& serial_init) { } void WinSerialPort::Close() { - if (fd_ != INVALID_HANDLE_VALUE) { + auto fd = fd_; + if (fd == INVALID_HANDLE_VALUE) { + return; + } + + poller_->Remove({fd}); + // Remove waits for any poll callback, which could still schedule EmitData. + scheduler_sub_.Reset(); + + auto lock = std::lock_guard{fd_lock_}; + if (fd_ == fd) { + CancelIoEx(fd_, nullptr); + // Cancellation is asynchronous. Keep OVERLAPPED and its buffer alive + // until the read completes; otherwise reopening can race outstanding I/O. + // Writes finish synchronously under fd_lock_, so only the read can remain. + if (read_pending_) { + DWORD bytes_read{0}; + if (!GetOverlappedResult(fd_, &overlapped_rd_, &bytes_read, TRUE)) { + auto error = GetLastError(); + if (error != ERROR_OPERATION_ABORTED) { + AE_TELED_ERROR("Serial read completion during close failed {}", + error); + } + } + read_pending_ = false; + } CloseHandle(fd_); fd_ = INVALID_HANDLE_VALUE; + CloseHandle(overlapped_rd_.hEvent); + overlapped_rd_.hEvent = nullptr; + buffers_.clear(); + read_flag_ = false; + AE_TELED_DEBUG("Serial port {} closed", serial_init_.port_name); } } diff --git a/aether/serial_ports/win_serial_port.h b/aether/serial_ports/win_serial_port.h index 517c015e0..f91fb3974 100644 --- a/aether/serial_ports/win_serial_port.h +++ b/aether/serial_ports/win_serial_port.h @@ -21,16 +21,16 @@ # define WIN_SERIAL_PORT_ENABLED 1 +# include # include # include -# include # include "aether/ae_context.h" # include "aether/poller/poller.h" -# include "aether/types/data_buffer.h" # include "aether/poller/win_poller.h" # include "aether/serial_ports/iserial_port.h" # include "aether/serial_ports/serial_port_types.h" +# include "aether/types/data_buffer.h" # include @@ -46,6 +46,7 @@ class WinSerialPort final : public ISerialPort { DataReadEvent::Subscriber read_event() override; bool IsOpen() override; + void Close() override; private: static void* OpenPort(SerialInit const& serial_init); @@ -56,8 +57,6 @@ class WinSerialPort final : public ISerialPort { void HandleRead(); void EmitData(); - void Close(); - AeContext ae_context_; SerialInit serial_init_; std::shared_ptr poller_; @@ -67,11 +66,12 @@ class WinSerialPort final : public ISerialPort { DataReadEvent read_event_; std::list buffers_; - std::atomic_bool read_flag_; + std::atomic_bool read_flag_{false}; TaskSubscription scheduler_sub_; DataBuffer read_buffer_; OVERLAPPED overlapped_rd_{}; + bool read_pending_{false}; OVERLAPPED overlapped_wr_{}; static constexpr int kReadBufSize{4096}; diff --git a/aether/server_connections/server_connection.cpp b/aether/server_connections/server_connection.cpp index 8afc277de..c2978cff8 100644 --- a/aether/server_connections/server_connection.cpp +++ b/aether/server_connections/server_connection.cpp @@ -59,10 +59,29 @@ ServerConnection::OutDataEvent::Subscriber ServerConnection::out_data_event() { } void ServerConnection::Restream() { + if (stopped_) { + return; + } // restream means something wrong with current channel ChannelError(); } +void ServerConnection::Stop() { + if (stopped_) { + return; + } + stopped_ = true; + defer_sub_.Reset(); + channel_stream_update_sub_.Reset(); + channel_stream_out_data_sub_.Reset(); + channel_select_action_.reset(); + stream_info_.is_writable = false; + stream_info_.link_state = LinkState::kUnlinked; + top_channel_ = nullptr; + stream_update_event_.Emit(); + stream_.reset(); +} + ServerConnection::ServerErrorEvent::Subscriber ServerConnection::server_error_event() { return EventSubscriber{server_error_}; @@ -131,6 +150,9 @@ ChannelEntry* ServerConnection::TopChannel() { } void ServerConnection::SelectChannel() { + if (stopped_) { + return; + } // prevent new channel selection while one is active if (channel_select_action_ && !channel_select_action_->is_finished()) { AE_TELED_DEBUG("Repeated select channel"); diff --git a/aether/server_connections/server_connection.h b/aether/server_connections/server_connection.h index 40c27e00e..09369a82f 100644 --- a/aether/server_connections/server_connection.h +++ b/aether/server_connections/server_connection.h @@ -50,6 +50,8 @@ class ServerConnection final : public ByteIStream { StreamInfo stream_info() const override; OutDataEvent::Subscriber out_data_event() override; void Restream() override; + // Terminal shutdown; call outside connection callbacks. + void Stop(); ServerErrorEvent::Subscriber server_error_event(); ChannelChangedEvent::Subscriber channel_changed_event(); @@ -78,6 +80,7 @@ class ServerConnection final : public ByteIStream { PtrView server_; bool full_connected_; + bool stopped_{}; ChannelEntry* top_channel_; std::unique_ptr stream_; std::vector channels_; diff --git a/aether/transport/modems/modem_transport.cpp b/aether/transport/modems/modem_transport.cpp index e32224a5d..8e33aeb03 100644 --- a/aether/transport/modems/modem_transport.cpp +++ b/aether/transport/modems/modem_transport.cpp @@ -18,6 +18,8 @@ #if MODEM_TRANSPORT_ENABLED +# include + # include "aether/vector_buffer.h" # include "aether/write_action/failed_write_action.h" @@ -151,7 +153,7 @@ ModemTransport::ModemTransport(AeContext const& ae_context, Connect(); } -ModemTransport::~ModemTransport() { Disconnect(); } +ModemTransport::~ModemTransport() { Disconnect(false); } ModemTransport::StreamUpdateEvent::Subscriber ModemTransport::stream_update_event() { @@ -171,6 +173,9 @@ void ModemTransport::Restream() { } WriteAction& ModemTransport::Write(DataBuffer&& in_data) { + if (!stream_info_.is_writable) { + return FailedWrite(); + } AE_TELE_DEBUG(kModemTransportSend, "Send data size {}", in_data.size()); if (protocol_ == Protocol::kTcp) { @@ -185,7 +190,8 @@ WriteAction& ModemTransport::Write(DataBuffer&& in_data) { WriteAction& ModemTransport::WriteTcp(DataBuffer&& in_data) { auto& send_queue_manager = - std::get(packet_queue_manager_); + std::get>( + packet_queue_manager_); // Make TCP packet with its size at the beginning auto packet_data = std::vector{}; @@ -204,14 +210,15 @@ WriteAction& ModemTransport::WriteTcp(DataBuffer&& in_data) { send_action->status_event().Subscribe([this](auto status) { if (status == WriteAction::Status::kFail) { AE_TELED_ERROR("Send error, disconnect!"); - OnConnectionFailed(); + ScheduleConnectionFailure(); } }); return *send_action; } WriteAction& ModemTransport::WriteUdp(DataBuffer&& in_data) { auto& send_queue_manager = - std::get(packet_queue_manager_); + std::get>( + packet_queue_manager_); auto* send_action = send_queue_manager.AddPacket(ae_context_, *this, std::move(in_data)); @@ -223,7 +230,7 @@ WriteAction& ModemTransport::WriteUdp(DataBuffer&& in_data) { send_action->status_event().Subscribe([this](auto status) { if (status == WriteAction::Status::kFail) { AE_TELED_ERROR("Send error, disconnect!"); - OnConnectionFailed(); + ScheduleConnectionFailure(); } }); return *send_action; @@ -266,6 +273,17 @@ void ModemTransport::Connect() { void ModemTransport::OnConnected(ConnectionIndex connection_index) { connection_ = connection_index; + connection_closed_sub_ = modem_driver_->connection_closed_event().Subscribe( + [this](ConnectionIndex connection) { + if (connection != connection_) { + return; + } + // The modem already closed it. Do not send CACLOSE for a reused ID. + connection_ = kInvalidConnectionIndex; + stream_info_.is_writable = false; + ScheduleConnectionFailure(); + }); + read_packet_sub_ = modem_driver_->data_event().Subscribe( MethodPtr<&ModemTransport::DataReceived>{this}); @@ -279,15 +297,33 @@ void ModemTransport::OnConnectionFailed() { Disconnect(); } -void ModemTransport::Disconnect() { - if (connection_ == kInvalidConnectionIndex) { - return; +void ModemTransport::ScheduleConnectionFailure() { + // The write action belongs to this transport. Notify its owner only after + // WriteAction::SetStatus has returned and finished the action: the owner may + // destroy the transport in the stream update callback. + if (!connection_failure_task_) { + connection_failure_task_ = + ae_context_.scheduler().Task([this]() { OnConnectionFailed(); }); + if (!connection_failure_task_) { + AE_TELED_ERROR("Failed to schedule modem transport disconnection"); + assert(false && "Task allocation failed"); + } } +} - modem_driver_->CloseNetwork(connection_); - connection_ = kInvalidConnectionIndex; +void ModemTransport::Disconnect(bool notify) { + stream_info_.is_writable = false; + connection_sub_.Reset(); + read_packet_sub_.Reset(); + connection_closed_sub_.Reset(); + if (connection_ != kInvalidConnectionIndex) { + modem_driver_->CloseNetwork(connection_); + connection_ = kInvalidConnectionIndex; + } - stream_update_event_.Emit(); + if (notify) { + stream_update_event_.Emit(); + } } void ModemTransport::DataReceived(ConnectionIndex connection, @@ -320,11 +356,15 @@ ModemTransport::PacketQueueManagerVar ModemTransport::MakePacketQueueManager( switch (endpoint.protocol) { case Protocol::kTcp: { return PacketQueueManagerVar{ - std::in_place_type_t{}, ae_context}; + std::in_place_type_t< + PacketQueueManager>{}, + ae_context}; } default: { return PacketQueueManagerVar{ - std::in_place_type_t{}, ae_context}; + std::in_place_type_t< + PacketQueueManager>{}, + ae_context}; } } } diff --git a/aether/transport/modems/modem_transport.h b/aether/transport/modems/modem_transport.h index b162a8621..7b046b08d 100644 --- a/aether/transport/modems/modem_transport.h +++ b/aether/transport/modems/modem_transport.h @@ -35,6 +35,7 @@ # include "aether/write_action/failed_write_action.h" namespace ae { + class ModemTransport final : public ByteIStream { class ModemSend : public PacketSendAction { public: @@ -82,13 +83,9 @@ class ModemTransport final : public ByteIStream { AE_MODEM_TCP_PACKET_QUEUE_SIZE; static constexpr auto kUdpPacketQueueCapacity = AE_MODEM_UDP_PACKET_QUEUE_SIZE; - - using ModemTcpPacketQueueManager = - PacketQueueManager; - using ModemUdpPacketQueueManager = - PacketQueueManager; using PacketQueueManagerVar = - std::variant; + std::variant, + PacketQueueManager>; public: ModemTransport(AeContext const& ae_context, IModemDriver& modem_driver, @@ -105,7 +102,8 @@ class ModemTransport final : public ByteIStream { void Connect(); void OnConnected(ConnectionIndex connection_index); void OnConnectionFailed(); - void Disconnect(); + void ScheduleConnectionFailure(); + void Disconnect(bool notify = true); void DataReceived(ConnectionIndex connection, DataBuffer const& data_in); void DataReceivedTcp(DataBuffer const& data_in); @@ -138,6 +136,8 @@ class ModemTransport final : public ByteIStream { MultiSubscription send_action_subs_; Subscription connection_sub_; Subscription read_packet_sub_; + Subscription connection_closed_sub_; + TaskSubscription connection_failure_task_; }; } // namespace ae diff --git a/aether/transport/packet_queue_manager.h b/aether/transport/packet_queue_manager.h index 0921abf54..3ca3585e3 100644 --- a/aether/transport/packet_queue_manager.h +++ b/aether/transport/packet_queue_manager.h @@ -23,13 +23,12 @@ #include #include "aether/ae_context.h" -#include "aether/common.h" #include "aether/transport/packet_send_action.h" #include "aether/transport/transport_tele.h" namespace ae { -template +template requires(std::is_base_of_v) class PacketQueueManager { public: @@ -50,8 +49,12 @@ class PacketQueueManager { return nullptr; } queue_.push(T{std::forward(args)...}); + auto& packet = queue_.back(); + // An asynchronous packet completion must wake the queue. Without this, + // the next packet is sent only when another packet happens to be added. + packet.finished_event().Subscribe([this]() { Enqueue(); }); Enqueue(); - return &queue_.back(); + return &packet; } /** diff --git a/aether/transport/system_sockets/udp/udp.h b/aether/transport/system_sockets/udp/udp.h index f176720c8..7f7e7b5ef 100644 --- a/aether/transport/system_sockets/udp/udp.h +++ b/aether/transport/system_sockets/udp/udp.h @@ -33,11 +33,11 @@ # include "aether/poller/poller.h" # include "aether/stream_api/istream.h" -# include "aether/transport/packet_queue_manager.h" -# include "aether/transport/packet_send_action.h" -# include "aether/transport/system_sockets/sockets/isocket.h" # include "aether/transport/transport_tele.h" +# include "aether/transport/packet_send_action.h" +# include "aether/transport/packet_queue_manager.h" # include "aether/write_action/failed_write_action.h" +# include "aether/transport/system_sockets/sockets/isocket.h" namespace ae { namespace upd_internal { diff --git a/aether/write_action/buffer_write.h b/aether/write_action/buffer_write.h index 9d90fe789..4b3a0bafd 100644 --- a/aether/write_action/buffer_write.h +++ b/aether/write_action/buffer_write.h @@ -71,7 +71,7 @@ class BufferedWriteAction final : public WriteAction { /** * \brief Buffers write requests until gate is not ready to accept them. */ -template +template class BufferWrite { struct BufferEntry { BufferedWriteAction wa; diff --git a/aether/write_action/failed_write_action.h b/aether/write_action/failed_write_action.h index fc11ad7d9..b557633c9 100644 --- a/aether/write_action/failed_write_action.h +++ b/aether/write_action/failed_write_action.h @@ -17,7 +17,11 @@ #ifndef AETHER_WRITE_ACTION_FAILED_WRITE_ACTION_H_ #define AETHER_WRITE_ACTION_FAILED_WRITE_ACTION_H_ +#include + #include "aether/ae_context.h" +#include "aether/tasks/details/task_subsctiption.h" +#include "aether/tele.h" #include "aether/write_action/write_action.h" namespace ae { @@ -27,8 +31,16 @@ namespace ae { class FailedWriteAction final : public WriteAction { public: explicit FailedWriteAction(AeContext const& context) { - context.scheduler().Task([&]() { SetStatus(WriteAction::Status::kFail); }); + task_ = context.scheduler().Task( + [this]() { SetStatus(WriteAction::Status::kFail); }); + if (!task_) { + AE_TELED_ERROR("Failed to schedule write failure"); + assert(false && "Task allocation failed"); + } } + + private: + TaskSubscription task_; }; } // namespace ae diff --git a/config/user_config_firebeetle_c6_sim7070.h b/config/user_config_firebeetle_c6_sim7070.h new file mode 100644 index 000000000..c841312ce --- /dev/null +++ b/config/user_config_firebeetle_c6_sim7070.h @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CONFIG_USER_CONFIG_FIREBEETLE_C6_SIM7070_H_ +#define CONFIG_USER_CONFIG_FIREBEETLE_C6_SIM7070_H_ +#include "config/user_config_modem.h" +#if defined(ESP_PLATFORM) +# include "sdkconfig.h" +# if CONFIG_ESP_CONSOLE_UART_DEFAULT +# error \ + "FireBeetle LTE: select USB Serial/JTAG console; UART0 shares GPIO16/17 with the modem" +# endif +#endif +#define AE_SUPPORT_WIFIS 0 +#define AE_MODEM_UART_PORT "UART1" +#define AE_MODEM_UART_TX_GPIO 16 +#define AE_MODEM_UART_RX_GPIO 17 +#define AE_MODEM_DTR_GPIO 8 +#define AE_MODEM_PWR_GPIO 14 +#endif diff --git a/config/user_config_modem.h b/config/user_config_modem.h index abc130c34..966787575 100644 --- a/config/user_config_modem.h +++ b/config/user_config_modem.h @@ -17,4 +17,6 @@ #include "config/user_config_hydrogen.h" #define AE_SUPPORT_MODEMS 1 -#define AE_ENABLE_THINGY91X 1 +#define AE_ENABLE_THINGY91X 0 +#define AE_ENABLE_SIM7070 1 +#define AE_ENABLE_BG95 0 diff --git a/examples/a_b_message_exchange/CMakeLists.txt b/examples/a_b_message_exchange/CMakeLists.txt index 9683a5586..961cd7151 100644 --- a/examples/a_b_message_exchange/CMakeLists.txt +++ b/examples/a_b_message_exchange/CMakeLists.txt @@ -30,6 +30,12 @@ else() SRCS main.cpp a_b_message_exchange.cpp INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} ../common REQUIRES esp_wifi esp_netif nvs_flash spiffs esp_driver_uart) + + target_compile_definitions(${COMPONENT_LIB} PRIVATE + WIFI_SSID="${WIFI_SSID}" + WIFI_PASSWORD="${WIFI_PASSWORD}" + ) + add_subdirectory("../../" aether) target_link_libraries(${COMPONENT_LIB} PRIVATE aether) else() diff --git a/examples/capi/oddity/CMakeLists.txt b/examples/capi/oddity/CMakeLists.txt index e25fc5524..7fa643c62 100644 --- a/examples/capi/oddity/CMakeLists.txt +++ b/examples/capi/oddity/CMakeLists.txt @@ -44,7 +44,14 @@ else() nvs_flash spiffs esp_driver_uart + esp_driver_gpio ) + + target_compile_definitions(${COMPONENT_LIB} PRIVATE + WIFI_SSID="${WIFI_SSID}" + WIFI_PASSWORD="${WIFI_PASSWORD}" + ) + add_subdirectory("../../../" aether) target_link_libraries(${COMPONENT_LIB} PRIVATE aether) if (NOT DEST_UID STREQUAL "") diff --git a/examples/cloud/CMakeLists.txt b/examples/cloud/CMakeLists.txt index 31999c9f5..2b8524b6b 100644 --- a/examples/cloud/CMakeLists.txt +++ b/examples/cloud/CMakeLists.txt @@ -40,8 +40,14 @@ else() nvs_flash spiffs esp_driver_uart + esp_driver_gpio ) + target_compile_definitions(${COMPONENT_LIB} PRIVATE + WIFI_SSID="${WIFI_SSID}" + WIFI_PASSWORD="${WIFI_PASSWORD}" + ) + add_subdirectory("../../" aether) target_link_libraries(${COMPONENT_LIB} PRIVATE aether) else() diff --git a/examples/cloud/cloud_test.cpp b/examples/cloud/cloud_test.cpp index c12589991..e28fbd83e 100644 --- a/examples/cloud/cloud_test.cpp +++ b/examples/cloud/cloud_test.cpp @@ -219,17 +219,22 @@ int AetherCloudExample() { * WaitUntil either waits until the next selected time or some action * triggers new event. */ + auto next_progress_log = ae::Now(); while (!aether_app->IsExited()) { - ae::Format(std::cout, - "~['_']~ Wait cloud test received_count={} confirmed_count={}\n", - received_count, confirmed_count); + auto current_time = ae::Now(); + if (current_time >= next_progress_log) { + ae::Format( + std::cout, + "~['_']~ Wait cloud test received_count={} confirmed_count={}\n", + received_count, confirmed_count); + next_progress_log = current_time + std::chrono::seconds{5}; + } if ((received_count == messages.size()) && (confirmed_count == messages.size())) { aether_app->Exit(0); - continue; } // Wait for next event or timeout - auto current_time = ae::Now(); + current_time = ae::Now(); auto next_time = aether_app->Update(current_time); aether_app->WaitUntil( std::min(next_time, current_time + std::chrono::seconds{5})); diff --git a/examples/common/aether_construct_esp_wifi.h b/examples/common/aether_construct_esp_wifi.h index c01599c3d..22a23b244 100644 --- a/examples/common/aether_construct_esp_wifi.h +++ b/examples/common/aether_construct_esp_wifi.h @@ -18,12 +18,12 @@ #define EXAMPLES_COMMON_AETHER_CONSTRUCT_ESP_WIFI_H_ #include "aether_construct.h" - +#include "aether/config.h" #if AE_EXAMPLE_ESP_WIFI namespace ae::examples { -static const std::string kWifi1Ssid = "Test1234"; -static const std::string kWifi1Pass = "Test1234"; +static const std::string kWifi1Ssid = std::string{WIFI_SSID}; +static const std::string kWifi1Pass = std::string{WIFI_PASSWORD}; static const std::string kWifi2Ssid = "Test2345"; static const std::string kWifi2Pass = "Test2345"; diff --git a/examples/common/aether_construct_modem.h b/examples/common/aether_construct_modem.h index e5770545e..6a4866668 100644 --- a/examples/common/aether_construct_modem.h +++ b/examples/common/aether_construct_modem.h @@ -25,27 +25,45 @@ # else namespace ae::examples { -static constexpr std::string_view kSerialPortModem = "COM1"; +# if defined(ESP_PLATFORM) +# if !defined(AE_MODEM_UART_PORT) || !defined(AE_MODEM_UART_TX_GPIO) || \ + !defined(AE_MODEM_UART_RX_GPIO) +# error "Select a modem board USER_CONFIG with UART port and pins" +# endif +static SerialInit const serial_init_modem{ + .port_name = AE_MODEM_UART_PORT, + .baud_rate = kBaudRate::kBaudRate115200, + .tx_io_num = AE_MODEM_UART_TX_GPIO, + .rx_io_num = AE_MODEM_UART_RX_GPIO, + .rts_io_num = -1, + .cts_io_num = -1}; +# else +static constexpr std::string_view kSerialPortModem = "COM28"; SerialInit serial_init_modem = {std::string(kSerialPortModem), kBaudRate::kBaudRate115200}; +# endif -static ae::ModemInit const modem_init{serial_init_modem, - {}, - {}, - 1111, - false, - ae::kModemMode::kModeNbIot, - "00001", - "", - "internet", - "user", - "password", - ae::kAuthType::kAuthTypeNone, - false, - "", - "", - "", - false}; +static ae::ModemInit const modem_init{ + serial_init_modem, ///< Platform serial port configuration. + {}, ///< Requested power-saving configuration. + {}, ///< Base-station configuration data. + 1111, ///< Numeric SIM PIN used when use_pin is enabled. + false, ///< Whether to submit the configured SIM PIN. + ae::kModemMode::kModeNbIot, ///< Requested radio access mode. + "00001", ///< Numeric operator code; an empty value permits + ///< automatic selection. + "", ///< Operator name, preferred over operator_code + ///< when supported. + "internet", ///< Access point name for packet data. + "user", ///< APN authentication user name. + "pass", ///< APN authentication password. + ae::kAuthType::kAuthTypeNone, ///< Requested APN authentication method. + false, ///< Optional authentication flag; support is driver-specific. + "", ///< Optional authentication user name. + "", ///< Optional authentication password. + "", ///< Optional SSL certificate configuration. + false ///< Optional SSL flag; support is driver-specific. +}; static std::unique_ptr construct_aether_app() { return AetherApp::Construct( diff --git a/examples/message_server/CMakeLists.txt b/examples/message_server/CMakeLists.txt index 551f29265..4586c88aa 100644 --- a/examples/message_server/CMakeLists.txt +++ b/examples/message_server/CMakeLists.txt @@ -34,6 +34,11 @@ else() INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} ../common REQUIRES esp_wifi esp_netif nvs_flash spiffs esp_driver_uart) + target_compile_definitions(${COMPONENT_LIB} PRIVATE + WIFI_SSID="${WIFI_SSID}" + WIFI_PASSWORD="${WIFI_PASSWORD}" + ) + add_subdirectory("../../" aether) target_link_libraries(${COMPONENT_LIB} PRIVATE aether) else() diff --git a/examples/prepared_test_iot/CMakeLists.txt b/examples/prepared_test_iot/CMakeLists.txt new file mode 100644 index 000000000..8606963fb --- /dev/null +++ b/examples/prepared_test_iot/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright 2026 Aethernet Inc. +# SPDX-License-Identifier: Apache-2.0 + +if(NOT WIN32) + message(FATAL_ERROR "prepared-test-iot requires Windows") +endif() +if(NOT AE_DISTILLATION OR AE_FILTRATION) + message(FATAL_ERROR "prepared-test-iot requires AE_DISTILLATION=ON and AE_FILTRATION=OFF") +endif() + +add_executable(prepared-test-iot main.cpp) +target_link_libraries(prepared-test-iot PRIVATE aether) +set_target_properties(prepared-test-iot PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") +if(MSVC) + target_compile_options(prepared-test-iot PRIVATE /Zc:preprocessor /W4 /WX) +endif() + +if(AE_BUILD_TESTS) + add_test(NAME prepared-test-iot-help COMMAND prepared-test-iot --help) + add_test(NAME prepared-test-iot-invalid-arguments COMMAND prepared-test-iot) + set_tests_properties(prepared-test-iot-invalid-arguments PROPERTIES WILL_FAIL TRUE) +endif() diff --git a/examples/prepared_test_iot/README.md b/examples/prepared_test_iot/README.md new file mode 100644 index 000000000..24cf813d8 --- /dev/null +++ b/examples/prepared_test_iot/README.md @@ -0,0 +1,211 @@ +# Prepared LTE IoT packet example for Windows + +This example sends one prepared Aether temperature message through a cellular +modem connected to a Windows COM port. It supports **SIMCom SIM7070G** and +**Nordic Thingy:91 X**, using either **NB-IoT** or **LTE-M**. + +## Choose your modem + +| Hardware | Visual Studio configuration | Configuration header | +| --- | --- | --- | +| SIM7070G | `prepared-iot-sim7070` | `user_config_sim7070g.h` | +| Thingy:91 X | `prepared-iot-thingy91x` | `user_config_thingy91x.h` | + +Both configurations build the same executable, `prepared-test-iot.exe`, in +separate directories. Each header enables exactly one modem driver. SIM7070G +uses the library's `Sim7070AtModem` driver; its existing macro is +`AE_ENABLE_SIM7070`. The former generic `user_config.h` has been renamed to +`user_config_sim7070g.h` to make the hardware selection explicit. + +Thingy:91 X must run **Serial LTE Modem firmware** supporting `AT#XSOCKET`, +`AT#XCONNECT`, and `AT#XSEND`, as required by `Thingy91xAtModem`. + +## Build in Visual Studio 2026 (no shell required) + +### 1. Open the repository + +Install the **Desktop development with C++** workload, including **C++ CMake +tools for Windows** and a Windows SDK, through Visual Studio Installer. + +In Visual Studio, select **File > Open > Folder** and open the repository root: + +```text +G:\projects\prj_aether\GitHub\aether-client-cpp +``` + +Open the root folder, not just `examples\prepared_test_iot`. This example uses +the root CMake project and its CPM dependencies. No generated `.sln` is needed. + +### 2. Select the modem configuration + +In the toolbar configuration dropdown (which may initially show `x64-Debug`), +select **prepared-iot-sim7070** or **prepared-iot-thingy91x**. + +These entries are already provided in the root `CMakeSettings.json`. They set +`USER_CONFIG`, enable this example and tests, and use x64 MSVC with Ninja. +Wait for CMake configuration to finish in the Output window. The first +configuration may download dependencies and requires internet access. + +### 3. Build the executable + +Switch Solution Explorer to **CMake Targets View**. Find **prepared-test-iot**, +right-click it, and select **Build**. The executable will be created at: + +```text +out\build\prepared-iot-sim7070\prepared-test-iot.exe +out\build\prepared-iot-thingy91x\prepared-test-iot.exe +``` + +After updating the serial-port shutdown implementation, perform a full clean +rebuild of the selected configuration: the `ISerialPort` interface has changed, +so previously compiled objects must not be reused. + +If the configurations are missing, reopen the root folder and check that Visual +Studio is using `CMakeSettings.json`. If CMake Presets integration was explicitly +enabled in IDE options, disable it for this workflow and reopen the folder. +The supplied configurations use CMakeSettings, not CMakePresets. + +See Microsoft's [CMake project guide](https://learn.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-170) +and [CMake settings reference](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170). + +## Run and debug in Visual Studio + +1. Select `prepared-test-iot.exe` in the **Startup Item** dropdown. +2. Select **Debug > Debug and Launch Settings for prepared-test-iot** (the + label may include `.exe`). Visual Studio opens `.vs/launch.vs.json`. +3. Keep the generated `project`, `projectTarget`, and `type` fields. Add or + update `args` and `currentDir` in that target's configuration: + +```json +"args": ["--help"], +"currentDir": "${cmake.buildRoot}" +``` + +4. Press **F5** to build and debug. `--help` prints usage without opening a + COM port or contacting the network. +5. To send a packet, replace `args` with your modem and recipient settings: + +```json +"args": [ + "COM9", + "internet.mts.ru", + "REPLACE_WITH_REAL_DESTINATION_UID", + "2500", + "nb-iot", + "25001" +], +"currentDir": "${cmake.buildRoot}" +``` + +The UID placeholder must be replaced with an existing recipient's Aether UID +in UUID format. COM9, the APN, and operator code are examples; use the values +for your hardware and SIM. Select the corresponding build configuration before +switching modems, and update the COM port in the launch arguments. + +The working directory setting keeps each configuration's `state` directory +inside its build directory. See Microsoft's [CMake debugging guide](https://learn.microsoft.com/en-us/cpp/build/configure-cmake-debugging-sessions?view=msvc-170) +for launch settings. + +## Program arguments + +```text +prepared-test-iot COM APN DESTINATION_UID [TEMPERATURE_CENTI_C] [nb-iot|lte-m] [OPERATOR_CODE] [PARENT_UID] +``` + +| Argument | Meaning / default | +| --- | --- | +| `COM` | Required Windows port, for example `COM9` or `COM15`. | +| `APN` | Required packet-data APN supplied by your carrier. | +| `DESTINATION_UID` | Required Aether recipient UID. | +| `TEMPERATURE_CENTI_C` | Temperature in hundredths of a degree Celsius; default `2500` (25 C), range `-3000` to `12500`. | +| Radio mode | `nb-iot` (default) or `lte-m`. | +| `OPERATOR_CODE` | Optional five- or six-digit carrier code; omitted or empty means automatic selection. | +| `PARENT_UID` | Optional registration parent; defaults to `3ac93165-3d37-4970-87a6-fa4ee27744e4`, as in the existing cloud examples. | + +Arguments are positional. To specify a parent UID with automatic operator +selection, use an empty string for the operator argument in the JSON array. + +The serial speed is **115200 baud**. Disable the SIM PIN before using this +example. It uses APN settings without authentication; see `ParseOptions()` in +`main.cpp` to customize `ModemInit` for other requirements. The modem needs +power, an antenna, and a SIM/carrier supporting the selected radio mode and UDP. + +## What happens during a run + +1. The full Aether client registers a sender through `ModemAdapter` and looks + up the destination cloud through the cellular connection. +2. It shuts down ordinary connections, explicitly releases the serial port, + and reserves one nonce with `PrepareSendMessageBlock`, then saves state and + releases the preparation application. +3. A separate RAM-only context supplies the scheduler and Windows poller, + with an empty adapter registry and no registered clients. A fresh modem + driver connects and opens a UDP socket to the prepared IPv4 endpoint. +4. `EncodePacket` produces the packet bytes, which are passed directly to + `IModemDriver::WritePacket` without additional Aether or TCP framing. +5. The program waits for the send result, closes sockets, and shuts down the + modem network service and serial port through `Stop()`. + +`Stop()` releases the COM port before reporting completion, including on +shutdown errors. It does not rely on destruction of the persistent object graph. +Windows waits for canceled serial reads to finish before closing the handle. +No delay is needed between the two stages. + +For an optional hardware check, run +`tests/run/test-serial-port.exe --serial-reopen COM9` from the build directory. +It checks 100 immediate close/reopen cycles without sending AT commands; use +the COM port assigned to your modem and close any serial terminal first. + +This demonstrates **prepare followed by send in one process**. Each run creates +a new sender in distillation mode. Full-client state is saved through the +standard application path under the working directory's `state` folder. The +prepared block stays in memory, is used once, and is not saved for reuse after +restarting the program. + +The implementation follows the [thermometer prepared-send example](https://github.com/aethernetio/temperature-sensor/blob/thermometer-prepared-send-v0/main/prepared_send/prepared_send.cpp). +It preserves that example's temperature payload: + +```text +03 03 0A +temperature = (centi_celsius / 100 + 30) * 3 +``` + +Division is integer division. For 2500 (25 C), the payload is `03 03 0A A5 00`. + +## Results and tests + +| Exit code | Meaning | +| --- | --- | +| `0` | The modem accepted the entire packet and completed shutdown. | +| `1` | Preparation, network, encoding, send, or shutdown failed. | +| `2` | Invalid command-line arguments. | + +Modem acceptance is **not recipient delivery confirmation**. Check receipt at +the destination. Real LTE testing requires connected hardware and a valid +recipient UID. + +Both configurations enable tests. In Visual Studio's CMake Targets View, build +`test-prepared-packet` and `test-serial-port` as well as `prepared-test-iot`. +These cover prepared-packet encoding and modem AT operations using mock serial +ports; the example also registers help and invalid-argument CTest checks. + +## Optional command-line workflow + +For automation, open Developer PowerShell for VS at the repository root: + +```powershell +cmake -S . -B out/build/prepared-iot-sim7070 -G Ninja ` + '-DCMAKE_BUILD_TYPE=Debug' '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON' ` + '-DAE_BUILD_PREPARED_TEST_IOT=ON' '-DAE_BUILD_EXAMPLES=OFF' ` + '-DAE_BUILD_TOOLS=OFF' '-DAE_INSTALL=OFF' '-DAE_BUILD_TESTS=ON' ` + '-DAE_DISTILLATION=ON' '-DAE_FILTRATION=OFF' ` + '-DUSER_CONFIG=examples/prepared_test_iot/user_config_sim7070g.h' +cmake --build out/build/prepared-iot-sim7070 --target prepared-test-iot test-prepared-packet test-serial-port --parallel +ctest --test-dir out/build/prepared-iot-sim7070 -R 'prepared|serial-port|modem-shutdown' --output-on-failure +``` + +For Thingy:91 X, change the build directory to `out/build/prepared-iot-thingy91x` +and `USER_CONFIG` to `examples/prepared_test_iot/user_config_thingy91x.h`. +An existing build using the old `user_config.h` must be reconfigured with the +renamed header before building. `CPM_SOURCE_CACHE` can share dependency downloads +between build directories. The example is included only when +`AE_BUILD_PREPARED_TEST_IOT=ON`. diff --git a/examples/prepared_test_iot/main.cpp b/examples/prepared_test_iot/main.cpp new file mode 100644 index 000000000..48e6b68f9 --- /dev/null +++ b/examples/prepared_test_iot/main.cpp @@ -0,0 +1,343 @@ +// Copyright 2026 Aethernet Inc. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aether-miscpp/format/format.h" +#include "aether-objects/domain_storage/ram_domain_storage.h" +#include "aether-objects/obj/obj_ptr.h" +#include "aether/adapter_registry.h" +#include "aether/adapters/modem_adapter.h" +#include "aether/aether_app.h" +#include "aether/connection_manager/client_cloud_manager.h" +#include "aether/executors/executors.h" +#include "aether/global_ids.h" +#include "aether/modems/imodem_driver.h" +#include "aether/modems/modem_factory.h" +#include "aether/prepared_packet/packet_encoder.h" +#include "aether/types/uid.h" + +#if !AE_SUPPORT_MODEMS || AE_ENABLE_BG95 || \ + (AE_ENABLE_SIM7070 + AE_ENABLE_THINGY91X != 1) +# error "Enable exactly one modem: SIM7070 or Thingy91X" +#endif +#if !AE_SUPPORT_UDP || !AE_SUPPORT_REGISTRATION +# error "Prepared IoT example requires UDP and registration support" +#endif + +namespace ae::examples::main_internal { +using Block = prepared_packet::PreparedSendMessageBlock; +constexpr auto kOperationTimeout = std::chrono::seconds{180}; +constexpr auto kPreparationTimeout = std::chrono::minutes{10}; + +struct Options { + ModemInit modem{}; + Uid destination{}; + Uid parent = Uid::FromString("3ac93165-3d37-4970-87a6-fa4ee27744e4"); + int temperature = 2500; +}; + +void Usage() { + std::cout + << "prepared-test-iot COM APN DESTINATION_UID [TEMPERATURE_CENTI_C] " + "[nb-iot|lte-m] [OPERATOR_CODE] [PARENT_UID]\n" +#if AE_ENABLE_SIM7070 + << "SIM7070" +#else + << "Thingy:91 X (Serial LTE Modem firmware)" +#endif + << ", 115200 baud, SIM PIN disabled. Default: 2500 (25 C), " + "NB-IoT, automatic operator selection.\n" + << "Registers/selects a sender over LTE, prepares one packet, shuts down " + "the full client, then sends it through a fresh modem UDP socket.\n"; +} + +bool ParseUid(std::string_view text, Uid& uid) { + if (text.size() != 36) { + return false; + } + for (std::size_t i = 0; i < text.size(); ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) { + if (text[i] != '-') { + return false; + } + } else if (!((text[i] >= '0' && text[i] <= '9') || + (text[i] >= 'a' && text[i] <= 'f') || + (text[i] >= 'A' && text[i] <= 'F'))) { + return false; + } + } + uid = Uid::FromString(UidString{text}); + return uid != Uid{}; +} + +bool ParseOptions(int argc, char** argv, Options& options) { + if (argc < 4 || argc > 8 || !ParseUid(argv[3], options.destination)) { + return false; + } + auto port = std::string{argv[1]}; + if (!port.starts_with("COM") || port.size() <= 3 || + !std::all_of(port.begin() + 3, port.end(), + [](char c) { return c >= '0' && c <= '9'; })) { + return false; + } + // WinSerialPort adds the Windows device namespace (including COM10+). + options.modem.serial_init = {port, kBaudRate::kBaudRate115200}; + auto apn = std::string{argv[2]}; + if (apn.empty() || !std::all_of(apn.begin(), apn.end(), [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '-'; + })) { + return false; + } + options.modem.apn_name = std::move(apn); + options.modem.modem_mode = kModemMode::kModeNbIot; + if (argc >= 5) { + auto text = std::string_view{argv[4]}; + auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), + options.temperature); + if (error != std::errc{} || end != text.data() + text.size() || + options.temperature < -3000 || options.temperature > 12500) { + return false; + } + } + if (argc >= 6) { + auto mode = std::string_view{argv[5]}; + if (mode == "lte-m") { + options.modem.modem_mode = kModemMode::kModeCatM; + } else if (mode != "nb-iot") { + return false; + } + } + if (argc >= 7) { + auto code = std::string{argv[6]}; + if (!code.empty() && ((code.size() != 5 && code.size() != 6) || + !std::all_of(code.begin(), code.end(), [](char c) { + return c >= '0' && c <= '9'; + }))) { + return false; + } + options.modem.operator_code = std::move(code); + } + return argc < 8 || ParseUid(argv[7], options.parent); +} + +// Drive the application scheduler, retaining the waiter until completion. +template +bool Await(AetherApp& app, Sender&& sender, Duration timeout, + std::string_view label) { + bool success = false; + bool done = false; + auto pipeline = std::forward(sender) | + ex::with_timeout(AeContext{app}, timeout) | + ex::then([&](auto&&...) noexcept { success = true; }) | + ex::upon_error([&](auto const& error) noexcept { + if constexpr (std::is_same_v, + ex::TimeoutError>) { + std::cerr << label << ": timeout\n"; + } else { + std::cerr << label << ": operation failed\n"; + } + }); + auto waiter = ex::AsyncWaiter{AeContext{app}, std::move(pipeline), + [&](auto) { done = true; }}; + while (!done) { + auto next = app.Update(Now()); + if (!done) { + app.WaitUntil(next); + } + } + return success; +} + +template +bool AwaitModem(AetherApp& app, Operation* operation, Consumer&& consume, + std::string_view label) { + if (operation == nullptr) { + std::cerr << label << ": modem rejected operation\n"; + return false; + } + // Drivers can return an already-completed action; action_wait only observes + // future events, so inspect the stored result before subscribing. + if (operation->result()) { + auto const& result = *operation->result(); + if (!result) { + std::cerr << label << ": modem error " << static_cast(result.error()) + << '\n'; + return false; + } + consume(result.value()); + return true; + } + return Await( + app, + ex::action_wait(*operation) | ex::then(std::forward(consume)), + kOperationTimeout, label); +} + +std::optional Prepare(Options const& options) { + auto app = AetherApp::Construct( + AetherAppContext{}.AddAdapterFactory([&](AetherAppContext const& ctx) { + return ModemAdapter::ptr::Create( + CreateWith{ctx.domain()}.with_id(GlobalId::kModemAdapter), + ctx.aether(), ctx.poller(), options.modem); + })); + if (!app) { + std::cerr << "Cannot construct preparation application\n"; + return std::nullopt; + } + Client::ptr client; + auto pipeline = ex::action_wait(app->aether()->SelectClient( + options.parent, "prepared-test-iot")) | + ex::then([&](Client::ptr const& selected) noexcept { + client = selected; + }); + auto ready = Await(*app, std::move(pipeline), kPreparationTimeout, + "Sender registration"); + if (ready) { + auto loaded_client = client.Load(); + if (!loaded_client) { + ready = false; + } else { + auto manager = loaded_client->cloud_manager().Load(); + ready = + manager && + Await(*app, ex::action_wait(manager->GetCloud(options.destination)), + kPreparationTimeout, "Destination cloud lookup"); + } + } + + // Stop ordinary traffic and close the serial port before reserving the nonce. + // Port release must not depend on reclamation of the persistent object graph. + app->Exit(ready ? 0 : 1); + while (!app->IsExited()) { + app->WaitUntil(app->Update(Now())); + } + if (!ready || app->ExitCode() != 0) { + return std::nullopt; + } + auto prepared = + prepared_packet::PrepareSendMessageBlock(client, options.destination, 1); + if (!prepared) { + std::cerr << "Preparation failed: " << prepared.error().msg << '\n'; + return std::nullopt; + } + std::cout << "Prepared one message; modem stopped and serial port released\n"; + // AetherApp's destructor persists the reserved nonce range. The prepared + // block itself lives only in this process and is never replayed from disk. + return std::move(prepared).value(); +} + +bool Send(Options const& options, Block& block) { + // A separate RAM-only domain supplies the scheduler and Windows poller. + // It has no clients or adapters and cannot send ordinary Aether messages. + auto context = + AetherAppContext{[] { return std::make_unique(); }}; + auto app = AetherApp::Construct( + std::move(context).AdaptersFactory([](AetherAppContext const& ctx) { + return AdapterRegistry::ptr::Create( + CreateWith{ctx.domain()}.with_id(GlobalId::kAdapterRegistry)); + })); + if (!app) { + return false; + } + auto const& poller = app->aether()->poller; + auto loaded_poller = poller.Load(); + if (!loaded_poller) { + return false; + } + DataBuffer packet; + auto modem = ModemDriverFactory::CreateModem(*app, poller, options.modem); + if (!modem) { + return false; + } + auto ignore = [](auto const&) noexcept {}; + auto success = AwaitModem(*app, modem->Start(), ignore, "LTE registration"); + auto endpoint = block.Resolve()->endpoint; + ConnectionIndex connection = kInvalidConnectionIndex; + if (success && (endpoint.protocol != Protocol::kUdp || + endpoint.address.Index() != AddrVersion::kIpV4)) { + std::cerr << "This example requires an IPv4 UDP endpoint\n"; + success = false; + } + if (success) { + auto host = Format("{}", endpoint.address.Get()); + std::cout << "Opening LTE UDP socket to " << host << ':' << endpoint.port + << '\n'; + success = AwaitModem( + *app, modem->OpenNetwork(Protocol::kUdp, host, endpoint.port), + [&](ConnectionIndex value) noexcept { connection = value; }, + "UDP open"); + } + if (success) { + // Exact thermometer payload: API prefix followed by encoded temperature. + auto temperature = + static_cast((options.temperature / 100 + 30) * 3); + auto payload = DataBuffer{0x03, 0x03, 0x0A, + static_cast(temperature & 0xff), + static_cast(temperature >> 8)}; + auto encoded = prepared_packet::EncodePacket(block, payload, packet); + success = static_cast(encoded); + if (!success) { + std::cerr << "EncodePacket failed: " << encoded.error().msg << '\n'; + } + } + if (success) { + std::size_t written = 0; + success = AwaitModem( + *app, modem->WritePacket(connection, packet), + [&](std::size_t size) noexcept { written = size; }, "UDP send"); + success = success && written == packet.size(); + if (success) { + std::cout + << "Modem accepted " << written + << " prepared UDP bytes (recipient delivery is not confirmed)\n"; + } else { + std::cerr << "Prepared UDP packet was not fully sent\n"; + } + } + // Stop also closes sockets. Keep packet, driver, poller and scheduler alive + // while any pending write and the queued shutdown operation complete. + auto* stop = modem->Stop(); + auto stopped = AwaitModem(*app, stop, ignore, "Modem shutdown"); + // A waiter timeout does not cancel driver-owned actions. Drain shutdown + // before releasing the driver; every AT stage has its own bounded wait. + if (stop != nullptr) { + while (!stop->is_finished()) { + auto next = app->Update(Now()); + if (!stop->is_finished()) { + app->WaitUntil(next); + } + } + } + return success && stopped; +} +} // namespace ae::examples::main_internal + +int main(int argc, char** argv) { + if (argc == 2 && std::string_view{argv[1]} == "--help") { + ae::examples::main_internal::Usage(); + return 0; + } + ae::examples::main_internal::Options options; + if (!ae::examples::main_internal::ParseOptions(argc, argv, options)) { + ae::examples::main_internal::Usage(); + return 2; + } + auto block = ae::examples::main_internal::Prepare(options); + if (!block) { + return 1; + } + return ae::examples::main_internal::Send(options, *block) ? 0 : 1; +} diff --git a/examples/prepared_test_iot/user_config_sim7070g.h b/examples/prepared_test_iot/user_config_sim7070g.h new file mode 100644 index 000000000..f4cc4ad3b --- /dev/null +++ b/examples/prepared_test_iot/user_config_sim7070g.h @@ -0,0 +1,18 @@ +// Copyright 2026 Aethernet Inc. +// SPDX-License-Identifier: Apache-2.0 +#ifndef EXAMPLES_PREPARED_TEST_IOT_USER_CONFIG_SIM7070G_H_ +#define EXAMPLES_PREPARED_TEST_IOT_USER_CONFIG_SIM7070G_H_ + +#include "config/user_config_hydrogen.h" // IWYU pragma: export + +#define AE_SUPPORT_MODEMS 1 +#define AE_ENABLE_SIM7070 1 +#define AE_ENABLE_THINGY91X 0 +#define AE_ENABLE_BG95 0 + +// PreparedSendMessage needs a numeric UDP endpoint. +#define AE_SUPPORT_UDP 1 +#define AE_SUPPORT_IPV4 1 +#define AE_SUPPORT_IPV6 0 + +#endif // EXAMPLES_PREPARED_TEST_IOT_USER_CONFIG_SIM7070G_H_ diff --git a/examples/prepared_test_iot/user_config_thingy91x.h b/examples/prepared_test_iot/user_config_thingy91x.h new file mode 100644 index 000000000..130271090 --- /dev/null +++ b/examples/prepared_test_iot/user_config_thingy91x.h @@ -0,0 +1,16 @@ +// Copyright 2026 Aethernet Inc. +// SPDX-License-Identifier: Apache-2.0 +#ifndef EXAMPLES_PREPARED_TEST_IOT_USER_CONFIG_THINGY91X_H_ +#define EXAMPLES_PREPARED_TEST_IOT_USER_CONFIG_THINGY91X_H_ + +#include "config/user_config_hydrogen.h" // IWYU pragma: export + +#define AE_SUPPORT_MODEMS 1 +#define AE_ENABLE_SIM7070 0 +#define AE_ENABLE_THINGY91X 1 +#define AE_ENABLE_BG95 0 +#define AE_SUPPORT_UDP 1 +#define AE_SUPPORT_IPV4 1 +#define AE_SUPPORT_IPV6 0 + +#endif // EXAMPLES_PREPARED_TEST_IOT_USER_CONFIG_THINGY91X_H_ diff --git a/projects/espressif_riscv/platformio/aether-client-cpp/.clangd b/projects/espressif_riscv/platformio/aether-client-cpp/.clangd new file mode 100644 index 000000000..437f25545 --- /dev/null +++ b/projects/espressif_riscv/platformio/aether-client-cpp/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/projects/espressif_riscv/platformio/aether-client-cpp/sdkconfig.defaults b/projects/espressif_riscv/platformio/aether-client-cpp/sdkconfig.defaults index a6f0ec386..90dcdc8ea 100644 --- a/projects/espressif_riscv/platformio/aether-client-cpp/sdkconfig.defaults +++ b/projects/espressif_riscv/platformio/aether-client-cpp/sdkconfig.defaults @@ -1,6 +1,6 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="system/partitions.csv" -CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=24576 CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=8192 # diff --git a/projects/espressif_riscv/vscode/aether-client-cpp/.clangd b/projects/espressif_riscv/vscode/aether-client-cpp/.clangd new file mode 100644 index 000000000..437f25545 --- /dev/null +++ b/projects/espressif_riscv/vscode/aether-client-cpp/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/c_cpp_properties.json b/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/c_cpp_properties.json index 4fd114345..efeb4d9ad 100644 --- a/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/c_cpp_properties.json +++ b/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/c_cpp_properties.json @@ -2,7 +2,7 @@ "configurations": [ { "name": "ESP-IDF", - "compilerPath": "${config:idf.toolsPathWin}\\tools\\riscv32-esp-elf\\esp-13.2.0_20230928\\riscv32-esp-elf\\bin\\riscv32-esp-elf-gcc.exe", + "compilerPath": "", "compileCommands": "${workspaceFolder}/build/compile_commands.json", "includePath": [ "${config:idf.espIdfPath}/components/**", diff --git a/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json b/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json index e487d68c2..79f2e1294 100644 --- a/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json +++ b/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json @@ -3,12 +3,15 @@ "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", + "-DIDF_TARGET=esp32c6", + "-DWIFI_SSID=Test1234", + "-DWIFI_PASSWORD=Test1234", "-DUSER_CONFIG=../../../../../config/user_config_hydrogen.h" ], "C_Cpp.intelliSenseEngine": "Tag Parser", "idf.adapterTargetName": "esp32c6", "idf.openOcdConfigs": [ - "board/esp32c6-ftdi.cfg" + "board/esp32c6-lpcore-builtin.cfg" ], "idf.openOcdLaunchArgs": [ "-c adapter speed 20000", @@ -120,5 +123,17 @@ "filesystem": "cpp" }, "idf.flashType": "UART", - "idf.portWin": "COM9" + "idf.portWin": "COM9", + "idf.currentSetup": "G:\\dev\\esp32\\v6.1\\esp-idf", + "idf.customExtraVars": { + "OPENOCD_USB_ADAPTER_LOCATION": "2-10", + "IDF_TARGET": "esp32c6" + }, + "clangd.path": "G:\\dev\\esp32\\tools\\esp-clangd\\esp-21.1.3_20260408\\esp-clangd\\bin\\clangd.exe", + "clangd.arguments": [ + "--background-index", + "--query-driver=**", + "--compile-commands-dir=g:\\projects\\prj_aether\\GitHub\\aether-client-cpp\\projects\\espressif_riscv\\vscode\\aether-client-cpp\\build" + ], + "idf.port": "COM9" } \ No newline at end of file diff --git a/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig b/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig new file mode 100644 index 000000000..55d482e72 --- /dev/null +++ b/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig @@ -0,0 +1,3196 @@ +# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) 6.1.0 Project Configuration +# +# default: +CONFIG_SOC_ADC_SUPPORTED=y +# default: +CONFIG_SOC_DEDICATED_GPIO_SUPPORTED=y +# default: +CONFIG_SOC_UART_SUPPORTED=y +# default: +CONFIG_SOC_UHCI_SUPPORTED=y +# default: +CONFIG_SOC_GDMA_SUPPORTED=y +# default: +CONFIG_SOC_AHB_GDMA_SUPPORTED=y +# default: +CONFIG_SOC_GPTIMER_SUPPORTED=y +# default: +CONFIG_SOC_PCNT_SUPPORTED=y +# default: +CONFIG_SOC_MCPWM_SUPPORTED=y +# default: +CONFIG_SOC_TWAI_SUPPORTED=y +# default: +CONFIG_SOC_ETM_SUPPORTED=y +# default: +CONFIG_SOC_PARLIO_SUPPORTED=y +# default: +CONFIG_SOC_BT_SUPPORTED=y +# default: +CONFIG_SOC_IEEE802154_SUPPORTED=y +# default: +CONFIG_SOC_ASYNC_MEMCPY_SUPPORTED=y +# default: +CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED=y +# default: +CONFIG_SOC_TEMP_SENSOR_SUPPORTED=y +# default: +CONFIG_SOC_PHY_SUPPORTED=y +# default: +CONFIG_SOC_WIFI_SUPPORTED=y +# default: +CONFIG_SOC_SUPPORTS_SECURE_DL_MODE=y +# default: +CONFIG_SOC_ULP_SUPPORTED=y +# default: +CONFIG_SOC_LP_CORE_SUPPORTED=y +# default: +CONFIG_SOC_EFUSE_KEY_PURPOSE_FIELD=y +# default: +CONFIG_SOC_EFUSE_SUPPORTED=y +# default: +CONFIG_SOC_RTC_FAST_MEM_SUPPORTED=y +# default: +CONFIG_SOC_RTC_MEM_SUPPORTED=y +# default: +CONFIG_SOC_I2S_SUPPORTED=y +# default: +CONFIG_SOC_RMT_SUPPORTED=y +# default: +CONFIG_SOC_SDM_SUPPORTED=y +# default: +CONFIG_SOC_GPSPI_SUPPORTED=y +# default: +CONFIG_SOC_LEDC_SUPPORTED=y +# default: +CONFIG_SOC_I2C_SUPPORTED=y +# default: +CONFIG_SOC_SYSTIMER_SUPPORTED=y +# default: +CONFIG_SOC_SUPPORT_COEXISTENCE=y +# default: +CONFIG_SOC_AES_SUPPORTED=y +# default: +CONFIG_SOC_MPI_SUPPORTED=y +# default: +CONFIG_SOC_SHA_SUPPORTED=y +# default: +CONFIG_SOC_HMAC_SUPPORTED=y +# default: +CONFIG_SOC_DIG_SIGN_SUPPORTED=y +# default: +CONFIG_SOC_ECC_SUPPORTED=y +# default: +CONFIG_SOC_FLASH_ENC_SUPPORTED=y +# default: +CONFIG_SOC_SECURE_BOOT_SUPPORTED=y +# default: +CONFIG_SOC_SDIO_SLAVE_SUPPORTED=y +# default: +CONFIG_SOC_BOD_SUPPORTED=y +# default: +CONFIG_SOC_APM_SUPPORTED=y +# default: +CONFIG_SOC_PMU_SUPPORTED=y +# default: +CONFIG_SOC_PMU_PVT_SUPPORTED=y +# default: +CONFIG_SOC_PAU_SUPPORTED=y +# default: +CONFIG_SOC_RTC_TIMER_SUPPORTED=y +# default: +CONFIG_SOC_LP_AON_SUPPORTED=y +# default: +CONFIG_SOC_LP_PERIPHERALS_SUPPORTED=y +# default: +CONFIG_SOC_LP_I2C_SUPPORTED=y +# default: +CONFIG_SOC_ULP_LP_UART_SUPPORTED=y +# default: +CONFIG_SOC_CLK_TREE_SUPPORTED=y +# default: +CONFIG_SOC_ASSIST_DEBUG_SUPPORTED=y +# default: +CONFIG_SOC_WDT_SUPPORTED=y +# default: +CONFIG_SOC_RTC_WDT_SUPPORTED=y +# default: +CONFIG_SOC_SPI_FLASH_SUPPORTED=y +# default: +CONFIG_SOC_RNG_SUPPORTED=y +# default: +CONFIG_SOC_LIGHT_SLEEP_SUPPORTED=y +# default: +CONFIG_SOC_DEEP_SLEEP_SUPPORTED=y +# default: +CONFIG_SOC_MODEM_CLOCK_SUPPORTED=y +# default: +CONFIG_SOC_REGI2C_SUPPORTED=y +# default: +CONFIG_SOC_PM_SUPPORTED=y +# default: +CONFIG_SOC_SPI_EXTERNAL_NOR_FLASH_SUPPORTED=y +# default: +CONFIG_SOC_XTAL_SUPPORT_40M=y +# default: +CONFIG_SOC_XTAL_CLOCK_PATH_DEPENDS_ON_TOP_DOMAIN=y +# default: +CONFIG_SOC_AES_SUPPORT_DMA=y +# default: +CONFIG_SOC_AES_GDMA=y +# default: +CONFIG_SOC_AES_SUPPORT_AES_128=y +# default: +CONFIG_SOC_AES_SUPPORT_AES_256=y +# default: +CONFIG_SOC_ADC_DIG_CTRL_SUPPORTED=y +# default: +CONFIG_SOC_ADC_DIG_IIR_FILTER_SUPPORTED=y +# default: +CONFIG_SOC_ADC_MONITOR_SUPPORTED=y +# default: +CONFIG_SOC_ADC_DMA_SUPPORTED=y +# default: +CONFIG_SOC_ADC_PERIPH_NUM=1 +# default: +CONFIG_SOC_ADC_MAX_CHANNEL_NUM=7 +# default: +CONFIG_SOC_ADC_ATTEN_NUM=4 +# default: +CONFIG_SOC_ADC_DIGI_CONTROLLER_NUM=1 +# default: +CONFIG_SOC_ADC_PATT_LEN_MAX=8 +# default: +CONFIG_SOC_ADC_DIGI_MAX_BITWIDTH=12 +# default: +CONFIG_SOC_ADC_DIGI_MIN_BITWIDTH=12 +# default: +CONFIG_SOC_ADC_DIGI_IIR_FILTER_NUM=2 +# default: +CONFIG_SOC_ADC_DIGI_MONITOR_NUM=2 +# default: +CONFIG_SOC_ADC_DIGI_RESULT_BYTES=4 +# default: +CONFIG_SOC_ADC_DIGI_DATA_BYTES_PER_CONV=4 +# default: +CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_HIGH=83333 +# default: +CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_LOW=611 +# default: +CONFIG_SOC_ADC_RTC_MIN_BITWIDTH=12 +# default: +CONFIG_SOC_ADC_RTC_MAX_BITWIDTH=12 +# default: +CONFIG_SOC_ADC_CALIBRATION_V1_SUPPORTED=y +# default: +CONFIG_SOC_ADC_SELF_HW_CALI_SUPPORTED=y +# default: +CONFIG_SOC_ADC_CALIB_CHAN_COMPENS_SUPPORTED=y +# default: +CONFIG_SOC_ADC_TEMPERATURE_SHARE_INTR=y +# default: +CONFIG_SOC_ADC_SHARED_POWER=y +# default: +CONFIG_SOC_BROWNOUT_RESET_SUPPORTED=y +# default: +CONFIG_SOC_SHARED_IDCACHE_SUPPORTED=y +# default: +CONFIG_SOC_CACHE_FREEZE_SUPPORTED=y +# default: +CONFIG_SOC_CPU_CORES_NUM=1 +# default: +CONFIG_SOC_CPU_INTR_NUM=32 +# default: +CONFIG_SOC_CPU_HAS_FLEXIBLE_INTC=y +# default: +CONFIG_SOC_INT_PLIC_SUPPORTED=y +# default: +CONFIG_SOC_CPU_HAS_CSR_PC=y +# default: +CONFIG_SOC_CPU_BREAKPOINTS_NUM=4 +# default: +CONFIG_SOC_CPU_WATCHPOINTS_NUM=4 +# default: +CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x80000000 +# default: +CONFIG_SOC_CPU_HAS_PMA=y +# default: +CONFIG_SOC_CPU_IDRAM_SPLIT_USING_PMP=y +# default: +CONFIG_SOC_CPU_PMP_REGION_GRANULARITY=4 +# default: +CONFIG_SOC_CPU_MISALIGNED_ACCESS_ON_PMP_MISMATCH_ISSUE=y +# default: +CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=3072 +# default: +CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16 +# default: +CONFIG_SOC_DS_KEY_CHECK_MAX_WAIT_US=1100 +# default: +CONFIG_SOC_AHB_GDMA_VERSION=1 +# default: +CONFIG_SOC_GDMA_SUPPORT_ETM=y +# default: +CONFIG_SOC_GDMA_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_GPIO_PORT=1 +# default: +CONFIG_SOC_GPIO_PIN_COUNT=31 +# default: +CONFIG_SOC_GPIO_SUPPORT_PIN_GLITCH_FILTER=y +# default: +CONFIG_SOC_GPIO_FLEX_GLITCH_FILTER_NUM=8 +# default: +CONFIG_SOC_GPIO_SUPPORT_ETM=y +# default: +CONFIG_SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP=y +# default: +CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y +# default: +CONFIG_SOC_RTC_GPIO_EDGE_WAKEUP_SUPPORTED=y +# default: +CONFIG_SOC_GPIO_IN_RANGE_MAX=30 +# default: +CONFIG_SOC_GPIO_OUT_RANGE_MAX=30 +# default: +CONFIG_SOC_GPIO_HP_PERIPH_PD_SLEEP_WAKEABLE_MASK=0 +# default: +CONFIG_SOC_GPIO_VALID_DIGITAL_IO_PAD_MASK=0x000000007FFFFF00 +# default: +CONFIG_SOC_GPIO_SUPPORT_FORCE_HOLD=y +# default: +CONFIG_SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP=y +# default: +CONFIG_SOC_GPIO_CLOCKOUT_BY_GPIO_MATRIX=y +# default: +CONFIG_SOC_CLOCKOUT_HAS_SOURCE_GATE=y +# default: +CONFIG_SOC_GPIO_CLOCKOUT_CHANNEL_NUM=3 +# default: +CONFIG_SOC_RTCIO_PIN_COUNT=8 +# default: +CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y +# default: +CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y +# default: +CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y +# default: +CONFIG_SOC_SDM_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_ETM_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_I2C_NUM=2 +# default: +CONFIG_SOC_HP_I2C_NUM=1 +# default: +CONFIG_SOC_LP_I2C_NUM=1 +# default: +CONFIG_SOC_I2C_SUPPORT_XTAL=y +# default: +CONFIG_SOC_I2C_SUPPORT_RTC=y +# default: +CONFIG_SOC_I2C_SUPPORT_10BIT_ADDR=y +# default: +CONFIG_SOC_I2C_SUPPORT_SLAVE=y +# default: +CONFIG_SOC_I2C_SLAVE_SUPPORT_BROADCAST=y +# default: +CONFIG_SOC_I2C_SLAVE_CAN_GET_STRETCH_CAUSE=y +# default: +CONFIG_SOC_I2C_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_I2S_HW_VERSION_2=y +# default: +CONFIG_SOC_I2S_SUPPORTS_ETM=y +# default: +CONFIG_SOC_I2S_SUPPORTS_PCM=y +# default: +CONFIG_SOC_I2S_SUPPORTS_PDM=y +# default: +CONFIG_SOC_I2S_SUPPORTS_PDM_TX=y +# default: +CONFIG_SOC_I2S_SUPPORTS_PCM2PDM=y +# default: +CONFIG_SOC_I2S_SUPPORTS_PDM_RX=y +# default: +CONFIG_SOC_I2S_PDM_MAX_TX_LINES=2 +# default: +CONFIG_SOC_I2S_PDM_MAX_RX_LINES=1 +# default: +CONFIG_SOC_I2S_SUPPORTS_TDM=y +# default: +CONFIG_SOC_LEDC_SUPPORT_PLL_DIV_CLOCK=y +# default: +CONFIG_SOC_LEDC_SUPPORT_XTAL_CLOCK=y +# default: +CONFIG_SOC_LEDC_TIMER_NUM=4 +# default: +CONFIG_SOC_LEDC_CHANNEL_NUM=6 +# default: +CONFIG_SOC_LEDC_TIMER_BIT_WIDTH=20 +# default: +CONFIG_SOC_LEDC_SUPPORT_FADE_STOP=y +# default: +CONFIG_SOC_LEDC_GAMMA_CURVE_FADE_SUPPORTED=y +# default: +CONFIG_SOC_LEDC_GAMMA_CURVE_FADE_RANGE_MAX=16 +# default: +CONFIG_SOC_LEDC_FADE_PARAMS_BIT_WIDTH=10 +# default: +CONFIG_SOC_LEDC_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_LEDC_SUPPORT_ETM=y +# default: +CONFIG_SOC_MMU_PAGE_SIZE_CONFIGURABLE=y +# default: +CONFIG_SOC_MMU_PERIPH_NUM=1 +# default: +CONFIG_SOC_MMU_LINEAR_ADDRESS_REGION_NUM=1 +# default: +CONFIG_SOC_MMU_DI_VADDR_SHARED=y +# default: +CONFIG_SOC_MPU_MIN_REGION_SIZE=0x20000000 +# default: +CONFIG_SOC_MPU_REGIONS_MAX_NUM=8 +# default: +CONFIG_SOC_PCNT_SUPPORT_RUNTIME_THRES_UPDATE=y +# default: +CONFIG_SOC_RMT_MEM_WORDS_PER_CHANNEL=48 +# default: +CONFIG_SOC_RMT_SUPPORT_RX_PINGPONG=y +# default: +CONFIG_SOC_RMT_SUPPORT_TX_LOOP_COUNT=y +# default: +CONFIG_SOC_RMT_SUPPORT_TX_LOOP_AUTO_STOP=y +# default: +CONFIG_SOC_RMT_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_MCPWM_SWSYNC_CAN_PROPAGATE=y +# default: +CONFIG_SOC_MCPWM_SUPPORT_ETM=y +# default: +CONFIG_SOC_MCPWM_CAPTURE_CLK_FROM_GROUP=y +# default: +CONFIG_SOC_MCPWM_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH=16 +# default: +CONFIG_SOC_PARLIO_RX_UNIT_MAX_DATA_WIDTH=16 +# default: +CONFIG_SOC_PARLIO_TX_RX_SHARE_INTERRUPT=y +# default: +CONFIG_SOC_PARLIO_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_MPI_MEM_BLOCKS_NUM=4 +# default: +CONFIG_SOC_MPI_OPERATIONS_NUM=3 +# default: +CONFIG_SOC_RSA_MAX_BIT_LEN=3072 +# default: +CONFIG_SOC_SHA_DMA_MAX_BUFFER_SIZE=3968 +# default: +CONFIG_SOC_SHA_SUPPORT_DMA=y +# default: +CONFIG_SOC_SHA_SUPPORT_RESUME=y +# default: +CONFIG_SOC_SHA_GDMA=y +# default: +CONFIG_SOC_SHA_SUPPORT_SHA1=y +# default: +CONFIG_SOC_SHA_SUPPORT_SHA224=y +# default: +CONFIG_SOC_SHA_SUPPORT_SHA256=y +# default: +CONFIG_SOC_SPI_PERIPH_NUM=2 +# default: +CONFIG_SOC_SPI_MAXIMUM_BUFFER_SIZE=64 +# default: +CONFIG_SOC_SPI_SUPPORT_SLAVE_HD_VER2=y +# default: +CONFIG_SOC_SPI_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_WAIT_IDLE=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_SUSPEND=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_AUTO_RESUME=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_IDLE_INTR=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_SW_SUSPEND=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_CHECK_SUS=y +# default: +CONFIG_SOC_SPI_MEM_SUPPORT_WRAP=y +# default: +CONFIG_SOC_MEMSPI_IS_INDEPENDENT=y +# default: +CONFIG_SOC_MEMSPI_ENCRYPTION_ALIGNMENT=16 +# default: +CONFIG_SOC_SYSTIMER_SUPPORT_ETM=y +# default: +CONFIG_SOC_LP_TIMER_BIT_WIDTH_LO=32 +# default: +CONFIG_SOC_LP_TIMER_BIT_WIDTH_HI=16 +# default: +CONFIG_SOC_RTC_TIMER_V2=y +# default: +CONFIG_SOC_TIMER_SUPPORT_ETM=y +# default: +CONFIG_SOC_TIMER_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_MWDT_SUPPORT_XTAL=y +# default: +CONFIG_SOC_MWDT_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_TWAI_CONTROLLER_NUM=2 +# default: +CONFIG_SOC_TWAI_MASK_FILTER_NUM=1 +# default: +CONFIG_SOC_TWAI_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_EFUSE_DIS_DOWNLOAD_ICACHE=y +# default: +CONFIG_SOC_EFUSE_DIS_PAD_JTAG=y +# default: +CONFIG_SOC_EFUSE_DIS_USB_JTAG=y +# default: +CONFIG_SOC_EFUSE_DIS_DIRECT_BOOT=y +# default: +CONFIG_SOC_EFUSE_SOFT_DIS_JTAG=y +# default: +CONFIG_SOC_EFUSE_DIS_ICACHE=y +# default: +CONFIG_SOC_EFUSE_BLOCK9_KEY_PURPOSE_QUIRK=y +# default: +CONFIG_SOC_EFUSE_XTS_AES_KEY_128=y +# default: +CONFIG_SOC_SECURE_BOOT_V2_RSA=y +# default: +CONFIG_SOC_SECURE_BOOT_V2_ECC=y +# default: +CONFIG_SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS=3 +# default: +CONFIG_SOC_EFUSE_REVOKE_BOOT_KEY_DIGESTS=y +# default: +CONFIG_SOC_SUPPORT_SECURE_BOOT_REVOKE_KEY=y +# default: +CONFIG_SOC_FLASH_ENCRYPTED_XTS_AES_BLOCK_MAX=64 +# default: +CONFIG_SOC_FLASH_ENCRYPTION_XTS_AES=y +# default: +CONFIG_SOC_FLASH_ENCRYPTION_XTS_AES_128=y +# default: +CONFIG_SOC_FLASH_ENCRYPTION_PAGE_CONFIGURABLE=y +# default: +CONFIG_SOC_APM_CTRL_FILTER_SUPPORTED=y +# default: +CONFIG_SOC_APM_LP_APM0_SUPPORTED=y +# default: +CONFIG_SOC_APM_SUPPORT_LP_TEE_CTRL=y +# default: +CONFIG_SOC_CRYPTO_DPA_PROTECTION_SUPPORTED=y +# default: +CONFIG_SOC_UART_NUM=3 +# default: +CONFIG_SOC_UART_HP_NUM=2 +# default: +CONFIG_SOC_UART_LP_NUM=1 +# default: +CONFIG_SOC_UART_FIFO_LEN=128 +# default: +CONFIG_SOC_LP_UART_FIFO_LEN=16 +# default: +CONFIG_SOC_UART_BITRATE_MAX=5000000 +# default: +CONFIG_SOC_UART_SUPPORT_RTC_CLK=y +# default: +CONFIG_SOC_UART_SUPPORT_XTAL_CLK=y +# default: +CONFIG_SOC_UART_SUPPORT_WAKEUP_INT=y +# default: +CONFIG_SOC_UART_HAS_LP_UART=y +# default: +CONFIG_SOC_UART_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_UART_WAKEUP_CHARS_SEQ_MAX_LEN=5 +# default: +CONFIG_SOC_UART_WAKEUP_SUPPORT_ACTIVE_THRESH_MODE=y +# default: +CONFIG_SOC_UART_WAKEUP_SUPPORT_FIFO_THRESH_MODE=y +# default: +CONFIG_SOC_UART_WAKEUP_SUPPORT_START_BIT_MODE=y +# default: +CONFIG_SOC_UART_WAKEUP_SUPPORT_CHAR_SEQ_MODE=y +# default: +CONFIG_SOC_COEX_HW_PTI=y +# default: +CONFIG_SOC_EXTERNAL_COEX_ADVANCE=y +# default: +CONFIG_SOC_PHY_DIG_REGS_MEM_SIZE=21 +# default: +CONFIG_SOC_WIFI_LIGHT_SLEEP_CLK_WIDTH=12 +# default: +CONFIG_SOC_RTC_MEM_SUPPORT_SPEED_MODE_SWITCH=y +# default: +CONFIG_SOC_PM_SUPPORT_WIFI_WAKEUP=y +# default: +CONFIG_SOC_PM_SUPPORT_BEACON_WAKEUP=y +# default: +CONFIG_SOC_PM_SUPPORT_BT_WAKEUP=y +# default: +CONFIG_SOC_PM_SUPPORT_EXT1_WAKEUP=y +# default: +CONFIG_SOC_PM_SUPPORT_EXT1_WAKEUP_MODE_PER_PIN=y +# default: +CONFIG_SOC_PM_SUPPORT_CPU_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_MODEM_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_XTAL32K_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_RC32K_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_RC_FAST_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_VDDSDIO_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_TOP_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_HP_AON_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_MAC_BB_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_RTC_PERIPH_PD=y +# default: +CONFIG_SOC_PM_SUPPORT_PMU_MODEM_STATE=y +# default: +CONFIG_SOC_PM_SUPPORT_PMU_CLK_ICG=y +# default: +CONFIG_SOC_PM_SUPPORT_MODEM_CLOCK_DOMAIN_ICG=y +# default: +CONFIG_SOC_PM_SUPPORT_DEEPSLEEP_CHECK_STUB_ONLY=y +# default: +CONFIG_SOC_PM_CPU_RETENTION_BY_SW=y +# default: +CONFIG_SOC_PM_MODEM_RETENTION_BY_REGDMA=y +# default: +CONFIG_SOC_PM_RETENTION_HAS_CLOCK_BUG=y +# default: +CONFIG_SOC_PM_MODEM_RF_FLAG_UPDATE_WORKAROUND=y +# default: +CONFIG_SOC_PM_PAU_LINK_NUM=4 +# default: +CONFIG_SOC_PM_PAU_REGDMA_LINK_MULTI_ADDR=y +# default: +CONFIG_SOC_PM_PAU_REGDMA_LINK_WIFIMAC=y +# default: +CONFIG_SOC_PM_PAU_REGDMA_UPDATE_CACHE_BEFORE_WAIT_COMPARE=y +# default: +CONFIG_SOC_PM_PMU_MIN_SLP_SLOW_CLK_CYCLE_FIXED=y +# default: +CONFIG_SOC_PM_RETENTION_MODULE_NUM=32 +# default: +CONFIG_SOC_PM_TOP_DEPENDS_ON_RTC_PERIPH=y +# default: +CONFIG_SOC_CLK_RC_FAST_SUPPORT_CALIBRATION=y +# default: +CONFIG_SOC_MODEM_CLOCK_IS_INDEPENDENT=y +# default: +CONFIG_SOC_CLK_XTAL32K_SUPPORTED=y +# default: +CONFIG_SOC_CLK_OSC_SLOW_SUPPORTED=y +# default: +CONFIG_SOC_CLK_RC32K_SUPPORTED=y +# default: +CONFIG_SOC_CLK_LP_FAST_SUPPORT_XTAL_D2=y +# default: +CONFIG_SOC_RCC_IS_INDEPENDENT=y +# default: +CONFIG_SOC_TEMPERATURE_SENSOR_SUPPORT_FAST_RC=y +# default: +CONFIG_SOC_TEMPERATURE_SENSOR_SUPPORT_XTAL=y +# default: +CONFIG_SOC_TEMPERATURE_SENSOR_INTR_SUPPORT=y +# default: +CONFIG_SOC_TEMPERATURE_SENSOR_SUPPORT_ETM=y +# default: +CONFIG_SOC_TEMPERATURE_SENSOR_SUPPORT_SLEEP_RETENTION=y +# default: +CONFIG_SOC_TEMPERATURE_SENSOR_UNDER_PD_TOP_DOMAIN=y +# default: +CONFIG_SOC_RNG_CLOCK_IS_INDEPENDENT=y +# default: +CONFIG_SOC_WIFI_HW_TSF=y +# default: +CONFIG_SOC_WIFI_FTM_SUPPORT=y +# default: +CONFIG_SOC_WIFI_GCMP_SUPPORT=y +# default: +CONFIG_SOC_WIFI_WAPI_SUPPORT=y +# default: +CONFIG_SOC_WIFI_TXOP_SUPPORT=y +# default: +CONFIG_SOC_WIFI_CSI_SUPPORT=y +# default: +CONFIG_SOC_WIFI_MESH_SUPPORT=y +# default: +CONFIG_SOC_WIFI_HE_SUPPORT=y +# default: +CONFIG_SOC_WIFI_MAC_VERSION_NUM=2 +# default: +CONFIG_SOC_BLE_SUPPORTED=y +# default: +CONFIG_SOC_BLE_MESH_SUPPORTED=y +# default: +CONFIG_SOC_ESP_NIMBLE_CONTROLLER=y +# default: +CONFIG_SOC_BLE_50_SUPPORTED=y +# default: +CONFIG_SOC_BLE_DEVICE_PRIVACY_SUPPORTED=y +# default: +CONFIG_SOC_BLE_POWER_CONTROL_SUPPORTED=y +# default: +CONFIG_SOC_BLE_PERIODIC_ADV_ENH_SUPPORTED=y +# default: +CONFIG_SOC_BLUFI_SUPPORTED=y +# default: +CONFIG_SOC_BLE_MULTI_CONN_OPTIMIZATION=y +# default: +CONFIG_SOC_BLE_SUBRATE_SUPPORTED=y +# default: +CONFIG_SOC_BLE_PERIODIC_ADV_WITH_RESPONSE=y +# default: +CONFIG_SOC_BLE_USE_WIFI_PWR_CLK_WORKAROUND=y +# default: +CONFIG_SOC_PHY_COMBO_MODULE=y +# default: +CONFIG_SOC_CAPS_NO_RESET_BY_ANA_BOD=y +# default: +CONFIG_SOC_LP_CORE_SINGLE_INTERRUPT_VECTOR=y +# default: +CONFIG_SOC_LP_CORE_SUPPORT_ETM=y +# default: +CONFIG_SOC_LP_CORE_SUPPORT_I2C=y +# default: +CONFIG_SOC_LP_CORE_HW_AUTO_CLRWAKEUPCAUSE=y +# default: +CONFIG_SOC_LP_CORE_LP_UART_WAKEUP_KEEP_TRIGGERED=y +# default: +CONFIG_SOC_DEBUG_HAVE_OCD_STUB_BINS=y +# default: +CONFIG_IDF_CMAKE=y +# default: +CONFIG_IDF_TOOLCHAIN="gcc" +# default: +CONFIG_IDF_TOOLCHAIN_GCC=y +# default: +CONFIG_IDF_TARGET_ARCH_RISCV=y +# default: +CONFIG_IDF_TARGET_ARCH="riscv" +# default: +CONFIG_IDF_TARGET="esp32c6" +# default: +CONFIG_IDF_INIT_VERSION="6.1.0" +# default: +CONFIG_IDF_TARGET_ESP32C6=y +# default: +CONFIG_IDF_FIRMWARE_CHIP_ID=0x000D + +# +# Build type +# +# default: +CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y +# default: +# CONFIG_APP_BUILD_TYPE_RAM is not set +# default: +CONFIG_APP_BUILD_GENERATE_BINARIES=y +# default: +CONFIG_APP_BUILD_BOOTLOADER=y +# default: +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y + +# +# Minimize binary changes between builds +# +# default: +# CONFIG_APP_BUILD_MINIMIZE_BINARY_CHANGES is not set +# end of Minimize binary changes between builds + +# default: +# CONFIG_APP_REPRODUCIBLE_BUILD is not set +# default: +# CONFIG_APP_NO_BLOBS is not set +# end of Build type + +# +# App Update config +# +# end of App Update config + +# +# Bootloader config +# + +# +# Bootloader manager +# +# default: +CONFIG_BOOTLOADER_COMPILE_TIME_DATE=y +# default: +CONFIG_BOOTLOADER_PROJECT_VER=1 +# end of Bootloader manager + +# +# Application Rollback +# +# default: +# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set +# end of Application Rollback + +# +# Recovery Bootloader and Rollback +# +# end of Recovery Bootloader and Rollback + +# default: +CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x0 +# default: +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +# default: +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set +# default: +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set + +# +# Log +# +# default: +CONFIG_BOOTLOADER_LOG_VERSION_1=y +# default: +CONFIG_BOOTLOADER_LOG_VERSION=1 +CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y +# CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_INFO is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set +# default: +CONFIG_BOOTLOADER_LOG_LEVEL=0 + +# +# Format +# +# default: +# CONFIG_BOOTLOADER_LOG_COLORS is not set +# default: +CONFIG_BOOTLOADER_LOG_TIMESTAMP_SOURCE_CPU_TICKS=y +# end of Format + +# +# Settings +# +# default: +CONFIG_BOOTLOADER_LOG_MODE_TEXT_EN=y +# default: +CONFIG_BOOTLOADER_LOG_MODE_TEXT=y +# end of Settings +# end of Log + +# default: +CONFIG_BOOTLOADER_CPU_CLK_FREQ_MHZ=80 + +# +# Serial Flash Configurations +# +# default: +# CONFIG_BOOTLOADER_FLASH_DC_AWARE is not set +# default: +CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y +# end of Serial Flash Configurations + +# default: +# CONFIG_BOOTLOADER_FACTORY_RESET is not set +# default: +# CONFIG_BOOTLOADER_APP_TEST is not set +# default: +CONFIG_BOOTLOADER_REGION_PROTECTION_ENABLE=y +# default: +CONFIG_BOOTLOADER_WDT_ENABLE=y +# default: +# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set +# default: +CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y +CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON=y +# default: +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set +# default: +CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0x10 +# default: +# CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set +# default: +CONFIG_BOOTLOADER_RESERVE_RTC_MEM=y +# end of Bootloader config + +# +# Security features +# +# default: +CONFIG_SECURE_BOOT_V2_RSA_SUPPORTED=y +# default: +CONFIG_SECURE_BOOT_V2_ECC_SUPPORTED=y +# default: +CONFIG_SECURE_BOOT_V2_PREFERRED=y +# default: +# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set +# default: +CONFIG_SECURE_BOOT_IMAGE_DIGEST_LEN=32 +# default: +CONFIG_SECURE_BOOT_ROM_FAST_WAKE_RESERVE_SIZE=0 +# default: +# CONFIG_SECURE_BOOT is not set +# default: +# CONFIG_SECURE_FLASH_ENC_ENABLED is not set +# default: +CONFIG_SECURE_ROM_DL_MODE_ENABLED=y +# end of Security features + +# +# Application manager +# +# default: +CONFIG_APP_COMPILE_TIME_DATE=y +# default: +# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set +# default: +# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set +# default: +# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set +# default: +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=9 +# end of Application manager + +# default: +CONFIG_ESP_ROM_HAS_CRC_LE=y +# default: +CONFIG_ESP_ROM_HAS_CRC_BE=y +# default: +CONFIG_ESP_ROM_HAS_JPEG_DECODE=y +# default: +CONFIG_ESP_ROM_UART_CLK_IS_XTAL=y +# default: +CONFIG_ESP_ROM_USB_SERIAL_DEVICE_NUM=3 +# default: +CONFIG_ESP_ROM_HAS_RETARGETABLE_LOCKING=y +# default: +CONFIG_ESP_ROM_GET_CLK_FREQ=y +# default: +CONFIG_ESP_ROM_HAS_RVFPLIB=y +# default: +CONFIG_ESP_ROM_HAS_HAL_WDT=y +# default: +CONFIG_ESP_ROM_HAS_HAL_SYSTIMER=y +# default: +CONFIG_ESP_ROM_HAS_HEAP_TLSF=y +# default: +CONFIG_ESP_ROM_TLSF_CHECK_PATCH=y +# default: +CONFIG_ESP_ROM_MULTI_HEAP_WALK_PATCH=y +# default: +CONFIG_ESP_ROM_HAS_LAYOUT_TABLE=y +# default: +CONFIG_ESP_ROM_HAS_SPI_FLASH=y +# default: +CONFIG_ESP_ROM_HAS_SPI_FLASH_MMAP=y +# default: +CONFIG_ESP_ROM_HAS_NEWLIB=y +# default: +CONFIG_ESP_ROM_HAS_NEWLIB_NORMAL_FORMAT=y +# default: +CONFIG_ESP_ROM_REV0_HAS_NO_ECDSA_INTERFACE=y +# default: +CONFIG_ESP_ROM_WDT_INIT_PATCH=y +# default: +CONFIG_ESP_ROM_NEEDS_SET_CACHE_MMU_SIZE=y +# default: +CONFIG_ESP_ROM_RAM_APP_NEEDS_MMU_INIT=y +# default: +CONFIG_ESP_ROM_HAS_SW_FLOAT=y +# default: +CONFIG_ESP_ROM_USB_OTG_NUM=-1 +# default: +CONFIG_ESP_ROM_HAS_VERSION=y +# default: +CONFIG_ESP_ROM_SUPPORT_DEEP_SLEEP_WAKEUP_STUB=y +# default: +CONFIG_ESP_ROM_HAS_OUTPUT_PUTC_FUNC=y +# default: +CONFIG_ESP_ROM_NO_USB_SERIAL_OUTPUT_API=y +# default: +CONFIG_ESP_ROM_HAS_SUBOPTIMAL_NEWLIB_ON_MISALIGNED_MEMORY=y +# default: +CONFIG_ESP_ROM_SUPPORT_SECURE_BOOT_FAST_WAKEUP=y +# default: +CONFIG_ESP_ROM_BOOTLOADER_OFFSET_FLASH=0x0 + +# +# Boot ROM Behavior +# +# default: +CONFIG_BOOT_ROM_LOG_ALWAYS_ON=y +# default: +# CONFIG_BOOT_ROM_LOG_ALWAYS_OFF is not set +# default: +# CONFIG_BOOT_ROM_LOG_ON_GPIO_HIGH is not set +# default: +# CONFIG_BOOT_ROM_LOG_ON_GPIO_LOW is not set +# end of Boot ROM Behavior + +# +# Serial flasher config +# +# default: +# CONFIG_ESPTOOLPY_NO_STUB is not set +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set +# CONFIG_ESPTOOLPY_FLASHMODE_DIO is not set +# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set +# default: +CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y +# default: +CONFIG_ESPTOOLPY_FLASHMODE="dio" +# default: +CONFIG_ESPTOOLPY_FLASHMODE_VAL=1 +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +# CONFIG_ESPTOOLPY_FLASHFREQ_40M is not set +# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set +# default: +CONFIG_ESPTOOLPY_FLASHFREQ="80m" +# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set +# default: +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +# default: +# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set +# default: +CONFIG_ESPTOOLPY_BEFORE_RESET=y +# default: +# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set +# default: +CONFIG_ESPTOOLPY_BEFORE="default-reset" +# default: +CONFIG_ESPTOOLPY_AFTER_RESET=y +# default: +# CONFIG_ESPTOOLPY_AFTER_NORESET is not set +# default: +CONFIG_ESPTOOLPY_AFTER="hard-reset" +# default: +CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 +# end of Serial flasher config + +# +# Partition Table +# +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="system/partitions.csv" +# default: +CONFIG_PARTITION_TABLE_FILENAME="system/partitions.csv" +# default: +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +# default: +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# Compiler options +# +# CONFIG_COMPILER_OPTIMIZATION_DEBUG is not set +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +# CONFIG_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_COMPILER_OPTIMIZATION_NONE is not set +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE is not set +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y +# default: +# CONFIG_COMPILER_ASSERT_NDEBUG_EVALUATE is not set +# default: +# CONFIG_COMPILER_FLOAT_LIB_FROM_GCCLIB is not set +# default: +CONFIG_COMPILER_FLOAT_LIB_FROM_RVFPLIB=y +# default: +CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=0 +# default: +# CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set +# default: +CONFIG_COMPILER_HIDE_PATHS_MACROS=y +# default: +# CONFIG_COMPILER_CXX_EXCEPTIONS is not set +# default: +# CONFIG_COMPILER_CXX_RTTI is not set +# default: +CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y +# default: +# CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set +# default: +# CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set +# default: +# CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set +# default: +# CONFIG_COMPILER_NO_MERGE_CONSTANTS is not set +# default: +# CONFIG_COMPILER_WARN_WRITE_STRINGS is not set +# default: +# CONFIG_COMPILER_SAVE_RESTORE_LIBCALLS is not set +# default: +# CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS is not set +# default: +# CONFIG_COMPILER_DISABLE_GCC12_WARNINGS is not set +# default: +# CONFIG_COMPILER_DISABLE_GCC13_WARNINGS is not set +# default: +# CONFIG_COMPILER_DISABLE_GCC14_WARNINGS is not set +# default: +# CONFIG_COMPILER_DISABLE_GCC15_WARNINGS is not set +# default: +# CONFIG_COMPILER_DUMP_RTL_FILES is not set +# default: +CONFIG_COMPILER_RT_LIB_GCCLIB=y +# default: +CONFIG_COMPILER_RT_LIB_NAME="gcc" +# default: +CONFIG_COMPILER_ORPHAN_SECTIONS_ERROR=y +# default: +# CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING is not set +# default: +# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set +# default: +# CONFIG_COMPILER_STATIC_ANALYZER is not set +# default: +CONFIG_COMPILER_CXX_GLIBCXX_CONSTEXPR_NO_CHANGE=y +# default: +# CONFIG_COMPILER_CXX_GLIBCXX_CONSTEXPR_COLD_CONSTEXPR is not set +# default: +# CONFIG_COMPILER_CXX_GLIBCXX_CONSTEXPR_COLD is not set +# end of Compiler options + +# +# Component config +# + +# +# eFuse Bit Manager +# +# default: +# CONFIG_EFUSE_CUSTOM_TABLE is not set +# default: +# CONFIG_EFUSE_VIRTUAL is not set +# default: +CONFIG_EFUSE_MAX_BLK_LEN=256 +# end of eFuse Bit Manager + +# +# Wireless Coexistence +# +# default: +CONFIG_ESP_COEX_ENABLED=y +# default: +# CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE is not set +# end of Wireless Coexistence + +# +# Common ESP-related +# +# default: +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +# end of Common ESP-related + +# +# GDMA Configurations +# +# default: +# CONFIG_GDMA_CTRL_FUNC_IN_IRAM is not set +# default: +CONFIG_GDMA_ISR_HANDLER_IN_IRAM=y +# default: +CONFIG_GDMA_OBJ_DRAM_SAFE=y +# default: +# CONFIG_GDMA_ENABLE_DEBUG_LOG is not set +# end of GDMA Configurations + +# +# ESP-Driver:GPIO Configurations +# +# default: +# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:GPIO Configurations + +# +# ESP-Driver:UART Configurations +# +# default: +# CONFIG_UART_ISR_IN_IRAM is not set +# end of ESP-Driver:UART Configurations + +# +# ESP-Driver:UHCI Configurations +# +# default: +# CONFIG_UHCI_ISR_HANDLER_IN_IRAM is not set +# default: +# CONFIG_UHCI_ISR_CACHE_SAFE is not set +# default: +# CONFIG_UHCI_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:UHCI Configurations + +# +# ESP-Driver:USB Serial/JTAG Configuration +# +# default: +CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=y +# default: +# CONFIG_USJ_NO_AUTO_LS_ON_CONNECTION is not set +# end of ESP-Driver:USB Serial/JTAG Configuration + +# +# Event Loop Library +# +# default: +# CONFIG_ESP_EVENT_LOOP_PROFILING is not set +# default: +CONFIG_ESP_EVENT_POST_FROM_ISR=y +# default: +CONFIG_ESP_EVENT_POST_FROM_ISR_SIZE=4 +# default: +CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y +# end of Event Loop Library + +# +# Hardware Settings +# +# default: +CONFIG_ESP_HW_SUPPORT_FUNC_IN_IRAM=y + +# +# Chip revision +# +CONFIG_ESP32C6_REV_MIN_0=y +# CONFIG_ESP32C6_REV_MIN_1 is not set +# CONFIG_ESP32C6_REV_MIN_2 is not set +# default: +CONFIG_ESP32C6_REV_MIN_FULL=0 +# default: +CONFIG_ESP_REV_MIN_FULL=0 + +# +# Maximum Supported ESP32-C6 Revision (Rev v0.99) +# +# default: +CONFIG_ESP32C6_REV_MAX_FULL=99 +# default: +CONFIG_ESP_REV_MAX_FULL=99 +# default: +CONFIG_ESP_EFUSE_BLOCK_REV_MIN_FULL=0 +# default: +CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL=99 + +# +# Maximum Supported ESP32-C6 eFuse Block Revision (eFuse Block Rev v0.99) +# +# end of Chip revision + +# +# MAC Config +# +# default: +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y +# default: +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y +# default: +CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y +# default: +CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y +# default: +CONFIG_ESP_MAC_ADDR_UNIVERSE_IEEE802154=y +# default: +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES_FOUR=y +# default: +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES=4 +# default: +# CONFIG_ESP32C6_UNIVERSAL_MAC_ADDRESSES_TWO is not set +# default: +CONFIG_ESP32C6_UNIVERSAL_MAC_ADDRESSES_FOUR=y +# default: +CONFIG_ESP32C6_UNIVERSAL_MAC_ADDRESSES=4 +# default: +# CONFIG_ESP_MAC_USE_CUSTOM_MAC_AS_BASE_MAC is not set +# end of MAC Config + +# +# Sleep Config +# +CONFIG_ESP_SLEEP_POWER_DOWN_FLASH=y +# default: +CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y +# default: +# CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU is not set +# default: +CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND=y +# default: +CONFIG_ESP_SLEEP_WAIT_FLASH_READY_EXTRA_DELAY=0 +# default: +# CONFIG_ESP_SLEEP_CACHE_SAFE_ASSERTION is not set +# default: +# CONFIG_ESP_SLEEP_DEBUG is not set +# default: +CONFIG_ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS=y +# default: +# CONFIG_ESP_SLEEP_EVENT_CALLBACKS is not set +# end of Sleep Config + +# +# RTC Clock Config +# +CONFIG_RTC_CLK_SRC_INT_RC=y +# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_RTC_CLK_SRC_EXT_OSC is not set +# default: +CONFIG_RTC_CLK_CAL_CYCLES=1024 +# default: +CONFIG_RTC_CLK_FUNC_IN_IRAM=y +# default: +CONFIG_RTC_TIME_FUNC_IN_IRAM=y +# end of RTC Clock Config + +# +# Peripheral Control +# +# default: +CONFIG_ESP_PERIPH_CTRL_FUNC_IN_IRAM=y +# default: +CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM=y +# default: +# CONFIG_ESP_MODEM_CLOCK_ENABLE_CHECKING is not set +# end of Peripheral Control + +# +# Event Task Matrix (ETM) Configurations +# +# default: +# CONFIG_ETM_ENABLE_DEBUG_LOG is not set +# end of Event Task Matrix (ETM) Configurations + +# +# Main XTAL Config +# +# default: +CONFIG_XTAL_FREQ_40=y +# default: +CONFIG_XTAL_FREQ=40 +# end of Main XTAL Config + +# +# Power Supplier +# + +# +# Brownout Detector +# +# CONFIG_ESP_BROWNOUT_DET is not set +# default: +CONFIG_ESP_BROWNOUT_USE_INTR=y +# end of Brownout Detector +# end of Power Supplier + +# default: +# CONFIG_ESP_ENABLE_PVT is not set +# default: +CONFIG_ESP_INTR_IN_IRAM=y +# end of Hardware Settings + +# +# LibC +# +# default: +# CONFIG_LIBC_NEWLIB is not set +# default: +CONFIG_LIBC_PICOLIBC=y +# default: +CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY=y +# default: +CONFIG_LIBC_MISC_IN_IRAM=y +# default: +CONFIG_LIBC_LOCKS_PLACE_IN_IRAM=y +# default: +CONFIG_LIBC_STDOUT_LINE_ENDING_CRLF=y +# default: +# CONFIG_LIBC_STDOUT_LINE_ENDING_LF is not set +# default: +# CONFIG_LIBC_STDOUT_LINE_ENDING_CR is not set +# default: +# CONFIG_LIBC_STDIN_LINE_ENDING_CRLF is not set +# default: +# CONFIG_LIBC_STDIN_LINE_ENDING_LF is not set +# default: +CONFIG_LIBC_STDIN_LINE_ENDING_CR=y +# default: +CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y +# default: +# CONFIG_LIBC_TIME_SYSCALL_USE_RTC is not set +# default: +# CONFIG_LIBC_TIME_SYSCALL_USE_HRT is not set +# default: +# CONFIG_LIBC_TIME_SYSCALL_USE_NONE is not set +# default: +CONFIG_LIBC_OPTIMIZED_MISALIGNED_ACCESS=y +# default: +CONFIG_LIBC_ASSERT_BUFFER_SIZE=200 +# end of LibC + +# +# ESP-MM: Memory Management Configurations +# +# end of ESP-MM: Memory Management Configurations + +# +# ESP NETIF Adapter +# +# default: +CONFIG_ESP_NETIF_LOST_IP_TIMER_ENABLE=y +# default: +CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 +# default: +# CONFIG_ESP_NETIF_PROVIDE_CUSTOM_IMPLEMENTATION is not set +# default: +CONFIG_ESP_NETIF_TCPIP_LWIP=y +# default: +# CONFIG_ESP_NETIF_LOOPBACK is not set +# default: +CONFIG_ESP_NETIF_USES_TCPIP_WITH_BSD_API=y +# default: +CONFIG_ESP_NETIF_REPORT_DATA_TRAFFIC=y +# default: +CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS=y +# default: +# CONFIG_ESP_NETIF_L2_TAP is not set +# default: +# CONFIG_ESP_NETIF_BRIDGE_EN is not set +# default: +# CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF is not set +# end of ESP NETIF Adapter + +# +# Partition API Configuration +# +# end of Partition API Configuration + +# +# PHY +# +# default: +CONFIG_ESP_PHY_ENABLED=y +# default: +CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y +# default: +# CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set +# default: +CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20 +# default: +CONFIG_ESP_PHY_MAX_TX_POWER=20 +# default: +# CONFIG_ESP_PHY_MAC_BB_PD is not set +# default: +# CONFIG_ESP_PHY_ENABLE_CERT_TEST is not set +CONFIG_ESP_PHY_RF_CAL_PARTIAL=y +# CONFIG_ESP_PHY_RF_CAL_NONE is not set +# CONFIG_ESP_PHY_RF_CAL_FULL is not set +# default: +CONFIG_ESP_PHY_CALIBRATION_MODE=0 +# default: +CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000 +# default: +# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set +# default: +# CONFIG_ESP_PHY_RECORD_USED_TIME is not set +# default: +CONFIG_ESP_PHY_IRAM_OPT=y +# default: +# CONFIG_ESP_PHY_DEBUG is not set +# default: +CONFIG_ESP_PHY_ENABLE_VERSION_PRINT=y +# end of PHY + +# +# Power Management +# +# default: +CONFIG_PM_SLEEP_FUNC_IN_IRAM=y +CONFIG_PM_ENABLE=y +# default: +# CONFIG_PM_DFS_INIT_AUTO is not set +# default: +# CONFIG_PM_PROFILING is not set +# default: +# CONFIG_PM_TRACE is not set +# default: +CONFIG_PM_SLP_IRAM_OPT=y +# default: +CONFIG_PM_RTOS_IDLE_OPT=y +# default: +CONFIG_PM_SLP_DISABLE_GPIO=y +# default: +CONFIG_PM_SLP_DEFAULT_PARAMS_OPT=y +# default: +CONFIG_PM_LIGHTSLEEP_RTC_OSC_CAL_INTERVAL=1 +# default: +CONFIG_PM_POWER_DOWN_CPU_IN_LIGHT_SLEEP=y +# default: +CONFIG_PM_CPU_RETENTION_DYNAMIC=y +# default: +# CONFIG_PM_CPU_RETENTION_STATIC is not set +# default: +# CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP is not set +# default: +CONFIG_PM_ESP_SLEEP_POWER_DOWN_CPU=y +# default: +# CONFIG_PM_LIGHT_SLEEP_CALLBACKS is not set +# default: +# CONFIG_PM_LIGHTSLEEP_TICK_OVERFLOW_PROTECTION is not set +# end of Power Management + +# +# ESP PSRAM +# + +# +# ESP Ringbuf +# +# default: +# CONFIG_RINGBUF_IN_IRAM is not set +# default: +# CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH is not set +# end of ESP Ringbuf + +# +# ESP-ROM +# +# default: +CONFIG_ESP_ROM_PRINT_IN_IRAM=y +# end of ESP-ROM + +# +# ESP Security Specific +# + +# +# Crypto DPA Protection +# +# default: +CONFIG_ESP_CRYPTO_DPA_PROTECTION_AT_STARTUP=y +# default: +CONFIG_ESP_CRYPTO_DPA_PROTECTION_LEVEL_LOW=y +# default: +# CONFIG_ESP_CRYPTO_DPA_PROTECTION_LEVEL_MEDIUM is not set +# default: +# CONFIG_ESP_CRYPTO_DPA_PROTECTION_LEVEL_HIGH is not set +# default: +CONFIG_ESP_CRYPTO_DPA_PROTECTION_LEVEL=1 +# end of Crypto DPA Protection +# end of ESP Security Specific + +# +# ESP-STDIO +# +# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_NONE is not set +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y +# default: +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y +# default: +CONFIG_ESP_CONSOLE_UART_NUM=-1 +# default: +CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=3 +# default: +CONFIG_ESP_STDIO_MAX_VFS_ENTRIES=2 +# end of ESP-STDIO + +# +# ESP System Settings +# +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_120 is not set +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y +# default: +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 +# default: +CONFIG_ESP_SYSTEM_IN_IRAM=y +# default: +# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set +# default: +CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y +# default: +# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set +# default: +CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0 +# default: +CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE=y +# default: +CONFIG_ESP_SYSTEM_RTC_FAST_MEM_AS_HEAP_DEPCHECK=y +# default: +CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y +# default: +CONFIG_ESP_SYSTEM_NO_BACKTRACE=y +# default: +# CONFIG_ESP_SYSTEM_USE_EH_FRAME is not set +# default: +# CONFIG_ESP_SYSTEM_USE_FRAME_POINTER is not set +# default: +CONFIG_ESP_SYSTEM_MEMPROT=y +# default: +CONFIG_ESP_SYSTEM_MEMPROT_PMP=y +# default: +# CONFIG_ESP_SYSTEM_MEMPROT_PMP_LP_CORE_RESERVE_MEM_EXEC is not set +# default: +CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 +# default: +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=24576 +# default: +CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y +# default: +# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set +# default: +CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 +# default: +CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 +# default: +CONFIG_ESP_INT_WDT=y +# default: +CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 +# default: +CONFIG_ESP_TASK_WDT_EN=y +# default: +CONFIG_ESP_TASK_WDT_INIT=y +# default: +# CONFIG_ESP_TASK_WDT_PANIC is not set +# default: +CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 +# default: +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +# default: +# CONFIG_ESP_PANIC_HANDLER_IRAM is not set +# default: +# CONFIG_ESP_DEBUG_STUBS_ENABLE is not set +# default: +# CONFIG_ESP_DEBUG_INCLUDE_OCD_STUB_BINS is not set +# default: +CONFIG_ESP_DEBUG_OCDAWARE=y +# default: +CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_4=y +# default: +CONFIG_ESP_SYSTEM_HW_STACK_GUARD=y +# default: +CONFIG_ESP_SYSTEM_BBPLL_RECALIB=y +# default: +CONFIG_ESP_SYSTEM_HW_PC_RECORD=y +# end of ESP System Settings + +# +# IPC (Inter-Processor Call) +# +# default: +CONFIG_ESP_IPC_TASK_STACK_SIZE=1024 +# end of IPC (Inter-Processor Call) + +# +# ESP Timer (High Resolution Timer) +# +# default: +CONFIG_ESP_TIMER_IN_IRAM=y +# default: +# CONFIG_ESP_TIMER_PROFILING is not set +# default: +CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y +# default: +CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y +# default: +CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584 +# default: +CONFIG_ESP_TIMER_INTERRUPT_LEVEL=1 +# default: +# CONFIG_ESP_TIMER_SHOW_EXPERIMENTAL is not set +# default: +CONFIG_ESP_TIMER_TASK_AFFINITY=0x0 +# default: +CONFIG_ESP_TIMER_TASK_AFFINITY_CPU0=y +# default: +CONFIG_ESP_TIMER_ISR_AFFINITY_CPU0=y +# default: +# CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is not set +# default: +CONFIG_ESP_TIMER_IMPL_SYSTIMER=y +# end of ESP Timer (High Resolution Timer) + +# +# Wi-Fi +# +# default: +CONFIG_ESP_WIFI_ENABLED=y +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=40 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=60 +# default: +# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set +# default: +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y +# default: +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=40 +# default: +CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y +# default: +# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set +# default: +CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0 +# default: +CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5 +# default: +# CONFIG_ESP_WIFI_CSI_ENABLED is not set +# CONFIG_ESP_WIFI_AMPDU_TX_ENABLED is not set +# CONFIG_ESP_WIFI_AMPDU_RX_ENABLED is not set +# CONFIG_ESP_WIFI_NVS_ENABLED is not set +# default: +CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752 +# default: +CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32 +# default: +CONFIG_ESP_WIFI_IRAM_OPT=y +# default: +CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y +# default: +CONFIG_ESP_WIFI_RX_IRAM_OPT=y +# default: +CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y +# default: +CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y +# default: +CONFIG_ESP_WIFI_ENABLE_SAE_PK=y +# default: +CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y +# default: +CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y +# default: +CONFIG_ESP_WIFI_WPA3_COMPATIBLE_SUPPORT=y +# default: +CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_SOFTAP=y +# default: +# CONFIG_ESP_WIFI_PASN_SUPPORT is not set +# default: +CONFIG_ESP_WIFI_SLP_IRAM_OPT=y +# default: +CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50 +# default: +CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT=y +# default: +CONFIG_ESP_WIFI_SLP_DEFAULT_MAX_ACTIVE_TIME=10 +# default: +CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 +# default: +# CONFIG_ESP_WIFI_FTM_ENABLE is not set +# default: +CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y +# default: +# CONFIG_ESP_WIFI_GCMP_SUPPORT is not set +# default: +CONFIG_ESP_WIFI_GMAC_SUPPORT=y +# default: +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y +# default: +# CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set +# default: +CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7 +# default: +CONFIG_ESP_WIFI_MBEDTLS_CRYPTO=y +# default: +CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y +# default: +# CONFIG_ESP_WIFI_WAPI_PSK is not set +# default: +# CONFIG_ESP_WIFI_SUITE_B_192 is not set +# default: +# CONFIG_ESP_WIFI_11KV_SUPPORT is not set +# default: +# CONFIG_ESP_WIFI_MBO_SUPPORT is not set +# default: +# CONFIG_ESP_WIFI_ENABLE_ROAMING_APP is not set +# default: +# CONFIG_ESP_WIFI_DPP_SUPPORT is not set +# default: +# CONFIG_ESP_WIFI_11R_SUPPORT is not set +# default: +# CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR is not set +# CONFIG_ESP_WIFI_ENABLE_WIFI_TX_STATS is not set +# CONFIG_ESP_WIFI_ENABLE_WIFI_RX_STATS is not set +# default: +CONFIG_ESP_WIFI_TX_HETB_QUEUE_NUM=3 + +# +# WPS Configuration Options +# +# default: +# CONFIG_ESP_WIFI_WPS_STRICT is not set +# default: +# CONFIG_ESP_WIFI_WPS_PASSPHRASE is not set +# default: +# CONFIG_ESP_WIFI_WPS_RECONNECT_ON_FAIL is not set +# end of WPS Configuration Options + +# default: +# CONFIG_ESP_WIFI_DEBUG_PRINT is not set +# default: +CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y +# default: +# CONFIG_ESP_WIFI_ENT_FREE_DYNAMIC_BUFFER is not set +# default: +# CONFIG_ESP_WIFI_PASSIVE_HIDDEN_AP_SUPPORT is not set +# default: +# CONFIG_ESP_WIFI_PRIVACY_ENHANCEMENTS_ENABLED is not set +# end of Wi-Fi + +# +# Wi-Fi Remote +# +# default: +CONFIG_ESP_WIFI_REMOTE_IN_IDF=y +# default: +CONFIG_ESP_WIFI_REMOTE_ENABLED=y +# default: +CONFIG_SLAVE_IDF_TARGET_ESP32=y +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32S2 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32C3 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32S3 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32C2 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32C6 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32C5 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32C61 is not set +# default: +# CONFIG_SLAVE_IDF_TARGET_ESP32S31 is not set +# default: +CONFIG_SLAVE_SOC_WIFI_SUPPORTED=y +# default: +CONFIG_SLAVE_SOC_WIFI_WAPI_SUPPORT=y +# default: +CONFIG_SLAVE_SOC_WIFI_CSI_SUPPORT=y +# default: +CONFIG_SLAVE_SOC_WIFI_MESH_SUPPORT=y +# default: +CONFIG_SLAVE_SOC_WIFI_SUPPORT_VARIABLE_BEACON_WINDOW=y +# default: +CONFIG_SLAVE_SOC_WIFI_NAN_SUPPORT=y +# default: +CONFIG_SLAVE_IDF_TARGET_ARCH_XTENSA=y + +# +# Wi-Fi configuration +# +# default: +CONFIG_WIFI_RMT_STATIC_RX_BUFFER_NUM=10 +# default: +CONFIG_WIFI_RMT_DYNAMIC_RX_BUFFER_NUM=32 +# default: +# CONFIG_WIFI_RMT_STATIC_TX_BUFFER is not set +# default: +CONFIG_WIFI_RMT_DYNAMIC_TX_BUFFER=y +# default: +CONFIG_WIFI_RMT_TX_BUFFER_TYPE=1 +# default: +CONFIG_WIFI_RMT_DYNAMIC_TX_BUFFER_NUM=32 +# default: +CONFIG_WIFI_RMT_STATIC_RX_MGMT_BUFFER=y +# default: +# CONFIG_WIFI_RMT_DYNAMIC_RX_MGMT_BUFFER is not set +# default: +CONFIG_WIFI_RMT_DYNAMIC_RX_MGMT_BUF=0 +# default: +CONFIG_WIFI_RMT_RX_MGMT_BUF_NUM_DEF=5 +# default: +# CONFIG_WIFI_RMT_CSI_ENABLED is not set +# default: +CONFIG_WIFI_RMT_AMPDU_TX_ENABLED=y +# default: +CONFIG_WIFI_RMT_TX_BA_WIN=6 +# default: +CONFIG_WIFI_RMT_AMPDU_RX_ENABLED=y +# default: +CONFIG_WIFI_RMT_RX_BA_WIN=6 +# default: +CONFIG_WIFI_RMT_NVS_ENABLED=y +# default: +CONFIG_WIFI_RMT_TASK_PINNED_TO_CORE_0=y +# default: +# CONFIG_WIFI_RMT_TASK_PINNED_TO_CORE_1 is not set +# default: +CONFIG_WIFI_RMT_SOFTAP_BEACON_MAX_LEN=752 +# default: +CONFIG_WIFI_RMT_MGMT_SBUF_NUM=32 +# default: +CONFIG_WIFI_RMT_IRAM_OPT=y +# default: +# CONFIG_WIFI_RMT_EXTRA_IRAM_OPT is not set +# default: +CONFIG_WIFI_RMT_RX_IRAM_OPT=y +# default: +CONFIG_WIFI_RMT_ENABLE_WPA3_SAE=y +# default: +CONFIG_WIFI_RMT_ENABLE_SAE_H2E=y +# default: +CONFIG_WIFI_RMT_ENABLE_SAE_PK=y +# default: +CONFIG_WIFI_RMT_SOFTAP_SAE_SUPPORT=y +# default: +CONFIG_WIFI_RMT_ENABLE_WPA3_OWE_STA=y +# default: +CONFIG_WIFI_RMT_WPA3_COMPATIBLE_SUPPORT=y +# default: +CONFIG_WIFI_RMT_ENABLE_WPA3_OWE_SOFTAP=y +# default: +# CONFIG_WIFI_RMT_PASN_SUPPORT is not set +# default: +# CONFIG_WIFI_RMT_SLP_IRAM_OPT is not set +# default: +CONFIG_WIFI_RMT_SLP_DEFAULT_MIN_ACTIVE_TIME=50 +# default: +# CONFIG_WIFI_RMT_BSS_MAX_IDLE_SUPPORT is not set +# default: +CONFIG_WIFI_RMT_SLP_DEFAULT_MAX_ACTIVE_TIME=10 +# default: +CONFIG_WIFI_RMT_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 +# default: +CONFIG_WIFI_RMT_STA_DISCONNECTED_PM_ENABLE=y +# default: +CONFIG_WIFI_RMT_GMAC_SUPPORT=y +# default: +CONFIG_WIFI_RMT_SOFTAP_SUPPORT=y +# default: +# CONFIG_WIFI_RMT_SLP_BEACON_LOST_OPT is not set +# default: +CONFIG_WIFI_RMT_ESPNOW_MAX_ENCRYPT_NUM=7 +# default: +# CONFIG_WIFI_RMT_NAN_SYNC_ENABLE is not set +# default: +CONFIG_WIFI_RMT_MBEDTLS_CRYPTO=y +# default: +CONFIG_WIFI_RMT_MBEDTLS_TLS_CLIENT=y +# default: +# CONFIG_WIFI_RMT_WAPI_PSK is not set +# default: +# CONFIG_WIFI_RMT_11KV_SUPPORT is not set +# default: +# CONFIG_WIFI_RMT_MBO_SUPPORT is not set +# default: +# CONFIG_WIFI_RMT_ENABLE_ROAMING_APP is not set +# default: +# CONFIG_WIFI_RMT_DPP_SUPPORT is not set +# default: +# CONFIG_WIFI_RMT_11R_SUPPORT is not set +# default: +# CONFIG_WIFI_RMT_WPS_SOFTAP_REGISTRAR is not set + +# +# WPS Configuration Options +# +# default: +# CONFIG_WIFI_RMT_WPS_STRICT is not set +# default: +# CONFIG_WIFI_RMT_WPS_PASSPHRASE is not set +# default: +# CONFIG_WIFI_RMT_WPS_RECONNECT_ON_FAIL is not set +# end of WPS Configuration Options + +# default: +# CONFIG_WIFI_RMT_DEBUG_PRINT is not set +# default: +CONFIG_WIFI_RMT_ENTERPRISE_SUPPORT=y +# default: +# CONFIG_WIFI_RMT_ENT_FREE_DYNAMIC_BUFFER is not set +# default: +# CONFIG_WIFI_RMT_PASSIVE_HIDDEN_AP_SUPPORT is not set +# default: +# CONFIG_WIFI_RMT_PRIVACY_ENHANCEMENTS_ENABLED is not set +# end of Wi-Fi configuration + +# default: +CONFIG_ESP_WIFI_REMOTE_LIBRARY_HOSTED=y +# default: +# CONFIG_ESP_WIFI_REMOTE_LIBRARY_EPPP is not set +# default: +# CONFIG_ESP_WIFI_REMOTE_LIBRARY_CUSTOM is not set +# default: +CONFIG_ESP_WIFI_REMOTE_EAP_ENABLED=y +# end of Wi-Fi Remote + +# +# FreeRTOS +# + +# +# Kernel +# +# default: +CONFIG_FREERTOS_UNICORE=y +# default: +CONFIG_FREERTOS_HZ=100 +# default: +CONFIG_FREERTOS_OPTIMIZED_SCHEDULER=y +# default: +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set +# default: +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set +# default: +CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y +# default: +CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 +# default: +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 +# default: +# CONFIG_FREERTOS_USE_IDLE_HOOK is not set +# default: +# CONFIG_FREERTOS_USE_TICK_HOOK is not set +# default: +CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 +# default: +# CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY is not set +# default: +CONFIG_FREERTOS_USE_TIMERS=y +# default: +CONFIG_FREERTOS_TIMER_SERVICE_TASK_NAME="Tmr Svc" +# default: +# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU0 is not set +# default: +CONFIG_FREERTOS_TIMER_TASK_NO_AFFINITY=y +# default: +CONFIG_FREERTOS_TIMER_SERVICE_TASK_CORE_AFFINITY=0x7FFFFFFF +# default: +CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 +# default: +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048 +# default: +CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10 +# default: +CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 +# default: +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +# default: +# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set +# default: +# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set +# default: +# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +# default: +CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3 +# default: +# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set +# end of Kernel + +# +# Port +# +# default: +# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set +# default: +CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS=y +# default: +# CONFIG_FREERTOS_TASK_PRE_DELETION_HOOK is not set +# default: +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y +# default: +CONFIG_FREERTOS_ISR_STACKSIZE=1536 +# default: +CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y +# default: +CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y +# default: +CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y +# default: +# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set +# default: +CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y +# default: +# CONFIG_FREERTOS_IN_IRAM is not set +# default: +# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set +# end of Port + +# +# Extra +# +# end of Extra + +# default: +CONFIG_FREERTOS_PORT=y +# default: +CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF +# default: +CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y +# default: +CONFIG_FREERTOS_DEBUG_OCDAWARE=y +# default: +CONFIG_FREERTOS_NUMBER_OF_CORES=1 +# end of FreeRTOS + +# +# Hardware Abstraction Layer (HAL) and Low Level (LL) +# +# default: +CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y +# default: +# CONFIG_HAL_ASSERTION_DISABLE is not set +# default: +CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=0 +# default: +CONFIG_HAL_WDT_USE_ROM_IMPL=y +# default: +CONFIG_HAL_SYSTIMER_USE_ROM_IMPL=y +# default: +CONFIG_HAL_GPIO_USE_ROM_IMPL=y +# end of Hardware Abstraction Layer (HAL) and Low Level (LL) + +# +# Heap memory debugging +# +# default: +CONFIG_HEAP_POISONING_DISABLED=y +# default: +# CONFIG_HEAP_POISONING_LIGHT is not set +# default: +# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set +# default: +CONFIG_HEAP_TRACING_OFF=y +# default: +# CONFIG_HEAP_TRACING_STANDALONE is not set +# default: +# CONFIG_HEAP_TRACING_TOHOST is not set +# default: +# CONFIG_HEAP_USE_HOOKS is not set +# default: +# CONFIG_HEAP_TASK_TRACKING is not set +# default: +# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set +# default: +CONFIG_HEAP_TLSF_USE_ROM_IMPL=y +# default: +# CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH is not set +# end of Heap memory debugging + +# +# Log +# +# default: +CONFIG_LOG_VERSION_1=y +# default: +# CONFIG_LOG_VERSION_2 is not set +# default: +CONFIG_LOG_VERSION=1 + +# +# Log Level +# +CONFIG_LOG_DEFAULT_LEVEL_NONE=y +# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set +# CONFIG_LOG_DEFAULT_LEVEL_INFO is not set +# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set +# default: +CONFIG_LOG_DEFAULT_LEVEL=0 +# CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT is not set +# CONFIG_LOG_MAXIMUM_LEVEL_ERROR is not set +# CONFIG_LOG_MAXIMUM_LEVEL_WARN is not set +CONFIG_LOG_MAXIMUM_LEVEL_INFO=y +# CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set +# CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set +# default: +CONFIG_LOG_MAXIMUM_LEVEL=3 + +# +# Level Settings +# +# default: +# CONFIG_LOG_MASTER_LEVEL is not set +# default: +CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=y +# default: +# CONFIG_LOG_TAG_LEVEL_IMPL_NONE is not set +# default: +# CONFIG_LOG_TAG_LEVEL_IMPL_LINKED_LIST is not set +# default: +CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_AND_LINKED_LIST=y +# default: +# CONFIG_LOG_TAG_LEVEL_CACHE_ARRAY is not set +# default: +CONFIG_LOG_TAG_LEVEL_CACHE_BINARY_MIN_HEAP=y +# default: +CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_SIZE=31 +# end of Level Settings +# end of Log Level + +# +# Format +# +# default: +# CONFIG_LOG_COLORS is not set +# default: +CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y +# default: +# CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set +# end of Format + +# +# Settings +# +# default: +CONFIG_LOG_MODE_TEXT_EN=y +# default: +CONFIG_LOG_MODE_TEXT=y +# end of Settings + +# default: +CONFIG_LOG_IN_IRAM=y +# end of Log + +# +# LWIP +# +# default: +CONFIG_LWIP_ENABLE=y +# default: +CONFIG_LWIP_LOCAL_HOSTNAME="espressif" +# default: +CONFIG_LWIP_TCPIP_TASK_PRIO=18 +CONFIG_LWIP_TCPIP_CORE_LOCKING=y +CONFIG_LWIP_TCPIP_CORE_LOCKING_INPUT=y +# default: +# CONFIG_LWIP_CHECK_THREAD_SAFETY is not set +# default: +CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y +# default: +# CONFIG_LWIP_L2_TO_L3_COPY is not set +# default: +# CONFIG_LWIP_IRAM_OPTIMIZATION is not set +CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION=y +# default: +CONFIG_LWIP_TIMERS_ONDEMAND=y +# default: +CONFIG_LWIP_MAX_SOCKETS=10 +# default: +# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set +# default: +# CONFIG_LWIP_SO_LINGER is not set +# default: +CONFIG_LWIP_SO_REUSE=y +# default: +CONFIG_LWIP_SO_REUSE_RXTOALL=y +# default: +# CONFIG_LWIP_SO_RCVBUF is not set +# default: +# CONFIG_LWIP_NETBUF_RECVINFO is not set +# default: +CONFIG_LWIP_IP_DEFAULT_TTL=64 +# default: +CONFIG_LWIP_IP4_FRAG=y +# default: +# CONFIG_LWIP_IP4_REASSEMBLY is not set +CONFIG_LWIP_IP_REASS_MAX_PBUFS=15 +# default: +# CONFIG_LWIP_IP_FORWARD is not set +# CONFIG_LWIP_STATS is not set +# CONFIG_LWIP_ESP_GRATUITOUS_ARP is not set +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=64 +# default: +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y +# default: +# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set +# default: +# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set +# default: +# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set +# default: +CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y +# default: +# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set +# default: +CONFIG_LWIP_DHCP_OPTIONS_LEN=69 +# default: +CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0 +# default: +CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1 + +# +# DHCP server +# +# default: +CONFIG_LWIP_DHCPS=y +# default: +CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME=y +# default: +CONFIG_LWIP_DHCPS_LEASE_UNIT=60 +# default: +CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 +# default: +CONFIG_LWIP_DHCPS_MAX_HOSTNAME_LEN=64 +# default: +CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y +# end of DHCP server + +# CONFIG_LWIP_AUTOIP is not set +# default: +CONFIG_LWIP_IPV4=y +# CONFIG_LWIP_IPV6 is not set +# default: +# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set +# default: +# CONFIG_LWIP_NETIF_LINK_CALLBACK is not set +# default: +CONFIG_LWIP_NETIF_LOOPBACK=y +# default: +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 + +# +# TCP +# +# default: +CONFIG_LWIP_MAX_ACTIVE_TCP=16 +# default: +CONFIG_LWIP_MAX_LISTENING_TCP=16 +# default: +CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y +# default: +CONFIG_LWIP_TCP_MAXRTX=12 +# default: +CONFIG_LWIP_TCP_SYNMAXRTX=12 +# default: +CONFIG_LWIP_TCP_MSS=1440 +# default: +CONFIG_LWIP_TCP_TMR_INTERVAL=250 +# default: +CONFIG_LWIP_TCP_MSL=60000 +# default: +CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=20000 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=51200 +CONFIG_LWIP_TCP_WND_DEFAULT=65535 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=64 +# default: +CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=6 +# default: +CONFIG_LWIP_TCP_QUEUE_OOSEQ=y +# default: +CONFIG_LWIP_TCP_OOSEQ_TIMEOUT=6 +# default: +CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 +# default: +# CONFIG_LWIP_TCP_SACK_OUT is not set +# default: +CONFIG_LWIP_TCP_OVERSIZE_MSS=y +# default: +# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set +# default: +# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +# default: +CONFIG_LWIP_TCP_RTO_TIME=1500 +# end of TCP + +# +# UDP +# +# default: +CONFIG_LWIP_MAX_UDP_PCBS=16 +CONFIG_LWIP_UDP_RECVMBOX_SIZE=64 +# end of UDP + +# +# Checksums +# +# default: +# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set +# default: +# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set +# default: +CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y +# end of Checksums + +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=8192 +# default: +CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# default: +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set +# default: +CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# default: +# CONFIG_LWIP_PPP_SUPPORT is not set +# default: +# CONFIG_LWIP_SLIP_SUPPORT is not set + +# +# ICMP +# +# default: +CONFIG_LWIP_ICMP=y +# CONFIG_LWIP_MULTICAST_PING is not set +# default: +# CONFIG_LWIP_BROADCAST_PING is not set +# end of ICMP + +# +# LWIP RAW API +# +# default: +CONFIG_LWIP_MAX_RAW_PCBS=16 +# end of LWIP RAW API + +# +# SNTP +# +# default: +CONFIG_LWIP_SNTP_MAX_SERVERS=1 +# default: +# CONFIG_LWIP_DHCP_GET_NTP_SRV is not set +# default: +CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 +# default: +CONFIG_LWIP_SNTP_STARTUP_DELAY=y +# default: +CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000 +# end of SNTP + +# +# DNS +# +# default: +CONFIG_LWIP_DNS_MAX_HOST_IP=1 +# default: +CONFIG_LWIP_DNS_MAX_SERVERS=3 +# default: +# CONFIG_LWIP_FALLBACK_DNS_SERVER_SUPPORT is not set +# default: +# CONFIG_LWIP_DNS_SETSERVER_WITH_NETIF is not set +# end of DNS + +# default: +CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7 + +# +# Hooks +# +# default: +# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set +# default: +CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y +# default: +# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set +# default: +CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_NONE=y +# default: +# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_DEFAULT is not set +# default: +# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_CUSTOM is not set +# default: +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y +# default: +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set +# default: +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set +# default: +CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_NONE=y +# default: +# CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_CUSTOM is not set +# end of Hooks + +# default: +# CONFIG_LWIP_DEBUG is not set +# end of LWIP + +# +# mbedTLS +# + +# +# Core Configuration +# +# default: +CONFIG_MBEDTLS_VER_4_X_SUPPORT=y +# default: +# CONFIG_MBEDTLS_COMPILER_OPTIMIZATION_NONE is not set +# default: +CONFIG_MBEDTLS_COMPILER_OPTIMIZATION_SIZE=y +# default: +# CONFIG_MBEDTLS_COMPILER_OPTIMIZATION_PERF is not set +# default: +CONFIG_MBEDTLS_FS_IO=y +# default: +# CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND is not set +# default: +CONFIG_MBEDTLS_THREADING_C=y +# default: +# CONFIG_MBEDTLS_THREADING_ALT is not set +# default: +CONFIG_MBEDTLS_THREADING_PTHREAD=y +# default: +CONFIG_MBEDTLS_ERROR_STRINGS=y +# default: +CONFIG_MBEDTLS_VERSION_C=y +# default: +CONFIG_MBEDTLS_HAVE_TIME=y +# default: +# CONFIG_MBEDTLS_PLATFORM_TIME_ALT is not set +# default: +# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set +# default: +CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y +# default: +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set +# default: +# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set +# default: +CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y +# default: +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 +# default: +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 +# default: +# CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set +# default: +# CONFIG_MBEDTLS_VERSION_FEATURES is not set +# default: +# CONFIG_MBEDTLS_DEBUG is not set +# default: +CONFIG_MBEDTLS_SELF_TEST=y +# end of Core Configuration + +# +# Certificates +# +# default: +CONFIG_MBEDTLS_X509_USE_C=y +# default: +CONFIG_MBEDTLS_PEM_PARSE_C=y +# default: +CONFIG_MBEDTLS_PEM_WRITE_C=y +# default: +CONFIG_MBEDTLS_PK_C=y +# default: +CONFIG_MBEDTLS_PK_PARSE_C=y +# default: +CONFIG_MBEDTLS_PK_WRITE_C=y +# default: +# CONFIG_MBEDTLS_X509_REMOVE_INFO is not set +# default: +CONFIG_MBEDTLS_X509_CRL_PARSE_C=y +# default: +CONFIG_MBEDTLS_X509_CRT_PARSE_C=y +# default: +CONFIG_MBEDTLS_X509_CSR_PARSE_C=y +# default: +# CONFIG_MBEDTLS_X509_CREATE_C is not set +# default: +CONFIG_MBEDTLS_X509_RSASSA_PSS_SUPPORT=y +# default: +# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set +# default: +CONFIG_MBEDTLS_ASN1_PARSE_C=y +# default: +CONFIG_MBEDTLS_ASN1_WRITE_C=y +# default: +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + +# +# Certificate Bundle Configuration +# +# default: +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +# default: +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set +# default: +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set +# default: +# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set +# default: +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEPRECATED_LIST is not set +# default: +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_MAX_CERTS=200 +# end of Certificate Bundle Configuration + +# default: +# CONFIG_MBEDTLS_ALLOW_WEAK_CERTIFICATE_VERIFICATION is not set +# default: +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY is not set +# end of Certificates + +# default: +CONFIG_MBEDTLS_TLS_ENABLED=y + +# +# TLS Protocol Configuration +# +# default: +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y +# default: +# CONFIG_MBEDTLS_SSL_PROTO_TLS1_3 is not set +# default: +# CONFIG_MBEDTLS_SSL_PROTO_GMTSSL1_1 is not set +# default: +CONFIG_MBEDTLS_TLS_SERVER=y +# default: +CONFIG_MBEDTLS_TLS_CLIENT=y +# default: +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +# default: +# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set +# default: +# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set +# default: +# CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE is not set +# default: +# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set +# default: +CONFIG_MBEDTLS_SSL_CACHE_C=y +# default: +CONFIG_MBEDTLS_SSL_ALL_ALERT_MESSAGES=y + +# +# TLS Key Exchange Configuration +# +# default: +# CONFIG_MBEDTLS_PSK_MODES is not set +# default: +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y +# default: +CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y +# default: +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y +# default: +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y +# end of TLS Key Exchange Configuration + +# default: +CONFIG_MBEDTLS_SSL_SERVER_NAME_INDICATION=y +# default: +CONFIG_MBEDTLS_SSL_ALPN=y +# default: +CONFIG_MBEDTLS_SSL_MAX_FRAGMENT_LENGTH=y +# default: +# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set +# default: +CONFIG_MBEDTLS_SSL_RENEGOTIATION=y +# default: +CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y +# default: +CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y +# default: +# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set +# end of TLS Protocol Configuration + +# default: +# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set + +# +# Symmetric Ciphers +# +# default: +CONFIG_MBEDTLS_AES_C=y +# default: +# CONFIG_MBEDTLS_CAMELLIA_C is not set +# default: +# CONFIG_MBEDTLS_ARIA_C is not set +# default: +# CONFIG_MBEDTLS_DES_C is not set +# default: +CONFIG_MBEDTLS_CCM_C=y +# default: +CONFIG_MBEDTLS_CIPHER_MODE_CBC=y +# default: +CONFIG_MBEDTLS_CIPHER_MODE_CFB=y +# default: +CONFIG_MBEDTLS_CIPHER_MODE_CTR=y +# default: +CONFIG_MBEDTLS_CIPHER_MODE_OFB=y +# default: +CONFIG_MBEDTLS_CIPHER_MODE_XTS=y +# default: +CONFIG_MBEDTLS_GCM_C=y +# default: +# CONFIG_MBEDTLS_NIST_KW_C is not set +# default: +CONFIG_MBEDTLS_AES_ROM_TABLES=y +# default: +# CONFIG_MBEDTLS_AES_FEWER_TABLES is not set +# default: +# CONFIG_MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH is not set +# default: +CONFIG_MBEDTLS_CMAC_C=y +# end of Symmetric Ciphers + +# +# Asymmetric Ciphers +# +# default: +CONFIG_MBEDTLS_RSA_C=y +# default: +CONFIG_MBEDTLS_ECP_C=y + +# +# Supported Curves +# +# default: +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y +# default: +CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y +# end of Supported Curves + +# +# Elliptic Curve Ciphers Configuration +# +# default: +CONFIG_MBEDTLS_ECP_NIST_OPTIM=y +# default: +# CONFIG_MBEDTLS_ECP_FIXED_POINT_OPTIM is not set +# default: +# CONFIG_MBEDTLS_DHM_C is not set +# default: +CONFIG_MBEDTLS_ECDH_C=y +# default: +# CONFIG_MBEDTLS_ECJPAKE_C is not set +# default: +CONFIG_MBEDTLS_ECDSA_C=y +# default: +CONFIG_MBEDTLS_PK_PARSE_EC_EXTENDED=y +# default: +CONFIG_MBEDTLS_PK_PARSE_EC_COMPRESSED=y +# default: +CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y +# default: +# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set +# end of Elliptic Curve Ciphers Configuration +# end of Asymmetric Ciphers + +# +# Hash functions +# +# default: +# CONFIG_MBEDTLS_RIPEMD160_C is not set +# default: +CONFIG_MBEDTLS_MD_C=y +# default: +CONFIG_MBEDTLS_MD5_C=y +# default: +CONFIG_MBEDTLS_SHA1_C=y +# default: +# CONFIG_MBEDTLS_SHA224_C is not set +# default: +CONFIG_MBEDTLS_SHA256_C=y +# default: +CONFIG_MBEDTLS_SHA384_C=y +# default: +CONFIG_MBEDTLS_SHA512_C=y +# default: +# CONFIG_MBEDTLS_SHA3_C is not set +# default: +CONFIG_MBEDTLS_ROM_MD5=y +# end of Hash functions + +# +# Hardware Acceleration +# +# default: +CONFIG_MBEDTLS_HARDWARE_ECC=y +# default: +CONFIG_MBEDTLS_ECC_OTHER_CURVES_SOFT_FALLBACK=y +# default: +CONFIG_MBEDTLS_HARDWARE_SHA=y +# default: +CONFIG_MBEDTLS_HARDWARE_MPI=y +# default: +CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI=y +# default: +CONFIG_MBEDTLS_MPI_USE_INTERRUPT=y +# default: +CONFIG_MBEDTLS_MPI_INTERRUPT_LEVEL=0 +# default: +CONFIG_MBEDTLS_HARDWARE_AES=y +# default: +# CONFIG_MBEDTLS_AES_SOFT_FALLBACK is not set +# default: +CONFIG_MBEDTLS_GCM_SUPPORT_NON_AES_CIPHER=y +# default: +CONFIG_MBEDTLS_AES_USE_INTERRUPT=y +# default: +CONFIG_MBEDTLS_AES_INTERRUPT_LEVEL=0 +# default: +CONFIG_MBEDTLS_AES_HW_SMALL_DATA_LEN_OPTIM=y +# default: +# CONFIG_MBEDTLS_SECURE_ELEMENT_DRIVER_ENABLED is not set +# default: +# CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL is not set +# end of Hardware Acceleration + +# +# Entropy and Random Number Generation +# +# default: +# CONFIG_MBEDTLS_ENTROPY_FORCE_SHA256 is not set +# default: +CONFIG_MBEDTLS_CTR_DRBG_C=y +# default: +CONFIG_MBEDTLS_HMAC_DRBG_C=y +# end of Entropy and Random Number Generation + +# +# Encoding/Decoding +# +# default: +CONFIG_MBEDTLS_BASE64_C=y +# default: +CONFIG_MBEDTLS_PKCS5_C=y +# default: +CONFIG_MBEDTLS_PKCS7_C=y +# default: +CONFIG_MBEDTLS_PKCS1_V15=y +# default: +CONFIG_MBEDTLS_PKCS1_V21=y +# end of Encoding/Decoding + +# +# Stream Cipher +# +# default: +# CONFIG_MBEDTLS_CHACHA20_C is not set +# end of Stream Cipher +# end of mbedTLS + +# +# NVS +# +# default: +# CONFIG_NVS_ENCRYPTION is not set +# default: +# CONFIG_NVS_ASSERT_ERROR_CHECK is not set +# default: +# CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set +# default: +# CONFIG_NVS_BDL_STACK is not set +# default: +# CONFIG_NVS_FLASH_VERIFY_ERASE is not set +# end of NVS + +# +# PThreads +# +# default: +CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 +# default: +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +# default: +CONFIG_PTHREAD_STACK_MIN=768 +# default: +CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1 +# default: +CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread" +# end of PThreads + +# +# MMU Config +# +# default: +CONFIG_MMU_PAGE_SIZE_64KB=y +# default: +CONFIG_MMU_PAGE_MODE="64KB" +# default: +CONFIG_MMU_PAGE_SIZE=0x10000 +# end of MMU Config + +# +# Main Flash configuration +# + +# +# SPI Flash behavior when brownout +# +# default: +CONFIG_SPI_FLASH_BROWNOUT_RESET_XMC=y +# default: +CONFIG_SPI_FLASH_BROWNOUT_RESET=y +# end of SPI Flash behavior when brownout + +# +# Optional and Experimental Features (READ DOCS FIRST) +# + +# +# Features here require specific hardware (READ DOCS FIRST!) +# +# default: +# CONFIG_SPI_FLASH_AUTO_SUSPEND is not set +# default: +CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US=50 +# default: +# CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND is not set +# default: +# CONFIG_SPI_FLASH_FORCE_ENABLE_C6_H2_SUSPEND is not set +# default: +CONFIG_SPI_FLASH_PLACE_FUNCTIONS_IN_IRAM=y +# end of Optional and Experimental Features (READ DOCS FIRST) +# end of Main Flash configuration + +# +# SPI Flash driver +# +# default: +# CONFIG_SPI_FLASH_VERIFY_WRITE is not set +# default: +# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set +# default: +# CONFIG_SPI_FLASH_ROM_IMPL is not set +# default: +CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y +# default: +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set +# default: +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set +# default: +# CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set +# default: +CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y +# default: +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 +# default: +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 +# default: +CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 +# default: +# CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set +# default: +# CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set +# default: +# CONFIG_SPI_FLASH_OVERRIDE_CHIP_DRIVER_LIST is not set + +# +# Auto-detect flash chips +# +# default: +CONFIG_SPI_FLASH_VENDOR_XMC_SUPPORT_ENABLED=y +# default: +CONFIG_SPI_FLASH_VENDOR_GD_SUPPORT_ENABLED=y +# default: +# CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP is not set +# default: +# CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP is not set +# default: +CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y +# default: +# CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP is not set +# default: +# CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP is not set +# default: +# CONFIG_SPI_FLASH_SUPPORT_TH_CHIP is not set +# end of Auto-detect flash chips + +# default: +CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y +# end of SPI Flash driver + +# +# SPIFFS Configuration +# +# default: +CONFIG_SPIFFS_MAX_PARTITIONS=3 + +# +# SPIFFS Cache Configuration +# +# default: +CONFIG_SPIFFS_CACHE=y +# default: +CONFIG_SPIFFS_CACHE_WR=y +# default: +# CONFIG_SPIFFS_CACHE_STATS is not set +# end of SPIFFS Cache Configuration + +# default: +CONFIG_SPIFFS_PAGE_CHECK=y +# default: +CONFIG_SPIFFS_GC_MAX_RUNS=10 +# default: +# CONFIG_SPIFFS_GC_STATS is not set +# default: +CONFIG_SPIFFS_PAGE_SIZE=256 +# default: +CONFIG_SPIFFS_OBJ_NAME_LEN=32 +# default: +# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set +# default: +CONFIG_SPIFFS_USE_MAGIC=y +# default: +CONFIG_SPIFFS_USE_MAGIC_LENGTH=y +# default: +CONFIG_SPIFFS_META_LENGTH=4 +# default: +CONFIG_SPIFFS_USE_MTIME=y + +# +# Debug Configuration +# +# default: +# CONFIG_SPIFFS_DBG is not set +# default: +# CONFIG_SPIFFS_API_DBG is not set +# default: +# CONFIG_SPIFFS_GC_DBG is not set +# default: +# CONFIG_SPIFFS_CACHE_DBG is not set +# default: +# CONFIG_SPIFFS_CHECK_DBG is not set +# default: +# CONFIG_SPIFFS_TEST_VISUALISATION is not set +# end of Debug Configuration +# end of SPIFFS Configuration + +# +# Virtual file system +# +# default: +CONFIG_VFS_SUPPORT_IO=y +# default: +CONFIG_VFS_SUPPORT_DIR=y +# default: +CONFIG_VFS_SUPPORT_SELECT=y +# default: +CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y +# default: +# CONFIG_VFS_SELECT_IN_RAM is not set +# default: +# CONFIG_VFS_SUPPORT_TERMIOS is not set +# default: +CONFIG_VFS_MAX_COUNT=8 +# default: +# CONFIG_VFS_SUPPRESS_CTX_DEPRECATION is not set + +# +# Host File System I/O (Semihosting) +# +# default: +CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +# end of Host File System I/O (Semihosting) + +# default: +CONFIG_VFS_INITIALIZE_DEV_NULL=y +# end of Virtual file system +# end of Component config + +# default: +# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set + +# Deprecated options for backward compatibility +# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set +# CONFIG_NO_BLOBS is not set +# CONFIG_APP_ROLLBACK_ENABLE is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set +CONFIG_LOG_BOOTLOADER_LEVEL_NONE=y +# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_INFO is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set +CONFIG_LOG_BOOTLOADER_LEVEL=0 +# CONFIG_FLASH_ENCRYPTION_ENABLED is not set +CONFIG_FLASHMODE_QIO=y +# CONFIG_FLASHMODE_QOUT is not set +# CONFIG_FLASHMODE_DIO is not set +# CONFIG_FLASHMODE_DOUT is not set +CONFIG_MONITOR_BAUD=115200 +# CONFIG_OPTIMIZATION_LEVEL_DEBUG is not set +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG is not set +# CONFIG_COMPILER_OPTIMIZATION_DEFAULT is not set +CONFIG_OPTIMIZATION_LEVEL_RELEASE=y +CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE=y +# CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED is not set +# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set +CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED=y +CONFIG_OPTIMIZATION_ASSERTION_LEVEL=0 +# CONFIG_CXX_EXCEPTIONS is not set +CONFIG_STACK_CHECK_NONE=y +# CONFIG_STACK_CHECK_NORM is not set +# CONFIG_STACK_CHECK_STRONG is not set +# CONFIG_STACK_CHECK_ALL is not set +# CONFIG_WARN_WRITE_STRINGS is not set +# CONFIG_EXTERNAL_COEX_ENABLE is not set +# CONFIG_ESP_WIFI_EXTERNAL_COEXIST_ENABLE is not set +# CONFIG_EVENT_LOOP_PROFILING is not set +CONFIG_POST_EVENTS_FROM_ISR=y +CONFIG_POST_EVENTS_FROM_IRAM_ISR=y +CONFIG_ESP_SYSTEM_PD_FLASH=y +CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y +# CONFIG_BROWNOUT_DET is not set +CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y +# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set +CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP32_PHY_MAX_TX_POWER=20 +# CONFIG_MAC_BB_PD is not set +# CONFIG_ESP32_PHY_MAC_BB_PD is not set +CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y +# CONFIG_CONSOLE_UART_DEFAULT is not set +# CONFIG_CONSOLE_UART_CUSTOM is not set +# CONFIG_CONSOLE_UART_NONE is not set +# CONFIG_ESP_CONSOLE_UART_NONE is not set +CONFIG_CONSOLE_UART_NUM=-1 +CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT=y +CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_VIA_TEE=y +CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y +# CONFIG_ESP_SYSTEM_PMP_LP_CORE_RESERVE_MEM_EXECUTABLE is not set +CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_MAIN_TASK_STACK_SIZE=24576 +CONFIG_INT_WDT=y +CONFIG_INT_WDT_TIMEOUT_MS=300 +CONFIG_TASK_WDT=y +CONFIG_ESP_TASK_WDT=y +# CONFIG_TASK_WDT_PANIC is not set +CONFIG_TASK_WDT_TIMEOUT_S=5 +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +# CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set +CONFIG_IPC_TASK_STACK_SIZE=1024 +CONFIG_TIMER_TASK_STACK_SIZE=3584 +CONFIG_ESP32_WIFI_ENABLED=y +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=40 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=60 +# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=40 +# CONFIG_ESP32_WIFI_CSI_ENABLED is not set +# CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED is not set +# CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED is not set +# CONFIG_ESP32_WIFI_NVS_ENABLED is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y +CONFIG_WPA_MBEDTLS_CRYPTO=y +CONFIG_WPA_MBEDTLS_TLS_CLIENT=y +# CONFIG_WPA_WAPI_PSK is not set +# CONFIG_WPA_SUITE_B_192 is not set +# CONFIG_WPA_11KV_SUPPORT is not set +# CONFIG_WPA_MBO_SUPPORT is not set +# CONFIG_WPA_DPP_SUPPORT is not set +# CONFIG_WPA_11R_SUPPORT is not set +# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set +# CONFIG_WPA_WPS_STRICT is not set +# CONFIG_WPA_DEBUG_PRINT is not set +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=40 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=60 +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=40 +# CONFIG_ESP32_WIFI_CSI_ENABLED is not set +# CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED is not set +# CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED is not set +# CONFIG_ESP32_WIFI_NVS_ENABLED is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y +CONFIG_WPA_MBEDTLS_CRYPTO=y +CONFIG_WPA_MBEDTLS_TLS_CLIENT=y +# CONFIG_WPA_WAPI_PSK is not set +# CONFIG_WPA_SUITE_B_192 is not set +# CONFIG_WPA_11KV_SUPPORT is not set +# CONFIG_WPA_MBO_SUPPORT is not set +# CONFIG_WPA_DPP_SUPPORT is not set +# CONFIG_WPA_11R_SUPPORT is not set +# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set +# CONFIG_WPA_WPS_STRICT is not set +# CONFIG_WPA_DEBUG_PRINT is not set +CONFIG_TIMER_TASK_PRIORITY=1 +CONFIG_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_TIMER_QUEUE_LENGTH=10 +# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set +# CONFIG_L2_TO_L3_COPY is not set +# CONFIG_ESP_GRATUITOUS_ARP is not set +CONFIG_TCPIP_RECVMBOX_SIZE=64 +CONFIG_TCP_MAXRTX=12 +CONFIG_TCP_SYNMAXRTX=12 +CONFIG_TCP_MSS=1440 +CONFIG_TCP_MSL=60000 +CONFIG_TCP_SND_BUF_DEFAULT=51200 +CONFIG_TCP_WND_DEFAULT=65535 +CONFIG_TCP_RECVMBOX_SIZE=64 +CONFIG_TCP_QUEUE_OOSEQ=y +CONFIG_TCP_OVERSIZE_MSS=y +# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_TCP_OVERSIZE_DISABLE is not set +CONFIG_UDP_RECVMBOX_SIZE=64 +CONFIG_TCPIP_TASK_STACK_SIZE=8192 +CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set +CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# CONFIG_PPP_SUPPORT is not set +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set +CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_ESP32_PTHREAD_STACK_MIN=768 +CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" +CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set +CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y +# CONFIG_SUPPORT_TERMIOS is not set +CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +# End of deprecated options diff --git a/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig.defaults b/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig.defaults index 2e0134a28..32c597df3 100644 --- a/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig.defaults +++ b/projects/espressif_riscv/vscode/aether-client-cpp/sdkconfig.defaults @@ -1,6 +1,6 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="system/partitions.csv" -CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=24576 CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=8192 # @@ -65,15 +65,12 @@ CONFIG_ESP_SLEEP_POWER_DOWN_FLASH=y # Using an internal RC oscillator (the most economical sleep clocking option) CONFIG_RTC_CLK_SRC_INT_RC=y -# Redirecting the console to UART0 or completely disabling it (None) -# This frees up the resources of the built-in USB-JTAG -CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# UART0 uses GPIO16/17, which are connected to the LTE modem. +# Keep all console output on the native USB Serial/JTAG interface. +CONFIG_ESP_CONSOLE_UART_DEFAULT=n +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_SECONDARY_NONE=y - -# Enable USB console -CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=n -CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y -CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y +CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=n # Disabling USB-Serial-JTAG clocking in sleep mode CONFIG_ESP_BROWNOUT_DET=n diff --git a/projects/xtensa_lx6/platformio/aether-client-cpp/.vscode/extensions.json b/projects/xtensa_lx6/platformio/aether-client-cpp/.vscode/extensions.json new file mode 100644 index 000000000..080e70d08 --- /dev/null +++ b/projects/xtensa_lx6/platformio/aether-client-cpp/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/projects/xtensa_lx6/vscode/aether-client-cpp/README-firebeetle-sim7070.md b/projects/xtensa_lx6/vscode/aether-client-cpp/README-firebeetle-sim7070.md new file mode 100644 index 000000000..20bea6497 --- /dev/null +++ b/projects/xtensa_lx6/vscode/aether-client-cpp/README-firebeetle-sim7070.md @@ -0,0 +1,77 @@ +# FireBeetle 2 ESP32-C6 + Waveshare SIM7070G + +The modem uses the existing SIM7070 AT socket driver. ESP32 uses the ESP-IDF +UART driver, with scheduled nonblocking reads and queued writes. This profile +is for SIM7070G, not SIM7000 firmware. + +## Wiring + +| SIM7070G HAT signal | FireBeetle ESP32-C6 | +| --- | --- | +| TX | GPIO17 / RX | +| RX | GPIO16 / TX | +| DTR | GPIO8 / DC | +| PWR | GPIO14 / RES | +| GND | GND | + +Use 3.3 V UART logic on the HAT, a common ground, and the HAT's specified +5 V supply. GPIO14 controls the HAT PWR input, not the ESP32 reset input. +The board profile selects UART1 at 115200 baud, 8N1, without RTS/CTS. +USB Serial/JTAG is the console; UART0 console output must not drive GPIO16/17. + +PWR is active high on this HAT (its transistor pulls the module PWRKEY low). +DTR is held low to keep the AT UART awake. Initialization first probes AT for +about four seconds. If the modem responds, it is left powered on. If all probes +time out, initialization pulses PWR for 1100 ms and polls AT for readiness. +Boot uses bounded AT requests and a fixed retry count (about 25 seconds +maximum with a responsive scheduler). Cancelling initialization releases PWR. +A modem still booting longer than the initial probe window cannot be distinguished +from an off modem without a STATUS connection. + +## Build + +Open an exported ESP-IDF environment. Run from the repository root (PowerShell): + +```powershell +$repo = (Get-Location).Path +$project = "$repo/projects/xtensa_lx6/vscode/aether-client-cpp" +$build = "$repo/out/build/firebeetle-c6-sim7070" +idf.py -C $project -B $build ` + "-DIDF_TARGET=esp32c6" ` + "-DSDKCONFIG=$build/sdkconfig" ` + "-DSDKCONFIG_DEFAULTS=$project/sdkconfig.defaults;$project/sdkconfig.firebeetle_c6" ` + "-DUSER_CONFIG=$repo/config/user_config_firebeetle_c6_sim7070.h" ` + "-DCOMPILE_EXAMPLE=cloud" ` + "-DAE_FILTRATION=ON" "-DAE_DISTILLATION=OFF" build +idf.py -C $project -B $build -p COM_PORT flash monitor +``` + +Replace COM_PORT with the FireBeetle USB Serial/JTAG port. The shared ESP-IDF +project supports the C6 despite its historical xtensa_lx6 directory name. +Use a separate build directory from Wi-Fi or other board configurations. + +The cloud example already selects AE_EXAMPLE_MODEM. Operator, SIM PIN, APN and +radio mode are configured in examples/common/aether_construct_modem.h; its +current values are MTS / internet.mts.ru / NB-IoT. Set these to match your SIM. +Persisted modem adapter settings take precedence after loading saved state; +when changing UART or operator settings, provision fresh state or update the +saved adapter explicitly. Filtration creates missing objects and retains state +for subsequent boots. + +## Verification + +Desktop tests compile the real ESP32 UART implementation and boot senders against +an IDF API test double. They cover partial writes, callback-driven closure, +invalid/occupied UARTs, overflow, warm/cold boot, timeout and cancellation. + +```powershell +cmake --build --target test-serial-port test-esp32-serial-port +ctest --test-dir -R '^(test-serial-port|test-modem-shutdown|test-esp32-serial-port)$' --output-on-failure +``` + +These tests do not verify electrical timing, RF registration or cloud exchange +on hardware. Check those on the connected board after flashing. + +References: [Waveshare SIM7070G HAT](https://www.waveshare.com/wiki/SIM7070G_Cat-M/NB-IoT/GPRS_HAT), +[ESP-IDF UART API](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c6/api-reference/peripherals/uart.html). + diff --git a/projects/xtensa_lx6/vscode/aether-client-cpp/sdkconfig.firebeetle_c6 b/projects/xtensa_lx6/vscode/aether-client-cpp/sdkconfig.firebeetle_c6 new file mode 100644 index 000000000..da0899dd3 --- /dev/null +++ b/projects/xtensa_lx6/vscode/aether-client-cpp/sdkconfig.firebeetle_c6 @@ -0,0 +1,2 @@ +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y diff --git a/tests/test-api-protocol/test-method-call.cpp b/tests/test-api-protocol/test-method-call.cpp index 03053f4e2..251ea7eb9 100644 --- a/tests/test-api-protocol/test-method-call.cpp +++ b/tests/test-api-protocol/test-method-call.cpp @@ -142,6 +142,55 @@ void test_ReturnResult() { TEST_ASSERT(promise_get_value); } +void CheckUnexpectedResponse(bool error, bool duplicate) { + ProtocolContext pc; + ApiLevel0 api{pc}; + int completions = 0; + bool following_method_called = false; + Subscription method_sub = EventSubscriber{api.method_3_event}.Subscribe( + [&](int, std::string const&) { following_method_called = true; }); + Subscription response_sub; + if (duplicate) { + auto promise = ApiPromise{pc, RequestId{42}}; + response_sub = promise.Subscribe([&](auto const&) { ++completions; }); + } + + auto make_packet = [&]() { + auto context = ApiContext{api}; + if (error) { + context->return_result_api.SendError(RequestId{42}, 0, 7); + } else { + context->return_result_api.SendResult(RequestId{42}, Number{78}); + } + context->method_3(1, "must not run after an unexpected response"); + return std::move(context).Pack(); + }; + if (duplicate) { + auto packet = make_packet(); + ApiParser parser{pc, packet}; + parser.Parse(api); + TEST_ASSERT_EQUAL_INT(1, completions); + TEST_ASSERT_TRUE(following_method_called); + following_method_called = false; + } + auto packet = make_packet(); + ApiParser parser{pc, packet}; + parser.Parse(api); + TEST_ASSERT_EQUAL_INT(duplicate ? 1 : 0, completions); + TEST_ASSERT_FALSE(following_method_called); +} + +void test_UnknownResultCancelsParsing() { + CheckUnexpectedResponse(false, false); +} +void test_UnknownErrorCancelsParsing() { CheckUnexpectedResponse(true, false); } +void test_DuplicateResultCancelsParsing() { + CheckUnexpectedResponse(false, true); +} +void test_DuplicateErrorCancelsParsing() { + CheckUnexpectedResponse(true, true); +} + void test_MethodWithSubApi() { ProtocolContext pc; @@ -377,6 +426,10 @@ void test_PendingResponseDuplicateReplacementPreservesFifo() { int test_method_call() { UNITY_BEGIN(); RUN_TEST(ae::test_method_call::test_ReturnResult); + RUN_TEST(ae::test_method_call::test_UnknownResultCancelsParsing); + RUN_TEST(ae::test_method_call::test_UnknownErrorCancelsParsing); + RUN_TEST(ae::test_method_call::test_DuplicateResultCancelsParsing); + RUN_TEST(ae::test_method_call::test_DuplicateErrorCancelsParsing); RUN_TEST(ae::test_method_call::test_MethodWithSubApi); RUN_TEST(ae::test_method_call::test_ProtocolContextStackAccess); RUN_TEST(ae::test_method_call::test_PendingResponseCapacityEvictsOldest); diff --git a/tests/test-serial-port/CMakeLists.txt b/tests/test-serial-port/CMakeLists.txt index f4dfe01d6..27f48c2b8 100644 --- a/tests/test-serial-port/CMakeLists.txt +++ b/tests/test-serial-port/CMakeLists.txt @@ -22,6 +22,11 @@ list(APPEND test_srcs test-at-listener.cpp test-at-request.cpp test-at-stages.cpp + test-thingy91x.cpp + test-sim7070.cpp + test-app-shutdown.cpp + test-modem-transport-shutdown.cpp + test-win-serial-reopen.cpp ) if(NOT CM_PLATFORM) @@ -36,6 +41,20 @@ if(NOT CM_PLATFORM) target_link_libraries(${PROJECT_NAME} PRIVATE aether unity) add_test(NAME ${PROJECT_NAME} COMMAND $) + add_test(NAME test-modem-shutdown + COMMAND $ --modem-shutdown) else() message(WARNING "Not implemented for ${CM_PLATFORM}") endif() + +# Compile the real ESP32 UART and boot senders against a small IDF test double. +if(NOT CM_PLATFORM) + add_executable(test-esp32-serial-port + test-esp32-serial-port.cpp + ${ROOT_DIR}/aether/serial_ports/esp32_serial_port.cpp) + target_include_directories(test-esp32-serial-port BEFORE PRIVATE esp32-fake) + target_compile_definitions(test-esp32-serial-port PRIVATE + ESP_PLATFORM=1 AE_MODEM_PWR_GPIO=14 AE_MODEM_DTR_GPIO=8) + target_link_libraries(test-esp32-serial-port PRIVATE aether unity) + add_test(NAME test-esp32-serial-port COMMAND test-esp32-serial-port) +endif() diff --git a/tests/test-serial-port/esp32-fake/driver/gpio.h b/tests/test-serial-port/esp32-fake/driver/gpio.h new file mode 100644 index 000000000..39b82a2ae --- /dev/null +++ b/tests/test-serial-port/esp32-fake/driver/gpio.h @@ -0,0 +1,6 @@ +#pragma once +#include "driver/uart.h" +using gpio_num_t = int; +inline constexpr int GPIO_MODE_OUTPUT = 1; +esp_err_t gpio_set_level(gpio_num_t, std::uint32_t); +esp_err_t gpio_set_direction(gpio_num_t, int); diff --git a/tests/test-serial-port/esp32-fake/driver/uart.h b/tests/test-serial-port/esp32-fake/driver/uart.h new file mode 100644 index 000000000..6ec1c1129 --- /dev/null +++ b/tests/test-serial-port/esp32-fake/driver/uart.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include +using esp_err_t = int; +inline constexpr int ESP_OK = 0; +inline constexpr int ESP_ERR_NO_MEM = 257; +inline constexpr int ESP_ERR_TIMEOUT = 263; +using QueueHandle_t = void*; +inline constexpr int pdTRUE = 1; +inline constexpr int UART_PIN_NO_CHANGE = -1; +enum uart_port_t { UART_NUM_0, UART_NUM_1, UART_NUM_MAX }; +enum uart_word_length_t { + UART_DATA_5_BITS, + UART_DATA_6_BITS, + UART_DATA_7_BITS, + UART_DATA_8_BITS +}; +enum uart_parity_t { + UART_PARITY_DISABLE, + UART_PARITY_EVEN = 2, + UART_PARITY_ODD = 3 +}; +enum uart_stop_bits_t { + UART_STOP_BITS_1 = 1, + UART_STOP_BITS_1_5, + UART_STOP_BITS_2 +}; +inline constexpr int UART_HW_FLOWCTRL_DISABLE = 0; +inline constexpr int UART_SCLK_DEFAULT = 1; +enum uart_event_type_t { + UART_DATA, + UART_FIFO_OVF, + UART_BUFFER_FULL, + UART_FRAME_ERR, + UART_PARITY_ERR +}; +struct uart_event_t { + uart_event_type_t type; +}; +struct uart_config_t { + int baud_rate{}; + uart_word_length_t data_bits{}; + uart_parity_t parity{}; + uart_stop_bits_t stop_bits{}; + int flow_ctrl{}; + int source_clk{}; +}; +bool uart_is_driver_installed(uart_port_t); +esp_err_t uart_param_config(uart_port_t, uart_config_t const*); +esp_err_t uart_set_pin(uart_port_t, int, int, int, int); +esp_err_t uart_driver_install(uart_port_t, int, int, int, QueueHandle_t*, int); +esp_err_t uart_driver_delete(uart_port_t); +int uart_read_bytes(uart_port_t, void*, std::uint32_t, std::uint32_t); +esp_err_t uart_wait_tx_done(uart_port_t, std::uint32_t); +esp_err_t uart_get_tx_buffer_free_size(uart_port_t, std::size_t*); +int uart_write_bytes(uart_port_t, void const*, std::size_t); +int xQueueReceive(QueueHandle_t, void*, std::uint32_t); diff --git a/tests/test-serial-port/esp32-fake/sdkconfig.h b/tests/test-serial-port/esp32-fake/sdkconfig.h new file mode 100644 index 000000000..a75d93ff3 --- /dev/null +++ b/tests/test-serial-port/esp32-fake/sdkconfig.h @@ -0,0 +1,3 @@ +#pragma once +// Desktop IDF test double; no hardware architecture is selected. +#define CONFIG_IDF_TARGET "desktop-test" diff --git a/tests/test-serial-port/esp32-fake/soc/soc_caps.h b/tests/test-serial-port/esp32-fake/soc/soc_caps.h new file mode 100644 index 000000000..2d89a1848 --- /dev/null +++ b/tests/test-serial-port/esp32-fake/soc/soc_caps.h @@ -0,0 +1,2 @@ +#pragma once +#define SOC_UART_HP_NUM 2 diff --git a/tests/test-serial-port/main.cpp b/tests/test-serial-port/main.cpp index 78ebd337b..419b7fcb8 100644 --- a/tests/test-serial-port/main.cpp +++ b/tests/test-serial-port/main.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "aether/tele.h" @@ -31,8 +32,20 @@ extern int test_at_dispatcher(); extern int test_at_listener(); extern int test_at_request(); extern int test_at_stages(); +extern int test_thingy91x(); +extern int test_sim7070(); +extern int test_app_shutdown(); +extern int test_modem_transport_shutdown(); +extern int test_win_serial_reopen(char const* port); -int main() { +int main(int argc, char* argv[]) { + if (argc == 3 && std::string_view{argv[1]} == "--serial-reopen") { + return test_win_serial_reopen(argv[2]); + } + if (argc == 2 && std::string_view{argv[1]} == "--modem-shutdown") { + return test_thingy91x() + test_sim7070() + test_app_shutdown() + + test_modem_transport_shutdown(); + } int res = 0; res += test_at_support(); res += test_at_buffer(); @@ -40,5 +53,9 @@ int main() { res += test_at_listener(); res += test_at_request(); res += test_at_stages(); + res += test_thingy91x(); + res += test_sim7070(); + res += test_app_shutdown(); + res += test_modem_transport_shutdown(); return res; } diff --git a/tests/test-serial-port/mock-serial-port.h b/tests/test-serial-port/mock-serial-port.h index 765eb40ad..4068a08f6 100644 --- a/tests/test-serial-port/mock-serial-port.h +++ b/tests/test-serial-port/mock-serial-port.h @@ -29,6 +29,7 @@ class MockSerialPort final : public ISerialPort { MockSerialPort() = default; bool IsOpen() override { return is_open_; } + void Close() override { is_open_ = false; } void Write(std::span data) override { write_event_.Emit(data); diff --git a/tests/test-serial-port/test-app-shutdown.cpp b/tests/test-serial-port/test-app-shutdown.cpp new file mode 100644 index 000000000..e0c5b12ad --- /dev/null +++ b/tests/test-serial-port/test-app-shutdown.cpp @@ -0,0 +1,183 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "aether/config.h" +#if AE_DISTILLATION +# include "aether-objects/domain_storage/ram_domain_storage.h" +# include "aether/aether_app.h" +# include "aether/client.h" + +namespace ae::test_app_shutdown { +class ShutdownTestAdapter final : public Adapter { + AE_OBJECT(ShutdownTestAdapter, Adapter, 0) + protected: + ShutdownTestAdapter() = default; + + public: + ShutdownTestAdapter(ObjProp prop, TaskScheduler& scheduler, int& finished) + : Adapter{prop}, scheduler_{&scheduler}, finished_{&finished} {} + AE_OBJECT_REFLECT() + std::vector access_points() override { return {}; } + IAdapterStop& Stop() override { + if (on_stop) { + on_stop(); + } + ++stop_count; + task_ = scheduler_->DelayedTask( + [this]() { + ++*finished_; + action_.Finish(); + }, + Now() + std::chrono::milliseconds{20}); + return action_; + } + int stop_count{}; + std::function on_stop; + + private: + TaskScheduler* scheduler_{}; + int* finished_{}; + IAdapterStop action_; + TaskSubscription task_; +}; + +std::unique_ptr MakeApp(int& finished, + ShutdownTestAdapter::ptr& adapter) { + return AetherApp::Construct( + AetherAppContext{[]() { + return std::make_unique(); + }}.AddAdapterFactory([&](AetherAppContext const& context) + -> Adapter::ptr { + adapter = ShutdownTestAdapter::ptr::Create( + context.domain(), *context.aether()->task_scheduler, finished); + return adapter; + })); +} + +void test_ExitWaitsForAdapterAndKeepsOriginalExitCode() { + int finished = 0; + ShutdownTestAdapter::ptr adapter; + auto app = MakeApp(finished, adapter); + app->Exit(7); + app->Exit(9); + TEST_ASSERT_FALSE(app->IsExited()); + app->Update(Now()); + TEST_ASSERT_EQUAL_INT(1, adapter->stop_count); + TEST_ASSERT_EQUAL_INT(0, finished); + TEST_ASSERT_FALSE(app->IsExited()); + for (int i = 0; i < 8; ++i) { + app->Update(Now() + std::chrono::milliseconds{100}); + } + TEST_ASSERT_EQUAL_INT(1, finished); + TEST_ASSERT_TRUE(app->IsExited()); + TEST_ASSERT_EQUAL_INT(7, app->ExitCode()); + // Release the external persistent reference before its domain is destroyed. + adapter.Reset(); +} + +void test_ExitStopsClientConnectionsBeforeAdapter() { + int finished = 0; + ShutdownTestAdapter::ptr adapter; + auto app = MakeApp(finished, adapter); + ClientConfig config{}; + config.cloud.push_back(ServerConfig{ServerId{7}, {}}); + auto client = app->aether()->CreateClient(config, "shutdown-test"); + Ptr loaded = client.Load(); + auto& connections = loaded->cloud_connection(); + TEST_ASSERT_FALSE(connections.selected_servers().empty()); + bool checked = false; + adapter->on_stop = [&]() { + TEST_ASSERT_TRUE(connections.selected_servers().empty()); + checked = true; + }; + app->Exit(); + for (int i = 0; i < 8; ++i) { + app->Update(Now() + std::chrono::milliseconds{100}); + } + TEST_ASSERT_TRUE(checked); + TEST_ASSERT_TRUE(app->IsExited()); + auto server = app->aether()->GetServer(ServerId{7}); + TEST_ASSERT_NULL(loaded->server_connection_manager() + .CreateConnection(server.Load()) + .get()); + server.Reset(); + adapter->on_stop = {}; + loaded = {}; + client.Reset(); + adapter.Reset(); +} + +void test_DestructorWaitsForAdapterShutdown() { + int finished = 0; + ShutdownTestAdapter::ptr adapter; + auto app = MakeApp(finished, adapter); + adapter.Reset(); + app.reset(); + TEST_ASSERT_EQUAL_INT(1, finished); +} + +void test_AdapterStopCompletesImmediatelyWithoutPendingWork() { + AdapterStop empty; + TEST_ASSERT_TRUE(empty.is_finished()); + Action completed; + completed.Finish(); + AdapterStop stop{completed}; + TEST_ASSERT_TRUE(stop.is_finished()); +} + +void test_AdapterStopWaitsForDriverAndCompletesOnlyOnce() { + Action driver; + AdapterStop stop{driver}; + int completions = 0; + Subscription sub = stop.finished_event().Subscribe([&]() { ++completions; }); + TEST_ASSERT_FALSE(stop.is_finished()); + driver.Finish(); + TEST_ASSERT_TRUE(stop.is_finished()); + TEST_ASSERT_EQUAL_INT(1, completions); + driver.Finish(); + TEST_ASSERT_EQUAL_INT(1, completions); +} + +void test_AdapterStopUnsubscribesWhenDestroyed() { + Action driver; + { + AdapterStop stop{driver}; + TEST_ASSERT_FALSE(stop.is_finished()); + } + driver.Finish(); + TEST_ASSERT_TRUE(driver.is_finished()); +} +} // namespace ae::test_app_shutdown +#endif +int test_app_shutdown() { +#if AE_DISTILLATION + using namespace ae::test_app_shutdown; // NOLINT: Unity suite entry. + UNITY_BEGIN(); + RUN_TEST(test_ExitStopsClientConnectionsBeforeAdapter); + RUN_TEST(test_ExitWaitsForAdapterAndKeepsOriginalExitCode); + RUN_TEST(test_DestructorWaitsForAdapterShutdown); + RUN_TEST(test_AdapterStopCompletesImmediatelyWithoutPendingWork); + RUN_TEST(test_AdapterStopWaitsForDriverAndCompletesOnlyOnce); + RUN_TEST(test_AdapterStopUnsubscribesWhenDestroyed); + return UNITY_END(); +#else + return 0; +#endif +} diff --git a/tests/test-serial-port/test-at-buffer.cpp b/tests/test-serial-port/test-at-buffer.cpp index 2fbd17c79..7d393d366 100644 --- a/tests/test-serial-port/test-at-buffer.cpp +++ b/tests/test-serial-port/test-at-buffer.cpp @@ -72,6 +72,9 @@ void test_AtBufferMultipleCompleteLines() { void test_AtBufferIncompleteLine() { tests::MockSerialPort mock_serial{}; AtBuffer buffer{mock_serial}; + int updates = 0; + Subscription update_sub = + buffer.update_event().Subscribe([&](auto) { ++updates; }); // Send incomplete line without \r\n DataBuffer data; @@ -79,14 +82,21 @@ void test_AtBufferIncompleteLine() { data.insert(data.end(), line.begin(), line.end()); mock_serial.WriteOut(data); - // Note: Current implementation stores incomplete lines - // Verify buffer contains the incomplete line + // Partial input must not be published before the entire CR/LF arrives. + TEST_ASSERT_TRUE(buffer.begin() == buffer.end()); + TEST_ASSERT_EQUAL_INT(0, updates); + mock_serial.WriteOut(DataBuffer{'\r'}); + TEST_ASSERT_TRUE(buffer.begin() == buffer.end()); + TEST_ASSERT_EQUAL_INT(0, updates); + mock_serial.WriteOut(DataBuffer{'\n'}); TEST_ASSERT_FALSE(buffer.begin() == buffer.end()); + TEST_ASSERT_EQUAL_INT(1, updates); auto it = buffer.begin(); TEST_ASSERT_EQUAL(2, it->size()); // "AT" without \r\n std::string_view line_content(reinterpret_cast(it->data()), it->size()); TEST_ASSERT_EQUAL_STRING_LEN("AT", line_content.data(), line_content.size()); + TEST_ASSERT_TRUE(++it == buffer.end()); } void test_AtBufferFindPatternBasic() { diff --git a/tests/test-serial-port/test-at-request.cpp b/tests/test-serial-port/test-at-request.cpp index 44a609d29..d07adab69 100644 --- a/tests/test-serial-port/test-at-request.cpp +++ b/tests/test-serial-port/test-at-request.cpp @@ -51,7 +51,8 @@ void test_AtRequestStringCommand0WaitsSuccess() { auto res = ex::SyncWait(std::move(at_request)); - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", received.data(), 5); + TEST_ASSERT_EQUAL_UINT(4, received.size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", received.data(), 4); } void test_AtRequestStringCommand0WaitsError() { @@ -85,7 +86,8 @@ void test_AtRequestStringCommand1WaitSuccess() { at::Wait{"OK"}); mock_serial.write_event().Subscribe([&](auto data) { - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", data.data(), 5); + TEST_ASSERT_EQUAL_UINT(4, data.size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", data.data(), 4); // Add expected response "OK" to trigger wait observer std::string_view ok_line{"OK\r\n"}; mock_serial.WriteOut(std::span{ @@ -123,7 +125,8 @@ void test_AtRequestStringCommand1WaitErrorTimeout() { DataBuffer received; mock_serial.write_event().Subscribe([&](auto data) { - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", data.data(), 5); + TEST_ASSERT_EQUAL_UINT(4, data.size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", data.data(), 4); std::copy(std::begin(data), std::end(data), std::back_inserter(received)); }); diff --git a/tests/test-serial-port/test-at-stages.cpp b/tests/test-serial-port/test-at-stages.cpp index 069226d43..507e86654 100644 --- a/tests/test-serial-port/test-at-stages.cpp +++ b/tests/test-serial-port/test-at-stages.cpp @@ -62,7 +62,8 @@ void test_RunAtStage() { ctx.Update(); - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", received.data(), 5); + TEST_ASSERT_EQUAL_UINT(4, received.size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", received.data(), 4); } void test_RunMultipleStages() { @@ -87,9 +88,13 @@ void test_RunMultipleStages() { ctx.Update(); - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", received[0].data(), 5); - TEST_ASSERT_EQUAL_STRING_LEN("MT\r\n", received[1].data(), 5); - TEST_ASSERT_EQUAL_STRING_LEN("UT\r\n", received[2].data(), 5); + TEST_ASSERT_EQUAL_UINT(3, received.size()); + TEST_ASSERT_EQUAL_UINT(4, received[0].size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", received[0].data(), 4); + TEST_ASSERT_EQUAL_UINT(4, received[1].size()); + TEST_ASSERT_EQUAL_MEMORY("MT\r\n", received[1].data(), 4); + TEST_ASSERT_EQUAL_UINT(4, received[2].size()); + TEST_ASSERT_EQUAL_MEMORY("UT\r\n", received[2].data(), 4); } void test_RunAtWithWaitStage() { @@ -100,7 +105,8 @@ void test_RunAtWithWaitStage() { ActionsQueue queue; mock_serial.write_event().Subscribe([&](auto data) { - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", data.data(), 5); + TEST_ASSERT_EQUAL_UINT(4, data.size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", data.data(), 4); }); bool executed = false; @@ -129,7 +135,8 @@ void test_RunMultipleStagesWithWait() { ActionsQueue queue; mock_serial.write_event().Subscribe([&](auto data) { - TEST_ASSERT_EQUAL_STRING_LEN("AT\r\n", data.data(), 5); + TEST_ASSERT_EQUAL_UINT(4, data.size()); + TEST_ASSERT_EQUAL_MEMORY("AT\r\n", data.data(), 4); }); bool executed1 = false; diff --git a/tests/test-serial-port/test-escaped-io-stream-trap.cpp b/tests/test-serial-port/test-escaped-io-stream-trap.cpp new file mode 100644 index 000000000..e8a19a6cc --- /dev/null +++ b/tests/test-serial-port/test-escaped-io-stream-trap.cpp @@ -0,0 +1,52 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include "aether/escaped_io_stream_trap.h" + +namespace ae::test_escaped_io_stream_trap { +class BlobCollector final : public tele::ILogCollector { + public: + void WriteLine(tele::ILogLine& log_line) override { log_line.Blob(data); } + + std::array data{'A', '\r', '\n', 'B', '\t', 0x00, 0x7F, 'C'}; +}; + +void test_ControlCharactersAreRenderedAsHex() { + std::ostringstream stream; + auto trap = tele::EscapedIoStreamTrap{stream}; + auto collector = BlobCollector{}; + constexpr auto module = tele::Module{1, 0, 1, "Test"}; + auto const tag = tele::Tag{0, module, "Test"}; + + trap.LogLine(tag, collector); + + TEST_ASSERT_EQUAL_STRING(" 0:A{0x0D}{0x0A}B{0x09}{0x00}{0x7F}C\n", + stream.str().c_str()); +} +} // namespace ae::test_escaped_io_stream_trap + +int test_escaped_io_stream_trap() { + using namespace ae::test_escaped_io_stream_trap; // NOLINT + UNITY_BEGIN(); + RUN_TEST(test_ControlCharactersAreRenderedAsHex); + return UNITY_END(); +} diff --git a/tests/test-serial-port/test-esp32-serial-port.cpp b/tests/test-serial-port/test-esp32-serial-port.cpp new file mode 100644 index 000000000..968f2ccf5 --- /dev/null +++ b/tests/test-serial-port/test-esp32-serial-port.cpp @@ -0,0 +1,353 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include "aether/modems/esp32_modem_boot.h" +#include "aether/serial_ports/esp32_serial_port.h" +#include "tests/test-serial-port/mock-serial-port.h" + +namespace ae::test_esp32_serial_port { +struct Fake { + bool installed{}; + int installs{}, deletes{}, reads{}, tx_pin{}, rx_pin{}, power_high{}; + int pin_error{}; + std::size_t tx_capacity{8192}; + int tx_buffer_size{}; + bool tx_busy{}; + std::size_t tx_free{8192}; + uart_port_t port{UART_NUM_MAX}; + uart_config_t config{}; + std::vector received, transmitted; + std::deque events; + std::array levels{}; +}; +Fake fake; +struct Context { + AeCtx ToAeContext() const { + static constexpr auto table = + AeCtxTable{nullptr, [](void* p) -> TaskScheduler& { + return static_cast(p)->scheduler; + }}; + return {this, &table}; + } + void Tick() { + scheduler.Update(std::chrono::system_clock::now() + + std::chrono::milliseconds{10}); + } + TaskScheduler scheduler; +}; +SerialInit Init() { + return {.port_name = "UART1", .tx_io_num = 16, .rx_io_num = 17}; +} +void test_ConfigAndReopen() { + Context c; + { + Esp32SerialPort port{c, Init()}; + TEST_ASSERT_TRUE(port.IsOpen()); + TEST_ASSERT_EQUAL_INT(UART_NUM_1, fake.port); + TEST_ASSERT_EQUAL_INT(16, fake.tx_pin); + TEST_ASSERT_EQUAL_INT(17, fake.rx_pin); + TEST_ASSERT_EQUAL_INT(115200, fake.config.baud_rate); + TEST_ASSERT_EQUAL_INT(8192, fake.tx_buffer_size); + port.Close(); + port.Close(); + TEST_ASSERT_EQUAL_INT(1, fake.deletes); + c.Tick(); + TEST_ASSERT_EQUAL_INT(0, fake.reads); + } + Esp32SerialPort reopened{c, Init()}; + TEST_ASSERT_TRUE(reopened.IsOpen()); + TEST_ASSERT_EQUAL_INT(2, fake.installs); +} +void test_InvalidAndOccupiedPort() { + Context c; + auto init = Init(); + init.port_name = "UART2"; + Esp32SerialPort invalid{c, init}; + TEST_ASSERT_FALSE(invalid.IsOpen()); + Esp32SerialPort valid{c, Init()}; + { + Esp32SerialPort occupied{c, Init()}; + TEST_ASSERT_FALSE(occupied.IsOpen()); + } + TEST_ASSERT_EQUAL_INT(0, fake.deletes); + TEST_ASSERT_TRUE(valid.IsOpen()); +} +void test_BufferedWritesKeepOrderWhileBusy() { + Context c; + Esp32SerialPort port{c, Init()}; + std::array first{1, 2, 3, 4, 5}; + std::array second{6, 7}; + port.Write(first); + fake.tx_busy = true; + port.Write(second); + c.Tick(); + TEST_ASSERT_EQUAL_UINT(first.size(), fake.transmitted.size()); + fake.tx_busy = false; + fake.tx_free = 0; + c.Tick(); + TEST_ASSERT_EQUAL_UINT(first.size(), fake.transmitted.size()); + fake.tx_free = 8192; + for (int i = 0; i < 5; ++i) { + c.Tick(); + } + std::array expected{1, 2, 3, 4, 5, 6, 7}; + TEST_ASSERT_EQUAL_UINT(expected.size(), fake.transmitted.size()); + TEST_ASSERT_EQUAL_MEMORY(expected.data(), fake.transmitted.data(), + expected.size()); +} +void test_CloseInReadCallback() { + Context c; + Esp32SerialPort port{c, Init()}; + int calls = 0; + Subscription sub = port.read_event().Subscribe([&](auto data) { + ++calls; + TEST_ASSERT_EQUAL_UINT(3, data.size()); + port.Close(); + }); + fake.received = {1, 2, 3}; + c.Tick(); + c.Tick(); + TEST_ASSERT_EQUAL_INT(1, calls); + TEST_ASSERT_EQUAL_INT(1, fake.reads); +} +void test_BinaryPacketAcrossFifoBoundary() { + Context c; + Esp32SerialPort port{c, Init()}; + std::array packet{}; + for (std::size_t i = 0; i < packet.size(); ++i) { + packet[i] = static_cast(i); + } + port.Write(packet); + TEST_ASSERT_EQUAL_UINT(packet.size(), fake.transmitted.size()); + c.Tick(); + TEST_ASSERT_EQUAL_UINT(packet.size(), fake.transmitted.size()); + TEST_ASSERT_EQUAL_MEMORY(packet.data(), fake.transmitted.data(), + packet.size()); +} +void test_ReceiveOverflowClosesPort() { + Context c; + Esp32SerialPort port{c, Init()}; + fake.events.push_back(UART_FIFO_OVF); + c.Tick(); + TEST_ASSERT_FALSE(port.IsOpen()); +} +void test_ShortBufferedWriteClosesPort() { + Context c; + Esp32SerialPort port{c, Init()}; + fake.tx_capacity = 128; + std::array packet{}; + port.Write(packet); + TEST_ASSERT_FALSE(port.IsOpen()); + c.Tick(); + TEST_ASSERT_EQUAL_UINT(128, fake.transmitted.size()); +} +void test_TransmitOverflowClosesPort() { + Context c; + Esp32SerialPort port{c, Init()}; + fake.tx_capacity = 0; + std::vector data(16385); + port.Write(data); + TEST_ASSERT_FALSE(port.IsOpen()); +} +void test_RunningModemDoesNotPulsePower() { + Context c; + tests::MockSerialPort serial; + AtSupport at{serial}; + bool done = false; + auto waiter = + ex::AsyncWaiter{AeContext{c}, + esp32_modem_boot_internal::EnsureReady(c, at) | + ex::then([&]() noexcept { done = true; }) | + ex::upon_error([](auto) noexcept { + TEST_FAIL_MESSAGE("Unexpected boot failure"); + }), + [](auto&&) noexcept {}}; + constexpr std::array ok{'O', 'K', '\r', '\n'}; + serial.WriteOut(ok); + TEST_ASSERT_TRUE(done); + TEST_ASSERT_EQUAL_INT(0, fake.power_high); + TEST_ASSERT_EQUAL_INT(0, fake.levels[8]); +} +void test_ColdBootPulsesAndWaitsForAt() { + Context c; + tests::MockSerialPort serial; + AtSupport at{serial}; + bool done = false; + bool failed = false; + auto waiter = + ex::AsyncWaiter{AeContext{c}, + esp32_modem_boot_internal::EnsureReady(c, at) | + ex::then([&]() noexcept { done = true; }) | + ex::upon_error([&](auto) noexcept { failed = true; }), + [](auto&&) noexcept {}}; + auto later = std::chrono::system_clock::now() + std::chrono::seconds{2}; + for (int i = 0; i < 10 && fake.power_high == 0; ++i) { + c.scheduler.Update(later); + } + TEST_ASSERT_EQUAL_INT(1, fake.power_high); + TEST_ASSERT_EQUAL_INT(1, fake.levels[14]); + TEST_ASSERT_FALSE(done); + c.scheduler.Update(later); + TEST_ASSERT_EQUAL_INT(0, fake.levels[14]); + constexpr std::array ok{'O', 'K', '\r', '\n'}; + serial.WriteOut(ok); + TEST_ASSERT_TRUE(done); + TEST_ASSERT_FALSE(failed); +} +void test_CancelReleasesPowerKey() { + Context c; + tests::MockSerialPort serial; + AtSupport at{serial}; + { + auto waiter = + ex::AsyncWaiter{AeContext{c}, + esp32_modem_boot_internal::EnsureReady(c, at) | + ex::upon_error([](auto) noexcept {}), + [](auto&&) noexcept {}}; + auto later = std::chrono::system_clock::now() + std::chrono::seconds{2}; + for (int i = 0; i < 10 && fake.power_high == 0; ++i) { + c.scheduler.Update(later); + } + TEST_ASSERT_EQUAL_INT(1, fake.levels[14]); + } + TEST_ASSERT_EQUAL_INT(0, fake.levels[14]); + c.Tick(); +} +void test_BootTimeoutIsReported() { + Context c; + tests::MockSerialPort serial; + AtSupport at{serial}; + bool timed_out = false; + auto waiter = ex::AsyncWaiter{ + AeContext{c}, + esp32_modem_boot_internal::EnsureReady(c, at) | + ex::upon_error([&](auto) noexcept { timed_out = true; }), + [](auto&&) noexcept {}}; + auto later = std::chrono::system_clock::now() + std::chrono::seconds{2}; + for (int i = 0; i < 40 && !timed_out; ++i) { + c.scheduler.Update(later); + } + TEST_ASSERT_TRUE(timed_out); + for (int i = 0; i < 4; ++i) { + c.scheduler.Update(later); + } + TEST_ASSERT_EQUAL_INT(1, fake.power_high); + TEST_ASSERT_EQUAL_INT(0, fake.levels[14]); +} +} // namespace ae::test_esp32_serial_port + +bool uart_is_driver_installed(uart_port_t) { + return ae::test_esp32_serial_port::fake.installed; +} +esp_err_t uart_param_config(uart_port_t p, uart_config_t const* c) { + auto& f = ae::test_esp32_serial_port::fake; + f.port = p; + f.config = *c; + return ESP_OK; +} +esp_err_t uart_set_pin(uart_port_t, int tx, int rx, int, int) { + auto& f = ae::test_esp32_serial_port::fake; + f.tx_pin = tx; + f.rx_pin = rx; + return f.pin_error; +} +esp_err_t uart_driver_install(uart_port_t, int, int tx_buffer_size, int, + QueueHandle_t* q, int) { + auto& f = ae::test_esp32_serial_port::fake; + ++f.installs; + f.tx_buffer_size = tx_buffer_size; + f.installed = true; + *q = &f; + return ESP_OK; +} +esp_err_t uart_driver_delete(uart_port_t) { + auto& f = ae::test_esp32_serial_port::fake; + ++f.deletes; + f.installed = false; + return ESP_OK; +} +int uart_read_bytes(uart_port_t, void* out, std::uint32_t size, std::uint32_t) { + auto& f = ae::test_esp32_serial_port::fake; + ++f.reads; + auto n = std::min(size, f.received.size()); + if (n != 0) { + std::memcpy(out, f.received.data(), n); + } + f.received.erase(f.received.begin(), + f.received.begin() + static_cast(n)); + return static_cast(n); +} +esp_err_t uart_wait_tx_done(uart_port_t, std::uint32_t timeout) { + TEST_ASSERT_EQUAL_UINT(0, timeout); + return ae::test_esp32_serial_port::fake.tx_busy ? ESP_ERR_TIMEOUT : ESP_OK; +} +int uart_write_bytes(uart_port_t, void const* source, std::size_t size) { + auto const* data = static_cast(source); + auto& f = ae::test_esp32_serial_port::fake; + auto n = std::min(size, f.tx_capacity); + f.transmitted.insert(f.transmitted.end(), data, data + n); + return static_cast(n); +} +esp_err_t uart_get_tx_buffer_free_size(uart_port_t, std::size_t* size) { + *size = ae::test_esp32_serial_port::fake.tx_free; + return ESP_OK; +} +int xQueueReceive(QueueHandle_t, void* out, std::uint32_t) { + auto& f = ae::test_esp32_serial_port::fake; + if (f.events.empty()) { + return 0; + } + static_cast(out)->type = f.events.front(); + f.events.pop_front(); + return pdTRUE; +} +esp_err_t gpio_set_level(gpio_num_t p, std::uint32_t value) { + auto& f = ae::test_esp32_serial_port::fake; + f.levels[p] = static_cast(value); + if (p == 14 && value != 0) { + ++f.power_high; + } + return ESP_OK; +} +esp_err_t gpio_set_direction(gpio_num_t, int) { return ESP_OK; } +void setUp() { ae::test_esp32_serial_port::fake = {}; } +void tearDown() {} +int test_esp32_serial_port() { + // NOLINTNEXTLINE: suite entry imports test names for Unity. + using namespace ae::test_esp32_serial_port; + UNITY_BEGIN(); + RUN_TEST(test_ConfigAndReopen); + RUN_TEST(test_InvalidAndOccupiedPort); + RUN_TEST(test_BufferedWritesKeepOrderWhileBusy); + RUN_TEST(test_ShortBufferedWriteClosesPort); + RUN_TEST(test_BinaryPacketAcrossFifoBoundary); + RUN_TEST(test_CloseInReadCallback); + RUN_TEST(test_ReceiveOverflowClosesPort); + RUN_TEST(test_TransmitOverflowClosesPort); + RUN_TEST(test_RunningModemDoesNotPulsePower); + RUN_TEST(test_ColdBootPulsesAndWaitsForAt); + RUN_TEST(test_CancelReleasesPowerKey); + RUN_TEST(test_BootTimeoutIsReported); + return UNITY_END(); +} +int main() { return test_esp32_serial_port(); } diff --git a/tests/test-serial-port/test-modem-transport-shutdown.cpp b/tests/test-serial-port/test-modem-transport-shutdown.cpp new file mode 100644 index 000000000..dd3cb73bb --- /dev/null +++ b/tests/test-serial-port/test-modem-transport-shutdown.cpp @@ -0,0 +1,286 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "aether/config.h" +#if AE_SUPPORT_MODEMS +# include "aether/channels/modem_channel.h" +# include "aether/clock.h" +# include "aether/transport/modems/modem_transport.h" + +namespace ae { +struct ModemChannelTestAccess { + static TransportBuildSender ConnectTransport( + std::unique_ptr stream) { + return ModemChannel::ConnectTransport(std::move(stream)); + } +}; +} // namespace ae + +namespace ae::test_modem_transport_shutdown { +class ConnectingStream final : public ByteIStream { + public: + void SetState(LinkState state) { + info_.link_state = state; + updates_.Emit(); + } + WriteAction& Write(DataBuffer&&) override { return write_; } + StreamInfo stream_info() const override { return info_; } + StreamUpdateEvent::Subscriber stream_update_event() override { + return EventSubscriber{updates_}; + } + OutDataEvent::Subscriber out_data_event() override { + return EventSubscriber{data_}; + } + void Restream() override {} + + private: + StreamInfo info_{}; + StreamUpdateEvent updates_; + OutDataEvent data_; + WriteAction write_; +}; + +struct ConnectResult { + std::unique_ptr transport; + int successes{}; + int errors{}; +}; +struct ConnectReceiver { + using receiver_concept = ex::receiver_t; + void set_value(std::unique_ptr transport) && noexcept { + ++result->successes; + result->transport = std::move(transport); + } + void set_error(int) && noexcept { ++result->errors; } + void set_error(std::exception_ptr) && noexcept { ++result->errors; } + void set_stopped() && noexcept { ++result->errors; } + auto get_env() const noexcept { return ex::env{}; } + ConnectResult* result; +}; + +void test_ConnectedTransportCanDisconnectWhileBuilderRemainsAlive() { + auto stream = std::make_unique(); + auto* raw = stream.get(); + raw->SetState(LinkState::kUnlinked); + ConnectResult result; + auto operation = + ex::connect(ModemChannelTestAccess::ConnectTransport(std::move(stream)), + ConnectReceiver{&result}); + ex::start(operation); + raw->SetState(LinkState::kLinked); + TEST_ASSERT_EQUAL_INT(1, result.successes); + TEST_ASSERT_NOT_NULL(result.transport.get()); + // Keep operation alive, just like the real channel builder after success. + raw->SetState(LinkState::kLinkError); + raw->SetState(LinkState::kLinked); + TEST_ASSERT_EQUAL_INT(1, result.successes); + TEST_ASSERT_EQUAL_INT(0, result.errors); +} + +void test_ConnectionErrorCompletesOnlyOnce() { + auto stream = std::make_unique(); + auto* raw = stream.get(); + raw->SetState(LinkState::kUnlinked); + ConnectResult result; + auto operation = + ex::connect(ModemChannelTestAccess::ConnectTransport(std::move(stream)), + ConnectReceiver{&result}); + ex::start(operation); + raw->SetState(LinkState::kLinkError); + raw->SetState(LinkState::kLinked); + TEST_ASSERT_EQUAL_INT(1, result.errors); + TEST_ASSERT_EQUAL_INT(0, result.successes); +} + +void test_AlreadyFailedTransportCompletesImmediately() { + auto stream = std::make_unique(); + stream->SetState(LinkState::kLinkError); + ConnectResult result; + auto operation = + ex::connect(ModemChannelTestAccess::ConnectTransport(std::move(stream)), + ConnectReceiver{&result}); + ex::start(operation); + TEST_ASSERT_EQUAL_INT(1, result.errors); + TEST_ASSERT_EQUAL_INT(0, result.successes); +} + +struct TestContext { + AeCtx ToAeContext() const { + static constexpr auto table = + AeCtxTable{nullptr, [](void* obj) -> TaskScheduler& { + return static_cast(obj)->scheduler; + }}; + return AeCtx{const_cast(this), &table}; // NOLINT + } + TaskScheduler scheduler; +}; +class OpenedOperation final : public OpenNetworkOperation { + public: + OpenedOperation() { SetResult(Ok{ConnectionIndex{1}}); } +}; +class StoppingDriver final : public IModemDriver { + public: + void RemoteClose(ConnectionIndex connection) { + NotifyConnectionClosed(connection); + } + ModemOperation* Start() override { return nullptr; } + ModemOperation* Stop() override { return nullptr; } + OpenNetworkOperation* OpenNetwork(Protocol, std::string const&, + std::uint16_t) override { + return &opened_; + } + ModemOperation* CloseNetwork(ConnectionIndex) override { + ++closed; + return nullptr; + } + WriteOperation* WritePacket(ConnectionIndex, + std::span) override { + ++rejected; + return nullptr; + } + DataEvent::Subscriber data_event() override { return EventSubscriber{data_}; } + ModemOperation* SetPowerSaveParam(ModemPowerSaveParam const&) override { + return nullptr; + } + ModemOperation* PowerOff() override { return nullptr; } + int closed{}; + int rejected{}; + + private: + OpenedOperation opened_; + DataEvent data_; +}; + +void test_RemoteCloseDefersNotificationAndDoesNotCloseReusedId() { + TestContext context; + StoppingDriver driver; + Endpoint endpoint{}; + endpoint.protocol = Protocol::kTcp; + auto transport = std::make_unique(context, driver, endpoint); + int updates = 0; + Subscription update = transport->stream_update_event().Subscribe([&]() { + ++updates; + TEST_ASSERT_TRUE(transport->stream_info().link_state == + LinkState::kLinkError); + update.Reset(); + transport.reset(); + }); + driver.RemoteClose(0); + TEST_ASSERT_TRUE(transport->stream_info().is_writable); + driver.RemoteClose(1); + TEST_ASSERT_FALSE(transport->stream_info().is_writable); + transport->Write(DataBuffer{1, 2, 3}); + TEST_ASSERT_EQUAL_INT(0, driver.rejected); + TEST_ASSERT_EQUAL_INT(0, updates); + driver.RemoteClose(1); + for (int i = 0; i < 8; ++i) { + context.scheduler.Update(Now()); + } + TEST_ASSERT_NULL(transport.get()); + TEST_ASSERT_EQUAL_INT(1, updates); + TEST_ASSERT_EQUAL_INT(0, driver.closed); +} + +void CheckFailureDestroysTransportAfterWriteFinishes(Protocol protocol) { + TestContext context; + StoppingDriver driver; + Endpoint endpoint{}; + endpoint.protocol = protocol; + endpoint.port = 9000; + auto transport = std::make_unique(context, driver, endpoint); + bool finished = false; + bool finished_before_disconnect = false; + int updates = 0; + Subscription update_sub = transport->stream_update_event().Subscribe([&]() { + ++updates; + finished_before_disconnect = finished; + // Unsubscribe before deletion to isolate the write action lifetime bug. + update_sub.Reset(); + if (finished) { + transport.reset(); + } + }); + auto& write = transport->Write(DataBuffer{1, 2, 3}); + Subscription finished_sub = + write.finished_event().Subscribe([&]() { finished = true; }); + for (int i = 0; i < 8; ++i) { + context.scheduler.Update(Now()); + } + // On the old implementation, retain the transport until Finish returns so + // this regression reports the bad ordering instead of crashing the suite. + auto destroyed_on_disconnect = transport == nullptr; + transport.reset(); + TEST_ASSERT_TRUE(finished_before_disconnect); + TEST_ASSERT_TRUE(destroyed_on_disconnect); + TEST_ASSERT_NULL(transport.get()); + TEST_ASSERT_EQUAL_INT(1, driver.rejected); + TEST_ASSERT_EQUAL_INT(1, driver.closed); + TEST_ASSERT_EQUAL_INT(1, updates); +} +void test_UdpShutdownFailureFinishesBeforeTransportDestruction() { + CheckFailureDestroysTransportAfterWriteFinishes(Protocol::kUdp); +} +void test_TcpShutdownFailureFinishesBeforeTransportDestruction() { + CheckFailureDestroysTransportAfterWriteFinishes(Protocol::kTcp); +} +void test_DestructionCancelsPendingDisconnectWithoutNotifyingAgain() { + TestContext context; + StoppingDriver driver; + Endpoint endpoint{}; + endpoint.protocol = Protocol::kUdp; + auto transport = std::make_unique(context, driver, endpoint); + int updates = 0; + Subscription update_sub = + transport->stream_update_event().Subscribe([&]() { ++updates; }); + auto& write = transport->Write(DataBuffer{1}); + bool finished = false; + Subscription finished_sub = + write.finished_event().Subscribe([&]() { finished = true; }); + for (int i = 0; i < 8 && !finished; ++i) { + context.scheduler.Update(Now()); + } + TEST_ASSERT_TRUE(finished); + TEST_ASSERT_EQUAL_INT(0, updates); + transport.reset(); + for (int i = 0; i < 8; ++i) { + context.scheduler.Update(Now()); + } + TEST_ASSERT_EQUAL_INT(0, updates); + TEST_ASSERT_EQUAL_INT(1, driver.closed); +} +} // namespace ae::test_modem_transport_shutdown +#endif +int test_modem_transport_shutdown() { +#if AE_SUPPORT_MODEMS + using namespace ae::test_modem_transport_shutdown; // NOLINT: Unity suite + // entry. + UNITY_BEGIN(); + RUN_TEST(test_RemoteCloseDefersNotificationAndDoesNotCloseReusedId); + RUN_TEST(test_ConnectedTransportCanDisconnectWhileBuilderRemainsAlive); + RUN_TEST(test_ConnectionErrorCompletesOnlyOnce); + RUN_TEST(test_AlreadyFailedTransportCompletesImmediately); + RUN_TEST(test_UdpShutdownFailureFinishesBeforeTransportDestruction); + RUN_TEST(test_TcpShutdownFailureFinishesBeforeTransportDestruction); + RUN_TEST(test_DestructionCancelsPendingDisconnectWithoutNotifyingAgain); + return UNITY_END(); +#else + return 0; +#endif +} diff --git a/tests/test-serial-port/test-sim7070.cpp b/tests/test-serial-port/test-sim7070.cpp new file mode 100644 index 000000000..45b1f4dfe --- /dev/null +++ b/tests/test-serial-port/test-sim7070.cpp @@ -0,0 +1,349 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include +#include "aether/config.h" +#if AE_SUPPORT_MODEMS && AE_ENABLE_SIM7070 +# include "aether/clock.h" +# include "aether/modems/sim7070_at_modem.h" +# include "tests/test-serial-port/mock-serial-port.h" + +namespace ae { +struct Sim7070AtModemTestAccess { + static std::unique_ptr Create( + AeContext const& context, std::unique_ptr serial) { + return std::unique_ptr{ + new Sim7070AtModem{context, ModemInit{}, std::move(serial)}}; + } + static void Connected(Sim7070AtModem& modem) { + modem.started_ = true; + modem.connections_ = {0, 1}; + modem.SetupPoll(); + } +}; +namespace test_sim7070 { +struct TestContext { + AeCtx ToAeContext() const { + static constexpr auto table = + AeCtxTable{nullptr, [](void* obj) -> TaskScheduler& { + return static_cast(obj)->scheduler; + }}; + return AeCtx{const_cast(this), &table}; // NOLINT + } + TaskScheduler scheduler; +}; +struct Fixture { + Fixture() { + auto port = std::make_unique(); + serial = port.get(); + writes = serial->write_event().Subscribe([this](auto data) { + commands.emplace_back(reinterpret_cast(data.data()), + data.size()); + }); + modem = Sim7070AtModemTestAccess::Create(context, std::move(port)); + Pump(); + Reply("OK\r\n"); + Reply("OK\r\n"); + Reply("OK\r\n"); + commands.clear(); + } + void Pump() { + for (int i = 0; i < 8; ++i) { + context.scheduler.Update(now); + } + } + void Reply(std::string_view response) { + serial->WriteOut(std::span{ + reinterpret_cast(response.data()), + response.size()}); + Pump(); + } + void Expect(char const* command) { + TEST_ASSERT_FALSE(commands.empty()); + TEST_ASSERT_EQUAL_STRING(command, commands.back().c_str()); + } + TestContext context; + TimePoint now{Now()}; + tests::MockSerialPort* serial{}; + std::vector commands; + Subscription writes; + std::unique_ptr modem; +}; + +void test_RemoteCloseNotifiesOnlyTrackedConnectionOnce() { + Fixture f; + Sim7070AtModemTestAccess::Connected(*f.modem); + std::vector closed; + Subscription subscription = f.modem->connection_closed_event().Subscribe( + [&](ConnectionIndex connection) { closed.push_back(connection); }); + f.Reply( + "+CASTATE: 0,1\r\n+CASTATE: 8,0\r\n+CASTATE: 256,0\r\n" + "+CASTATE: invalid\r\n"); + TEST_ASSERT_TRUE(closed.empty()); + f.Reply("+CASTATE: 0,0\r\n+CASTATE: 0,0\r\n+CASTATE: 1,0\r\n"); + TEST_ASSERT_EQUAL_UINT(2, closed.size()); + TEST_ASSERT_EQUAL_INT(0, closed[0]); + TEST_ASSERT_EQUAL_INT(1, closed[1]); + TEST_ASSERT_TRUE(f.commands.empty()); +} + +void test_StopClosesSocketsAndWaitsForContextDeactivation() { + Fixture f; + Sim7070AtModemTestAccess::Connected(*f.modem); + auto* stop = f.modem->Stop(); + bool closed_at_result = false; + Subscription stopped = stop->result_event().Subscribe( + [&](auto const&) { closed_at_result = !f.serial->IsOpen(); }); + TEST_ASSERT_EQUAL_PTR(stop, f.modem->Stop()); + TEST_ASSERT_NULL(f.modem->OpenNetwork(Protocol::kTcp, "127.0.0.1", 9000)); + TEST_ASSERT_NULL(f.modem->WritePacket(0, {})); + TEST_ASSERT_NULL(f.modem->Start()); + f.Pump(); + f.Expect("AT+CACLOSE=0\r\n"); + f.Reply("+CADATAIND: 0\r\nOK\r\n"); + f.Expect("AT+CACLOSE=1\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CNACT?\r\n"); + f.Reply("+CNACT: 0,1,\"10.0.0.1\"\r\nOK\r\n"); + f.Expect("AT+CNACT=0,0\r\n"); + f.Reply("+APP PDP: 0,DEACTIVE\r\n"); + TEST_ASSERT_FALSE(stop->is_finished()); + f.Expect("AT+CNACT=0,0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CPOWD=1\r\n"); + TEST_ASSERT_FALSE(stop->is_finished()); + TEST_ASSERT_TRUE(f.serial->IsOpen()); + f.Reply("OK\r\n"); + TEST_ASSERT_FALSE(stop->is_finished()); + TEST_ASSERT_TRUE(f.serial->IsOpen()); + f.Reply("NORMAL POWER DOWN\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_TRUE(closed_at_result); + TEST_ASSERT_TRUE(stop->result()->IsOk()); + TEST_ASSERT_EQUAL_PTR(stop, f.modem->Stop()); + TEST_ASSERT_EQUAL_PTR(stop, f.modem->CloseNetwork(0)); + f.Pump(); + TEST_ASSERT_EQUAL_UINT(5, f.commands.size()); +} + +void test_StopContinuesAfterCloseErrorAndDeactivationTimeout() { + Fixture f; + Sim7070AtModemTestAccess::Connected(*f.modem); + auto* stop = f.modem->Stop(); + f.Pump(); + f.Expect("AT+CACLOSE=0\r\n"); + f.Reply("ERROR\r\n"); + f.Expect("AT+CACLOSE=1\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CNACT?\r\n"); + f.Reply("+CNACT: 0,1,\"10.0.0.1\"\r\nOK\r\n"); + f.Expect("AT+CNACT=0,0\r\n"); + f.Reply("OK\r\n"); + f.context.scheduler.Update(f.now + std::chrono::seconds{11}); + f.now = Now(); + f.Pump(); + f.Expect("AT+CPOWD=1\r\n"); + f.Reply("NORMAL POWER DOWN\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_FALSE(stop->result()->IsOk()); +} + +void test_StopSkipsAlreadyInactiveContext() { + Fixture f; + auto* stop = f.modem->Stop(); + f.Pump(); + f.Expect("AT+CNACT?\r\n"); + f.Reply("+CNACT: 0,0,\"0.0.0.0\"\r\nOK\r\n"); + f.Expect("AT+CPOWD=1\r\n"); + f.Reply("NORMAL POWER DOWN\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_TRUE(stop->result()->IsOk()); + TEST_ASSERT_EQUAL_UINT(2, f.commands.size()); +} + +void test_StopPowerOffTimeoutClosesSerialAndReportsError() { + Fixture f; + auto* stop = f.modem->Stop(); + f.Pump(); + f.Reply("+CNACT: 0,0,\"0.0.0.0\"\r\nOK\r\n"); + f.Expect("AT+CPOWD=1\r\n"); + TEST_ASSERT_FALSE(stop->is_finished()); + f.now += std::chrono::seconds{31}; + f.Pump(); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(stop->result()->IsOk()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_EQUAL_UINT(2, f.commands.size()); +} + +void test_PowerOffWaitsForNormalPowerDownWithoutEnablingRadio() { + Fixture f; + auto* power_off = f.modem->PowerOff(); + f.Pump(); + f.Expect("AT+CPOWD=1\r\n"); + f.Reply("OK\r\n"); + TEST_ASSERT_FALSE(power_off->is_finished()); + f.Reply("NORMAL POWER DOWN\r\n"); + TEST_ASSERT_TRUE(power_off->is_finished()); + TEST_ASSERT_TRUE(power_off->result()->IsOk()); + TEST_ASSERT_EQUAL_UINT(1, f.commands.size()); +} + +void test_StopPowerOffErrorFinishes() { + Fixture f; + auto* stop = f.modem->Stop(); + f.Pump(); + f.Reply("+CNACT: 0,0,\"0.0.0.0\"\r\nOK\r\n"); + f.Expect("AT+CPOWD=1\r\n"); + f.Reply("ERROR\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_FALSE(stop->result()->IsOk()); +} + +void test_StartActivatesContextBeforeShutdown() { + Fixture f; + auto* start = f.modem->Start(); + TEST_ASSERT_NOT_NULL(start); + f.Pump(); + f.Expect("AT+CFUN=1,0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CPIN?\r\n"); + f.Reply("+CPIN: READY\r\nOK\r\n"); + f.Expect("AT+IPR=115200\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CNMP=2\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CREG=1;+CGREG=1;+CEREG=1\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CEREG?\r\n"); + f.Reply("+CEREG: 1,0\r\nOK\r\n"); + f.Expect("AT+COPS=0\r\n"); + f.Reply("+CME ERROR: 13\r\n"); + TEST_ASSERT_FALSE(start->is_finished()); + f.context.scheduler.Update(Now() + std::chrono::milliseconds{1100}); + f.now = Now(); + f.Pump(); + f.Expect("AT+COPS=0\r\n"); + TEST_ASSERT_FALSE(start->is_finished()); + f.Reply("OK\r\n"); + f.Expect("AT+CEREG?\r\n"); + f.Reply("+CEREG: 1,1\r\nOK\r\n"); + f.Expect("AT+CGDCONT=1,\"IP\",\"\"\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CNCFG=0,0,\"\",\"\",\"\",0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CNACT?\r\n"); + f.Reply("+CNACT: 0,0,\"0.0.0.0\"\r\nOK\r\n"); + f.Expect("AT+CNACT=0,1\r\n"); + f.Reply("OK\r\n+APP PDP: 0,ACTIVE\r\n"); + f.Expect("AT+CASTATE?\r\n"); + f.Reply("+CASTATE: 0,1\r\nOK\r\n"); + f.Expect("AT+CACLOSE=0\r\n"); + f.Reply("OK\r\n"); + TEST_ASSERT_TRUE(start->is_finished()); + TEST_ASSERT_TRUE(start->result()->IsOk()); + auto* stop = f.modem->Stop(); + f.Pump(); + f.Expect("AT+CNACT?\r\n"); + f.Reply("+CNACT: 0,1,\"10.0.0.1\"\r\nOK\r\n"); + f.Expect("AT+CNACT=0,0\r\n"); + f.Reply("OK\r\n+APP PDP: 0,DEACTIVE\r\n"); + f.Expect("AT+CPOWD=1\r\n"); + f.Reply("NORMAL POWER DOWN\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_TRUE(stop->result()->IsOk()); +} + +void test_StartDoesNotQuerySimWhenEnablingModemFails() { + Fixture f; + auto* start = f.modem->Start(); + f.Pump(); + f.Expect("AT+CFUN=1,0\r\n"); + f.Reply("ERROR\r\n"); + TEST_ASSERT_TRUE(start->is_finished()); + TEST_ASSERT_FALSE(start->result()->IsOk()); + TEST_ASSERT_EQUAL_UINT(1, f.commands.size()); +} + +void test_StartRetriesSimWhileInterfaceIsWakingUp() { + Fixture f; + auto* start = f.modem->Start(); + f.Pump(); + f.Expect("AT+CFUN=1,0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CPIN?\r\n"); + f.Reply("+CME ERROR: 13\r\n"); + TEST_ASSERT_FALSE(start->is_finished()); + TEST_ASSERT_EQUAL_UINT(2, f.commands.size()); + f.context.scheduler.Update(Now() + std::chrono::milliseconds{600}); + f.now = Now(); + f.Pump(); + TEST_ASSERT_EQUAL_UINT(3, f.commands.size()); + f.Expect("AT+CPIN?\r\n"); + f.Reply("+CPIN: READY\r\nOK\r\n"); + f.Expect("AT+IPR=115200\r\n"); + f.Reply("ERROR\r\n"); + TEST_ASSERT_TRUE(start->is_finished()); +} + +void test_StartSimReadinessTimeoutFinishes() { + Fixture f; + auto* start = f.modem->Start(); + f.Pump(); + f.Reply("OK\r\n"); + f.Expect("AT+CPIN?\r\n"); + f.context.scheduler.Update(Now() + std::chrono::seconds{16}); + f.now = Now(); + f.Pump(); + TEST_ASSERT_TRUE(start->is_finished()); + TEST_ASSERT_FALSE(start->result()->IsOk()); + TEST_ASSERT_EQUAL_UINT(2, f.commands.size()); +} +} // namespace test_sim7070 +} // namespace ae +#endif + +int test_sim7070() { +#if AE_SUPPORT_MODEMS && AE_ENABLE_SIM7070 + using namespace ae::test_sim7070; // NOLINT: Unity suite entry. + UNITY_BEGIN(); + RUN_TEST(test_RemoteCloseNotifiesOnlyTrackedConnectionOnce); + RUN_TEST(test_StopClosesSocketsAndWaitsForContextDeactivation); + RUN_TEST(test_StopContinuesAfterCloseErrorAndDeactivationTimeout); + RUN_TEST(test_StopSkipsAlreadyInactiveContext); + RUN_TEST(test_StopPowerOffErrorFinishes); + RUN_TEST(test_StopPowerOffTimeoutClosesSerialAndReportsError); + RUN_TEST(test_PowerOffWaitsForNormalPowerDownWithoutEnablingRadio); + RUN_TEST(test_StartActivatesContextBeforeShutdown); + RUN_TEST(test_StartDoesNotQuerySimWhenEnablingModemFails); + RUN_TEST(test_StartRetriesSimWhileInterfaceIsWakingUp); + RUN_TEST(test_StartSimReadinessTimeoutFinishes); + return UNITY_END(); +#else + return 0; +#endif +} diff --git a/tests/test-serial-port/test-thingy91x.cpp b/tests/test-serial-port/test-thingy91x.cpp new file mode 100644 index 000000000..adc815776 --- /dev/null +++ b/tests/test-serial-port/test-thingy91x.cpp @@ -0,0 +1,301 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "aether/config.h" + +#if AE_SUPPORT_MODEMS && AE_ENABLE_THINGY91X +# include "aether/clock.h" +# include "aether/modems/thingy91x_at_modem.h" +# include "tests/test-serial-port/mock-serial-port.h" + +namespace ae { +struct Thingy91xAtModemTestAccess { + static void Connected(Thingy91xAtModem& modem) { + modem.started_ = true; + modem.connections_ = {4, 5}; + modem.SetupPoll(); + } + static std::unique_ptr Create( + AeContext const& context, std::unique_ptr serial, + kModemMode mode) { + auto init = ModemInit{}; + init.modem_mode = mode; + return std::unique_ptr{ + new Thingy91xAtModem{context, std::move(init), std::move(serial)}}; + } +}; + +namespace test_thingy91x { +struct TestContext { + AeCtx ToAeContext() const { + static constexpr auto table = + AeCtxTable{nullptr, [](void* obj) -> TaskScheduler& { + return static_cast(obj)->scheduler; + }}; + return AeCtx{const_cast(this), &table}; // NOLINT + } + TaskScheduler scheduler; +}; + +struct Fixture { + explicit Fixture(kModemMode mode = kModemMode::kModeAuto) { + auto port = std::make_unique(); + serial = port.get(); + writes = serial->write_event().Subscribe([this](auto data) { + commands.emplace_back(reinterpret_cast(data.data()), + data.size()); + }); + modem = Thingy91xAtModemTestAccess::Create(context, std::move(port), mode); + Pump(); + Reply("OK\r\n"); + Reply("OK\r\n"); + commands.clear(); + } + + void Pump() { + for (int i = 0; i < 8; ++i) { + context.scheduler.Update(now); + } + } + void Reply(std::string_view response) { + serial->WriteOut(std::span{ + reinterpret_cast(response.data()), + response.size()}); + Pump(); + } + void Open() { + auto* operation = modem->OpenNetwork(Protocol::kTcp, "127.0.0.1", 9000); + TEST_ASSERT_NOT_NULL(operation); + result_sub = operation->result_event().Subscribe( + [this](auto res) { result.emplace(std::move(res)); }); + Pump(); + Expect("AT#XSOCKET=1,1,0\r\n"); + } + void Connect() { + Open(); + Reply("#XSOCKET: 4,1,6\r\nOK\r\n"); + Expect("AT#XSOCKETSELECT=4\r\n"); + Reply("OK\r\n"); + Expect("AT#XSOCKETOPT=1,20,30\r\n"); + Reply("OK\r\n"); + Expect("AT#XCONNECT=\"127.0.0.1\",9000\r\n"); + } + void Expect(char const* command) { + TEST_ASSERT_FALSE(commands.empty()); + TEST_ASSERT_EQUAL_STRING(command, commands.back().c_str()); + } + void CompleteRollback(int error) { + Expect("AT#XSOCKETSELECT=4\r\n"); + TEST_ASSERT_FALSE(result.has_value()); + Reply("OK\r\n"); + Expect("AT#XSOCKET=0\r\n"); + TEST_ASSERT_FALSE(result.has_value()); + Reply("#XSOCKET: 4,0\r\nOK\r\n"); + TEST_ASSERT_TRUE(result.has_value()); + TEST_ASSERT_FALSE(result->IsOk()); + TEST_ASSERT_EQUAL_INT(error, static_cast(result->error())); + } + + TestContext context; + TimePoint now{Now()}; + tests::MockSerialPort* serial{}; + std::vector commands; + std::optional result; + Subscription writes; + Subscription result_sub; + std::unique_ptr modem; +}; + +void CheckStartMode(kModemMode mode, char const* command) { + Fixture f{mode}; + auto* start = f.modem->Start(); + TEST_ASSERT_NOT_NULL(start); + f.Pump(); + f.Expect("AT+CFUN=0\r\n"); + f.Reply("OK\r\n"); + f.Expect(command); + // A rejected mode must finish with an error before dependent AT commands. + f.Reply("ERROR\r\n"); + TEST_ASSERT_TRUE(start->is_finished()); + TEST_ASSERT_TRUE(start->result().has_value()); + TEST_ASSERT_TRUE(start->result()->IsErr()); + f.Expect(command); +} + +void test_StartSelectsNbIot() { + CheckStartMode(kModemMode::kModeNbIot, "AT%XSYSTEMMODE=0,1,0,2\r\n"); +} + +void test_StartSelectsLteM() { + CheckStartMode(kModemMode::kModeCatM, "AT%XSYSTEMMODE=1,0,0,1\r\n"); +} + +void test_CreateFailureDoesNotCloseAnotherSocket() { + Fixture f; + f.Open(); + f.Reply("ERROR\r\n"); + TEST_ASSERT_TRUE(f.result.has_value()); + TEST_ASSERT_FALSE(f.result->IsOk()); + TEST_ASSERT_EQUAL_UINT(1, f.commands.size()); +} + +void test_ConnectFailureClosesSocketBeforeReportingError() { + Fixture f; + f.Connect(); + f.Reply("ERROR\r\n"); + f.CompleteRollback(-1); +} + +void test_ConnectTimeoutClosesSocketBeforeReportingError() { + Fixture f; + f.Connect(); + f.context.scheduler.Update(f.now + std::chrono::seconds{11}); + f.now = Now(); + f.Pump(); + f.CompleteRollback(-2); +} + +void test_RollbackSelectFailureDoesNotCloseAnotherSocket() { + Fixture f; + f.Connect(); + f.Reply("ERROR\r\n"); + f.Expect("AT#XSOCKETSELECT=4\r\n"); + f.Reply("ERROR\r\n"); + TEST_ASSERT_TRUE(f.result.has_value()); + TEST_ASSERT_FALSE(f.result->IsOk()); + TEST_ASSERT_EQUAL_INT(-1, static_cast(f.result->error())); + f.Expect("AT#XSOCKETSELECT=4\r\n"); +} + +void test_StopClosesSocketsBeforeDeactivation() { + Fixture f; + Thingy91xAtModemTestAccess::Connected(*f.modem); + auto* stop = f.modem->Stop(); + bool closed_at_result = false; + Subscription stopped = stop->result_event().Subscribe( + [&](auto const&) { closed_at_result = !f.serial->IsOpen(); }); + TEST_ASSERT_EQUAL_PTR(stop, f.modem->Stop()); + TEST_ASSERT_NULL(f.modem->OpenNetwork(Protocol::kTcp, "127.0.0.1", 9000)); + TEST_ASSERT_NULL(f.modem->WritePacket(4, {})); + TEST_ASSERT_NULL(f.modem->Start()); + f.Pump(); + f.Expect("AT#XSOCKETSELECT=4\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT#XSOCKET=0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT#XSOCKETSELECT=5\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT#XSOCKET=0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CFUN=0\r\n"); + TEST_ASSERT_FALSE(stop->is_finished()); + TEST_ASSERT_TRUE(f.serial->IsOpen()); + f.Reply("OK\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_TRUE(closed_at_result); + TEST_ASSERT_TRUE(stop->result()->IsOk()); + TEST_ASSERT_EQUAL_PTR(stop, f.modem->Stop()); + TEST_ASSERT_EQUAL_PTR(stop, f.modem->CloseNetwork(4)); + f.context.scheduler.Update(f.now + std::chrono::seconds{1}); + f.Pump(); + TEST_ASSERT_EQUAL_UINT(5, f.commands.size()); +} + +void test_StopContinuesAfterSelectErrorAndCloseTimeout() { + Fixture f; + Thingy91xAtModemTestAccess::Connected(*f.modem); + auto* stop = f.modem->Stop(); + f.Pump(); + f.Expect("AT#XSOCKETSELECT=4\r\n"); + f.Reply("ERROR\r\n"); + f.Expect("AT#XSOCKETSELECT=5\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT#XSOCKET=0\r\n"); + f.context.scheduler.Update(f.now + std::chrono::seconds{11}); + f.now = Now(); + f.Pump(); + f.Expect("AT+CFUN=0\r\n"); + TEST_ASSERT_FALSE(stop->is_finished()); + f.Reply("OK\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_FALSE(stop->result()->IsOk()); + TEST_ASSERT_EQUAL_UINT(4, f.commands.size()); +} + +void test_StopDeactivationTimeoutFinishes() { + Fixture f; + auto* stop = f.modem->Stop(); + f.Pump(); + f.Expect("AT+CFUN=0\r\n"); + f.context.scheduler.Update(f.now + std::chrono::seconds{31}); + f.now = Now(); + f.Pump(); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_FALSE(stop->result()->IsOk()); +} + +void test_StopIncludesSocketOpenedByPendingOperation() { + Fixture f; + f.Connect(); + auto* stop = f.modem->Stop(); + f.Reply("OK\r\n"); + TEST_ASSERT_TRUE(f.result.has_value()); + TEST_ASSERT_TRUE(f.result->IsOk()); + f.Expect("AT#XSOCKETSELECT=4\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT#XSOCKET=0\r\n"); + f.Reply("OK\r\n"); + f.Expect("AT+CFUN=0\r\n"); + f.Reply("OK\r\n"); + TEST_ASSERT_TRUE(stop->is_finished()); + TEST_ASSERT_FALSE(f.serial->IsOpen()); + TEST_ASSERT_TRUE(stop->result()->IsOk()); +} +} // namespace test_thingy91x +} // namespace ae +#endif + +int test_thingy91x() { +#if AE_SUPPORT_MODEMS && AE_ENABLE_THINGY91X + using namespace ae::test_thingy91x; // NOLINT: Unity suite entry. + UNITY_BEGIN(); + RUN_TEST(test_StartSelectsNbIot); + RUN_TEST(test_StartSelectsLteM); + RUN_TEST(test_CreateFailureDoesNotCloseAnotherSocket); + RUN_TEST(test_ConnectFailureClosesSocketBeforeReportingError); + RUN_TEST(test_ConnectTimeoutClosesSocketBeforeReportingError); + RUN_TEST(test_RollbackSelectFailureDoesNotCloseAnotherSocket); + RUN_TEST(test_StopClosesSocketsBeforeDeactivation); + RUN_TEST(test_StopContinuesAfterSelectErrorAndCloseTimeout); + RUN_TEST(test_StopDeactivationTimeoutFinishes); + RUN_TEST(test_StopIncludesSocketOpenedByPendingOperation); + return UNITY_END(); +#else + return 0; +#endif +} diff --git a/tests/test-serial-port/test-win-serial-reopen.cpp b/tests/test-serial-port/test-win-serial-reopen.cpp new file mode 100644 index 000000000..3ccda6245 --- /dev/null +++ b/tests/test-serial-port/test-win-serial-reopen.cpp @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "aether/config.h" + +#if defined(_WIN32) +# include "aether-objects/domain_storage/ram_domain_storage.h" +# include "aether-objects/obj/domain.h" +# include "aether/serial_ports/win_serial_port.h" + +namespace ae::test_win_serial_reopen { +struct TestContext { + AeCtx ToAeContext() const { + static constexpr auto table = + AeCtxTable{nullptr, [](void* obj) -> TaskScheduler& { + return static_cast(obj)->scheduler; + }}; + return AeCtx{this, &table}; + } + TaskScheduler scheduler; +}; +} // namespace ae::test_win_serial_reopen +#endif + +// Opt-in hardware check: opens the port without sending any AT commands. +int test_win_serial_reopen(char const* port) { +#if defined(_WIN32) + ae::test_win_serial_reopen::TestContext context; + ae::RamDomainStorage storage; + ae::Domain domain{storage}; + ae::SerialInit init; + init.port_name = port; + for (int i = 0; i < 100; ++i) { + ae::IPoller::ptr poller = ae::WinPoller::ptr::Create(domain); + ae::WinSerialPort serial{context, init, poller}; + if (!serial.IsOpen()) { + std::cerr << "Serial reopen failed at iteration " << i << '\n'; + return 1; + } + serial.Close(); + serial.Close(); + if (serial.IsOpen()) { + std::cerr << "Closed serial port still reports open\n"; + return 1; + } + // The old port object is still alive, just like a retained modem driver. + // The prepared send stage uses a separate context and poller. + ae::IPoller::ptr next_poller = ae::WinPoller::ptr::Create(domain); + ae::WinSerialPort reopened{context, init, next_poller}; + if (!reopened.IsOpen()) { + std::cerr << "Reopen with old object alive failed at iteration " << i + << '\n'; + return 1; + } + } + std::cout << "100 immediate serial reopen cycles passed\n"; + return 0; +#else + (void)port; + std::cerr << "Serial reopen check requires Windows\n"; + return 2; +#endif +} diff --git a/tests/test-server-connection/test_cloud_quarantine_loop.cpp b/tests/test-server-connection/test_cloud_quarantine_loop.cpp index 315fab312..e7acc717e 100644 --- a/tests/test-server-connection/test_cloud_quarantine_loop.cpp +++ b/tests/test-server-connection/test_cloud_quarantine_loop.cpp @@ -147,6 +147,21 @@ struct CloudFixture { std::unique_ptr connections; }; +void test_StopCancelsQuarantineReconnect() { + auto factory = std::make_unique(); + auto* raw = factory.get(); + CloudFixture f{std::move(factory), raw}; + TEST_ASSERT_TRUE(f.AnyQuarantined()); + auto attempts = raw->attempts; + f.connections->Stop(); + f.connections->Stop(); + f.connections->Restream(); + f.ctx.PumpAt(Now() + std::chrono::milliseconds{ + AE_CLOUD_SERVER_QUARANTINE_TIME_MS * 2}); + TEST_ASSERT_EQUAL_INT(attempts, raw->attempts); + TEST_ASSERT_TRUE(f.connections->selected_servers().empty()); +} + void AssertCanonicalPriorities(CloudServerConnections& connections) { auto const& servers = connections.servers(); for (std::size_t i = 0; i < servers.size(); ++i) { @@ -366,6 +381,7 @@ int run_test_cloud_quarantine_loop() { using namespace ae::test_cloud_quarantine_loop; // NOLINT UNITY_BEGIN(); + RUN_TEST(test_StopCancelsQuarantineReconnect); RUN_TEST(test_CloudQuarantineAndReleasePreserveCanonicalOrder); RUN_TEST(test_CloudQuarantineDoesNotBusyLoop); RUN_TEST(test_CloudImmediateUnusableCandidatesUseQuarantinePath); diff --git a/tests/test-server-connection/test_registration_root_server_select.cpp b/tests/test-server-connection/test_registration_root_server_select.cpp index d30aee28b..00cf62cee 100644 --- a/tests/test-server-connection/test_registration_root_server_select.cpp +++ b/tests/test-server-connection/test_registration_root_server_select.cpp @@ -14,7 +14,10 @@ * limitations under the License. */ +#include #include +#include +#include #include #include @@ -29,6 +32,8 @@ # include "aether/adapter_registry.h" # include "aether/ae_context.h" # include "aether/aether.h" +# include "aether/channels/channel.h" +# include "aether/clock.h" # include "aether/registration/root_server_select_stream.h" # include "aether/registration_cloud.h" # include "aether/server.h" @@ -49,6 +54,54 @@ struct RootServerSelectStreamTestAccess { namespace test_registration_root_server_select { +class RegistrationTestStream final : public ByteIStream { + public: + explicit RegistrationTestStream(int& destroyed) : destroyed_{&destroyed} {} + ~RegistrationTestStream() override { ++*destroyed_; } + WriteAction& Write(DataBuffer&&) override { return write_; } + StreamInfo stream_info() const override { + return StreamInfo{512, 1024, true, LinkState::kLinked, true}; + } + StreamUpdateEvent::Subscriber stream_update_event() override { + return EventSubscriber{updates_}; + } + OutDataEvent::Subscriber out_data_event() override { + return EventSubscriber{data_}; + } + void Restream() override {} + + private: + int* destroyed_; + WriteAction write_; + StreamUpdateEvent updates_; + OutDataEvent data_; +}; + +class RegistrationTestChannel final : public Channel { + AE_OBJECT(RegistrationTestChannel, Channel, 0) + + protected: + RegistrationTestChannel() = default; + + public: + RegistrationTestChannel(ObjProp prop, int& destroyed) + : Channel{prop}, destroyed_{&destroyed} {} + AE_OBJECT_REFLECT() + + TransportBuildSender TransportBuilder() override { + return ex::just(std::unique_ptr{ + std::make_unique(*destroyed_)}); + } + Duration TransportBuildTimeout() const override { + return std::chrono::seconds{1}; + } + Duration ResponseTimeout() const override { return std::chrono::seconds{1}; } + std::optional endpoint() const override { return std::nullopt; } + + private: + int* destroyed_{}; +}; + struct TestContext { AeCtx ToAeContext() const { static constexpr auto table = @@ -119,6 +172,30 @@ void test_RegistrationServersFailOverByNextPriority() { TEST_ASSERT_EQUAL_INT(1, cloud_errors); } +void test_DisconnectReleasesTransportWhileRegistrationStreamLives() { + int destroyed = 0; + Fixture f; + auto server = f.AddServer(ServerId{0}, 0); + auto channel = + RegistrationTestChannel::ptr::Create(CreateWith{f.domain}, destroyed); + server->channels.emplace_back(channel); + RootServerSelectStream stream{f.ae_context, f.cloud.Load()}; + for (int i = 0; i < 8; ++i) { + f.context.scheduler.Update(Now()); + } + TEST_ASSERT_TRUE(stream.stream_info().is_writable); + TEST_ASSERT_EQUAL_INT(0, destroyed); + + stream.Disconnect(); + TEST_ASSERT_EQUAL_INT(1, destroyed); + TEST_ASSERT_FALSE(stream.stream_info().is_writable); + stream.Disconnect(); + for (int i = 0; i < 8; ++i) { + f.context.scheduler.Update(Now()); + } + TEST_ASSERT_EQUAL_INT(1, destroyed); +} + } // namespace test_registration_root_server_select } // namespace ae @@ -128,6 +205,7 @@ int run_test_registration_root_server_select() { UNITY_BEGIN(); RUN_TEST(test_SingleRegistrationServerSelectsPriorityZero); RUN_TEST(test_RegistrationServersFailOverByNextPriority); + RUN_TEST(test_DisconnectReleasesTransportWhileRegistrationStreamLives); return UNITY_END(); } diff --git a/tests/test-server-connection/test_server_connection_recovery.cpp b/tests/test-server-connection/test_server_connection_recovery.cpp index 57d92e775..4fa2c1462 100644 --- a/tests/test-server-connection/test_server_connection_recovery.cpp +++ b/tests/test-server-connection/test_server_connection_recovery.cpp @@ -343,6 +343,51 @@ void test_FullPoolStillReachesLinkError() { TEST_ASSERT_EQUAL(1, builds); } +void test_StopCancelsPendingChannelFallback() { + TestContext ctx; + AeContext ae_ctx{ctx}; + RamDomainStorage storage; + Domain domain{storage}; + int builds = 0; + FakeBuildPolicy policy{FakeBuildPolicy::Mode::kFailThenSucceed, &builds, + &ae_ctx}; + auto first = FakeChannel::ptr::Create(CreateWith{domain}, policy); + auto second = FakeChannel::ptr::Create(CreateWith{domain}, policy); + auto server = MakeServerWithChannels(domain, {first, second}); + ServerConnection connection{ae_ctx, server.Load()}; + TEST_ASSERT_EQUAL_INT(1, builds); + connection.Stop(); + connection.Restream(); + connection.Stop(); + ctx.Pump(); + TEST_ASSERT_EQUAL_INT(1, builds); + TEST_ASSERT_FALSE(connection.stream_info().is_writable); + TEST_ASSERT_TRUE(connection.stream_info().link_state == LinkState::kUnlinked); +} + +void test_StopClosesLinkedConnectionWithoutFailover() { + TestContext ctx; + AeContext ae_ctx{ctx}; + RamDomainStorage storage; + Domain domain{storage}; + int builds = 0; + FakeBuildPolicy policy{FakeBuildPolicy::Mode::kAlwaysSucceed, &builds, + &ae_ctx}; + auto channel = FakeChannel::ptr::Create(CreateWith{domain}, policy); + auto server = MakeServerWithChannels(domain, {channel}); + ServerConnection connection{ae_ctx, server.Load()}; + TEST_ASSERT_TRUE(connection.stream_info().is_writable); + int errors = 0; + Subscription sub = + connection.server_error_event().Subscribe([&]() { ++errors; }); + connection.Stop(); + connection.Restream(); + ctx.Pump(); + TEST_ASSERT_EQUAL_INT(0, errors); + TEST_ASSERT_EQUAL_INT(1, builds); + TEST_ASSERT_FALSE(connection.stream_info().is_writable); +} + void test_NoBusyLoopOnPermanentFailure() { TestContext ctx; AeContext ae_ctx{ctx}; @@ -368,6 +413,8 @@ void test_NoBusyLoopOnPermanentFailure() { int run_test_server_connection_recovery() { using namespace ae::test_server_connection_recovery; // NOLINT UNITY_BEGIN(); + RUN_TEST(test_StopCancelsPendingChannelFallback); + RUN_TEST(test_StopClosesLinkedConnectionWithoutFailover); RUN_TEST(test_SingleChannelBuildFailureReachesLinkError); RUN_TEST(test_ResultCallbackSeesFinishedAction); RUN_TEST(test_SecondChannelSucceedsAfterFirstFailure); diff --git a/tests/test-transport/test-packet-queue-manager.cpp b/tests/test-transport/test-packet-queue-manager.cpp new file mode 100644 index 000000000..adbe99492 --- /dev/null +++ b/tests/test-transport/test-packet-queue-manager.cpp @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include "aether/ae_context.h" +#include "aether/clock.h" +#include "aether/transport/packet_queue_manager.h" + +namespace ae::test_packet_queue_manager { +struct TestContext { + AeCtx ToAeContext() const { + static constexpr AeCtxTable vtable = { + .aether_getter = nullptr, + .scheduler_getter = [](void* obj) -> TaskScheduler& { + return static_cast(obj)->scheduler; + }, + }; + return AeCtx{ + .obj = const_cast(this), // NOLINT + .vtable = &vtable, + }; + } + + TaskScheduler scheduler; +}; + +class AsyncPacket final : public PacketSendAction { + public: + AsyncPacket(AeContext context, std::vector& sent_packets, int id) + : context_{context}, sent_packets_{&sent_packets}, id_{id} {} + + void Send() override { + if (started_) { + return; + } + started_ = true; + sent_packets_->push_back(id_); + completion_task_ = context_.scheduler().Task([this]() { + done_ = true; + SetStatus(Status::kSuccess); + }); + } + + bool is_done() const override { return done_; } + bool re_enqueue() const override { return false; } + + private: + AeContext context_; + std::vector* sent_packets_; + int id_; + bool started_{}; + bool done_{}; + TaskSubscription completion_task_; +}; + +void test_AsynchronousCompletionSendsNextPacket() { + TestContext context; + auto queue = PacketQueueManager{AeContext{context}}; + std::vector sent_packets; + + TEST_ASSERT_NOT_NULL(queue.AddPacket(AeContext{context}, sent_packets, 1)); + TEST_ASSERT_NOT_NULL(queue.AddPacket(AeContext{context}, sent_packets, 2)); + TEST_ASSERT_NOT_NULL(queue.AddPacket(AeContext{context}, sent_packets, 3)); + + for (std::size_t i = 0; i < 8; ++i) { + context.scheduler.Update(Now()); + } + + TEST_ASSERT_EQUAL_size_t(3, sent_packets.size()); + TEST_ASSERT_EQUAL_INT(1, sent_packets[0]); + TEST_ASSERT_EQUAL_INT(2, sent_packets[1]); + TEST_ASSERT_EQUAL_INT(3, sent_packets[2]); +} +} // namespace ae::test_packet_queue_manager + +int test_packet_queue_manager() { + UNITY_BEGIN(); + RUN_TEST( + ae::test_packet_queue_manager::test_AsynchronousCompletionSendsNextPacket); + return UNITY_END(); +}