Repository: nasa/fprime (language: python)

Issue:
Distributing command sequences throughout CmdSequencers
### Discussed in https://github.com/nasa/fprime/discussions/2608

<div type='discussions-op-text'>

<sup>Originally posted by **zimri-leisher** March 20, 2024</sup>
Hello,
We'd like to run multiple command sequences at the same time. As I understand, that means we need more than one command sequencer. However, it would be nice not to have to decide which command sequencer we want to run a command on. Instead, we want to just say "run this sequence on an available cmd sequencer".
Any suggestions for this? It seems like it would be a common problem. Or is this a bad idea and we should have to explicitly declare which command sequencer we want to use?</div>
Distributing command sequences throughout CmdSequencers
### Discussed in https://github.com/nasa/fprime/discussions/2608

<div type='discussions-op-text'>

<sup>Originally posted by **zimri-leisher** March 20, 2024</sup>
Hello,
We'd like to run multiple command sequences at the same time. As I understand, that means we need more than one command sequencer. However, it would be nice not to have to decide which command sequencer we want to run a command on. Instead, we want to just say "run this sequence on an available cmd sequencer".
Any suggestions for this? It seems like it would be a common problem. Or is this a bad idea and we should have to explicitly declare which command sequencer we want to use?</div>


Discussion / hints:

Fix patch:
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 <Svc/SeqDispatcher/SeqDispatcher.hpp>
+
+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<U16>(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<U16>(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<U16>(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 = "<no seq>";
+  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<U16>(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 = "<no seq>";
+    } 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 0000000000..a85d7c7f45
Binary files /dev/null and b/Svc/SeqDispatcher/docs/seq_dispatcher_model.png differ
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
 


Test patch:
diff --git a/Svc/SeqDispatcher/test/ut/SeqDispatcherTestMain.cpp b/Svc/SeqDispatcher/test/ut/SeqDispatcherTestMain.cpp
new file mode 100644
index 0000000000..b4362ef055
--- /dev/null
+++ b/Svc/SeqDispatcher/test/ut/SeqDispatcherTestMain.cpp
@@ -0,0 +1,21 @@
+// ----------------------------------------------------------------------
+// TestMain.cpp
+// ----------------------------------------------------------------------
+
+#include "SeqDispatcherTester.hpp"
+
+TEST(Nominal, testDispatch) {
+    Svc::SeqDispatcherTester tester;
+    tester.testDispatch();
+}
+
+TEST(Nominal, testLogStatus) {
+    Svc::SeqDispatcherTester tester;
+    tester.testLogStatus();
+}
+
+
+int main(int argc, char **argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/Svc/SeqDispatcher/test/ut/SeqDispatcherTester.cpp b/Svc/SeqDispatcher/test/ut/SeqDispatcherTester.cpp
new file mode 100644
index 0000000000..261f5a275c
--- /dev/null
+++ b/Svc/SeqDispatcher/test/ut/SeqDispatcherTester.cpp
@@ -0,0 +1,92 @@
+// ======================================================================
+// \title  SeqDispatcher.hpp
+// \author zimri.leisher
+// \brief  cpp file for SeqDispatcher test harness implementation class
+// ======================================================================
+
+#include "SeqDispatcherTester.hpp"
+
+namespace Svc{
+
+// ----------------------------------------------------------------------
+// Construction and destruction
+// ----------------------------------------------------------------------
+
+SeqDispatcherTester ::SeqDispatcherTester()
+    : SeqDispatcherGTestBase("SeqDispatcherTester", SeqDispatcherTester::MAX_HISTORY_SIZE),
+      component("SeqDispatcher") {
+  this->connectPorts();
+  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