From c816d19288ee6a919f797081313d4a68f219207e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:15:17 +0000 Subject: [PATCH 1/4] EvsMirror: mirror events via CFE_EVS_SendEventWithAppID using app ID captured at init CFE_EVS_SendEvent resolves the calling task's application context, but the TextLogger handler runs on F Prime threads with no ES task record, so every mirrored event failed with CFE_EVS_APP_ILLEGAL_APP_ID. Capture the app ID on the app's main task in an init() override and send with it instead. Also sync the ComCfg override with fprime devel (saIndex, FW_PACKET_PARAM) and update the SchAppDriver UT for the new Fw::Buffer setData semantics. Co-Authored-By: michael.d.starch --- FPrimeCfs/EvsMirror/EvsMirror.cpp | 29 +++++++++++-------- FPrimeCfs/EvsMirror/EvsMirror.hpp | 29 +++++++++++++++++++ .../EvsMirror/test/ut/EvsMirrorTester.cpp | 9 +++++- .../EvsMirror/test/ut/EvsMirrorTester.hpp | 3 ++ .../EvsMirror/test/ut/stubs/CfeStubs.cpp | 28 ++++++++++++++++++ .../EvsMirror/test/ut/stubs/CfeStubs.hpp | 10 +++++-- FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h | 6 ++++ .../test/ut/SchAppDriverTester.cpp | 3 +- config/FPrimeCfsConfig/ComCfg.fpp | 6 ++++ 9 files changed, 106 insertions(+), 17 deletions(-) diff --git a/FPrimeCfs/EvsMirror/EvsMirror.cpp b/FPrimeCfs/EvsMirror/EvsMirror.cpp index 1992868..1c77966 100644 --- a/FPrimeCfs/EvsMirror/EvsMirror.cpp +++ b/FPrimeCfs/EvsMirror/EvsMirror.cpp @@ -6,15 +6,6 @@ #include "FPrimeCfs/EvsMirror/EvsMirror.hpp" #include "Fw/Logger/Logger.hpp" -// Some cFE versions (e.g. draco) use compound literals in inline functions, -// which is a GCC extension when compiled as C++ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" -extern "C" { -#include "cfe.h" // for CFE_EVS_SendEvent and event types -} -#pragma GCC diagnostic pop - static_assert(FW_ENABLE_TEXT_LOGGING, "EvsMirror requires text logging (FW_ENABLE_TEXT_LOGGING) to receive event text"); namespace FPrimeCfs { @@ -23,10 +14,22 @@ namespace FPrimeCfs { // Component construction and destruction // ---------------------------------------------------------------------- -EvsMirror ::EvsMirror(const char* const compName) : EvsMirrorComponentBase(compName) {} +EvsMirror ::EvsMirror(const char* const compName) + : EvsMirrorComponentBase(compName), m_appId(CFE_ES_APPID_UNDEFINED) {} EvsMirror ::~EvsMirror() {} +void EvsMirror ::init(FwEnumStoreType instance) { + EvsMirrorComponentBase::init(instance); + // Capture the host application ID while running on the app's main + // (ES-registered) task; see the declaration comment for the rationale + CFE_Status_t status = CFE_ES_GetAppID(&this->m_appId); + if (status != CFE_SUCCESS) { + this->m_appId = CFE_ES_APPID_UNDEFINED; + Fw::Logger::log("[ERROR] EvsMirror failed to get cFS app ID: 0x%08x\n", static_cast(status)); + } +} + // ---------------------------------------------------------------------- // Handler implementations for typed input ports // ---------------------------------------------------------------------- @@ -38,8 +41,10 @@ void EvsMirror ::TextLogger_handler(FwIndexType portNum, Fw::TextLogString& text) { // EVS event IDs are 16 bits; the F Prime event ID is truncated to its low 16 // bits. EVS truncates event text to CFE_MISSION_EVS_MAX_MESSAGE_LENGTH itself. - CFE_Status_t status = - CFE_EVS_SendEvent(static_cast(id), EvsMirror::mapSeverity(severity), "%s", text.toChar()); + // CFE_EVS_SendEventWithAppID is used with the app ID captured in init() + // because this handler may run on a task unknown to CFE_ES + CFE_Status_t status = CFE_EVS_SendEventWithAppID(static_cast(id), EvsMirror::mapSeverity(severity), + this->m_appId, "%s", text.toChar()); if (status != CFE_SUCCESS) { Fw::Logger::log("[ERROR] Failed to mirror event 0x%08x to EVS: 0x%08x\n", static_cast(id), static_cast(status)); diff --git a/FPrimeCfs/EvsMirror/EvsMirror.hpp b/FPrimeCfs/EvsMirror/EvsMirror.hpp index 52545f3..380db16 100644 --- a/FPrimeCfs/EvsMirror/EvsMirror.hpp +++ b/FPrimeCfs/EvsMirror/EvsMirror.hpp @@ -8,6 +8,15 @@ #include "FPrimeCfs/EvsMirror/EvsMirrorComponentAc.hpp" +// Some cFE versions (e.g. draco) use compound literals in inline functions, +// which is a GCC extension when compiled as C++ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +extern "C" { +#include "cfe.h" // for CFE_ES_AppId_t +} +#pragma GCC diagnostic pop + namespace FPrimeCfs { class EvsMirror final : public EvsMirrorComponentBase { @@ -23,6 +32,19 @@ class EvsMirror final : public EvsMirrorComponentBase { //! Destroy EvsMirror object ~EvsMirror(); + //! Initialize EvsMirror object + //! + //! Overrides the base initialization to also capture the host cFS + //! application ID. EVS resolves the calling task's application context, + //! but the TextLogger handler runs on F Prime threads that are not + //! created through CFE_ES and therefore have no ES task record, making + //! CFE_EVS_SendEvent fail with CFE_EVS_APP_ILLEGAL_APP_ID. init() runs + //! on the app's main (ES-registered) task during topology setup, so the + //! app ID is captured here and events are mirrored with + //! CFE_EVS_SendEventWithAppID instead. + void init(FwEnumStoreType instance = 0 //!< The instance number + ); + private: // ---------------------------------------------------------------------- // Handler implementations for typed input ports @@ -44,6 +66,13 @@ class EvsMirror final : public EvsMirrorComponentBase { //! Map an F Prime event severity to a cFS EVS event type static U16 mapSeverity(const Fw::LogSeverity& severity); + + // ---------------------------------------------------------------------- + // Member variables + // ---------------------------------------------------------------------- + + //! Host cFS application ID, captured on the main task in init() + CFE_ES_AppId_t m_appId; }; } // namespace FPrimeCfs diff --git a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp index 967184a..43a53e1 100644 --- a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp +++ b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp @@ -9,15 +9,20 @@ namespace FPrimeCfs { +const CFE_ES_AppId_t EvsMirrorTester::TEST_APP_ID; + // ---------------------------------------------------------------------- // Construction and destruction // ---------------------------------------------------------------------- EvsMirrorTester ::EvsMirrorTester() : EvsMirrorGTestBase("EvsMirrorTester", EvsMirrorTester::MAX_HISTORY_SIZE), component("EvsMirror") { + // Inject the app ID before init so the component captures it there + CfeStub::reset(); + CfeStub::state().appId = EvsMirrorTester::TEST_APP_ID; this->initComponents(); this->connectPorts(); - CfeStub::reset(); + CfeStub::state().appId = CFE_ES_APPID_UNDEFINED; } EvsMirrorTester ::~EvsMirrorTester() { @@ -44,6 +49,8 @@ void EvsMirrorTester ::testMirror() { const CfeStub::SendEventCall& call = CfeStub::state().sendEventCalls[0]; ASSERT_EQ(call.eventId, static_cast(id)); ASSERT_EQ(call.eventType, CFE_EVS_EventType_ERROR); + // The app ID captured at init() is used, not a live lookup + ASSERT_EQ(call.appId, EvsMirrorTester::TEST_APP_ID); ASSERT_STREQ(call.text, text.toChar()); ASSERT_EQ(logger.messageCount, 0u); } diff --git a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp index 31750fd..ae090a4 100644 --- a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp +++ b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp @@ -24,6 +24,9 @@ class EvsMirrorTester final : public EvsMirrorGTestBase { // Instance ID supplied to the component instance under test static const FwEnumStoreType TEST_INSTANCE_ID = 0; + // App ID injected into the CFE_ES_GetAppID stub before component init + static const CFE_ES_AppId_t TEST_APP_ID = 0x1234; + public: // ---------------------------------------------------------------------- // Construction and destruction diff --git a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp index ff89ec7..5d504e8 100644 --- a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp +++ b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp @@ -17,6 +17,7 @@ State& state() { void reset() { (void)memset(&state(), 0, sizeof(State)); state().sendEventStatus = CFE_SUCCESS; + state().getAppIdStatus = CFE_SUCCESS; } } // namespace CfeStub @@ -27,6 +28,7 @@ extern "C" CFE_Status_t CFE_EVS_SendEvent(uint16 EventID, uint16 EventType, cons CfeStub::SendEventCall& call = s.sendEventCalls[s.sendEventCount]; call.eventId = EventID; call.eventType = EventType; + call.appId = CFE_ES_APPID_UNDEFINED; va_list args; va_start(args, Spec); (void)vsnprintf(call.text, sizeof call.text, Spec, args); @@ -36,3 +38,29 @@ extern "C" CFE_Status_t CFE_EVS_SendEvent(uint16 EventID, uint16 EventType, cons s.sendEventCount++; return s.sendEventStatus; } + +extern "C" CFE_Status_t CFE_EVS_SendEventWithAppID(uint16 EventID, uint16 EventType, CFE_ES_AppId_t AppID, const char* Spec, ...) { + CfeStub::State& s = CfeStub::state(); + if (s.sendEventCount < CfeStub::STUB_MAX_ENTRIES) { + CfeStub::SendEventCall& call = s.sendEventCalls[s.sendEventCount]; + call.eventId = EventID; + call.eventType = EventType; + call.appId = AppID; + va_list args; + va_start(args, Spec); + (void)vsnprintf(call.text, sizeof call.text, Spec, args); + va_end(args); + } + // Counts every call; only the first STUB_MAX_ENTRIES calls are recorded in sendEventCalls + s.sendEventCount++; + return s.sendEventStatus; +} + +extern "C" CFE_Status_t CFE_ES_GetAppID(CFE_ES_AppId_t* AppIdPtr) { + CfeStub::State& s = CfeStub::state(); + s.getAppIdCount++; + if (s.getAppIdStatus == CFE_SUCCESS) { + *AppIdPtr = s.appId; + } + return s.getAppIdStatus; +} diff --git a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp index 8d79d85..b9828ab 100644 --- a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp +++ b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp @@ -17,20 +17,26 @@ static const unsigned int STUB_MAX_ENTRIES = 32; //! Maximum formatted text bytes captured per sent event static const unsigned int STUB_MAX_TEXT = 256; -//! Record of a CFE_EVS_SendEvent call +//! Record of a CFE_EVS_SendEventWithAppID call struct SendEventCall { uint16 eventId; uint16 eventType; + CFE_ES_AppId_t appId; char text[STUB_MAX_TEXT]; }; //! Shared state of the cFE stub layer struct State { - // Injectable return status + // Injectable return statuses CFE_Status_t sendEventStatus; + CFE_Status_t getAppIdStatus; + + // Injectable app ID returned by CFE_ES_GetAppID + CFE_ES_AppId_t appId; // Call records unsigned int sendEventCount; + unsigned int getAppIdCount; SendEventCall sendEventCalls[STUB_MAX_ENTRIES]; }; diff --git a/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h b/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h index fa5b098..3c72458 100644 --- a/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h +++ b/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h @@ -20,9 +20,11 @@ typedef uint32_t uint32; typedef int32_t int32; typedef int32 CFE_Status_t; +typedef uint32 CFE_ES_AppId_t; #define CFE_SUCCESS ((CFE_Status_t)0) #define CFE_EVS_APP_NOT_REGISTERED ((CFE_Status_t)0xc2000005) +#define CFE_ES_APPID_UNDEFINED ((CFE_ES_AppId_t)0) /* Event types (values match CFE_EVS_EventType_Enum_t) */ #define CFE_EVS_EventType_DEBUG (1) @@ -32,6 +34,10 @@ typedef int32 CFE_Status_t; /* Event services API (implemented by CfeStubs.cpp) */ CFE_Status_t CFE_EVS_SendEvent(uint16 EventID, uint16 EventType, const char* Spec, ...); +CFE_Status_t CFE_EVS_SendEventWithAppID(uint16 EventID, uint16 EventType, CFE_ES_AppId_t AppID, const char* Spec, ...); + +/* Executive services API (implemented by CfeStubs.cpp) */ +CFE_Status_t CFE_ES_GetAppID(CFE_ES_AppId_t* AppIdPtr); #if defined(__cplusplus) } diff --git a/FPrimeCfs/SchAppDriver/test/ut/SchAppDriverTester.cpp b/FPrimeCfs/SchAppDriver/test/ut/SchAppDriverTester.cpp index b080ff1..5fe1cb9 100644 --- a/FPrimeCfs/SchAppDriver/test/ut/SchAppDriverTester.cpp +++ b/FPrimeCfs/SchAppDriver/test/ut/SchAppDriverTester.cpp @@ -69,8 +69,7 @@ void SchAppDriverTester ::sendSchMessage(Fw::Buffer& buffer, U8 functionCode) { for (FwSizeType i = 0; i < size; i++) { this->m_messageData[i] = static_cast(STest::Pick::lowerUpper(0, 0xFF)); } - buffer.setData(this->m_messageData); - buffer.setSize(size); + buffer.set(this->m_messageData, size); this->invoke_to_cfsCommandIn(0, functionCode, buffer); } diff --git a/config/FPrimeCfsConfig/ComCfg.fpp b/config/FPrimeCfsConfig/ComCfg.fpp index a86b2b0..36bd0fa 100644 --- a/config/FPrimeCfsConfig/ComCfg.fpp +++ b/config/FPrimeCfsConfig/ComCfg.fpp @@ -41,12 +41,16 @@ module ComCfg { FW_PACKET_PACKETIZED_TLM = 0x0020 @< Packetized telemetry packet type FW_PACKET_DP = 0x0005 @< Data Product packet type FW_PACKET_IDLE = 0x0006 @< F Prime idle + FW_PACKET_PARAM = 0x0007 @< Parameter value type - outgoing FW_PACKET_HAND = 0x00FE @< F Prime handshake FW_PACKET_UNKNOWN = 0x00FF @< F Prime unknown packet SPP_IDLE_PACKET = 0x07FF @< Per Space Packet Standard, all 1s (11bits) is reserved for Idle Packets INVALID_UNINITIALIZED = 0x0800 @< Anything equal or higher value is invalid and should not be used } default INVALID_UNINITIALIZED + @ Reserved SA index sentinel meaning "unset"; SA index 0xFFFF cannot be selected via context + constant SaIndexUnset = 0xFFFF + @ Type used to pass context info between components during framing/deframing struct FrameContext { comQueueIndex: FwIndexType @< Queue Index used by the ComQueue, other components shall not modify @@ -57,6 +61,7 @@ module ComCfg { vcId: U8 @< 6 bit Virtual Channel ID - used for AOS, TC, and TM Protocols pvn: Pvn @< Packet Version Number - used for AOS deframing to identify packet type sendNow: bool @< Flag to AOS Framer that the Frame this packet goes into should be sent ASAP + saIndex: U16 @< Security Association Index - set by SDLS deframers, read by SDLS framers functionCode: U8 @< cFS command function code, placed in the command secondary header by FPrimeCfs.CfsCmdFramer } default { @@ -68,6 +73,7 @@ module ComCfg { vcId = 1 pvn = Pvn.INVALID_UNINITIALIZED sendNow = false + saIndex = SaIndexUnset functionCode = 0 } From b0558c1e5f14a50ef5af5a9e6d81a2f136c9794b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:12:04 +0000 Subject: [PATCH 2/4] Add Os_Task_Cfs: create F Prime tasks as cFE ES child tasks F Prime tasks were raw pthreads unknown to cFE ES, so any cFE API resolving the calling task's app context failed (e.g. EVS filtering dropped mirrored events). Os_Task_Cfs creates tasks via CFE_ES_CreateChildTask so every F Prime task has an ES record. CfsCore subtopology priorities move to cFS/OSAL semantics (lower = more urgent). Co-Authored-By: michael.d.starch --- FPrimeCfs/CMakeLists.txt | 1 + FPrimeCfs/Os/Cfs/CMakeLists.txt | 34 +++ FPrimeCfs/Os/Cfs/DefaultTask.cpp | 15 ++ FPrimeCfs/Os/Cfs/Task.cpp | 192 ++++++++++++++ FPrimeCfs/Os/Cfs/Task.hpp | 140 ++++++++++ FPrimeCfs/Os/Cfs/docs/sdd.md | 101 ++++++++ FPrimeCfs/Os/Cfs/test/ut/CfsTaskTestMain.cpp | 242 ++++++++++++++++++ FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.cpp | 88 +++++++ FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.hpp | 65 +++++ FPrimeCfs/Os/Cfs/test/ut/stubs/cfe.h | 66 +++++ .../CfsCore/CfsCoreConfig/CfsCoreConfig.fpp | 10 +- 11 files changed, 950 insertions(+), 4 deletions(-) create mode 100644 FPrimeCfs/Os/Cfs/CMakeLists.txt create mode 100644 FPrimeCfs/Os/Cfs/DefaultTask.cpp create mode 100644 FPrimeCfs/Os/Cfs/Task.cpp create mode 100644 FPrimeCfs/Os/Cfs/Task.hpp create mode 100644 FPrimeCfs/Os/Cfs/docs/sdd.md create mode 100644 FPrimeCfs/Os/Cfs/test/ut/CfsTaskTestMain.cpp create mode 100644 FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.cpp create mode 100644 FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.hpp create mode 100644 FPrimeCfs/Os/Cfs/test/ut/stubs/cfe.h diff --git a/FPrimeCfs/CMakeLists.txt b/FPrimeCfs/CMakeLists.txt index 78c39e1..4ab706e 100644 --- a/FPrimeCfs/CMakeLists.txt +++ b/FPrimeCfs/CMakeLists.txt @@ -7,6 +7,7 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/CfsRouter") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/EvsMirror") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/CfsSystemTime") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/CfsTlmFramer") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/Os/Cfs") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/Ports") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PollingTimer") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SchAppDriver") diff --git a/FPrimeCfs/Os/Cfs/CMakeLists.txt b/FPrimeCfs/Os/Cfs/CMakeLists.txt new file mode 100644 index 0000000..8f194ed --- /dev/null +++ b/FPrimeCfs/Os/Cfs/CMakeLists.txt @@ -0,0 +1,34 @@ +#### +# F Prime CMakeLists.txt: +# +# Registers the cFE ES child task implementation of Os::Task (Os_Task_Cfs). +# Select it in a deployment with: +# +# fprime_target_implementations("" Os_Task_Cfs) +# +#### + +register_os_implementation("Task" Cfs Fw_Logger) + +if (TARGET core_api) + # Building within a cFS mission build: link against the real cFE core API + target_link_libraries(Os_Task_Cfs_Implementation PUBLIC core_api) +else() + # Standalone (unit test) build: use the cFE stub layer in test/ut/stubs + target_include_directories(Os_Task_Cfs_Implementation PUBLIC "${CMAKE_CURRENT_LIST_DIR}/test/ut/stubs") +endif() + +### Unit Tests ### +register_fprime_ut( + FPrimeCfs_Os_Cfs_Task_ut_exe + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CfsTaskTestMain.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/stubs/CfeEsStubs.cpp" + DEPENDS + Fw_Types + Fw_Time + Fw_Logger + Os + CHOOSES_IMPLEMENTATIONS + Os_Task_Cfs +) diff --git a/FPrimeCfs/Os/Cfs/DefaultTask.cpp b/FPrimeCfs/Os/Cfs/DefaultTask.cpp new file mode 100644 index 0000000..13bc378 --- /dev/null +++ b/FPrimeCfs/Os/Cfs/DefaultTask.cpp @@ -0,0 +1,15 @@ +// ====================================================================== +// \title FPrimeCfs/Os/Cfs/DefaultTask.cpp +// \brief sets default Os::Task to the cFE ES child task implementation via linker +// ====================================================================== +#include "FPrimeCfs/Os/Cfs/Task.hpp" +#include "Os/Delegate.hpp" +#include "Os/Task.hpp" + +namespace Os { + +TaskInterface* TaskInterface::getDelegate(TaskHandleStorage& aligned_new_memory) { + return Os::Delegate::makeDelegate(aligned_new_memory); +} + +} // namespace Os diff --git a/FPrimeCfs/Os/Cfs/Task.cpp b/FPrimeCfs/Os/Cfs/Task.cpp new file mode 100644 index 0000000..767c651 --- /dev/null +++ b/FPrimeCfs/Os/Cfs/Task.cpp @@ -0,0 +1,192 @@ +// ====================================================================== +// \title FPrimeCfs/Os/Cfs/Task.cpp +// \brief implementation of cFE ES implementation of Os::Task +// ====================================================================== +#include + +#include "FPrimeCfs/Os/Cfs/Task.hpp" +#include "Fw/Logger/Logger.hpp" +#include "Fw/Types/Assert.hpp" +#include "Fw/Types/StringUtils.hpp" + +namespace Os { +namespace Cfs { +namespace Task { + +// Out-of-class definitions for odr-used constexpr members (required in C++14) +constexpr CFE_ES_TaskPriority_Atom_t CfsTask::DEFAULT_PRIORITY; +constexpr FwSizeType CfsTask::MIN_PRIORITY; +constexpr FwSizeType CfsTask::MAX_PRIORITY; +constexpr FwSizeType CfsTask::DEFAULT_STACK_SIZE; +constexpr U32 CfsTask::HANDOFF_POLL_LIMIT; +constexpr U32 CfsTask::JOIN_POLL_DELAY_MS; + +//! Handoff slot used to deliver the F Prime routine and argument to a newly +//! created child task: the cFE child task entry point takes no arguments, so +//! the routine/argument pair is published here by the parent before task +//! creation and consumed by the child on startup. s_handoff_taken serializes +//! the exchange: the parent busy-waits (with delays) on it before starting +//! another task, so at most one handoff is pending at a time. Task creation +//! is restricted to the app's main task by cFE, which also serializes the +//! writers. +static Os::Task::taskRoutine s_handoff_routine = nullptr; +static void* s_handoff_argument = nullptr; +static std::atomic s_handoff_taken(true); + +//! \brief map a CFE_ES_CreateChildTask status to an Os::Task status +static Os::Task::Status map_create_status(CFE_Status_t cfe_status) { + Os::Task::Status status = Os::Task::Status::UNKNOWN_ERROR; + switch (cfe_status) { + case CFE_SUCCESS: + status = Os::Task::Status::OP_OK; + break; + case CFE_ES_BAD_ARGUMENT: + status = Os::Task::Status::INVALID_PARAMS; + break; + // cFE reports both "not the app main task" and "out of task records" + // through CFE_ES_ERR_CHILD_TASK_CREATE + case CFE_ES_ERR_CHILD_TASK_CREATE: + status = Os::Task::Status::ERROR_RESOURCES; + break; + // Returned when the caller is not a task belonging to any cFS app + case CFE_ES_ERR_RESOURCEID_NOT_VALID: + status = Os::Task::Status::INVALID_STATE; + break; + default: + status = Os::Task::Status::UNKNOWN_ERROR; + break; + } + return status; +} + +void CfsTask::childTaskEntry() { + // Consume the pending routine/argument pair published by start() + Os::Task::taskRoutine routine = s_handoff_routine; + void* argument = s_handoff_argument; + s_handoff_taken.store(true); + FW_ASSERT(routine != nullptr); + routine(argument); + // The F Prime routine returned: remove this child task's ES record + CFE_ES_ExitChildTask(); +} + +void CfsTask::onStart() {} + +Os::Task::Status CfsTask::start(const Arguments& arguments) { + FW_ASSERT(arguments.m_routine != nullptr); + + // Affinity is not supported by the cFE child task API + if (arguments.m_cpuAffinity != Os::Task::TASK_DEFAULT) { + Fw::Logger::log("[WARNING] %s cpu affinity is not supported by cFE child tasks\n", + arguments.m_name.toChar()); + } + + // Priorities pass through directly as OSAL/cFS priorities (lower value = + // more urgent), clamped to the valid OSAL range + FwSizeType priority = static_cast(arguments.m_priority); + if (arguments.m_priority == Os::Task::TASK_PRIORITY_DEFAULT) { + priority = static_cast(CfsTask::DEFAULT_PRIORITY); + } else if (priority < CfsTask::MIN_PRIORITY) { + Fw::Logger::log("[WARNING] %s task priority of %" PRI_FwSizeType " clamped to %" PRI_FwSizeType "\n", + arguments.m_name.toChar(), priority, CfsTask::MIN_PRIORITY); + priority = CfsTask::MIN_PRIORITY; + } else if (priority > CfsTask::MAX_PRIORITY) { + Fw::Logger::log("[WARNING] %s task priority of %" PRI_FwSizeType " clamped to %" PRI_FwSizeType "\n", + arguments.m_name.toChar(), priority, CfsTask::MAX_PRIORITY); + priority = CfsTask::MAX_PRIORITY; + } + + FwSizeType stackSize = + (arguments.m_stackSize == Os::Task::TASK_DEFAULT) ? CfsTask::DEFAULT_STACK_SIZE : arguments.m_stackSize; + + // Publish the routine/argument pair for the child task entry point. The + // preceding start() call's handoff must have completed (s_handoff_taken + // is initialized true and set true by each child on startup). + FW_ASSERT(s_handoff_taken.load()); + s_handoff_routine = arguments.m_routine; + s_handoff_argument = arguments.m_routine_argument; + s_handoff_taken.store(false); + + // OSAL limits task names to OS_MAX_API_NAME including the terminator; + // keep the distinctive trailing characters when truncating + char name[OS_MAX_API_NAME]; + const char* fullName = arguments.m_name.toChar(); + const FwSizeType nameLength = arguments.m_name.length(); + if (nameLength >= static_cast(OS_MAX_API_NAME)) { + fullName = fullName + (nameLength - (OS_MAX_API_NAME - 1)); + Fw::Logger::log("[WARNING] %s task name truncated to %s\n", arguments.m_name.toChar(), fullName); + } + (void)Fw::StringUtils::string_copy(name, fullName, static_cast(sizeof(name))); + + CFE_ES_TaskId_t taskId = CFE_ES_TASKID_UNDEFINED; + CFE_Status_t cfe_status = CFE_ES_CreateChildTask( + &taskId, name, CfsTask::childTaskEntry, CFE_ES_TASK_STACK_ALLOCATE, + static_cast(stackSize), static_cast(priority), 0); + if (cfe_status != CFE_SUCCESS) { + // The child task will never run: reclaim the handoff slot + s_handoff_taken.store(true); + Fw::Logger::log("[ERROR] %s failed to create cFE child task: 0x%08" PRIx32 "\n", arguments.m_name.toChar(), + static_cast(cfe_status)); + return map_create_status(cfe_status); + } + + // Wait (bounded) for the child task to consume its routine/argument pair + // before allowing another handoff to be published + U32 polls = 0; + for (polls = 0; polls < CfsTask::HANDOFF_POLL_LIMIT; polls++) { + if (s_handoff_taken.load()) { + break; + } + (void)OS_TaskDelay(1); + } + // A created child task that never runs indicates a system-level fault + FW_ASSERT(polls < CfsTask::HANDOFF_POLL_LIMIT, static_cast(polls)); + + this->m_handle.m_task_id = taskId; + this->m_handle.m_is_valid = true; + return Os::Task::Status::OP_OK; +} + +Os::Task::Status CfsTask::join() { + if (not this->m_handle.m_is_valid) { + return Os::Task::Status::INVALID_HANDLE; + } + // cFE has no native join: poll until the child task record disappears. + // This loop is unbounded by design, matching join semantics (blocks until + // the task exits), as with pthread_join in the Posix implementation. + for (;;) { + CFE_ES_TaskInfo_t info; + CFE_Status_t status = CFE_ES_GetTaskInfo(&info, this->m_handle.m_task_id); + if (status != CFE_SUCCESS) { + break; + } + (void)OS_TaskDelay(CfsTask::JOIN_POLL_DELAY_MS); + } + this->m_handle.m_is_valid = false; + return Os::Task::Status::OP_OK; +} + +TaskHandle* CfsTask::getHandle() { + return &this->m_handle; +} + +// Note: not implemented for cFE child tasks. cFE ES provides no child task +// suspend/resume API; this matches the Posix implementation's behavior. +void CfsTask::suspend(Os::Task::SuspensionType suspensionType) { + FW_ASSERT(false); +} + +void CfsTask::resume() { + FW_ASSERT(false); +} + +Os::Task::Status CfsTask::_delay(const Fw::TimeInterval& interval) { + // Round up to whole milliseconds: OS_TaskDelay has millisecond granularity + U32 delay_ms = (interval.getSeconds() * 1000) + ((interval.getUSeconds() + 999) / 1000); + int32 status = OS_TaskDelay(delay_ms); + return (status == OS_SUCCESS) ? Os::Task::Status::OP_OK : Os::Task::Status::DELAY_ERROR; +} + +} // end namespace Task +} // end namespace Cfs +} // end namespace Os diff --git a/FPrimeCfs/Os/Cfs/Task.hpp b/FPrimeCfs/Os/Cfs/Task.hpp new file mode 100644 index 0000000..2cf8403 --- /dev/null +++ b/FPrimeCfs/Os/Cfs/Task.hpp @@ -0,0 +1,140 @@ +// ====================================================================== +// \title FPrimeCfs/Os/Cfs/Task.hpp +// \brief definitions of cFE ES implementation of Os::Task +// ====================================================================== +#ifndef FPrimeCfs_Os_Cfs_Task_hpp_ +#define FPrimeCfs_Os_Cfs_Task_hpp_ + +#include +#include + +// Some cFE versions (e.g. draco) use compound literals in inline functions, +// which is a GCC extension when compiled as C++ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +extern "C" { +#include "cfe.h" +} +#pragma GCC diagnostic pop + +namespace Os { +namespace Cfs { +namespace Task { + +//! TaskHandle class definition for cFE ES child task implementations. +//! +struct CfsTaskHandle : public TaskHandle { + //! cFE ES child task identifier + CFE_ES_TaskId_t m_task_id = CFE_ES_TASKID_UNDEFINED; + //! Is the above identifier valid + bool m_is_valid = false; +}; + +//! \brief cFE ES child task implementation of Os::Task +//! +//! Creates F Prime tasks as cFE ES child tasks (CFE_ES_CreateChildTask) of the +//! hosting cFS application rather than raw OS threads. This gives every +//! F Prime task an ES task record, so cFE APIs that resolve the calling +//! task's application context (e.g. CFE_ES_GetAppID, CFE_EVS_SendEvent) work +//! from F Prime task contexts. +//! +//! Constraint: cFE only permits child task creation from the application's +//! main task. Tasks must therefore be started during topology setup (which +//! runs on the app's main task); start() returns an error status otherwise. +class CfsTask : public TaskInterface { + public: + //! OSAL priority used when the F Prime task supplies TASK_PRIORITY_DEFAULT. + //! 200 places default tasks well below (less urgent than) cFS core and + //! typical application task priorities (OSAL: lower number = more urgent). + static constexpr CFE_ES_TaskPriority_Atom_t DEFAULT_PRIORITY = 200; + + //! Lowest valid OSAL priority value (most urgent). 0 is reserved. + static constexpr FwSizeType MIN_PRIORITY = 1; + + //! Highest valid OSAL priority value (least urgent), i.e. OS_MAX_TASK_PRIORITY + static constexpr FwSizeType MAX_PRIORITY = 255; + + //! Stack size used when the F Prime task supplies TASK_DEFAULT. + //! Matches the deployment's default F Prime task stack size (64KiB). + static constexpr FwSizeType DEFAULT_STACK_SIZE = 64 * 1024; + + //! Maximum number of 1ms polls waiting for the child task to take its + //! routine argument (bounds the handoff loop; 10 seconds total) + static constexpr U32 HANDOFF_POLL_LIMIT = 10000; + + //! Delay between join() polls of the child task's existence, in milliseconds + static constexpr U32 JOIN_POLL_DELAY_MS = 10; + + //! \brief default constructor + CfsTask() = default; + + //! \brief default virtual destructor + ~CfsTask() = default; + + //! \brief copy constructor is forbidden + CfsTask(const CfsTask& other) = delete; + + //! \brief assignment operator is forbidden + CfsTask& operator=(const CfsTask& other) = delete; + + //! \brief perform required task start actions + void onStart() override; + + //! \brief start the task as a cFE ES child task + //! + //! Starts the task via CFE_ES_CreateChildTask. The routine argument is + //! delivered through a serialized static handoff slot because the cFE + //! child task entry point takes no arguments. Must be called from the + //! hosting application's main task (a cFE restriction); an error status + //! is returned otherwise. + //! + //! It is illegal for arguments.m_routine to be null. + //! + //! \param arguments: arguments supplied to the task start call + //! \return status of the task start + Status start(const Arguments& arguments) override; + + //! \brief block until the task has ended + //! + //! cFE has no native child task join; this polls CFE_ES_GetTaskInfo until + //! the child task record no longer exists. + //! + //! \return status of the block + Status join() override; + + //! \brief suspend the task (unsupported; asserts) + //! + //! cFE provides no child task suspend API; matches the Posix + //! implementation's behavior of asserting on use. + //! + //! \param suspensionType intentionality of the suspension + void suspend(SuspensionType suspensionType) override; + + //! \brief resume a suspended task (unsupported; asserts) + void resume() override; + + //! \brief delay the current task via OS_TaskDelay + //! + //! \param interval: delay time + //! \return status of the delay + Status _delay(const Fw::TimeInterval& interval) override; + + //! \brief return the underlying task handle (implementation specific) + //! \return internal task handle representation + TaskHandle* getHandle() override; + + private: + //! \brief cFE ES child task entry point + //! + //! Takes the pending routine argument from the handoff slot, runs the + //! F Prime task routine, and exits the child task cleanly when the + //! routine returns. + static void childTaskEntry(); + + CfsTaskHandle m_handle; //!< cFS task tracking +}; + +} // end namespace Task +} // end namespace Cfs +} // end namespace Os +#endif // FPrimeCfs_Os_Cfs_Task_hpp_ diff --git a/FPrimeCfs/Os/Cfs/docs/sdd.md b/FPrimeCfs/Os/Cfs/docs/sdd.md new file mode 100644 index 0000000..7c3bfd9 --- /dev/null +++ b/FPrimeCfs/Os/Cfs/docs/sdd.md @@ -0,0 +1,101 @@ +# Os::Task cFE ES Implementation (Os_Task_Cfs) + +## 1. Introduction + +`Os_Task_Cfs` is an F Prime `Os::Task` implementation that creates F Prime +tasks as **cFE ES child tasks** (`CFE_ES_CreateChildTask`) of the hosting cFS +application, rather than raw OS threads. + +### Motivation + +When F Prime runs as a cFS application, tasks created through the default +Posix `Os::Task` implementation are unknown to cFE Executive Services: ES has +no task record for them, so any cFE API that resolves the *calling task's* +application context fails from F Prime task contexts. Observed consequences: + +- `CFE_EVS_SendEvent` fails with `CFE_EVS_APP_ILLEGAL_APP_ID` (0xc2000003). +- `CFE_EVS_SendEventWithAppID` is silently filtered, because + `EVS_IsFiltered` -> `CFE_EVS_GetTypeEnable` re-resolves the calling task via + `CFE_ES_GetAppID`. + +Creating F Prime tasks as ES child tasks gives every F Prime task an ES task +record, so `CFE_ES_GetAppID` (and every API built on it) resolves correctly on +any F Prime task. + +## 2. Requirements + +| ID | Shall Statement | Verification | +|---|---|---| +| REQ-OSTASKCFS-001 | `start()` shall create the task via `CFE_ES_CreateChildTask`. | Unit test `StartNominal` | +| REQ-OSTASKCFS-002 | `start()` shall pass the task name (truncated to its trailing `OS_MAX_API_NAME - 1` characters when longer), stack size (default when `TASK_DEFAULT`), and priority to cFE and return `OP_OK` on success. | Unit tests `StartNominal`, `StartDefaultStack`, `StartNameTruncation` | +| REQ-OSTASKCFS-003 | `start()` shall pass F Prime priorities through directly as OSAL/cFS priorities (lower = more urgent), clamped to 1–255, with a defined default (200) for `TASK_PRIORITY_DEFAULT`. | Unit tests `StartDefaultPriority`, `StartPriorityClamping` | +| REQ-OSTASKCFS-004 | `start()` shall deliver the routine/argument pair to the child task through a serialized handoff with a handshake guaranteeing each child receives its own argument. | Unit test `StartAsynchronousChild` | +| REQ-OSTASKCFS-005 | On cFE error, `start()` shall return the corresponding `Os::TaskInterface::Status` and leave the handle invalid. | Unit test `StartCreateFailures` | +| REQ-OSTASKCFS-006 | The child task shall call `CFE_ES_ExitChildTask()` when the user routine returns. | Unit tests `StartNominal`, `StartAsynchronousChild` | +| REQ-OSTASKCFS-007 | `join()` shall block until the child task record no longer exists; `join()` on an invalid handle shall return `INVALID_HANDLE`. | Unit tests `JoinNominal`, `JoinInvalidHandle` | +| REQ-OSTASKCFS-008 | `suspend()`/`resume()` shall be unsupported and shall assert. | Unit test `SuspendResumeAssert` | +| REQ-OSTASKCFS-009 | `_delay()` shall delay via `OS_TaskDelay` (rounded up to whole milliseconds) and map failures to `DELAY_ERROR`. | Unit test `Delay` | +| REQ-OSTASKCFS-010 | `start()` shall be documented as callable only from an ES-registered main task (a cFE restriction); violations surface through the REQ-005 error path. | Inspection; unit test `StartCreateFailures` | + +## 3. Design + +### 3.1 Module layout + +``` +FPrimeCfs/Os/Cfs/ + Task.hpp / Task.cpp Os::Cfs::Task::CfsTask (TaskInterface), CfsTaskHandle + DefaultTask.cpp TaskInterface::getDelegate -> CfsTask + CMakeLists.txt register_os_implementation("Task" Cfs Fw_Logger) + test/ut/ GTest suite + recording cFE ES/OSAL stubs +``` + +The implementation is selected per deployment: + +```cmake +fprime_target_implementations("" Os_Task_Cfs) +``` + +### 3.2 Routine/argument handoff + +`CFE_ES_ChildTaskMainFuncPtr_t` takes no arguments, so the F Prime routine and +its argument are delivered through a static handoff slot: + +``` +start(): childTaskEntry(): + assert previous handoff complete routine = s_handoff_routine + publish routine + argument argument = s_handoff_argument + s_handoff_taken = false s_handoff_taken = true + CFE_ES_CreateChildTask(entry, ...) routine(argument) + poll s_handoff_taken (bounded, 10 s) CFE_ES_ExitChildTask() +``` + +The exchange is serialized: `start()` does not return until the child has +consumed its pair, so at most one handoff is pending. cFE restricts child +task creation to the app's main task, which also serializes the writers. + +### 3.3 Priority and stack mapping + +- Priorities pass through directly as OSAL priorities (lower = more urgent), + clamped to [1, 255]; topology priorities must therefore be specified in + cFS terms. +- `TASK_PRIORITY_DEFAULT` maps to OSAL 200 (well below cFS core priorities). +- `TASK_DEFAULT` stack maps to 64 KiB (the deployments' default F Prime task + stack size); cFE allocates the stack (`CFE_ES_TASK_STACK_ALLOCATE`). +- CPU affinity is unsupported by the cFE API; a warning is logged if + requested. + +### 3.4 Constraints + +- `start()` must be called from the app's **main task** — cFE rejects child + task creation from child tasks. F Prime starts active-component tasks + during topology setup, which runs on the app's main task. +- `suspend()`/`resume()` assert: cFE ES has no child task suspend API + (matches the Posix implementation's behavior). +- `join()` polls `CFE_ES_GetTaskInfo` every 10 ms: cFE has no native join. + +## 4. Unit Testing + +The GTest suite (`test/ut/`) tests `CfsTask` directly against recording +cFE ES/OSAL stubs (`test/ut/stubs/`), which allow injecting create failures, +deferring child execution to a real thread (to exercise the asynchronous +handoff), and simulating a running-then-exited child for `join()`. diff --git a/FPrimeCfs/Os/Cfs/test/ut/CfsTaskTestMain.cpp b/FPrimeCfs/Os/Cfs/test/ut/CfsTaskTestMain.cpp new file mode 100644 index 0000000..4c3a589 --- /dev/null +++ b/FPrimeCfs/Os/Cfs/test/ut/CfsTaskTestMain.cpp @@ -0,0 +1,242 @@ +// ====================================================================== +// \title FPrimeCfs/Os/Cfs/test/ut/CfsTaskTestMain.cpp +// \brief unit tests for the cFE ES child task implementation of Os::Task +// ====================================================================== +#include + +#include +#include +#include + +#include "CfeEsStubs.hpp" +#include "FPrimeCfs/Os/Cfs/Task.hpp" +#include "Fw/Types/Assert.hpp" +#include "Fw/Types/String.hpp" + +using Os::Cfs::Task::CfsTask; +using Os::Cfs::Task::CfsTaskHandle; + +namespace { + +// Assert hook that records asserts instead of aborting, for testing +// FW_ASSERT-on-unsupported behavior (REQ-OSTASKCFS-008) +class RecordingAssertHook : public Fw::AssertHook { + public: + void reportAssert(FILE_NAME_ARG file, + FwSizeType lineNo, + FwSizeType numArgs, + FwAssertArgType arg1, + FwAssertArgType arg2, + FwAssertArgType arg3, + FwAssertArgType arg4, + FwAssertArgType arg5, + FwAssertArgType arg6) override { + this->m_asserts++; + } + void doAssert() override {} + unsigned int m_asserts = 0; +}; + +// Routine capture state +struct RoutineRecord { + std::atomic calls{0}; + std::atomic lastArgument{nullptr}; +}; + +RoutineRecord& routineRecord() { + static RoutineRecord s_record; + return s_record; +} + +void testRoutine(void* argument) { + routineRecord().lastArgument.store(argument); + routineRecord().calls++; +} + +class CfsTaskTest : public ::testing::Test { + protected: + void SetUp() override { + CfeEsStub::reset(); + routineRecord().calls = 0; + routineRecord().lastArgument = nullptr; + } + CfsTask m_task; + U32 m_argument_target = 0; +}; + +// REQ-OSTASKCFS-001/002/006: nominal start creates a cFE child task with the +// supplied name/stack/priority, runs the routine with its argument, and the +// child exits via CFE_ES_ExitChildTask when the routine returns +TEST_F(CfsTaskTest, StartNominal) { + Fw::String name("TestTask"); + Os::Task::Arguments arguments(name, testRoutine, &this->m_argument_target, 100, 32 * 1024); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + + CfeEsStub::State& s = CfeEsStub::state(); + ASSERT_EQ(s.createCount, 1u); + ASSERT_STREQ(s.lastCreate.name, "TestTask"); + ASSERT_EQ(s.lastCreate.stackSize, static_cast(32 * 1024)); + ASSERT_EQ(s.lastCreate.priority, static_cast(100)); + ASSERT_EQ(s.lastCreate.stackPtr, nullptr); // CFE_ES_TASK_STACK_ALLOCATE + + // Routine ran exactly once with the supplied argument + ASSERT_EQ(routineRecord().calls.load(), 1u); + ASSERT_EQ(routineRecord().lastArgument.load(), &this->m_argument_target); + + // Child exited its ES record after the routine returned + ASSERT_EQ(s.exitChildTaskCount, 1u); + + // Handle holds the created task id + CfsTaskHandle* handle = static_cast(this->m_task.getHandle()); + ASSERT_TRUE(handle->m_is_valid); + ASSERT_EQ(handle->m_task_id, s.createdTaskId); +} + +// REQ-OSTASKCFS-003: default priority sentinel maps to the defined default +TEST_F(CfsTaskTest, StartDefaultPriority) { + Fw::String name("DefaultPrio"); + Os::Task::Arguments arguments(name, testRoutine); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + ASSERT_EQ(CfeEsStub::state().lastCreate.priority, CfsTask::DEFAULT_PRIORITY); +} + +// REQ-OSTASKCFS-002: default stack sentinel maps to the defined default +TEST_F(CfsTaskTest, StartDefaultStack) { + Fw::String name("DefaultStack"); + Os::Task::Arguments arguments(name, testRoutine); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + ASSERT_EQ(CfeEsStub::state().lastCreate.stackSize, static_cast(CfsTask::DEFAULT_STACK_SIZE)); +} + +// REQ-OSTASKCFS-002: names longer than OS_MAX_API_NAME are truncated to their +// distinctive trailing characters +TEST_F(CfsTaskTest, StartNameTruncation) { + Fw::String name("FPrimeApp.rateGroup1"); // 20 chars: exceeds the 19-char limit + Os::Task::Arguments arguments(name, testRoutine); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + ASSERT_STREQ(CfeEsStub::state().lastCreate.name, "PrimeApp.rateGroup1"); +} + +// REQ-OSTASKCFS-003: priorities pass through directly; out-of-range values +// clamp (0 is reserved in OSAL; the high clamp is unreachable when +// FwTaskPriorityType is 8 bits, since its maximum value is the default +// sentinel) +TEST_F(CfsTaskTest, StartPriorityClamping) { + { + Fw::String name("PassThrough"); + Os::Task::Arguments arguments(name, testRoutine, nullptr, 43); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + ASSERT_EQ(CfeEsStub::state().lastCreate.priority, static_cast(43)); + } + { + CfsTask task; + Fw::String name("ClampLow"); + Os::Task::Arguments arguments(name, testRoutine, nullptr, 0); + ASSERT_EQ(task.start(arguments), Os::Task::Status::OP_OK); + ASSERT_EQ(CfeEsStub::state().lastCreate.priority, static_cast(1)); + } +} + +// REQ-OSTASKCFS-004: the routine/argument pair is delivered through the +// handoff slot even when the child task starts asynchronously +TEST_F(CfsTaskTest, StartAsynchronousChild) { + CfeEsStub::state().childRunMode = CfeEsStub::DEFER_CHILD; + + // Emulate the asynchronous child task: wait for the entry point recorded + // by the create stub, then run it on a separate thread + std::thread child([]() { + while (CfeEsStub::state().deferredEntry == nullptr) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CfeEsStub::state().deferredEntry(); + }); + + Fw::String name("AsyncChild"); + Os::Task::Arguments arguments(name, testRoutine, &this->m_argument_target); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + child.join(); + + ASSERT_EQ(routineRecord().calls.load(), 1u); + ASSERT_EQ(routineRecord().lastArgument.load(), &this->m_argument_target); + ASSERT_EQ(CfeEsStub::state().exitChildTaskCount, 1u); +} + +// REQ-OSTASKCFS-005: cFE errors map to Os::Task statuses and leave the handle +// invalid; the handoff slot is reclaimed so a subsequent start succeeds +TEST_F(CfsTaskTest, StartCreateFailures) { + struct Case { + CFE_Status_t cfeStatus; + Os::Task::Status expected; + }; + const Case cases[3] = { + {CFE_ES_BAD_ARGUMENT, Os::Task::Status::INVALID_PARAMS}, + {CFE_ES_ERR_CHILD_TASK_CREATE, Os::Task::Status::ERROR_RESOURCES}, + {CFE_ES_ERR_RESOURCEID_NOT_VALID, Os::Task::Status::INVALID_STATE}, + }; + for (const Case& testCase : cases) { + CfsTask task; + CfeEsStub::state().createStatus = testCase.cfeStatus; + Fw::String name("FailTask"); + Os::Task::Arguments arguments(name, testRoutine); + ASSERT_EQ(task.start(arguments), testCase.expected); + CfsTaskHandle* handle = static_cast(task.getHandle()); + ASSERT_FALSE(handle->m_is_valid); + ASSERT_EQ(routineRecord().calls.load(), 0u); + } + // The handoff slot was reclaimed on failure: a subsequent start succeeds + CfeEsStub::state().createStatus = CFE_SUCCESS; + Fw::String name("Recovery"); + Os::Task::Arguments arguments(name, testRoutine); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + ASSERT_EQ(routineRecord().calls.load(), 1u); +} + +// REQ-OSTASKCFS-007: join blocks (polling) until the child task record no +// longer exists +TEST_F(CfsTaskTest, JoinNominal) { + Fw::String name("JoinTask"); + Os::Task::Arguments arguments(name, testRoutine); + ASSERT_EQ(this->m_task.start(arguments), Os::Task::Status::OP_OK); + + CfeEsStub::State& s = CfeEsStub::state(); + s.taskAlivePolls = 3; + unsigned int delaysBefore = s.taskDelayCount; + ASSERT_EQ(this->m_task.join(), Os::Task::Status::OP_OK); + // 3 alive polls + 1 final poll observing the task gone + ASSERT_EQ(s.getTaskInfoCount, 4u); + // One delay per alive poll + ASSERT_EQ(s.taskDelayCount - delaysBefore, 3u); + // A second join reports the invalid handle + ASSERT_EQ(this->m_task.join(), Os::Task::Status::INVALID_HANDLE); +} + +// REQ-OSTASKCFS-007: join on a never-started task reports INVALID_HANDLE +TEST_F(CfsTaskTest, JoinInvalidHandle) { + ASSERT_EQ(this->m_task.join(), Os::Task::Status::INVALID_HANDLE); +} + +// REQ-OSTASKCFS-008: suspend and resume are unsupported and assert +TEST_F(CfsTaskTest, SuspendResumeAssert) { + RecordingAssertHook hook; + hook.registerHook(); + this->m_task.suspend(Os::Task::SuspensionType::INTENTIONAL); + ASSERT_EQ(hook.m_asserts, 1u); + this->m_task.resume(); + ASSERT_EQ(hook.m_asserts, 2u); + hook.deregisterHook(); +} + +// REQ-OSTASKCFS-009: delay delegates to OS_TaskDelay and maps failures +TEST_F(CfsTaskTest, Delay) { + ASSERT_EQ(this->m_task._delay(Fw::TimeInterval(0, 500000)), Os::Task::Status::OP_OK); + ASSERT_EQ(CfeEsStub::state().taskDelayCount, 1u); + CfeEsStub::state().taskDelayStatus = OS_ERROR; + ASSERT_EQ(this->m_task._delay(Fw::TimeInterval(1, 0)), Os::Task::Status::DELAY_ERROR); +} + +} // namespace + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.cpp b/FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.cpp new file mode 100644 index 0000000..f19d79c --- /dev/null +++ b/FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.cpp @@ -0,0 +1,88 @@ +// ====================================================================== +// \title FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.cpp +// \brief recording cFE ES/OSAL stubs used by CfsTask unit tests +// ====================================================================== +#include "CfeEsStubs.hpp" + +#include +#include +#include + +namespace CfeEsStub { + +State& state() { + static State s_state; + return s_state; +} + +void reset() { + State& s = state(); + (void)std::memset(&s, 0, sizeof(State)); + s.createStatus = CFE_SUCCESS; + s.taskDelayStatus = OS_SUCCESS; + s.createdTaskId = 0x42001; + s.childRunMode = RUN_CHILD_INLINE; +} + +} // namespace CfeEsStub + +extern "C" CFE_Status_t CFE_ES_CreateChildTask(CFE_ES_TaskId_t* TaskIdPtr, + const char* TaskName, + CFE_ES_ChildTaskMainFuncPtr_t FunctionPtr, + CFE_ES_StackPointer_t StackPtr, + size_t StackSize, + CFE_ES_TaskPriority_Atom_t Priority, + uint32 Flags) { + CfeEsStub::State& s = CfeEsStub::state(); + s.createCount++; + CfeEsStub::CreateChildTaskCall& call = s.lastCreate; + call.stackSize = StackSize; + call.priority = Priority; + call.flags = Flags; + call.stackPtr = StackPtr; + call.name[0] = '\0'; + if (TaskName != nullptr) { + (void)std::strncpy(call.name, TaskName, sizeof call.name - 1); + call.name[sizeof call.name - 1] = '\0'; + } + if (s.createStatus != CFE_SUCCESS) { + return s.createStatus; + } + if (TaskIdPtr != nullptr) { + *TaskIdPtr = s.createdTaskId; + } + if (s.childRunMode == CfeEsStub::RUN_CHILD_INLINE) { + // Simulate the child task running to completion before create returns + FunctionPtr(); + } else { + s.deferredEntry = FunctionPtr; + } + return CFE_SUCCESS; +} + +extern "C" CFE_Status_t CFE_ES_GetTaskInfo(CFE_ES_TaskInfo_t* TaskInfo, CFE_ES_TaskId_t TaskId) { + CfeEsStub::State& s = CfeEsStub::state(); + s.getTaskInfoCount++; + if (s.taskAlivePolls > 0) { + s.taskAlivePolls--; + if (TaskInfo != nullptr) { + TaskInfo->TaskId = TaskId; + } + return CFE_SUCCESS; + } + return CFE_ES_ERR_RESOURCEID_NOT_VALID; +} + +extern "C" void CFE_ES_ExitChildTask(void) { + CfeEsStub::state().exitChildTaskCount++; +} + +extern "C" int32 OS_TaskDelay(uint32 millisecond) { + CfeEsStub::State& s = CfeEsStub::state(); + s.taskDelayCount++; + // Sleep briefly so polling loops in the code under test yield to other + // threads (e.g. the emulated asynchronous child task) + (void)millisecond; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return s.taskDelayStatus; +} diff --git a/FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.hpp b/FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.hpp new file mode 100644 index 0000000..408eed4 --- /dev/null +++ b/FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.hpp @@ -0,0 +1,65 @@ +// ====================================================================== +// \title FPrimeCfs/Os/Cfs/test/ut/stubs/CfeEsStubs.hpp +// \brief test state for the cFE ES/OSAL stubs used by CfsTask unit tests +// ====================================================================== +#ifndef FPRIME_CFS_OS_CFS_UT_CFE_ES_STUBS_HPP +#define FPRIME_CFS_OS_CFS_UT_CFE_ES_STUBS_HPP + +#include "cfe.h" + +namespace CfeEsStub { + +static const unsigned int STUB_MAX_NAME = 64; + +//! Modes for how the CFE_ES_CreateChildTask stub runs the child entry point +enum ChildRunMode { + RUN_CHILD_INLINE, //!< run the child entry function before returning (default) + DEFER_CHILD, //!< record the entry function; the test runs it later +}; + +//! Record of a CFE_ES_CreateChildTask call +struct CreateChildTaskCall { + char name[STUB_MAX_NAME]; + size_t stackSize; + CFE_ES_TaskPriority_Atom_t priority; + uint32 flags; + CFE_ES_StackPointer_t stackPtr; +}; + +struct State { + // Injectable return statuses + CFE_Status_t createStatus; + int32 taskDelayStatus; + + // Injectable task id written by CFE_ES_CreateChildTask + CFE_ES_TaskId_t createdTaskId; + + // How the stub executes the child entry point + ChildRunMode childRunMode; + + // Entry point recorded when childRunMode == DEFER_CHILD + CFE_ES_ChildTaskMainFuncPtr_t deferredEntry; + + // Number of CFE_ES_GetTaskInfo calls that report the task alive before + // reporting it gone (simulates a running-then-exited child for join()) + unsigned int taskAlivePolls; + + // Call counters + unsigned int createCount; + unsigned int getTaskInfoCount; + unsigned int exitChildTaskCount; + unsigned int taskDelayCount; + + // Last recorded create call + CreateChildTaskCall lastCreate; +}; + +//! Access the stub state singleton +State& state(); + +//! Reset stub state to nominal defaults +void reset(); + +} // namespace CfeEsStub + +#endif diff --git a/FPrimeCfs/Os/Cfs/test/ut/stubs/cfe.h b/FPrimeCfs/Os/Cfs/test/ut/stubs/cfe.h new file mode 100644 index 0000000..a54fdfa --- /dev/null +++ b/FPrimeCfs/Os/Cfs/test/ut/stubs/cfe.h @@ -0,0 +1,66 @@ +/** + * @file + * Minimal cFE/OSAL stub header for Os::Cfs::Task unit testing. + * + * Provides only the types, constants, and function prototypes used by the + * CfsTask implementation. The functions are implemented by CfeEsStubs.cpp + * which records calls and allows tests to inject return values and child + * task execution behavior. + */ +#ifndef FPRIME_CFS_OS_CFS_UT_STUB_CFE_H +#define FPRIME_CFS_OS_CFS_UT_STUB_CFE_H + +#include +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef uint16_t uint16; +typedef uint32_t uint32; +typedef int32_t int32; + +typedef int32 CFE_Status_t; +typedef uint32 CFE_ES_TaskId_t; +typedef uint16 CFE_ES_TaskPriority_Atom_t; +typedef void* CFE_ES_StackPointer_t; +typedef void (*CFE_ES_ChildTaskMainFuncPtr_t)(void); + +#define CFE_SUCCESS ((CFE_Status_t)0) +#define CFE_ES_BAD_ARGUMENT ((CFE_Status_t)0xc4000002) +#define CFE_ES_ERR_CHILD_TASK_CREATE ((CFE_Status_t)0xc4000009) +#define CFE_ES_ERR_RESOURCEID_NOT_VALID ((CFE_Status_t)0xc400000b) + +#define CFE_ES_TASKID_UNDEFINED ((CFE_ES_TaskId_t)0) +#define CFE_ES_TASK_STACK_ALLOCATE NULL + +#define OS_MAX_API_NAME 20 + +/* Subset of CFE_ES_TaskInfo_t sufficient for CfsTask usage */ +typedef struct { + CFE_ES_TaskId_t TaskId; + char TaskName[OS_MAX_API_NAME]; +} CFE_ES_TaskInfo_t; + +/* Executive services API (implemented by CfeEsStubs.cpp) */ +CFE_Status_t CFE_ES_CreateChildTask(CFE_ES_TaskId_t* TaskIdPtr, + const char* TaskName, + CFE_ES_ChildTaskMainFuncPtr_t FunctionPtr, + CFE_ES_StackPointer_t StackPtr, + size_t StackSize, + CFE_ES_TaskPriority_Atom_t Priority, + uint32 Flags); +CFE_Status_t CFE_ES_GetTaskInfo(CFE_ES_TaskInfo_t* TaskInfo, CFE_ES_TaskId_t TaskId); +void CFE_ES_ExitChildTask(void); + +/* OSAL API (implemented by CfeEsStubs.cpp) */ +#define OS_SUCCESS (0) +#define OS_ERROR (-1) +int32 OS_TaskDelay(uint32 millisecond); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/FPrimeCfs/Subtopologies/CfsCore/CfsCoreConfig/CfsCoreConfig.fpp b/FPrimeCfs/Subtopologies/CfsCore/CfsCoreConfig/CfsCoreConfig.fpp index bb78265..eabb584 100644 --- a/FPrimeCfs/Subtopologies/CfsCore/CfsCoreConfig/CfsCoreConfig.fpp +++ b/FPrimeCfs/Subtopologies/CfsCore/CfsCoreConfig/CfsCoreConfig.fpp @@ -15,10 +15,12 @@ module CfsCoreConfig { constant tlmSend = 64 * 1024 } + # Priorities use cFS/OSAL semantics: lower number = more urgent. + # Values sit below cFE core services but above background tasks. module Priorities { - constant cmdDisp = 35 - constant $health = 24 - constant events = 23 - constant tlmSend = 22 + constant cmdDisp = 140 + constant $health = 150 + constant events = 160 + constant tlmSend = 170 } } From 9ba78569d85ecb412af24e09f4e311940ece2184 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:35:41 +0000 Subject: [PATCH 3/4] EvsMirror: revert to plain CFE_EVS_SendEvent With Os_Task_Cfs, F Prime tasks are ES-registered child tasks, so EVS resolves the calling task's app context and the explicit-AppID capture is no longer needed. Co-Authored-By: michael.d.starch --- FPrimeCfs/EvsMirror/EvsMirror.cpp | 29 ++++++++----------- FPrimeCfs/EvsMirror/EvsMirror.hpp | 29 ------------------- .../EvsMirror/test/ut/EvsMirrorTester.cpp | 9 +----- .../EvsMirror/test/ut/EvsMirrorTester.hpp | 3 -- .../EvsMirror/test/ut/stubs/CfeStubs.cpp | 28 ------------------ .../EvsMirror/test/ut/stubs/CfeStubs.hpp | 10 ++----- FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h | 6 ---- 7 files changed, 15 insertions(+), 99 deletions(-) diff --git a/FPrimeCfs/EvsMirror/EvsMirror.cpp b/FPrimeCfs/EvsMirror/EvsMirror.cpp index 1c77966..1992868 100644 --- a/FPrimeCfs/EvsMirror/EvsMirror.cpp +++ b/FPrimeCfs/EvsMirror/EvsMirror.cpp @@ -6,6 +6,15 @@ #include "FPrimeCfs/EvsMirror/EvsMirror.hpp" #include "Fw/Logger/Logger.hpp" +// Some cFE versions (e.g. draco) use compound literals in inline functions, +// which is a GCC extension when compiled as C++ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +extern "C" { +#include "cfe.h" // for CFE_EVS_SendEvent and event types +} +#pragma GCC diagnostic pop + static_assert(FW_ENABLE_TEXT_LOGGING, "EvsMirror requires text logging (FW_ENABLE_TEXT_LOGGING) to receive event text"); namespace FPrimeCfs { @@ -14,22 +23,10 @@ namespace FPrimeCfs { // Component construction and destruction // ---------------------------------------------------------------------- -EvsMirror ::EvsMirror(const char* const compName) - : EvsMirrorComponentBase(compName), m_appId(CFE_ES_APPID_UNDEFINED) {} +EvsMirror ::EvsMirror(const char* const compName) : EvsMirrorComponentBase(compName) {} EvsMirror ::~EvsMirror() {} -void EvsMirror ::init(FwEnumStoreType instance) { - EvsMirrorComponentBase::init(instance); - // Capture the host application ID while running on the app's main - // (ES-registered) task; see the declaration comment for the rationale - CFE_Status_t status = CFE_ES_GetAppID(&this->m_appId); - if (status != CFE_SUCCESS) { - this->m_appId = CFE_ES_APPID_UNDEFINED; - Fw::Logger::log("[ERROR] EvsMirror failed to get cFS app ID: 0x%08x\n", static_cast(status)); - } -} - // ---------------------------------------------------------------------- // Handler implementations for typed input ports // ---------------------------------------------------------------------- @@ -41,10 +38,8 @@ void EvsMirror ::TextLogger_handler(FwIndexType portNum, Fw::TextLogString& text) { // EVS event IDs are 16 bits; the F Prime event ID is truncated to its low 16 // bits. EVS truncates event text to CFE_MISSION_EVS_MAX_MESSAGE_LENGTH itself. - // CFE_EVS_SendEventWithAppID is used with the app ID captured in init() - // because this handler may run on a task unknown to CFE_ES - CFE_Status_t status = CFE_EVS_SendEventWithAppID(static_cast(id), EvsMirror::mapSeverity(severity), - this->m_appId, "%s", text.toChar()); + CFE_Status_t status = + CFE_EVS_SendEvent(static_cast(id), EvsMirror::mapSeverity(severity), "%s", text.toChar()); if (status != CFE_SUCCESS) { Fw::Logger::log("[ERROR] Failed to mirror event 0x%08x to EVS: 0x%08x\n", static_cast(id), static_cast(status)); diff --git a/FPrimeCfs/EvsMirror/EvsMirror.hpp b/FPrimeCfs/EvsMirror/EvsMirror.hpp index 380db16..52545f3 100644 --- a/FPrimeCfs/EvsMirror/EvsMirror.hpp +++ b/FPrimeCfs/EvsMirror/EvsMirror.hpp @@ -8,15 +8,6 @@ #include "FPrimeCfs/EvsMirror/EvsMirrorComponentAc.hpp" -// Some cFE versions (e.g. draco) use compound literals in inline functions, -// which is a GCC extension when compiled as C++ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" -extern "C" { -#include "cfe.h" // for CFE_ES_AppId_t -} -#pragma GCC diagnostic pop - namespace FPrimeCfs { class EvsMirror final : public EvsMirrorComponentBase { @@ -32,19 +23,6 @@ class EvsMirror final : public EvsMirrorComponentBase { //! Destroy EvsMirror object ~EvsMirror(); - //! Initialize EvsMirror object - //! - //! Overrides the base initialization to also capture the host cFS - //! application ID. EVS resolves the calling task's application context, - //! but the TextLogger handler runs on F Prime threads that are not - //! created through CFE_ES and therefore have no ES task record, making - //! CFE_EVS_SendEvent fail with CFE_EVS_APP_ILLEGAL_APP_ID. init() runs - //! on the app's main (ES-registered) task during topology setup, so the - //! app ID is captured here and events are mirrored with - //! CFE_EVS_SendEventWithAppID instead. - void init(FwEnumStoreType instance = 0 //!< The instance number - ); - private: // ---------------------------------------------------------------------- // Handler implementations for typed input ports @@ -66,13 +44,6 @@ class EvsMirror final : public EvsMirrorComponentBase { //! Map an F Prime event severity to a cFS EVS event type static U16 mapSeverity(const Fw::LogSeverity& severity); - - // ---------------------------------------------------------------------- - // Member variables - // ---------------------------------------------------------------------- - - //! Host cFS application ID, captured on the main task in init() - CFE_ES_AppId_t m_appId; }; } // namespace FPrimeCfs diff --git a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp index 43a53e1..967184a 100644 --- a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp +++ b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.cpp @@ -9,20 +9,15 @@ namespace FPrimeCfs { -const CFE_ES_AppId_t EvsMirrorTester::TEST_APP_ID; - // ---------------------------------------------------------------------- // Construction and destruction // ---------------------------------------------------------------------- EvsMirrorTester ::EvsMirrorTester() : EvsMirrorGTestBase("EvsMirrorTester", EvsMirrorTester::MAX_HISTORY_SIZE), component("EvsMirror") { - // Inject the app ID before init so the component captures it there - CfeStub::reset(); - CfeStub::state().appId = EvsMirrorTester::TEST_APP_ID; this->initComponents(); this->connectPorts(); - CfeStub::state().appId = CFE_ES_APPID_UNDEFINED; + CfeStub::reset(); } EvsMirrorTester ::~EvsMirrorTester() { @@ -49,8 +44,6 @@ void EvsMirrorTester ::testMirror() { const CfeStub::SendEventCall& call = CfeStub::state().sendEventCalls[0]; ASSERT_EQ(call.eventId, static_cast(id)); ASSERT_EQ(call.eventType, CFE_EVS_EventType_ERROR); - // The app ID captured at init() is used, not a live lookup - ASSERT_EQ(call.appId, EvsMirrorTester::TEST_APP_ID); ASSERT_STREQ(call.text, text.toChar()); ASSERT_EQ(logger.messageCount, 0u); } diff --git a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp index ae090a4..31750fd 100644 --- a/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp +++ b/FPrimeCfs/EvsMirror/test/ut/EvsMirrorTester.hpp @@ -24,9 +24,6 @@ class EvsMirrorTester final : public EvsMirrorGTestBase { // Instance ID supplied to the component instance under test static const FwEnumStoreType TEST_INSTANCE_ID = 0; - // App ID injected into the CFE_ES_GetAppID stub before component init - static const CFE_ES_AppId_t TEST_APP_ID = 0x1234; - public: // ---------------------------------------------------------------------- // Construction and destruction diff --git a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp index 5d504e8..ff89ec7 100644 --- a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp +++ b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.cpp @@ -17,7 +17,6 @@ State& state() { void reset() { (void)memset(&state(), 0, sizeof(State)); state().sendEventStatus = CFE_SUCCESS; - state().getAppIdStatus = CFE_SUCCESS; } } // namespace CfeStub @@ -28,7 +27,6 @@ extern "C" CFE_Status_t CFE_EVS_SendEvent(uint16 EventID, uint16 EventType, cons CfeStub::SendEventCall& call = s.sendEventCalls[s.sendEventCount]; call.eventId = EventID; call.eventType = EventType; - call.appId = CFE_ES_APPID_UNDEFINED; va_list args; va_start(args, Spec); (void)vsnprintf(call.text, sizeof call.text, Spec, args); @@ -38,29 +36,3 @@ extern "C" CFE_Status_t CFE_EVS_SendEvent(uint16 EventID, uint16 EventType, cons s.sendEventCount++; return s.sendEventStatus; } - -extern "C" CFE_Status_t CFE_EVS_SendEventWithAppID(uint16 EventID, uint16 EventType, CFE_ES_AppId_t AppID, const char* Spec, ...) { - CfeStub::State& s = CfeStub::state(); - if (s.sendEventCount < CfeStub::STUB_MAX_ENTRIES) { - CfeStub::SendEventCall& call = s.sendEventCalls[s.sendEventCount]; - call.eventId = EventID; - call.eventType = EventType; - call.appId = AppID; - va_list args; - va_start(args, Spec); - (void)vsnprintf(call.text, sizeof call.text, Spec, args); - va_end(args); - } - // Counts every call; only the first STUB_MAX_ENTRIES calls are recorded in sendEventCalls - s.sendEventCount++; - return s.sendEventStatus; -} - -extern "C" CFE_Status_t CFE_ES_GetAppID(CFE_ES_AppId_t* AppIdPtr) { - CfeStub::State& s = CfeStub::state(); - s.getAppIdCount++; - if (s.getAppIdStatus == CFE_SUCCESS) { - *AppIdPtr = s.appId; - } - return s.getAppIdStatus; -} diff --git a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp index b9828ab..8d79d85 100644 --- a/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp +++ b/FPrimeCfs/EvsMirror/test/ut/stubs/CfeStubs.hpp @@ -17,26 +17,20 @@ static const unsigned int STUB_MAX_ENTRIES = 32; //! Maximum formatted text bytes captured per sent event static const unsigned int STUB_MAX_TEXT = 256; -//! Record of a CFE_EVS_SendEventWithAppID call +//! Record of a CFE_EVS_SendEvent call struct SendEventCall { uint16 eventId; uint16 eventType; - CFE_ES_AppId_t appId; char text[STUB_MAX_TEXT]; }; //! Shared state of the cFE stub layer struct State { - // Injectable return statuses + // Injectable return status CFE_Status_t sendEventStatus; - CFE_Status_t getAppIdStatus; - - // Injectable app ID returned by CFE_ES_GetAppID - CFE_ES_AppId_t appId; // Call records unsigned int sendEventCount; - unsigned int getAppIdCount; SendEventCall sendEventCalls[STUB_MAX_ENTRIES]; }; diff --git a/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h b/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h index 3c72458..fa5b098 100644 --- a/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h +++ b/FPrimeCfs/EvsMirror/test/ut/stubs/cfe.h @@ -20,11 +20,9 @@ typedef uint32_t uint32; typedef int32_t int32; typedef int32 CFE_Status_t; -typedef uint32 CFE_ES_AppId_t; #define CFE_SUCCESS ((CFE_Status_t)0) #define CFE_EVS_APP_NOT_REGISTERED ((CFE_Status_t)0xc2000005) -#define CFE_ES_APPID_UNDEFINED ((CFE_ES_AppId_t)0) /* Event types (values match CFE_EVS_EventType_Enum_t) */ #define CFE_EVS_EventType_DEBUG (1) @@ -34,10 +32,6 @@ typedef uint32 CFE_ES_AppId_t; /* Event services API (implemented by CfeStubs.cpp) */ CFE_Status_t CFE_EVS_SendEvent(uint16 EventID, uint16 EventType, const char* Spec, ...); -CFE_Status_t CFE_EVS_SendEventWithAppID(uint16 EventID, uint16 EventType, CFE_ES_AppId_t AppID, const char* Spec, ...); - -/* Executive services API (implemented by CfeStubs.cpp) */ -CFE_Status_t CFE_ES_GetAppID(CFE_ES_AppId_t* AppIdPtr); #if defined(__cplusplus) } From ad2941ecd1f3b42a1ef0d023ff7b4531cfec141c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:17:11 +0000 Subject: [PATCH 4/4] Os_Task_Cfs: simplify tail-keeping name truncation to a pointer offset Pass the (offset) name pointer directly to CFE_ES_CreateChildTask instead of copying into a local buffer. Co-Authored-By: michael.d.starch --- FPrimeCfs/Os/Cfs/Task.cpp | 130 ++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/FPrimeCfs/Os/Cfs/Task.cpp b/FPrimeCfs/Os/Cfs/Task.cpp index 767c651..0322665 100644 --- a/FPrimeCfs/Os/Cfs/Task.cpp +++ b/FPrimeCfs/Os/Cfs/Task.cpp @@ -7,19 +7,21 @@ #include "FPrimeCfs/Os/Cfs/Task.hpp" #include "Fw/Logger/Logger.hpp" #include "Fw/Types/Assert.hpp" -#include "Fw/Types/StringUtils.hpp" -namespace Os { -namespace Cfs { -namespace Task { +namespace Os +{ +namespace Cfs +{ +namespace Task +{ // Out-of-class definitions for odr-used constexpr members (required in C++14) constexpr CFE_ES_TaskPriority_Atom_t CfsTask::DEFAULT_PRIORITY; -constexpr FwSizeType CfsTask::MIN_PRIORITY; -constexpr FwSizeType CfsTask::MAX_PRIORITY; -constexpr FwSizeType CfsTask::DEFAULT_STACK_SIZE; -constexpr U32 CfsTask::HANDOFF_POLL_LIMIT; -constexpr U32 CfsTask::JOIN_POLL_DELAY_MS; +constexpr FwSizeType CfsTask::MIN_PRIORITY; +constexpr FwSizeType CfsTask::MAX_PRIORITY; +constexpr FwSizeType CfsTask::DEFAULT_STACK_SIZE; +constexpr U32 CfsTask::HANDOFF_POLL_LIMIT; +constexpr U32 CfsTask::JOIN_POLL_DELAY_MS; //! Handoff slot used to deliver the F Prime routine and argument to a newly //! created child task: the cFE child task entry point takes no arguments, so @@ -29,14 +31,16 @@ constexpr U32 CfsTask::JOIN_POLL_DELAY_MS; //! another task, so at most one handoff is pending at a time. Task creation //! is restricted to the app's main task by cFE, which also serializes the //! writers. -static Os::Task::taskRoutine s_handoff_routine = nullptr; -static void* s_handoff_argument = nullptr; -static std::atomic s_handoff_taken(true); +static Os::Task::taskRoutine s_handoff_routine = nullptr; +static void *s_handoff_argument = nullptr; +static std::atomic s_handoff_taken(true); //! \brief map a CFE_ES_CreateChildTask status to an Os::Task status -static Os::Task::Status map_create_status(CFE_Status_t cfe_status) { +static Os::Task::Status map_create_status(CFE_Status_t cfe_status) +{ Os::Task::Status status = Os::Task::Status::UNKNOWN_ERROR; - switch (cfe_status) { + switch (cfe_status) + { case CFE_SUCCESS: status = Os::Task::Status::OP_OK; break; @@ -59,10 +63,11 @@ static Os::Task::Status map_create_status(CFE_Status_t cfe_status) { return status; } -void CfsTask::childTaskEntry() { +void CfsTask::childTaskEntry() +{ // Consume the pending routine/argument pair published by start() - Os::Task::taskRoutine routine = s_handoff_routine; - void* argument = s_handoff_argument; + Os::Task::taskRoutine routine = s_handoff_routine; + void *argument = s_handoff_argument; s_handoff_taken.store(true); FW_ASSERT(routine != nullptr); routine(argument); @@ -72,25 +77,31 @@ void CfsTask::childTaskEntry() { void CfsTask::onStart() {} -Os::Task::Status CfsTask::start(const Arguments& arguments) { +Os::Task::Status CfsTask::start(const Arguments &arguments) +{ FW_ASSERT(arguments.m_routine != nullptr); // Affinity is not supported by the cFE child task API - if (arguments.m_cpuAffinity != Os::Task::TASK_DEFAULT) { - Fw::Logger::log("[WARNING] %s cpu affinity is not supported by cFE child tasks\n", - arguments.m_name.toChar()); + if (arguments.m_cpuAffinity != Os::Task::TASK_DEFAULT) + { + Fw::Logger::log("[WARNING] %s cpu affinity is not supported by cFE child tasks\n", arguments.m_name.toChar()); } // Priorities pass through directly as OSAL/cFS priorities (lower value = // more urgent), clamped to the valid OSAL range FwSizeType priority = static_cast(arguments.m_priority); - if (arguments.m_priority == Os::Task::TASK_PRIORITY_DEFAULT) { + if (arguments.m_priority == Os::Task::TASK_PRIORITY_DEFAULT) + { priority = static_cast(CfsTask::DEFAULT_PRIORITY); - } else if (priority < CfsTask::MIN_PRIORITY) { + } + else if (priority < CfsTask::MIN_PRIORITY) + { Fw::Logger::log("[WARNING] %s task priority of %" PRI_FwSizeType " clamped to %" PRI_FwSizeType "\n", arguments.m_name.toChar(), priority, CfsTask::MIN_PRIORITY); priority = CfsTask::MIN_PRIORITY; - } else if (priority > CfsTask::MAX_PRIORITY) { + } + else if (priority > CfsTask::MAX_PRIORITY) + { Fw::Logger::log("[WARNING] %s task priority of %" PRI_FwSizeType " clamped to %" PRI_FwSizeType "\n", arguments.m_name.toChar(), priority, CfsTask::MAX_PRIORITY); priority = CfsTask::MAX_PRIORITY; @@ -103,26 +114,27 @@ Os::Task::Status CfsTask::start(const Arguments& arguments) { // preceding start() call's handoff must have completed (s_handoff_taken // is initialized true and set true by each child on startup). FW_ASSERT(s_handoff_taken.load()); - s_handoff_routine = arguments.m_routine; + s_handoff_routine = arguments.m_routine; s_handoff_argument = arguments.m_routine_argument; s_handoff_taken.store(false); - // OSAL limits task names to OS_MAX_API_NAME including the terminator; - // keep the distinctive trailing characters when truncating - char name[OS_MAX_API_NAME]; - const char* fullName = arguments.m_name.toChar(); + // OSAL limits task names to OS_MAX_API_NAME including the terminator; keep + // the trailing characters when truncating, as fully-qualified F Prime names + // share a topology prefix and the tail carries the distinguishing name + const char *name = arguments.m_name.toChar(); const FwSizeType nameLength = arguments.m_name.length(); - if (nameLength >= static_cast(OS_MAX_API_NAME)) { - fullName = fullName + (nameLength - (OS_MAX_API_NAME - 1)); - Fw::Logger::log("[WARNING] %s task name truncated to %s\n", arguments.m_name.toChar(), fullName); + if (nameLength >= static_cast(OS_MAX_API_NAME)) + { + name = &name[nameLength - (OS_MAX_API_NAME - 1)]; + Fw::Logger::log("[WARNING] %s task name truncated to %s\n", arguments.m_name.toChar(), name); } - (void)Fw::StringUtils::string_copy(name, fullName, static_cast(sizeof(name))); CFE_ES_TaskId_t taskId = CFE_ES_TASKID_UNDEFINED; - CFE_Status_t cfe_status = CFE_ES_CreateChildTask( - &taskId, name, CfsTask::childTaskEntry, CFE_ES_TASK_STACK_ALLOCATE, - static_cast(stackSize), static_cast(priority), 0); - if (cfe_status != CFE_SUCCESS) { + CFE_Status_t cfe_status = + CFE_ES_CreateChildTask(&taskId, name, CfsTask::childTaskEntry, CFE_ES_TASK_STACK_ALLOCATE, + static_cast(stackSize), static_cast(priority), 0); + if (cfe_status != CFE_SUCCESS) + { // The child task will never run: reclaim the handoff slot s_handoff_taken.store(true); Fw::Logger::log("[ERROR] %s failed to create cFE child task: 0x%08" PRIx32 "\n", arguments.m_name.toChar(), @@ -133,8 +145,10 @@ Os::Task::Status CfsTask::start(const Arguments& arguments) { // Wait (bounded) for the child task to consume its routine/argument pair // before allowing another handoff to be published U32 polls = 0; - for (polls = 0; polls < CfsTask::HANDOFF_POLL_LIMIT; polls++) { - if (s_handoff_taken.load()) { + for (polls = 0; polls < CfsTask::HANDOFF_POLL_LIMIT; polls++) + { + if (s_handoff_taken.load()) + { break; } (void)OS_TaskDelay(1); @@ -142,22 +156,26 @@ Os::Task::Status CfsTask::start(const Arguments& arguments) { // A created child task that never runs indicates a system-level fault FW_ASSERT(polls < CfsTask::HANDOFF_POLL_LIMIT, static_cast(polls)); - this->m_handle.m_task_id = taskId; + this->m_handle.m_task_id = taskId; this->m_handle.m_is_valid = true; return Os::Task::Status::OP_OK; } -Os::Task::Status CfsTask::join() { - if (not this->m_handle.m_is_valid) { +Os::Task::Status CfsTask::join() +{ + if (not this->m_handle.m_is_valid) + { return Os::Task::Status::INVALID_HANDLE; } // cFE has no native join: poll until the child task record disappears. // This loop is unbounded by design, matching join semantics (blocks until // the task exits), as with pthread_join in the Posix implementation. - for (;;) { + for (;;) + { CFE_ES_TaskInfo_t info; - CFE_Status_t status = CFE_ES_GetTaskInfo(&info, this->m_handle.m_task_id); - if (status != CFE_SUCCESS) { + CFE_Status_t status = CFE_ES_GetTaskInfo(&info, this->m_handle.m_task_id); + if (status != CFE_SUCCESS) + { break; } (void)OS_TaskDelay(CfsTask::JOIN_POLL_DELAY_MS); @@ -166,27 +184,31 @@ Os::Task::Status CfsTask::join() { return Os::Task::Status::OP_OK; } -TaskHandle* CfsTask::getHandle() { +TaskHandle *CfsTask::getHandle() +{ return &this->m_handle; } // Note: not implemented for cFE child tasks. cFE ES provides no child task // suspend/resume API; this matches the Posix implementation's behavior. -void CfsTask::suspend(Os::Task::SuspensionType suspensionType) { +void CfsTask::suspend(Os::Task::SuspensionType suspensionType) +{ FW_ASSERT(false); } -void CfsTask::resume() { +void CfsTask::resume() +{ FW_ASSERT(false); } -Os::Task::Status CfsTask::_delay(const Fw::TimeInterval& interval) { +Os::Task::Status CfsTask::_delay(const Fw::TimeInterval &interval) +{ // Round up to whole milliseconds: OS_TaskDelay has millisecond granularity - U32 delay_ms = (interval.getSeconds() * 1000) + ((interval.getUSeconds() + 999) / 1000); - int32 status = OS_TaskDelay(delay_ms); + U32 delay_ms = (interval.getSeconds() * 1000) + ((interval.getUSeconds() + 999) / 1000); + int32 status = OS_TaskDelay(delay_ms); return (status == OS_SUCCESS) ? Os::Task::Status::OP_OK : Os::Task::Status::DELAY_ERROR; } -} // end namespace Task -} // end namespace Cfs -} // end namespace Os +} // end namespace Task +} // end namespace Cfs +} // end namespace Os