From b0716ad6059371e8c4023dde5abb2f66ea403ef1 Mon Sep 17 00:00:00 2001 From: Zimri Leisher Date: Tue, 17 Sep 2024 19:55:09 -0500 Subject: [PATCH] Add sequence dispatcher component (#2731) * Add sequence dispatcher component * Add seq start port to cmd sequencer * Update author names and some include paths * Get fully compiling, move consts/enums to correct places, check for connections on init * Add spelling exceptions * Get unit tests almost compiling... * Fix string type in port, call component init in test * Fix unit test compilation errors and assertions * Switch back to using StringBase * Switch to FwIndexType, remove textLogIn * UpperCamel events, add warning for unexpected seq start * remove init method, add check for connected to getNextAvailableIdx * Update sdd, change event from low to high, static cast a portnum * Add state diagram, add more warnings, fix wrong header types, use assert instead of warning for runSeq --------- Co-authored-by: Zimri Leisher --- .github/actions/spelling/expect.txt | 3 + Svc/CMakeLists.txt | 1 + Svc/CmdSequencer/CmdSequencer.fpp | 3 + Svc/CmdSequencer/CmdSequencerImpl.cpp | 11 +- Svc/CmdSequencer/CmdSequencerImpl.hpp | 9 +- Svc/CmdSequencer/Sequence.cpp | 7 + Svc/Seq/Seq.fpp | 2 +- Svc/SeqDispatcher/CMakeLists.txt | 24 +++ Svc/SeqDispatcher/SeqDispatcher.cpp | 186 ++++++++++++++++++ Svc/SeqDispatcher/SeqDispatcher.fpp | 55 ++++++ Svc/SeqDispatcher/SeqDispatcher.hpp | 95 +++++++++ Svc/SeqDispatcher/SeqDispatcherCommands.fppi | 9 + Svc/SeqDispatcher/SeqDispatcherEvents.fppi | 38 ++++ Svc/SeqDispatcher/SeqDispatcherTelemetry.fppi | 8 + Svc/SeqDispatcher/docs/sdd.md | 51 +++++ .../docs/seq_dispatcher_model.png | Bin 0 -> 37139 bytes .../test/ut/SeqDispatcherTestMain.cpp | 21 ++ .../test/ut/SeqDispatcherTester.cpp | 92 +++++++++ .../test/ut/SeqDispatcherTester.hpp | 79 ++++++++ config/AcConstants.fpp | 3 + docs/UsersGuide/dev/configuring-fprime.md | 2 +- 21 files changed, 695 insertions(+), 4 deletions(-) create mode 100644 Svc/SeqDispatcher/CMakeLists.txt create mode 100644 Svc/SeqDispatcher/SeqDispatcher.cpp create mode 100644 Svc/SeqDispatcher/SeqDispatcher.fpp create mode 100644 Svc/SeqDispatcher/SeqDispatcher.hpp create mode 100644 Svc/SeqDispatcher/SeqDispatcherCommands.fppi create mode 100644 Svc/SeqDispatcher/SeqDispatcherEvents.fppi create mode 100644 Svc/SeqDispatcher/SeqDispatcherTelemetry.fppi create mode 100644 Svc/SeqDispatcher/docs/sdd.md create mode 100644 Svc/SeqDispatcher/docs/seq_dispatcher_model.png create mode 100644 Svc/SeqDispatcher/test/ut/SeqDispatcherTestMain.cpp create mode 100644 Svc/SeqDispatcher/test/ut/SeqDispatcherTester.cpp create mode 100644 Svc/SeqDispatcher/test/ut/SeqDispatcherTester.hpp diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index a55382cce5..d608eeac1f 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -152,6 +152,7 @@ CMDPACKET CMDREG cmds CMDSEQ +cmdsequencer cnt cntx cobj @@ -548,6 +549,7 @@ lammertbies LASTLOG LBLOCK LCHILD +leisher lemstarch lestarch levelname @@ -1212,4 +1214,5 @@ xsh xsltproc xxxx yacgen +zimri zmq \ No newline at end of file diff --git a/Svc/CMakeLists.txt b/Svc/CMakeLists.txt index 37de2e0110..2ab95efc03 100644 --- a/Svc/CMakeLists.txt +++ b/Svc/CMakeLists.txt @@ -42,6 +42,7 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PassiveRateGroup") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PolyDb/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PrmDb/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/RateGroupDriver/") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SeqDispatcher/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/StaticMemory/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/TlmChan/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/TlmPacketizer/") diff --git a/Svc/CmdSequencer/CmdSequencer.fpp b/Svc/CmdSequencer/CmdSequencer.fpp index 52df683856..df55efaa74 100644 --- a/Svc/CmdSequencer/CmdSequencer.fpp +++ b/Svc/CmdSequencer/CmdSequencer.fpp @@ -85,6 +85,9 @@ module Svc { @ Schedule in port async input port schedIn: Svc.Sched + @ Notifies that a sequence has started running + output port seqStartOut: Svc.CmdSeqIn + # ---------------------------------------------------------------------- # Commands # ---------------------------------------------------------------------- diff --git a/Svc/CmdSequencer/CmdSequencerImpl.cpp b/Svc/CmdSequencer/CmdSequencerImpl.cpp index 7e1c84d25b..88e2018a62 100644 --- a/Svc/CmdSequencer/CmdSequencerImpl.cpp +++ b/Svc/CmdSequencer/CmdSequencerImpl.cpp @@ -122,6 +122,9 @@ namespace Svc { // Check the step mode. If it is auto, start the sequence if (AUTO == this->m_stepMode) { this->m_runMode = RUNNING; + if(this->isConnected_seqStartOut_OutputPort(0)) { + this->seqStartOut_out(0, this->m_sequence->getStringFileName()); + } this->performCmd_Step(); } @@ -159,7 +162,7 @@ namespace Svc { //! Handler for input port seqRunIn void CmdSequencerComponentImpl::seqRunIn_handler( NATIVE_INT_TYPE portNum, - Fw::String &filename + const Fw::StringBase& filename ) { if (!this->requireRunMode(STOPPED)) { @@ -190,6 +193,9 @@ namespace Svc { // Check the step mode. If it is auto, start the sequence if (AUTO == this->m_stepMode) { this->m_runMode = RUNNING; + if(this->isConnected_seqStartOut_OutputPort(0)) { + this->seqStartOut_out(0, this->m_sequence->getStringFileName()); + } this->performCmd_Step(); } @@ -359,6 +365,9 @@ namespace Svc { this->m_runMode = RUNNING; this->performCmd_Step(); this->log_ACTIVITY_HI_CS_CmdStarted(this->m_sequence->getLogFileName()); + if(this->isConnected_seqStartOut_OutputPort(0)) { + this->seqStartOut_out(0, this->m_sequence->getStringFileName()); + } this->cmdResponse_out(opcode, cmdSeq, Fw::CmdResponse::OK); } diff --git a/Svc/CmdSequencer/CmdSequencerImpl.hpp b/Svc/CmdSequencer/CmdSequencerImpl.hpp index 6e595000b4..655cebd4f3 100644 --- a/Svc/CmdSequencer/CmdSequencerImpl.hpp +++ b/Svc/CmdSequencer/CmdSequencerImpl.hpp @@ -235,6 +235,10 @@ namespace Svc { //! \return The log file name Fw::LogStringArg& getLogFileName(); + //! Get the normal string file name + //! \return The normal string file name + Fw::String& getStringFileName(); + //! Get the sequence header const Header& getHeader() const; @@ -277,6 +281,9 @@ namespace Svc { //! Copy of file name for events Fw::LogStringArg m_logFileName; + //! Copy of file name for ports + Fw::String m_stringFileName; + //! Serialize buffer to hold the binary sequence data Fw::ExternalSerializeBuffer m_buffer; @@ -582,7 +589,7 @@ namespace Svc { //! Handler for input port seqRunIn void seqRunIn_handler( NATIVE_INT_TYPE portNum, //!< The port number - Fw::String &filename //!< The sequence file + const Fw::StringBase& filename //!< The sequence file ); //! Handler for ping port diff --git a/Svc/CmdSequencer/Sequence.cpp b/Svc/CmdSequencer/Sequence.cpp index 235de84c13..30cc2ccb50 100644 --- a/Svc/CmdSequencer/Sequence.cpp +++ b/Svc/CmdSequencer/Sequence.cpp @@ -112,6 +112,7 @@ namespace Svc { { this->m_fileName = fileName; this->m_logFileName = fileName; + this->m_stringFileName = fileName; } Fw::CmdStringArg& CmdSequencerComponentImpl::Sequence :: @@ -126,5 +127,11 @@ namespace Svc { return this->m_logFileName; } + Fw::String& CmdSequencerComponentImpl::Sequence :: + getStringFileName() + { + return this->m_stringFileName; + } + } diff --git a/Svc/Seq/Seq.fpp b/Svc/Seq/Seq.fpp index 66a5d9888c..1caf1ce037 100644 --- a/Svc/Seq/Seq.fpp +++ b/Svc/Seq/Seq.fpp @@ -2,7 +2,7 @@ module Svc { @ Port to request a sequence be run port CmdSeqIn( - ref filename: Fw.String @< The sequence file + filename: string size 240 @< The sequence file ) @ Port to cancel a sequence diff --git a/Svc/SeqDispatcher/CMakeLists.txt b/Svc/SeqDispatcher/CMakeLists.txt new file mode 100644 index 0000000000..bb92108919 --- /dev/null +++ b/Svc/SeqDispatcher/CMakeLists.txt @@ -0,0 +1,24 @@ +#### +# F prime CMakeLists.txt: +# +# SOURCE_FILES: combined list of source and autocoding files +# MOD_DEPS: (optional) module dependencies +# UT_SOURCE_FILES: list of source files for unit tests +# +#### +set(SOURCE_FILES + "${CMAKE_CURRENT_LIST_DIR}/SeqDispatcher.fpp" + "${CMAKE_CURRENT_LIST_DIR}/SeqDispatcher.cpp" +) + +register_fprime_module() + +### UTS ### +set(UT_AUTO_HELPERS ON) + +set(UT_SOURCE_FILES + "${FPRIME_FRAMEWORK_PATH}/Svc/SeqDispatcher/SeqDispatcher.fpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/SeqDispatcherTester.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/SeqDispatcherTestMain.cpp" +) +register_fprime_ut() diff --git a/Svc/SeqDispatcher/SeqDispatcher.cpp b/Svc/SeqDispatcher/SeqDispatcher.cpp new file mode 100644 index 0000000000..c55bda62c0 --- /dev/null +++ b/Svc/SeqDispatcher/SeqDispatcher.cpp @@ -0,0 +1,186 @@ +// ====================================================================== +// \title SeqDispatcher.cpp +// \author zimri.leisher +// \brief cpp file for SeqDispatcher component implementation class +// ====================================================================== + +#include + +namespace Svc { + +// ---------------------------------------------------------------------- +// Construction, initialization, and destruction +// ---------------------------------------------------------------------- + +SeqDispatcher ::SeqDispatcher(const char* const compName) + : SeqDispatcherComponentBase(compName) {} + +SeqDispatcher ::~SeqDispatcher() {} + +FwIndexType SeqDispatcher::getNextAvailableSequencerIdx() { + for (FwIndexType i = 0; i < SeqDispatcherSequencerPorts; i++) { + if (this->isConnected_seqRunOut_OutputPort(i) && + this->m_entryTable[i].state == SeqDispatcher_CmdSequencerState::AVAILABLE) { + return i; + } + } + return -1; +} + +void SeqDispatcher::runSequence(FwIndexType sequencerIdx, + const Fw::StringBase& fileName, + Fw::Wait block) { + // this function is only designed for internal usage + // we can guarantee it cannot be called with input that would fail + FW_ASSERT(sequencerIdx >= 0 && sequencerIdx < SeqDispatcherSequencerPorts, + sequencerIdx); + FW_ASSERT(this->isConnected_seqRunOut_OutputPort(sequencerIdx)); + FW_ASSERT(this->m_entryTable[sequencerIdx].state == + SeqDispatcher_CmdSequencerState::AVAILABLE, + this->m_entryTable[sequencerIdx].state); + + if (block == Fw::Wait::NO_WAIT) { + this->m_entryTable[sequencerIdx].state = + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_NO_BLOCK; + } else { + this->m_entryTable[sequencerIdx].state = + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_BLOCK; + } + + this->m_sequencersAvailable--; + this->tlmWrite_sequencersAvailable(this->m_sequencersAvailable); + this->m_entryTable[sequencerIdx].sequenceRunning = fileName; + + this->m_dispatchedCount++; + this->tlmWrite_dispatchedCount(this->m_dispatchedCount); + this->seqRunOut_out(sequencerIdx, + this->m_entryTable[sequencerIdx].sequenceRunning); +} + +void SeqDispatcher::seqStartIn_handler( + NATIVE_INT_TYPE portNum, //!< The port number + const Fw::StringBase& fileName //!< The sequence file name +) { + FW_ASSERT(portNum >= 0 && portNum < SeqDispatcherSequencerPorts, portNum); + if (this->m_entryTable[portNum].state == + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_BLOCK || + this->m_entryTable[portNum].state == + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_NO_BLOCK) { + // we were aware of this sequencer running a sequence + if (this->m_entryTable[portNum].sequenceRunning != fileName) { + // uh oh. entry table is wrong + // let's just update it to be correct. nothing we can do about + // it except raise a warning and update our state + this->log_WARNING_HI_ConflictingSequenceStarted(static_cast(portNum), fileName, this->m_entryTable[portNum].sequenceRunning); + this->m_entryTable[portNum].sequenceRunning = fileName; + } + } else { + // we were not aware that this sequencer was running. ground must have + // directly commanded that specific sequencer + + // warn because this may be unintentional + this->log_WARNING_LO_UnexpectedSequenceStarted(static_cast(portNum), fileName); + + // update the state + this->m_entryTable[portNum].state = + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_NO_BLOCK; + this->m_entryTable[portNum].sequenceRunning = fileName; + this->m_sequencersAvailable--; + this->tlmWrite_sequencersAvailable(this->m_sequencersAvailable); + } +} + +void SeqDispatcher::seqDoneIn_handler( + NATIVE_INT_TYPE portNum, //!< The port number + FwOpcodeType opCode, //!< Command Op Code + U32 cmdSeq, //!< Command Sequence + const Fw::CmdResponse& response //!< The command response argument +) { + FW_ASSERT(portNum >= 0 && portNum < SeqDispatcherSequencerPorts, portNum); + if (this->m_entryTable[portNum].state != + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_BLOCK && + this->m_entryTable[portNum].state != + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_NO_BLOCK) { + // this sequencer was not running a sequence that we were aware of. + + // we should have caught this in seqStartIn and updated the state + // accordingly, but somehow we didn't? very sad and shouldn't happen + + // anyways, don't have to do anything cuz now that this seq we didn't know + // about is done, the sequencer is available again (which is its current + // state in our internal entry table already) + this->log_WARNING_LO_UnknownSequenceFinished(static_cast(portNum)); + } else { + // ok, a sequence has finished that we knew about + if (this->m_entryTable[portNum].state == + SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_BLOCK) { + // we need to give a cmd response cuz some other sequence is being blocked + // by this + this->cmdResponse_out(this->m_entryTable[portNum].opCode, + this->m_entryTable[portNum].cmdSeq, response); + + if (response == Fw::CmdResponse::EXECUTION_ERROR) { + // dispatched sequence errored + this->m_errorCount++; + this->tlmWrite_errorCount(this->m_errorCount); + } + } + } + + // all command responses mean the sequence is no longer running + // so component should be available + this->m_entryTable[portNum].state = SeqDispatcher_CmdSequencerState::AVAILABLE; + this->m_entryTable[portNum].sequenceRunning = ""; + this->m_sequencersAvailable++; + this->tlmWrite_sequencersAvailable(this->m_sequencersAvailable); +} + +//! Handler for input port seqRunIn +void SeqDispatcher::seqRunIn_handler(NATIVE_INT_TYPE portNum, + const Fw::StringBase& fileName) { + FwIndexType idx = this->getNextAvailableSequencerIdx(); + // no available sequencers + if (idx == -1) { + this->log_WARNING_HI_NoAvailableSequencers(); + return; + } + + this->runSequence(idx, fileName, Fw::Wait::NO_WAIT); +} +// ---------------------------------------------------------------------- +// Command handler implementations +// ---------------------------------------------------------------------- + +void SeqDispatcher ::RUN_cmdHandler(const FwOpcodeType opCode, + const U32 cmdSeq, + const Fw::CmdStringArg& fileName, + Fw::Wait block) { + FwIndexType idx = this->getNextAvailableSequencerIdx(); + // no available sequencers + if (idx == -1) { + this->log_WARNING_HI_NoAvailableSequencers(); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + this->runSequence(idx, fileName, block); + + if (block == Fw::Wait::NO_WAIT) { + // return instantly + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + } else { + // otherwise don't return a response yet. just save the opCode and cmdSeq + // so we can return a response later + this->m_entryTable[idx].opCode = opCode; + this->m_entryTable[idx].cmdSeq = cmdSeq; + } +} + +void SeqDispatcher::LOG_STATUS_cmdHandler( + const FwOpcodeType opCode, /*!< The opcode*/ + const U32 cmdSeq) { /*!< The command sequence number*/ + for(FwIndexType idx = 0; idx < SeqDispatcherSequencerPorts; idx++) { + this->log_ACTIVITY_LO_LogSequencerStatus(static_cast(idx), this->m_entryTable[idx].state, Fw::LogStringArg(this->m_entryTable[idx].sequenceRunning)); + } +} +} // end namespace components diff --git a/Svc/SeqDispatcher/SeqDispatcher.fpp b/Svc/SeqDispatcher/SeqDispatcher.fpp new file mode 100644 index 0000000000..3fc6d9702f --- /dev/null +++ b/Svc/SeqDispatcher/SeqDispatcher.fpp @@ -0,0 +1,55 @@ +module Svc { + @ Dispatches command sequences to available command sequencers + active component SeqDispatcher { + + enum CmdSequencerState { + AVAILABLE = 0 + RUNNING_SEQUENCE_BLOCK = 1 + RUNNING_SEQUENCE_NO_BLOCK = 2 + } + + include "SeqDispatcherCommands.fppi" + include "SeqDispatcherTelemetry.fppi" + include "SeqDispatcherEvents.fppi" + + @ Dispatches a sequence to the first available command sequencer + async input port seqRunIn: Svc.CmdSeqIn + + output port seqRunOut: [SeqDispatcherSequencerPorts] Svc.CmdSeqIn + + @ Called by a command sequencer whenever it has finished any sequence + async input port seqDoneIn: [SeqDispatcherSequencerPorts] Fw.CmdResponse + + @ Called by cmdsequencer whenever it starts any sequence + async input port seqStartIn: [SeqDispatcherSequencerPorts] Svc.CmdSeqIn + + match seqRunOut with seqDoneIn + + match seqRunOut with seqStartIn + + ############################################################################### + # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # + ############################################################################### + @ Port for requesting the current time + time get port timeCaller + + @ Port for sending command registrations + command reg port cmdRegOut + + @ Port for receiving commands + command recv port cmdIn + + @ Port for sending command responses + command resp port cmdResponseOut + + @ Port for sending textual representation of events + text event port logTextOut + + @ Port for sending events to downlink + event port logOut + + @ Port for sending telemetry channels to downlink + telemetry port tlmOut + + } +} \ No newline at end of file diff --git a/Svc/SeqDispatcher/SeqDispatcher.hpp b/Svc/SeqDispatcher/SeqDispatcher.hpp new file mode 100644 index 0000000000..9c8b9f6806 --- /dev/null +++ b/Svc/SeqDispatcher/SeqDispatcher.hpp @@ -0,0 +1,95 @@ +// ====================================================================== +// \title SeqDispatcher.hpp +// \author zimri.leisher +// \brief hpp file for SeqDispatcher component implementation class +// ====================================================================== + +#ifndef SeqDispatcher_HPP +#define SeqDispatcher_HPP + +#include "Svc/SeqDispatcher/SeqDispatcherComponentAc.hpp" +#include "Svc/SeqDispatcher/SeqDispatcher_CmdSequencerStateEnumAc.hpp" +#include "FppConstantsAc.hpp" +#include "Fw/Types/WaitEnumAc.hpp" +#include "Fw/Types/StringBase.hpp" + +namespace Svc { + +class SeqDispatcher : public SeqDispatcherComponentBase { + public: + // ---------------------------------------------------------------------- + // Construction, initialization, and destruction + // ---------------------------------------------------------------------- + + //! Construct object SeqDispatcher + //! + SeqDispatcher(const char* const compName /*!< The component name*/ + ); + + //! Destroy object SeqDispatcher + //! + ~SeqDispatcher(); + + PROTECTED: + + //! Handler for input port seqDoneIn + void + seqDoneIn_handler(NATIVE_INT_TYPE portNum, //!< The port number + FwOpcodeType opCode, //!< Command Op Code + U32 cmdSeq, //!< Command Sequence + const Fw::CmdResponse& response //!< The command response argument + ); + + //! Handler for input port seqStartIn + void seqStartIn_handler(NATIVE_INT_TYPE portNum, //!< The port number + const Fw::StringBase& fileName //!< The sequence file + ); + + //! Handler for input port seqRunIn + void seqRunIn_handler(NATIVE_INT_TYPE portNum, //!< The port number + const Fw::StringBase& fileName //!< The sequence file + ); + + PRIVATE: + + // number of sequences dispatched (successful or otherwise) + U32 m_dispatchedCount = 0; + // number of errors from dispatched sequences (CmdResponse::EXECUTION_ERROR) + U32 m_errorCount = 0; + // number of sequencers in state AVAILABLE + U32 m_sequencersAvailable = SeqDispatcherSequencerPorts; + + struct DispatchEntry { + FwOpcodeType opCode; //!< opcode of entry + U32 cmdSeq; + // store the state of each sequencer + SeqDispatcher_CmdSequencerState state; + // store the sequence currently running for each sequencer + Fw::String sequenceRunning = ""; + } m_entryTable[SeqDispatcherSequencerPorts]; //!< table of dispatch + //!< entries + + FwIndexType getNextAvailableSequencerIdx(); + + void runSequence(FwIndexType sequencerIdx, + const Fw::StringBase& fileName, + Fw::Wait block); + + // ---------------------------------------------------------------------- + // Command handler implementations + // ---------------------------------------------------------------------- + + //! Implementation for RUN command handler + //! + void RUN_cmdHandler(const FwOpcodeType opCode, /*!< The opcode*/ + const U32 cmdSeq, /*!< The command sequence number*/ + const Fw::CmdStringArg& fileName, /*!< The name of the sequence file*/ + Fw::Wait block); + + void LOG_STATUS_cmdHandler(const FwOpcodeType opCode, /*!< The opcode*/ + const U32 cmdSeq); /*!< The command sequence number*/ +}; + +} // end namespace components + +#endif diff --git a/Svc/SeqDispatcher/SeqDispatcherCommands.fppi b/Svc/SeqDispatcher/SeqDispatcherCommands.fppi new file mode 100644 index 0000000000..378c2f6ec7 --- /dev/null +++ b/Svc/SeqDispatcher/SeqDispatcherCommands.fppi @@ -0,0 +1,9 @@ +@ Dispatches a sequence to the first available sequencer +async command RUN( + fileName: string size 240 @< The name of the sequence file + $block: Fw.Wait @< Return command status when complete or not + ) \ + opcode 0 + +@ Logs via Events the state of each connected command sequencer +async command LOG_STATUS() opcode 1 \ No newline at end of file diff --git a/Svc/SeqDispatcher/SeqDispatcherEvents.fppi b/Svc/SeqDispatcher/SeqDispatcherEvents.fppi new file mode 100644 index 0000000000..9b92eca254 --- /dev/null +++ b/Svc/SeqDispatcher/SeqDispatcherEvents.fppi @@ -0,0 +1,38 @@ +event InvalidSequencer( + idx: U16 +) \ + severity warning high \ + format "Invalid sequence index {}" + +event NoAvailableSequencers() \ + severity warning high \ + format "No available cmd sequencers to dispatch a sequence to" + +event UnknownSequenceFinished( + idx: U16 +) \ + severity warning low \ + format "Sequencer {} completed a sequence with no matching start notification" + +event ConflictingSequenceStarted( + idx: U16, + newSequence: string size 240, + sequenceInInternalState: string size 240 +) \ + severity warning high \ + format "Sequencer {} started a sequence {} while still running {}" + +event UnexpectedSequenceStarted( + idx: U16, + newSequence: string size 240 +) \ + severity warning low \ + format "Sequencer {} was externally commanded to start a sequence {}" + +event LogSequencerStatus( + idx: U16 + state: CmdSequencerState + filename: string size 240 +) \ + severity activity low \ + format "Sequencer {} with state {} is running file {}" \ No newline at end of file diff --git a/Svc/SeqDispatcher/SeqDispatcherTelemetry.fppi b/Svc/SeqDispatcher/SeqDispatcherTelemetry.fppi new file mode 100644 index 0000000000..3728c9a063 --- /dev/null +++ b/Svc/SeqDispatcher/SeqDispatcherTelemetry.fppi @@ -0,0 +1,8 @@ +@ Number of sequences dispatched +telemetry dispatchedCount: U32 +@ Number of sequences dispatched that returned an error. Note: if a sequence +@ was run in non-blocking mode, even if the sequence errors out, this error +@ count will never increase +telemetry errorCount: U32 +@ Number of sequencers in an available state +telemetry sequencersAvailable: U32 \ No newline at end of file diff --git a/Svc/SeqDispatcher/docs/sdd.md b/Svc/SeqDispatcher/docs/sdd.md new file mode 100644 index 0000000000..86fa11160f --- /dev/null +++ b/Svc/SeqDispatcher/docs/sdd.md @@ -0,0 +1,51 @@ +# components::SeqDispatcher + +Dispatches command sequences to available command sequencers, allowing the spacecraft controllers to run multiple sequences at once without having to manually manage which `CmdSequencer`s those sequences run on. + +### Usage +* Call the `RUN` command just like you would call it on a `CmdSequencer` +* If any connected `CmdSequencer` is available, it will route the sequence to the first one it finds +* `RUN` can be made blocking or non-blocking, just like `CmdSequencer`'s `RUN` + +## State diagram +![State diagram of the SeqDispatcher](seq_dispatcher_model.png "SeqDispatcher model") + +## Port Descriptions +|Type| Name | Description | +|async input|seqRunIn|Equivalent to the RUN cmd, dispatches a sequence to the first available sequencer| +|output|seqRunOut|This is used by the SeqDispatcher to send sequence run calls to sequencers| +|async input|seqDoneIn|Called by a command sequencer whenever it has finished any sequence| +|async input|seqStartIn|Called by a command sequencer whenever it starts any sequence| + +## Commands +| Name | Description | +|RUN|Dispatches a sequence to the first available sequencer| +|LOG_STATUS|Logs via Events the state of each connected command sequencer| + +## Events +| Name | Description | +|InvalidSequencer|The given sequencer index is invalid for an unspecified reason| +|NoAvailableSequencers|There are no available sequencers to dispatch a sequence to| +|UnknownSequenceFinished|We received a call to seqDoneIn that didn't have a corresponding seqStartIn call| +|UnexpectedSequenceStarted|We received a call to seqStartIn but we didn't receive a call to seqDoneIn before that| +|LogSequencerStatus|Shows the current state and sequence filename for a particular sequencer. Produced by the LOG_STATUS command| + + + +## Telemetry +| Name | Description | +|dispatchedCount|Number of sequences dispatched| +|errorCount|Number of sequences dispatched that returned an error. Note: if a sequence was run in non-blocking mode, even if the sequence errors out, this error count will never increase| +|sequencersAvailable|Number of sequencers ready to run a sequence| + +## Unit Tests +Add unit test descriptions in the chart below +| Name | Description | +|testDispatch|Tests the basic dispatch functionality of the `SeqDispatcher`| +|testLogStatus|Tests the LOG_STATUS command| + +## Requirements +Add requirements in the chart below +| Name | Description | Validation | +|---|---|---| +|---|---|---| \ No newline at end of file diff --git a/Svc/SeqDispatcher/docs/seq_dispatcher_model.png b/Svc/SeqDispatcher/docs/seq_dispatcher_model.png new file mode 100644 index 0000000000000000000000000000000000000000..a85d7c7f455925a166b8daf3e5c43dd8e072066e GIT binary patch literal 37139 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 + this->initComponents(); +} + +SeqDispatcherTester ::~SeqDispatcherTester() {} + +// ---------------------------------------------------------------------- +// Tests +// ---------------------------------------------------------------------- + +void SeqDispatcherTester ::testDispatch() { + // test that it fails when we dispatch too many sequences + for (int i = 0; i < SeqDispatcherSequencerPorts; i++) { + sendCmd_RUN(0, 0, Fw::String("test"), Fw::Wait::WAIT); + this->component.doDispatch(); + // no response cuz blocking + ASSERT_CMD_RESPONSE_SIZE(0); + ASSERT_EVENTS_SIZE(0); + } + ASSERT_TLM_sequencersAvailable(SeqDispatcherSequencerPorts - 1, 0); + this->clearHistory(); + // all sequencers should be busy + sendCmd_RUN(0, 0, Fw::String("test"), Fw::Wait::WAIT); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, SeqDispatcher::OPCODE_RUN, 0, + Fw::CmdResponse::EXECUTION_ERROR); + + this->clearHistory(); + + this->invoke_to_seqDoneIn(0, 0, 0, Fw::CmdResponse::OK); + this->component.doDispatch(); + ASSERT_EVENTS_SIZE(0); + // we should have gotten a cmd response now + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, SeqDispatcher::OPCODE_RUN, 0, Fw::CmdResponse::OK); + + this->clearHistory(); + // ok now we should be able to send another sequence + // let's test non blocking now + sendCmd_RUN(0, 0, Fw::String("test"), Fw::Wait::NO_WAIT); + this->component.doDispatch(); + + // should immediately return + ASSERT_EVENTS_SIZE(0); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, SeqDispatcher::OPCODE_RUN, 0, Fw::CmdResponse::OK); + this->clearHistory(); + + // ok now check that if a sequence errors on block it will return error + this->invoke_to_seqDoneIn(1, 0, 0, Fw::CmdResponse::EXECUTION_ERROR); + this->component.doDispatch(); + ASSERT_EVENTS_SIZE(0); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, SeqDispatcher::OPCODE_RUN, 0, Fw::CmdResponse::EXECUTION_ERROR); + +} + +void SeqDispatcherTester::testLogStatus() { + this->sendCmd_RUN(0,0, Fw::String("test"), Fw::Wait::WAIT); + this->component.doDispatch(); + this->sendCmd_LOG_STATUS(0,0); + this->component.doDispatch(); + ASSERT_EVENTS_SIZE(SeqDispatcherSequencerPorts); + ASSERT_EVENTS_LogSequencerStatus(0, 0, SeqDispatcher_CmdSequencerState::RUNNING_SEQUENCE_BLOCK, "test"); +} + +void SeqDispatcherTester::seqRunOut_handler( + FwIndexType portNum, //!< The port number + const Fw::StringBase& filename //!< The sequence file +) { + this->pushFromPortEntry_seqRunOut(filename); +} + +} // end namespace components diff --git a/Svc/SeqDispatcher/test/ut/SeqDispatcherTester.hpp b/Svc/SeqDispatcher/test/ut/SeqDispatcherTester.hpp new file mode 100644 index 0000000000..f1f9aee162 --- /dev/null +++ b/Svc/SeqDispatcher/test/ut/SeqDispatcherTester.hpp @@ -0,0 +1,79 @@ +// ====================================================================== +// \title SeqDispatcher/test/ut/Tester.hpp +// \author zimri.leisher +// \brief hpp file for SeqDispatcher test harness implementation class +// ====================================================================== + +#ifndef TESTER_HPP +#define TESTER_HPP + +#include "SeqDispatcherGTestBase.hpp" +#include "Svc/SeqDispatcher/SeqDispatcher.hpp" + +namespace Svc{ + +class SeqDispatcherTester : public SeqDispatcherGTestBase { + // ---------------------------------------------------------------------- + // Construction and destruction + // ---------------------------------------------------------------------- + + public: + // Maximum size of histories storing events, telemetry, and port outputs + static const NATIVE_INT_TYPE MAX_HISTORY_SIZE = 10; + // Instance ID supplied to the component instance under test + static const NATIVE_INT_TYPE TEST_INSTANCE_ID = 0; + // Queue depth supplied to component instance under test + static const NATIVE_INT_TYPE TEST_INSTANCE_QUEUE_DEPTH = 10; + + //! Construct object SeqDispatcherTester + //! + SeqDispatcherTester(); + + //! Destroy object SeqDispatcherTester + //! + ~SeqDispatcherTester(); + + public: + // ---------------------------------------------------------------------- + // Tests + // ---------------------------------------------------------------------- + + void testDispatch(); + void testLogStatus(); + + private: + // ---------------------------------------------------------------------- + // Handlers for typed from ports + // ---------------------------------------------------------------------- + + void seqRunOut_handler( + FwIndexType portNum, //!< The port number + const Fw::StringBase& filename //!< The sequence file + ); + + private: + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + //! Connect ports + //! + void connectPorts(); + + //! Initialize components + //! + void initComponents(); + + private: + // ---------------------------------------------------------------------- + // Variables + // ---------------------------------------------------------------------- + + //! The component under test + //! + SeqDispatcher component; +}; + +} // end namespace components + +#endif diff --git a/config/AcConstants.fpp b/config/AcConstants.fpp index 2f0f10706c..3ddcff26e6 100644 --- a/config/AcConstants.fpp +++ b/config/AcConstants.fpp @@ -18,6 +18,9 @@ constant CmdDispatcherComponentCommandPorts = 30 @ Used for uplink/sequencer buffer/response ports constant CmdDispatcherSequencePorts = 5 +@ Used for dispatching sequences to command sequencers +constant SeqDispatcherSequencerPorts = 2 + @ Used for sizing the command splitter input arrays constant CmdSplitterPorts = CmdDispatcherSequencePorts diff --git a/docs/UsersGuide/dev/configuring-fprime.md b/docs/UsersGuide/dev/configuring-fprime.md index 6464fb8732..dc9ee6ea55 100644 --- a/docs/UsersGuide/dev/configuring-fprime.md +++ b/docs/UsersGuide/dev/configuring-fprime.md @@ -53,7 +53,7 @@ number of components. | CmdDispatcherSequencePorts | Number of incoming ports to command dispatcher, e.g. uplink and command sequencer | 5 | Positive integer | | RateGroupDriverRateGroupPorts | Number of rate group driver output ports. Limits total number of different rate groups | 3 | Positive integer | | HealthPingPorts | Number of health ping output ports. Limits number of components attached to health component | 25 | Positive integer | - +| SeqDispatcherSequencerPorts | Number of CmdSequencers that the SeqDispatcher can dispatch sequences to | 2 | Positive integer ## FpConfig.h