diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e192dd09113..d5fc42d95fd 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,5 +1,6 @@ Aadil AArray +acknak AClass ACTIVERATEGROUP ACTIVERATEGROUPCFG @@ -75,13 +76,16 @@ CBLOCK CCACHE CCB CComponent +CCs ccsds ccsparc cdh CDHCORE CDHCORESUBTOPOLOGY cerrno -CFDP +cfdp +CFDPMANAGER +CFDPTIMER cff cflag cfsetispeed @@ -91,7 +95,9 @@ Chieu CHIPINFO CHK CHNG +chunklist CIRCULARSTATE +clist CLOSEFILE cloudbees CMDDISP @@ -166,11 +172,13 @@ deployables DEPRECATEDLIST deser Deserial +destq DEVICESM DHTML diafile diles dinkel +diropen dnf dnp docbook @@ -208,6 +216,7 @@ eay ECLIPSEHELP EEnum EHAs +eid eip Elts emptydir @@ -217,6 +226,7 @@ endmacro endraw enduml EPP +eod ERRORCHECK errornum ert @@ -231,6 +241,8 @@ evt externalproject FAKELOGGER fbuild +fdir +fdirective FDISP fdp featherm @@ -247,15 +259,21 @@ FILEDISPATCHERCFG FILEDOWNLINK FILEDOWNLINKCFG FILEHANDLING +FILEHANDLINGCFDP +FILEHANDLINGCFDPSUBTOPOLOGY FILEHANDLINGSUBTOPOLOGY FILEID FILEMANAGERCONFIG FILEOPENERROR +filestore FILEWRITEERROR +finack fio fle +fnames FNDELAY fne +foffs fontcolor FONTPATH foodoodie @@ -279,6 +297,7 @@ freeram Fregoso frsize fsblkcnt +fsize fsw FWCASSERT gcda @@ -305,6 +324,7 @@ Graphviz grayscales GROUNDINTERFACERULES GSE +GSW gtags gtest gtimeout @@ -412,6 +432,7 @@ lseek LTK lvar LVL +LVs lxml MACROFILE MACROSTART @@ -453,6 +474,7 @@ mutexattr Mutexed muxed mycompany +NAKs nasafprime nbits ncsl @@ -529,6 +551,7 @@ PKTS plainnat plantuml PNGs +polldir pollfd POLLIN POLYDB @@ -560,8 +583,10 @@ projectnumber propget propput protothreading +psn ptbool ptf +PTFO pthread ptrt pvn @@ -570,6 +595,7 @@ qhelpgenerator QHG qhp qsf +queueidx RAII randtbl raspberrypi @@ -602,11 +628,14 @@ Rizvi ROOTDIR rpi rptr +rsp +RSubstate SAlias sanitizers sats SBF SBINDIR +sbintf sbom scid scm @@ -659,7 +688,9 @@ sqa srandom SRCS sreddy +sret sss +SSubstate STAMEM startuml stdbool @@ -692,6 +723,8 @@ tabdnp tabkermit tagfile tbase +tbd +tbl tcanham tcflush tcgetattr @@ -728,6 +761,7 @@ TLMPACKET TLMPACKETIZER TLMPACKETIZERCOMPONENTIMPLCFG TLMPACKETIZERTYPES +tlv TODOLIST TOKENBUCKETTESTER topologydefs @@ -735,9 +769,13 @@ totalram tparam TPP trinomials +tsn tts Tumbar tumbar +txa +txm +txw typedef typedef'ed uart @@ -747,6 +785,7 @@ uge uitofp UML umod +unack unconfigured UNEXP unistd @@ -777,6 +816,7 @@ WORKDIR wrs wxgui wxy +XACT Xapian XBee xdf @@ -786,3 +826,4 @@ xxxx XXYY ziext zimri + diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index 8d5bad22f79..c9f8187f838 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -146,3 +146,6 @@ TeX/AMS # .get...() .set...() autocoded functions \.get\w+\( \.set\w+\( + +# CCSDS specification version numbers (e.g., CCSDS 727.0-B-5) +\bCCSDS\s+\d+\.\d+-[A-Z]-\d+(?:\s+section\s+\d+(?:\.\d+)?)?(?:,\s+section\s+\d+(?:\.\d+)?(?:,\s+table\s+[-\d]+)?)? diff --git a/Os/File.hpp b/Os/File.hpp index d9309cf0d32..a28d6af4b63 100644 --- a/Os/File.hpp +++ b/Os/File.hpp @@ -27,28 +27,28 @@ struct FileHandle {}; class FileInterface { public: enum Mode { - OPEN_NO_MODE, //!< File mode not yet selected - OPEN_READ, //!< Open file for reading + OPEN_NO_MODE, //!< File mode not yet selected + OPEN_READ, //!< Open file for reading OPEN_CREATE, //!< Open file for writing and truncates file if it exists, ie same flags as creat() - OPEN_WRITE, //!< Open file for writing - OPEN_SYNC_WRITE, //!< Open file for writing; writes don't return until data is on disk + OPEN_WRITE, //!< Open file for writing + OPEN_SYNC_WRITE, //!< Open file for writing; writes don't return until data is on disk OPEN_APPEND, //!< Open file for appending MAX_OPEN_MODE //!< Maximum value of mode }; enum Status { - OP_OK, //!< Operation was successful - DOESNT_EXIST, //!< File doesn't exist (for read) - NO_SPACE, //!< No space left - NO_PERMISSION, //!< No permission to read/write file - BAD_SIZE, //!< Invalid size parameter - NOT_OPENED, //!< file hasn't been opened yet + OP_OK, //!< Operation was successful + DOESNT_EXIST, //!< File doesn't exist (for read) + NO_SPACE, //!< No space left + NO_PERMISSION, //!< No permission to read/write file + BAD_SIZE, //!< Invalid size parameter + NOT_OPENED, //!< file hasn't been opened yet FILE_EXISTS, //!< file already exist (for CREATE with O_EXCL enabled) NOT_SUPPORTED, //!< Kernel or file system does not support operation INVALID_MODE, //!< Mode for file access is invalid for current operation INVALID_ARGUMENT, //!< Invalid argument passed in NO_MORE_RESOURCES, //!< No more available resources - OTHER_ERROR, //!< A catch-all for other errors. Have to look in implementation-specific code + OTHER_ERROR, //!< A catch-all for other errors. Have to look in implementation-specific code MAX_STATUS //!< Maximum value of status }; diff --git a/Svc/Ccsds/CMakeLists.txt b/Svc/Ccsds/CMakeLists.txt index fa3326c2521..901846ee764 100644 --- a/Svc/Ccsds/CMakeLists.txt +++ b/Svc/Ccsds/CMakeLists.txt @@ -6,3 +6,4 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/TcDeframer/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/TmFramer/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/AosFramer/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ApidManager/") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/CfdpManager/") diff --git a/Svc/Ccsds/CfdpManager/ATTRIBUTION.md b/Svc/Ccsds/CfdpManager/ATTRIBUTION.md new file mode 100644 index 00000000000..a6ad8b22ada --- /dev/null +++ b/Svc/Ccsds/CfdpManager/ATTRIBUTION.md @@ -0,0 +1,58 @@ +# CFDP Manager Attribution + +This component implements the CCSDS File Delivery Protocol (CFDP) for F-Prime (F'). It includes both ported code from NASA's Core Flight System (cFS) CFDP application and new F' implementations. + +## Source Attribution + +Portions of this code are derived from the NASA Core Flight System (cFS) CFDP (CF) Application: +- **Repository**: https://github.com/nasa/CF +- **Version**: 3.0.0 +- **License**: Apache License 2.0 +- **Copyright**: Copyright (c) 2019 United States Government as represented by the Administrator of the National Aeronautics and Space Administration +- **NASA Docket**: GSC-18,447-1 + +## Files Ported from CF + +The following files are ports/adaptations from CF source code and retain the original NASA copyright: + +### Core Engine & Transaction Management +- `Engine.hpp` / `.cpp` - from `cf_cfdp.c` / `cf_cfdp.h` +- `Transaction.hpp` - from `cf_cfdp_r.h` / `cf_cfdp_s.h` / `cf_cfdp_dispatch.h` +- `TransactionTx.cpp` - from `cf_cfdp_s.c` / `cf_cfdp_dispatch.c` +- `TransactionRx.cpp` - from `cf_cfdp_r.c` / `cf_cfdp_dispatch.c` + +### Data Structures & Utilities +- `Types/Types.hpp` - from `cf_cfdp_types.h` +- `Utils.hpp` / `.cpp` - from `cf_utils.h` / `cf_utils.c` +- `Channel.hpp` / `.cpp` - from channel functions in `cf_cfdp.c` / `cf_utils.c` +- `Chunk.hpp` / `.cpp` - from `cf_chunks.h` / `cf_chunks.c` +- `Clist.hpp` / `.cpp` - from `cf_clist.h` / `cf_clist.c` + +Each of these files includes the full NASA copyright notice and Apache 2.0 license text in its header. + +## New F-Prime Implementations + +The following files are new implementations for F-Prime and do not contain CF-derived code: + +### Integration Layer +- `CfdpManager.hpp` / `.cpp` - F-Prime component wrapper +- `Timer.hpp` / `.cpp` - F-Prime timer implementation + +### PDU Object-Oriented Implementation +All files in the `Types/` directory are new F' serializable implementations based on the CFDP Blue Book specification (CCSDS 727.0-B-5): +- `Types/PduBase.hpp` - Base class for all PDU types +- `Types/PduHeader.hpp` / `.cpp` - PDU header encoding/decoding +- `Types/MetadataPdu.hpp` / `.cpp` - Metadata PDU +- `Types/FileDataPdu.hpp` / `.cpp` - File Data PDU +- `Types/EofPdu.hpp` / `.cpp` - End of File PDU +- `Types/FinPdu.hpp` / `.cpp` - Finished PDU +- `Types/AckPdu.hpp` / `.cpp` - Acknowledge PDU +- `Types/NakPdu.hpp` / `.cpp` - Negative Acknowledge PDU + +These files implement CFDP PDU encoding/decoding based on the specification rather than porting CF's C-style codec. + +## License + +This component as a whole is licensed under the Apache License 2.0. See the top-level [LICENSE.txt](../../../LICENSE.txt) for the full license text. + +The CF-derived portions retain their original NASA copyright and Apache 2.0 license as documented in their file headers. diff --git a/Svc/Ccsds/CfdpManager/CMakeLists.txt b/Svc/Ccsds/CfdpManager/CMakeLists.txt new file mode 100644 index 00000000000..54df267ea83 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/CMakeLists.txt @@ -0,0 +1,43 @@ +#### +# F Prime CMakeLists.txt: +# +# SOURCES: list of source files (to be compiled) +# AUTOCODER_INPUTS: list of files to be passed to the autocoders +# DEPENDS: list of libraries that this module depends on +# +# More information in the F´ CMake API documentation: +# https://fprime.jpl.nasa.gov/latest/docs/reference/api/cmake/API/ +# +#### + +register_fprime_library( + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/CfdpManager.fpp" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/CfdpManager.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Engine.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Chunk.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Clist.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Utils.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Timer.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Channel.cpp" + "${CMAKE_CURRENT_LIST_DIR}/TransactionTx.cpp" + "${CMAKE_CURRENT_LIST_DIR}/TransactionRx.cpp" + DEPENDS + CFDP_Checksum + Svc_Ccsds_CfdpManager_Types +) + +### Subdirectories ### +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/Types/") + +### Unit Tests ### +register_fprime_ut( + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/CfdpManager.fpp" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CfdpManagerTestMain.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CfdpManagerTester.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/PduTester.cpp" + UT_AUTO_HELPERS +) diff --git a/Svc/Ccsds/CfdpManager/CfdpManager.cpp b/Svc/Ccsds/CfdpManager/CfdpManager.cpp new file mode 100644 index 00000000000..b01c6553e25 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/CfdpManager.cpp @@ -0,0 +1,737 @@ +// ====================================================================== +// \title CfdpManager.cpp +// \author Brian Campuzano +// \brief cpp file for CfdpManager component implementation class +// ====================================================================== + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ---------------------------------------------------------------------- +// Component construction and destruction +// ---------------------------------------------------------------------- + +CfdpManager ::CfdpManager(const char* const compName) : + CfdpManagerComponentBase(compName), + m_engine(nullptr) +{ + +} + +CfdpManager ::~CfdpManager() { + // Clean up the queue resources allocated during initialization + this->deinit(); + + delete this->m_engine; + this->m_engine = nullptr; +} + +void CfdpManager ::configure(void) +{ + // TODO BPC: Update to use a mem allocator + // Create and initialize the CFDP engine + this->m_engine = new Engine(this); + FW_ASSERT(this->m_engine != nullptr); + this->m_engine->init(); + + // Initialize telemetry counters to zero + for (U8 i = 0; i < Cfdp::NumChannels; i++) { + this->m_channelTelemetry[i] = Cfdp::ChannelTelemetry(); + } +} + +// ---------------------------------------------------------------------- +// Handler implementations for typed input ports +// ---------------------------------------------------------------------- + +void CfdpManager ::run1Hz_handler(FwIndexType portNum, U32 context) +{ + // The timer logic built into the CFDP engine requires it to be driven at 1 Hz + FW_ASSERT(this->m_engine != NULL); + this->m_engine->cycle(); + + // Emit telemetry once per second + this->tlmWrite_ChannelTelemetry(this->m_channelTelemetry); +} + +void CfdpManager ::dataReturnIn_handler(FwIndexType portNum, Fw::Buffer& fwBuffer) +{ + // dataReturnIn is the allocated buffer coming back from the dataOut call + // Port mapping is the same from bufferAllocate -> dataOut -> dataReturnIn -> bufferDeallocate + FW_ASSERT(portNum < Cfdp::NumChannels, portNum, Cfdp::NumChannels); + this->bufferDeallocate_out(portNum, fwBuffer); +} + +void CfdpManager ::dataIn_handler(FwIndexType portNum, Fw::Buffer& fwBuffer) +{ + // There is a direct mapping between port number and channel index + FW_ASSERT(portNum < Cfdp::NumChannels, portNum, Cfdp::NumChannels); + FW_ASSERT(portNum >= 0, portNum); + + // TODO JMP Is there a more efficient way of doing this? Look into receivePdu() + // Strip FW_PACKET_FILE descriptor (first 2 bytes) from buffer + // FprimeRouter sends the entire Space Packet data field, which includes the packet type descriptor + if (fwBuffer.getSize() < sizeof(FwPacketDescriptorType)) { + // Buffer too small - silently ignore + this->dataInReturn_out(portNum, fwBuffer); + return; + } + + // Read and verify packet type descriptor + FwPacketDescriptorType packetType = 0; + Fw::SerializeStatus status = fwBuffer.getDeserializer().deserializeTo(packetType); + if (status != Fw::FW_SERIALIZE_OK || packetType != Fw::ComPacketType::FW_PACKET_FILE) { + // Invalid packet type - silently ignore (consistent with FileUplink behavior) + this->dataInReturn_out(portNum, fwBuffer); + return; + } + + // Create a new buffer view that skips the descriptor + // The deserializer advanced past the 2-byte descriptor, but Engine::receivePdu + // calls getData() which returns the raw pointer from byte 0. We need to create + // a buffer that starts after the descriptor. + const FwSizeType descriptorSize = sizeof(FwPacketDescriptorType); + Fw::Buffer pduBuffer( + fwBuffer.getData() + descriptorSize, + fwBuffer.getSize() - descriptorSize, + fwBuffer.getContext() + ); + + // Pass the adjusted buffer to the engine + FW_ASSERT(this->m_engine != NULL); + this->m_engine->receivePdu(static_cast(portNum), pduBuffer); + + // Return buffer + this->dataInReturn_out(portNum, fwBuffer); +} + +Svc::SendFileResponse CfdpManager ::fileIn_handler( + FwIndexType portNum, + const Fw::StringBase& sourceFileName, + const Fw::StringBase& destFileName, + U32 offset, + U32 length) +{ + Svc::SendFileResponse response; + FW_ASSERT(this->m_engine != NULL); + + // TODO BPC: CFDP engine does not support partial file retransmit at this time + if(offset > 0 || length > 0) + { + response.set_status(Svc::SendFileStatus::STATUS_INVALID); + this->log_WARNING_LO_UnsupportedSendFileArguments(offset, length); + } + else + { + // Get parameters for fileIn port-initiated transfers + Fw::ParamValid valid; + U8 channelId = this->paramGet_FileInDefaultChannel(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + EntityId destEid = this->paramGet_FileInDefaultDestEntityId(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + Class::T cfdpClass = this->paramGet_FileInDefaultClass(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + Keep::T keep = this->paramGet_FileInDefaultKeep(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + U8 priority = this->paramGet_FileInDefaultPriority(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Attempt to initiate the file transfer (mark as port-initiated) + Status::T status = this->m_engine->txFile( + sourceFileName, destFileName, cfdpClass, keep, + channelId, priority, destEid, INIT_BY_PORT); + + // Map CFDP status to SendFileStatus + if (status == Status::SUCCESS) { + response.set_status(Svc::SendFileStatus::STATUS_OK); + this->log_ACTIVITY_LO_SendFileInitiated(sourceFileName); + } else { + response.set_status(Svc::SendFileStatus::STATUS_ERROR); + this->log_WARNING_LO_SendFileInitiateFail(sourceFileName); + } + } + + // Set context to portNum so we can identify this transaction later + response.set_context(static_cast(portNum)); + + return response; +} + +void CfdpManager ::pingIn_handler(FwIndexType portNum, U32 key) +{ + // send ping response + this->pingOut_out(0, key); +} + +// ---------------------------------------------------------------------- +// Port calls that are invoked by the CFDP engine +// These functions are analogous to the functions in cf_cfdp_sbintf.* +// However these functions were not directly migrated due to the +// architectural differences between F' and cFE +// ---------------------------------------------------------------------- + +Status::T CfdpManager ::getPduBuffer(Fw::Buffer& buffer, Channel& channel, + FwSizeType size) +{ + Status::T status = Status::ERROR; + FwIndexType portNum; + + // There is a direct mapping between channel index and port number + portNum = static_cast(channel.getChannelId()); + + // Check if we have reached the maximum number of output PDUs for this cycle + U32 max_pdus = getMaxOutgoingPdusPerCycleParam(channel.getChannelId()); + if (channel.getOutgoingCounter() >= max_pdus) + { + status = Status::SEND_PDU_NO_BUF_AVAIL_ERROR; + } + else + { + buffer = this->bufferAllocate_out(portNum, size); + // Check the allocation was successful based on size + if(buffer.getSize() == size) + { + channel.incrementOutgoingCounter(); + status = Status::SUCCESS; + } + else + { + this->log_WARNING_LO_BuffersExhausted(); + status = Status::SEND_PDU_NO_BUF_AVAIL_ERROR; + } + } + return status; +} + +void CfdpManager ::returnPduBuffer(Channel& channel, Fw::Buffer& pduBuffer) +{ + FwIndexType portNum; + + // There is a direct mapping between channel index and port number + portNum = static_cast(channel.getChannelId()); + + // Was unable to successfully populate the PDU buffer, return it + this->bufferDeallocate_out(portNum, pduBuffer); +} + +void CfdpManager ::sendPduBuffer(Channel& channel, Fw::Buffer& pduBuffer) +{ + FwIndexType portNum; + + // There is a direct mapping between channel index and port number + portNum = static_cast(channel.getChannelId()); + + // ComQueue expects buffers to start with a 2-byte packet descriptor (APID) + // Prepend FW_PACKET_FILE descriptor to the CFDP PDU + + U8* bufferData = pduBuffer.getData(); + const FwSizeType pduSize = pduBuffer.getSize(); + const FwSizeType descriptorSize = sizeof(FwPacketDescriptorType); + const FwSizeType totalSize = descriptorSize + pduSize; + + // Safety check: ensure size won't overflow + FW_ASSERT_NO_OVERFLOW(pduSize, size_t); + + // Shift PDU data forward to make room for descriptor + // Use memmove (not memcpy) since source and destination overlap + memmove(bufferData + descriptorSize, bufferData, static_cast(pduSize)); + + // Write FW_PACKET_FILE descriptor at the beginning (big-endian U16) + const FwPacketDescriptorType descriptor = + static_cast(Fw::ComPacketType::FW_PACKET_FILE); + bufferData[0] = static_cast((descriptor >> 8) & 0xFF); // High byte + bufferData[1] = static_cast(descriptor & 0xFF); // Low byte + + // Update buffer size to include descriptor + pduBuffer.setSize(totalSize); + + // Send buffer with descriptor + this->dataOut_out(portNum, pduBuffer); +} + +void CfdpManager::sendFileComplete(Svc::SendFileStatus::T status) +{ + Svc::SendFileResponse response; + response.set_status(status); + response.set_context(0); + + this->fileDoneOut_out(0, response); +} + +// ---------------------------------------------------------------------- +// Handler implementations for commands +// ---------------------------------------------------------------------- + +void CfdpManager ::SendFile_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U8 channelId, EntityId destId, + Class cfdpClass, Keep keep, U8 priority, + const Fw::CmdStringArg& sourceFileName, + const Fw::CmdStringArg& destFileName) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + // Check channel index is in range + rspStatus = this->checkCommandChannelIndex(channelId); + FW_ASSERT(this->m_engine != NULL); + + if ((rspStatus == Fw::CmdResponse::OK) && + (Status::SUCCESS == this->m_engine->txFile(sourceFileName, destFileName, cfdpClass.e, keep.e, + channelId, priority, destId))) + { + this->log_ACTIVITY_LO_SendFileInitiated(sourceFileName); + rspStatus = Fw::CmdResponse::OK; + } + else + { + // TODO BPC: Was failure reason already emitted? + // Do we need this EVR? + this->log_WARNING_LO_SendFileInitiateFail(sourceFileName); + rspStatus = Fw::CmdResponse::EXECUTION_ERROR; + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::PlaybackDirectory_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U8 channelId, EntityId destId, + Class cfdpClass, Keep keep, U8 priority, + const Fw::CmdStringArg& sourceDirectory, + const Fw::CmdStringArg& destDirectory) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + // Check channel index is in range + rspStatus = this->checkCommandChannelIndex(channelId); + if ((rspStatus == Fw::CmdResponse::OK) && + (Status::SUCCESS == this->m_engine->playbackDir(sourceDirectory.toChar(), destDirectory.toChar(), cfdpClass.e, + keep.e, channelId, priority, destId))) + { + this->log_ACTIVITY_LO_PlaybackInitiated(sourceDirectory); + } + else + { + // TODO BPC: Was failure reason already emitted? + // Do we need this EVR? + this->log_WARNING_LO_PlaybackInitiateFail(sourceDirectory); + rspStatus = Fw::CmdResponse::EXECUTION_ERROR; + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::PollDirectory_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U8 channelId, U8 pollId, + EntityId destId, Class cfdpClass, U8 priority, + U32 interval, const Fw::CmdStringArg& sourceDirectory, + const Fw::CmdStringArg& destDirectory) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + // Check channel index and poll index are in range + rspStatus = this->checkCommandChannelIndex(channelId); + if (rspStatus == Fw::CmdResponse::OK) + { + rspStatus = this->checkCommandChannelPollIndex(pollId); + } + + if ((rspStatus == Fw::CmdResponse::OK) && + (Status::SUCCESS == this->m_engine->startPollDir(channelId, pollId, sourceDirectory, destDirectory, + cfdpClass.e, priority, destId, interval))) + { + this->log_ACTIVITY_LO_PollDirInitiated(sourceDirectory); + } + else + { + // Failure EVR was already emitted + rspStatus = Fw::CmdResponse::EXECUTION_ERROR; + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::StopPollDirectory_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U8 channelId, U8 pollId) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + // Check channel index and poll index are in range + rspStatus = this->checkCommandChannelIndex(channelId); + if (rspStatus == Fw::CmdResponse::OK) + { + rspStatus = this->checkCommandChannelPollIndex(pollId); + } + + if ((rspStatus == Fw::CmdResponse::OK) && + (Status::SUCCESS == this->m_engine->stopPollDir(channelId, pollId))) + { + this->log_ACTIVITY_LO_PollDirStopped(channelId, pollId); + } + // Failure EVR was already emitted + // Not failing the command if the stop request failed + // This allows operators to reinforce state prior to calling PollDirectory + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::SetChannelFlow_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U8 channelId, Flow flowState) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + // Check channel index is in range + rspStatus = checkCommandChannelIndex(channelId); + if (rspStatus == Fw::CmdResponse::OK) + { + this->m_engine->setChannelFlowState(channelId, flowState); + this->log_ACTIVITY_LO_SetFlowState(channelId, flowState); + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); + +} + +void CfdpManager ::SuspendResumeTransaction_cmdHandler( + FwOpcodeType opCode, + U32 cmdSeq, + U8 channelId, + TransactionSeq transactionSeq, + EntityId entityId, + SuspendResume action) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + + rspStatus = checkCommandChannelIndex(channelId); + + if (rspStatus == Fw::CmdResponse::OK) { + Status::T status = this->m_engine->setSuspendResumeTransaction(channelId, transactionSeq, entityId, action); + if (status == Status::SUCCESS) { + if (action == SuspendResume::SUSPEND) { + log_ACTIVITY_LO_TransactionSuspended(transactionSeq, entityId); + } else { + log_ACTIVITY_LO_TransactionResumed(transactionSeq, entityId); + } + } else { + log_WARNING_LO_TransactionNotFound(transactionSeq, entityId); + rspStatus = Fw::CmdResponse::EXECUTION_ERROR; + } + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::CancelTransaction_cmdHandler( + FwOpcodeType opCode, + U32 cmdSeq, + U8 channelId, + TransactionSeq transactionSeq, + EntityId entityId) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + + rspStatus = checkCommandChannelIndex(channelId); + + if (rspStatus == Fw::CmdResponse::OK) { + Status::T status = this->m_engine->cancelTransactionBySeq(channelId, transactionSeq, entityId); + if (status == Status::SUCCESS) { + log_ACTIVITY_HI_TransactionCanceled(transactionSeq, entityId); + } else { + log_WARNING_LO_TransactionNotFound(transactionSeq, entityId); + rspStatus = Fw::CmdResponse::EXECUTION_ERROR; + } + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::AbandonTransaction_cmdHandler( + FwOpcodeType opCode, + U32 cmdSeq, + U8 channelId, + TransactionSeq transactionSeq, + EntityId entityId) +{ + Fw::CmdResponse::T rspStatus = Fw::CmdResponse::OK; + + FW_ASSERT(this->m_engine != NULL); + + rspStatus = checkCommandChannelIndex(channelId); + + if (rspStatus == Fw::CmdResponse::OK) { + Status::T status = this->m_engine->abandonTransaction(channelId, transactionSeq, entityId); + if (status == Status::SUCCESS) { + log_ACTIVITY_HI_TransactionAbandoned(transactionSeq, entityId); + } else { + log_WARNING_LO_TransactionNotFound(transactionSeq, entityId); + rspStatus = Fw::CmdResponse::EXECUTION_ERROR; + } + } + + this->cmdResponse_out(opCode, cmdSeq, rspStatus); +} + +void CfdpManager ::ResetCounters_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U8 channelId) +{ + // 0xFF means reset all channels + if (channelId == 0xFF) + { + for (U8 i = 0; i < Cfdp::NumChannels; i++) + { + this->m_channelTelemetry[i] = Cfdp::ChannelTelemetry(); + } + this->log_ACTIVITY_HI_ResetCounters(0xFF); + } + // Otherwise reset specific channel + else if (channelId < Cfdp::NumChannels) + { + this->m_channelTelemetry[channelId] = Cfdp::ChannelTelemetry(); + this->log_ACTIVITY_HI_ResetCounters(channelId); + } + else + { + // Invalid channel ID + this->log_WARNING_LO_InvalidChannel(channelId, Cfdp::NumChannels - 1); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Emit updated telemetry + this->tlmWrite_ChannelTelemetry(this->m_channelTelemetry); + + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + +// ---------------------------------------------------------------------- +// Private command helper functions +// ---------------------------------------------------------------------- + +Fw::CmdResponse::T CfdpManager ::checkCommandChannelIndex(U8 channelIndex) +{ + if(channelIndex >= Cfdp::NumChannels) + { + this->log_WARNING_LO_InvalidChannel(channelIndex, Cfdp::NumChannels); + return Fw::CmdResponse::VALIDATION_ERROR; + } + else + { + return Fw::CmdResponse::OK; + } +} + +Fw::CmdResponse::T CfdpManager ::checkCommandChannelPollIndex(U8 pollIndex) +{ + if(pollIndex >= CFDP_MAX_POLLING_DIR_PER_CHAN) + { + this->log_WARNING_LO_InvalidChannelPoll(pollIndex, CFDP_MAX_POLLING_DIR_PER_CHAN); + return Fw::CmdResponse::VALIDATION_ERROR; + } + else + { + return Fw::CmdResponse::OK; + } +} + + // ---------------------------------------------------------------------- + // Parameter helpers used by the CFDP engine + // ---------------------------------------------------------------------- + + EntityId CfdpManager:: getLocalEidParam(void) + { + Fw::ParamValid valid; + + // Check for coding errors as all CFDP parameters must have a default + EntityId localEid = this->paramGet_LocalEid(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + return localEid; + } + + U32 CfdpManager:: getOutgoingFileChunkSizeParam(void) + { + Fw::ParamValid valid; + + // Check for coding errors as all CFDP parameters must have a default + U32 chunkSize = this->paramGet_OutgoingFileChunkSize(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + return chunkSize; + } + U32 CfdpManager:: getRxCrcCalcBytesPerCycleParam(void) + { + Fw::ParamValid valid; + + // Check for coding errors as all CFDP parameters must have a default + U32 rxSize = this->paramGet_RxCrcCalcBytesPerCycle(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + return rxSize; + } + + Fw::String CfdpManager:: getTmpDirParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_tmp_dir(); + } + + Fw::String CfdpManager:: getFailDirParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_fail_dir(); + } + + U8 CfdpManager:: getAckLimitParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_ack_limit(); + } + + U8 CfdpManager:: getNackLimitParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_nack_limit(); + } + + U32 CfdpManager:: getAckTimerParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_ack_timer(); + } + + U32 CfdpManager:: getInactivityTimerParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_inactivity_timer(); + } + + Fw::Enabled CfdpManager:: getDequeueEnabledParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_dequeue_enabled(); + } + + Fw::String CfdpManager:: getMoveDirParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_move_dir(); + } + + U32 CfdpManager ::getMaxOutgoingPdusPerCycleParam(U8 channelIndex) + { + Fw::ParamValid valid; + + FW_ASSERT(channelIndex < Cfdp::NumChannels, channelIndex, Cfdp::NumChannels); + + // Check for coding errors as all CFDP parameters must have a default + // Get the array first + ChannelArrayParams paramArray = paramGet_ChannelConfig(valid); + FW_ASSERT(valid != Fw::ParamValid::INVALID && valid != Fw::ParamValid::UNINIT, + static_cast(valid.e)); + + // Now get individual parameter + return paramArray[channelIndex].get_max_outgoing_pdus_per_cycle(); + } + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + diff --git a/Svc/Ccsds/CfdpManager/CfdpManager.fpp b/Svc/Ccsds/CfdpManager/CfdpManager.fpp new file mode 100644 index 00000000000..af5d8476dbf --- /dev/null +++ b/Svc/Ccsds/CfdpManager/CfdpManager.fpp @@ -0,0 +1,83 @@ +module Svc { +module Ccsds { +module Cfdp { + + @ F' implementation of the CFDP file transfer protocol + active component CfdpManager { + + ############################################################################## + # Includes + ############################################################################## + + include "Commands.fppi" + include "Events.fppi" + include "Parameters.fppi" + include "Telemetry.fppi" + + ############################################################################## + # Custom ports + ############################################################################## + + # Admin ports + @ Run port which must be invoked at 1 Hz in order to satisfy CFDP timer logic + async input port run1Hz: Svc.Sched + + @ Ping in port + async input port pingIn: Svc.Ping + + @ Ping out port + output port pingOut: Svc.Ping + + # Downlink ports + @ Port for outputting PDU data + output port dataOut: [NumChannels] Fw.BufferSend + + @ Buffer that was sent via the dataOut port and is now being returned + async input port dataReturnIn: [NumChannels] Fw.BufferSend + + @ Port for allocating buffers to hold PDU data + output port bufferAllocate: [NumChannels] Fw.BufferGet + + @ Port for deallocating buffers allocated for PDU data + output port bufferDeallocate: [NumChannels] Fw.BufferSend + + # Uplink ports + @ Port for input PDU data + async input port dataIn: [NumChannels] Fw.BufferSend + + @ Return buffer that was received on the dataIn port + output port dataInReturn: [NumChannels] Fw.BufferSend + + # DP ports + @ File send request port + guarded input port fileIn: Svc.SendFileRequest + + @ File send complete notification port + output port fileDoneOut: Svc.SendFileComplete + + + ############################################################################### + # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # + ############################################################################### + @ Port for requesting the current time + time get port timeCaller + + @ Enables command handling + import Fw.Command + + @ Enables event handling + import Fw.Event + + @ Enables telemetry channels handling + import Fw.Channel + + @ Port to return the value of a parameter + param get port prmGetOut + + @Port to set the value of a parameter + param set port prmSetOut + + } +} # end Cfdp +} # end Ccsds +} # end Svc \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/CfdpManager.hpp b/Svc/Ccsds/CfdpManager/CfdpManager.hpp new file mode 100644 index 00000000000..163ddd94f57 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/CfdpManager.hpp @@ -0,0 +1,476 @@ +// ====================================================================== +// \title CfdpManager.hpp +// \author Brian Campuzano +// \brief hpp file for CfdpManager component implementation class +// ====================================================================== + +#ifndef CCSDS_CFDPMANAGER_HPP +#define CCSDS_CFDPMANAGER_HPP + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// Forward declarations +class Engine; +class Channel; +class Transaction; + +class CfdpManager final : public CfdpManagerComponentBase { + friend class CfdpManagerTester; + // Give access to protected functions for EVRs and Telemetry + friend class Engine; + friend class Transaction; + + public: + // ---------------------------------------------------------------------- + // Component construction and destruction + // ---------------------------------------------------------------------- + + //! Construct CfdpManager object + CfdpManager(const char* const compName //!< The component name + ); + + //! Destroy CfdpManager object + ~CfdpManager(); + + //! Configure CFDP engine + //! + //! Initializes the CFDP engine and allocates all memory resources needed + //! for CFDP operations including transactions, chunks, and histories. + //! Must be called once after construction and before any CFDP operations. + void configure(void); + + public: + // ---------------------------------------------------------------------- + // Port calls that are invoked by the CFDP engine + // These functions are analogous to the functions in cf_cfdp_sbintf.* + // However these functions are not direct ports due to the architectural + // differences between F' and cFE + // ---------------------------------------------------------------------- + + //! Get a buffer for constructing an outgoing CFDP PDU + //! + //! Allocates a buffer from the downstream component for building a PDU. + //! Checks against the maximum number of PDUs allowed per cycle. + //! Equivalent to CF_CFDP_MsgOutGet in cFS. + //! + //! \param buffer [out] Buffer object to be populated with allocated memory + //! \param channel [in] Channel to allocate buffer for + //! \param size [in] Size of buffer needed in bytes + //! \return Status::SUCCESS if buffer allocated, Status::SEND_PDU_NO_BUF_AVAIL_ERROR otherwise + Status::T getPduBuffer(Fw::Buffer& buffer, Channel& channel, + FwSizeType size); + + //! Return an unused PDU buffer + //! + //! Deallocates a buffer that was obtained but not sent (e.g., due to error). + //! + //! \param channel [in] Channel that owns the buffer + //! \param pduBuffer [in] Buffer to return/deallocate + void returnPduBuffer(Channel& channel, Fw::Buffer& pduBuffer); + + //! Send a PDU buffer via output port + //! + //! Transmits a fully constructed PDU buffer via the dataOut port. + //! + //! \param channel [in] Channel to send on + //! \param pduBuffer [in] Buffer containing the PDU to send + void sendPduBuffer(Channel& channel, Fw::Buffer& pduBuffer); + + //! Send file completion notification for port-initiated transfers + //! + //! Invokes the fileDoneOut output port with the transaction status. + //! + //! \param status Transaction completion status + void sendFileComplete(Svc::SendFileStatus::T status); + + // ---------------------------------------------------------------- + // Telemetry helper methods (public for Engine/Transaction access) + // ---------------------------------------------------------------- + + //! Increment receive error counter + void incrementRecvErrors(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_recvErrors(m_channelTelemetry[chanId].get_recvErrors() + 1); + } + + //! Increment receive dropped counter + void incrementRecvDropped(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_recvDropped(m_channelTelemetry[chanId].get_recvDropped() + 1); + } + + //! Increment receive spurious counter + void incrementRecvSpurious(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_recvSpurious(m_channelTelemetry[chanId].get_recvSpurious() + 1); + } + + //! Add to received file data bytes + void addRecvFileDataBytes(U8 chanId, U32 bytes) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_recvFileDataBytes(m_channelTelemetry[chanId].get_recvFileDataBytes() + bytes); + } + + //! Add to received NAK segment requests + void addRecvNakSegmentRequests(U8 chanId, U32 count) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_recvNakSegmentRequests(m_channelTelemetry[chanId].get_recvNakSegmentRequests() + count); + } + + //! Increment received PDU counter + void incrementRecvPdu(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_recvPdu(m_channelTelemetry[chanId].get_recvPdu() + 1); + } + + //! Add to sent NAK segment requests + void addSentNakSegmentRequests(U8 chanId, U32 count) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_sentNakSegmentRequests(m_channelTelemetry[chanId].get_sentNakSegmentRequests() + count); + } + + //! Add sent file data bytes + void addSentFileDataBytes(U8 chanId, U32 bytes) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_sentFileDataBytes(m_channelTelemetry[chanId].get_sentFileDataBytes() + bytes); + } + + //! Increment sent PDU counter + void incrementSentPdu(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_sentPdu(m_channelTelemetry[chanId].get_sentPdu() + 1); + } + + //! Increment fault ACK limit counter + void incrementFaultAckLimit(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultAckLimit(m_channelTelemetry[chanId].get_faultAckLimit() + 1); + } + + //! Increment fault NAK limit counter + void incrementFaultNakLimit(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultNakLimit(m_channelTelemetry[chanId].get_faultNakLimit() + 1); + } + + //! Increment fault inactivity timer counter + void incrementFaultInactivityTimer(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultInactivityTimer(m_channelTelemetry[chanId].get_faultInactivityTimer() + 1); + } + + //! Increment fault CRC mismatch counter + void incrementFaultCrcMismatch(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultCrcMismatch(m_channelTelemetry[chanId].get_faultCrcMismatch() + 1); + } + + //! Increment fault file size mismatch counter + void incrementFaultFileSizeMismatch(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultFileSizeMismatch(m_channelTelemetry[chanId].get_faultFileSizeMismatch() + 1); + } + + //! Increment fault file open counter + void incrementFaultFileOpen(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultFileOpen(m_channelTelemetry[chanId].get_faultFileOpen() + 1); + } + + //! Increment fault file read counter + void incrementFaultFileRead(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultFileRead(m_channelTelemetry[chanId].get_faultFileRead() + 1); + } + + //! Increment fault file write counter + void incrementFaultFileWrite(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultFileWrite(m_channelTelemetry[chanId].get_faultFileWrite() + 1); + } + + //! Increment fault file seek counter + void incrementFaultFileSeek(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultFileSeek(m_channelTelemetry[chanId].get_faultFileSeek() + 1); + } + + //! Increment fault file rename counter + void incrementFaultFileRename(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultFileRename(m_channelTelemetry[chanId].get_faultFileRename() + 1); + } + + //! Increment fault directory read counter + void incrementFaultDirectoryRead(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + m_channelTelemetry[chanId].set_faultDirectoryRead(m_channelTelemetry[chanId].get_faultDirectoryRead() + 1); + } + + //! Get reference to channel telemetry for queue depth updates + Cfdp::ChannelTelemetry& getChannelTelemetryRef(U8 chanId) { + FW_ASSERT(chanId < Cfdp::NumChannels); + return m_channelTelemetry[chanId]; + } + + private: + // ---------------------------------------------------------------------- + // Handler implementations for typed input ports + // ---------------------------------------------------------------------- + + //! Handler implementation for run1Hz + //! + //! Run port which must be invoked at 1 Hz in order to satisfy CFDP timer logic + void run1Hz_handler(FwIndexType portNum, //!< The port number + U32 context //!< The call order + ) override; + + //! Handler for input port dataReturnIn + void dataReturnIn_handler( + FwIndexType portNum, //!< The port number + Fw::Buffer& fwBuffer + ) override; + + //! Handler for input port dataIn + void dataIn_handler(FwIndexType portNum, //!< The port number + Fw::Buffer& fwBuffer //!< The buffer + ) override; + + //! Handler for input port fileIn + Svc::SendFileResponse fileIn_handler( + FwIndexType portNum, //!< The port number + const Fw::StringBase& sourceFileName, //!< Path of file to send + const Fw::StringBase& destFileName, //!< Path to store file at destination + U32 offset, //!< Byte offset to start reading from + U32 length //!< Number of bytes to read (0 = entire file) + ) override; + + //! Handler for input port pingIn + void pingIn_handler( + FwIndexType portNum, //!< The port number + U32 key //!< Value to return to pinger + ) override; + + private: + // ---------------------------------------------------------------------- + // Handler implementations for commands + // ---------------------------------------------------------------------- + + //! Handler for command SendFile + //! + //! Command to start a CFDP file transaction + void SendFile_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID for the file transaction + EntityId destId, //!< Destination entity id + Class cfdpClass, //!< CFDP class for the file transfer + Keep keep, //!< Whether or not to keep or delete the file upon completion + U8 priority, //!< Priority: 0=highest priority + const Fw::CmdStringArg& sourceFileName, //!< The name of the on-board file to send + const Fw::CmdStringArg& destFileName //!< The name of the destination file on the ground + ) override; + + //! Handler for command PlaybackDirectory + //! + //! Command to start a directory playback + void PlaybackDirectory_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID for the file transaction(s) + EntityId destId, //!< Destination entity id + Class cfdpClass, //!< CFDP class for the file transfer(s) + Keep keep, //!< Whether or not to keep or delete the file(s) upon completion + U8 priority, //!< Priority: 0=highest priority + const Fw::CmdStringArg& sourceDirectory, //!< The name of the on-board directory to send + const Fw::CmdStringArg& destDirectory //!< The name of the destination directory on the ground + ) override; + + //! Handler for command PollDirectory + //! + //! Command to start a directory poll + void PollDirectory_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID for the file transaction(s) + U8 pollId, //!< Channel poll ID for the file transaction(s) + EntityId destId, //!< Destination entity id + Class cfdpClass, //!< CFDP class for the file transfer(s) + U8 priority, //!< Priority: 0=highest priority + U32 interval, //!< Interval to poll the directory in seconds + const Fw::CmdStringArg& sourceDirectory, //!< The name of the on-board directory to send + const Fw::CmdStringArg& destDirectory //!< The name of the destination directory on the ground + ) override; + + //! Handler for command StopPollDirectory + //! + //! Command to stop a directory poll + void StopPollDirectory_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID to stop + U8 pollId //!< Channel poll ID to stop + ) override; + + //! Handler for command SetChannelFlow + //! + //! Command to set channel's flow status + void SetChannelFlow_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID to set + Flow freeze //!< Flow state to set + ) override; + + //! Handler for command SuspendResumeTransaction + //! + //! Command to suspend or resume a transaction + void SuspendResumeTransaction_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID for the transaction + TransactionSeq transactionSeq, //!< Transaction sequence number + EntityId entityId, //!< Entity ID of the transaction + SuspendResume action //!< Action to take: SUSPEND or RESUME + ) override; + + //! Handler for command CancelTransaction + //! + //! Command to cancel a transaction with graceful close-out + void CancelTransaction_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID for the transaction + TransactionSeq transactionSeq, //!< Transaction sequence number + EntityId entityId //!< Entity ID of the transaction + ) override; + + //! Handler for command AbandonTransaction + //! + //! Command to abandon a transaction immediately + void AbandonTransaction_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId, //!< Channel ID for the transaction + TransactionSeq transactionSeq, //!< Transaction sequence number + EntityId entityId //!< Entity ID of the transaction + ) override; + + //! Handler for command ResetCounters + //! + //! Command to reset telemetry counters + void ResetCounters_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U8 channelId //!< Channel ID to reset (0xFF for all channels) + ) override; + + private: + // ---------------------------------------------------------------------- + // Private command helper functions + // ---------------------------------------------------------------------- + + //! Checks if the requested channel index is valid, and emits an EVR if not + Fw::CmdResponse::T checkCommandChannelIndex(U8 channelIndex //!< The channel index to check + ); + + //! Checks if the requested channel poll index is valid, and emits an EVR if not + Fw::CmdResponse::T checkCommandChannelPollIndex(U8 pollIndex //!< The poll index to check + ); + + public: + // ---------------------------------------------------------------------- + // Parameter helpers used by the CFDP engine + // ---------------------------------------------------------------------- + + //! Get the local entity ID parameter + //! + //! \return The local CFDP entity ID + EntityId getLocalEidParam(void); + + //! Get the outgoing file chunk size parameter + //! + //! \return Maximum size in bytes for file data segments in outgoing PDUs + U32 getOutgoingFileChunkSizeParam(void); + + //! Get the RX CRC calculation bytes per scheduler cycle parameter + //! + //! \return Number of bytes to process per cycle when calculating received file CRC + U32 getRxCrcCalcBytesPerCycleParam(void); + + //! Get the temporary directory parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Path to temporary directory for in-progress file transfers + Fw::String getTmpDirParam(U8 channelIndex); + + //! Get the failure directory parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Path to directory where failed transfers are moved + Fw::String getFailDirParam(U8 channelIndex); + + //! Get the ACK limit parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Maximum number of times to retry sending an ACK PDU + U8 getAckLimitParam(U8 channelIndex); + + //! Get the NAK limit parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Maximum number of times to retry sending a NAK PDU + U8 getNackLimitParam(U8 channelIndex); + + //! Get the ACK timer parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return ACK timeout value in seconds + U32 getAckTimerParam(U8 channelIndex); + + //! Get the inactivity timer parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Inactivity timeout value in seconds + U32 getInactivityTimerParam(U8 channelIndex); + + //! Get the dequeue enabled parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Whether the channel is enabled for dequeuing transactions + Fw::Enabled getDequeueEnabledParam(U8 channelIndex); + + //! Get the move directory parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Path to directory where completed transfers are moved + Fw::String getMoveDirParam(U8 channelIndex); + + //! Get the maximum outgoing PDUs per cycle parameter for a channel + //! + //! \param channelIndex [in] Index of the channel + //! \return Maximum number of PDUs that can be sent per engine cycle + U32 getMaxOutgoingPdusPerCycleParam(U8 channelIndex); + + private: + // ---------------------------------------------------------------------- + // Member variables + // ---------------------------------------------------------------------- + // CFDP Engine - owns all protocol state and operations + Engine* m_engine; + + //! Telemetry array for all CFDP channels + Cfdp::ChannelTelemetryArray m_channelTelemetry; + +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // CCSDS_CFDPMANAGER_HPP diff --git a/Svc/Ccsds/CfdpManager/Channel.cpp b/Svc/Ccsds/CfdpManager/Channel.cpp new file mode 100644 index 00000000000..230c00b5420 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Channel.cpp @@ -0,0 +1,922 @@ +// ====================================================================== +// \title Channel.cpp +// \brief CFDP Channel operations implementation +// +// This file is a port of channel-specific functions from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp.c (channel processing functions) +// - cf_utils.c (channel transaction and resource management) +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include +#include + +#include + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ---------------------------------------------------------------------- +// Construction +// ---------------------------------------------------------------------- + +Channel::Channel(Engine* engine, U8 channelId, CfdpManager* cfdpManager) : + m_engine(engine), + m_numCmdTx(0), + m_currentTxn(nullptr), + m_cfdpManager(cfdpManager), + m_tickType(0), + m_channelId(channelId), + m_flowState(Cfdp::Flow::NOT_FROZEN), + m_outgoingCounter(0), + m_transactions(nullptr), + m_histories(nullptr), + m_chunks(nullptr), + m_chunkMem(nullptr) +{ + FW_ASSERT(engine != nullptr); + FW_ASSERT(cfdpManager != nullptr); + + // Initialize queue pointers + for (U32 i = 0; i < QueueId::NUM; i++) { + m_qs[i] = nullptr; + } + + // Initialize command/history lists + for (U32 i = 0; i < DIRECTION_NUM; i++) { + m_cs[i] = nullptr; + } + + // Initialize poll directory playback state + for (U32 i = 0; i < CFDP_MAX_POLLING_DIR_PER_CHAN; i++) { + m_polldir[i].pb.busy = false; + m_polldir[i].pb.diropen = false; + m_polldir[i].pb.counted = false; + m_polldir[i].pb.num_ts = 0; + m_polldir[i].pb.pending_file[0] = '\0'; + } + + // Initialize playback structures + for (U32 i = 0; i < CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN; i++) { + m_playback[i].busy = false; + m_playback[i].diropen = false; + m_playback[i].counted = false; + m_playback[i].num_ts = 0; + m_playback[i].pending_file[0] = '\0'; + } + + // Allocate and initialize per-channel resources + U32 j, k; + History* history; + Transaction* txn; + CfdpChunkWrapper* cw; + CListNode** list_head; + U32 chunk_mem_offset = 0; + U32 total_chunks_needed; + + // Initialize chunk configuration for this channel + const U32 rxChunksPerChannel[] = CFDP_CHANNEL_NUM_RX_CHUNKS_PER_TRANSACTION; + const U32 txChunksPerChannel[] = CFDP_CHANNEL_NUM_TX_CHUNKS_PER_TRANSACTION; + m_dirMaxChunks[DIRECTION_RX] = rxChunksPerChannel[m_channelId]; + m_dirMaxChunks[DIRECTION_TX] = txChunksPerChannel[m_channelId]; + + // Calculate total chunks needed for this channel + total_chunks_needed = 0; + for (k = 0; k < DIRECTION_NUM; ++k) { + total_chunks_needed += m_dirMaxChunks[k] * CFDP_NUM_TRANSACTIONS_PER_CHANNEL; + } + + // Allocate arrays + // Use operator new for raw memory (for types requiring placement new with constructor params) + m_transactions = static_cast( + ::operator new(CFDP_NUM_TRANSACTIONS_PER_CHANNEL * sizeof(Transaction)) + ); + m_chunks = static_cast( + ::operator new((CFDP_NUM_TRANSACTIONS_PER_CHANNEL * DIRECTION_NUM) * sizeof(CfdpChunkWrapper)) + ); + // Regular new for simple types + m_histories = new History[CFDP_NUM_HISTORIES_PER_CHANNEL]; + m_chunkMem = new Chunk[total_chunks_needed]; + + // Initialize transactions using placement new with parameterized constructor + cw = m_chunks; + for (j = 0; j < CFDP_NUM_TRANSACTIONS_PER_CHANNEL; ++j) + { + // Construct transaction in-place with parameterized constructor + txn = new (&m_transactions[j]) Transaction(this, m_channelId, m_engine, m_cfdpManager); + + // Put transaction on free list + this->freeTransaction(txn); + + // Initialize chunk wrappers for this transaction (TX and RX) + for (k = 0; k < DIRECTION_NUM; ++k, ++cw) + { + list_head = this->getChunkListHead(static_cast(k)); + + // Use placement new to construct CfdpChunkWrapper with the new class-based interface + new (cw) CfdpChunkWrapper(static_cast(m_dirMaxChunks[k]), &m_chunkMem[chunk_mem_offset]); + chunk_mem_offset += m_dirMaxChunks[k]; + CfdpCListInitNode(&cw->cl_node); + CfdpCListInsertBack(list_head, &cw->cl_node); + } + } + + // Initialize histories + for (j = 0; j < CFDP_NUM_HISTORIES_PER_CHANNEL; ++j) + { + history = &m_histories[j]; + // Zero-initialize using aggregate initialization + *history = {}; + CfdpCListInitNode(&history->cl_node); + this->insertBackInQueue(QueueId::HIST_FREE, &history->cl_node); + } +} + +Channel::~Channel() +{ + // Free dynamically allocated resources + if (m_transactions != nullptr) { + // Manually call destructors since we used placement new + for (U32 j = 0; j < CFDP_NUM_TRANSACTIONS_PER_CHANNEL; ++j) { + m_transactions[j].~Transaction(); + } + // Free raw memory allocated with operator new + ::operator delete(m_transactions); + m_transactions = nullptr; + } + if (m_histories != nullptr) { + delete[] m_histories; + m_histories = nullptr; + } + if (m_chunks != nullptr) { + // Manually call destructors since we used placement new + for (U32 j = 0; j < (CFDP_NUM_TRANSACTIONS_PER_CHANNEL * DIRECTION_NUM); ++j) { + m_chunks[j].~CfdpChunkWrapper(); + } + // Free raw memory allocated with operator new + ::operator delete(m_chunks); + m_chunks = nullptr; + } + if (m_chunkMem != nullptr) { + delete[] m_chunkMem; + m_chunkMem = nullptr; + } +} + +// ---------------------------------------------------------------------- +// Channel Processing +// ---------------------------------------------------------------------- + +void Channel::cycleTx() +{ + Transaction* txn; + CycleTxArgs args; + + if (m_cfdpManager->getDequeueEnabledParam(m_channelId)) + { + args.chan = this; + args.ran_one = 0; + + // loop through as long as there are pending transactions, and a message buffer to send their PDUs on + + // NOTE: tick processing is higher priority than sending new filedata PDUs, so only send however many + // PDUs that can be sent once we get to here + if (!this->m_currentTxn) + { // don't enter if currentTxn is set, since we need to pick up where we left off on tick processing next scheduler cycle + + // TODO BPC: refactor all while loops + while (true) + { + // Attempt to run something on TXA + CfdpCListTraverse(m_qs[QueueId::TXA], + [this](CListNode* node, void* context) -> CListTraverseStatus { + return this->cycleTxFirstActive(node, context); + }, + &args); + + // Keep going until QueueId::PEND is empty or something is run + if (args.ran_one || m_qs[QueueId::PEND] == NULL) + { + break; + } + + txn = container_of_cpp(m_qs[QueueId::PEND], &Transaction::m_cl_node); + + // Class 2 transactions need a chunklist for NAK processing, get one now. + // Class 1 transactions don't need chunks since they don't support NAKs. + if (txn->getClass() == Cfdp::Class::CLASS_2) + { + if (txn->m_chunks == NULL) + { + txn->m_chunks = this->findUnusedChunks(DIRECTION_TX); + } + if (txn->m_chunks == NULL) + { + // TODO BPC: Emit EVR + // Leave transaction pending until a chunklist is available. + break; + } + } + + m_engine->armInactTimer(txn); + this->moveTransaction(txn, QueueId::TXA); + } + } + + // in case the loop exited due to no message buffers, clear it and start from the top next time + this->m_currentTxn = NULL; + } +} + +void Channel::tickTransactions() +{ + bool reset = true; + + void (Transaction::*fns[CFDP_TICK_TYPE_NUM_TYPES])(int*) = {&Transaction::rTick, &Transaction::sTick, + &Transaction::sTickNak}; + int qs[CFDP_TICK_TYPE_NUM_TYPES] = {QueueId::RX, QueueId::TXW, QueueId::TXW}; + + FW_ASSERT(m_tickType < CFDP_TICK_TYPE_NUM_TYPES, m_tickType); + + for (; m_tickType < CFDP_TICK_TYPE_NUM_TYPES; ++m_tickType) + { + TickArgs args = {this, fns[m_tickType], 0, 0}; + + do + { + args.cont = 0; + CfdpCListTraverse(m_qs[qs[m_tickType]], + [this](CListNode* node, void* context) -> CListTraverseStatus { + return this->doTick(node, context); + }, + &args); + + if (args.early_exit) + { + // early exit means we ran out of available outgoing messages this scheduler cycle. + // If current tick type is NAK response, then reset tick type. It would be + // bad to let NAK response starve out RX or TXW ticks on the next cycle. + // + // If RX ticks use up all available messages, then we pick up where we left + // off on the next cycle. (This causes some RX tick counts to be missed, + // but that's ok. Precise timing isn't required.) + // + // This scheme allows the following priority for use of outgoing messages: + // + // RX state messages + // TXW state messages + // NAK response (could be many) + // + // New file data on TXA + if (m_tickType != CFDP_TICK_TYPE_TXW_NAK) + { + reset = false; + } + + break; + } + } + while (args.cont); + + if (!reset) + { + break; + } + } + + if (reset) + { + m_tickType = CFDP_TICK_TYPE_RX; // reset tick type + } +} + +void Channel::processPlaybackDirectories() +{ + U32 i; + U8 playback_count = 0; + + for (i = 0; i < CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN; ++i) + { + this->processPlaybackDirectory(&m_playback[i]); + // Count active playback operations + if (m_playback[i].busy) + { + playback_count++; + } + } + + // Update playback counter telemetry + Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId); + tlm.set_playbackCounter(playback_count); +} + +void Channel::processPollingDirectories() +{ + CfdpPollDir* pd; + U32 i; + U8 poll_count = 0; + Status::T status; + + for (i = 0; i < CFDP_MAX_POLLING_DIR_PER_CHAN; ++i) + { + pd = &m_polldir[i]; + + if (pd->enabled) + { + poll_count++; + + if ((pd->pb.busy == false) && (pd->pb.num_ts == 0)) + { + if ((pd->intervalTimer.getStatus() != Timer::Status::RUNNING) && (pd->intervalSec > 0)) + { + // timer was not set, so set it now + pd->intervalTimer.setTimer(pd->intervalSec); + } + else if (pd->intervalTimer.getStatus() == Timer::Status::EXPIRED) + { + // the timer has expired + status = m_engine->playbackDirInitiate(&pd->pb, pd->srcDir, pd->dstDir, pd->cfdpClass, + Cfdp::Keep::DELETE, m_channelId, pd->priority, + pd->destEid); + if (status != Cfdp::Status::SUCCESS) + { + // error occurred in playback directory, so reset the timer + // an event is sent when initiating playback directory so there is no reason to + // to have another here + pd->intervalTimer.setTimer(pd->intervalSec); + } + } + else + { + pd->intervalTimer.run(); + } + } + else + { + // playback is active, so step it + this->processPlaybackDirectory(&pd->pb); + } + } + } + + // Update poll counter telemetry + Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId); + tlm.set_pollCounter(poll_count); +} + +// ---------------------------------------------------------------------- +// Transaction Management +// ---------------------------------------------------------------------- + +Transaction* Channel::findUnusedTransaction(Direction direction) +{ + CListNode* node; + Transaction* txn; + QueueId::T q_index; // initialized below in if + + if (m_qs[QueueId::FREE]) + { + node = m_qs[QueueId::FREE]; + txn = container_of_cpp(node, &Transaction::m_cl_node); + + this->removeFromQueue(QueueId::FREE, &txn->m_cl_node); + + // now that a transaction is acquired, must also acquire a history slot to go along with it + if (m_qs[QueueId::HIST_FREE]) + { + q_index = QueueId::HIST_FREE; + } + else + { + // no free history, so take the oldest one from the channel's history queue + FW_ASSERT(m_qs[QueueId::HIST]); + q_index = QueueId::HIST; + } + + txn->m_history = container_of_cpp(m_qs[q_index], &History::cl_node); + + this->removeFromQueue(q_index, &txn->m_history->cl_node); + + // Indicate that this was freshly pulled from the free list + // notably this state is distinguishable from items still on the free list + txn->m_state = TXN_STATE_INIT; + txn->m_history->dir = direction; + txn->m_chan = this; // Set channel pointer + + // Re-initialize the linked list node to clear stale pointers from FREE list + CfdpCListInitNode(&txn->m_cl_node); + } + else + { + txn = NULL; + } + + return txn; +} + +Transaction* Channel::findTransactionBySequenceNumber(TransactionSeq transaction_sequence_number, + EntityId src_eid) +{ + // need to find transaction by sequence number. It will either be the active transaction (front of Q_PEND), + // or on Q_TX or Q_RX. Once a transaction moves to history, then it's done. + // + // Let's put QueueId::RX up front, because most RX packets will be file data PDUs + CfdpTraverseTransSeqArg ctx = {transaction_sequence_number, src_eid, NULL}; + CListNode* ptrs[] = {m_qs[QueueId::RX], m_qs[QueueId::PEND], m_qs[QueueId::TXA], + m_qs[QueueId::TXW]}; + Transaction* ret = NULL; + + for (CListNode* head : ptrs) + { + CfdpCListTraverse(head, Transaction::findBySequenceNumberCallback, &ctx); + if (ctx.txn) + { + ret = ctx.txn; + break; + } + } + + return ret; +} + +I32 Channel::traverseAllTransactions(CfdpTraverseAllTransactionsFunc fn, void* context) +{ + CfdpTraverseAllArg args = {fn, context, 0}; + for (I32 queueidx = QueueId::PEND; queueidx <= QueueId::RX; ++queueidx) + { + CfdpCListTraverse(m_qs[queueidx], + [&args](CListNode* node, void*) -> CListTraverseStatus { + Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node); + args.fn(txn, args.context); + ++args.counter; + return CLIST_TRAVERSE_CONTINUE; + }, + nullptr); + } + + return args.counter; +} + +void Channel::resetHistory(History* history) +{ + this->removeFromQueue(QueueId::HIST, &history->cl_node); + this->insertBackInQueue(QueueId::HIST_FREE, &history->cl_node); +} + +// ---------------------------------------------------------------------- +// Transaction Queue Management +// ---------------------------------------------------------------------- + +void Channel::dequeueTransaction(Transaction* txn) +{ + FW_ASSERT(txn); + CfdpCListRemove(&m_qs[txn->m_flags.com.q_index], &txn->m_cl_node); + + // Update queue depth telemetry + Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId); + switch (txn->m_flags.com.q_index) { + case Cfdp::QueueId::FREE: + + tlm.set_queueFree(tlm.get_queueFree() - 1); + break; + case Cfdp::QueueId::TXA: + + tlm.set_queueTxActive(tlm.get_queueTxActive() - 1); + break; + case Cfdp::QueueId::TXW: + + tlm.set_queueTxWaiting(tlm.get_queueTxWaiting() - 1); + break; + case Cfdp::QueueId::RX: + + tlm.set_queueRx(tlm.get_queueRx() - 1); + break; + case Cfdp::QueueId::HIST: + + tlm.set_queueHistory(tlm.get_queueHistory() - 1); + break; + case Cfdp::QueueId::PEND: + case Cfdp::QueueId::HIST_FREE: + // PEND and HIST_FREE queues are not tracked in telemetry + break; + default: + FW_ASSERT(0, txn->m_flags.com.q_index); + } +} + +void Channel::moveTransaction(Transaction* txn, QueueId::T queue) +{ + FW_ASSERT(txn); + Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId); + + // Decrement old queue + CfdpCListRemove(&m_qs[txn->m_flags.com.q_index], &txn->m_cl_node); + switch (txn->m_flags.com.q_index) { + case Cfdp::QueueId::FREE: + + tlm.set_queueFree(tlm.get_queueFree() - 1); + break; + case Cfdp::QueueId::TXA: + + tlm.set_queueTxActive(tlm.get_queueTxActive() - 1); + break; + case Cfdp::QueueId::TXW: + + tlm.set_queueTxWaiting(tlm.get_queueTxWaiting() - 1); + break; + case Cfdp::QueueId::RX: + + tlm.set_queueRx(tlm.get_queueRx() - 1); + break; + case Cfdp::QueueId::HIST: + + tlm.set_queueHistory(tlm.get_queueHistory() - 1); + break; + case Cfdp::QueueId::PEND: + case Cfdp::QueueId::HIST_FREE: + // PEND and HIST_FREE queues are not tracked in telemetry + break; + default: + FW_ASSERT(0, txn->m_flags.com.q_index); + } + + // Increment new queue + CfdpCListInsertBack(&m_qs[queue], &txn->m_cl_node); + txn->m_flags.com.q_index = queue; + switch (queue) { + case Cfdp::QueueId::FREE: + tlm.set_queueFree(tlm.get_queueFree() + 1); + break; + case Cfdp::QueueId::TXA: + tlm.set_queueTxActive(tlm.get_queueTxActive() + 1); + break; + case Cfdp::QueueId::TXW: + tlm.set_queueTxWaiting(tlm.get_queueTxWaiting() + 1); + break; + case Cfdp::QueueId::RX: + tlm.set_queueRx(tlm.get_queueRx() + 1); + break; + case Cfdp::QueueId::HIST: + tlm.set_queueHistory(tlm.get_queueHistory() + 1); + break; + case Cfdp::QueueId::PEND: + case Cfdp::QueueId::HIST_FREE: + // PEND and HIST_FREE queues are not tracked in telemetry + break; + default: + FW_ASSERT(0, queue); + } +} + +void Channel::freeTransaction(Transaction* txn) +{ + // Reset transaction to default state (preserves channel context) + txn->reset(); + + // Initialize the linked list node for the FREE queue + CfdpCListInitNode(&txn->m_cl_node); + this->insertBackInQueue(QueueId::FREE, &txn->m_cl_node); +} + +void Channel::recycleTransaction(Transaction *txn) +{ + CListNode **chunklist_head; + QueueId::T hist_destq; + + // File should have been closed by the state machine, but if + // it still hanging open at this point, close it now so its not leaked. + // This is not normal/expected so log it if this happens. + if (true == txn->m_fd.isOpen()) + { + // CFE_ES_WriteToSysLog("%s(): Closing dangling file handle: %lu\n", __func__, OS_ObjectIdToInteger(txn->fd)); + txn->m_fd.close(); + } + + this->dequeueTransaction(txn); // this makes it "float" (not in any queue) + + // this should always be + if (txn->m_history != NULL) + { + if (txn->m_chunks != NULL) + { + chunklist_head = this->getChunkListHead(txn->m_history->dir); + if (chunklist_head != NULL) + { + CfdpCListInsertBack(chunklist_head, &txn->m_chunks->cl_node); + txn->m_chunks = NULL; + } + } + + if (txn->m_flags.com.keep_history) + { + // move transaction history to history queue + hist_destq = QueueId::HIST; + } + else + { + hist_destq = QueueId::HIST_FREE; + } + this->insertBackInQueue(hist_destq, &txn->m_history->cl_node); + txn->m_history = NULL; + } + + // this wipes it and puts it back onto the list to be found by + // Channel::findUnusedTransaction(). Need to preserve the chan_num + // and keep it associated with this channel, though. + this->freeTransaction(txn); +} + +void Channel::insertSortPrio(Transaction* txn, QueueId::T queue) +{ + bool insert_back = false; + + FW_ASSERT(txn); + + // look for proper position on PEND queue for this transaction. + // This is a simple priority sort. + + if (!m_qs[queue]) + { + // list is empty, so just insert + insert_back = true; + } + else + { + CfdpTraversePriorityArg arg = {NULL, txn->getPriority()}; + CfdpCListTraverseR(m_qs[queue], Transaction::prioritySearchCallback, &arg); + if (arg.txn) + { + this->insertAfterInQueue(queue, &arg.txn->m_cl_node, &txn->m_cl_node); + } + else + { + insert_back = true; + } + } + + if (insert_back) + { + this->insertBackInQueue(queue, &txn->m_cl_node); + } + txn->m_flags.com.q_index = queue; +} + +// ---------------------------------------------------------------------- +// Channel State Management +// ---------------------------------------------------------------------- + +void Channel::decrementCmdTxCounter() +{ + FW_ASSERT(m_numCmdTx); // sanity check + --m_numCmdTx; +} + +void Channel::clearCurrentIfMatch(Transaction* txn) +{ + // Done with this TX transaction + if (this->m_currentTxn == txn) + { + this->m_currentTxn = NULL; + } +} + +void Channel::setCurrentTxn(const Transaction* txn) +{ + this->m_currentTxn = txn; +} + +// ---------------------------------------------------------------------- +// Resource Management +// ---------------------------------------------------------------------- + +CListNode** Channel::getChunkListHead(U8 direction) +{ + CListNode** result; + + if (direction < DIRECTION_NUM) + { + result = &m_cs[direction]; + } + else + { + result = NULL; + } + + return result; +} + +CfdpChunkWrapper* Channel::findUnusedChunks(Direction dir) +{ + CfdpChunkWrapper* ret = NULL; + CListNode* node; + CListNode** chunklist_head; + + chunklist_head = this->getChunkListHead(dir); + + // this should never be null + FW_ASSERT(chunklist_head); + + if (*chunklist_head != NULL) + { + node = CfdpCListPop(chunklist_head); + if (node != NULL) + { + ret = container_of_cpp(node, &CfdpChunkWrapper::cl_node); + } + } + + return ret; +} + +// ---------------------------------------------------------------------- +// Private helper methods +// ---------------------------------------------------------------------- + +void Channel::processPlaybackDirectory(Playback* pb) +{ + Transaction* txn; + char path[MaxFilePathSize]; + Os::Directory::Status status; + + // either there's no transaction (first one) or the last one was finished, so check for a new one + + memset(&path, 0, sizeof(path)); + + while (pb->diropen && (pb->num_ts < CFDP_NUM_TRANSACTIONS_PER_PLAYBACK)) + { + if (pb->pending_file[0] == 0) + { + status = pb->dir.read(path, MaxFilePathSize); + if (status == Os::Directory::NO_MORE_FILES) + { + // TODO BPC: Emit playback success EVR + pb->dir.close(); + pb->diropen = false; + break; + } + if (status != Os::Directory::OP_OK) + { + // TODO BPC: emit playback error EVR + pb->dir.close(); + pb->diropen = false; + break; + } + + strncpy(pb->pending_file, path, sizeof(pb->pending_file) - 1); + pb->pending_file[sizeof(pb->pending_file) - 1] = 0; + } + else + { + txn = this->findUnusedTransaction(DIRECTION_TX); + if (txn == NULL) + { + // while not expected this can certainly happen, because + // rx transactions consume in these as well. + // should not need to do anything special, will come back next tick + break; + } + + // Append file name to source/destination folders + txn->m_history->fnames.src_filename = pb->fnames.src_filename; + txn->m_history->fnames.src_filename += "/"; + txn->m_history->fnames.src_filename += pb->pending_file; + + txn->m_history->fnames.dst_filename = pb->fnames.dst_filename; + txn->m_history->fnames.dst_filename += "/"; + txn->m_history->fnames.dst_filename += pb->pending_file; + + m_engine->txFileInitiate(txn, pb->cfdp_class, pb->keep, m_channelId, pb->priority, + pb->dest_id); + + txn->m_pb = pb; + ++pb->num_ts; + + pb->pending_file[0] = 0; // continue reading dir + } + } + + if (!pb->diropen && !pb->num_ts) + { + // the directory has been exhausted, and there are no more active transactions + // for this playback -- so mark it as not busy + pb->busy = false; + } +} + +void Channel::updatePollPbCounted(Playback* pb, int up, U8* counter) +{ + if (pb->counted != up) + { + // only handle on state change + pb->counted = !!up; // !! ensure 0 or 1, should be optimized out + + if (up) + { + ++*counter; + } + else + { + FW_ASSERT(*counter); // sanity check it isn't zero + --*counter; + } + } +} + +CListTraverseStatus Channel::cycleTxFirstActive(CListNode* node, void* context) +{ + CycleTxArgs* args = static_cast(context); + Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node); + CListTraverseStatus ret = CLIST_TRAVERSE_EXIT; // default option is exit traversal + + if (txn->m_flags.com.suspended) + { + ret = CLIST_TRAVERSE_CONTINUE; // suspended, so move on to next + } + else + { + FW_ASSERT(txn->m_flags.com.q_index == QueueId::TXA); // huh? + + // if no more messages, then chan->m_currentTxn will be set. + // If the transaction sent the last filedata PDU and EOF, it will move itself + // off the active queue. Run until either of these occur. + while (!this->m_currentTxn && txn->m_flags.com.q_index == QueueId::TXA) + { + m_engine->dispatchTx(txn); + } + + args->ran_one = 1; + } + + return ret; +} + +CListTraverseStatus Channel::doTick(CListNode* node, void* context) +{ + CListTraverseStatus ret = CLIST_TRAVERSE_CONTINUE; // CLIST_TRAVERSE_CONTINUE means don't tick one, keep looking for currentTxn + TickArgs* args = static_cast(context); + Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node); + if (!this->m_currentTxn || (this->m_currentTxn == txn)) + { + // found where we left off, so clear that and move on + this->m_currentTxn = NULL; + if (!txn->m_flags.com.suspended) + { + (txn->*args->fn)(&args->cont); + } + + // if this->m_currentTxn was set to not-NULL above, then exit early + // NOTE: if channel is frozen, then tick processing won't have been entered. + // so there is no need to check it here + if (this->m_currentTxn) + { + ret = CLIST_TRAVERSE_EXIT; + args->early_exit = true; + } + } + + return ret; // don't tick one, keep looking for currentTxn +} + +Transaction* Channel::getTransaction(U32 index) +{ + FW_ASSERT(index < CFDP_NUM_TRANSACTIONS_PER_CHANNEL); + return &m_transactions[index]; +} + +History* Channel::getHistory(U32 index) +{ + FW_ASSERT(index < CFDP_NUM_HISTORIES_PER_CHANNEL); + return &m_histories[index]; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Channel.hpp b/Svc/Ccsds/CfdpManager/Channel.hpp new file mode 100644 index 00000000000..30ea2d8a2c0 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Channel.hpp @@ -0,0 +1,495 @@ +// ====================================================================== +// \title Channel.hpp +// \brief CFDP Channel operations +// +// This file is a port of channel-specific functions from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp.c (channel processing functions) +// - cf_utils.c (channel transaction and resource management) +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#ifndef CFDP_CHANNEL_HPP +#define CFDP_CHANNEL_HPP + +#include + +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// Forward declarations +class Engine; +class Transaction; + +/** + * @brief CFDP Channel class + * + * Encapsulates channel-specific operations for CFDP protocol processing. + * Each channel manages its own set of transactions, playback directories, + * and polling directories. + */ +class Channel { + public: + // ---------------------------------------------------------------------- + // Construction + // ---------------------------------------------------------------------- + + /** + * @brief Construct a Channel + * + * @param engine Pointer to parent CFDP engine + * @param channelId Channel ID (index) + * @param cfdpManager Pointer to parent CfdpManager component + */ + Channel(Engine* engine, U8 channelId, CfdpManager* cfdpManager); + + /** + * @brief Destruct a Channel + * + * Frees dynamically allocated resources (transactions, histories, chunks) + */ + ~Channel(); + + // ---------------------------------------------------------------------- + // Channel Processing + // ---------------------------------------------------------------------- + + /** + * @brief Cycle the TX side of this channel + * + * Processes outgoing transactions and sends PDUs for this channel. + */ + void cycleTx(); + + /** + * @brief Tick all transactions on this channel + * + * Processes timer expirations and retransmissions for all active transactions. + */ + void tickTransactions(); + + /** + * @brief Process all playback directories for this channel + */ + void processPlaybackDirectories(); + + /** + * @brief Process all polling directories for this channel + */ + void processPollingDirectories(); + + // ---------------------------------------------------------------------- + // Transaction Management + // ---------------------------------------------------------------------- + + /** + * @brief Find an unused transaction on this channel + * + * @param direction Intended direction of data flow (TX or RX) + * + * @returns Pointer to a free transaction + * @retval NULL if no free transactions available. + */ + Transaction* findUnusedTransaction(Direction direction); + + /** + * @brief Finds an active transaction by sequence number + * + * This function traverses the active rx, pending, txa, and txw + * transaction queues and looks for the requested transaction. + * + * @param transaction_sequence_number Sequence number to find + * @param src_eid Entity ID associated with sequence number + * + * @returns Pointer to the given transaction if found + * @retval NULL if the transaction is not found + */ + Transaction* findTransactionBySequenceNumber(TransactionSeq transaction_sequence_number, + EntityId src_eid); + + /** + * @brief Traverses all transactions on all active queues and performs an operation on them + * + * @param fn Callback to invoke for all traversed transactions + * @param context Opaque object to pass to all callbacks + * + * @returns Number of transactions traversed + */ + I32 traverseAllTransactions(CfdpTraverseAllTransactionsFunc fn, void* context); + + /** + * @brief Returns a history structure back to its unused state + * + * There's nothing to do currently other than remove the history + * from its current queue and put it back on QueueId::HIST_FREE. + * + * @param history Pointer to the history entry + */ + void resetHistory(History* history); + + // ---------------------------------------------------------------------- + // Channel State Management + // ---------------------------------------------------------------------- + + /** + * @brief Get the channel ID + * + * @returns Channel ID + */ + inline U8 getChannelId() const { return m_channelId; } + + /** + * @brief Get the outgoing PDU counter for this cycle + * + * @returns Current outgoing PDU count + */ + inline U32 getOutgoingCounter() const { return m_outgoingCounter; } + + /** + * @brief Increment the outgoing PDU counter + */ + inline void incrementOutgoingCounter() { ++m_outgoingCounter; } + + /** + * @brief Reset the outgoing PDU counter to zero + */ + inline void resetOutgoingCounter() { m_outgoingCounter = 0; } + + /** + * @brief Get the number of commanded TX transactions + * + * @returns Number of commanded TX transactions + */ + inline U32 getNumCmdTx() const { return m_numCmdTx; } + + /** + * @brief Increment the command TX counter for this channel + */ + inline void incrementCmdTxCounter() { ++m_numCmdTx; } + + /** + * @brief Decrement the command TX counter for this channel + */ + void decrementCmdTxCounter(); + + /** + * @brief Check if current transaction matches and clear if so + * + * @param txn Transaction to check against current + */ + void clearCurrentIfMatch(Transaction* txn); + + /** + * @brief Set current transaction + * + * Used when a transaction cannot make progress this cycle + * (e.g., throttle limit reached, file transfer complete). + * + * @param txn Transaction to set as current + */ + void setCurrentTxn(const Transaction* txn); + + /** + * @brief Set the flow state for this channel + * + * @param flowState New flow state (NORMAL or FROZEN) + */ + inline void setFlowState(Flow::T flowState) { m_flowState = flowState; } + + /** + * @brief Get the flow state for this channel + * + * @returns Current flow state + */ + inline Flow::T getFlowState() const { return m_flowState; } + + /** + * @brief Get a playback directory entry + * + * @param index Index of playback directory + * @returns Pointer to playback directory + */ + inline Playback* getPlayback(U32 index) { + FW_ASSERT(index < CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN); + return &m_playback[index]; + } + + /** + * @brief Get a polling directory entry + * + * @param index Index of polling directory + * @returns Pointer to polling directory + */ + inline CfdpPollDir* getPollDir(U32 index) { + FW_ASSERT(index < CFDP_MAX_POLLING_DIR_PER_CHAN); + return &m_polldir[index]; + } + + /** + * @brief Get a transaction by index (for testing) + * + * @param index Transaction index within this channel + * @returns Pointer to transaction + */ + Transaction* getTransaction(U32 index); + + /** + * @brief Get a history by index (for testing) + * + * @param index History index within this channel + * @returns Pointer to history entry + */ + History* getHistory(U32 index); + + // ---------------------------------------------------------------------- + // Resource Management + // ---------------------------------------------------------------------- + + /** + * @brief Gets the head of the chunk list for this channel + direction + * + * The chunk list contains structs that are available for tracking the chunks + * associated with files in transit. An entry needs to be pulled from this + * list for every transaction, and returned to this list when the transaction + * completes. + * + * @param direction Whether this is TX or RX + * + * @returns Pointer to list head + */ + CListNode** getChunkListHead(U8 direction); + + /** + * @brief Find unused chunks for this channel + * + * @param dir Direction (TX or RX) + * + * @returns Pointer to unused chunk wrapper + * @retval NULL if no chunks available + */ + CfdpChunkWrapper* findUnusedChunks(Direction dir); + + // ---------------------------------------------------------------------- + // Transaction Management + // ---------------------------------------------------------------------- + + /** + * @brief Free a transaction from the queue it's on + * + * NOTE: this leaves the transaction in a bad state, + * so it must be followed by placing the transaction on + * another queue. Need this function because the path of + * freeing a transaction (returning to default state) + * means that it must be removed from the current queue + * otherwise if the structure is zero'd out the queue + * will become corrupted due to other nodes on the queue + * pointing to an invalid node + * + * @param txn Pointer to the transaction object + */ + void dequeueTransaction(Transaction* txn); + + /** + * @brief Move a transaction from one queue to another + * + * @param txn Pointer to the transaction object + * @param queue Index of destination queue + */ + void moveTransaction(Transaction* txn, QueueId::T queue); + + /** + * @brief Frees and resets a transaction and returns it for later use + * + * @param txn Pointer to the transaction object + */ + void freeTransaction(Transaction* txn); + + /** + * @brief Recover resources associated with a transaction + * + * Wipes all data in the transaction struct and returns everything to its + * relevant FREE list so it can be used again. + * + * Notably, should any PDUs arrive after this that is related to this + * transaction, these PDUs will not be identifiable, and no longer associable + * to this transaction. + * + * It is imperative that nothing uses the txn struct after this call, + * as it will now be invalid. This is effectively like free(). + * + * @param txn Pointer to the transaction object + */ + void recycleTransaction(Transaction *txn); + + /** + * @brief Insert a transaction into a priority sorted transaction queue + * + * This function works by walking the queue in reverse to find a + * transaction with a higher priority than the given transaction. + * The given transaction is then inserted after that one, since it + * would be the next lower priority. + * + * @param txn Pointer to the transaction object + * @param queue Index of queue to insert into + */ + void insertSortPrio(Transaction* txn, QueueId::T queue); + + // ---------------------------------------------------------------------- + // Queue Management + // ---------------------------------------------------------------------- + + /** + * @brief Remove a node from a channel queue + * + * @param queueidx Queue index + * @param node Node to remove + */ + inline void removeFromQueue(QueueId::T queueidx, CListNode* node); + + /** + * @brief Insert a node after another in a channel queue + * + * @param queueidx Queue index + * @param start Node to insert after + * @param after Node to insert + */ + inline void insertAfterInQueue(QueueId::T queueidx, CListNode* start, CListNode* after); + + /** + * @brief Insert a node at the back of a channel queue + * + * @param queueidx Queue index + * @param node Node to insert + */ + inline void insertBackInQueue(QueueId::T queueidx, CListNode* node); + + // ---------------------------------------------------------------------- + // Callback methods (public so wrappers can call them) + // ---------------------------------------------------------------------- + + /** + * @brief Traverse callback for cycling the first active transaction + * + * @param node List node being traversed + * @param context Callback context (CycleTxArgs*) + * @returns Traversal status (CONT or EXIT) + */ + CListTraverseStatus cycleTxFirstActive(CListNode* node, void* context); + + /** + * @brief Traverse callback for ticking a transaction + * + * @param node List node being traversed + * @param context Callback context (TickArgs*) + * @returns Traversal status (CONT or EXIT) + */ + CListTraverseStatus doTick(CListNode* node, void* context); + + private: + // ---------------------------------------------------------------------- + // Private helper methods + // ---------------------------------------------------------------------- + + /** + * @brief Step each active playback directory + * + * Check if a playback directory needs iterated, and if so does, and + * if a valid file is found initiates playback on it. + * + * @param pb The playback state + */ + void processPlaybackDirectory(Playback* pb); + + /** + * @brief Update playback/poll counted state + * + * @param pb Playback state + * @param up Whether to increment (1) or decrement (0) + * @param counter Counter to update + */ + void updatePollPbCounted(Playback* pb, int up, U8* counter); + + private: + // ---------------------------------------------------------------------- + // Member variables + // ---------------------------------------------------------------------- + + Engine* m_engine; //!< Parent CFDP engine + + CListNode* m_qs[QueueId::NUM]; //!< Transaction queues + CListNode* m_cs[DIRECTION_NUM]; //!< Command/history lists + + U32 m_numCmdTx; //!< Number of commanded TX transactions + + Playback m_playback[CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN]; //!< Playback state + CfdpPollDir m_polldir[CFDP_MAX_POLLING_DIR_PER_CHAN]; //!< Polling directory state + + const Transaction* m_currentTxn; //!< Current transaction during channel cycle + CfdpManager* m_cfdpManager; //!< Reference to F' component for parameters + + U8 m_tickType; //!< Type of tick being processed + U8 m_channelId; //!< Channel ID (index into engine array) + + Flow::T m_flowState; //!< Channel flow state (normal/frozen) + U32 m_outgoingCounter; //!< PDU throttling counter + + // Per-channel resource arrays (dynamically allocated, moved from Engine) + Transaction* m_transactions; //!< Array of CFDP_NUM_TRANSACTIONS_PER_CHANNEL + History* m_histories; //!< Array of CFDP_NUM_HISTORIES_PER_CHANNEL + CfdpChunkWrapper* m_chunks; //!< Array of CFDP_NUM_TRANSACTIONS_PER_CHANNEL * DIRECTION_NUM + Chunk* m_chunkMem; //!< Chunk memory backing store + + U32 m_dirMaxChunks[DIRECTION_NUM]; //!< Max chunks per direction (RX/TX) for this channel + + // Friend declarations for testing + friend class CfdpManagerTester; +}; + +// ---------------------------------------------------------------------- +// Inline function implementations +// ---------------------------------------------------------------------- + +inline void Channel::removeFromQueue(QueueId::T queueidx, CListNode* node) +{ + CfdpCListRemove(&m_qs[queueidx], node); +} + +inline void Channel::insertAfterInQueue(QueueId::T queueidx, CListNode* start, CListNode* after) +{ + CfdpCListInsertAfter(&m_qs[queueidx], start, after); +} + +inline void Channel::insertBackInQueue(QueueId::T queueidx, CListNode* node) +{ + CfdpCListInsertBack(&m_qs[queueidx], node); +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // CFDP_CHANNEL_HPP \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Chunk.cpp b/Svc/Ccsds/CfdpManager/Chunk.cpp new file mode 100644 index 00000000000..80b79aaafbf --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Chunk.cpp @@ -0,0 +1,360 @@ +// ====================================================================== +// \title Chunk.cpp +// \brief CFDP chunks (sparse gap tracking) logic file +// +// This file is a port of the cf_chunks.c file from the +// NASA Core Flight System (cFS) CFDP (CF) Application, +// version 3.0.0, adapted for use within the F-Prime (F') framework. +// +// This class handles the complexity of sparse gap tracking so that +// the CFDP engine doesn't need to worry about it. Information is given +// to the class and when needed calculations are made internally to +// help the engine build NAK packets. Received NAK segment requests +// are stored in this class as well and used for re-transmit processing. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include + +#include + +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ====================================================================== +// CfdpChunkList Class Implementation +// ====================================================================== + +CfdpChunkList::CfdpChunkList(ChunkIdx maxChunks, Chunk* chunkMem) + : m_count(0), m_maxChunks(maxChunks), m_chunks(chunkMem) +{ + FW_ASSERT(maxChunks > 0); + FW_ASSERT(chunkMem != nullptr); + reset(); +} + +void CfdpChunkList::reset() +{ + m_count = 0; + memset(m_chunks, 0, sizeof(*m_chunks) * m_maxChunks); +} + +void CfdpChunkList::add(FileSize offset, FileSize size) +{ + const Chunk chunk = {offset, size}; + const ChunkIdx i = findInsertPosition(&chunk); + + // PTFO: files won't be so big we need to gracefully handle overflow, + // and in that case the user should change everything in chunks + // to use 64-bit numbers + FW_ASSERT((offset + size) >= offset, offset, size); + + insert(i, &chunk); +} + +const Chunk* CfdpChunkList::getFirstChunk() const +{ + return m_count ? &m_chunks[0] : nullptr; +} + +void CfdpChunkList::removeFromFirst(FileSize size) +{ + Chunk* chunk = &m_chunks[0]; /* front is always 0 */ + + if (size > chunk->size) + { + size = chunk->size; + } + chunk->size -= size; + + if (!chunk->size) + { + eraseChunk(0); + } + else + { + chunk->offset += size; + } +} + +U32 CfdpChunkList::computeGaps(ChunkIdx maxGaps, + FileSize total, + FileSize start, + const GapComputeCallback& callback, + void* opaque) const +{ + U32 ret = 0; + ChunkIdx i = 0; + FileSize next_off; + FileSize gap_start; + Chunk chunk; + + FW_ASSERT(total); /* does it make sense to have a 0 byte file? */ + FW_ASSERT(start < total, start, total); + + /* simple case: there is no chunk data, which means there is a single gap of the entire size */ + if (!m_count) + { + chunk.offset = 0; + chunk.size = total; + if (callback) + { + callback(&chunk, opaque); + } + ret = 1; + } + else + { + /* Handle initial gap if needed */ + if (start < m_chunks[0].offset) + { + chunk.offset = start; + chunk.size = m_chunks[0].offset - start; + if (callback) + { + callback(&chunk, opaque); + } + ret = 1; + } + + while ((ret < maxGaps) && (i < m_count)) + { + next_off = (i == (m_count - 1)) ? total : m_chunks[i + 1].offset; + gap_start = (m_chunks[i].offset + m_chunks[i].size); + + chunk.offset = (gap_start > start) ? gap_start : start; + chunk.size = (next_off - chunk.offset); + + if (gap_start >= total) + { + break; + } + else if (start < next_off) + { + /* Only report if gap finishes after start */ + if (callback) + { + callback(&chunk, opaque); + } + ++ret; + } + ++i; + } + } + + return ret; +} + +void CfdpChunkList::insertChunk(ChunkIdx index, const Chunk* chunk) +{ + FW_ASSERT(m_count < m_maxChunks, m_count, m_maxChunks); + FW_ASSERT(index <= m_count, index, m_count); + + if (m_count && (index != m_count)) + { + memmove(&m_chunks[index + 1], &m_chunks[index], + sizeof(*chunk) * (m_count - index)); + } + memcpy(&m_chunks[index], chunk, sizeof(*chunk)); + + ++m_count; +} + +void CfdpChunkList::eraseChunk(ChunkIdx index) +{ + FW_ASSERT(m_count > 0); + FW_ASSERT(index < m_count, index, m_count); + + /* to erase, move memory over the old one */ + memmove(&m_chunks[index], &m_chunks[index + 1], + sizeof(*m_chunks) * (m_count - 1 - index)); + --m_count; +} + +void CfdpChunkList::eraseRange(ChunkIdx start, ChunkIdx end) +{ + /* Sanity check */ + FW_ASSERT(end <= m_count, end, m_count); + + if (start < end) + { + memmove(&m_chunks[start], &m_chunks[end], sizeof(*m_chunks) * (m_count - end)); + m_count -= static_cast(end - start); + } +} + +ChunkIdx CfdpChunkList::findInsertPosition(const Chunk* chunk) +{ + ChunkIdx first = 0; + ChunkIdx i; + ChunkIdx count = m_count; + ChunkIdx step; + + while (count > 0) + { + i = first; + step = count / 2; + i += step; + if (m_chunks[i].offset < chunk->offset) + { + first = i + 1; + count -= static_cast(step + 1); + } + else + { + count = step; + } + } + + return first; +} + +bool CfdpChunkList::combineNext(ChunkIdx i, const Chunk* chunk) +{ + ChunkIdx combined_i = i; + bool ret = false; + FileSize chunk_end = chunk->offset + chunk->size; + + /* Assert no rollover, only possible as a bug */ + FW_ASSERT(chunk_end > chunk->offset, chunk_end, chunk->offset); + + /* Determine how many can be combined */ + for (; combined_i < m_count; ++combined_i) + { + /* Advance combine index until there is a gap between end and the next offset */ + if (chunk_end < m_chunks[combined_i].offset) + { + break; + } + } + + /* If index advanced the range of chunks can be combined */ + if (i != combined_i) + { + /* End is the max of last combined chunk end or new chunk end */ + chunk_end = + CfdpChunkMax(m_chunks[combined_i - 1].offset + m_chunks[combined_i - 1].size, chunk_end); + + /* Use current slot as combined entry */ + m_chunks[i].size = chunk_end - chunk->offset; + m_chunks[i].offset = chunk->offset; + + /* Erase the rest of the combined chunks (if any) */ + eraseRange(i + 1, combined_i); + ret = true; + } + + return ret; +} + +bool CfdpChunkList::combinePrevious(ChunkIdx i, const Chunk* chunk) +{ + Chunk* prev; + FileSize prev_end; + FileSize chunk_end; + bool ret = false; + + FW_ASSERT(i <= m_maxChunks, i, m_maxChunks); + + /* Only need to check if there is a previous */ + if (i > 0) + { + chunk_end = chunk->offset + chunk->size; + prev = &m_chunks[i - 1]; + prev_end = prev->offset + prev->size; + + /* Check if start of new chunk is less than end of previous (overlaps) */ + if (chunk->offset <= prev_end) + { + /* When combining, use the bigger of the two endings */ + if (prev_end < chunk_end) + { + /* Combine with previous chunk */ + prev->size = chunk_end - prev->offset; + } + ret = true; + } + } + return ret; +} + +void CfdpChunkList::insert(ChunkIdx i, const Chunk* chunk) +{ + ChunkIdx smallest_i; + Chunk* smallest_c; + bool next = combineNext(i, chunk); + bool combined; + + if (next) + { + combined = combinePrevious(i, &m_chunks[i]); + if (combined) + { + eraseChunk(i); + } + } + else + { + combined = combinePrevious(i, chunk); + if (!combined) + { + if (m_count < m_maxChunks) + { + insertChunk(i, chunk); + } + else + { + smallest_i = findSmallestSize(); + smallest_c = &m_chunks[smallest_i]; + if (smallest_c->size < chunk->size) + { + eraseChunk(smallest_i); + insertChunk(findInsertPosition(chunk), chunk); + } + } + } + } +} + +ChunkIdx CfdpChunkList::findSmallestSize() const +{ + ChunkIdx i; + ChunkIdx smallest = 0; + + for (i = 1; i < m_count; ++i) + { + if (m_chunks[i].size < m_chunks[smallest].size) + { + smallest = i; + } + } + + return smallest; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Chunk.hpp b/Svc/Ccsds/CfdpManager/Chunk.hpp new file mode 100644 index 00000000000..534ee771a61 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Chunk.hpp @@ -0,0 +1,296 @@ +// ====================================================================== +// \title Chunk.hpp +// \brief CFDP chunks (spare gap tracking) header file +// +// This file is a port of CFDP chunk/gap tracking from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_chunks.h (CFDP chunk and gap tracking definitions) +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#ifndef CFDP_CHUNK_HPP +#define CFDP_CHUNK_HPP + +#include + +#include + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +using ChunkIdx = U16; + +/** + * @brief Pairs an offset with a size to identify a specific piece of a file + */ +struct Chunk +{ + FileSize offset; /**< \brief The start offset of the chunk within the file */ + FileSize size; /**< \brief The size of the chunk */ +}; + +/** + * @brief Selects the larger of the two passed-in offsets + * + * @param a First chunk offset + * @param b Second chunk offset + * @return the larger FileSize value + */ +static inline FileSize CfdpChunkMax(FileSize a, FileSize b) +{ + if (a > b) + { + return a; + } + else + { + return b; + } +} + +/** + * @brief Callback type for gap computation + * + * std::function-based callback used by CfdpChunkList::computeGaps(). + * The callback receives the gap chunk and an opaque context pointer. + */ +using GapComputeCallback = std::function; + +/** + * @brief C++ class encapsulation of CFDP chunk list operations + * + * This class provides modern C++ encapsulation around the gap tracking functionality + * previously implemented as C-style free functions. The class does not own the backing + * memory for chunks; it takes a pointer to pre-allocated memory in the constructor, + * preserving the existing memory pooling system managed by Channel. + * + * The chunk list maintains file segments in offset-sorted order and provides operations + * for adding segments, computing gaps, and managing the list. This is primarily used for: + * - RX transactions: Track received file data segments to identify gaps for NAK packets + * - TX transactions: Track NAK segment requests for retransmission + */ +class CfdpChunkList { + public: + // ---------------------------------------------------------------------- + // Construction and Destruction + // ---------------------------------------------------------------------- + + /** + * @brief Constructor - initializes chunk list with pre-allocated memory + * + * @param maxChunks Maximum number of chunks this list can hold + * @param chunkMem Pointer to pre-allocated array of Chunk objects + * + * @note The class does NOT take ownership of chunkMem; memory is externally managed + */ + CfdpChunkList(ChunkIdx maxChunks, Chunk* chunkMem); + + // ---------------------------------------------------------------------- + // Public Interface + // ---------------------------------------------------------------------- + + /** + * @brief Add a chunk (file segment) to the list + * + * Adds a new chunk representing a file segment at the given offset and size. + * The chunk may be combined with adjacent chunks if they are contiguous. + * If the list is full, the smallest chunk may be evicted. + * + * @param offset Starting offset of the chunk within the file + * @param size Size of the chunk in bytes + */ + void add(FileSize offset, FileSize size); + + /** + * @brief Reset the chunk list to empty state + * + * Removes all chunks from the list while preserving the max_chunks and + * memory pointer configuration. After reset, the list is in the same state + * as immediately after construction. + */ + void reset(); + + /** + * @brief Get the first chunk in the list + * + * Returns a pointer to the first chunk (lowest offset) in the list without + * removing it from the list. + * + * @returns Pointer to the first chunk + * @retval nullptr if the list is empty + */ + const Chunk* getFirstChunk() const; + + /** + * @brief Remove a specified size from the first chunk + * + * Reduces the size of the first chunk by the specified amount. If the + * size exactly matches the chunk size, the entire chunk is removed. + * This is used for consuming data in-order during processing. + * + * @param size Number of bytes to remove from the first chunk + * + * @note The list must not be empty when calling this function + */ + void removeFromFirst(FileSize size); + + /** + * @brief Compute gaps between chunks and invoke callback for each + * + * Walks the chunk list and computes gaps (missing file segments) between + * chunks. For each gap found, invokes the provided callback function. + * This is used to generate NAK segment requests. + * + * @param maxGaps Maximum number of gaps to compute + * @param total Total size of the file + * @param start Starting offset for gap computation + * @param callback Callback function to invoke for each gap + * @param opaque Opaque pointer passed through to callback + * + * @returns Number of gaps computed (may be less than maxGaps if fewer gaps exist) + */ + U32 computeGaps(ChunkIdx maxGaps, + FileSize total, + FileSize start, + const GapComputeCallback& callback, + void* opaque) const; + + /** + * @brief Get the current number of chunks in the list + * @returns Current chunk count + */ + ChunkIdx getCount() const { return m_count; } + + /** + * @brief Get the maximum number of chunks this list can hold + * @returns Maximum chunk capacity + */ + ChunkIdx getMaxChunks() const { return m_maxChunks; } + + private: + // ---------------------------------------------------------------------- + // Private Implementation + // ---------------------------------------------------------------------- + + /** + * @brief Insert a chunk at a specific index + * + * Inserts the chunk at the specified index position, shifting existing + * chunks as needed. May combine with adjacent chunks if contiguous. + * + * @param index Index position to insert at + * @param chunk Chunk data to insert + */ + void insertChunk(ChunkIdx index, const Chunk* chunk); + + /** + * @brief Erase a single chunk at the given index + * + * Removes the chunk and shifts subsequent chunks to close the gap. + * + * @param index Index of chunk to erase + */ + void eraseChunk(ChunkIdx index); + + /** + * @brief Erase a range of chunks + * + * Removes chunks from start (inclusive) to end (exclusive) and shifts + * remaining chunks to close the gap. + * + * @param start Starting index (inclusive) + * @param end Ending index (exclusive) + */ + void eraseRange(ChunkIdx start, ChunkIdx end); + + /** + * @brief Find where a chunk should be inserted to maintain sorted order + * + * Uses binary search to find the insertion point based on chunk offset. + * + * @param chunk Chunk data to find insertion point for + * @returns Index where chunk should be inserted + */ + ChunkIdx findInsertPosition(const Chunk* chunk); + + /** + * @brief Attempt to combine chunk with the next chunk + * + * If the chunk is contiguous with the next chunk in the list, combines them. + * + * @param i Index of the current chunk + * @param chunk Chunk data to attempt combining + * @returns true if chunks were combined, false otherwise + */ + bool combineNext(ChunkIdx i, const Chunk* chunk); + + /** + * @brief Attempt to combine chunk with the previous chunk + * + * If the chunk is contiguous with the previous chunk in the list, combines them. + * + * @param i Index of the current chunk + * @param chunk Chunk data to attempt combining + * @returns true if chunks were combined, false otherwise + */ + bool combinePrevious(ChunkIdx i, const Chunk* chunk); + + /** + * @brief Insert a chunk, potentially combining with neighbors + * + * Inserts the chunk at position i and attempts to combine with adjacent chunks. + * + * @param i Position to insert at + * @param chunk Chunk data to insert + */ + void insert(ChunkIdx i, const Chunk* chunk); + + /** + * @brief Find the index of the chunk with the smallest size + * + * Used when the list is full and a chunk needs to be evicted. + * + * @returns Index of the smallest chunk, or 0 if list is empty + */ + ChunkIdx findSmallestSize() const; + + private: + // ---------------------------------------------------------------------- + // Private Member Variables + // ---------------------------------------------------------------------- + + ChunkIdx m_count; //!< Current number of chunks in the list + ChunkIdx m_maxChunks; //!< Maximum number of chunks allowed + Chunk* m_chunks; //!< Pointer to pre-allocated chunk array (not owned) +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif /* !CFDP_CHUNK_HPP */ diff --git a/Svc/Ccsds/CfdpManager/Clist.cpp b/Svc/Ccsds/CfdpManager/Clist.cpp new file mode 100644 index 00000000000..8535baeec81 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Clist.cpp @@ -0,0 +1,265 @@ +// ====================================================================== +// \title Clist.cpp +// \brief CFDP circular list definition source file +// +// This file is a port of the cf_clist.c file from the +// NASA Core Flight System (cFS) CFDP (CF) Application, +// version 3.0.0, adapted for use within the F-Prime (F') framework. +// +// This is a circular doubly-linked list implementation. It is used for +// multiple data structures in CFDP. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include + +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void CfdpCListInitNode(CListNode *node) +{ + node->next = node; + node->prev = node; +} + +void CfdpCListInsertFront(CListNode **head, CListNode *node) +{ + CListNode *last; + + FW_ASSERT(head); + FW_ASSERT(node); + FW_ASSERT(node->next == node); + FW_ASSERT(node->prev == node); + + if (*head) + { + last = (*head)->prev; + + node->next = *head; + node->prev = last; + + last->next = node; + (*head)->prev = node; + } + + *head = node; +} + +void CfdpCListInsertBack(CListNode **head, CListNode *node) +{ + CListNode *last; + + FW_ASSERT(head); + FW_ASSERT(node); + FW_ASSERT(node->next == node); + FW_ASSERT(node->prev == node); + + if (!*head) + { + *head = node; + } + else + { + last = (*head)->prev; + + node->next = *head; + (*head)->prev = node; + node->prev = last; + last->next = node; + } +} + +CListNode *CfdpCListPop(CListNode **head) +{ + CListNode *ret; + + FW_ASSERT(head); + + ret = *head; + if (ret) + { + CfdpCListRemove(head, ret); + } + + return ret; +} + +void CfdpCListRemove(CListNode **head, CListNode *node) +{ + FW_ASSERT(head); + FW_ASSERT(node); + FW_ASSERT(*head); + + if (node->next == node) + { + /* only node in the list, so this one is easy */ + FW_ASSERT(node == *head); /* sanity check */ + *head = NULL; + } + else if (*head == node) + { + /* removing the first node in the list, so make the second node in the list the first */ + (*head)->prev->next = node->next; + *head = node->next; + + (*head)->prev = node->prev; + } + else + { + node->next->prev = node->prev; + node->prev->next = node->next; + } + + CfdpCListInitNode(node); +} + +void CfdpCListInsertAfter(CListNode **head, CListNode *start, CListNode *after) +{ + /* calling insert_after with nothing to insert after (no head) makes no sense */ + FW_ASSERT(head); + FW_ASSERT(*head); + FW_ASSERT(start); + FW_ASSERT(start != after); + + /* knowing that head is not empty, and knowing that start is non-zero, this is an easy operation */ + after->next = start->next; + start->next = after; + after->prev = start; + after->next->prev = after; +} + +void CfdpCListTraverse(CListNode *start, CListFunc fn, void *context) +{ + CListNode *node = start; + CListNode *node_next; + bool last = false; + + if (node) + { + do + { + /* set node_next in case callback removes this node from the list */ + node_next = node->next; + if (node_next == start) + { + last = true; + } + if (!CfdpCListTraverseStatusIsContinue(fn(node, context))) + { + break; + } + /* list traversal is robust against an item deleting itself during traversal, + * but there is a special case if that item is the starting node. Since this is + * a circular list, start is remembered so we know when to stop. Must set start + * to the next node in this case. */ + if ((start == node) && (node->next != node_next)) + { + start = node_next; + } + node = node_next; + } + while (!last); + } +} + +void CfdpCListTraverse(CListNode *start, const CListTraverseCallback& callback, void *context) +{ + CListNode *node = start; + CListNode *node_next; + bool last = false; + + if (node) + { + do + { + /* set node_next in case callback removes this node from the list */ + node_next = node->next; + if (node_next == start) + { + last = true; + } + if (!CfdpCListTraverseStatusIsContinue(callback(node, context))) + { + break; + } + /* list traversal is robust against an item deleting itself during traversal, + * but there is a special case if that item is the starting node. Since this is + * a circular list, start is remembered so we know when to stop. Must set start + * to the next node in this case. */ + if ((start == node) && (node->next != node_next)) + { + start = node_next; + } + node = node_next; + } + while (!last); + } +} + +void CfdpCListTraverseR(CListNode *end, CListFunc fn, void *context) +{ + if (end) + { + CListNode *node = end->prev; + CListNode *node_next; + bool last = false; + + if (node) + { + end = node; + + do + { + /* set node_next in case callback removes this node from the list */ + node_next = node->prev; + if (node_next == end) + { + last = true; + } + + if (!CfdpCListTraverseStatusIsContinue(fn(node, context))) + { + break; + } + + /* list traversal is robust against an item deleting itself during traversal, + * but there is a special case if that item is the starting node. Since this is + * a circular list, "end" is remembered so we know when to stop. Must set "end" + * to the next node in this case. */ + if ((end == node) && (node->prev != node_next)) + { + end = node_next; + } + node = node_next; + } + while (!last); + } + } +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Clist.hpp b/Svc/Ccsds/CfdpManager/Clist.hpp new file mode 100644 index 00000000000..8a14b37591e --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Clist.hpp @@ -0,0 +1,200 @@ +// ====================================================================== +// \title Clist.hpp +// \brief CFDP circular list header file +// +// This file is a port of CFDP circular list from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_clist.h (CFDP circular list data structure definitions) +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#ifndef CFDP_CLIST_HPP +#define CFDP_CLIST_HPP + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +/** + * @brief Traverse status for circular list operations + */ +enum CListTraverseStatus : U8 +{ + CLIST_TRAVERSE_CONTINUE = 0, /**< \brief Continue traversing the list */ + CLIST_TRAVERSE_EXIT = 1 /**< \brief Stop traversing the list */ +}; + +/** \brief Constant indicating to continue traversal */ +constexpr U8 CFDP_CLIST_CONT = CLIST_TRAVERSE_CONTINUE; + +/** \brief Constant indicating to stop traversal */ +constexpr U8 CFDP_CLIST_EXIT = CLIST_TRAVERSE_EXIT; + +/** + * Checks if the list traversal should continue + */ +static inline bool CfdpCListTraverseStatusIsContinue(CListTraverseStatus stat) +{ + return (stat == CLIST_TRAVERSE_CONTINUE); +} + +/** + * @brief Circular linked list node structure + */ +struct CListNode +{ + struct CListNode *next; /**< \brief Pointer to next node */ + struct CListNode *prev; /**< \brief Pointer to previous node */ +}; + +/** + * @brief Obtains a pointer to the parent structure + * + * Given a pointer to a CListNode object which is known to be a member of a + * larger container, this converts the pointer to that of the parent. + */ +template +constexpr Container* container_of_cpp(Member* member_ptr, + Member Container::*member) +{ + return reinterpret_cast( + reinterpret_cast(member_ptr) - reinterpret_cast(&(reinterpret_cast(0)->*member)) + ); +} + + +/** + * @brief Callback function type for use with CfdpCListTraverse() + * + * @param node Current node being traversed + * @param context Opaque pointer passed through from initial call + * + * @returns integer status code indicating whether to continue traversal + * @retval #CFDP_CLIST_CONT Indicates to continue traversing the list + * @retval #CFDP_CLIST_EXIT Indicates to stop traversing the list + */ +using CListFunc = CListTraverseStatus(*)(CListNode*, void*); + +/** + * @brief Modern callback type for list traversal + * + * Replaces CListFunc with a more flexible std::function-based callback. + * The callback receives the node and an opaque context pointer. + */ +using CListTraverseCallback = std::function; + +/************************************************************************/ +/** @brief Initialize a clist node. + * + * @param node Pointer to node structure to be initialized + */ +void CfdpCListInitNode(CListNode *node); + +/************************************************************************/ +/** @brief Insert the given node into the front of a list. + * + * @param head Pointer to head of list to insert into + * @param node Pointer to node to insert + */ +void CfdpCListInsertFront(CListNode **head, CListNode *node); + +/************************************************************************/ +/** @brief Insert the given node into the back of a list. + * + * @param head Pointer to head of list to insert into + * @param node Pointer to node to insert + */ +void CfdpCListInsertBack(CListNode **head, CListNode *node); + +/************************************************************************/ +/** @brief Remove the given node from the list. + * + * @param head Pointer to head of list to remove from + * @param node Pointer to node to remove + */ +void CfdpCListRemove(CListNode **head, CListNode *node); + +/************************************************************************/ +/** @brief Remove the first node from a list and return it. + * + * @param head Pointer to head of list to remove from + * + * @returns The first node (now removed) in the list + * @retval NULL if list was empty. + */ +CListNode *CfdpCListPop(CListNode **head); + +/************************************************************************/ +/** @brief Insert the given node into the last after the given start node. + * + * @param head Pointer to head of list to remove from + * @param start Pointer to node to insert + * @param after Pointer to position to insert after + */ +void CfdpCListInsertAfter(CListNode **head, CListNode *start, CListNode *after); + +/************************************************************************/ +/** @brief Traverse the entire list, calling the given function on all nodes. + * + * @note on traversal it's ok to delete the current node, but do not delete + * other nodes in the same list!! + * + * @param start List to traverse (first node) + * @param fn Callback function to invoke for each node + * @param context Opaque pointer to pass to callback + */ +void CfdpCListTraverse(CListNode *start, CListFunc fn, void *context); + +/************************************************************************/ +/** @brief Traverse the entire list, calling the given function on all nodes (modern C++ version). + * + * @note on traversal it's ok to delete the current node, but do not delete + * other nodes in the same list!! + * + * @param start List to traverse (first node) + * @param callback Callback function to invoke for each node + * @param context Opaque pointer to pass to callback + */ +void CfdpCListTraverse(CListNode *start, const CListTraverseCallback& callback, void *context); + +/************************************************************************/ +/** @brief Reverse list traversal, starting from end, calling given function on all nodes. + * + * @note traverse_R will work backwards from the parameter's prev, and end on param + * + * @param end List to traverse (last node) + * @param fn Callback function to invoke for each node + * @param context Opaque pointer to pass to callback + */ +void CfdpCListTraverseR(CListNode *end, CListFunc fn, void *context); + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif /* !CFDP_CLIST_HPP */ diff --git a/Svc/Ccsds/CfdpManager/Commands.fppi b/Svc/Ccsds/CfdpManager/Commands.fppi new file mode 100644 index 00000000000..65f7c24b6f8 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Commands.fppi @@ -0,0 +1,72 @@ +@ Command to start a CFDP file transaction +async command SendFile( + channelId: U8 @< Channel ID for the file transaction + destId: Cfdp.EntityId @< Destination entity id + cfdpClass: Cfdp.Class @< CFDP class for the file transfer + keep: Cfdp.Keep @< Whether or not to keep or delete the file upon completion + $priority: U8 @< Priority: 0=highest priority + sourceFileName: string size MaxFilePathSize @< The name of the on-board file to send + destFileName: string size MaxFilePathSize @< The name of the destination file on the ground +) + +@ Command to start a directory playback +async command PlaybackDirectory( + channelId: U8 @< Channel ID for the file transaction(s) + destId: Cfdp.EntityId @< Destination entity id + cfdpClass: Cfdp.Class @< CFDP class for the file transfer(s) + keep: Cfdp.Keep @< Whether or not to keep or delete the file(s) upon completion + $priority: U8 @< Priority: 0=highest priority + sourceDirectory: string size MaxFilePathSize @< The name of the on-board directory to send + destDirectory: string size MaxFilePathSize @< The name of the destination directory on the ground +) + +@ Command to start a directory poll +async command PollDirectory( + channelId: U8 @< Channel ID for the file transaction(s) + pollId: U8 @< Channel poll ID for the file transaction(s) + destId: Cfdp.EntityId @< Destination entity id + cfdpClass: Cfdp.Class @< CFDP class for the file transfer(s) + $priority: U8 @< Priority: 0=highest priority + interval: U32 @< Interval to poll the directory in seconds + sourceDirectory: string size MaxFilePathSize @< The name of the on-board directory to send + destDirectory: string size MaxFilePathSize @< The name of the destination directory on the ground +) + +@ Command to stop a directory poll +async command StopPollDirectory( + channelId: U8 @< Channel ID to stop + pollId: U8 @< Channel poll ID to stop +) + +@ Command to set channel's flow status +async command SetChannelFlow( + channelId: U8 @< Channel ID to set + freeze: Cfdp.Flow @< Flow state to set +) + +@ Command to suspend or resume a transaction +async command SuspendResumeTransaction( + channelId: U8 @< Channel ID for the transaction + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID of the transaction + $action: Cfdp.SuspendResume @< Action to take: SUSPEND or RESUME +) + +@ Command to cancel a transaction with graceful close-out +async command CancelTransaction( + channelId: U8 @< Channel ID for the transaction + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID of the transaction +) + +@ Command to abandon a transaction immediately +async command AbandonTransaction( + channelId: U8 @< Channel ID for the transaction + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID of the transaction +) + +@ Command to reset telemetry counters +async command ResetCounters( + channelId: U8 @< Channel ID to reset (0xFF for all channels) +) \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Engine.cpp b/Svc/Ccsds/CfdpManager/Engine.cpp new file mode 100644 index 00000000000..95234ebb0a9 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Engine.cpp @@ -0,0 +1,1348 @@ +// ====================================================================== +// \title Engine.cpp +// \brief CFDP Engine implementation +// +// This file is a port of CFDP engine operations from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp.c (CFDP PDU validation, processing, and engine operations) +// +// This file contains two sets of functions. The first is what is needed +// to deal with CFDP PDUs. Specifically validating them for correctness +// and ensuring the byte-order is correct for the target. The second +// is incoming and outgoing CFDP PDUs pass through here. All receive +// CFDP PDU logic is performed here and the data is passed to the +// R (rx) and S (tx) logic. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ---------------------------------------------------------------------- +// Construction and destruction +// ---------------------------------------------------------------------- + +Engine::Engine(CfdpManager* manager) : + m_manager(manager), + m_seqNum(0) +{ + for (U8 i = 0; i < Cfdp::NumChannels; ++i) + { + m_channels[i] = nullptr; + } +} + +Engine::~Engine() +{ + for (U8 i = 0; i < Cfdp::NumChannels; ++i) + { + if (m_channels[i] != nullptr) + { + delete m_channels[i]; + m_channels[i] = nullptr; + } + } +} + +// ---------------------------------------------------------------------- +// Public interface methods +// ---------------------------------------------------------------------- + +void Engine::init() +{ + // Create all channels + for (U8 i = 0; i < Cfdp::NumChannels; ++i) + { + m_channels[i] = new Channel(this, i, this->m_manager); + FW_ASSERT(m_channels[i] != nullptr); + } +} + + +void Engine::armAckTimer(Transaction *txn) +{ + txn->m_ack_timer.setTimer(txn->m_cfdpManager->getAckTimerParam(txn->m_chan_num)); + txn->m_flags.com.ack_timer_armed = true; +} + + +void Engine::armInactTimer(Transaction *txn) +{ + U32 timerDuration = 0; + + // select timeout based on the state + if (GetTxnStatus(txn) == ACK_TXN_STATUS_ACTIVE) + { + // in an active transaction, we expect traffic so use the normal inactivity timer + timerDuration = txn->m_cfdpManager->getInactivityTimerParam(txn->m_chan_num); + } + else + { + // in an inactive transaction, we do NOT expect traffic, and this timer is now used + // just in case any late straggler PDUs dp get delivered. In this case the + // time should be longer than the retransmit time (ack timer) but less than the full + // inactivity timer (because again, we are not expecting traffic, so waiting the full + // timeout would hold resources longer than needed). Using double the ack timer should + // ensure that if the remote retransmitted anything, we will see it, and avoids adding + // another config option just for this. + timerDuration = txn->m_cfdpManager->getAckTimerParam(txn->m_chan_num) * 2; + } + + txn->m_inactivity_timer.setTimer(timerDuration); +} + +void Engine::dispatchRecv(Transaction *txn, const Fw::Buffer& buffer) +{ + // Dispatch based on transaction state + switch (txn->m_state) + { + case TXN_STATE_INIT: + this->recvInit(txn, buffer); + break; + case TXN_STATE_R1: + txn->r1Recv(buffer); + break; + case TXN_STATE_S1: + txn->s1Recv(buffer); + break; + case TXN_STATE_R2: + txn->r2Recv(buffer); + break; + case TXN_STATE_S2: + txn->s2Recv(buffer); + break; + case TXN_STATE_DROP: + this->recvDrop(txn, buffer); + break; + case TXN_STATE_HOLD: + this->recvHold(txn, buffer); + break; + default: + // Invalid or undefined state + break; + } + + this->armInactTimer(txn); // whenever a packet was received by the other size, always arm its inactivity timer +} + +void Engine::dispatchTx(Transaction *txn) +{ + static const TxnSendDispatchTable state_fns = { + { + nullptr, // TXN_STATE_UNDEF + nullptr, // TXN_STATE_INIT + nullptr, // TXN_STATE_R1 + &Transaction::s1Tx, // TXN_STATE_S1 + nullptr, // TXN_STATE_R2 + &Transaction::s2Tx, // TXN_STATE_S2 + nullptr, // TXN_STATE_DROP + nullptr // TXN_STATE_HOLD + } + }; + + txn->txStateDispatch(&state_fns); +} + +Status::T Engine::sendMd(Transaction *txn) +{ + Fw::Buffer buffer; + Status::T status = Cfdp::Status::SUCCESS; + + FW_ASSERT((txn->m_state == TXN_STATE_S1) || (txn->m_state == TXN_STATE_S2), txn->m_state); + FW_ASSERT(txn->m_chan != NULL); + + // Create and initialize Metadata PDU + MetadataPdu md; + + // Set closure requested flag based on transaction class + // Class 1: closure not requested (0), Class 2: closure requested (1) + U8 closureRequested = (txn->m_state == TXN_STATE_S2) ? 1 : 0; + + // Direction is toward receiver for metadata PDU sent by sender + Cfdp::PduDirection direction = DIRECTION_TOWARD_RECEIVER; + + md.initialize( + direction, + txn->getClass(), // transmission mode (Class 1 or 2) + m_manager->getLocalEidParam(), // source EID + txn->m_history->seq_num, // transaction sequence number + txn->m_history->peer_eid, // destination EID + txn->m_fsize, // file size + txn->m_history->fnames.src_filename, // source filename + txn->m_history->fnames.dst_filename, // destination filename + CHECKSUM_TYPE_MODULAR, // checksum type + closureRequested // closure requested flag + ); + + // Allocate buffer + status = m_manager->getPduBuffer(buffer, *txn->m_chan, md.getBufferSize()); + if (status == Cfdp::Status::SUCCESS) { + // Serialize to buffer + Fw::SerialBuffer sb(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus serStatus = md.serializeTo(sb); + if (serStatus != Fw::FW_SERIALIZE_OK) { + // Failed to serialize, return the buffer + m_manager->log_WARNING_LO_FailMetadataPduSerialization(txn->getChannelId(), static_cast(serStatus)); + m_manager->returnPduBuffer(*txn->m_chan, buffer); + status = Cfdp::Status::ERROR; + } else { + // Update buffer size to actual serialized size + buffer.setSize(sb.getSize()); + // Send the PDU + m_manager->sendPduBuffer(*txn->m_chan, buffer); + // Increment sent PDU counter + m_manager->incrementSentPdu(txn->getChannelId()); + } + } + + return status; +} + +Status::T Engine::sendFd(Transaction *txn, FileDataPdu& fdPdu) +{ + Fw::Buffer buffer; + Status::T status = Cfdp::Status::SUCCESS; + + status = m_manager->getPduBuffer(buffer, *txn->m_chan, fdPdu.getBufferSize()); + if (status == Cfdp::Status::SUCCESS) { + // Serialize to buffer + Fw::SerialBuffer sb(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus serStatus = fdPdu.serializeTo(sb); + if (serStatus != Fw::FW_SERIALIZE_OK) { + m_manager->log_WARNING_LO_FailFileDataPduSerialization(txn->getChannelId(), static_cast(serStatus)); + m_manager->returnPduBuffer(*txn->m_chan, buffer); + status = Cfdp::Status::ERROR; + } else { + // Update buffer size to actual serialized size + buffer.setSize(sb.getSize()); + m_manager->sendPduBuffer(*txn->m_chan, buffer); + // Increment sent PDU counter and file data bytes + m_manager->incrementSentPdu(txn->getChannelId()); + m_manager->addSentFileDataBytes(txn->getChannelId(), fdPdu.getDataSize()); + } + } + + return status; +} + +Status::T Engine::sendEof(Transaction *txn) +{ + Fw::Buffer buffer; + Status::T status = Cfdp::Status::SUCCESS; + + // Create and initialize EOF PDU + EofPdu eof; + + // Direction is toward receiver for EOF sent by sender + Cfdp::PduDirection direction = DIRECTION_TOWARD_RECEIVER; + ConditionCode conditionCode = static_cast(TxnStatusToConditionCode(txn->m_history->txn_stat)); + + eof.initialize( + direction, + txn->getClass(), // transmission mode + m_manager->getLocalEidParam(), // source EID + txn->m_history->seq_num, // transaction sequence number + txn->m_history->peer_eid, // destination EID + conditionCode, // condition code + txn->m_crc.getValue(), // checksum + txn->m_fsize // file size + ); + + // Add entity ID TLV on error conditions (optional per CCSDS spec) + if (conditionCode != CONDITION_CODE_NO_ERROR) { + Cfdp::Tlv tlv; + tlv.initialize(m_manager->getLocalEidParam()); // Local entity ID + eof.appendTlv(tlv); + } + + // Allocate buffer + status = m_manager->getPduBuffer(buffer, *txn->m_chan, eof.getBufferSize()); + if (status == Cfdp::Status::SUCCESS) { + // Serialize to buffer + Fw::SerialBuffer sb(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus serStatus = eof.serializeTo(sb); + if (serStatus != Fw::FW_SERIALIZE_OK) { + // Failed to serialize, return the buffer + m_manager->log_WARNING_LO_FailEofPduSerialization(txn->getChannelId(), static_cast(serStatus)); + m_manager->returnPduBuffer(*txn->m_chan, buffer); + status = Cfdp::Status::ERROR; + } else { + // Update buffer size to actual serialized size + buffer.setSize(sb.getSize()); + // Send the PDU + m_manager->sendPduBuffer(*txn->m_chan, buffer); + // Increment sent PDU counter + m_manager->incrementSentPdu(txn->getChannelId()); + } + } + + return status; +} + +Status::T Engine::sendAck(Transaction *txn, AckTxnStatus ts, FileDirective dir_code, + ConditionCode cc, EntityId peer_eid, TransactionSeq tsn) +{ + Fw::Buffer buffer; + Status::T status = Cfdp::Status::SUCCESS; + + FW_ASSERT((dir_code == FILE_DIRECTIVE_END_OF_FILE) || (dir_code == FILE_DIRECTIVE_FIN), dir_code); + + // Determine source and destination EIDs based on transaction direction + EntityId src_eid; + EntityId dst_eid; + if (txn->getHistory()->dir == DIRECTION_TX) + { + src_eid = m_manager->getLocalEidParam(); + dst_eid = peer_eid; + } + else + { + src_eid = peer_eid; + dst_eid = m_manager->getLocalEidParam(); + } + + // Create and initialize ACK PDU + AckPdu ack; + + // Direction: toward sender for EOF ACK, toward receiver for FIN ACK + Cfdp::PduDirection direction = (dir_code == FILE_DIRECTIVE_END_OF_FILE) ? + Cfdp::DIRECTION_TOWARD_SENDER : Cfdp::DIRECTION_TOWARD_RECEIVER; + + ack.initialize( + direction, + txn->getClass(), // transmission mode + src_eid, // source EID + tsn, // transaction sequence number + dst_eid, // destination EID + dir_code, // directive being acknowledged + 1, // directive subtype code (always 1) + cc, // condition code + ts // transaction status + ); + + // Allocate buffer + status = m_manager->getPduBuffer(buffer, *txn->m_chan, ack.getBufferSize()); + if (status == Cfdp::Status::SUCCESS) { + // Serialize to buffer + Fw::SerialBuffer sb(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus serStatus = ack.serializeTo(sb); + if (serStatus != Fw::FW_SERIALIZE_OK) { + // Failed to serialize, return the buffer + m_manager->log_WARNING_LO_FailAckPduSerialization(txn->getChannelId(), static_cast(serStatus)); + m_manager->returnPduBuffer(*txn->m_chan, buffer); + status = Cfdp::Status::ERROR; + } else { + // Update buffer size to actual serialized size + buffer.setSize(sb.getSize()); + // Send the PDU + m_manager->sendPduBuffer(*txn->m_chan, buffer); + // Increment sent PDU counter + m_manager->incrementSentPdu(txn->getChannelId()); + } + } + + return status; +} + +Status::T Engine::sendFin(Transaction *txn, FinDeliveryCode dc, FinFileStatus fs, + ConditionCode cc) +{ + Fw::Buffer buffer; + Status::T status = Cfdp::Status::SUCCESS; + + // Create and initialize FIN PDU + FinPdu fin; + + // Direction is toward sender for FIN sent by receiver + Cfdp::PduDirection direction = DIRECTION_TOWARD_SENDER; + + fin.initialize( + direction, + txn->getClass(), // transmission mode + txn->m_history->peer_eid, // source EID (receiver) + txn->m_history->seq_num, // transaction sequence number + m_manager->getLocalEidParam(), // destination EID (sender) + cc, // condition code + static_cast(dc), // delivery code + static_cast(fs) // file status + ); + + // Add entity ID TLV on error conditions (optional per CCSDS spec) + if (cc != CONDITION_CODE_NO_ERROR) { + Cfdp::Tlv tlv; + tlv.initialize(m_manager->getLocalEidParam()); // Local entity ID + fin.appendTlv(tlv); + } + + // Allocate buffer + status = m_manager->getPduBuffer(buffer, *txn->m_chan, fin.getBufferSize()); + if (status == Cfdp::Status::SUCCESS) { + // Serialize to buffer + Fw::SerialBuffer sb(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus serStatus = fin.serializeTo(sb); + if (serStatus != Fw::FW_SERIALIZE_OK) { + // Failed to serialize, return the buffer + m_manager->log_WARNING_LO_FailFinPduSerialization(txn->getChannelId(), static_cast(serStatus)); + m_manager->returnPduBuffer(*txn->m_chan, buffer); + status = Cfdp::Status::ERROR; + } else { + // Update buffer size to actual serialized size + buffer.setSize(sb.getSize()); + // Send the PDU + m_manager->sendPduBuffer(*txn->m_chan, buffer); + // Increment sent PDU counter + m_manager->incrementSentPdu(txn->getChannelId()); + } + } + + return status; +} + +Status::T Engine::sendNak(Transaction *txn, NakPdu& nakPdu) +{ + Fw::Buffer buffer; + Status::T status = Cfdp::Status::SUCCESS; + + // Verify this is a Class 2 transaction (NAK only used in Class 2) + Class::T tx_class = txn->getClass(); + FW_ASSERT(tx_class == Cfdp::Class::CLASS_2, tx_class); + + status = m_manager->getPduBuffer(buffer, *txn->m_chan, nakPdu.getBufferSize()); + if (status == Cfdp::Status::SUCCESS) { + // Serialize to buffer + Fw::SerialBuffer sb(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus serStatus = nakPdu.serializeTo(sb); + if (serStatus != Fw::FW_SERIALIZE_OK) { + m_manager->log_WARNING_LO_FailNakPduSerialization(txn->getChannelId(), static_cast(serStatus)); + m_manager->returnPduBuffer(*txn->m_chan, buffer); + status = Cfdp::Status::ERROR; + } else { + // Update buffer size to actual serialized size + buffer.setSize(sb.getSize()); + m_manager->sendPduBuffer(*txn->m_chan, buffer); + // Increment sent PDU counter + m_manager->incrementSentPdu(txn->getChannelId()); + } + } + + return status; +} + +void Engine::recvMd(Transaction *txn, const MetadataPdu& md) +{ + /* store the expected file size in transaction */ + txn->m_fsize = md.getFileSize(); + + /* store the filenames in transaction - validation already done during deserialization */ + txn->m_history->fnames.src_filename = md.getSourceFilename(); + txn->m_history->fnames.dst_filename = md.getDestFilename(); + + this->m_manager->log_ACTIVITY_LO_MetadataReceived( + txn->m_history->fnames.src_filename, + txn->m_history->fnames.dst_filename); +} + +Status::T Engine::recvFd(Transaction *txn, const FileDataPdu& fd) +{ + Status::T ret = Cfdp::Status::SUCCESS; + + // Extract header + const Cfdp::PduHeader& header = fd.asHeader(); + + // Check for segment metadata flag (not currently supported) + if (header.hasSegmentMetadata()) + { + /* If recv PDU has the "segment_meta_flag" set, this is not currently handled in CF. */ + this->m_manager->log_WARNING_HI_FileDataSegmentMetadata(); + this->setTxnStatus(txn, TXN_STATUS_PROTOCOL_ERROR); + this->m_manager->incrementRecvErrors(txn->getChannelId()); + ret = Cfdp::Status::ERROR; + } + + return ret; +} + +Status::T Engine::recvEof(Transaction *txn, const EofPdu& eofPdu) +{ + // EOF PDU has been validated during fromBuffer() + + // Process TLVs if present + const Cfdp::TlvList& tlvList = eofPdu.getTlvList(); + for (U8 i = 0; i < tlvList.getNumTlv(); i++) { + const Cfdp::Tlv& tlv = tlvList.getTlv(i); + if (tlv.getType() == Cfdp::TLV_TYPE_ENTITY_ID) { + // Entity ID TLV present - could validate entity ID matches expected + // Future enhancement: Add validation or logging + // TODO BPC: What does GSW what to do with these if anything + } + // Other TLV types can be processed here in the future + } + + return Cfdp::Status::SUCCESS; +} + +Status::T Engine::recvFin(Transaction *txn, const FinPdu& finPdu) +{ + // FIN PDU has been validated during fromBuffer() + + // Process TLVs if present + const Cfdp::TlvList& tlvList = finPdu.getTlvList(); + for (U8 i = 0; i < tlvList.getNumTlv(); i++) { + const Cfdp::Tlv& tlv = tlvList.getTlv(i); + if (tlv.getType() == Cfdp::TLV_TYPE_ENTITY_ID) { + // Entity ID TLV present - could validate entity ID matches expected + // Future enhancement: Add validation or logging + // TODO BPC: What does GSW what to do with these if anything + } + // Other TLV types can be processed here in the future + } + + return Cfdp::Status::SUCCESS; +} + +Status::T Engine::recvNak(Transaction *txn, const NakPdu& pdu) +{ + // NAK PDU has been validated during fromBuffer() + return Cfdp::Status::SUCCESS; +} + +void Engine::recvDrop(Transaction *txn, const Fw::Buffer& buffer) +{ + this->m_manager->incrementRecvDropped(txn->getChannelId()); + (void)buffer; // Unused - we're just dropping the PDU +} + +void Engine::recvHold(Transaction *txn, const Fw::Buffer& buffer) +{ + // anything received in this state is considered spurious + this->m_manager->incrementRecvSpurious(txn->getChannelId()); + + // + // Normally we do not expect PDUs for a transaction in holdover, because + // from the local point of view it is completed and done. But the reason + // for the holdover is because the remote side might not have gotten all + // the acks and could still be [re-]sending us PDUs for anything it does + // not know we got already. + // + // If an R2 sent FIN, it's possible that the peer missed the + // FIN-ACK and is sending another FIN. In that case we need to send + // another ACK. + // + + // currently the only thing we will re-ack is the FIN. + + // Use peekPduType to determine the PDU type + Cfdp::PduTypeEnum pduType = Cfdp::peekPduType(buffer); + + // Check if this is a FIN PDU for a Class 2 transaction + if (pduType == Cfdp::T_FIN && + txn->getClass() == Cfdp::Class::CLASS_2) { + + // Deserialize FIN PDU + FinPdu fin; + Fw::SerialBuffer sb2(const_cast(buffer.getData()), buffer.getSize()); + sb2.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = fin.deserializeFrom(sb2); + if (deserStatus == Fw::FW_SERIALIZE_OK) { + // Re-send the FIN-ACK + this->sendAck(txn, ACK_TXN_STATUS_TERMINATED, + FILE_DIRECTIVE_FIN, + fin.getConditionCode(), + txn->m_history->peer_eid, + txn->m_history->seq_num); + } + // Note: Deserialization errors are silently ignored in hold state + // as we're just trying to be helpful by re-acknowledging FIN if we can + } +} + +void Engine::recvInit(Transaction *txn, const Fw::Buffer& buffer) +{ + // Use peekPduType to determine the PDU type before deserializing + Cfdp::PduTypeEnum pduType = Cfdp::peekPduType(buffer); + + // First parse header to get transaction information + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Cfdp::PduHeader header; + Fw::SerializeStatus status = header.fromSerialBuffer(sb); + + if (status == Fw::FW_SERIALIZE_OK) { + TransactionSeq transactionSeq = header.getTransactionSeq(); + EntityId sourceEid = header.getSourceEid(); + Class::T txmMode = header.getTxmMode(); + + // only RX transactions dare tread here + txn->m_history->seq_num = transactionSeq; + + // peer_eid is always the remote partner. src_eid is always the transaction source. + // in this case, they are the same + txn->m_history->peer_eid = sourceEid; + txn->m_history->src_eid = sourceEid; + + // all RX transactions will need a chunk list to track file segments + if (txn->m_chunks == NULL) + { + txn->m_chunks = txn->m_chan->findUnusedChunks(DIRECTION_RX); + } + if (txn->m_chunks == NULL) + { + this->m_manager->log_WARNING_HI_ChunklistUnavailable(transactionSeq); + } + else + { + if (pduType == Cfdp::T_FILE_DATA) + { + // file data PDU + // being idle and receiving a file data PDU means that no active transaction knew + // about the transaction in progress, so most likely PDUs were missed. + + if (txmMode == Cfdp::Class::CLASS_1) + { + // R1, can't do anything without metadata first + txn->m_state = TXN_STATE_DROP; // drop all incoming + // use inactivity timer to ultimately free the state + } + else + { + // R2 can handle missing metadata, so go ahead and create a temp file + txn->m_state = TXN_STATE_R2; + txn->m_txn_class = Cfdp::Class::CLASS_2; + txn->rInit(); + this->dispatchRecv(txn, buffer); // re-dispatch to enter r2 + } + } + else if (pduType == Cfdp::T_METADATA) + { + // file directive PDU with metadata - this is the expected case for starting a new RX transaction + MetadataPdu md; + Fw::SerialBuffer sb2(const_cast(buffer.getData()), buffer.getSize()); + sb2.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = md.deserializeFrom(sb2); + if (deserStatus == Fw::FW_SERIALIZE_OK) + { + this->recvMd(txn, md); + + // NOTE: whether or not class 1 or 2, get a free chunks. It's cheap, and simplifies cleanup path + txn->m_state = txmMode == Cfdp::Class::CLASS_1 ? TXN_STATE_R1 : TXN_STATE_R2; + txn->m_txn_class = txmMode; + txn->m_flags.rx.md_recv = true; + txn->rInit(); // initialize R + } + else + { + m_manager->log_WARNING_LO_FailMetadataPduDeserialization(txn->getChannelId(), static_cast(deserStatus)); + } + } + else + { + // Unexpected PDU type in init state + this->m_manager->log_WARNING_LO_UnhandledPduInIdleState(); + this->m_manager->incrementRecvErrors(txn->getChannelId()); + } + } + + if (txn->m_state == TXN_STATE_INIT) + { + // state was not changed, so free the transaction + this->finishTransaction(txn, false); + } + } else { + m_manager->log_WARNING_LO_FailPduHeaderDeserialization(txn->getChannelId(), status); + } +} + +void Engine::receivePdu(U8 chan_id, const Fw::Buffer& buffer) +{ + Transaction *txn = NULL; + Channel *chan = NULL; + + FW_ASSERT(chan_id < Cfdp::NumChannels, chan_id, Cfdp::NumChannels); + + chan = m_channels[chan_id]; + FW_ASSERT(chan != NULL); + + // Parse the header to get transaction routing info + // Avoid full PDU deserialization here to defer it until the appropriate handler + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Cfdp::PduHeader header; + Fw::SerializeStatus status = header.fromSerialBuffer(sb); + + if (status == Fw::FW_SERIALIZE_OK) { + // Increment received PDU counter for PDUs with valid headers + this->m_manager->incrementRecvPdu(chan_id); + + TransactionSeq transactionSeq = header.getTransactionSeq(); + EntityId sourceEid = header.getSourceEid(); + EntityId destEid = header.getDestEid(); + + // Look up transaction by sequence number + txn = chan->findTransactionBySequenceNumber(transactionSeq, sourceEid); + + if (txn == NULL) + { + // if no match found, then it must be the case that we would be the destination entity id, so verify it + if (destEid == this->m_manager->getLocalEidParam()) + { + // we didn't find a match, so assign it to a transaction + // assume this is initiating an RX transaction, as TX transactions are only commanded + txn = this->startRxTransaction(chan->getChannelId()); + if (txn == NULL) + { + this->m_manager->log_WARNING_HI_RxTransactionLimitReached( + sourceEid, + transactionSeq); + } + } + else + { + this->m_manager->log_WARNING_LO_InvalidDestinationEid(destEid); + } + } + + if (txn != NULL) + { + // found one! Send it to the transaction state processor + this->dispatchRecv(txn, buffer); + } + else + { + // TODO BPC: Add throttled EVR + // TODO JMP: One of the two EVRs above get sent right before an EVR here (throttle those too?) + } + } else { + // Invalid PDU header, drop packet + m_manager->log_WARNING_LO_FailPduHeaderDeserialization(chan_id, static_cast(status)); + } +} + + +void Engine::setChannelFlowState(U8 channelId, Flow::T flowState) +{ + FW_ASSERT(channelId <= Cfdp::NumChannels, channelId, Cfdp::NumChannels); + m_channels[channelId]->setFlowState(flowState); +} + +Status::T Engine::setSuspendResumeTransaction(U8 channelId, TransactionSeq transactionSeq, EntityId entityId, SuspendResume::T action) +{ + Status::T status = Status::ERROR; + + FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels); + + Channel* chan = m_channels[channelId]; + Transaction* txn = chan->findTransactionBySequenceNumber(transactionSeq, entityId); + + if (txn != nullptr) { + txn->m_flags.com.suspended = (action == SuspendResume::SUSPEND); + status = Status::SUCCESS; + } + + return status; +} + +Status::T Engine::cancelTransactionBySeq(U8 channelId, TransactionSeq transactionSeq, EntityId entityId) +{ + Status::T status = Status::ERROR; + + FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels); + + Channel* chan = m_channels[channelId]; + Transaction* txn = chan->findTransactionBySequenceNumber(transactionSeq, entityId); + + if (txn != nullptr) { + this->cancelTransaction(txn); + status = Status::SUCCESS; + } + + return status; +} + +Status::T Engine::abandonTransaction(U8 channelId, TransactionSeq transactionSeq, EntityId entityId) +{ + Status::T status = Status::ERROR; + + FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels); + + Channel* chan = m_channels[channelId]; + Transaction* txn = chan->findTransactionBySequenceNumber(transactionSeq, entityId); + + if (txn != nullptr) { + this->finishTransaction(txn, false); + status = Status::SUCCESS; + } + + return status; +} + +void Engine::txFileInitiate(Transaction *txn, Class::T cfdp_class, Keep::T keep, U8 chan, + U8 priority, EntityId dest_id) +{ + txn->initTxFile(cfdp_class, keep, chan, priority); + + // Increment sequence number for new transaction + ++this->m_seqNum; + + // Capture info for history + txn->m_history->seq_num = this->m_seqNum; + txn->m_history->src_eid = m_manager->getLocalEidParam(); + txn->m_history->peer_eid = dest_id; + + txn->m_chan->insertSortPrio(txn, QueueId::PEND); +} + +Status::T Engine::txFile(const Fw::String& src_filename, const Fw::String& dst_filename, + Class::T cfdp_class, Keep::T keep, U8 chan_num, + U8 priority, EntityId dest_id, TransactionInitType initType) +{ + Transaction *txn; + Channel* chan = nullptr; + + FW_ASSERT(chan_num < Cfdp::NumChannels, chan_num, Cfdp::NumChannels); + chan = m_channels[chan_num]; + + Status::T ret = Cfdp::Status::SUCCESS; + + if (chan->getNumCmdTx() < CFDP_MAX_COMMANDED_PLAYBACK_FILES_PER_CHAN) + { + txn = chan->findUnusedTransaction(DIRECTION_TX); + } + else + { + txn = NULL; + } + + if (txn == NULL) + { + this->m_manager->log_WARNING_HI_MaxTxTransactionsReached(); + ret = Cfdp::Status::ERROR; + } + else + { + // NOTE: the caller of this function ensures the provided src and dst filenames are NULL terminated + + txn->m_history->fnames.src_filename = src_filename; + txn->m_history->fnames.dst_filename = dst_filename; + this->txFileInitiate(txn, cfdp_class, keep, chan_num, priority, dest_id); + + chan->incrementCmdTxCounter(); + txn->m_flags.tx.cmd_tx = true; + + // Set transaction initiation type + txn->m_initType = initType; + } + + return ret; +} + +Transaction *Engine::startRxTransaction(U8 chan_num) +{ + Channel *chan = nullptr; + Transaction *txn; + + FW_ASSERT(chan_num < Cfdp::NumChannels, chan_num, Cfdp::NumChannels); + chan = m_channels[chan_num]; + + // if (CF_AppData.hk.Payload.channel_hk[chan_num].q_size[QueueId::RX] < CF_MAX_SIMULTANEOUS_RX) + // { + // txn = chan->findUnusedTransaction(DIRECTION_RX); + // } + // else + // { + // txn = NULL; + // } + // TODO BPC: Do I need to limit receive transactions? + txn = chan->findUnusedTransaction(DIRECTION_RX); + + if (txn != NULL) + { + // set default FIN status + txn->m_state_data.receive.r2.dc = FIN_DELIVERY_CODE_INCOMPLETE; + txn->m_state_data.receive.r2.fs = FIN_FILE_STATUS_DISCARDED; + + txn->m_flags.com.q_index = QueueId::RX; + chan->insertBackInQueue(static_cast(txn->m_flags.com.q_index), &txn->m_cl_node); + } + + return txn; +} + +Status::T Engine::playbackDirInitiate(Playback *pb, const Fw::String& src_filename, const Fw::String& dst_filename, + Class::T cfdp_class, Keep::T keep, U8 chan, U8 priority, + EntityId dest_id) +{ + Status::T status = Cfdp::Status::SUCCESS; + Os::Directory::Status dirStatus; + + // make sure the directory can be open + dirStatus = pb->dir.open(src_filename.toChar(), Os::Directory::READ); + if (dirStatus != Os::Directory::OP_OK) + { + this->m_manager->log_WARNING_HI_PlaybackDirOpenFailed( + src_filename, + dirStatus); + this->m_manager->incrementFaultDirectoryRead(chan); + status = Cfdp::Status::ERROR; + } + else + { + pb->diropen = true; + pb->busy = true; + pb->keep = keep; + pb->priority = priority; + pb->dest_id = dest_id; + pb->cfdp_class = cfdp_class; + + // NOTE: the caller of this function ensures the provided src and dst filenames are NULL terminated + pb->fnames.src_filename = src_filename; + pb->fnames.dst_filename = dst_filename; + } + + // the executor will start the transfer next cycle + return status; +} + +Status::T Engine::playbackDir(const Fw::String& src_filename, const Fw::String& dst_filename, Class::T cfdp_class, + Keep::T keep, U8 chan, U8 priority, EntityId dest_id) +{ + int i; + Playback *pb; + Status::T status; + + // Loop through the channel's playback directories to find an open slot + for (i = 0; i < CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN; ++i) + { + pb = m_channels[chan]->getPlayback(i); + if (!pb->busy) + { + break; + } + } + + if (i == CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN) + { + this->m_manager->log_WARNING_HI_PlaybackDirSlotUnavailable(); + status = Cfdp::Status::ERROR; + } + else + { + status = this->playbackDirInitiate(pb, src_filename, dst_filename, cfdp_class, keep, chan, priority, dest_id); + } + + return status; +} + +Status::T Engine::startPollDir(U8 chanId, U8 pollId, const Fw::String& srcDir, const Fw::String& dstDir, + Class::T cfdp_class, U8 priority, EntityId destEid, + U32 intervalSec) +{ + Status::T status = Cfdp::Status::SUCCESS; + CfdpPollDir* pd = NULL; + + FW_ASSERT(chanId < Cfdp::NumChannels, chanId, Cfdp::NumChannels); + FW_ASSERT(pollId < CFDP_MAX_POLLING_DIR_PER_CHAN, pollId, CFDP_MAX_POLLING_DIR_PER_CHAN); + + // First check if the poll directory is already in use + pd = m_channels[chanId]->getPollDir(pollId); + if(pd->enabled == Fw::Enabled::DISABLED) + { + // Populate arguments + pd->intervalSec = intervalSec; + pd->priority = priority; + pd->cfdpClass = cfdp_class; + pd->destEid = destEid; + pd->srcDir = srcDir; + pd->dstDir = dstDir; + + // Set timer and enable polling + pd->intervalTimer.setTimer(pd->intervalSec); + pd->enabled = Fw::Enabled::ENABLED; + } + else + { + // TODO BPC: emit EVR here + status = Cfdp::Status::ERROR; + } + + return status; +} + +Status::T Engine::stopPollDir(U8 chanId, U8 pollId) +{ + Status::T status = Cfdp::Status::SUCCESS; + CfdpPollDir* pd = NULL; + + FW_ASSERT(chanId < Cfdp::NumChannels, chanId, Cfdp::NumChannels); + FW_ASSERT(pollId < CFDP_MAX_POLLING_DIR_PER_CHAN, pollId, CFDP_MAX_POLLING_DIR_PER_CHAN); + + // Check if the poll directory is in use + pd = m_channels[chanId]->getPollDir(pollId); + if(pd->enabled == Fw::Enabled::DISABLED) + { + // Clear poll directory arguments + pd->intervalSec = 0; + pd->priority = 0; + pd->cfdpClass = static_cast(0); + pd->destEid = static_cast(0); + pd->srcDir = ""; + pd->dstDir = ""; + + // Disable timer and polling + pd->intervalTimer.disableTimer(); + pd->enabled = Fw::Enabled::DISABLED; + } + else + { + // TODO BPC: emit EVR here + status = Cfdp::Status::ERROR; + } + + return status; +} + +void Engine::cycle(void) +{ + int i; + + for (i = 0; i < Cfdp::NumChannels; ++i) + { + Channel* chan = m_channels[i]; + FW_ASSERT(chan != nullptr); + + chan->resetOutgoingCounter(); + + if (chan->getFlowState() == Cfdp::Flow::NOT_FROZEN) + { + // handle ticks before tx cycle. Do this because there may be a limited number of TX messages available + // this cycle, and it's important to respond to class 2 ACK/NAK more than it is to send new filedata + // PDUs. + + // cycle all transactions (tick) + chan->tickTransactions(); + + // cycle the current tx transaction + chan->cycleTx(); + + chan->processPlaybackDirectories(); + chan->processPollingDirectories(); + } + } +} + +void Engine::finishTransaction(Transaction *txn, bool keep_history) +{ + if (txn->m_flags.com.q_index == QueueId::FREE) + { + this->m_manager->log_DIAGNOSTIC_ResetFreedTransaction(); + return; + } + + // this should always be + FW_ASSERT(txn->m_chan != NULL); + + // If this was on the TXA queue (transmit side) then we need to move it out + // so the tick processor will stop trying to actively transmit something - + // it should move on to the next transaction. + // + // RX transactions can stay on the RX queue, that does not hurt anything + // because they are only triggered when a PDU comes in matching that seq_num + // (RX queue is not separated into A/W parts) + if (txn->m_flags.com.q_index == QueueId::TXA) + { + txn->m_chan->dequeueTransaction(txn); + txn->m_chan->insertSortPrio(txn, QueueId::TXW); + } + + if (true == txn->m_fd.isOpen()) + { + txn->m_fd.close(); + + if (!txn->m_keep) + { + this->handleNotKeepFile(txn); + } + } + + if (txn->m_history != NULL) + { + // Emit completion events for successful transactions + if (!TxnStatusIsError(txn->m_history->txn_stat)) + { + if (txn->m_history->dir == DIRECTION_TX) + { + this->m_manager->log_ACTIVITY_HI_TxFileTransferCompleted( + txn->m_txn_class, + txn->m_history->src_eid, + txn->m_history->seq_num, + txn->m_history->fnames.src_filename, + txn->m_history->fnames.dst_filename, + static_cast(txn->m_fsize) + ); + } + else if (txn->m_history->dir == DIRECTION_RX) + { + this->m_manager->log_ACTIVITY_HI_RxFileTransferCompleted( + txn->m_txn_class, + txn->m_history->src_eid, + txn->m_history->seq_num, + txn->m_history->fnames.src_filename, + txn->m_history->fnames.dst_filename, + static_cast(txn->m_fsize) + ); + } + } + + this->sendEotPkt(txn); + + // extra bookkeeping for tx direction only + if (txn->m_history->dir == DIRECTION_TX && txn->m_flags.tx.cmd_tx) + { + txn->m_chan->decrementCmdTxCounter(); + } + + // Notify via port if this was a port-initiated transfer + if (txn->m_initType == INIT_BY_PORT) + { + // Map transaction status to SendFileStatus + Svc::SendFileStatus::T status; + if (TxnStatusIsError(txn->m_history->txn_stat)) + { + status = Svc::SendFileStatus::STATUS_ERROR; + } + else + { + status = Svc::SendFileStatus::STATUS_OK; + } + + // Invoke the file complete notification + this->m_manager->sendFileComplete(status); + } + + txn->m_flags.com.keep_history = keep_history; + } + + if (txn->m_pb) + { + // a playback's transaction is now done, decrement the playback counter + FW_ASSERT(txn->m_pb->num_ts); + --txn->m_pb->num_ts; + } + + txn->m_chan->clearCurrentIfMatch(txn); + + // Put this transaction into the holdover state, inactivity timer will recycle it + txn->m_state = TXN_STATE_HOLD; + this->armInactTimer(txn); +} + +void Engine::setTxnStatus(Transaction *txn, TxnStatus txn_stat) +{ + if (!TxnStatusIsError(txn->m_history->txn_stat)) + { + txn->m_history->txn_stat = txn_stat; + } +} + +void Engine::sendEotPkt(Transaction *txn) +{ + // TODO BPC: This is sending a telemetry packet at the end of a completed transaction + // How do we want to handle this in F' telemetry? + + // CF_EotPacket_t * EotPktPtr; + // CFE_SB_Buffer_t *BufPtr; + + // /* + // ** Get a Message block of memory and initialize it + // */ + // BufPtr = CFE_SB_AllocateMessageBuffer(sizeof(*EotPktPtr)); + + // if (BufPtr != NULL) + // { + // EotPktPtr = (void *)BufPtr; + + // CFE_MSG_Init(CFE_MSG_PTR(EotPktPtr->TelemetryHeader), CFE_SB_ValueToMsgId(CF_EOT_TLM_MID), sizeof(*EotPktPtr)); + + // EotPktPtr->Payload.channel = txn->getChannelId(); + // EotPktPtr->Payload.direction = txn->m_history->dir; + // EotPktPtr->Payload.fnames = txn->m_history->fnames; + // EotPktPtr->Payload.state = txn->m_state; + // EotPktPtr->Payload.txn_stat = txn->m_history->txn_stat; + // EotPktPtr->Payload.src_eid = txn->m_history->src_eid; + // EotPktPtr->Payload.peer_eid = txn->m_history->peer_eid; + // EotPktPtr->Payload.seq_num = txn->m_history->seq_num; + // EotPktPtr->Payload.fsize = txn->m_fsize; + // EotPktPtr->Payload.crc_result = txn->m_crc.getValue(); + + // /* + // ** Timestamp and send eod of transaction telemetry + // */ + // CFE_SB_TimeStampMsg(CFE_MSG_PTR(EotPktPtr->TelemetryHeader)); + // CFE_SB_TransmitBuffer(BufPtr, true); + // } +} + +void Engine::cancelTransaction(Transaction *txn) +{ + void (Transaction::*fns[DIRECTION_NUM])() = {nullptr}; + + fns[DIRECTION_RX] = &Transaction::rCancel; + fns[DIRECTION_TX] = &Transaction::sCancel; + + if (!txn->m_flags.com.canceled) + { + txn->m_flags.com.canceled = true; + this->setTxnStatus(txn, TXN_STATUS_CANCEL_REQUEST_RECEIVED); + + // this should always be true, just confirming before indexing into array + if (txn->m_history->dir < DIRECTION_NUM) + { + (txn->*fns[txn->m_history->dir])(); + } + } +} + +bool Engine::isPollingDir(const char *src_file, U8 chan_num) +{ + bool return_code = false; + char src_dir[MaxFilePathSize] = "\0"; + CfdpPollDir * pd; + int i; + + const char* last_slash = strrchr(src_file, '/'); + if (last_slash != NULL) + { + strncpy(src_dir, src_file, last_slash - src_file); + } + + for (i = 0; i < CFDP_MAX_POLLING_DIR_PER_CHAN; ++i) + { + pd = m_channels[chan_num]->getPollDir(i); + if (strcmp(src_dir, pd->srcDir.toChar()) == 0) + { + return_code = true; + break; + } + } + + return return_code; +} + +void Engine::handleNotKeepFile(Transaction *txn) +{ + Os::FileSystem::Status fileStatus = Os::FileSystem::OTHER_ERROR; + Fw::String failDir; + Fw::String moveDir; + + // Sender + if (txn->getHistory()->dir == DIRECTION_TX) + { + if (!TxnStatusIsError(txn->getHistory()->txn_stat)) + { + // If move directory is defined attempt move + moveDir = m_manager->getMoveDirParam(txn->getChannelId()); + if(moveDir.length() > 0) + { + fileStatus = Os::FileSystem::moveFile(txn->m_history->fnames.src_filename.toChar(), moveDir.toChar()); + if(fileStatus != Os::FileSystem::OP_OK) + { + m_manager->log_WARNING_LO_FailKeepFileMove(txn->m_history->fnames.src_filename, + moveDir, fileStatus); + } + } + + // If move_dir is empty or move failed, delete the file + if(fileStatus != Os::FileSystem::OP_OK) + { + fileStatus = Os::FileSystem::removeFile(txn->m_history->fnames.src_filename.toChar()); + if(fileStatus != Os::FileSystem::OP_OK) + { + m_manager->log_WARNING_LO_FileRemoveFailed(txn->m_history->fnames.src_filename, fileStatus); + } + } + } + else + { + // file inside a polling directory + if (this->isPollingDir(txn->m_history->fnames.src_filename.toChar(), txn->getChannelId())) + { + // If fail directory is defined attempt move + failDir = m_manager->getFailDirParam(txn->getChannelId()); + if(failDir.length() > 0) + { + fileStatus = Os::FileSystem::moveFile(txn->m_history->fnames.src_filename.toChar(), failDir.toChar()); + if(fileStatus != Os::FileSystem::OP_OK) + { + m_manager->log_WARNING_LO_FailPollFileMove(txn->m_history->fnames.src_filename, + failDir, fileStatus); + } + } + + // If fail_dir is empty or move failed, delete the file + if(fileStatus != Os::FileSystem::OP_OK) + { + fileStatus = Os::FileSystem::removeFile(txn->m_history->fnames.src_filename.toChar()); + if(fileStatus != Os::FileSystem::OP_OK) + { + m_manager->log_WARNING_LO_FileRemoveFailed(txn->m_history->fnames.src_filename, fileStatus); + } + } + } + } + } + // Not Sender + else + { + fileStatus = Os::FileSystem::removeFile(txn->m_history->fnames.dst_filename.toChar()); + if(fileStatus != Os::FileSystem::OP_OK) + { + m_manager->log_WARNING_LO_FileRemoveFailed(txn->m_history->fnames.dst_filename, fileStatus); + } + } +} + +Cfdp::ChannelTelemetry& Engine::getChannelTelemetryRef(U8 channelId) +{ + return this->m_manager->getChannelTelemetryRef(channelId); +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Engine.hpp b/Svc/Ccsds/CfdpManager/Engine.hpp new file mode 100644 index 00000000000..18d6f825a63 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Engine.hpp @@ -0,0 +1,659 @@ +// ====================================================================== +// \title Engine.hpp +// \brief CFDP Engine header +// +// This file is a port of CFDP engine definitions from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp.h (CFDP engine and packet parsing definitions) +// +// CFDP engine and packet parsing header file +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#ifndef CFDP_ENGINE_HPP +#define CFDP_ENGINE_HPP + +#include + +#include +#include +#include +#include + +// Forward declarations - do NOT include CfdpManager.hpp to avoid circular dependency +namespace Svc { +namespace Ccsds { + class CfdpManager; // CfdpManager stays in Svc::Ccsds + namespace Cfdp { + class Channel; // Channel is in Svc::Ccsds::Cfdp + } +} +} + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +/** + * @brief Structure for use with the Channel::cycleTx() function + */ +struct CycleTxArgs +{ + Channel *chan; /**< \brief channel object */ + int ran_one; /**< \brief should be set to 1 if a transaction was cycled */ +}; + +/** + * @brief Structure for use with the Channel::doTick() function + */ +struct TickArgs +{ + Channel *chan; /**< \brief channel object */ + void (Transaction::*fn)(int *); /**< \brief member function pointer */ + bool early_exit; /**< \brief early exit result */ + int cont; /**< \brief if 1, then re-traverse the list */ +}; + +/** + * @brief CFDP Protocol Engine + * + * Manages the CFDP protocol engine lifecycle, transactions, and operations. + * This class owns all CFDP protocol state (formerly global) and provides + * a clean interface to CfdpManager. + * + * Key design points: + * - Owns engine data + * - Has access to CfdpManager's protected logging methods via manager_ pointer + * - Private methods encapsulate all internal CFDP protocol logic + */ +class Engine { + public: + // ---------------------------------------------------------------------- + // Construction and destruction + // ---------------------------------------------------------------------- + + /** + * @brief Construct a new Engine object + * + * @param manager Pointer to parent CfdpManager component + */ + explicit Engine(CfdpManager* manager); + + /** + * @brief Destroy the Engine object + */ + ~Engine(); + + // ---------------------------------------------------------------------- + // Public interface + // ---------------------------------------------------------------------- + + /** + * @brief Initialize the CFDP engine + */ + void init(); + + /** + * @brief Cycle the engine once per scheduler call + * + * This drives all CFDP protocol processing + */ + void cycle(); + + /** + * @brief Receive and process a PDU + * + * @param chan_id Channel ID receiving the PDU + * @param buffer Buffer containing the PDU to decode and process + */ + void receivePdu(U8 chan_id, const Fw::Buffer& buffer); + + /** + * @brief Begin transmit of a file + * + * @param src Local filename + * @param dst Remote filename + * @param cfdp_class Whether to perform a class 1 or class 2 transfer + * @param keep Whether to keep or delete the local file after completion + * @param chan_num CFDP channel number to use + * @param priority CFDP priority level + * @param dest_id Entity ID of remote receiver + * @param initType Transaction initiation method (command or port) + * @returns Cfdp::Status::SUCCESS on success, error code otherwise + */ + Status::T txFile(const Fw::String& src, const Fw::String& dst, + Class::T cfdp_class, Keep::T keep, + U8 chan_num, U8 priority, EntityId dest_id, + TransactionInitType initType = INIT_BY_COMMAND); + + /** + * @brief Begin transmit of a directory + * + * @param src Local directory + * @param dst Remote directory + * @param cfdp_class Whether to perform a class 1 or class 2 transfer + * @param keep Whether to keep or delete the local file after completion + * @param chan CFDP channel number to use + * @param priority CFDP priority level + * @param dest_id Entity ID of remote receiver + * @returns Cfdp::Status::SUCCESS on success, error code otherwise + */ + Status::T playbackDir(const Fw::String& src, const Fw::String& dst, + Class::T cfdp_class, Keep::T keep, + U8 chan, U8 priority, EntityId dest_id); + + /** + * @brief Start polling a directory + * + * @param chanId CFDP channel number to use + * @param pollId Channel poll directory index to use + * @param srcDir Local directory + * @param dstDir Remote directory + * @param cfdp_class Whether to perform a class 1 or class 2 transfer + * @param priority CFDP priority level + * @param destEid Entity ID of remote receiver + * @param intervalSec Time between directory playbacks in seconds + * @returns Cfdp::Status::SUCCESS on success, error code otherwise + */ + Status::T startPollDir(U8 chanId, U8 pollId, const Fw::String& srcDir, + const Fw::String& dstDir, Class::T cfdp_class, + U8 priority, EntityId destEid, U32 intervalSec); + + /** + * @brief Stop polling a directory + * + * @param chanId CFDP channel number + * @param pollId Channel poll directory index + * @returns Cfdp::Status::SUCCESS on success, error code otherwise + */ + Status::T stopPollDir(U8 chanId, U8 pollId); + + /** + * @brief Set channel flow state + * + * Called by CfdpManager::SetChannelFlow_cmdHandler() + * + * @param channelId Channel index + * @param flowState Flow state to set (normal or frozen) + */ + void setChannelFlowState(U8 channelId, Flow::T flowState); + + /** + * @brief Set transaction suspend state + * + * @param channelId Channel ID + * @param transactionSeq Transaction sequence number + * @param entityId Entity ID + * @param action Suspend or resume action + * @return Status::SUCCESS if transaction was found and suspended/resumed, Status::ERROR otherwise + */ + Status::T setSuspendResumeTransaction(U8 channelId, TransactionSeq transactionSeq, EntityId entityId, SuspendResume::T action); + + /** + * @brief Cancel a transaction with graceful close-out + * + * @param channelId Channel ID + * @param transactionSeq Transaction sequence number + * @param entityId Entity ID + * @return Status::SUCCESS if transaction was found and canceled, Status::ERROR otherwise + */ + Status::T cancelTransactionBySeq(U8 channelId, TransactionSeq transactionSeq, EntityId entityId); + + /** + * @brief Abandon a transaction immediately + * + * @param channelId Channel ID + * @param transactionSeq Transaction sequence number + * @param entityId Entity ID + * @return Status::SUCCESS if transaction was found and abandoned, Status::ERROR otherwise + */ + Status::T abandonTransaction(U8 channelId, TransactionSeq transactionSeq, EntityId entityId); + + // ---------------------------------------------------------------------- + // Public Transaction Interface + // Methods used by CfdpRx/CfdpTx transaction processing + // ---------------------------------------------------------------------- + + /** + * @brief Finish a transaction + * + * This marks the transaction as completed and puts it into a holdover state. + * After the inactivity timer expires, the resources will be recycled and + * become available for re-use. + * + * Holdover is necessary because even though locally we consider the transaction + * to be complete, there may be undelivered PDUs still in network queues that + * get delivered to us late. By holding this transaction for a bit longer, + * we can still associate those PDUs with this transaction/seq_num and + * appropriately handle them as dupes/spurious deliveries. + * + * @param txn Pointer to the transaction object + * @param keep_history Whether the transaction info should be preserved in history + */ + void finishTransaction(Transaction *txn, bool keep_history); + + /** + * @brief Helper function to store transaction status code only + * + * This records the status in the history block but does not set FIN flag + * or take any other protocol/state machine actions. + * + * @param txn Pointer to the transaction object + * @param txn_stat Status Code value to set within transaction + */ + void setTxnStatus(Transaction *txn, TxnStatus txn_stat); + + /** + * @brief Arm the ACK timer for a transaction + * + * Sets the ACK timer duration based on the channel configuration and marks + * the timer as armed. + * + * @param txn Pointer to the transaction object + */ + void armAckTimer(Transaction *txn); + + /** + * @brief Arm the inactivity timer for a transaction + * + * Sets the inactivity timer duration based on the transaction state and + * channel configuration. + * + * @param txn Pointer to the transaction object + */ + void armInactTimer(Transaction *txn); + + /** + * @brief Create, encode, and send a Metadata PDU + * + * Creates a MetadataPdu object, initializes it with transaction parameters, + * requests a buffer from CfdpManager, serializes the PDU into the buffer, + * and sends it via the output port. + * + * @param txn Pointer to the transaction object + * + * @returns Status::T status code + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval Cfdp::Status::ERROR if serialization fails. + */ + Status::T sendMd(Transaction *txn); + + /** + * @brief Encode and send a File Data PDU + * + * Accepts a FileDataPdu object that has been initialized by the caller, + * requests a buffer from CfdpManager, serializes the PDU into the buffer, + * and sends it via the output port. + * + * @param txn Pointer to the transaction object + * @param fdPdu Reference to initialized FileDataPdu object + * + * @returns Status::T status code + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval Cfdp::Status::ERROR if serialization fails. + */ + Status::T sendFd(Transaction *txn, FileDataPdu& fdPdu); + + /** + * @brief Create, encode, and send an EOF (End of File) PDU + * + * Creates an EofPdu object, initializes it with transaction parameters + * including file size and checksum, requests a buffer from CfdpManager, + * serializes the PDU into the buffer, and sends it via the output port. + * + * @param txn Pointer to the transaction object + * + * @returns Status::T status code + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval Cfdp::Status::ERROR if serialization fails. + */ + Status::T sendEof(Transaction *txn); + + /** + * @brief Create, encode, and send an ACK (Acknowledgment) PDU + * + * Creates an AckPdu object, initializes it with the specified parameters, + * requests a buffer from CfdpManager, serializes the PDU into the buffer, + * and sends it via the output port. + * + * @note This function takes explicit peer_eid and tsn parameters instead of + * getting them from transaction history because of the special case where a + * FIN-ACK must be sent for an unknown transaction. It's better for long term + * maintenance to not build an incomplete History for it. + * + * @param txn Pointer to the transaction object + * @param ts Transaction ACK status + * @param dir_code File directive code being acknowledged (EOF or FIN) + * @param cc Condition code of transaction + * @param peer_eid Remote entity ID + * @param tsn Transaction sequence number + * + * @returns Status::T status code + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval Cfdp::Status::ERROR if serialization fails. + */ + Status::T sendAck(Transaction *txn, AckTxnStatus ts, FileDirective dir_code, + ConditionCode cc, EntityId peer_eid, TransactionSeq tsn); + + /** + * @brief Create, encode, and send a FIN (Finished) PDU + * + * Creates a FinPdu object, initializes it with the specified delivery code, + * file status, and condition code, requests a buffer from CfdpManager, + * serializes the PDU into the buffer, and sends it via the output port. + * + * @param txn Pointer to the transaction object + * @param dc Final delivery status code (complete or incomplete) + * @param fs Final file status (retained or rejected, etc) + * @param cc Final CFDP condition code + * + * @returns Status::T status code + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval Cfdp::Status::ERROR if serialization fails. + */ + Status::T sendFin(Transaction *txn, FinDeliveryCode dc, FinFileStatus fs, + ConditionCode cc); + + /** + * @brief Encode and send a NAK (Negative Acknowledgment) PDU + * + * Accepts a NakPdu object that has been initialized by the caller with the + * requested file segments, requests a buffer from CfdpManager, serializes + * the PDU into the buffer, and sends it via the output port. + * + * @note Transaction must be Class 2 (NAK only used in Class 2). + * + * @param txn Pointer to the transaction object + * @param nakPdu Reference to initialized NakPdu object + * + * @returns Status::T status code + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval Cfdp::Status::ERROR if serialization fails. + */ + Status::T sendNak(Transaction *txn, NakPdu& nakPdu); + + /** + * @brief Handle receipt of metadata PDU + * + * This should only be invoked for buffers that have been identified + * as a metadata PDU. PDU validation already done in MetadataPdu::fromBuffer. + * + * @param txn Pointer to the transaction state + * @param pdu The metadata PDU + */ + void recvMd(Transaction *txn, const MetadataPdu& pdu); + + /** + * @brief Unpack a file data PDU from a received message + * + * This should only be invoked for buffers that have been identified + * as a file data PDU. + * + * @param txn Pointer to the transaction state + * @param pdu The file data PDU + * + * @returns integer status code + * @retval Cfdp::Status::SUCCESS on success + * @retval Cfdp::Status::ERROR for general errors + * @retval Cfdp::Status::SHORT_PDU_ERROR PDU too short + */ + Status::T recvFd(Transaction *txn, const FileDataPdu& pdu); + + /** + * @brief Unpack an EOF PDU from a received message + * + * This should only be invoked for buffers that have been identified + * as an end of file PDU. + * + * @param txn Pointer to the transaction state + * @param pdu The EOF PDU + * + * @returns integer status code + * @retval Cfdp::Status::SUCCESS on success + * @retval Cfdp::Status::SHORT_PDU_ERROR on error + */ + Status::T recvEof(Transaction *txn, const EofPdu& pdu); + + /** + * @brief Unpack an FIN PDU from a received message + * + * This should only be invoked for buffers that have been identified + * as a final PDU. + * + * @param txn Pointer to the transaction state + * @param pdu The FIN PDU + * + * @returns integer status code + * @retval Cfdp::Status::SUCCESS on success + * @retval Cfdp::Status::SHORT_PDU_ERROR on error + */ + Status::T recvFin(Transaction *txn, const FinPdu& pdu); + + /** + * @brief Unpack a NAK PDU from a received message + * + * This should only be invoked for buffers that have been identified + * as a negative/non-acknowledgment PDU. + * + * @param txn Pointer to the transaction state + * @param pdu The NAK PDU + * + * @returns integer status code + * @retval Cfdp::Status::SUCCESS on success + * @retval Cfdp::Status::SHORT_PDU_ERROR on error + */ + Status::T recvNak(Transaction *txn, const NakPdu& pdu); + + /** + * @brief Initiate a file transfer transaction + * + * @param txn Pointer to the transaction state + * @param cfdp_class Set to class 1 or class 2 + * @param keep Whether to keep the local file + * @param chan CFDP channel number + * @param priority Priority of transfer + * @param dest_id Destination entity ID + */ + void txFileInitiate(Transaction *txn, Class::T cfdp_class, Keep::T keep, U8 chan, + U8 priority, EntityId dest_id); + + /** + * @brief Initiate playback of a directory + * + * @param pb Playback state + * @param src_filename Source filename + * @param dst_filename Destination filename + * @param cfdp_class Set to class 1 or class 2 + * @param keep Whether to keep the local file + * @param chan CFDP channel number + * @param priority Priority of transfer + * @param dest_id Destination entity ID + * @returns SUCCESS if initiated, error otherwise + */ + Status::T playbackDirInitiate(Playback *pb, const Fw::String& src_filename, const Fw::String& dst_filename, + Class::T cfdp_class, Keep::T keep, U8 chan, U8 priority, EntityId dest_id); + + /** + * @brief Dispatch TX state machine for a transaction + * + * Called by Channel to drive the TX state machine for a transaction. + * + * @param txn Pointer to the transaction state + */ + void dispatchTx(Transaction *txn); + + /** + * @brief Get reference to channel telemetry for Channel class + * + * Allows Channel to access telemetry without exposing m_manager + * + * @param channelId Channel ID + * @return Reference to channel telemetry structure + */ + Cfdp::ChannelTelemetry& getChannelTelemetryRef(U8 channelId); + + private: + // ---------------------------------------------------------------------- + // Private member variables + // ---------------------------------------------------------------------- + + //!< Parent component for event and telemetry methods + CfdpManager* m_manager; + + //! Channel data structures + Channel* m_channels[Cfdp::NumChannels]; + + //! Sequence number tracker for outgoing transactions + TransactionSeq m_seqNum; + + // Note: Transactions, histories, and chunks are now owned by each Channel + + // ---------------------------------------------------------------------- + // Private helper methods + // All the non-public CFDP functions converted to methods + // ---------------------------------------------------------------------- + + // Transaction Management + + /** + * @brief Send an end of transaction packet + * + * @param txn Pointer to the transaction object + */ + void sendEotPkt(Transaction *txn); + + /** + * @brief Cancels a transaction + * + * @param txn Pointer to the transaction state + */ + void cancelTransaction(Transaction *txn); + + /** + * @brief Helper function to start a new RX transaction + * + * This sets various fields inside a newly-allocated transaction + * structure appropriately for receiving a file. Note that in the + * receive direction, most fields are unknown until the MD is received, + * and thus are left in their initial state here (generally 0). + * + * If there is no capacity for another RX transaction, this returns NULL. + * + * @param chan_num CFDP channel number + * @returns Pointer to new transaction + */ + Transaction* startRxTransaction(U8 chan_num); + + // PDU Operations - Send + + // PDU Operations - Receive + + /** + * @brief Receive state function to ignore a packet + * + * This function signature must match all receive state functions. + * The parameter txn is ignored here. + * + * @param txn Pointer to the transaction state + * @param buffer Buffer containing the PDU to process + */ + void recvDrop(Transaction *txn, const Fw::Buffer& buffer); + + /** + * @brief Receive state function during holdover period + * + * This function signature must match all receive state functions. + * Handles any possible spurious PDUs that might come in after the + * transaction is considered done. This can happen if ACKs were + * lost in transmission causing the sender to retransmit PDUs even + * though we already completed the transaction. + * + * @param txn Pointer to the transaction state + * @param buffer Buffer containing the PDU to process + */ + void recvHold(Transaction *txn, const Fw::Buffer& buffer); + + /** + * @brief Receive state function to process new rx transaction + * + * An idle transaction has never had message processing performed on it. + * Typically, the first packet received for a transaction would be + * the metadata PDU. There's a special case for R2 where the metadata + * PDU could be missed, and filedata comes in instead. In that case, + * an R2 transaction must still be started. + * + * @param txn Pointer to the transaction state + * @param buffer Buffer containing the PDU to process + */ + void recvInit(Transaction *txn, const Fw::Buffer& buffer); + + // Dispatch + + /** + * @brief Dispatch received packet to its handler + * + * This dispatches the PDU to the appropriate handler + * based on the transaction state. + * + * @param txn Pointer to the transaction state + * @param buffer Buffer containing the PDU to dispatch + */ + void dispatchRecv(Transaction *txn, const Fw::Buffer& buffer); + + // Channel Processing + + /** + * @brief Check if source file came from polling directory + * + * @param src_file Source file path to check + * @param chan_num Channel number + * + * @retval true/false + */ + bool isPollingDir(const char *src_file, U8 chan_num); + + /** + * @brief Remove/Move file after transaction + * + * This helper is used to handle "not keep" file option after a transaction. + * + * @param txn Pointer to the transaction object + */ + void handleNotKeepFile(Transaction *txn); + + // Friend declarations for testing + friend class CfdpManagerTester; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif /* !CFDP_ENGINE_HPP */ \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Events.fppi b/Svc/Ccsds/CfdpManager/Events.fppi new file mode 100644 index 00000000000..10d87ea2685 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Events.fppi @@ -0,0 +1,553 @@ +event BuffersExhausted severity warning low \ + format "Unable to allocate a PDU buffer" + +event InvalidChannel( + channelId: U8 @< Requested channel index + maxChannelId: U8 @< Maximum channel index +) \ + severity warning low \ + format "Invalid channel ID {}, maximum channel ID is {}" + +event SendFileInitiated( + sourceFileName: string size MaxFilePathSize @< Source file being sent +) \ + severity activity low \ + format "Successfully initiated file send transfer for {}" + +event UnsupportedSendFileArguments( + offset: U32 @< Offset of the send file request + length: U32 @< Length of the send file request +) \ + severity warning low \ + format "Invalid send file port request with offset {}, length {}" + +event SendFileInitiateFail( + sourceFileName: string size MaxFilePathSize @< Source file being sent +) \ + severity warning low \ + format "Failed to initiate file send transfer for {}" + +event PlaybackInvalidChannel( + channelId: U8 @< Requested channel index + maxChannelId: U8 @< Maximum channel index +) \ + severity warning low \ + format "Invalid channel ID {}, maximum channel ID is {}" + +event PlaybackInitiated( + sourceDirectory: string size MaxFilePathSize @< Source directory being sent +) \ + severity activity low \ + format "Successfully initiated directory playback for {}" + +event PlaybackInitiateFail( + sourceDirectory: string size MaxFilePathSize @< Source directory being sent +) \ + severity warning low \ + format "Failed to initiate file send transfer for {}" + +event PollDirInitiated( + sourceDirectory: string size MaxFilePathSize @< Source directory being sent +) \ + severity activity low \ + format "Successfully initiated directory poll for {}" + +event PollDirStopped( + channelId: U8 @< Channel index stopped + pollId: U8 @< Channel poll index stopped +) \ + severity activity low \ + format "Successfully stopped directory poll for channel {}, index {}" + +event SetFlowState( + channelId: U8 @< Channel being set + flowState: Cfdp.Flow @< Flow state set +) \ + severity activity low \ + format "Set channel {} to {}" + +event InvalidChannelPoll( + pollId: U8 @< Requested channel index + maxPollId: U8 @< Maximum channel index +) \ + severity warning low \ + format "Invalid poll ID {}, maximum poll ID is {}" + +event FailKeepFileMove( + srcFile: string size MaxFilePathSize @< Source file being moved + moveDir: string size MaxFilePathSize @< Directory file was moved to + status: I32 @< Status of the move operation +) \ + severity warning low \ + format "Failed to move {} to {} error {}" + +event FailPollFileMove( + srcFile: string size MaxFilePathSize @< Source file being moved + failDir: string size MaxFilePathSize @< Directory file was moved to + status: I32 @< Status of the move operation +) \ + severity warning low \ + format "Failed to move {} to {} error {}" + +event FailPduHeaderDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize PDU header on channel {}, status {}" + +event FailMetadataPduSerialization( + channelId: U8 @< Channel sending the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to serialize Metadata PDU on channel {}, status {}" + +event FailFileDataPduSerialization( + channelId: U8 @< Channel sending the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to serialize File Data PDU on channel {}, status {}" + +event FailEofPduSerialization( + channelId: U8 @< Channel sending the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to serialize EOF PDU on channel {}, status {}" + +event FailAckPduSerialization( + channelId: U8 @< Channel sending the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to serialize ACK PDU on channel {}, status {}" + +event FailFinPduSerialization( + channelId: U8 @< Channel sending the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to serialize FIN PDU on channel {}, status {}" + +event FailNakPduSerialization( + channelId: U8 @< Channel sending the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to serialize NAK PDU on channel {}, status {}" + +event FailMetadataPduDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize Metadata PDU on channel {}, status {}" + +event FailFileDataPduDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize File Data PDU on channel {}, status {}" + +event FailEofPduDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize EOF PDU on channel {}, status {}" + +event FailAckPduDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize ACK PDU on channel {}, status {}" + +event FailFinPduDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize FIN PDU on channel {}, status {}" + +event FailNakPduDeserialization( + channelId: U8 @< Channel that received the PDU + status: I32 @< Serialization status code +) \ + severity warning low \ + format "Failed to deserialize NAK PDU on channel {}, status {}" + +event RxAckLimitReached( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "RX class {} ACK limit reached for transaction {}:{}, no fin-ack sent" + +event RxTempFileCreated( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + filename: string size MaxFilePathSize @< Temporary filename created +) \ + severity activity low \ + format "RX class {} transaction {}:{} creating temp file {} without metadata" + +event RxFileCreateFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + filename: string size MaxFilePathSize @< File that failed to create + status: I32 @< File operation status +) \ + severity warning high \ + format "RX class {} transaction {}:{} failed to create file {}, error={}" + +event RxCrcMismatch( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + expected: U32 @< Expected CRC value + actual: U32 @< Actual CRC value +) \ + severity warning high \ + format "RX class {} transaction {}:{} CRC mismatch: expected 0x{x} actual 0x{x}" + +event RxNakLimitReached( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "RX class {} transaction {}:{} NAK limit reached" + +event RxSeekFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + offset: U32 @< Offset that failed to seek + status: I32 @< File operation status +) \ + severity warning high \ + format "RX class {} transaction {}:{} failed to seek to offset {}, error={}" + +event RxWriteFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + expected: U32 @< Expected bytes to write + actual: I32 @< Actual bytes written +) \ + severity warning high \ + format "RX class {} transaction {}:{} write failed: expected {} bytes, got {}" + +event RxFileSizeMismatch( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + expected: U32 @< Expected file size from metadata + actual: U32 @< Actual file size from EOF +) \ + severity warning high \ + format "RX class {} transaction {}:{} EOF file size mismatch: expected {} got {}" + +event RxInvalidEofPdu( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning low \ + format "RX class {} transaction {}:{} received invalid EOF PDU" + +event RxSeekCrcFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + offset: U32 @< Offset that failed during CRC calculation + status: I32 @< File operation status +) \ + severity warning high \ + format "RX class {} transaction {}:{} failed to seek offset {} during CRC calculation, error={}" + +event RxReadCrcFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + expected: U32 @< Expected bytes to read + actual: I32 @< Actual bytes read +) \ + severity warning high \ + format "RX class {} transaction {}:{} failed to read during CRC calculation: expected {} bytes, got {}" + +event RxEofMdSizeMismatch( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + mdSize: U32 @< File size from metadata + eofSize: U32 @< File size from EOF +) \ + severity warning high \ + format "RX class {} transaction {}:{} EOF/metadata size mismatch: metadata={}, EOF={}" + +event RxFileRenameFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + tempFile: string size MaxFilePathSize @< Temporary file path + finalFile: string size MaxFilePathSize @< Final file path + status: I32 @< File system operation status +) \ + severity warning high \ + format "RX class {} transaction {}:{} failed to rename {} to {}, error={}" + +event RxFileReopenFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + filename: string size MaxFilePathSize @< File that failed to reopen + status: I32 @< File operation status +) \ + severity warning high \ + format "RX class {} transaction {}:{} failed to reopen file {} after rename, error={}" + +event RxInactivityTimeout( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "RX class {} transaction {}:{} inactivity timer expired" + +event RxInvalidDirectiveCode( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + directiveCode: U8 @< Invalid directive code received + substate: U8 @< Current transaction substate +) \ + severity warning low \ + format "RX class {} transaction {}:{} received invalid directive code {} for substate {}" + +event TxAckLimitReached( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "TX class {} transaction {}:{} ACK limit reached, no eof-ack received" + +event TxInactivityTimeout( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "TX class {} transaction {}:{} inactivity timer expired" + +event TxFileOpenFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + filename: string size MaxFilePathSize @< File that failed to open + status: I32 @< File operation status +) \ + severity warning high \ + format "TX class {} transaction {}:{} failed to open file {}, error={}" + +event TxFileSeekFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + status: I32 @< File operation status +) \ + severity warning high \ + format "TX class {} transaction {}:{} failed to seek to beginning of file, error={}" + +event TxSendMetadataFailed( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "TX class {} transaction {}:{} failed to send metadata PDU" + +event TxEarlyFinReceived( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "TX class {} transaction {}:{} received early FIN, cancelling transfer" + +event TxInvalidNakPdu( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning high \ + format "TX class {} transaction {}:{} received invalid NAK PDU" + +event TxInvalidSegmentRequests( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + badCount: U32 @< Number of invalid segment requests +) \ + severity warning low \ + format "TX class {} transaction {}:{} received {} invalid NAK segment requests" + +event TxNonFileDirectivePduReceived( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number +) \ + severity warning low \ + format "TX class {} transaction {}:{} received non-file-directive PDU" + +event TxInvalidDirectiveCode( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + directiveCode: U8 @< Invalid directive code received + substate: U8 @< Current transaction substate +) \ + severity warning low \ + format "TX class {} transaction {}:{} received invalid directive code {} for substate {}" + +event TxFileTransferStarted( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + srcFile: string size MaxFilePathSize @< Source filename + destEid: U32 @< Destination entity ID + destFile: string size MaxFilePathSize @< Destination filename + fileSize: U32 @< File size in bytes +) \ + severity activity high \ + format "TX starting class {} transfer {}:{} -> {}:{}, {} bytes" + +event TxFileTransferCompleted( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + srcFile: string size MaxFilePathSize @< Source filename + destFile: string size MaxFilePathSize @< Destination filename + fileSize: U32 @< File size in bytes +) \ + severity activity high \ + format "TX completed class {} transaction {}:{}, {} -> {}, {} bytes" + +event RxFileTransferCompleted( + cfdpClass: Cfdp.Class @< CFDP class + srcEid: U32 @< Source entity ID + seqNum: U32 @< Transaction sequence number + srcFile: string size MaxFilePathSize @< Source filename + destFile: string size MaxFilePathSize @< Destination filename + fileSize: U32 @< File size in bytes +) \ + severity activity high \ + format "RX completed class {} transaction {}:{}, {} -> {}, {} bytes" + +event MetadataReceived( + srcFile: string size MaxFilePathSize @< Source filename from metadata + destFile: string size MaxFilePathSize @< Destination filename from metadata +) \ + severity activity low \ + format "Metadata received for source: {}, dest: {}" + +event FileDataSegmentMetadata severity warning high \ + format "File data PDU with unsupported segment metadata received" + +event ChunklistUnavailable( + transactionSeq: U32 @< Transaction sequence number +) \ + severity warning high \ + format "Cannot get chunklist, abandoning transaction {}" + +event UnhandledPduInIdleState severity warning low \ + format "Unhandled PDU type received in idle state" + +event RxTransactionLimitReached( + srcEid: U32 @< Source entity ID + transactionSeq: U32 @< Transaction sequence number +) \ + severity warning high \ + format "Dropping packet from {} transaction {} due to max RX transactions reached" + +event InvalidDestinationEid( + destEid: U32 @< Invalid destination entity ID +) \ + severity warning low \ + format "Dropping packet for invalid destination EID {}" + +event MaxTxTransactionsReached severity warning high \ + format "Maximum number of commanded TX files reached" + +event PlaybackDirOpenFailed( + directory: string size MaxFilePathSize @< Directory that failed to open + status: I32 @< Directory operation status +) \ + severity warning high \ + format "Failed to open playback directory {}, error={}" + +event PlaybackDirSlotUnavailable severity warning high \ + format "No playback directory slot available" + +event ResetFreedTransaction severity diagnostic \ + format "Attempt to reset a transaction that has already been freed" + +event FileRemoveFailed( + filename: string size MaxFilePathSize @< File that failed to delete + status: I32 @< File system operation status +) \ + severity warning low \ + format "Failed to remove file {}, error={}" + +@ Transaction was suspended +event TransactionSuspended( + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID +) \ + severity activity low \ + format "Transaction ({}, {}) suspended" + +@ Transaction was resumed +event TransactionResumed( + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID +) \ + severity activity low \ + format "Transaction ({}, {}) resumed" + +@ Transaction canceled +event TransactionCanceled( + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID +) \ + severity activity high \ + format "Transaction ({}, {}) canceled" + +@ Transaction abandoned +event TransactionAbandoned( + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID +) \ + severity activity high \ + format "Transaction ({}, {}) abandoned" + +@ Transaction not found for command +event TransactionNotFound( + transactionSeq: Cfdp.TransactionSeq @< Transaction sequence number + entityId: Cfdp.EntityId @< Entity ID +) \ + severity warning low \ + format "Transaction ({}, {}) not found" + +event ResetCounters( + channelId: U8 @< Channel ID reset (0xFF indicates all channels) +) \ + severity activity high \ + format "Reset telemetry counters for channel {}" diff --git a/Svc/Ccsds/CfdpManager/Parameters.fppi b/Svc/Ccsds/CfdpManager/Parameters.fppi new file mode 100644 index 00000000000..b20503bed48 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Parameters.fppi @@ -0,0 +1,58 @@ +@ CFDP ID to denote the current node when sending PDUs +param LocalEid: Cfdp.EntityId \ + default 42 + +@ Maximum number of bytes to put into a file PDU +param OutgoingFileChunkSize: U32 \ + default 992 + +@ The maximum number of received bytes to calculate a CRC for in a single scheduler cycle +param RxCrcCalcBytesPerCycle: U32 \ + default 65536 + +@ Default CFDP channel for fileIn port-initiated file transfers +param FileInDefaultChannel: U8 \ + default 0 + +@ Default destination entity ID for fileIn port-initiated file transfers +param FileInDefaultDestEntityId: Cfdp.EntityId \ + default 100 + +@ Default CFDP class for fileIn port-initiated file transfers +param FileInDefaultClass: Cfdp.Class \ + default Cfdp.Class.CLASS_2 + +@ Default file retention policy for fileIn port-initiated file transfers +param FileInDefaultKeep: Cfdp.Keep \ + default Cfdp.Keep.DELETE + +@ Default priority for fileIn port-initiated file transfers +param FileInDefaultPriority: U8 \ + default 0 + +@ Parameter configuration for an array CFDP channels +param ChannelConfig: Cfdp.ChannelArrayParams \ + default [ \ + { + ack_limit = 4, \ + nack_limit = 4, \ + ack_timer = 3, \ + inactivity_timer = 30, \ + dequeue_enabled = Fw.Enabled.ENABLED, \ + move_dir = "", \ + max_outgoing_pdus_per_cycle = 64, \ + tmp_dir = "/tmp", \ + fail_dir = "/fail" \ + }, \ + { + ack_limit = 4, \ + nack_limit = 4, \ + ack_timer = 3, \ + inactivity_timer = 30, \ + dequeue_enabled = Fw.Enabled.ENABLED, \ + move_dir = "", \ + max_outgoing_pdus_per_cycle = 64, \ + tmp_dir = "/tmp", \ + fail_dir = "/fail" \ + } \ + ] \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Telemetry.fppi b/Svc/Ccsds/CfdpManager/Telemetry.fppi new file mode 100644 index 00000000000..28b73b0ad99 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Telemetry.fppi @@ -0,0 +1,13 @@ +@ CFDP Manager Telemetry +@ +@ This file defines proposed telemetry channels for CfdpManager based on +@ telemetry counters from the NASA CF (CFDP) Application. +@ +@ Note: These are currently PROPOSALS and not yet implemented. +@ Implementation would require adding telemetry channels and updating +@ the code to emit telemetry at appropriate locations. + +@ Array of per-channel telemetry counters +@ Each element contains receive counters, sent counters, fault counters, +@ queue depths, and activity counters for one CFDP channel. +telemetry ChannelTelemetry: Cfdp.ChannelTelemetryArray diff --git a/Svc/Ccsds/CfdpManager/Timer.cpp b/Svc/Ccsds/CfdpManager/Timer.cpp new file mode 100644 index 00000000000..51e4e699af3 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Timer.cpp @@ -0,0 +1,60 @@ +// ====================================================================== +// \title Timer.cpp +// \author Brian Campuzano +// \brief cpp file for the Timer class implementation +// ====================================================================== + +#include + +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ---------------------------------------------------------------------- +// Class construction and destruction +// ---------------------------------------------------------------------- + +Timer ::Timer() : timerStatus(UNINITIALIZED), secondsRemaining(0) {} + +Timer ::~Timer() {} + +// ---------------------------------------------------------------------- +// Class interfaces +// ---------------------------------------------------------------------- + +void Timer ::setTimer(U32 timerDuration) +{ + this->timerStatus = RUNNING; + this->secondsRemaining = timerDuration; +} + +void Timer ::disableTimer(void) +{ + this->timerStatus = EXPIRED; + this->secondsRemaining = 0; +} + +Timer::Status Timer ::getStatus(void) +{ + return this->timerStatus; +} + +void Timer ::run(void) +{ + if(this->timerStatus == RUNNING) + { + FW_ASSERT(this->secondsRemaining > 0); + this->secondsRemaining--; + + if(this->secondsRemaining == 0) + { + this->timerStatus = EXPIRED; + } + } +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Timer.hpp b/Svc/Ccsds/CfdpManager/Timer.hpp new file mode 100644 index 00000000000..f38fb57f4cc --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Timer.hpp @@ -0,0 +1,72 @@ +// ====================================================================== +// \title Timer.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP timer that is driven by +// ====================================================================== + +#ifndef CCSDS_CFDPTIMER_HPP +#define CCSDS_CFDPTIMER_HPP + +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +class Timer { + // ---------------------------------------------------------------------- + // Class types + // ---------------------------------------------------------------------- + public: + enum Status { + UNINITIALIZED, + RUNNING, + EXPIRED + }; + + public: + // ---------------------------------------------------------------------- + // Class construction and destruction + // ---------------------------------------------------------------------- + + //! Construct Timer object + Timer(); + + //! Destroy Timer object + ~Timer(); + + public: + // ---------------------------------------------------------------------- + // Class interfaces + // ---------------------------------------------------------------------- + + //! Initialize a CFDP timer and start its execution + void setTimer(U32 timerDuration //!< The duration of the timer in seconds + ); + + //! Disables a CFDP timer + void disableTimer(void); + + //! Get the status of a CFDP timer + Status getStatus(void); + + //! Runs a one second increment of the CFDP timers + void run(void); + + private: + // ---------------------------------------------------------------------- + // Class member variables + // ---------------------------------------------------------------------- + + //! Number of seconds until the timer expires + Status timerStatus; + + //! Number of seconds until the timer expires + U32 secondsRemaining; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // CCSDS_CFDPTIMER_HPP diff --git a/Svc/Ccsds/CfdpManager/Transaction.hpp b/Svc/Ccsds/CfdpManager/Transaction.hpp new file mode 100644 index 00000000000..42883fa0edb --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Transaction.hpp @@ -0,0 +1,860 @@ +// ====================================================================== +// \title Transaction.hpp +// \brief CFDP Transaction state machine class for TX and RX operations +// +// This file is a port of transaction state machine definitions from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp_r.h (receive transaction state machine definitions) +// - cf_cfdp_s.h (send transaction state machine definitions) +// - cf_cfdp_dispatch.h (transaction dispatch definitions) +// +// This file contains the unified interface for CFDP transaction state +// machines, encompassing both TX (send) and RX (receive) operations. +// The implementation is split across TransactionTx.cpp and +// TransactionRx.cpp for maintainability. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#ifndef Svc_Ccsds_CfdpTransaction_HPP +#define Svc_Ccsds_CfdpTransaction_HPP + +#include + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// Forward declarations +class CfdpManager; +class Engine; +class Channel; +class Transaction; + +// ====================================================================== +// Dispatch Table Type Definitions +// ====================================================================== + +/** + * @brief A member function pointer for dispatching actions to a handler, without existing PDU data + * + * This allows quick delegation to handler functions using dispatch tables. This version is + * used on the transmit side, where a PDU will likely be generated/sent by the handler being + * invoked. + * + * @note This is a member function pointer - invoke with: (txn->*fn)() + */ +using StateSendFunc = void (Transaction::*)(); + +/** + * @brief A member function pointer for dispatching actions to a handler, with existing PDU data + * + * This allows quick delegation of PDUs to handler functions using dispatch tables. This version is + * used on the receive side where a PDU buffer is associated with the activity, which is then + * interpreted by the handler being invoked. + * + * @param[inout] buffer The buffer containing the PDU currently being received/processed + * @note This is a member function pointer - invoke with: (txn->*fn)(buffer) + */ +using StateRecvFunc = void (Transaction::*)(const Fw::Buffer& buffer); + +/** + * @brief A table of transmit handler functions based on transaction state + * + * This reflects the main dispatch table for the transmit side of a transaction. + * Each possible state has a corresponding function pointer in the table to implement + * the PDU transmit action(s) associated with that state. + */ +struct TxnSendDispatchTable +{ + StateSendFunc tx[TXN_STATE_INVALID]; /**< \brief Transmit handler function */ +}; + +/** + * @brief A table of receive handler functions based on transaction state + * + * This reflects the main dispatch table for the receive side of a transaction. + * Each possible state has a corresponding function pointer in the table to implement + * the PDU receive action(s) associated with that state. + */ +struct TxnRecvDispatchTable +{ + /** \brief a separate recv handler for each possible file directive PDU in this state */ + StateRecvFunc rx[TXN_STATE_INVALID]; +}; + +/** + * @brief A table of receive handler functions based on file directive code + * + * For PDUs identified as a "file directive" type - generally anything other + * than file data - this provides a table to branch to a different handler + * function depending on the value of the file directive code. + */ +struct FileDirectiveDispatchTable +{ + /** \brief a separate recv handler for each possible file directive PDU in this state */ + StateRecvFunc fdirective[FILE_DIRECTIVE_INVALID_MAX]; +}; + +/** + * @brief A dispatch table for receive file transactions, receive side + * + * This is used for "receive file" transactions upon receipt of a directive PDU. + * Depending on the sub-state of the transaction, a different action may be taken. + */ +struct RSubstateDispatchTable +{ + const FileDirectiveDispatchTable *state[RX_SUB_STATE_NUM_STATES]; +}; + +/** + * @brief A dispatch table for send file transactions, receive side + * + * This is used for "send file" transactions upon receipt of a directive PDU. + * Depending on the sub-state of the transaction, a different action may be taken. + */ +struct SSubstateRecvDispatchTable +{ + const FileDirectiveDispatchTable *substate[TX_SUB_STATE_NUM_STATES]; +}; + +/** + * @brief A dispatch table for send file transactions, transmit side + * + * This is used for "send file" transactions to generate the next PDU to be sent. + * Depending on the sub-state of the transaction, a different action may be taken. + */ +struct SSubstateSendDispatchTable +{ + StateSendFunc substate[TX_SUB_STATE_NUM_STATES]; +}; + +/** + * @brief CFDP Transaction state machine class + * + * This class provides TX and RX state machine operations for CFDP transactions. + * Implementation is split across multiple files for maintainability: + * - TransactionTx.cpp: TX (send) state machine implementation + * - TransactionRx.cpp: RX (receive) state machine implementation + */ +class Transaction { + friend class Engine; + friend class Channel; + friend class CfdpManagerTester; + + public: + // ---------------------------------------------------------------------- + // Construction and Destruction + // ---------------------------------------------------------------------- + + //! Parameterized constructor for channel-bound transaction initialization + //! @param channel Pointer to the channel this transaction belongs to + //! @param channelId Channel ID number + //! @param engine Pointer to the CFDP engine + //! @param manager Pointer to the CfdpManager component + Transaction(Channel* channel, U8 channelId, Engine* engine, CfdpManager* manager); + + ~Transaction(); + + /** + * @brief Reset transaction to default state + * + * Resets the transaction to a clean state while preserving channel binding. + * Used when returning a transaction to the free pool for reuse. + */ + void reset(); + + /** + * @brief Initialize transaction for outgoing file transfer + * + * Sets up transaction state for transmitting a file. + * + * @param cfdp_class CFDP class (1 or 2) + * @param keep Whether to keep file after transfer + * @param chan Channel number + * @param priority Transaction priority + */ + void initTxFile(Class::T cfdp_class, Keep::T keep, U8 chan, U8 priority); + + /** + * @brief Static callback for finding transaction by sequence number + * + * C-style callback for list traversal. Used with CfdpCListTraverse. + * + * @param node List node pointer + * @param context Pointer to CfdpTraverseTransSeqArg + * @return Traversal status (CONTINUE or EXIT) + */ + static CListTraverseStatus findBySequenceNumberCallback(CListNode *node, void *context); + + /** + * @brief Static callback for priority search + * + * C-style callback for list traversal. Used with CfdpCListTraverseR. + * + * @param node List node pointer + * @param context Pointer to CfdpTraversePriorityArg + * @return Traversal status (CONTINUE or EXIT) + */ + static CListTraverseStatus prioritySearchCallback(CListNode *node, void *context); + + // ---------------------------------------------------------------------- + // Accessors + // ---------------------------------------------------------------------- + + /** + * @brief Get transaction history + * @return Pointer to history structure + */ + History* getHistory() const { return m_history; } + + /** + * @brief Get transaction priority + * @return Priority value + */ + U8 getPriority() const { return m_priority; } + + /** + * @brief Get channel ID + * @return Channel ID number + */ + U8 getChannelId() const { return m_chan_num; } + + /** + * @brief Get transaction class (CLASS_1 or CLASS_2) + * @return Transaction class + */ + Class::T getClass() const { return m_txn_class; } + + /** + * @brief Get transaction state + * @return Transaction state + */ + TxnState getState() const { return m_state; } + + // ---------------------------------------------------------------------- + // TX State Machine - Implemented in TransactionTx.cpp + // ---------------------------------------------------------------------- + + /************************************************************************/ + /** @brief S1 receive PDU processing. + * + * @param buffer The buffer containing the PDU to process + */ + void s1Recv(const Fw::Buffer& buffer); + + /************************************************************************/ + /** @brief S2 receive PDU processing. + * + * @param buffer The buffer containing the PDU to process + */ + void s2Recv(const Fw::Buffer& buffer); + + /************************************************************************/ + /** @brief S1 dispatch function. + */ + void s1Tx(); + + /************************************************************************/ + /** @brief S2 dispatch function. + */ + void s2Tx(); + + /************************************************************************/ + /** @brief Perform acknowledgement timer tick (time-based) processing for S transactions. + * + * This is invoked as part of overall timer tick processing if the transaction + * has some sort of acknowledgement pending from the remote. + */ + void sAckTimerTick(); + + /************************************************************************/ + /** @brief Perform tick (time-based) processing for S transactions. + * + * This function is called on every transaction by the engine on + * every scheduler cycle. This is where flags are checked to send EOF or + * FIN-ACK. If nothing else is sent, it checks to see if a NAK + * retransmit must occur. + * + * @param cont Unused, exists for compatibility with tick processor + */ + void sTick(int *cont); + + /************************************************************************/ + /** @brief Perform NAK response for TX transactions + * + * This function is called at tick processing time to send pending + * NAK responses. It indicates "cont" is 1 if there are more responses + * left to send. + * + * @param cont Set to 1 if a NAK was generated + */ + void sTickNak(int *cont); + + /************************************************************************/ + /** @brief Cancel an S transaction. + */ + void sCancel(); + + /************************************************************************/ + /** @brief Sends an EOF for S1. + */ + void s1SubstateSendEof(); + + /************************************************************************/ + /** @brief Triggers tick processing to send an EOF and wait for EOF-ACK for S2 + */ + void s2SubstateSendEof(); + + /************************************************************************/ + /** @brief Standard state function to send the next file data PDU for active transaction. + * + * During the transfer of active transaction file data PDUs, the file + * offset is saved. This function sends the next chunk of data. If + * the file offset equals the file size, then transition to the EOF + * state. + */ + void sSubstateSendFileData(); + + /************************************************************************/ + /** @brief Send filedata handling for S2. + * + * S2 will either respond to a NAK by sending retransmits, or in + * absence of a NAK, it will send more of the original file data. + */ + void s2SubstateSendFileData(); + + /************************************************************************/ + /** @brief Send metadata PDU. + * + * Construct and send a metadata PDU. This function determines the + * size of the file to put in the metadata PDU. + */ + void sSubstateSendMetadata(); + + /************************************************************************/ + /** @brief A FIN was received before file complete, so abandon the transaction. + * + * @param pdu The PDU to process + */ + void s2EarlyFin(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief S2 received FIN, so set flag to send FIN-ACK. + * + * @param pdu Buffer containing the FIN PDU to process + */ + void s2Fin(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief S2 NAK PDU received handling. + * + * Stores the segment requests from the NAK packet in the chunks + * structure. These can be used to generate re-transmit filedata + * PDUs. + * + * @param pdu Buffer containing the NAK PDU to process + */ + void s2Nak(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief S2 NAK handling but with arming the NAK timer. + * + * @param pdu Buffer containing the NAK PDU to process + */ + void s2NakArm(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief S2 received ACK PDU. + * + * Handles reception of an ACK PDU + * + * @param pdu Buffer containing the ACK PDU to process + */ + void s2EofAck(const Fw::Buffer& pdu); + + private: + /*********************************************************************** + * + * Handler routines for send-file transactions + * These are not called from outside this module, but are declared here so they can be unit tested + * + ************************************************************************/ + + /************************************************************************/ + /** @brief Send an EOF PDU. + * + * @retval Cfdp::Status::SUCCESS on success. + * @retval Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR if message buffer cannot be obtained. + * @retval SEND_PDU_ERROR if an error occurred while building the packet. + */ + Status::T sSendEof(); + + Status::T sSendFileData(FileSize foffs, FileSize bytes_to_read, U8 calc_crc, FileSize* bytes_processed); + + Status::T sCheckAndRespondNak(bool* nakProcessed); + + Status::T sSendFinAck(); + + public: + // ---------------------------------------------------------------------- + // RX State Machine - Implemented in TransactionRx.cpp + // ---------------------------------------------------------------------- + + /************************************************************************/ + /** @brief R1 receive PDU processing. + * + * @param buffer The buffer containing the PDU to process + */ + void r1Recv(const Fw::Buffer& buffer); + + /************************************************************************/ + /** @brief R2 receive PDU processing. + * + * @param buffer The buffer containing the PDU to process + */ + void r2Recv(const Fw::Buffer& buffer); + + /************************************************************************/ + /** @brief Perform acknowledgement timer tick (time-based) processing for R transactions. + * + * This is invoked as part of overall timer tick processing if the transaction + * has some sort of acknowledgement pending from the remote. + */ + void rAckTimerTick(); + + /************************************************************************/ + /** @brief Perform tick (time-based) processing for R transactions. + * + * This function is called on every transaction by the engine on + * every scheduler cycle. This is where flags are checked to send ACK, + * NAK, and FIN. It checks for inactivity timer and processes the + * ACK timer. The ACK timer is what triggers re-sends of PDUs + * that require acknowledgment. + * + * @param cont Ignored/Unused + */ + void rTick(int *cont); + + /************************************************************************/ + /** @brief Cancel an R transaction. + */ + void rCancel(); + + /************************************************************************/ + /** @brief Initialize a transaction structure for R. + */ + void rInit(); + + /************************************************************************/ + /** @brief Helper function to store transaction status code and set send_fin flag. + * + * @param txn_stat Status Code value to set within transaction + */ + void r2SetFinTxnStatus(TxnStatus txn_stat); + + /************************************************************************/ + /** @brief CFDP R1 transaction reset function. + * + * All R transactions use this call to indicate the transaction + * state can be returned to the system. While this function currently + * only calls the transaction reset logic, it is here as a placeholder. + */ + void r1Reset(); + + /************************************************************************/ + /** @brief CFDP R2 transaction reset function. + * + * Handles reset logic for R2, then calls R1 reset logic. + */ + void r2Reset(); + + /************************************************************************/ + /** @brief Checks that the transaction file's CRC matches expected. + * + * @retval Cfdp::Status::SUCCESS on CRC match, otherwise Cfdp::Status::CFDP_ERROR. + * + * @param expected_crc Expected CRC + */ + Status::T rCheckCrc(U32 expected_crc); + + /************************************************************************/ + /** @brief Checks R2 transaction state for transaction completion status. + * + * This function is called anywhere there's a desire to know if the + * transaction has completed. It may trigger other actions by setting + * flags to be handled during tick processing. In order for a + * transaction to be complete, it must have had its meta-data PDU + * received, the EOF must have been received, and there must be + * no gaps in the file. EOF is not checked in this function, because + * it's only called from functions after EOF is received. + * + * @param ok_to_send_nak If set to 0, suppress sending of a NAK packet + */ + void r2Complete(int ok_to_send_nak); + + // ---------------------------------------------------------------------- + // Dispatch Methods (ported from cf_cfdp_dispatch.c) + // ---------------------------------------------------------------------- + + /************************************************************************/ + /** @brief Dispatch function for received PDUs on receive-file transactions + * + * Receive file transactions primarily only react/respond to received PDUs. + * This function dispatches to the appropriate handler based on the + * transaction substate and PDU type. + * + * @param buffer Buffer containing the PDU to dispatch + * @param dispatch Dispatch table for file directive PDUs + * @param fd_fn Function to handle file data PDUs + */ + void rDispatchRecv(const Fw::Buffer& buffer, + const RSubstateDispatchTable *dispatch, + StateRecvFunc fd_fn); + + /************************************************************************/ + /** @brief Dispatch function for received PDUs on send-file transactions + * + * Send file transactions also react/respond to received PDUs. + * Note that a file data PDU is not expected here. + * + * @param buffer Buffer containing the PDU to dispatch + * @param dispatch Dispatch table for file directive PDUs + */ + void sDispatchRecv(const Fw::Buffer& buffer, + const SSubstateRecvDispatchTable *dispatch); + + /************************************************************************/ + /** @brief Dispatch function to send/generate PDUs on send-file transactions + * + * Send file transactions generate PDUs each cycle based on the + * transaction state. This does not have an existing PDU buffer at + * the time of dispatch, but one may be generated by the invoked function. + * + * @param dispatch State-based dispatch table + */ + void sDispatchTransmit(const SSubstateSendDispatchTable *dispatch); + + /************************************************************************/ + /** @brief Top-level Dispatch function to send a PDU based on current state + * + * This does not have an existing PDU buffer at the time of dispatch, + * but one may be generated by the invoked function. + * + * @param dispatch Transaction State-based Dispatch table + */ + void txStateDispatch(const TxnSendDispatchTable *dispatch); + + private: + /************************************************************************/ + /** @brief Process a filedata PDU on a transaction. + * + * @retval Cfdp::Status::SUCCESS on success. Cfdp::Status::CFDP_ERROR on error. + * + * @param pdu Buffer containing the file data PDU to process + */ + Status::T rProcessFd(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Processing receive EOF common functionality for R1/R2. + * + * This function is used for both R1 and R2 EOF receive. It calls + * the unmarshaling function and then checks known transaction + * data against the PDU. + * + * @retval Cfdp::Status::SUCCESS on success. Returns anything else on error. + * + * @param pdu Buffer containing the EOF PDU to process + */ + Status::T rSubstateRecvEof(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Process receive EOF for R1. + * + * Only need to confirm CRC for R1. + * + * @param pdu Buffer containing the EOF PDU to process + */ + void r1SubstateRecvEof(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Process receive EOF for R2. + * + * For R2, need to trigger the send of EOF-ACK and then call the + * check complete function which will either send NAK or FIN. + * + * @param pdu Buffer containing the EOF PDU to process + */ + void r2SubstateRecvEof(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Process received file data for R1. + * + * For R1, only need to digest the CRC. + * + * @param pdu Buffer containing the file data PDU to process + */ + void r1SubstateRecvFileData(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Process received file data for R2. + * + * For R2, the CRC is checked after the whole file is received + * since there may be gaps. Instead, insert file received range + * data into chunks. Once NAK has been received, this function + * always checks for completion. This function also re-arms + * the ACK timer. + * + * @param pdu Buffer containing the file data PDU to process + */ + void r2SubstateRecvFileData(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Loads a single NAK segment request. + * + * This is a callback function used with CfdpChunkList::computeGaps(). + * For each gap found, this function adds a segment request to the NAK PDU. + * + * @param chunk Pointer to the gap chunk information + * @param nak Pointer to the NAK PDU being constructed + */ + void r2GapCompute(const Chunk *chunk, NakPdu& nak); + + /************************************************************************/ + /** @brief Send a NAK PDU for R2. + * + * NAK PDU is sent when there are gaps in the received data. The + * chunks class tracks this and generates the NAK PDU by calculating + * gaps internally and calling r2GapCompute(). There is a special + * case where if a metadata PDU has not been received, then a NAK + * packet will be sent to request another. + * + * @retval Cfdp::Status::SUCCESS on success. Cfdp::Status::CFDP_ERROR on error. + */ + Status::T rSubstateSendNak(); + + /************************************************************************/ + /** @brief Calculate up to the configured amount of bytes of CRC. + * + * The RxCrcCalcBytesPerCycle parameter specifies the number of bytes + * to calculate per transaction per scheduler cycle. At each cycle, the file is + * read and this number of bytes are calculated. This function will set + * the checksum error condition code if the final CRC does not match. + * + * @par PTFO + * Increase throughput by consuming all CRC bytes per scheduler cycle in + * transaction-order. This would require a change to the meaning + * of the RxCrcCalcBytesPerCycle parameter. + * + * @retval Cfdp::Status::SUCCESS on completion. + * @retval Cfdp::Status::CFDP_ERROR on non-completion. + */ + Status::T r2CalcCrcChunk(); + + /************************************************************************/ + /** @brief Send a FIN PDU. + * + * @retval Cfdp::Status::SUCCESS on success. Cfdp::Status::CFDP_ERROR on error. + */ + Status::T r2SubstateSendFin(); + + /************************************************************************/ + /** @brief Process receive FIN-ACK PDU. + * + * This is the end of an R2 transaction. Simply reset the transaction + * state. + * + * @param pdu Buffer containing the ACK PDU to process + */ + void r2RecvFinAck(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Process receive metadata PDU for R2. + * + * It's possible that metadata PDU was missed in cf_cfdp.c, or that + * it was re-sent. This function checks if it was already processed, + * and if not, handles it. If there was a temp file opened due to + * missed metadata PDU, it will move the file to the correct + * destination according to the metadata PDU. + * + * @param pdu Buffer containing the metadata PDU to process + */ + void r2RecvMd(const Fw::Buffer& pdu); + + /************************************************************************/ + /** @brief Logs an inactivity timer expired event. + */ + void rSendInactivityEvent(); + + private: + // ---------------------------------------------------------------------- + // Member Variables + // ---------------------------------------------------------------------- + + /** + * @brief High-level transaction state + * + * Each engine is commanded to do something, which is the overall state. + */ + TxnState m_state; + + /** + * @brief Transaction class (CLASS_1 or CLASS_2) + * + * Set at initialization and never changes. + */ + Class::T m_txn_class; + + /** + * @brief Pointer to history entry + * + * Holds active filenames and possibly other info. + */ + History* m_history; + + /** + * @brief Pointer to chunk wrapper + * + * For gap tracking, only used on class 2. + */ + CfdpChunkWrapper* m_chunks; + + /** + * @brief Inactivity timer + * + * Set to the overall inactivity timer of a remote. + */ + Timer m_inactivity_timer; + + /** + * @brief ACK/NAK timer + * + * Called ack_timer, but is also nak_timer. + */ + Timer m_ack_timer; + + /** + * @brief File size + */ + FileSize m_fsize; + + /** + * @brief File offset for next read + */ + FileSize m_foffs; + + /** + * @brief File descriptor + */ + Os::File m_fd; + + /** + * @brief CRC checksum object + */ + CFDP::Checksum m_crc; + + /** + * @brief Keep file flag + */ + Keep::T m_keep; + + /** + * @brief Channel number + * + * If ever more than one engine, this may need to change to pointer. + */ + U8 m_chan_num; + + /** + * @brief Priority + */ + U8 m_priority; + + /** + * @brief Transaction initiation method + * + * Indicates whether this transaction was initiated via command or port. + * Used to determine whether completion should be notified via FileComplete port. + */ + TransactionInitType m_initType; + + /** + * @brief Circular list node + * + * For connection to a CList (intrusive linked list). + */ + CListNode m_cl_node; + + /** + * @brief Pointer to playback entry + * + * NULL if transaction does not belong to a playback. + */ + Playback* m_pb; + + /** + * @brief State-specific data (TX or RX) + */ + CfdpStateData m_state_data; + + /** + * @brief State flags (TX or RX) + * + * Note: The flags here look a little strange, because there are different + * flags for TX and RX. Both types share the same type of flag, though. + * Since RX flags plus the global flags is over one byte, storing them this + * way allows 2 bytes to cover all possible flags. Please ignore the + * duplicate declarations of the "all" flags. + */ + CfdpStateFlags m_flags; + + /** + * @brief Reference to the wrapper F' component + * + * Used to send PDUs. + */ + CfdpManager* m_cfdpManager; + + /** + * @brief Pointer to the channel wrapper + * + * The channel this transaction belongs to. + */ + Channel* m_chan; + + /** + * @brief Pointer to the CFDP engine + * + * The engine this transaction belongs to. + */ + Engine* m_engine; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_CfdpTransaction_HPP \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/TransactionRx.cpp b/Svc/Ccsds/CfdpManager/TransactionRx.cpp new file mode 100644 index 00000000000..4d2ae6d2459 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/TransactionRx.cpp @@ -0,0 +1,1294 @@ +// ====================================================================== +// \title TransactionRx.cpp +// \brief cpp file for CFDP RX Transaction state machine +// +// This file is a port of RX transaction state machine operations from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp_r.c (receive-file transaction state handling routines) +// - cf_cfdp_dispatch.c (RX state machine dispatch functions) +// +// This file contains various state handling routines for +// transactions which are receiving a file, as well as dispatch +// functions for RX state machines and top-level transaction dispatch. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ====================================================================== +// Construction and Destruction +// ====================================================================== + +Transaction::Transaction(Channel* channel, U8 channelId, Engine* engine, CfdpManager* manager) : + m_state(TXN_STATE_UNDEF), + m_txn_class(Cfdp::Class::CLASS_1), + m_history(nullptr), + m_chunks(nullptr), + m_inactivity_timer(), + m_ack_timer(), + m_fsize(0), + m_foffs(0), + m_fd(), + m_crc(), + m_keep(Cfdp::Keep::KEEP), + m_chan_num(channelId), // Initialize from parameter + m_priority(0), + m_initType(INIT_BY_COMMAND), + m_cl_node{}, + m_pb(nullptr), + m_state_data{}, + m_flags{}, + m_cfdpManager(manager), // Initialize from parameter + m_chan(channel), // Initialize from parameter + m_engine(engine) // Initialize from parameter +{ + // All members initialized via member initializer list above +} + +Transaction::~Transaction() { } + +void Transaction::reset() +{ + // Reset transaction state to default values + this->m_state = TXN_STATE_UNDEF; + this->m_txn_class = Cfdp::Class::CLASS_1; + this->m_fsize = 0; + this->m_foffs = 0; + this->m_keep = Cfdp::Keep::KEEP; + this->m_priority = 0; + this->m_initType = INIT_BY_COMMAND; + this->m_crc = CFDP::Checksum(0); + this->m_pb = nullptr; + + // Use aggregate initialization to zero out unions + this->m_state_data = {}; + this->m_flags = {}; + + // Close the file if it is open + if(this->m_fd.isOpen()) + { + this->m_fd.close(); + } + + // The following state information is PRESERVED across reset (NOT modified): + // - this->m_cfdpManager // Channel binding + // - this->m_chan // Channel binding + // - this->m_engine // Channel binding + // - this->m_chan_num // Channel binding + // - this->m_history // Assigned when transaction is activated + // - this->m_chunks // Assigned when transaction is activated + // - this->m_ack_timer // Timer state preserved + // - this->m_inactivity_timer // Timer state preserved + // - this->m_cl_node // Managed by queue operations in freeTransaction() +} + +// ====================================================================== +// RX State Machine - Public Methods +// ====================================================================== + +void Transaction::r1Recv(const Fw::Buffer& buffer) { + static const FileDirectiveDispatchTable r1_fdir_handlers = { + { + nullptr, /* CFDP_FileDirective_INVALID_MIN */ + nullptr, /* 1 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 2 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 3 is unused in the CFDP_FileDirective_t enum */ + &Transaction::r1SubstateRecvEof, /* CFDP_FileDirective_EOF */ + nullptr, /* CFDP_FileDirective_FIN */ + nullptr, /* CFDP_FileDirective_ACK */ + nullptr, /* CFDP_FileDirective_METADATA */ + nullptr, /* CFDP_FileDirective_NAK */ + nullptr, /* CFDP_FileDirective_PROMPT */ + nullptr, /* 10 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 11 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* CFDP_FileDirective_KEEP_ALIVE */ + } + }; + + static const RSubstateDispatchTable substate_fns = { + { + &r1_fdir_handlers, /* RX_SUB_STATE_FILEDATA */ + &r1_fdir_handlers, /* RX_SUB_STATE_EOF */ + &r1_fdir_handlers, /* RX_SUB_STATE_CLOSEOUT_SYNC */ + } + }; + + this->rDispatchRecv(buffer, &substate_fns, &Transaction::r1SubstateRecvFileData); +} + +void Transaction::r2Recv(const Fw::Buffer& buffer) { + static const FileDirectiveDispatchTable r2_fdir_handlers_normal = { + { + nullptr, /* CFDP_FileDirective_INVALID_MIN */ + nullptr, /* 1 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 2 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 3 is unused in the CFDP_FileDirective_t enum */ + &Transaction::r2SubstateRecvEof, /* CFDP_FileDirective_EOF */ + nullptr, /* CFDP_FileDirective_FIN */ + nullptr, /* CFDP_FileDirective_ACK */ + &Transaction::r2RecvMd, /* CFDP_FileDirective_METADATA */ + nullptr, /* CFDP_FileDirective_NAK */ + nullptr, /* CFDP_FileDirective_PROMPT */ + nullptr, /* 10 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 11 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* CFDP_FileDirective_KEEP_ALIVE */ + } + }; + static const FileDirectiveDispatchTable r2_fdir_handlers_finack = { + { + nullptr, /* CFDP_FileDirective_INVALID_MIN */ + nullptr, /* 1 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 2 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 3 is unused in the CFDP_FileDirective_t enum */ + &Transaction::r2SubstateRecvEof, /* CFDP_FileDirective_EOF */ + nullptr, /* CFDP_FileDirective_FIN */ + &Transaction::r2RecvFinAck, /* CFDP_FileDirective_ACK */ + nullptr, /* CFDP_FileDirective_METADATA */ + nullptr, /* CFDP_FileDirective_NAK */ + nullptr, /* CFDP_FileDirective_PROMPT */ + nullptr, /* 10 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* 11 is unused in the CFDP_FileDirective_t enum */ + nullptr, /* CFDP_FileDirective_KEEP_ALIVE */ + } + }; + + static const RSubstateDispatchTable substate_fns = { + { + &r2_fdir_handlers_normal, /* RX_SUB_STATE_FILEDATA */ + &r2_fdir_handlers_normal, /* RX_SUB_STATE_EOF */ + &r2_fdir_handlers_finack, /* RX_SUB_STATE_CLOSEOUT_SYNC */ + } + }; + + this->rDispatchRecv(buffer, &substate_fns, &Transaction::r2SubstateRecvFileData); +} + +void Transaction::rAckTimerTick() { + U8 ack_limit = 0; + + /* note: the ack timer is only ever armed on class 2 */ + if (this->m_state != TXN_STATE_R2 || !this->m_flags.com.ack_timer_armed) + { + /* nothing to do */ + return; + } + + if (this->m_ack_timer.getStatus() == Timer::Status::RUNNING) + { + this->m_ack_timer.run(); + } + else + { + /* ACK timer expired, so check for completion */ + if (!this->m_flags.rx.complete) + { + this->r2Complete(true); + } + else if (this->m_state_data.receive.sub_state == RX_SUB_STATE_CLOSEOUT_SYNC) + { + /* Increment acknak counter */ + ++this->m_state_data.receive.r2.acknak_count; + + /* Check limit and handle if needed */ + ack_limit = this->m_cfdpManager->getAckLimitParam(this->m_chan_num); + if (this->m_state_data.receive.r2.acknak_count >= ack_limit) + { + this->m_cfdpManager->log_WARNING_HI_RxAckLimitReached( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_engine->setTxnStatus(this, TXN_STATUS_ACK_LIMIT_NO_FIN); + this->m_cfdpManager->incrementFaultAckLimit(this->m_chan_num); + + /* give up on this */ + this->m_engine->finishTransaction(this, true); + this->m_flags.com.ack_timer_armed = false; + } + else + { + this->m_flags.rx.send_fin = true; + } + } + + /* re-arm the timer if it is still pending */ + if (this->m_flags.com.ack_timer_armed) + { + /* whether sending FIN or waiting for more filedata, need ACK timer armed */ + this->m_engine->armAckTimer(this); + } + } +} + +void Transaction::rTick(int *cont /* unused */) { + /* Steven is not real happy with this function. There should be a better way to separate out + * the logic by state so that it isn't a bunch of if statements for different flags + */ + + Status::T sret; + bool pending_send; + + if (!this->m_flags.com.inactivity_fired) + { + if (this->m_inactivity_timer.getStatus() == Timer::Status::RUNNING) + { + this->m_inactivity_timer.run(); + } + else + { + this->m_flags.com.inactivity_fired = true; + + /* HOLD state is the normal path to recycle transaction objects, not an error */ + /* inactivity is abnormal in any other state */ + if (this->m_state != TXN_STATE_HOLD) + { + this->rSendInactivityEvent(); + + /* in class 2 this also triggers sending an early FIN response */ + if (this->m_state == TXN_STATE_R2) + { + this->r2SetFinTxnStatus(TXN_STATUS_INACTIVITY_DETECTED); + } + } + } + } + + pending_send = true; /* maybe; tbd */ + + /* rx maintenance: possibly process send_eof_ack, send_nak or send_fin */ + if (this->m_flags.rx.send_eof_ack) + { + sret = this->m_engine->sendAck(this, ACK_TXN_STATUS_ACTIVE, FILE_DIRECTIVE_END_OF_FILE, + static_cast(this->m_state_data.receive.r2.eof_cc), + this->m_history->peer_eid, this->m_history->seq_num); + FW_ASSERT(sret != Cfdp::Status::SEND_PDU_ERROR); + + /* if Cfdp::Status::SUCCESS, then move on in the state machine. CFDP_SendAck does not return + * SEND_PDU_ERROR */ + if (sret != Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR) + { + this->m_flags.rx.send_eof_ack = false; + } + } + else if (this->m_flags.rx.send_nak) + { + if (!this->rSubstateSendNak()) + { + this->m_flags.rx.send_nak = false; /* will re-enter on error */ + } + } + else if (this->m_flags.rx.send_fin) + { + if (!this->r2SubstateSendFin()) + { + this->m_flags.rx.send_fin = false; /* will re-enter on error */ + } + } + else + { + /* no pending responses to the sender */ + pending_send = false; + } + + /* if the inactivity timer ran out, then there is no sense + * pending for responses for anything. Send out anything + * that we need to send (i.e. the FIN) just in case the sender + * is still listening to us but do not expect any future ACKs */ + if (this->m_flags.com.inactivity_fired && !pending_send) + { + /* the transaction is now recyclable - this means we will + * no longer have a record of this transaction seq. If the sender + * wakes up or if the network delivers severely delayed PDUs at + * some future point, then they will be seen as spurious. They + * will no longer be associable with this transaction at all */ + this->m_chan->recycleTransaction(this); + + /* NOTE: this must be the last thing in here. Do not use txn after this */ + } + else + { + /* transaction still valid so process the ACK timer, if relevant */ + this->rAckTimerTick(); + } +} + +void Transaction::rCancel() { + /* for cancel, only need to send FIN if R2 */ + if ((this->m_state == TXN_STATE_R2) && (this->m_state_data.receive.sub_state < RX_SUB_STATE_CLOSEOUT_SYNC)) + { + this->m_flags.rx.send_fin = true; + } + else + { + this->r1Reset(); /* if R1, just call it quits */ + } +} + +void Transaction::rInit() { + Os::File::Status status; + Fw::String tmpDir; + Fw::String dst; + + if (this->m_state == TXN_STATE_R2) + { + if (!this->m_flags.rx.md_recv) + { + tmpDir = this->m_cfdpManager->getTmpDirParam(this->m_chan_num); + /* we need to make a temp file and then do a NAK for md PDU */ + /* the transaction already has a history, and that has a buffer that we can use to + * hold the temp filename which is defined by the sequence number and the source entity ID */ + + // Create destination filepath with format: /:.tmp + dst.format("%s/%" CFDP_PRI_ENTITY_ID ":%" CFDP_PRI_TRANSACTION_SEQ ".tmp", + tmpDir.toChar(), + this->m_history->src_eid, + this->m_history->seq_num); + + this->m_history->fnames.dst_filename = dst; + + this->m_cfdpManager->log_ACTIVITY_LO_RxTempFileCreated( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_history->fnames.dst_filename); + } + + this->m_engine->armAckTimer(this); + } + + status = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_CREATE, Os::File::OVERWRITE); + if (status != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxFileCreateFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_history->fnames.dst_filename, + status); + this->m_cfdpManager->incrementFaultFileOpen(this->m_chan_num); + if (this->m_state == TXN_STATE_R2) + { + this->r2SetFinTxnStatus(TXN_STATUS_FILESTORE_REJECTION); + } + else + { + this->r1Reset(); + } + } + else + { + this->m_state_data.receive.sub_state = RX_SUB_STATE_FILEDATA; + } +} + +void Transaction::r2SetFinTxnStatus(TxnStatus txn_stat) { + this->m_engine->setTxnStatus(this, txn_stat); + this->m_flags.rx.send_fin = true; +} + +void Transaction::r1Reset() { + this->m_engine->finishTransaction(this, true); +} + +void Transaction::r2Reset() { + if ((this->m_state_data.receive.sub_state == RX_SUB_STATE_CLOSEOUT_SYNC) || + (this->m_state_data.receive.r2.eof_cc != CONDITION_CODE_NO_ERROR) || + TxnStatusIsError(this->m_history->txn_stat) || this->m_flags.com.canceled) + { + this->r1Reset(); /* it's done */ + } + else + { + /* not waiting for FIN ACK, so trigger send FIN */ + this->m_flags.rx.send_fin = true; + } +} + +Status::T Transaction::rCheckCrc(U32 expected_crc) { + Status::T ret = Cfdp::Status::SUCCESS; + U32 crc_result; + + // The F' version does not have an equivalent finalize call as it + // - Never stores a partial word internally + // - Never needs to "flush" anything + // - Always accounts for padding at update time + crc_result = this->m_crc.getValue(); + if (crc_result != expected_crc) + { + this->m_cfdpManager->log_WARNING_HI_RxCrcMismatch( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + expected_crc, + crc_result); + this->m_cfdpManager->incrementFaultCrcMismatch(this->m_chan_num); + ret = Cfdp::Status::ERROR; + } + + return ret; +} + +void Transaction::r2Complete(int ok_to_send_nak) { + U32 ret; + bool send_nak = false; + bool send_fin = false; + U8 nack_limit = 0; + /* checking if r2 is complete. Check NAK list, and send NAK if appropriate */ + /* if all data is present, then there will be no gaps in the chunk */ + + if (!TxnStatusIsError(this->m_history->txn_stat)) + { + /* first, check if md is received. If not, send specialized NAK */ + if (!this->m_flags.rx.md_recv) + { + send_nak = true; + } + else + { + /* only look for 1 gap, since the goal here is just to know that there are gaps */ + ret = this->m_chunks->chunks.computeGaps(1, this->m_fsize, 0, nullptr, nullptr); + + if (ret) + { + /* there is at least 1 gap, so send a NAK */ + send_nak = true; + } + else if (this->m_flags.rx.eof_recv) + { + /* the EOF was received, and there are no NAKs -- process completion in send FIN state */ + send_fin = true; + } + } + + if (send_nak && ok_to_send_nak) + { + /* Increment the acknak counter */ + ++this->m_state_data.receive.r2.acknak_count; + + /* Check limit and handle if needed */ + nack_limit = this->m_cfdpManager->getNackLimitParam(this->m_chan_num); + if (this->m_state_data.receive.r2.acknak_count >= nack_limit) + { + this->m_cfdpManager->log_WARNING_HI_RxNakLimitReached( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + send_fin = true; + this->m_cfdpManager->incrementFaultNakLimit(this->m_chan_num); + /* don't use CFDP_R2_SetFinTxnStatus because many places in this function set send_fin */ + this->m_engine->setTxnStatus(this, TXN_STATUS_NAK_LIMIT_REACHED); + this->m_state_data.receive.r2.acknak_count = 0; /* reset for fin/ack */ + } + else + { + this->m_flags.rx.send_nak = true; + } + } + + if (send_fin) + { + this->m_flags.rx.complete = true; /* latch completeness, since send_fin is cleared later */ + + /* the transaction is now considered complete, but this will not overwrite an + * error status code if there was one set */ + this->r2SetFinTxnStatus(TXN_STATUS_NO_ERROR); + } + + /* always go to RX_SUB_STATE_FILEDATA, and let tick change state */ + this->m_state_data.receive.sub_state = RX_SUB_STATE_FILEDATA; + } +} + +// ====================================================================== +// RX State Machine - Private Helper Methods +// ====================================================================== + +Status::T Transaction::rProcessFd(const Fw::Buffer& buffer) { + Status::T ret = Cfdp::Status::SUCCESS; + + /* this function is only entered for data PDUs */ + // Deserialize FileData PDU from buffer + FileDataPdu fd; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = fd.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + this->m_cfdpManager->log_WARNING_LO_FailFileDataPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + ret = Cfdp::Status::ERROR; + } + + /* + * NOTE: The decode routine should have left a direct pointer to the data and actual data length + * within the PDU. The length has already been verified, too. Should not need to make any + * adjustments here, just write it. + */ + + FileSize offset = fd.getOffset(); + U16 dataSize = fd.getDataSize(); + const U8* dataPtr = fd.getData(); + + // Seek to file offset if needed + if (ret == Cfdp::Status::SUCCESS) { + if (this->m_state_data.receive.cached_pos != offset) + { + Os::File::Status status = this->m_fd.seek(offset, Os::File::SeekType::ABSOLUTE); + if (status != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxSeekFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + offset, + status); + this->m_engine->setTxnStatus(this, TXN_STATUS_FILE_SIZE_ERROR); + this->m_cfdpManager->incrementFaultFileSeek(this->m_chan_num); + ret = Cfdp::Status::ERROR; + } + } + } + + // Write file data + if (ret == Cfdp::Status::SUCCESS) + { + FwSizeType write_size = dataSize; + Os::File::Status status = this->m_fd.write(dataPtr, write_size, Os::File::WaitType::WAIT); + if (status != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxWriteFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + dataSize, + static_cast(write_size)); + this->m_engine->setTxnStatus(this, TXN_STATUS_FILESTORE_REJECTION); + this->m_cfdpManager->incrementFaultFileWrite(this->m_chan_num); + ret = Cfdp::Status::ERROR; + } + else + { + this->m_state_data.receive.cached_pos = static_cast(dataSize) + offset; + this->m_cfdpManager->addRecvFileDataBytes(this->m_chan_num, dataSize); + } + } + + return ret; +} + +Status::T Transaction::rSubstateRecvEof(const Fw::Buffer& buffer) { + Status::T ret = Cfdp::Status::SUCCESS; + + // Deserialize EOF PDU from buffer + EofPdu eof; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = eof.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + this->m_cfdpManager->log_WARNING_LO_FailEofPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + ret = Cfdp::Status::REC_PDU_BAD_EOF_ERROR; + } + + if (ret == Cfdp::Status::SUCCESS) + { + if (!this->m_engine->recvEof(this, eof)) + { + /* this function is only entered for PDUs identified as EOF type */ + + /* only check size if MD received, otherwise it's still OK */ + if (this->m_flags.rx.md_recv && (eof.getFileSize() != this->m_fsize)) + { + this->m_cfdpManager->log_WARNING_HI_RxFileSizeMismatch( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_fsize, + eof.getFileSize()); + this->m_cfdpManager->incrementFaultFileSizeMismatch(this->m_chan_num); + ret = Cfdp::Status::REC_PDU_FSIZE_MISMATCH_ERROR; + } + } + else + { + this->m_cfdpManager->log_WARNING_LO_RxInvalidEofPdu( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_cfdpManager->incrementRecvErrors(this->m_chan_num); + ret = Cfdp::Status::REC_PDU_BAD_EOF_ERROR; + } + } + + return ret; +} + +void Transaction::r1SubstateRecvEof(const Fw::Buffer& buffer) { + // Deserialize EOF PDU from buffer + EofPdu eof; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = eof.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad EOF, reset transaction + this->m_cfdpManager->log_WARNING_LO_FailEofPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + this->r1Reset(); + return; + } + + Status::T ret = this->rSubstateRecvEof(buffer); + U32 crc; + + /* this function is only entered for PDUs identified as EOF type */ + crc = eof.getChecksum(); + + if (ret == Cfdp::Status::SUCCESS) + { + /* Verify CRC */ + if (this->rCheckCrc(crc) == Cfdp::Status::SUCCESS) + { + /* successfully processed the file */ + this->m_keep = Cfdp::Keep::KEEP; /* save the file */ + } + /* if file failed to process, there's nothing to do. CFDP_R_CheckCrc() generates an event on failure */ + } + + /* after exit, always reset since we are done */ + /* reset even if the EOF failed -- class 1, so it won't come again! */ + this->r1Reset(); +} + +void Transaction::r2SubstateRecvEof(const Fw::Buffer& buffer) { + Status::T ret; + + if (!this->m_flags.rx.eof_recv) + { + // Deserialize EOF PDU from buffer + EofPdu eof; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = eof.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad EOF, return to FILEDATA substate + this->m_cfdpManager->log_WARNING_LO_FailEofPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + this->m_state_data.receive.sub_state = RX_SUB_STATE_FILEDATA; + return; + } + + ret = this->rSubstateRecvEof(buffer); + + /* did receiving EOF succeed? */ + if (ret == Cfdp::Status::SUCCESS) + { + + this->m_flags.rx.eof_recv = true; + + /* need to remember the EOF CRC for later */ + this->m_state_data.receive.r2.eof_crc = eof.getChecksum(); + this->m_state_data.receive.r2.eof_size = eof.getFileSize(); + + /* always ACK the EOF, even if we're not done */ + this->m_state_data.receive.r2.eof_cc = static_cast(eof.getConditionCode()); + this->m_flags.rx.send_eof_ack = true; /* defer sending ACK to tick handling */ + + /* only check for complete if EOF with no errors */ + if (this->m_state_data.receive.r2.eof_cc == CONDITION_CODE_NO_ERROR) + { + this->r2Complete(true); /* CFDP_R2_Complete() will change state */ + } + else + { + /* All CFDP CC values directly correspond to a Transaction Status of the same numeric value */ + this->m_engine->setTxnStatus(this, static_cast(static_cast(this->m_state_data.receive.r2.eof_cc))); + this->r2Reset(); + } + } + else + { + /* bad EOF sent? */ + if (ret == Cfdp::Status::REC_PDU_FSIZE_MISMATCH_ERROR) + { + this->r2SetFinTxnStatus(TXN_STATUS_FILE_SIZE_ERROR); + } + else + { + /* can't do anything with this bad EOF, so return to FILEDATA */ + this->m_state_data.receive.sub_state = RX_SUB_STATE_FILEDATA; + } + } + } +} + +void Transaction::r1SubstateRecvFileData(const Fw::Buffer& buffer) { + Status::T ret; + + // Deserialize FileData PDU from buffer + FileDataPdu fd; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = fd.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad file data PDU, reset transaction + this->m_cfdpManager->log_WARNING_LO_FailFileDataPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + this->r1Reset(); + return; + } + + /* got file data PDU? */ + ret = this->m_engine->recvFd(this, fd); + if (ret == Cfdp::Status::SUCCESS) + { + ret = this->rProcessFd(buffer); + } + + if (ret == Cfdp::Status::SUCCESS) + { + /* class 1 digests CRC */ + this->m_crc.update(fd.getData(), fd.getOffset(), + static_cast(fd.getDataSize())); + } + else + { + /* Reset transaction on failure */ + this->r1Reset(); + } +} + +void Transaction::r2SubstateRecvFileData(const Fw::Buffer& buffer) { + Status::T ret; + + // If CRC calculation has started (file reopened in READ mode), ignore late FileData PDUs. + // This can happen if retransmitted FileData arrives after EOF was received and CRC began. + if (this->m_state_data.receive.r2.rx_crc_calc_bytes > 0) + { + // Silently ignore - file is complete and we're calculating CRC + // TODO BPC: do we want a throttled EVR here? + return; + } + + // Deserialize FileData PDU from buffer + FileDataPdu fd; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = fd.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad file data PDU, reset transaction + this->m_cfdpManager->log_WARNING_LO_FailFileDataPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + this->r2Reset(); + return; + } + + /* got file data PDU? */ + ret = this->m_engine->recvFd(this, fd); + if (ret == Cfdp::Status::SUCCESS) + { + ret = this->rProcessFd(buffer); + } + + if (ret == Cfdp::Status::SUCCESS) + { + /* class 2 does CRC at FIN, but track gaps */ + this->m_chunks->chunks.add(fd.getOffset(), static_cast(fd.getDataSize())); + + if (this->m_flags.rx.fd_nak_sent) + { + this->r2Complete(false); /* once nak-retransmit received, start checking for completion at each fd */ + } + + if (!this->m_flags.rx.complete) + { + this->m_engine->armAckTimer(this); /* re-arm ACK timer, since we got data */ + } + + this->m_state_data.receive.r2.acknak_count = 0; + } + else + { + /* Reset transaction on failure */ + this->r2Reset(); + } +} + +void Transaction::r2GapCompute(const Chunk *chunk, NakPdu& nak) { + FW_ASSERT(chunk->size > 0, chunk->size); + + // Calculate segment offsets relative to scope start + FileSize offsetStart = chunk->offset - nak.getScopeStart(); + FileSize offsetEnd = offsetStart + chunk->size; + + // Add segment to NAK PDU (returns false if array is full) + nak.addSegment(offsetStart, offsetEnd); +} + +Status::T Transaction::rSubstateSendNak() { + Status::T status = Cfdp::Status::SUCCESS; + + // Create and initialize NAK PDU + NakPdu nakPdu; + Cfdp::PduDirection direction = DIRECTION_TOWARD_SENDER; + + if (this->m_flags.rx.md_recv) { + // We have metadata, so send NAK with file data gaps + nakPdu.initialize( + direction, + this->getClass(), // transmission mode + this->m_history->peer_eid, // source EID (receiver) + this->m_history->seq_num, // transaction sequence number + this->m_cfdpManager->getLocalEidParam(), // destination EID (sender) + 0, // scope start + 0 // scope end + ); + + // Compute gaps and add segments to NAK PDU + U32 chunkCount = this->m_chunks->chunks.getCount(); + U32 maxChunks = this->m_chunks->chunks.getMaxChunks(); + U32 gapLimit = (chunkCount < maxChunks) ? maxChunks : (maxChunks - 1); + + // For each gap found, add it as a segment to the NAK PDU via callback + U32 gapCount = this->m_chunks->chunks.computeGaps( + static_cast(gapLimit), + this->m_fsize, + 0, + [this, &nakPdu](const Chunk* chunk, void* opaque) { + this->r2GapCompute(chunk, nakPdu); + }, + nullptr); + + if (!gapCount) { + // No gaps left, file reception is complete + this->m_flags.rx.complete = true; + status = Cfdp::Status::SUCCESS; + } else { + // Gaps are present, send the NAK PDU + status = this->m_engine->sendNak(this, nakPdu); + if (status == Cfdp::Status::SUCCESS) { + this->m_flags.rx.fd_nak_sent = true; + this->m_cfdpManager->addSentNakSegmentRequests(this->m_chan_num, gapCount); + } + } + } else { + // Need to send NAK to request metadata PDU again + // Special case: scope start/end and segment[0] all zeros requests metadata + nakPdu.initialize( + direction, + this->getClass(), // transmission mode + this->m_history->peer_eid, // source EID (receiver) + this->m_history->seq_num, // transaction sequence number + this->m_cfdpManager->getLocalEidParam(), // destination EID (sender) + 0, // scope start (special value) + 0 // scope end (special value) + ); + + // Add special segment [0,0] to request metadata + nakPdu.addSegment(0, 0); + + status = this->m_engine->sendNak(this, nakPdu); + } + + return status; +} + +Status::T Transaction::r2CalcCrcChunk() { + U8 buf[CFDP_R2_CRC_CHUNK_SIZE]; + FileSize count_bytes; + FileSize want_offs_size; + FwSizeType read_size; + Os::File::Status fileStatus; + Status::T ret = Cfdp::Status::SUCCESS; + FileSize rx_crc_calc_bytes_per_cycle = 0; + + memset(buf, 0, sizeof(buf)); + + count_bytes = 0; + + // Open file for CRC calculation if needed + if (ret == Cfdp::Status::SUCCESS) { + if (this->m_state_data.receive.r2.rx_crc_calc_bytes == 0) + { + this->m_crc = CFDP::Checksum(0); + + // For Class 2 RX, the file was opened in WRITE mode for receiving FileData PDUs. + // Now we need to READ it for CRC calculation. Close and reopen in READ mode. + if (this->m_fd.isOpen()) + { + this->m_fd.close(); + } + + fileStatus = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_READ); + if (fileStatus != Os::File::OP_OK) + { + this->m_engine->setTxnStatus(this, TXN_STATUS_FILE_SIZE_ERROR); + ret = Cfdp::Status::ERROR; + } else { + // Reset cached position since we just reopened the file + this->m_state_data.receive.cached_pos = 0; + } + } + } + + // Process file in chunks + if (ret == Cfdp::Status::SUCCESS) { + rx_crc_calc_bytes_per_cycle = this->m_cfdpManager->getRxCrcCalcBytesPerCycleParam(); + + while ((ret == Cfdp::Status::SUCCESS) && + (count_bytes < rx_crc_calc_bytes_per_cycle) && + (this->m_state_data.receive.r2.rx_crc_calc_bytes < this->m_fsize)) + { + want_offs_size = this->m_state_data.receive.r2.rx_crc_calc_bytes + sizeof(buf); + + if (want_offs_size > this->m_fsize) + { + read_size = this->m_fsize - this->m_state_data.receive.r2.rx_crc_calc_bytes; + } + else + { + read_size = sizeof(buf); + } + + if (this->m_state_data.receive.cached_pos != this->m_state_data.receive.r2.rx_crc_calc_bytes) + { + fileStatus = this->m_fd.seek(this->m_state_data.receive.r2.rx_crc_calc_bytes, Os::File::SeekType::ABSOLUTE); + if (fileStatus != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxSeekCrcFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_state_data.receive.r2.rx_crc_calc_bytes, + fileStatus); + // this->m_engine->setTxnStatus(this, TXN_STATUS_FILE_SIZE_ERROR); + this->m_cfdpManager->incrementFaultFileSeek(this->m_chan_num); + ret = Cfdp::Status::ERROR; + } + } + + if (ret == Cfdp::Status::SUCCESS) { + FwSizeType expected_read_size = read_size; + fileStatus = this->m_fd.read(buf, read_size, Os::File::WaitType::WAIT); + if (fileStatus != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxReadCrcFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + static_cast(expected_read_size), + static_cast(read_size)); + this->m_engine->setTxnStatus(this, TXN_STATUS_FILE_SIZE_ERROR); + this->m_cfdpManager->incrementFaultFileRead(this->m_chan_num); + ret = Cfdp::Status::ERROR; + } else { + this->m_crc.update(buf, this->m_state_data.receive.r2.rx_crc_calc_bytes, static_cast(read_size)); + this->m_state_data.receive.r2.rx_crc_calc_bytes += static_cast(read_size); + this->m_state_data.receive.cached_pos = this->m_state_data.receive.r2.rx_crc_calc_bytes; + count_bytes += static_cast(read_size); + + // Reset inactivity timer to indicate transaction is actively processing + this->m_engine->armInactTimer(this); + } + } + } + } + + // Check final CRC if all bytes processed + if (ret == Cfdp::Status::SUCCESS) { + if (this->m_state_data.receive.r2.rx_crc_calc_bytes == this->m_fsize) + { + /* all bytes calculated, so now check */ + if (this->rCheckCrc(this->m_state_data.receive.r2.eof_crc) == Cfdp::Status::SUCCESS) + { + /* CRC matched! We are happy */ + this->m_keep = Cfdp::Keep::KEEP; /* save the file */ + + /* set FIN PDU status */ + this->m_state_data.receive.r2.dc = FIN_DELIVERY_CODE_COMPLETE; + this->m_state_data.receive.r2.fs = FIN_FILE_STATUS_RETAINED; + } + else + { + this->r2SetFinTxnStatus(TXN_STATUS_FILE_CHECKSUM_FAILURE); + } + + this->m_flags.com.crc_calc = true; + } else { + // Not all bytes processed yet, return ERROR to signal need to continue + ret = Cfdp::Status::ERROR; + } + } + + return ret; +} + +Status::T Transaction::r2SubstateSendFin() { + Status::T sret; + Status::T ret = Cfdp::Status::SUCCESS; + + if (!TxnStatusIsError(this->m_history->txn_stat) && !this->m_flags.com.crc_calc) + { + /* no error, and haven't checked CRC -- so start checking it */ + if (this->r2CalcCrcChunk()) + { + ret = Cfdp::Status::ERROR; /* signal to caller to re-enter next tick */ + } + } + + if (ret != Cfdp::Status::ERROR) + { + sret = this->m_engine->sendFin(this, this->m_state_data.receive.r2.dc, this->m_state_data.receive.r2.fs, + static_cast(TxnStatusToConditionCode(this->m_history->txn_stat))); + /* CFDP_SendFin does not return SEND_PDU_ERROR */ + FW_ASSERT(sret != Cfdp::Status::SEND_PDU_ERROR); + this->m_state_data.receive.sub_state = + RX_SUB_STATE_CLOSEOUT_SYNC; /* whether or not FIN send successful, ok to transition state */ + if (sret != Cfdp::Status::SUCCESS) + { + ret = Cfdp::Status::ERROR; + } + } + + /* if no message, then try again next time */ + return ret; +} + +void Transaction::r2RecvFinAck(const Fw::Buffer& buffer) { + // Deserialize ACK PDU from buffer + AckPdu ack; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = ack.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad ACK PDU + this->m_cfdpManager->log_WARNING_LO_FailAckPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + this->m_cfdpManager->incrementRecvErrors(this->m_chan_num); + return; + } + + // ACK PDU has been validated during deserialization + // Got fin-ack, so time to close the state + this->r2Reset(); +} + +void Transaction::r2RecvMd(const Fw::Buffer& buffer) { + Fw::String fname; + Os::File::Status fileStatus; + Os::FileSystem::Status fileSysStatus; + bool success = true; + + /* it isn't an error to get another MD PDU, right? */ + if (!this->m_flags.rx.md_recv) + { + /* NOTE: this->m_flags.rx.md_recv always 1 in R1, so this is R2 only */ + /* parse the md PDU. this will overwrite the transaction's history, which contains our filename. so let's + * save the filename in a local buffer so it can be used with moveFile upon successful parsing of + * the md PDU */ + fname = this->m_history->fnames.dst_filename; + + // Deserialize Metadata PDU from buffer + MetadataPdu md; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = md.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad metadata PDU + this->m_cfdpManager->log_WARNING_LO_FailMetadataPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + return; + } + + // PDU validation already done during deserialization + this->m_engine->recvMd(this, md); + + /* successfully obtained md PDU */ + if (this->m_flags.rx.eof_recv) + { + /* EOF was received, so check that md and EOF sizes match */ + if (this->m_state_data.receive.r2.eof_size != this->m_fsize) + { + this->m_cfdpManager->log_WARNING_HI_RxEofMdSizeMismatch( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_fsize, + this->m_state_data.receive.r2.eof_size); + this->m_cfdpManager->incrementFaultFileSizeMismatch(this->m_chan_num); + this->r2SetFinTxnStatus(TXN_STATUS_FILE_SIZE_ERROR); + success = false; + } + } + + if (success) + { + /* close and rename file */ + this->m_fd.close(); + + fileSysStatus = Os::FileSystem::moveFile(fname.toChar(), + this->m_history->fnames.dst_filename.toChar()); + if (fileSysStatus != Os::FileSystem::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxFileRenameFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + fname, + this->m_history->fnames.dst_filename, + fileSysStatus); + this->r2SetFinTxnStatus(TXN_STATUS_FILESTORE_REJECTION); + this->m_cfdpManager->incrementFaultFileRename(this->m_chan_num); + success = false; + } + else + { + // File was successfully renamed, open for writing + fileStatus = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_WRITE); + if (fileStatus != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_RxFileReopenFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_history->fnames.dst_filename, + fileStatus); + this->r2SetFinTxnStatus(TXN_STATUS_FILESTORE_REJECTION); + this->m_cfdpManager->incrementFaultFileOpen(this->m_chan_num); + success = false; + } + } + + if (success) + { + this->m_state_data.receive.cached_pos = 0; /* reset psn due to open */ + this->m_flags.rx.md_recv = true; + this->m_state_data.receive.r2.acknak_count = 0; /* in case part of NAK */ + this->r2Complete(true); /* check for completion now that md is received */ + } + } + } +} + +void Transaction::rSendInactivityEvent() { + this->m_cfdpManager->log_WARNING_HI_RxInactivityTimeout( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_cfdpManager->incrementFaultInactivityTimer(this->m_chan_num); +} + +// ====================================================================== +// Dispatch Methods +// ====================================================================== + +void Transaction::rDispatchRecv(const Fw::Buffer& buffer, + const RSubstateDispatchTable *dispatch, + StateRecvFunc fd_fn) +{ + StateRecvFunc selected_handler; + + FW_ASSERT(this->m_state_data.receive.sub_state < RX_SUB_STATE_NUM_STATES, + this->m_state_data.receive.sub_state, RX_SUB_STATE_NUM_STATES); + + selected_handler = NULL; + + // Peek at PDU type from buffer + Cfdp::PduTypeEnum pduType = Cfdp::peekPduType(buffer); + + // Special handling for file data PDU + if (pduType == Cfdp::T_FILE_DATA) + { + /* For file data PDU, use the provided fd_fn */ + if (!TxnStatusIsError(this->m_history->txn_stat)) + { + selected_handler = fd_fn; + } + } + else if (pduType != Cfdp::T_NONE) + { + // It's a directive PDU - parse header to get directive code + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Cfdp::PduHeader header; + if (header.fromSerialBuffer(sb) == Fw::FW_SERIALIZE_OK) + { + // Read directive code (first byte after header for directive PDUs) + U8 directiveCodeByte; + if (sb.deserializeTo(directiveCodeByte) == Fw::FW_SERIALIZE_OK) + { + FileDirective directiveCode = static_cast(directiveCodeByte); + + if (directiveCode < FILE_DIRECTIVE_INVALID_MAX) + { + /* The CFDP_R_SubstateDispatchTable_t is only used with file directive PDU */ + if (dispatch->state[this->m_state_data.receive.sub_state] != NULL) + { + selected_handler = dispatch->state[this->m_state_data.receive.sub_state]->fdirective[directiveCode]; + } + } + else + { + this->m_cfdpManager->incrementRecvSpurious(this->m_chan_num); + this->m_cfdpManager->log_WARNING_LO_RxInvalidDirectiveCode( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + directiveCodeByte, + this->m_state_data.receive.sub_state); + } + } + } + } + + /* + * NOTE: if no handler is selected, this will drop packets on the floor here. + */ + if (selected_handler != NULL) + { + (this->*selected_handler)(buffer); + } + else + { + this->m_cfdpManager->incrementRecvDropped(this->m_chan_num); + } + +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/TransactionTx.cpp b/Svc/Ccsds/CfdpManager/TransactionTx.cpp new file mode 100644 index 00000000000..806d28e04c7 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/TransactionTx.cpp @@ -0,0 +1,898 @@ +// ====================================================================== +// \title TransactionTx.cpp +// \brief cpp file for CFDP TX Transaction state machine +// +// This file is a port of TX transaction state machine operations from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_cfdp_s.c (send-file transaction state handling routines) +// - cf_cfdp_dispatch.c (TX state machine dispatch functions) +// +// This file contains various state handling routines for +// transactions which are sending a file, as well as dispatch +// functions for TX state machines and top-level transaction dispatch. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ====================================================================== +// TX State Machine - Private Helper (anonymous namespace) +// ====================================================================== + +namespace { + +// Helper to build dispatch tables +FileDirectiveDispatchTable makeFileDirectiveTable( + StateRecvFunc fin, + StateRecvFunc ack, + StateRecvFunc nak +) +{ + FileDirectiveDispatchTable table = {}; + memset(&table, 0, sizeof(table)); + + table.fdirective[FILE_DIRECTIVE_FIN] = fin; + table.fdirective[FILE_DIRECTIVE_ACK] = ack; + table.fdirective[FILE_DIRECTIVE_NAK] = nak; + + return table; +} + +} // anonymous namespace + +// ====================================================================== +// TX State Machine - Public Methods +// ====================================================================== + +void Transaction::s1Recv(const Fw::Buffer& buffer) { + // s1 doesn't need to receive anything + static const SSubstateRecvDispatchTable substate_fns = {{NULL}}; + this->sDispatchRecv(buffer, &substate_fns); +} + +void Transaction::s2Recv(const Fw::Buffer& buffer) { + static const FileDirectiveDispatchTable s2_meta = + makeFileDirectiveTable( + &Transaction::s2EarlyFin, + nullptr, + nullptr + ); + + static const FileDirectiveDispatchTable s2_fd_or_eof = + makeFileDirectiveTable( + &Transaction::s2EarlyFin, + nullptr, + &Transaction::s2Nak + ); + + static const FileDirectiveDispatchTable s2_wait_ack = + makeFileDirectiveTable( + &Transaction::s2Fin, + &Transaction::s2EofAck, + &Transaction::s2NakArm + ); + + static const SSubstateRecvDispatchTable substate_fns = { + { + &s2_meta, /* TX_SUB_STATE_METADATA */ + &s2_fd_or_eof, /* TX_SUB_STATE_FILEDATA */ + &s2_fd_or_eof, /* TX_SUB_STATE_EOF */ + &s2_wait_ack /* TX_SUB_STATE_CLOSEOUT_SYNC */ + } + }; + + this->sDispatchRecv(buffer, &substate_fns); +} + +void Transaction::initTxFile(Class::T cfdp_class, Keep::T keep, U8 chan, U8 priority) +{ + m_chan_num = chan; + m_priority = priority; + m_keep = keep; + m_txn_class = cfdp_class; + m_state = (cfdp_class == Cfdp::Class::CLASS_2) ? TXN_STATE_S2 : TXN_STATE_S1; + m_state_data.send.sub_state = TX_SUB_STATE_METADATA; +} + +void Transaction::s1Tx() { + static const SSubstateSendDispatchTable substate_fns = {{ + &Transaction::sSubstateSendMetadata, // TX_SUB_STATE_METADATA + &Transaction::sSubstateSendFileData, // TX_SUB_STATE_FILEDATA + &Transaction::s1SubstateSendEof, // TX_SUB_STATE_EOF + nullptr // TX_SUB_STATE_CLOSEOUT_SYNC + }}; + + this->sDispatchTransmit(&substate_fns); +} + +void Transaction::s2Tx() { + static const SSubstateSendDispatchTable substate_fns = {{ + &Transaction::sSubstateSendMetadata, // TX_SUB_STATE_METADATA + &Transaction::s2SubstateSendFileData, // TX_SUB_STATE_FILEDATA + &Transaction::s2SubstateSendEof, // TX_SUB_STATE_EOF + nullptr // TX_SUB_STATE_CLOSEOUT_SYNC + }}; + + this->sDispatchTransmit(&substate_fns); +} + +void Transaction::sAckTimerTick() { + U8 ack_limit = 0; + + // note: the ack timer is only ever relevant on class 2 + if (this->m_state != TXN_STATE_S2 || !this->m_flags.com.ack_timer_armed) + { + // nothing to do + return; + } + + if (this->m_ack_timer.getStatus() == Timer::Status::RUNNING) + { + this->m_ack_timer.run(); + } + else if (this->m_state_data.send.sub_state == TX_SUB_STATE_CLOSEOUT_SYNC) + { + // Check limit and handle if needed + ack_limit = this->m_cfdpManager->getAckLimitParam(this->m_chan_num); + if (this->m_state_data.send.s2.acknak_count >= ack_limit) + { + this->m_cfdpManager->log_WARNING_HI_TxAckLimitReached( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_engine->setTxnStatus(this, TXN_STATUS_ACK_LIMIT_NO_EOF); + this->m_cfdpManager->incrementFaultAckLimit(this->m_chan_num); + + // give up on this + this->m_engine->finishTransaction(this, true); + this->m_flags.com.ack_timer_armed = false; + } + else + { + // Increment acknak counter + ++this->m_state_data.send.s2.acknak_count; + + // If the peer sent FIN that is an implicit EOF ack, it is not supposed + // to send it before EOF unless an error occurs, and either way we do not + // re-transmit anything after FIN unless we get another FIN + if (!this->m_flags.tx.eof_ack_recv && !this->m_flags.tx.fin_recv) + { + this->m_flags.tx.send_eof = true; + } + else + { + // no response is pending + this->m_flags.com.ack_timer_armed = false; + } + } + + // reset the ack timer if still waiting on something + if (this->m_flags.com.ack_timer_armed) + { + this->m_engine->armAckTimer(this); + } + } + else + { + // if we are not waiting for anything, why is the ack timer armed? + this->m_flags.com.ack_timer_armed = false; + } +} + +void Transaction::sTick(int *cont /* unused */) { + bool pending_send; + + pending_send = true; // maybe; tbd, will be reset if not + + // at each tick, various timers used by S are checked + // first, check inactivity timer + if (!this->m_flags.com.inactivity_fired) + { + if (this->m_inactivity_timer.getStatus() == Timer::Status::RUNNING) + { + this->m_inactivity_timer.run(); + } + else + { + this->m_flags.com.inactivity_fired = true; + + // HOLD state is the normal path to recycle transaction objects, not an error + // inactivity is abnormal in any other state + if (this->m_state != TXN_STATE_HOLD && this->m_state == TXN_STATE_S2) + { + this->m_cfdpManager->log_WARNING_HI_TxInactivityTimeout( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_engine->setTxnStatus(this, TXN_STATUS_INACTIVITY_DETECTED); + + this->m_cfdpManager->incrementFaultInactivityTimer(this->m_chan_num); + } + } + } + + // tx maintenance: possibly process send_eof, or send_fin_ack + if (this->m_flags.tx.send_eof) + { + if (this->sSendEof() == Cfdp::Status::SUCCESS) + { + this->m_flags.tx.send_eof = false; + } + } + else if (this->m_flags.tx.send_fin_ack) + { + if (this->sSendFinAck() == Cfdp::Status::SUCCESS) + { + this->m_flags.tx.send_fin_ack = false; + } + } + else + { + pending_send = false; + } + + // if the inactivity timer ran out, then there is no sense + // pending for responses for anything. Send out anything + // that we need to send (i.e. the EOF) just in case the sender + // is still listening to us but do not expect any future ACKs + if (this->m_flags.com.inactivity_fired && !pending_send) + { + // the transaction is now recyclable - this means we will + // no longer have a record of this transaction seq. If the sender + // wakes up or if the network delivers severely delayed PDUs at + // some future point, then they will be seen as spurious. They + // will no longer be associable with this transaction at all + this->m_chan->recycleTransaction(this); + + // NOTE: this must be the last thing in here. Do not use txn after this + } + else + { + // transaction still valid so process the ACK timer, if relevant + this->sAckTimerTick(); + } +} + +void Transaction::sTickNak(int *cont) { + bool nakProcessed = false; + Status::T status; + + // Only Class 2 transactions should process NAKs + if (this->m_txn_class == Cfdp::Class::CLASS_2) + { + status = this->sCheckAndRespondNak(&nakProcessed); + if ((status == Cfdp::Status::SUCCESS) && nakProcessed) + { + *cont = 1; // cause dispatcher to re-enter this scheduler cycle + } + } +} + +void Transaction::sCancel() { + if (this->m_state_data.send.sub_state < TX_SUB_STATE_EOF) + { + // if state has not reached TX_SUB_STATE_EOF, then set it to TX_SUB_STATE_EOF now. + this->m_state_data.send.sub_state = TX_SUB_STATE_EOF; + } +} + +// ====================================================================== +// TX State Machine - Private Helper Methods +// ====================================================================== + +Status::T Transaction::sSendEof() { + // note the crc is "finalized" regardless of success or failure of the txn + // this is OK as we still need to put some value into the EOF + if (!this->m_flags.com.crc_calc) + { + // The F' version does not have an equivalent finalize call as it + // - Never stores a partial word internally + // - Never needs to "flush" anything + // - Always accounts for padding at update time + this->m_flags.com.crc_calc = true; + } + return this->m_engine->sendEof(this); +} + +void Transaction::s1SubstateSendEof() { + // set the flag, the EOF is sent by the tick handler + this->m_flags.tx.send_eof = true; + + // In class 1 this is the end of normal operation + // NOTE: this is not always true, as class 1 can request an EOF ack. + // In this case we could change state to CLOSEOUT_SYNC instead and wait, + // but right now we do not request an EOF ack in S1 + this->m_engine->finishTransaction(this, true); +} + +void Transaction::s2SubstateSendEof() { + // set the flag, the EOF is sent by the tick handler + this->m_flags.tx.send_eof = true; + + // wait for remaining responses to close out the state machine + this->m_state_data.send.sub_state = TX_SUB_STATE_CLOSEOUT_SYNC; + + // always move the transaction onto the wait queue now + this->m_chan->dequeueTransaction(this); + this->m_chan->insertSortPrio(this, QueueId::TXW); + + // the ack timer is armed in class 2 only + this->m_engine->armAckTimer(this); +} + +Status::T Transaction::sSendFileData(FileSize foffs, FileSize bytes_to_read, U8 calc_crc, FileSize* bytes_processed) { + FW_ASSERT(bytes_processed != NULL); + *bytes_processed = 0; + + Status::T status = Cfdp::Status::SUCCESS; + + // Local buffer for file data + U8 fileDataBuffer[MaxPduSize]; + + // Create File Data PDU + FileDataPdu fdPdu; + Cfdp::PduDirection direction = DIRECTION_TOWARD_RECEIVER; + + // Calculate maximum data size we can send, accounting for PDU overhead + U32 maxDataCapacity = fdPdu.getMaxFileDataSize(); + + // Limited by: bytes_to_read, outgoing_file_chunk_size, and maxDataCapacity + FileSize outgoing_file_chunk_size = this->m_cfdpManager->getOutgoingFileChunkSizeParam(); + FileSize max_data_bytes = bytes_to_read; + if (max_data_bytes > outgoing_file_chunk_size) { + max_data_bytes = outgoing_file_chunk_size; + } + if (max_data_bytes > maxDataCapacity) { + max_data_bytes = maxDataCapacity; + } + + // Seek to file offset if needed + FwSizeType actual_bytes = max_data_bytes; + if (status == Cfdp::Status::SUCCESS) { + if (this->m_state_data.send.cached_pos != foffs) { + Os::File::Status fileStatus = this->m_fd.seek(foffs, Os::File::SeekType::ABSOLUTE); + if (fileStatus != Os::File::OP_OK) { + status = Cfdp::Status::ERROR; + } + } + } + + // Read file data + if (status == Cfdp::Status::SUCCESS) { + Os::File::Status fileStatus = this->m_fd.read(fileDataBuffer, actual_bytes, Os::File::WaitType::WAIT); + if (fileStatus != Os::File::OP_OK) { + status = Cfdp::Status::ERROR; + } + } + + // Initialize and send PDU + if (status == Cfdp::Status::SUCCESS) { + // File has been read successfully, update cached_pos to reflect new file position + // This MUST be done before attempting to send, so if send fails (throttle/error), + // we don't try to read the same data again on next cycle + this->m_state_data.send.cached_pos += static_cast(actual_bytes); + + fdPdu.initialize( + direction, + this->getClass(), // transmission mode + this->m_cfdpManager->getLocalEidParam(), // source EID + this->m_history->seq_num, // transaction sequence number + this->m_history->peer_eid, // destination EID + foffs, // file offset + static_cast(actual_bytes), // data size + fileDataBuffer // data pointer + ); + + status = this->m_engine->sendFd(this, fdPdu); + } + + // Update CRC and bytes_processed + if (status == Cfdp::Status::SUCCESS) { + + FW_ASSERT((foffs + actual_bytes) <= this->m_fsize, foffs, static_cast(actual_bytes), this->m_fsize); + + if (calc_crc) { + this->m_crc.update(fileDataBuffer, foffs, static_cast(actual_bytes)); + } + + *bytes_processed = static_cast(actual_bytes); + } + + return status; +} + +void Transaction::sSubstateSendFileData() { + FileSize bytes_processed = 0; + Status::T status = this->sSendFileData(this->m_foffs, (this->m_fsize - this->m_foffs), 1, &bytes_processed); + + // When SEND_PDU_NO_BUF_AVAIL_ERROR is returned, it means either: + // 1) The throttle limit (max_outgoing_pdus_per_cycle) was reached, OR + // 2) Buffer allocation failed + // In either case, we should stay in FILEDATA state and retry next cycle. + // This is NOT a file I/O error, so we should NOT transition to EOF. + // We also need to break the cycleTx loop by setting m_chan->m_currentTxn. + if(status == Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR) + { + // Throttle limit or buffer exhaustion - stay in FILEDATA, retry next cycle + // Set m_currentTxn to break the cycleTx loop for this cycle + this->m_chan->setCurrentTxn(this); + } + else if(status != Cfdp::Status::SUCCESS) + { + // IO error -- change state and send EOF + this->m_engine->setTxnStatus(this, TXN_STATUS_FILESTORE_REJECTION); + this->m_state_data.send.sub_state = TX_SUB_STATE_EOF; + } + else if (bytes_processed > 0) + { + this->m_foffs += bytes_processed; + if (this->m_foffs == this->m_fsize) + { + // file is done - transition to EOF state, which will be sent in next loop iteration + this->m_state_data.send.sub_state = TX_SUB_STATE_EOF; + } + } + else + { + // don't care about other cases + } +} + +Status::T Transaction::sCheckAndRespondNak(bool* nakProcessed) { + const Chunk *chunk; + Status::T sret; + Status::T ret = Cfdp::Status::SUCCESS; + FileSize bytes_processed = 0; + + FW_ASSERT(nakProcessed != NULL); + *nakProcessed = false; + + // Class 2 transactions must have had chunks allocated + FW_ASSERT(this->m_chunks != NULL); + + if (this->m_flags.tx.md_need_send) + { + sret = this->m_engine->sendMd(this); + if (sret == Cfdp::Status::SEND_PDU_ERROR) + { + ret = Cfdp::Status::ERROR; // error occurred + } + else + { + if (sret == Cfdp::Status::SUCCESS) + { + this->m_flags.tx.md_need_send = false; + } + // unless SEND_PDU_ERROR, return 1 to keep caller from sending file data + *nakProcessed = true; // nak processed, so don't send filedata + + } + } + else + { + // Get first chunk and process if available + chunk = this->m_chunks->chunks.getFirstChunk(); + if (chunk != nullptr) + { + ret = this->sSendFileData(chunk->offset, chunk->size, 0, &bytes_processed); + if(ret != Cfdp::Status::SUCCESS) + { + // error occurred + ret = Cfdp::Status::ERROR; // error occurred + } + else if (bytes_processed > 0) + { + this->m_chunks->chunks.removeFromFirst(bytes_processed); + *nakProcessed = true; // nak processed, so caller doesn't send file data + } + } + } + + return ret; +} + +void Transaction::s2SubstateSendFileData() { + Status::T status; + bool nakProcessed = false; + + status = this->sCheckAndRespondNak(&nakProcessed); + if (status != Cfdp::Status::SUCCESS) + { + this->m_engine->setTxnStatus(this, TXN_STATUS_NAK_RESPONSE_ERROR); + this->m_flags.tx.send_eof = true; /* do not leave the remote hanging */ + this->m_engine->finishTransaction(this, true); + return; + } + + if (!nakProcessed) + { + this->sSubstateSendFileData(); + } + else + { + // NAK was processed, so do not send filedata + } +} + +void Transaction::sSubstateSendMetadata() { + Status::T status; + Os::File::Status fileStatus; + bool success = true; + + if (false == this->m_fd.isOpen()) + { + fileStatus = this->m_fd.open(this->m_history->fnames.src_filename.toChar(), Os::File::OPEN_READ); + if (fileStatus != Os::File::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_TxFileOpenFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + this->m_history->fnames.src_filename, + fileStatus); + this->m_cfdpManager->incrementFaultFileOpen(this->m_chan_num); + success = false; + } + + if (success) + { + FwSizeType file_size; + fileStatus = this->m_fd.size(file_size); + this->m_fsize = static_cast(file_size); + if (fileStatus != Os::File::Status::OP_OK) + { + this->m_cfdpManager->log_WARNING_HI_TxFileSeekFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + fileStatus); + this->m_cfdpManager->incrementFaultFileSeek(this->m_chan_num); + success = false; + } + else + { + // Check that file size is well formed + FW_ASSERT(this->m_fsize > 0, this->m_fsize); + } + } + } + + if (success) + { + status = this->m_engine->sendMd(this); + if (status == Cfdp::Status::SEND_PDU_ERROR) + { + /* failed to send md */ + this->m_cfdpManager->log_WARNING_HI_TxSendMetadataFailed( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + success = false; + } + else if (status == Cfdp::Status::SUCCESS) + { + /* once metadata is sent, switch to filedata mode */ + this->m_state_data.send.sub_state = TX_SUB_STATE_FILEDATA; + + this->m_cfdpManager->log_ACTIVITY_HI_TxFileTransferStarted( + this->getClass(), + this->m_history->src_eid, + this->m_history->fnames.src_filename, + this->m_history->peer_eid, + this->m_history->fnames.dst_filename, + static_cast(this->m_fsize)); + } + /* if status==Cfdp::Status::SEND_PDU_NO_BUF_AVAIL_ERROR, then try to send md again next cycle */ + /* TODO JMP What if status==Cfdp::Status::ERROR*/ + } + + if (!success) + { + this->m_engine->setTxnStatus(this, TXN_STATUS_FILESTORE_REJECTION); + this->m_engine->finishTransaction(this, true); + } + + // don't need to reset the CRC since its taken care of by reset_cfdp() +} + +Status::T Transaction::sSendFinAck() { + Status::T ret = this->m_engine->sendAck(this, + static_cast(GetTxnStatus(this)), + FILE_DIRECTIVE_FIN, + static_cast(this->m_state_data.send.s2.fin_cc), + this->m_history->peer_eid, this->m_history->seq_num); + return ret; +} + +void Transaction::s2EarlyFin(const Fw::Buffer& buffer) { + // received early fin, so just cancel + this->m_cfdpManager->log_WARNING_HI_TxEarlyFinReceived( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_engine->setTxnStatus(this, TXN_STATUS_EARLY_FIN); + + this->m_state_data.send.sub_state = TX_SUB_STATE_CLOSEOUT_SYNC; + + // otherwise do normal fin processing + this->s2Fin(buffer); +} + +void Transaction::s2Fin(const Fw::Buffer& buffer) { + // Deserialize FIN PDU from buffer + FinPdu fin; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = fin.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad FIN PDU + this->m_cfdpManager->log_WARNING_LO_FailFinPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + return; + } + + if (!this->m_engine->recvFin(this, fin)) + { + // set the CC only on the first time we get the FIN. If this is a dupe + // then re-ack but otherwise ignore it + if (!this->m_flags.tx.fin_recv) + { + + this->m_flags.tx.fin_recv = true; + this->m_state_data.send.s2.fin_cc = static_cast(fin.getConditionCode()); + this->m_state_data.send.s2.acknak_count = 0; // in case retransmits had occurred + + // note this is a no-op unless the status was unset previously + this->m_engine->setTxnStatus(this, static_cast(this->m_state_data.send.s2.fin_cc)); + + // Generally FIN is the last exchange in an S2 transaction, the remote is not supposed + // to send it until after the EOF+ACK. So at this point we stop trying to send anything + // to the peer, regardless of whether we got every ACK we expected. + this->m_engine->finishTransaction(this, true); + } + this->m_flags.tx.send_fin_ack = true; + } +} + +void Transaction::s2Nak(const Fw::Buffer& buffer) { + U8 counter; + U8 bad_sr; + + bad_sr = 0; + + // Deserialize NAK PDU from buffer + NakPdu nak; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = nak.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad NAK PDU + this->m_cfdpManager->log_WARNING_LO_FailNakPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + this->m_cfdpManager->incrementRecvErrors(this->m_chan_num); + return; + } + + // this function is only invoked for NAK PDU types + if (this->m_engine->recvNak(this, nak) == Cfdp::Status::SUCCESS && nak.getNumSegments() > 0) + { + for (counter = 0; counter < nak.getNumSegments(); ++counter) + { + const Cfdp::SegmentRequest& sr = nak.getSegment(counter); + + if (sr.offsetStart == 0 && sr.offsetEnd == 0) + { + // need to re-send metadata PDU + this->m_flags.tx.md_need_send = true; + } + else + { + if (sr.offsetEnd < sr.offsetStart) + { + ++bad_sr; + continue; + } + + // overflow probably won't be an issue + if (sr.offsetEnd > this->m_fsize) + { + ++bad_sr; + continue; + } + + // insert gap data in chunks + this->m_chunks->chunks.add(sr.offsetStart, sr.offsetEnd - sr.offsetStart); + } + } + + this->m_cfdpManager->addRecvNakSegmentRequests(this->m_chan_num, + nak.getNumSegments()); + if (bad_sr) + { + this->m_cfdpManager->log_WARNING_LO_TxInvalidSegmentRequests( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + bad_sr); + } + } + else + { + this->m_cfdpManager->log_WARNING_HI_TxInvalidNakPdu( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + this->m_cfdpManager->incrementRecvErrors(this->m_chan_num); + } +} + +void Transaction::s2NakArm(const Fw::Buffer& buffer) { + this->m_engine->armAckTimer(this); + this->s2Nak(buffer); +} + +void Transaction::s2EofAck(const Fw::Buffer& buffer) { + // Deserialize ACK PDU from buffer + AckPdu ack; + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Fw::SerializeStatus deserStatus = ack.deserializeFrom(sb); + if (deserStatus != Fw::FW_SERIALIZE_OK) { + // Bad ACK PDU + this->m_cfdpManager->log_WARNING_LO_FailAckPduDeserialization(this->getChannelId(), static_cast(deserStatus)); + return; + } + + // ACK PDU has been validated during deserialization + // Check if this is an EOF acknowledgment + if (ack.getDirectiveCode() == FILE_DIRECTIVE_END_OF_FILE) + { + this->m_flags.tx.eof_ack_recv = true; + this->m_flags.com.ack_timer_armed = false; // just wait for FIN now, nothing to re-send + this->m_state_data.send.s2.acknak_count = 0; // in case EOF retransmits had occurred + + // if FIN was also received then we are done (these can come out of order) + if (this->m_flags.tx.fin_recv) + { + this->m_engine->finishTransaction(this, true); + } + } +} + +// ====================================================================== +// Dispatch Methods (ported from cf_cfdp_dispatch.c) +// ====================================================================== + +void Transaction::sDispatchRecv(const Fw::Buffer& buffer, + const SSubstateRecvDispatchTable *dispatch) +{ + const FileDirectiveDispatchTable *substate_tbl; + StateRecvFunc selected_handler; + + FW_ASSERT(this->m_state_data.send.sub_state < TX_SUB_STATE_NUM_STATES, + this->m_state_data.send.sub_state, TX_SUB_STATE_NUM_STATES); + + // Peek at PDU type from buffer + Cfdp::PduTypeEnum pduType = Cfdp::peekPduType(buffer); + + // send state, so we only care about file directive PDU + selected_handler = NULL; + + if (pduType == Cfdp::T_FILE_DATA) + { + this->m_cfdpManager->log_WARNING_LO_TxNonFileDirectivePduReceived( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num); + } + else if (pduType != Cfdp::T_NONE) + { + // It's a directive PDU - parse header to get directive code + Fw::SerialBuffer sb(const_cast(buffer.getData()), buffer.getSize()); + sb.setBuffLen(buffer.getSize()); + + Cfdp::PduHeader header; + if (header.fromSerialBuffer(sb) == Fw::FW_SERIALIZE_OK) + { + // Read directive code (first byte after header for directive PDUs) + U8 directiveCodeByte; + if (sb.deserializeTo(directiveCodeByte) == Fw::FW_SERIALIZE_OK) + { + FileDirective directiveCode = static_cast(directiveCodeByte); + + if (directiveCode < FILE_DIRECTIVE_INVALID_MAX) + { + // This should be silent (no event) if no handler is defined in the table + substate_tbl = dispatch->substate[this->m_state_data.send.sub_state]; + if (substate_tbl != NULL) + { + selected_handler = substate_tbl->fdirective[directiveCode]; + } + } + else + { + this->m_cfdpManager->log_WARNING_LO_TxInvalidDirectiveCode( + this->getClass(), + this->m_history->src_eid, + this->m_history->seq_num, + directiveCodeByte, + this->m_state_data.send.sub_state); + } + } + } + } + + // check that there's a valid function pointer. If there isn't, + // then silently ignore. We may want to discuss if it's worth + // shutting down the whole transaction if a PDU is received + // that doesn't make sense to be received (For example, + // class 1 CFDP receiving a NAK PDU) but for now, we silently + // ignore the received packet and keep chugging along. + if (selected_handler) + { + (this->*selected_handler)(buffer); + } +} + +void Transaction::sDispatchTransmit(const SSubstateSendDispatchTable *dispatch) +{ + StateSendFunc selected_handler; + + selected_handler = dispatch->substate[this->m_state_data.send.sub_state]; + if (selected_handler != NULL) + { + (this->*selected_handler)(); + } +} + +void Transaction::txStateDispatch(const TxnSendDispatchTable *dispatch) +{ + StateSendFunc selected_handler; + + FW_ASSERT(this->m_state < TXN_STATE_INVALID, this->m_state, TXN_STATE_INVALID); + + selected_handler = dispatch->tx[this->m_state]; + if (selected_handler != NULL) + { + (this->*selected_handler)(); + } +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Types/AckPdu.cpp b/Svc/Ccsds/CfdpManager/Types/AckPdu.cpp new file mode 100644 index 00000000000..df7de407875 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/AckPdu.cpp @@ -0,0 +1,172 @@ +// ====================================================================== +// \title AckPdu.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP ACK (Acknowledge) PDU +// ====================================================================== + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void AckPdu::initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileDirective directiveCode, + U8 directiveSubtypeCode, + ConditionCode conditionCode, + AckTxnStatus transactionStatus) { + // Initialize header with T_ACK type + this->m_header.initialize(T_ACK, direction, txmMode, sourceEid, transactionSeq, destEid); + + this->m_directiveCode = directiveCode; + this->m_directiveSubtypeCode = directiveSubtypeCode; + this->m_conditionCode = conditionCode; + this->m_transactionStatus = transactionStatus; +} + +U32 AckPdu::getBufferSize() const { + U32 size = this->m_header.getBufferSize(); + + // Directive code: 1 byte (FILE_DIRECTIVE_ACK) + // Directive and subtype code (bit-packed): 1 byte + // Condition code and transaction status (bit-packed): 1 byte + size += sizeof(U8) + sizeof(U8) + sizeof(U8); + + return size; +} + +Fw::SerializeStatus AckPdu::serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) const { + return this->toSerialBuffer(buffer); +} + +Fw::SerializeStatus AckPdu::deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) { + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(buffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate this is a directive PDU (not file data) + if (this->m_header.m_pduType != PDU_TYPE_DIRECTIVE) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Validate directive code + U8 directiveCode; + status = buffer.deserializeTo(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + if (directiveCode != FILE_DIRECTIVE_ACK) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Now set the type to T_ACK since we've validated it + this->m_header.m_type = T_ACK; + + // Deserialize the ACK body + return this->fromSerialBuffer(buffer); +} + +Fw::SerializeStatus AckPdu::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + FW_ASSERT(this->m_header.m_type == T_ACK); + + // Calculate PDU data length (everything after header) + U32 dataLength = this->getBufferSize() - this->m_header.getBufferSize(); + + // Update header with data length + PduHeader headerCopy = this->m_header; + headerCopy.setPduDataLength(static_cast(dataLength)); + + // Serialize header + Fw::SerializeStatus status = headerCopy.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Directive code (ACK = 6) + U8 directiveCodeByte = static_cast(FILE_DIRECTIVE_ACK); + status = serialBuffer.serializeFrom(directiveCodeByte); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Directive and subtype code (bit-packed into 1 byte) + // Bits 7-4: Directive code being acknowledged (4 bits) + // Bits 3-0: Directive subtype code (4 bits) + U8 directiveAndSubtype = 0; + directiveAndSubtype |= (static_cast(this->m_directiveCode) & 0x0F) << 4; // Bits 7-4 + directiveAndSubtype |= (this->m_directiveSubtypeCode & 0x0F); // Bits 3-0 + + status = serialBuffer.serializeFrom(directiveAndSubtype); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Condition code and transaction status (bit-packed into 1 byte) + // Bits 7-4: Condition code (4 bits) + // Bits 3-2: Spare (0) + // Bits 1-0: Transaction status (2 bits) + U8 ccAndStatus = 0; + ccAndStatus |= (static_cast(this->m_conditionCode) & 0x0F) << 4; // Bits 7-4 + ccAndStatus |= (static_cast(this->m_transactionStatus) & 0x03); // Bits 1-0 + + status = serialBuffer.serializeFrom(ccAndStatus); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus AckPdu::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + FW_ASSERT(this->m_header.m_type == T_ACK); + + // Directive code already read by fromBuffer() + + // Directive and subtype code (packed byte) + U8 directiveAndSubtype; + Fw::SerializeStatus status = serialBuffer.deserializeTo(directiveAndSubtype); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Extract fields from directive and subtype byte: + // Bits 7-4: Directive code being acknowledged + // Bits 3-0: Directive subtype code + U8 directiveCodeVal = (directiveAndSubtype >> 4) & 0x0F; + U8 subtypeCodeVal = directiveAndSubtype & 0x0F; + + this->m_directiveCode = static_cast(directiveCodeVal); + this->m_directiveSubtypeCode = subtypeCodeVal; + + // Condition code and transaction status (packed byte) + U8 ccAndStatus; + status = serialBuffer.deserializeTo(ccAndStatus); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Extract fields from condition code and transaction status byte: + // Bits 7-4: Condition code + // Bits 3-2: Spare + // Bits 1-0: Transaction status + U8 conditionCodeVal = (ccAndStatus >> 4) & 0x0F; + U8 transactionStatusVal = ccAndStatus & 0x03; + + this->m_conditionCode = static_cast(conditionCodeVal); + this->m_transactionStatus = static_cast(transactionStatusVal); + + return Fw::FW_SERIALIZE_OK; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/AckPdu.hpp b/Svc/Ccsds/CfdpManager/Types/AckPdu.hpp new file mode 100644 index 00000000000..3f9b743dd2f --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/AckPdu.hpp @@ -0,0 +1,88 @@ +// ====================================================================== +// \title AckPdu.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP ACK PDU +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_AckPdu_HPP +#define Svc_Ccsds_Cfdp_AckPdu_HPP + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! The type of an ACK PDU +class AckPdu : public PduBase { + private: + //! Directive being acknowledged + FileDirective m_directiveCode; + + //! Directive subtype code + U8 m_directiveSubtypeCode; + + //! Condition code + ConditionCode m_conditionCode; + + //! Transaction status + AckTxnStatus m_transactionStatus; + + public: + //! Constructor + AckPdu() : m_directiveCode(FILE_DIRECTIVE_INVALID_MIN), + m_directiveSubtypeCode(0), + m_conditionCode(CONDITION_CODE_NO_ERROR), + m_transactionStatus(ACK_TXN_STATUS_UNDEFINED) {} + + //! Initialize an ACK PDU + void initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileDirective directiveCode, + U8 directiveSubtypeCode, + ConditionCode conditionCode, + AckTxnStatus transactionStatus); + + //! Compute the buffer size needed + U32 getBufferSize() const override; + + //! Fw::Serializable interface - serialize to buffer + Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override; + + //! Fw::Serializable interface - deserialize from buffer + Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override; + + //! Get this as a Header + const PduHeader& asHeader() const { return this->m_header; } + + //! Get directive code + FileDirective getDirectiveCode() const { return this->m_directiveCode; } + + //! Get directive subtype code + U8 getDirectiveSubtypeCode() const { return this->m_directiveSubtypeCode; } + + //! Get condition code + ConditionCode getConditionCode() const { return this->m_conditionCode; } + + //! Get transaction status + AckTxnStatus getTransactionStatus() const { return this->m_transactionStatus; } + + private: + //! Initialize this AckPdu from a SerialBuffer + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); + + //! Write this AckPdu to a SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_AckPdu_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/CMakeLists.txt b/Svc/Ccsds/CfdpManager/Types/CMakeLists.txt new file mode 100644 index 00000000000..684dfe711e2 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/CMakeLists.txt @@ -0,0 +1,33 @@ +#### +# F Prime CMakeLists.txt: +# +# SOURCES: list of source files (to be compiled) +# AUTOCODER_INPUTS: list of files to be passed to the autocoders +# DEPENDS: list of libraries that this module depends on +# +# More information in the F´ CMake API documentation: +# https://fprime.jpl.nasa.gov/latest/docs/reference/api/cmake/API/ +# +#### + +register_fprime_library( + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/Types.fpp" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/PduHeader.cpp" + "${CMAKE_CURRENT_LIST_DIR}/MetadataPdu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/FileDataPdu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/EofPdu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/FinPdu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/AckPdu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/NakPdu.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Tlv.cpp" + DEPENDS + Svc_Ccsds_Types +) + +### Unit Tests ### +register_fprime_ut( + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/PduTests.cpp" +) diff --git a/Svc/Ccsds/CfdpManager/Types/EofPdu.cpp b/Svc/Ccsds/CfdpManager/Types/EofPdu.cpp new file mode 100644 index 00000000000..f60b3dd466c --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/EofPdu.cpp @@ -0,0 +1,170 @@ +// ====================================================================== +// \title EofPdu.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP EOF PDU +// ====================================================================== + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void EofPdu::initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + ConditionCode conditionCode, + U32 checksum, + FileSize fileSize) { + // Initialize header with T_EOF type + this->m_header.initialize(T_EOF, direction, txmMode, sourceEid, transactionSeq, destEid); + + this->m_conditionCode = conditionCode; + this->m_checksum = checksum; + this->m_fileSize = fileSize; + + // Clear TLV list + this->m_tlvList.clear(); +} + +U32 EofPdu::getBufferSize() const { + U32 size = this->m_header.getBufferSize(); + + // Directive code: 1 byte + // Condition code: 1 byte + // Checksum: 4 bytes (U32) + // File size: sizeof(FileSize) bytes + size += sizeof(U8) + sizeof(U8) + sizeof(U32) + sizeof(FileSize); + + // Add TLV size + size += this->m_tlvList.getEncodedSize(); + + return size; +} + +Fw::SerializeStatus EofPdu::serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) const { + return this->toSerialBuffer(buffer); +} + +Fw::SerializeStatus EofPdu::deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) { + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(buffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate this is a directive PDU (not file data) + if (this->m_header.m_pduType != PDU_TYPE_DIRECTIVE) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Validate directive code + U8 directiveCode; + status = buffer.deserializeTo(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + if (directiveCode != FILE_DIRECTIVE_END_OF_FILE) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Now set the type to T_EOF since we've validated it + this->m_header.m_type = T_EOF; + + // Deserialize the EOF body + return this->fromSerialBuffer(buffer); +} + +Fw::SerializeStatus EofPdu::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + FW_ASSERT(this->m_header.m_type == T_EOF); + + // Calculate PDU data length (everything after header) + U32 dataLength = this->getBufferSize() - this->m_header.getBufferSize(); + + // Update header with data length + PduHeader headerCopy = this->m_header; + headerCopy.setPduDataLength(static_cast(dataLength)); + + // Serialize header + Fw::SerializeStatus status = headerCopy.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Directive code + U8 directiveCode = static_cast(FILE_DIRECTIVE_END_OF_FILE); + status = serialBuffer.serializeFrom(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Condition code + U8 conditionCode = static_cast(this->m_conditionCode); + status = serialBuffer.serializeFrom(conditionCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Checksum (U32) + status = serialBuffer.serializeFrom(this->m_checksum); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // File size (FileSize) + status = serialBuffer.serializeFrom(this->m_fileSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize TLVs (if any) + status = this->m_tlvList.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus EofPdu::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + FW_ASSERT(this->m_header.m_type == T_EOF); + + // Directive code already read by union wrapper + + // Condition code + U8 conditionCodeVal; + Fw::SerializeStatus status = serialBuffer.deserializeTo(conditionCodeVal); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + this->m_conditionCode = static_cast(conditionCodeVal); + + // Checksum + status = serialBuffer.deserializeTo(this->m_checksum); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // File size + status = serialBuffer.deserializeTo(this->m_fileSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Deserialize TLVs (consumes rest of buffer) + status = this->m_tlvList.fromSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/EofPdu.hpp b/Svc/Ccsds/CfdpManager/Types/EofPdu.hpp new file mode 100644 index 00000000000..bcc3e99c6e3 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/EofPdu.hpp @@ -0,0 +1,95 @@ +// ====================================================================== +// \title EofPdu.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP EOF PDU +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_EofPdu_HPP +#define Svc_Ccsds_Cfdp_EofPdu_HPP + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! The type of an EOF PDU +class EofPdu : public PduBase { + private: + //! Condition code + ConditionCode m_conditionCode; + + //! File checksum + U32 m_checksum; + + //! File size + FileSize m_fileSize; + + //! TLV list (optional) + TlvList m_tlvList; + + public: + //! Constructor + EofPdu() : m_conditionCode(CONDITION_CODE_NO_ERROR), m_checksum(0), m_fileSize(0) {} + + //! Initialize an EOF PDU + void initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + ConditionCode conditionCode, + U32 checksum, + FileSize fileSize); + + //! Compute the buffer size needed + U32 getBufferSize() const override; + + //! Fw::Serializable interface - serialize to buffer + Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override; + + //! Fw::Serializable interface - deserialize from buffer + Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override; + + //! Get this as a Header + const PduHeader& asHeader() const { return this->m_header; } + + //! Get condition code + ConditionCode getConditionCode() const { return this->m_conditionCode; } + + //! Get checksum + U32 getChecksum() const { return this->m_checksum; } + + //! Get file size + FileSize getFileSize() const { return this->m_fileSize; } + + //! Get directive code + FileDirective getDirectiveCode() const { return FILE_DIRECTIVE_END_OF_FILE; } + + //! Add a TLV to this EOF PDU + //! @return true if added successfully, false if list is full + bool appendTlv(const Tlv& tlv) { return this->m_tlvList.appendTlv(tlv); } + + //! Get TLV list + const TlvList& getTlvList() const { return this->m_tlvList; } + + //! Get number of TLVs + U8 getNumTlv() const { return this->m_tlvList.getNumTlv(); } + + private: + //! Initialize this EofPdu from a SerialBuffer + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); + + //! Write this EofPdu to a SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_EofPdu_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/FileDataPdu.cpp b/Svc/Ccsds/CfdpManager/Types/FileDataPdu.cpp new file mode 100644 index 00000000000..eae7b87e0cc --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/FileDataPdu.cpp @@ -0,0 +1,195 @@ +// ====================================================================== +// \title FileDataPdu.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP File Data PDU +// ====================================================================== + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void FileDataPdu::initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileSize offset, + U16 dataSize, + const U8* data) { + // Initialize header with T_FILE_DATA type + this->m_header.initialize(T_FILE_DATA, direction, txmMode, sourceEid, transactionSeq, destEid); + + this->m_offset = offset; + this->m_dataSize = dataSize; + this->m_data = data; +} + +U32 FileDataPdu::getBufferSize() const { + U32 size = this->m_header.getBufferSize(); + + // Offset field size depends on large file flag + if (this->m_header.m_largeFileFlag == LARGE_FILE_64_BIT) { + size += sizeof(U64); // 8-byte offset + } else { + size += sizeof(U32); // 4-byte offset + } + + size += this->m_dataSize; // actual data + return size; +} + +U32 FileDataPdu::getMaxFileDataSize() { + U32 size = this->m_header.getBufferSize(); + + // Offset field size depends on large file flag + if (this->m_header.m_largeFileFlag == LARGE_FILE_64_BIT) { + size += sizeof(U64); // 8-byte offset + } else { + size += sizeof(U32); // 4-byte offset + } + + return MaxPduSize - size; +} + +Fw::SerializeStatus FileDataPdu::toBuffer(Fw::Buffer& buffer) const { + Fw::SerialBuffer serialBuffer(buffer.getData(), buffer.getSize()); + Fw::SerializeStatus status = this->toSerialBuffer(serialBuffer); + if (status == Fw::FW_SERIALIZE_OK) { + buffer.setSize(serialBuffer.getSize()); + } + return status; +} + +Fw::SerializeStatus FileDataPdu::fromBuffer(const Fw::Buffer& buffer) { + // Create SerialBuffer from Buffer + Fw::SerialBuffer serialBuffer(const_cast(buffer).getData(), + const_cast(buffer).getSize()); + serialBuffer.fill(); + + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate this is a file data PDU + if (this->m_header.m_pduType != PDU_TYPE_FILE_DATA) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Set the type to T_FILE_DATA since we've validated it + this->m_header.m_type = T_FILE_DATA; + + // Deserialize the file data body + return this->fromSerialBuffer(serialBuffer); +} + +Fw::SerializeStatus FileDataPdu::toSerialBuffer(Fw::SerialBuffer& serialBuffer) const { + FW_ASSERT(this->m_header.m_type == T_FILE_DATA); + + // Calculate PDU data length (everything after header) + U32 dataLength = this->getBufferSize() - this->m_header.getBufferSize(); + + // Update header with data length + PduHeader headerCopy = this->m_header; + headerCopy.setPduDataLength(static_cast(dataLength)); + + // Serialize header + Fw::SerializeStatus status = headerCopy.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize offset - size depends on large file flag + if (this->m_header.m_largeFileFlag == LARGE_FILE_64_BIT) { + // Serialize as 8 bytes (64-bit) + U64 offset64 = this->m_offset; + status = serialBuffer.serializeFrom(offset64); + } else { + // Serialize as 4 bytes (32-bit) + U32 offset32 = static_cast(this->m_offset); + status = serialBuffer.serializeFrom(offset32); + } + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize file data (only if there is data to serialize) + if (this->m_dataSize > 0) { + status = serialBuffer.pushBytes(this->m_data, this->m_dataSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus FileDataPdu::fromSerialBuffer(Fw::SerialBuffer& serialBuffer) { + FW_ASSERT(this->m_header.m_type == T_FILE_DATA); + + // Deserialize offset - size depends on large file flag + Fw::SerializeStatus status; + U8 offsetSize; + status = serialBuffer.deserializeTo(this->m_offset); + offsetSize = sizeof(this->m_offset); + + // Calculate remaining data size based on header's PDU data length + U16 pduDataLength = this->m_header.getPduDataLength(); + this->m_dataSize = static_cast(pduDataLength - offsetSize); // minus offset size + + // Point to the data in the buffer (zero-copy) + this->m_data = serialBuffer.getBuffAddrLeft(); + + // Validate we have enough bytes + if (serialBuffer.getDeserializeSizeLeft() < this->m_dataSize) { + return Fw::FW_DESERIALIZE_SIZE_MISMATCH; + } + + // Advance the buffer pointer + U8 tempBuf[1]; // Dummy buffer for validation + for (U16 i = 0; i < this->m_dataSize; ++i) { + status = serialBuffer.popBytes(tempBuf, 1); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus FileDataPdu::serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) const { + // Cast to SerialBuffer and delegate to toSerialBuffer + Fw::SerialBuffer* serialBuffer = dynamic_cast(&buffer); + if (serialBuffer == nullptr) { + return Fw::FW_SERIALIZE_FORMAT_ERROR; + } + return this->toSerialBuffer(*serialBuffer); +} + +Fw::SerializeStatus FileDataPdu::deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) { + // Cast to SerialBuffer and delegate to fromSerialBuffer + Fw::SerialBuffer* serialBuffer = dynamic_cast(&buffer); + if (serialBuffer == nullptr) { + return Fw::FW_DESERIALIZE_FORMAT_ERROR; + } + + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(*serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Deserialize the file data body + return this->fromSerialBuffer(*serialBuffer); +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/FileDataPdu.hpp b/Svc/Ccsds/CfdpManager/Types/FileDataPdu.hpp new file mode 100644 index 00000000000..7ac869ae1df --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/FileDataPdu.hpp @@ -0,0 +1,89 @@ +// ====================================================================== +// \title FileDataPdu.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP File Data PDU +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_FileDataPdu_HPP +#define Svc_Ccsds_Cfdp_FileDataPdu_HPP + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! The type of a File Data PDU +class FileDataPdu : public PduBase { + private: + //! File offset + FileSize m_offset; + + //! Data size + U16 m_dataSize; + + //! Pointer to file data + const U8* m_data; + + public: + //! Constructor + FileDataPdu() : m_offset(0), m_dataSize(0), m_data(nullptr) {} + + //! Initialize a File Data PDU + void initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileSize offset, + U16 dataSize, + const U8* data); + + //! Compute the buffer size needed + U32 getBufferSize() const override; + + //! Convert this FileDataPdu to a Buffer + Fw::SerializeStatus toBuffer(Fw::Buffer& buffer) const; + + //! Initialize this FileDataPdu from a Buffer + Fw::SerializeStatus fromBuffer(const Fw::Buffer& buffer); + + //! Fw::Serializable interface - serialize to buffer + Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override; + + //! Fw::Serializable interface - deserialize from buffer + Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override; + + //! Get this as a Header + const PduHeader& asHeader() const { return this->m_header; } + + //! Get the file offset + FileSize getOffset() const { return this->m_offset; } + + //! Get the data size + U16 getDataSize() const { return this->m_dataSize; } + + //! Get the data pointer + const U8* getData() const { return this->m_data; } + + //! Calculate maximum file data payload size + //! @return Maximum number of data bytes that can fit in a File Data PDU + U32 getMaxFileDataSize(); + + private: + //! Initialize this FileDataPdu from a SerialBuffer + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBuffer& serialBuffer); + + //! Write this FileDataPdu to a SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBuffer& serialBuffer) const; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_FileDataPdu_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/FinPdu.cpp b/Svc/Ccsds/CfdpManager/Types/FinPdu.cpp new file mode 100644 index 00000000000..05b76e857d7 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/FinPdu.cpp @@ -0,0 +1,164 @@ +// ====================================================================== +// \title FinPdu.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP FIN (Finished) PDU +// ====================================================================== + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void FinPdu::initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + ConditionCode conditionCode, + FinDeliveryCode deliveryCode, + FinFileStatus fileStatus) { + // Initialize header with T_FIN type + this->m_header.initialize(T_FIN, direction, txmMode, sourceEid, transactionSeq, destEid); + + this->m_conditionCode = conditionCode; + this->m_deliveryCode = deliveryCode; + this->m_fileStatus = fileStatus; + + // Clear TLV list + this->m_tlvList.clear(); +} + +U32 FinPdu::getBufferSize() const { + U32 size = this->m_header.getBufferSize(); + + // Directive code: 1 byte + // Flags: 1 byte (condition code, delivery code, file status all packed) + size += sizeof(U8) + sizeof(U8); + + // Add TLV size + size += this->m_tlvList.getEncodedSize(); + + return size; +} + +Fw::SerializeStatus FinPdu::serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) const { + return this->toSerialBuffer(buffer); +} + +Fw::SerializeStatus FinPdu::deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) { + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(buffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate this is a directive PDU (not file data) + if (this->m_header.m_pduType != PDU_TYPE_DIRECTIVE) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Validate directive code + U8 directiveCode; + status = buffer.deserializeTo(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + if (directiveCode != FILE_DIRECTIVE_FIN) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Now set the type to T_FIN since we've validated it + this->m_header.m_type = T_FIN; + + // Deserialize the FIN body + return this->fromSerialBuffer(buffer); +} + +Fw::SerializeStatus FinPdu::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + FW_ASSERT(this->m_header.m_type == T_FIN); + + // Calculate PDU data length (everything after header) + U32 dataLength = this->getBufferSize() - this->m_header.getBufferSize(); + + // Update header with data length + PduHeader headerCopy = this->m_header; + headerCopy.setPduDataLength(static_cast(dataLength)); + + // Serialize header + Fw::SerializeStatus status = headerCopy.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Directive code (FIN = 5) + U8 directiveCode = static_cast(FILE_DIRECTIVE_FIN); + status = serialBuffer.serializeFrom(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Flags byte: condition code, delivery code, and file status packed together + // Bits 7-4: Condition code (4 bits) + // Bit 3: Spare (0) + // Bit 2: Delivery code (1 bit) + // Bits 1-0: File status (2 bits) + U8 flags = 0; + flags |= (static_cast(this->m_conditionCode) & 0x0F) << 4; // Bits 7-4 + flags |= (static_cast(this->m_deliveryCode) & 0x01) << 2; // Bit 2 + flags |= (static_cast(this->m_fileStatus) & 0x03); // Bits 1-0 + + status = serialBuffer.serializeFrom(flags); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize TLVs (if any) + status = this->m_tlvList.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus FinPdu::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + FW_ASSERT(this->m_header.m_type == T_FIN); + + // Directive code already read by fromBuffer() + + // Flags byte contains: condition code, delivery code, and file status + U8 flags; + Fw::SerializeStatus status = serialBuffer.deserializeTo(flags); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Extract fields from flags byte: + // Bits 7-4: Condition code (4 bits) + // Bit 3: Spare + // Bit 2: Delivery code (1 bit) + // Bits 1-0: File status (2 bits) + U8 conditionCodeVal = (flags >> 4) & 0x0F; + U8 deliveryCodeVal = (flags >> 2) & 0x01; + U8 fileStatusVal = flags & 0x03; + + this->m_conditionCode = static_cast(conditionCodeVal); + this->m_deliveryCode = static_cast(deliveryCodeVal); + this->m_fileStatus = static_cast(fileStatusVal); + + // Deserialize TLVs (consumes rest of buffer) + status = this->m_tlvList.fromSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/FinPdu.hpp b/Svc/Ccsds/CfdpManager/Types/FinPdu.hpp new file mode 100644 index 00000000000..8a36ccb2d4d --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/FinPdu.hpp @@ -0,0 +1,97 @@ +// ====================================================================== +// \title FinPdu.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP Finished PDU +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_FinPdu_HPP +#define Svc_Ccsds_Cfdp_FinPdu_HPP + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! The type of a Finished PDU +class FinPdu : public PduBase { + private: + //! Condition code + ConditionCode m_conditionCode; + + //! Delivery code + FinDeliveryCode m_deliveryCode; + + //! File status + FinFileStatus m_fileStatus; + + //! TLV list (optional) + TlvList m_tlvList; + + public: + //! Constructor + FinPdu() : m_conditionCode(CONDITION_CODE_NO_ERROR), + m_deliveryCode(FIN_DELIVERY_CODE_COMPLETE), + m_fileStatus(FIN_FILE_STATUS_RETAINED) {} + + //! Initialize a Finished PDU + void initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + ConditionCode conditionCode, + FinDeliveryCode deliveryCode, + FinFileStatus fileStatus); + + //! Compute the buffer size needed + U32 getBufferSize() const override; + + //! Fw::Serializable interface - serialize to buffer + Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override; + + //! Fw::Serializable interface - deserialize from buffer + Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override; + + //! Get this as a Header + const PduHeader& asHeader() const { return this->m_header; } + + //! Get condition code + ConditionCode getConditionCode() const { return this->m_conditionCode; } + + //! Get delivery code + FinDeliveryCode getDeliveryCode() const { return this->m_deliveryCode; } + + //! Get file status + FinFileStatus getFileStatus() const { return this->m_fileStatus; } + + //! Get directive code + FileDirective getDirectiveCode() const { return FILE_DIRECTIVE_FIN; } + + //! Add a TLV to this FIN PDU + //! @return true if added successfully, false if list is full + bool appendTlv(const Tlv& tlv) { return this->m_tlvList.appendTlv(tlv); } + + //! Get TLV list + const TlvList& getTlvList() const { return this->m_tlvList; } + + //! Get number of TLVs + U8 getNumTlv() const { return this->m_tlvList.getNumTlv(); } + + private: + //! Initialize this FinPdu from a SerialBuffer + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); + + //! Write this FinPdu to a SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_FinPdu_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/MetadataPdu.cpp b/Svc/Ccsds/CfdpManager/Types/MetadataPdu.cpp new file mode 100644 index 00000000000..91af2170261 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/MetadataPdu.cpp @@ -0,0 +1,256 @@ +// ====================================================================== +// \title MetadataPdu.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP Metadata PDU +// ====================================================================== + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void MetadataPdu::initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileSize fileSize, + const Fw::String& sourceFilename, + const Fw::String& destFilename, + ChecksumType checksumType, + U8 closureRequested) { + this->m_header.initialize(T_METADATA, direction, txmMode, sourceEid, transactionSeq, destEid); + + this->m_fileSize = fileSize; + + // Enforce MaxFilePathSize for source filename + FwSizeType srcLen = sourceFilename.length(); + FW_ASSERT(srcLen <= MaxFilePathSize, static_cast(srcLen), MaxFilePathSize); + this->m_sourceFilename = sourceFilename; + + // Enforce MaxFilePathSize for destination filename + FwSizeType dstLen = destFilename.length(); + FW_ASSERT(dstLen <= MaxFilePathSize, static_cast(dstLen), MaxFilePathSize); + this->m_destFilename = destFilename; + + this->m_checksumType = checksumType; + this->m_closureRequested = closureRequested; +} + +U32 MetadataPdu::getBufferSize() const { + U32 size = this->m_header.getBufferSize(); + + // Directive code: 1 byte + // Segmentation control byte (includes closure requested and checksum type): 1 byte + // File size: variable + size += sizeof(U8) + sizeof(U8) + sizeof(FileSize); + + // Source filename LV: length(1) + value(n) + size += 1 + static_cast(this->m_sourceFilename.length()); + + // Dest filename LV: length(1) + value(n) + size += 1 + static_cast(this->m_destFilename.length()); + + return size; +} + +Fw::SerializeStatus MetadataPdu::serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) const { + return this->toSerialBuffer(buffer); +} + +Fw::SerializeStatus MetadataPdu::deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) { + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(buffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate this is a directive PDU (not file data) + if (this->m_header.m_pduType != PDU_TYPE_DIRECTIVE) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Validate directive code + U8 directiveCode; + status = buffer.deserializeTo(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + if (directiveCode != FILE_DIRECTIVE_METADATA) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Now set the type to T_METADATA since we've validated it + this->m_header.m_type = T_METADATA; + + // Deserialize the metadata body + return this->fromSerialBuffer(buffer); +} + +Fw::SerializeStatus MetadataPdu::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + FW_ASSERT(this->m_header.m_type == T_METADATA); + + // Calculate PDU data length (everything after header) + U32 dataLength = this->getBufferSize() - this->m_header.getBufferSize(); + + // Update header with data length + PduHeader headerCopy = this->m_header; + headerCopy.setPduDataLength(static_cast(dataLength)); + + // Serialize header + Fw::SerializeStatus status = headerCopy.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Directive code (METADATA = 7) + U8 directiveCode = static_cast(FILE_DIRECTIVE_METADATA); + status = serialBuffer.serializeFrom(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Segmentation control byte + // bit 7: closure_requested + // bits 6-4: reserved (000b) + // bits 3-0: checksum_type + U8 segmentationControl = 0; + segmentationControl |= (this->m_closureRequested & 0x01) << 7; + segmentationControl |= (static_cast(this->m_checksumType) & 0x0F); + + status = serialBuffer.serializeFrom(segmentationControl); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // File size (FileSize) + status = serialBuffer.serializeFrom(this->m_fileSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Source filename LV + U8 sourceFilenameLength = static_cast(this->m_sourceFilename.length()); + status = serialBuffer.serializeFrom(sourceFilenameLength); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize source filename bytes without length prefix + status = serialBuffer.serializeFrom( + reinterpret_cast(this->m_sourceFilename.toChar()), + sourceFilenameLength, + Fw::Serialization::OMIT_LENGTH); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Destination filename LV + U8 destFilenameLength = static_cast(this->m_destFilename.length()); + status = serialBuffer.serializeFrom(destFilenameLength); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize dest filename bytes without length prefix + status = serialBuffer.serializeFrom( + reinterpret_cast(this->m_destFilename.toChar()), + destFilenameLength, + Fw::Serialization::OMIT_LENGTH); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus MetadataPdu::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + FW_ASSERT(this->m_header.m_type == T_METADATA); + + // Directive code already read by union wrapper + + // Segmentation control byte + U8 segmentationControl; + Fw::SerializeStatus status = serialBuffer.deserializeTo(segmentationControl); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + this->m_closureRequested = (segmentationControl >> 7) & 0x01; + U8 checksumTypeVal = segmentationControl & 0x0F; + this->m_checksumType = static_cast(checksumTypeVal); + + // File size + status = serialBuffer.deserializeTo(this->m_fileSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Source filename LV + U8 sourceFilenameLength; + status = serialBuffer.deserializeTo(sourceFilenameLength); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate filename length against MaxFilePathSize + if (sourceFilenameLength > MaxFilePathSize) { + return Fw::FW_DESERIALIZE_SIZE_MISMATCH; + } + + // Validate filename is not empty + if (sourceFilenameLength == 0) { + return Fw::FW_DESERIALIZE_SIZE_MISMATCH; + } + + // Read filename into temporary buffer + char sourceFilenameBuffer[MaxFilePathSize + 1]; + FwSizeType actualLength = sourceFilenameLength; + status = serialBuffer.deserializeTo(reinterpret_cast(sourceFilenameBuffer), actualLength, Fw::Serialization::OMIT_LENGTH); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + // Null-terminate before assigning to Fw::String + sourceFilenameBuffer[sourceFilenameLength] = '\0'; + this->m_sourceFilename = sourceFilenameBuffer; + + // Destination filename LV + U8 destFilenameLength; + status = serialBuffer.deserializeTo(destFilenameLength); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate filename length against MaxFilePathSize + if (destFilenameLength > MaxFilePathSize) { + return Fw::FW_DESERIALIZE_SIZE_MISMATCH; + } + + // Validate filename is not empty + if (destFilenameLength == 0) { + return Fw::FW_DESERIALIZE_SIZE_MISMATCH; + } + + // Read filename into temporary buffer + char destFilenameBuffer[MaxFilePathSize + 1]; + actualLength = destFilenameLength; + status = serialBuffer.deserializeTo(reinterpret_cast(destFilenameBuffer), actualLength, Fw::Serialization::OMIT_LENGTH); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + // Null-terminate before assigning to Fw::String + destFilenameBuffer[destFilenameLength] = '\0'; + this->m_destFilename = destFilenameBuffer; + + return Fw::FW_SERIALIZE_OK; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/MetadataPdu.hpp b/Svc/Ccsds/CfdpManager/Types/MetadataPdu.hpp new file mode 100644 index 00000000000..4eb3a0d3197 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/MetadataPdu.hpp @@ -0,0 +1,97 @@ +// ====================================================================== +// \title MetadataPdu.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP Metadata PDU +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_MetadataPdu_HPP +#define Svc_Ccsds_Cfdp_MetadataPdu_HPP + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! The type of a Metadata PDU +class MetadataPdu : public PduBase { + private: + //! Closure requested flag + U8 m_closureRequested; + + //! Checksum type + ChecksumType m_checksumType; + + //! File size + FileSize m_fileSize; + + //! Source filename + Fw::String m_sourceFilename; + + //! Destination filename + Fw::String m_destFilename; + + public: + //! Constructor + MetadataPdu() : m_sourceFilename(""), m_destFilename("") {} + + //! Initialize a Metadata PDU + void initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileSize fileSize, + const Fw::String& sourceFilename, + const Fw::String& destFilename, + ChecksumType checksumType, + U8 closureRequested); + + //! Compute the buffer size needed + U32 getBufferSize() const override; + + //! Fw::Serializable interface - serialize to buffer + Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override; + + //! Fw::Serializable interface - deserialize from buffer + Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override; + + //! Get this as a Header + const PduHeader& asHeader() const { return this->m_header; } + + //! Get the file size + FileSize getFileSize() const { return this->m_fileSize; } + + //! Get the source filename + const Fw::String& getSourceFilename() const { return this->m_sourceFilename; } + + //! Get the destination filename + const Fw::String& getDestFilename() const { return this->m_destFilename; } + + //! Get checksum type + ChecksumType getChecksumType() const { return this->m_checksumType; } + + //! Get closure requested flag + U8 getClosureRequested() const { return this->m_closureRequested; } + + //! Get directive code + FileDirective getDirectiveCode() const { return FILE_DIRECTIVE_METADATA; } + + private: + //! Initialize this MetadataPdu from a SerialBufferBase + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); + + //! Write this MetadataPdu to a SerialBufferBase + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_MetadataPdu_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/NakPdu.cpp b/Svc/Ccsds/CfdpManager/Types/NakPdu.cpp new file mode 100644 index 00000000000..c0f42dcd1f9 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/NakPdu.cpp @@ -0,0 +1,193 @@ +// ====================================================================== +// \title NakPdu.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP NAK (Negative Acknowledge) PDU +// ====================================================================== + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void NakPdu::initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileSize scopeStart, + FileSize scopeEnd) { + // Initialize header with T_NAK type + this->m_header.initialize(T_NAK, direction, txmMode, sourceEid, transactionSeq, destEid); + + this->m_scopeStart = scopeStart; + this->m_scopeEnd = scopeEnd; + this->m_numSegments = 0; +} + +bool NakPdu::addSegment(FileSize offsetStart, FileSize offsetEnd) { + if (this->m_numSegments >= CFDP_NAK_MAX_SEGMENTS) { + return false; + } + this->m_segments[this->m_numSegments].offsetStart = offsetStart; + this->m_segments[this->m_numSegments].offsetEnd = offsetEnd; + this->m_numSegments++; + return true; +} + +void NakPdu::clearSegments() { + this->m_numSegments = 0; +} + +U32 NakPdu::getBufferSize() const { + U32 size = this->m_header.getBufferSize(); + + // Directive code: 1 byte (FILE_DIRECTIVE_NAK) + // Scope start: sizeof(FileSize) bytes + // Scope end: sizeof(FileSize) bytes + // Segment requests: m_numSegments * (2 * sizeof(FileSize)) bytes + size += static_cast(sizeof(U8) + sizeof(FileSize) + sizeof(FileSize)); + size += static_cast(this->m_numSegments * (sizeof(FileSize) + sizeof(FileSize))); + + return size; +} + +Fw::SerializeStatus NakPdu::serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) const { + return this->toSerialBuffer(buffer); +} + +Fw::SerializeStatus NakPdu::deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode) { + // Deserialize header first + Fw::SerializeStatus status = this->m_header.fromSerialBuffer(buffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Validate this is a directive PDU (not file data) + if (this->m_header.m_pduType != PDU_TYPE_DIRECTIVE) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Validate directive code + U8 directiveCode; + status = buffer.deserializeTo(directiveCode); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + if (directiveCode != FILE_DIRECTIVE_NAK) { + return Fw::FW_DESERIALIZE_TYPE_MISMATCH; + } + + // Now set the type to T_NAK since we've validated it + this->m_header.m_type = T_NAK; + + // Deserialize the NAK body + return this->fromSerialBuffer(buffer); +} + +Fw::SerializeStatus NakPdu::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + FW_ASSERT(this->m_header.m_type == T_NAK); + + // Calculate PDU data length (everything after header) + U32 dataLength = this->getBufferSize() - this->m_header.getBufferSize(); + + // Update header with data length + PduHeader headerCopy = this->m_header; + headerCopy.setPduDataLength(static_cast(dataLength)); + + // Serialize header + Fw::SerializeStatus status = headerCopy.toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Directive code (NAK = 8) + U8 directiveCodeByte = static_cast(FILE_DIRECTIVE_NAK); + status = serialBuffer.serializeFrom(directiveCodeByte); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Scope start (file offset) + status = serialBuffer.serializeFrom(this->m_scopeStart); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Scope end (file offset) + status = serialBuffer.serializeFrom(this->m_scopeEnd); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize segment requests + for (U8 i = 0; i < this->m_numSegments; i++) { + // Segment start offset + status = serialBuffer.serializeFrom(this->m_segments[i].offsetStart); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Segment end offset + status = serialBuffer.serializeFrom(this->m_segments[i].offsetEnd); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus NakPdu::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + FW_ASSERT(this->m_header.m_type == T_NAK); + + // Directive code already read by fromBuffer() + + // Scope start (file offset) + Fw::SerializeStatus status = serialBuffer.deserializeTo(this->m_scopeStart); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Scope end (file offset) + status = serialBuffer.deserializeTo(this->m_scopeEnd); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Calculate number of segment requests from remaining buffer size + // Each segment is 2 * sizeof(FileSize) bytes + Fw::Serializable::SizeType remainingBytes = serialBuffer.getDeserializeSizeLeft(); + U32 segmentSize = sizeof(FileSize) + sizeof(FileSize); + U32 numSegsCalculated = static_cast(remainingBytes / segmentSize); + this->m_numSegments = static_cast(numSegsCalculated); + + // Limit to max segments + if (this->m_numSegments > CFDP_NAK_MAX_SEGMENTS) { + this->m_numSegments = CFDP_NAK_MAX_SEGMENTS; + } + + // Deserialize segment requests + for (U8 i = 0; i < this->m_numSegments; i++) { + // Segment start offset + status = serialBuffer.deserializeTo(this->m_segments[i].offsetStart); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Segment end offset + status = serialBuffer.deserializeTo(this->m_segments[i].offsetEnd); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + + return Fw::FW_SERIALIZE_OK; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/NakPdu.hpp b/Svc/Ccsds/CfdpManager/Types/NakPdu.hpp new file mode 100644 index 00000000000..b651b62853d --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/NakPdu.hpp @@ -0,0 +1,99 @@ +// ====================================================================== +// \title NakPdu.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP NAK PDU +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_NakPdu_HPP +#define Svc_Ccsds_Cfdp_NakPdu_HPP + +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! Segment request structure for NAK PDU +struct SegmentRequest { + FileSize offsetStart; //!< Start offset of missing data + FileSize offsetEnd; //!< End offset of missing data +}; + +//! The type of a NAK PDU +class NakPdu : public PduBase { + private: + //! Scope start offset + FileSize m_scopeStart; + + //! Scope end offset + FileSize m_scopeEnd; + + //! Number of segment requests + U8 m_numSegments; + + //! Segment requests array (max CFDP_NAK_MAX_SEGMENTS = 58) + SegmentRequest m_segments[58]; + + public: + //! Constructor + NakPdu() : m_scopeStart(0), m_scopeEnd(0), m_numSegments(0) {} + + //! Initialize a NAK PDU + void initialize(PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid, + FileSize scopeStart, + FileSize scopeEnd); + + //! Compute the buffer size needed + U32 getBufferSize() const override; + + //! Fw::Serializable interface - serialize to buffer + Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override; + + //! Fw::Serializable interface - deserialize from buffer + Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override; + + //! Get this as a Header + const PduHeader& asHeader() const { return this->m_header; } + + //! Get scope start + FileSize getScopeStart() const { return this->m_scopeStart; } + + //! Get scope end + FileSize getScopeEnd() const { return this->m_scopeEnd; } + + //! Get number of segments + U8 getNumSegments() const { return this->m_numSegments; } + + //! Get segment at index (no bounds checking - caller must verify index < getNumSegments()) + const SegmentRequest& getSegment(U8 index) const { return this->m_segments[index]; } + + //! Add a segment request + //! @return True if segment was added, false if segment array is full + bool addSegment(FileSize offsetStart, FileSize offsetEnd); + + //! Clear all segment requests + void clearSegments(); + + //! Get directive code + FileDirective getDirectiveCode() const { return FILE_DIRECTIVE_NAK; } + + private: + //! Initialize this NakPdu from a SerialBuffer + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); + + //! Write this NakPdu to a SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_NakPdu_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/PduBase.hpp b/Svc/Ccsds/CfdpManager/Types/PduBase.hpp new file mode 100644 index 00000000000..5420a9fe568 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/PduBase.hpp @@ -0,0 +1,95 @@ +// ====================================================================== +// \title PduBase.hpp +// \author Brian Campuzano +// \brief Base class for all CFDP PDU types +// +// This base class provides a common interface for all PDU types, +// enabling proper construction/destruction and type safety. +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_PduBase_HPP +#define Svc_Ccsds_Cfdp_PduBase_HPP + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +//! Base class for all CFDP PDUs +//! Inherits from Fw::Serializable for F-Prime ecosystem integration +class PduBase : public Fw::Serializable { + protected: + //! The PDU header (common to all PDUs) + PduHeader m_header; + + public: + //! Constructor + PduBase() {} + + //! Virtual destructor for proper cleanup + virtual ~PduBase() = default; + + //! Fw::Serializable interface - serialize to buffer + //! @param buffer The buffer to serialize to + //! @param mode The endianness mode (default: BIG) + //! @return Serialization status + virtual Fw::SerializeStatus serializeTo(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) const override = 0; + + //! Fw::Serializable interface - deserialize from buffer + //! @param buffer The buffer to deserialize from + //! @param mode The endianness mode (default: BIG) + //! @return Deserialization status + virtual Fw::SerializeStatus deserializeFrom(Fw::SerialBufferBase& buffer, + Fw::Endianness mode = Fw::Endianness::BIG) override = 0; + + //! Get the buffer size needed to hold this PDU + //! @return Buffer size in bytes + virtual U32 getBufferSize() const = 0; + + //! Get the PDU type + //! @return PDU type + PduTypeEnum getType() const { return this->m_header.getType(); } + + //! Get the direction + //! @return PduDirection (toward receiver or sender) + PduDirection getDirection() const { return this->m_header.getDirection(); } + + //! Get the transmission mode + //! @return Transmission mode (Class 1 or Class 2) + Cfdp::Class::T getTxmMode() const { return this->m_header.getTxmMode(); } + + //! Get the source entity ID + //! @return Source entity ID + EntityId getSourceEid() const { return this->m_header.getSourceEid(); } + + //! Get the transaction sequence number + //! @return Transaction sequence number + TransactionSeq getTransactionSeq() const { return this->m_header.getTransactionSeq(); } + + //! Get the destination entity ID + //! @return Destination entity ID + EntityId getDestEid() const { return this->m_header.getDestEid(); } + + //! Get the header + //! @return Reference to the PDU header + const PduHeader& getHeader() const { return this->m_header; } +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +// Include all concrete PDU types for convenience (umbrella header pattern) +#include +#include +#include +#include +#include +#include + +#endif // Svc_Ccsds_Cfdp_PduBase_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/PduHeader.cpp b/Svc/Ccsds/CfdpManager/Types/PduHeader.cpp new file mode 100644 index 00000000000..1fef3965074 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/PduHeader.cpp @@ -0,0 +1,291 @@ +// ====================================================================== +// \title PduHeader.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP PDU Header +// ====================================================================== + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +void PduHeader::initialize(PduTypeEnum type, + PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid) { + this->m_type = type; + this->m_version = 1; // CFDP version is always 1 + this->m_pduType = (type == T_FILE_DATA) ? PDU_TYPE_FILE_DATA : PDU_TYPE_DIRECTIVE; + this->m_direction = direction; + this->m_class = txmMode; + this->m_crcFlag = CRC_NOT_PRESENT; // CRC not currently supported + this->m_largeFileFlag = LARGE_FILE_32_BIT; // 32-bit file sizes + this->m_segmentationControl = 0; + this->m_segmentMetadataFlag = 0; + this->m_pduDataLength = 0; // To be set later + this->m_sourceEid = sourceEid; + this->m_transactionSeq = transactionSeq; + this->m_destEid = destEid; +} + +// Helper function to calculate minimum bytes needed to encode a value +U8 PduHeader::getValueEncodedSize(U64 value) { + U8 minSize; + U64 limit = 0x100; + + for (minSize = 1; minSize < 8 && value >= limit; ++minSize) { + limit <<= 8; + } + + return minSize; +} + +// Helper function to encode an integer in variable-length format +static Fw::SerializeStatus encodeIntegerInSize(Fw::SerialBufferBase& serialBuffer, U64 value, U8 encodeSize) { + // Encode from MSB to LSB (big-endian) + for (U8 i = 0; i < encodeSize; ++i) { + U8 shift = static_cast((encodeSize - 1 - i) * 8); + U8 byte = static_cast((value >> shift) & 0xFF); + Fw::SerializeStatus status = serialBuffer.serializeFrom(byte); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + return Fw::FW_SERIALIZE_OK; +} + +// Helper function to decode an integer from variable-length format +static U64 decodeIntegerInSize(Fw::SerialBufferBase& serialBuffer, U8 decodeSize, Fw::SerializeStatus& status) { + U64 value = 0; + + // Decode from MSB to LSB (big-endian) + for (U8 i = 0; i < decodeSize; ++i) { + U8 byte; + status = serialBuffer.deserializeTo(byte); + if (status != Fw::FW_SERIALIZE_OK) { + return 0; + } + value = (value << 8) | byte; + } + + return value; +} + +U32 PduHeader::getBufferSize() const { + // Fixed portion: flags(1) + length(2) + eidTsnLengths(1) = 4 bytes + U32 size = 4; + + // Variable-size entity IDs and transaction sequence number based on actual values + U8 eidSize = getValueEncodedSize(this->m_sourceEid > this->m_destEid ? + this->m_sourceEid : this->m_destEid); + U8 tsnSize = getValueEncodedSize(this->m_transactionSeq); + + size += eidSize; // source EID + size += tsnSize; // transaction sequence number + size += eidSize; // destination EID + + return size; +} + +Fw::SerializeStatus PduHeader::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + Fw::SerializeStatus status; + + // Variable-size entity IDs and transaction sequence number based on actual values + U8 eidSize = getValueEncodedSize(this->m_sourceEid > this->m_destEid ? + this->m_sourceEid : this->m_destEid); + U8 tsnSize = getValueEncodedSize(this->m_transactionSeq); + + // Byte 0: flags + // bits 7-5: version (001b = 1) + // bit 4: pdu_type (0=directive, 1=file data) + // bit 3: direction (0=toward receiver, 1=toward sender) + // bit 2: txm_mode (0=ack, 1=unack) + // bit 1: crc_flag (0=not present, 1=present) + // bit 0: large_file_flag (0=32-bit, 1=64-bit) + U8 flags = 0; + flags |= (this->m_version & 0x07) << 5; + flags |= (this->m_pduType & 0x01) << 4; + flags |= (this->m_direction & 0x01) << 3; + flags |= (this->m_class & 0x01) << 2; + flags |= (this->m_crcFlag & 0x01) << 1; + flags |= (this->m_largeFileFlag & 0x01); + + status = serialBuffer.serializeFrom(flags); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Bytes 1-2: PDU data length (big-endian) + status = serialBuffer.serializeFrom(static_cast(this->m_pduDataLength)); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Byte 3: eidTsnLengths + // bit 7: segmentation_control + // bits 6-4: eid_length - 1 (3 bits) + // bit 3: segment_metadata_flag + // bits 2-0: tsn_length - 1 (3 bits) + U8 eidTsnLengths = 0; + eidTsnLengths |= static_cast((this->m_segmentationControl & 0x01) << 7); + eidTsnLengths |= static_cast(((eidSize - 1) & 0x07) << 4); + eidTsnLengths |= static_cast((this->m_segmentMetadataFlag & 0x01) << 3); + eidTsnLengths |= static_cast((tsnSize - 1) & 0x07); + + status = serialBuffer.serializeFrom(eidTsnLengths); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Variable-width fields (size based on actual values) + status = encodeIntegerInSize(serialBuffer, this->m_sourceEid, eidSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + status = encodeIntegerInSize(serialBuffer, this->m_transactionSeq, tsnSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + status = encodeIntegerInSize(serialBuffer, this->m_destEid, eidSize); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus PduHeader::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + Fw::SerializeStatus status; + + // Byte 0: flags + U8 flags; + status = serialBuffer.deserializeTo(flags); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + this->m_version = (flags >> 5) & 0x07; + this->m_pduType = static_cast((flags >> 4) & 0x01); + this->m_direction = static_cast((flags >> 3) & 0x01); + this->m_class = static_cast((flags >> 2) & 0x01); + this->m_crcFlag = static_cast((flags >> 1) & 0x01); + this->m_largeFileFlag = static_cast(flags & 0x01); + + // Bytes 1-2: PDU data length + status = serialBuffer.deserializeTo(this->m_pduDataLength); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Byte 3: eidTsnLengths + U8 eidTsnLengths; + status = serialBuffer.deserializeTo(eidTsnLengths); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + this->m_segmentationControl = (eidTsnLengths >> 7) & 0x01; + U8 eidSize = ((eidTsnLengths >> 4) & 0x07) + 1; + this->m_segmentMetadataFlag = (eidTsnLengths >> 3) & 0x01; + U8 tsnSize = (eidTsnLengths & 0x07) + 1; + + // Validate that the sizes are within bounds (1-8 bytes) + FW_ASSERT(eidSize >= 1 && eidSize <= 8, static_cast(eidSize)); + FW_ASSERT(tsnSize >= 1 && tsnSize <= 8, static_cast(tsnSize)); + + // Variable-width fields (size determined by encoded length) + this->m_sourceEid = static_cast(decodeIntegerInSize(serialBuffer, eidSize, status)); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + this->m_transactionSeq = static_cast(decodeIntegerInSize(serialBuffer, tsnSize, status)); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + this->m_destEid = static_cast(decodeIntegerInSize(serialBuffer, eidSize, status)); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Don't set m_type yet - it will be determined by the directive code for directive PDUs + // or set to T_FILE_DATA for file data PDUs + if (this->m_pduType == PDU_TYPE_FILE_DATA) { + this->m_type = T_FILE_DATA; + } else { + // For directive PDUs, type will be set when directive code is read + this->m_type = T_NONE; + } + + return Fw::FW_SERIALIZE_OK; +} + +PduTypeEnum peekPduType(const Fw::Buffer& buffer) { + PduTypeEnum pduTypeEnum; + + // Check minimum size for a PDU header + if (buffer.getSize() < PduHeader::MIN_HEADERSIZE) { + return T_NONE; + } + + const U8* data = buffer.getData(); + FW_ASSERT(data != nullptr); + + // Byte 0: flags + // Bit 4 is PDU type: 0 = FILE_DATA, 1 = FILE_DIRECTIVE + U8 flags = data[0]; + PduType pduType = static_cast((flags >> 4) & 0x01); + + if (pduType == PDU_TYPE_FILE_DATA) { + pduTypeEnum = T_FILE_DATA; + } + else + { + // For directive PDUs, we need to read the directive code + // Parse byte 3 to get EID and TSN lengths + U8 eidTsnLengths = data[3]; + U8 eidSize = ((eidTsnLengths >> 4) & 0x07) + 1; // Bits 6-4: EID length - 1 + U8 tsnSize = (eidTsnLengths & 0x07) + 1; // Bits 2-0: TSN length - 1 + + // Calculate offset to directive code: 4 (fixed header) + eidSize + tsnSize + eidSize + U32 directiveCodeOffset = 4 + (2 * eidSize) + tsnSize; + + // Read directive code + U8 directiveCode = data[directiveCodeOffset]; + + // Map directive code to PduTypeEnum + switch (directiveCode) { + case FILE_DIRECTIVE_METADATA: + pduTypeEnum = T_METADATA; + break; + case FILE_DIRECTIVE_END_OF_FILE: + pduTypeEnum = T_EOF; + break; + case FILE_DIRECTIVE_FIN: + pduTypeEnum = T_FIN; + break; + case FILE_DIRECTIVE_ACK: + pduTypeEnum = T_ACK; + break; + case FILE_DIRECTIVE_NAK: + pduTypeEnum = T_NAK; + break; + default: + pduTypeEnum = T_NONE; // Unknown directive code + } + } + return pduTypeEnum; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/PduHeader.hpp b/Svc/Ccsds/CfdpManager/Types/PduHeader.hpp new file mode 100644 index 00000000000..76a4b7f8a44 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/PduHeader.hpp @@ -0,0 +1,175 @@ +// ====================================================================== +// \title PduHeader.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP PDU Header +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_PduHeader_HPP +#define Svc_Ccsds_Cfdp_PduHeader_HPP + +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// CFDP PDU Type +enum PduType : U8 { + PDU_TYPE_DIRECTIVE = 0, // File directive PDU + PDU_TYPE_FILE_DATA = 1 // File data PDU +}; + +// CFDP PduDirection +enum PduDirection : U8 { + DIRECTION_TOWARD_RECEIVER = 0, // Toward file receiver + DIRECTION_TOWARD_SENDER = 1 // Toward file sender +}; + +// CFDP CRC Flag +enum CrcFlag : U8 { + CRC_NOT_PRESENT = 0, // CRC not present + CRC_PRESENT = 1 // CRC present +}; + +// CFDP Large File Flag +enum LargeFileFlag : U8 { + LARGE_FILE_32_BIT = 0, // 32-bit file size + LARGE_FILE_64_BIT = 1 // 64-bit file size +}; + +// PDU type enum (discriminator for the union and for type identification) +enum PduTypeEnum : U8 { + T_METADATA = 0, + T_EOF = 1, + T_FIN = 2, + T_ACK = 3, + T_NAK = 4, + T_FILE_DATA = 5, + T_NONE = 255 +}; + +//! The type of a PDU header (common to all PDUs) +class PduHeader { + private: + //! PDU type (derived from directive code or file data flag) + PduTypeEnum m_type; + + //! CFDP version (should be 1) + U8 m_version; + + //! PDU type + PduType m_pduType; + + //! PduDirection + PduDirection m_direction; + + //! Transmission mode + Cfdp::Class::T m_class; + + //! CRC flag + CrcFlag m_crcFlag; + + //! Large file flag + LargeFileFlag m_largeFileFlag; + + //! Segmentation control + U8 m_segmentationControl; + + //! Segment metadata flag + U8 m_segmentMetadataFlag; + + //! PDU data length (excluding header) + U16 m_pduDataLength; + + //! Source entity ID + EntityId m_sourceEid; + + //! Transaction sequence number + TransactionSeq m_transactionSeq; + + //! Destination entity ID + EntityId m_destEid; + + public: + //! Header size (variable due to EID/TSN lengths) + enum { MIN_HEADERSIZE = 7 }; // Minimum fixed portion + + //! Initialize a PDU header + void initialize(PduTypeEnum type, + PduDirection direction, + Cfdp::Class::T txmMode, + EntityId sourceEid, + TransactionSeq transactionSeq, + EntityId destEid); + + //! Compute the buffer size needed to hold this Header + U32 getBufferSize() const; + + //! Calculate the number of bytes needed to encode a value + //! @param value The value to encode + //! @return Number of bytes needed (1, 2, 4, or 8) + static U8 getValueEncodedSize(U64 value); + + //! Initialize this Header from a SerialBufferBase + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); + + //! Write this Header to a SerialBufferBase + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; + + //! Get the PDU type + PduTypeEnum getType() const { return this->m_type; } + + //! Get the direction + PduDirection getDirection() const { return this->m_direction; } + + //! Get the transmission mode + Cfdp::Class::T getTxmMode() const { return this->m_class; } + + //! Get the source entity ID + EntityId getSourceEid() const { return this->m_sourceEid; } + + //! Get the transaction sequence number + TransactionSeq getTransactionSeq() const { return this->m_transactionSeq; } + + //! Get the destination entity ID + EntityId getDestEid() const { return this->m_destEid; } + + //! Get PDU data length + U16 getPduDataLength() const { return this->m_pduDataLength; } + + //! Set PDU data length (used during encoding) + void setPduDataLength(U16 length) { this->m_pduDataLength = length; } + + //! Get the large file flag + LargeFileFlag getLargeFileFlag() const { return this->m_largeFileFlag; } + + //! Check if segment metadata is present + bool hasSegmentMetadata() const { return this->m_segmentMetadataFlag != 0; } + + //! Set the large file flag (used for testing and configuration) + void setLargeFileFlag(LargeFileFlag flag) { this->m_largeFileFlag = flag; } + + //! Allow PDU classes to access private members + friend class MetadataPdu; + friend class FileDataPdu; + friend class EofPdu; + friend class FinPdu; + friend class AckPdu; + friend class NakPdu; +}; + +//! Peek at the PDU type from a buffer without consuming it +//! @param buffer The buffer containing the PDU +//! @return The PDU type, or T_NONE if the buffer is invalid +PduTypeEnum peekPduType(const Fw::Buffer& buffer); + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_PduHeader_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/Tlv.cpp b/Svc/Ccsds/CfdpManager/Types/Tlv.cpp new file mode 100644 index 00000000000..cf68ed12637 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/Tlv.cpp @@ -0,0 +1,242 @@ +// ====================================================================== +// \title Tlv.cpp +// \author Brian Campuzano +// \brief cpp file for CFDP TLV (Type-Length-Value) classes +// ====================================================================== + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ====================================================================== +// TlvData +// ====================================================================== + +TlvData::TlvData() : m_dataLength(0) { + // Initialize union to zero + memset(this->m_rawData, 0, sizeof(this->m_rawData)); +} + +void TlvData::setEntityId(EntityId eid) { + this->m_eid = eid; + // Entity ID length depends on the value + // For now, use sizeof(EntityId) as the length + this->m_dataLength = sizeof(EntityId); +} + +void TlvData::setData(const U8* data, U8 length) { + FW_ASSERT(length <= 255, length); + FW_ASSERT(data != nullptr); + + memcpy(this->m_rawData, data, length); + this->m_dataLength = length; +} + +EntityId TlvData::getEntityId() const { + return this->m_eid; +} + +const U8* TlvData::getData() const { + return this->m_rawData; +} + +U8 TlvData::getLength() const { + return this->m_dataLength; +} + +// ====================================================================== +// Tlv +// ====================================================================== + +Tlv::Tlv() : m_type(TLV_TYPE_ENTITY_ID) { + // Default constructor +} + +void Tlv::initialize(EntityId eid) { + this->m_type = TLV_TYPE_ENTITY_ID; + this->m_data.setEntityId(eid); +} + +void Tlv::initialize(TlvType type, const U8* data, U8 length) { + this->m_type = type; + this->m_data.setData(data, length); +} + +TlvType Tlv::getType() const { + return this->m_type; +} + +const TlvData& Tlv::getData() const { + return this->m_data; +} + +U32 Tlv::getEncodedSize() const { + // Type (1 byte) + Length (1 byte) + Data (variable) + return 2 + this->m_data.getLength(); +} + +Fw::SerializeStatus Tlv::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + Fw::SerializeStatus status; + + // Serialize type byte + status = serialBuffer.serializeFrom(static_cast(this->m_type)); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize length byte + U8 length = this->m_data.getLength(); + status = serialBuffer.serializeFrom(length); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Serialize data + if (this->m_type == TLV_TYPE_ENTITY_ID) { + // For Entity ID, serialize as EntityId + EntityId eid = this->m_data.getEntityId(); + status = serialBuffer.serializeFrom(eid); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } else { + // For other types, serialize raw data + const U8* data = this->m_data.getData(); + for (U8 i = 0; i < length; i++) { + status = serialBuffer.serializeFrom(data[i]); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus Tlv::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + Fw::SerializeStatus status; + + // Deserialize type byte + U8 typeVal; + status = serialBuffer.deserializeTo(typeVal); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + this->m_type = static_cast(typeVal); + + // Deserialize length byte + U8 length; + status = serialBuffer.deserializeTo(length); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + + // Deserialize data + if (this->m_type == TLV_TYPE_ENTITY_ID) { + // For Entity ID, deserialize as EntityId + EntityId eid; + status = serialBuffer.deserializeTo(eid); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + this->m_data.setEntityId(eid); + } else { + // For other types, deserialize raw data + U8 rawData[256]; + for (U8 i = 0; i < length; i++) { + status = serialBuffer.deserializeTo(rawData[i]); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + this->m_data.setData(rawData, length); + } + + return Fw::FW_SERIALIZE_OK; +} + +// ====================================================================== +// TlvList +// ====================================================================== + +TlvList::TlvList() : m_numTlv(0) { + // Default constructor +} + +bool TlvList::appendTlv(const Tlv& tlv) { + if (this->m_numTlv >= CFDP_MAX_TLV) { + return false; // List is full + } + + this->m_tlvs[this->m_numTlv] = tlv; + this->m_numTlv++; + return true; +} + +void TlvList::clear() { + this->m_numTlv = 0; +} + +U8 TlvList::getNumTlv() const { + return this->m_numTlv; +} + +const Tlv& TlvList::getTlv(U8 index) const { + FW_ASSERT(index < this->m_numTlv, index, this->m_numTlv); + return this->m_tlvs[index]; +} + +U32 TlvList::getEncodedSize() const { + U32 size = 0; + for (U8 i = 0; i < this->m_numTlv; i++) { + size += this->m_tlvs[i].getEncodedSize(); + } + return size; +} + +Fw::SerializeStatus TlvList::toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const { + Fw::SerializeStatus status; + + // Encode all TLVs + for (U8 i = 0; i < this->m_numTlv; i++) { + status = this->m_tlvs[i].toSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } + } + + return Fw::FW_SERIALIZE_OK; +} + +Fw::SerializeStatus TlvList::fromSerialBuffer(Fw::SerialBufferBase& serialBuffer) { + Fw::SerializeStatus status; + + // Clear existing TLVs + this->m_numTlv = 0; + + // Decode TLVs until buffer is exhausted or max count reached + while (serialBuffer.getDeserializeSizeLeft() > 0 && this->m_numTlv < CFDP_MAX_TLV) { + status = this->m_tlvs[this->m_numTlv].fromSerialBuffer(serialBuffer); + if (status != Fw::FW_SERIALIZE_OK) { + // If we fail to decode a TLV, stop (could be end of buffer or invalid data) + // Only return error if we haven't successfully decoded any TLVs yet + if (this->m_numTlv == 0) { + return status; + } else { + // We've decoded some TLVs successfully, so consider this a success + break; + } + } + this->m_numTlv++; + } + + return Fw::FW_SERIALIZE_OK; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/Types/Tlv.hpp b/Svc/Ccsds/CfdpManager/Types/Tlv.hpp new file mode 100644 index 00000000000..56429b371bb --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/Tlv.hpp @@ -0,0 +1,122 @@ +// ====================================================================== +// \title Tlv.hpp +// \author Brian Campuzano +// \brief hpp file for CFDP TLV types +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_Tlv_HPP +#define Svc_Ccsds_Cfdp_Tlv_HPP + +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// CFDP TLV Types +// Blue Book section 5.4, table 5-3 +enum TlvType : U8 { + TLV_TYPE_FILESTORE_REQUEST = 0, // Filestore request + TLV_TYPE_FILESTORE_RESPONSE = 1, // Filestore response + TLV_TYPE_MESSAGE_TO_USER = 2, // Message to user + TLV_TYPE_FAULT_HANDLER_OVERRIDE = 4, // Fault handler override + TLV_TYPE_FLOW_LABEL = 5, // Flow label + TLV_TYPE_ENTITY_ID = 6 // Entity ID +}; + +//! TLV data storage +class TlvData { + private: + union { + EntityId m_eid; // Valid when type=ENTITY_ID + U8 m_rawData[256]; // Valid for other types (max 255 bytes + null term) + }; + U8 m_dataLength; // Actual length of data + + public: + TlvData(); + + // Set entity ID (for TLV type 6) + void setEntityId(EntityId eid); + + // Set raw data (for other TLV types) + void setData(const U8* data, U8 length); + + // Get entity ID + EntityId getEntityId() const; + + // Get raw data pointer + const U8* getData() const; + + // Get data length + U8 getLength() const; +}; + +//! Single TLV entry +class Tlv { + private: + TlvType m_type; + TlvData m_data; + + public: + Tlv(); + + // Initialize with entity ID + void initialize(EntityId eid); + + // Initialize with raw data + void initialize(TlvType type, const U8* data, U8 length); + + // Getters + TlvType getType() const; + const TlvData& getData() const; + + // Compute encoded size + U32 getEncodedSize() const; + + // Encode to SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; + + // Decode from SerialBuffer + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); +}; + +//! List of TLVs +class TlvList { + private: + U8 m_numTlv; + Tlv m_tlvs[CFDP_MAX_TLV]; + + public: + TlvList(); + + // Add a TLV (returns false if list is full) + bool appendTlv(const Tlv& tlv); + + // Clear all TLVs + void clear(); + + // Get number of TLVs + U8 getNumTlv() const; + + // Get TLV at index + const Tlv& getTlv(U8 index) const; + + // Compute total encoded size of all TLVs + U32 getEncodedSize() const; + + // Encode all TLVs to SerialBuffer + Fw::SerializeStatus toSerialBuffer(Fw::SerialBufferBase& serialBuffer) const; + + // Decode all TLVs from SerialBuffer (reads until buffer exhausted) + Fw::SerializeStatus fromSerialBuffer(Fw::SerialBufferBase& serialBuffer); +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_Tlv_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/Types.fpp b/Svc/Ccsds/CfdpManager/Types/Types.fpp new file mode 100644 index 00000000000..4cbc60410b1 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/Types.fpp @@ -0,0 +1,121 @@ +module Svc { +module Ccsds { +module Cfdp { + + # ------------------------------------------------ + # CFDP Types + # ------------------------------------------------ + enum Status { + SUCCESS @< CFDP operation has been successful + ERROR @< Generic CFDP error return code + PDU_METADATA_ERROR @< Invalid metadata PDU + SHORT_PDU_ERROR @< PDU too short + REC_PDU_FSIZE_MISMATCH_ERROR @< Receive PDU: EOF file size mismatch + REC_PDU_BAD_EOF_ERROR @< Receive PDU: Invalid EOF packet + SEND_PDU_NO_BUF_AVAIL_ERROR @< Send PDU: No send buffer available, throttling limit reached + SEND_PDU_ERROR @< Send PDU: Send failed + } + + enum Flow { + NOT_FROZEN @< CFDP channel operations are executing nominally + FROZEN @< CFDP channel operations are frozen + } + + @ Values for CFDP file transfer class + @ + @ The CFDP specification prescribes two classes/modes of file + @ transfer protocol operation - unacknowledged/simple or + @ acknowledged/reliable. + @ + @ Defined per section 7.1 of CCSDS 727.0-B-5 + enum Class: U8 { + CLASS_2 = 0 @< CFDP class 2 - Reliable transfer (Acknowledged) + CLASS_1 = 1 @< CFDP class 1 - Unreliable transfer (Unacknowledged) + } + + @ Enum used to determine if a file should be kept or deleted after a CFDP transaction + enum Keep: U8 { + DELETE = 0 @< File will be deleted after the CFDP transaction + KEEP = 1 @< File will be kept after the CFDP transaction + } + + @ Suspend/resume action + enum SuspendResume: U8 { + SUSPEND = 0 @< Suspend transaction + RESUME = 1 @< Resume transaction + } + + @ CFDP queue identifiers + enum QueueId: U8 { + PEND = 0, @< first one on this list is active + TXA = 1 + TXW = 2 + RX = 3 + HIST = 4 + HIST_FREE = 5 + FREE = 6 + NUM = 7 + } + + @< Structure for configuration parameters for a single CFDP channel + struct ChannelParams { + ack_limit: U8 @< number of times to retry ACK (for ex, send FIN and wait for fin-ack) + nack_limit: U8 @< number of times to retry NAK before giving up (resets on a single response + ack_timer: U32 @< Acknowledge timer in seconds + inactivity_timer: U32 @< Inactivity timer in seconds + dequeue_enabled: Fw.Enabled @< if enabled, then the channel will make pending transactions active + move_dir: string size MaxFilePathSize @< Move directory if not empty + max_outgoing_pdus_per_cycle: U32 @< Maximum number of PDUs to send per cycle per channel for throttling + tmp_dir: string size MaxFilePathSize @< Temporary directory for uplink file reception + fail_dir: string size MaxFilePathSize @< Directory for failed poll files + } + + @< Structure for the configured array of CFDP channels + array ChannelArrayParams = [NumChannels] ChannelParams + + @< Structure for telemetry counters for a single CFDP channel + struct ChannelTelemetry { + # Receive counters + recvErrors: U32 @< Number of PDU receive errors + recvDropped: U32 @< Number of PDUs dropped due to lack of resources + recvSpurious: U32 @< Number of spurious PDUs received + recvFileDataBytes: U64 @< Total file data bytes received + recvNakSegmentRequests: U32 @< Number of NAK segment requests received from peer + recvPdu: U32 @< Number of PDUs received with valid headers + + # Sent counters + sentNakSegmentRequests: U32 @< Number of NAK segment requests sent to peer + sentFileDataBytes: U64 @< Total file data bytes sent + sentPdu: U32 @< Number of PDUs sent + + # Fault counters + faultAckLimit: U32 @< Number of transactions abandoned due to ACK limit exceeded + faultNakLimit: U32 @< Number of transactions abandoned due to NAK limit exceeded + faultInactivityTimer: U32 @< Number of transactions abandoned due to inactivity timeout + faultCrcMismatch: U32 @< Number of CRC mismatches detected in received files + faultFileSizeMismatch: U32 @< Number of file size mismatches detected + faultFileOpen: U32 @< Number of file open failures + faultFileRead: U32 @< Number of file read failures + faultFileWrite: U32 @< Number of file write failures + faultFileSeek: U32 @< Number of file seek failures + faultFileRename: U32 @< Number of file rename failures + faultDirectoryRead: U32 @< Number of directory read failures + + # Queue depths + queueFree: U16 @< Number of transactions in FREE queue + queueTxActive: U16 @< Number of transactions in active transmit queue (TXA) + queueTxWaiting: U16 @< Number of transactions in waiting transmit queue (TXW) + queueRx: U16 @< Number of transactions in receive queue (RX) + queueHistory: U16 @< Number of completed transactions in history queue + + # Activity counters + playbackCounter: U8 @< Number of active directory playback operations + pollCounter: U8 @< Number of active directory poll operations + } + + @< Structure for the telemetry array of CFDP channels + array ChannelTelemetryArray = [NumChannels] ChannelTelemetry + +} +} +} diff --git a/Svc/Ccsds/CfdpManager/Types/Types.hpp b/Svc/Ccsds/CfdpManager/Types/Types.hpp new file mode 100644 index 00000000000..2fd1459b741 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/Types.hpp @@ -0,0 +1,450 @@ +// ====================================================================== +// \title Types.hpp +// \author Brian Campuzano +// \brief hpp file for shared CFDP protocol type definitions +// ====================================================================== + +#ifndef Svc_Ccsds_Cfdp_Types_HPP +#define Svc_Ccsds_Cfdp_Types_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// Forward declarations for class types used in structs below +class Transaction; + +/** + * @brief Maximum possible number of transactions that may exist on a single CFDP channel + */ +#define CFDP_NUM_TRANSACTIONS_PER_CHANNEL \ + (CFDP_MAX_COMMANDED_PLAYBACK_FILES_PER_CHAN + CFDP_MAX_SIMULTANEOUS_RX + \ + ((CFDP_MAX_POLLING_DIR_PER_CHAN + CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN) * \ + CFDP_NUM_TRANSACTIONS_PER_PLAYBACK)) + +// CFDP File Directive Codes +// Blue Book section 5.2, table 5-4 +enum FileDirective : U8 { + FILE_DIRECTIVE_INVALID_MIN = 0, // Minimum used to limit range + FILE_DIRECTIVE_END_OF_FILE = 4, // End of File + FILE_DIRECTIVE_FIN = 5, // Finished + FILE_DIRECTIVE_ACK = 6, // Acknowledge + FILE_DIRECTIVE_METADATA = 7, // Metadata + FILE_DIRECTIVE_NAK = 8, // Negative Acknowledge + FILE_DIRECTIVE_PROMPT = 9, // Prompt + FILE_DIRECTIVE_KEEP_ALIVE = 12, // Keep Alive + FILE_DIRECTIVE_INVALID_MAX = 13 // Maximum used to limit range +}; + +// CFDP Condition Codes +// Blue Book section 5.2.2, table 5-5 +enum ConditionCode : U8 { + CONDITION_CODE_NO_ERROR = 0, + CONDITION_CODE_POS_ACK_LIMIT_REACHED = 1, + CONDITION_CODE_KEEP_ALIVE_LIMIT_REACHED = 2, + CONDITION_CODE_INVALID_TRANSMISSION_MODE = 3, + CONDITION_CODE_FILESTORE_REJECTION = 4, + CONDITION_CODE_FILE_CHECKSUM_FAILURE = 5, + CONDITION_CODE_FILE_SIZE_ERROR = 6, + CONDITION_CODE_NAK_LIMIT_REACHED = 7, + CONDITION_CODE_INACTIVITY_DETECTED = 8, + CONDITION_CODE_INVALID_FILE_STRUCTURE = 9, + CONDITION_CODE_CHECK_LIMIT_REACHED = 10, + CONDITION_CODE_UNSUPPORTED_CHECKSUM_TYPE = 11, + CONDITION_CODE_SUSPEND_REQUEST_RECEIVED = 14, + CONDITION_CODE_CANCEL_REQUEST_RECEIVED = 15 +}; + +// CFDP ACK Transaction Status +// Blue Book section 5.2.4, table 5-8 +enum AckTxnStatus : U8 { + ACK_TXN_STATUS_UNDEFINED = 0, + ACK_TXN_STATUS_ACTIVE = 1, + ACK_TXN_STATUS_TERMINATED = 2, + ACK_TXN_STATUS_UNRECOGNIZED = 3, + ACK_TXN_STATUS_INVALID = 4 +}; + +// CFDP FIN Delivery Code +// Blue Book section 5.2.3, table 5-7 +enum FinDeliveryCode : U8 { + FIN_DELIVERY_CODE_COMPLETE = 0, // Data complete + FIN_DELIVERY_CODE_INCOMPLETE = 1 // Data incomplete +}; + +// CFDP FIN File Status +// Blue Book section 5.2.3, table 5-7 +enum FinFileStatus : U8 { + FIN_FILE_STATUS_DISCARDED = 0, // File discarded deliberately + FIN_FILE_STATUS_DISCARDED_FILESTORE = 1, // File discarded due to filestore rejection + FIN_FILE_STATUS_RETAINED = 2, // File retained successfully + FIN_FILE_STATUS_UNREPORTED = 3 // File status unreported +}; + +// CFDP Checksum Type +// Blue Book section 5.2.5, table 5-9 +enum ChecksumType : U8 { + CHECKSUM_TYPE_MODULAR = 0, // Modular checksum + CHECKSUM_TYPE_CRC_32 = 1, // CRC-32 (not currently supported) + CHECKSUM_TYPE_NULL_CHECKSUM = 15 // Null checksum +}; + +/** + * @brief High-level state of a transaction + */ +enum TxnState : U8 +{ + TXN_STATE_UNDEF = 0, /**< \brief State assigned to an unused object on the free list */ + TXN_STATE_INIT = 1, /**< \brief State assigned to a newly allocated transaction object */ + TXN_STATE_R1 = 2, /**< \brief Receive file as class 1 */ + TXN_STATE_S1 = 3, /**< \brief Send file as class 1 */ + TXN_STATE_R2 = 4, /**< \brief Receive file as class 2 */ + TXN_STATE_S2 = 5, /**< \brief Send file as class 2 */ + TXN_STATE_DROP = 6, /**< \brief State where all PDUs are dropped */ + TXN_STATE_HOLD = 7, /**< \brief State assigned to a transaction after freeing it */ + TXN_STATE_INVALID = 8 /**< \brief Marker value for the highest possible state number */ +}; + +/** + * @brief Sub-state of a send file transaction + */ +enum TxSubState : U8 +{ + TX_SUB_STATE_METADATA = 0, /**< sending the initial MD directive */ + TX_SUB_STATE_FILEDATA = 1, /**< sending file data PDUs */ + TX_SUB_STATE_EOF = 2, /**< sending the EOF directive */ + TX_SUB_STATE_CLOSEOUT_SYNC = 3, /**< pending final acks from remote */ + TX_SUB_STATE_NUM_STATES = 4 +}; + +/** + * @brief Sub-state of a receive file transaction + */ +enum RxSubState : U8 +{ + RX_SUB_STATE_FILEDATA = 0, /**< receive file data PDUs */ + RX_SUB_STATE_EOF = 1, /**< got EOF directive */ + RX_SUB_STATE_CLOSEOUT_SYNC = 2, /**< pending final acks from remote */ + RX_SUB_STATE_NUM_STATES = 3 +}; + +/** + * @brief Direction identifier + * + * Differentiates between send and receive history entries + */ +enum Direction : U8 +{ + DIRECTION_RX = 0, + DIRECTION_TX = 1, + DIRECTION_NUM = 2, +}; + +/** + * @brief Transaction initiation method + * + * Differentiates between command-initiated and port-initiated transactions + */ +enum TransactionInitType : U8 +{ + INIT_BY_COMMAND = 0, //!< Transaction initiated via command interface + INIT_BY_PORT = 1 //!< Transaction initiated via port interface +}; + +/** + * @brief Identifies the type of timer tick being processed + */ +enum CfdpTickType : U8 +{ + CFDP_TICK_TYPE_RX, + CFDP_TICK_TYPE_TXW_NORM, + CFDP_TICK_TYPE_TXW_NAK, + CFDP_TICK_TYPE_NUM_TYPES +}; + +/** + * @brief Values for Transaction Status code + * + * This enum defines the possible values representing the + * result of a transaction. This is a superset of the condition codes + * defined in CCSDS book 727 (condition codes) but with additional + * values for local conditions that the blue book does not have, + * such as protocol/state machine or decoding errors. + * + * The values here are designed to not overlap with the condition + * codes defined in the blue book, but can be translated to one + * of those codes for the purposes of FIN/ACK/EOF PDUs. + */ +enum TxnStatus : I32 +{ + /** + * The undefined status is a placeholder for new transactions before a value is set. + */ + TXN_STATUS_UNDEFINED = -1, + + /* Status codes 0-15 share the same values/meanings as the CFDP condition code (CC) */ + TXN_STATUS_NO_ERROR = CONDITION_CODE_NO_ERROR, + TXN_STATUS_POS_ACK_LIMIT_REACHED = CONDITION_CODE_POS_ACK_LIMIT_REACHED, + TXN_STATUS_KEEP_ALIVE_LIMIT_REACHED = CONDITION_CODE_KEEP_ALIVE_LIMIT_REACHED, + TXN_STATUS_INVALID_TRANSMISSION_MODE = CONDITION_CODE_INVALID_TRANSMISSION_MODE, + TXN_STATUS_FILESTORE_REJECTION = CONDITION_CODE_FILESTORE_REJECTION, + TXN_STATUS_FILE_CHECKSUM_FAILURE = CONDITION_CODE_FILE_CHECKSUM_FAILURE, + TXN_STATUS_FILE_SIZE_ERROR = CONDITION_CODE_FILE_SIZE_ERROR, + TXN_STATUS_NAK_LIMIT_REACHED = CONDITION_CODE_NAK_LIMIT_REACHED, + TXN_STATUS_INACTIVITY_DETECTED = CONDITION_CODE_INACTIVITY_DETECTED, + TXN_STATUS_INVALID_FILE_STRUCTURE = CONDITION_CODE_INVALID_FILE_STRUCTURE, + TXN_STATUS_CHECK_LIMIT_REACHED = CONDITION_CODE_CHECK_LIMIT_REACHED, + TXN_STATUS_UNSUPPORTED_CHECKSUM_TYPE = CONDITION_CODE_UNSUPPORTED_CHECKSUM_TYPE, + TXN_STATUS_SUSPEND_REQUEST_RECEIVED = CONDITION_CODE_SUSPEND_REQUEST_RECEIVED, + TXN_STATUS_CANCEL_REQUEST_RECEIVED = CONDITION_CODE_CANCEL_REQUEST_RECEIVED, + + /* Additional status codes for items not representable in a CFDP CC, these can be set in + * transactions that did not make it to the point of sending FIN/EOF. */ + TXN_STATUS_PROTOCOL_ERROR = 16, + TXN_STATUS_ACK_LIMIT_NO_FIN = 17, + TXN_STATUS_ACK_LIMIT_NO_EOF = 18, + TXN_STATUS_NAK_RESPONSE_ERROR = 19, + TXN_STATUS_SEND_EOF_FAILURE = 20, + TXN_STATUS_EARLY_FIN = 21, + + /* keep last */ + TXN_STATUS_MAX = 22 +}; + +/** + * @brief Cache of source and destination filename + * + * This pairs a source and destination file name together + * to be retained for future reference in the transaction/history + */ +struct CfdpTxnFilenames +{ + Fw::String src_filename; + Fw::String dst_filename; +}; + +/** + * @brief CFDP History entry + * + * Records CFDP operations for future reference + */ +struct History +{ + CfdpTxnFilenames fnames; /**< \brief file names associated with this history entry */ + CListNode cl_node; /**< \brief for connection to a CList */ + Direction dir; /**< \brief direction of this history entry */ + TxnStatus txn_stat; /**< \brief final status of operation */ + EntityId src_eid; /**< \brief the source eid of the transaction */ + EntityId peer_eid; /**< \brief peer_eid is always the "other guy", same src_eid for RX */ + TransactionSeq seq_num; /**< \brief transaction identifier, stays constant for entire transfer */ +}; + +/** + * @brief Wrapper around a CfdpChunkList object + * + * This allows a CfdpChunkList to be stored within a CList data storage structure. + * The wrapper is pooled by Channel for reuse across transactions. + */ +struct CfdpChunkWrapper +{ + CfdpChunkList chunks; //!< Chunk list for gap tracking + CListNode cl_node; //!< Circular list node for pooling + + /** + * @brief Constructor for initializing the chunk list + * + * @param maxChunks Maximum number of chunks this list can hold + * @param chunkMem Pointer to pre-allocated chunk memory + */ + CfdpChunkWrapper(ChunkIdx maxChunks, Chunk* chunkMem) + : chunks(maxChunks, chunkMem), cl_node{} {} +}; + +/** + * @brief CFDP Playback entry + * + * Keeps the state of CFDP playback requests + */ +struct Playback +{ + Os::Directory dir; + Class::T cfdp_class; + CfdpTxnFilenames fnames; + U16 num_ts; /**< \brief number of transactions */ + U8 priority; + EntityId dest_id; + char pending_file[MaxFilePathSize]; + + bool busy; + bool diropen; + Keep::T keep; + bool counted; +}; + +/** + * \brief Directory poll entry + * + * Keeps the state of CFDP directory polling + */ +struct CfdpPollDir +{ + Playback pb; /**< \brief State of the current playback requests */ + Timer intervalTimer; /**< \brief Timer object used to poll the directory */ + + U32 intervalSec; /**< \brief number of seconds to wait before trying a new directory */ + + U8 priority; /**< \brief priority to use when placing transactions on the pending queue */ + Class::T cfdpClass; /**< \brief the CFDP class to send */ + EntityId destEid; /**< \brief destination entity id */ + + Fw::String srcDir; /**< \brief path to source dir */ + Fw::String dstDir; /**< \brief path to destination dir */ + + Fw::Enabled enabled; /**< \brief Enabled flag */ +}; + +/** + * @brief Data specific to a class 2 send file transaction + */ +struct CfdpTxS2Data +{ + U8 fin_cc; /**< \brief remember the cc in the received FIN PDU to echo in eof-fin */ + U8 acknak_count; +}; + +/** + * @brief Data specific to a send file transaction + */ +struct CfdpTxStateData +{ + TxSubState sub_state; + FileSize cached_pos; + + CfdpTxS2Data s2; +}; + +/** + * @brief Data specific to a class 2 receive file transaction + */ +struct CfdpRxS2Data +{ + U32 eof_crc; + FileSize eof_size; + FileSize rx_crc_calc_bytes; + FinDeliveryCode dc; + FinFileStatus fs; + U8 eof_cc; /**< \brief remember the cc in the received EOF PDU to echo in eof-ack */ + U8 acknak_count; +}; + +/** + * @brief Data specific to a receive file transaction + */ +struct CfdpRxStateData +{ + RxSubState sub_state; + FileSize cached_pos; + + CfdpRxS2Data r2; +}; + +/** + * @brief Data that applies to all types of transactions + */ +struct CfdpFlagsCommon +{ + U8 q_index; /**< \brief Q index this is in */ + bool ack_timer_armed; + bool suspended; + bool canceled; + bool crc_calc; + bool inactivity_fired; /**< \brief set whenever the inactivity timeout expires */ + bool keep_history; /**< \brief whether history should be preserved during recycle */ +}; + +/** + * @brief Flags that apply to receive transactions + */ +struct CfdpFlagsRx +{ + CfdpFlagsCommon com; + + bool md_recv; /**< \brief md received for r state */ + bool eof_recv; + bool send_nak; + bool send_fin; + bool send_eof_ack; + bool complete; /**< \brief r2 */ + bool fd_nak_sent; /**< \brief latches that at least one NAK has been sent for file data */ +}; + +/** + * @brief Flags that apply to send transactions + */ +struct CfdpFlagsTx +{ + CfdpFlagsCommon com; + + bool md_need_send; + bool send_eof; + bool eof_ack_recv; + bool fin_recv; + bool send_fin_ack; + bool cmd_tx; /**< \brief indicates transaction is commanded (ground) tx */ +}; + +/** + * @brief Summary of all possible transaction flags (tx and rx) + */ +union CfdpStateFlags +{ + CfdpFlagsCommon com; /**< \brief applies to all transactions */ + CfdpFlagsRx rx; /**< \brief applies to only receive file transactions */ + CfdpFlagsTx tx; /**< \brief applies to only send file transactions */ +}; + +/** + * @brief Summary of all possible transaction state information (tx and rx) + */ +union CfdpStateData +{ + CfdpTxStateData send; /**< \brief applies to only send file transactions */ + CfdpRxStateData receive; /**< \brief applies to only receive file transactions */ +}; + +/** + * @brief Callback function type for use with Channel::traverseAllTransactions() + * + * @param txn Pointer to current transaction being traversed + * @param context Opaque object passed from initial call + */ +using CfdpTraverseAllTransactionsFunc = std::function; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif // Svc_Ccsds_Cfdp_Types_HPP diff --git a/Svc/Ccsds/CfdpManager/Types/test/ut/PduTests.cpp b/Svc/Ccsds/CfdpManager/Types/test/ut/PduTests.cpp new file mode 100644 index 00000000000..c36539543b5 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/test/ut/PduTests.cpp @@ -0,0 +1,1921 @@ +// ====================================================================== +// \title PduTests.cpp +// \author Brian Campuzano +// \brief Unit tests for CFDP PDU classes +// ====================================================================== + +#include +#include +#include + +using namespace Svc::Ccsds; +using namespace Svc::Ccsds::Cfdp; + +// Test fixture for CFDP PDU tests +class PduTest : public ::testing::Test { + protected: + void SetUp() override { + // Common setup + } + + void TearDown() override { + // Common cleanup + } +}; + +// ====================================================================== +// Header Tests +// ====================================================================== + +TEST_F(PduTest, HeaderBufferSize) { + PduHeader header; + header.initialize(T_METADATA, DIRECTION_TOWARD_RECEIVER, + Cfdp::Class::CLASS_2, 123, 456, 789); + + // Minimum header size with 1-byte EIDs and TSN + // flags(1) + length(2) + eidTsnLengths(1) + sourceEid(2) + tsn(2) + destEid(2) = 10 + ASSERT_GE(header.getBufferSize(), 7U); +} + +TEST_F(PduTest, HeaderRoundTrip) { + // Arrange + PduHeader txHeader; + const PduDirection direction = DIRECTION_TOWARD_SENDER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 10; + const TransactionSeq transactionSeq = 20; + const EntityId destEid = 30; + const U16 pduDataLength = 100; + + txHeader.initialize(T_METADATA, direction, txmMode, sourceEid, transactionSeq, destEid); + txHeader.setPduDataLength(pduDataLength); + + U8 buffer[256]; + Fw::SerialBuffer serialBuffer(buffer, sizeof(buffer)); + + // Act - Encode + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txHeader.toSerialBuffer(serialBuffer)); + + // Act - Decode + serialBuffer.resetSer(); + serialBuffer.fill(); + PduHeader rxHeader; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxHeader.fromSerialBuffer(serialBuffer)); + + // Assert - Verify all fields + ASSERT_EQ(direction, rxHeader.getDirection()); + ASSERT_EQ(txmMode, rxHeader.getTxmMode()); + ASSERT_EQ(sourceEid, rxHeader.getSourceEid()); + ASSERT_EQ(transactionSeq, rxHeader.getTransactionSeq()); + ASSERT_EQ(destEid, rxHeader.getDestEid()); + ASSERT_EQ(pduDataLength, rxHeader.getPduDataLength()); +} + +// ====================================================================== +// Metadata PDU Tests +// ====================================================================== + +TEST_F(PduTest, MetadataBufferSize) { + MetadataPdu pdu; + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, 1024, "src.txt", "dst.txt", + CHECKSUM_TYPE_MODULAR, 1); + + U32 size = pdu.getBufferSize(); + // Should include header + directive + segmentation + filesize + 2 LVs + ASSERT_GT(size, 0U); +} + +TEST_F(PduTest, MetadataRoundTrip) { + // Arrange - Create and initialize transmit PDU + MetadataPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_SENDER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 100; + const TransactionSeq transactionSeq = 200; + const EntityId destEid = 300; + const FileSize fileSize = 2048; + const char* sourceFilename = "source_file.bin"; + const char* destFilename = "dest_file.bin"; + const ChecksumType checksumType = CHECKSUM_TYPE_MODULAR; + const U8 closureRequested = 1; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + fileSize, sourceFilename, destFilename, checksumType, closureRequested); + + // Serialize to first buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Copy to second buffer + U8 buffer2[512]; + memcpy(buffer2, buffer1, txBuffer.getSize()); + + // Deserialize from second buffer using SerialBuffer to read header + body + Fw::SerialBuffer serialBuffer(buffer2, txBuffer.getSize()); + serialBuffer.fill(); + + // Read header + PduHeader rxHeader; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxHeader.fromSerialBuffer(serialBuffer)); + + // Verify header fields + ASSERT_EQ(direction, rxHeader.getDirection()); + ASSERT_EQ(txmMode, rxHeader.getTxmMode()); + ASSERT_EQ(sourceEid, rxHeader.getSourceEid()); + ASSERT_EQ(transactionSeq, rxHeader.getTransactionSeq()); + ASSERT_EQ(destEid, rxHeader.getDestEid()); + + // Read and verify directive code + U8 directiveCode; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.deserializeTo(directiveCode)); + ASSERT_EQ(static_cast(FILE_DIRECTIVE_METADATA), directiveCode); + + // Read segmentation control byte + U8 segmentationControl; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.deserializeTo(segmentationControl)); + U8 rxClosureRequested = (segmentationControl >> 7) & 0x01; + U8 rxChecksumType = segmentationControl & 0x0F; + ASSERT_EQ(closureRequested, rxClosureRequested); + ASSERT_EQ(static_cast(checksumType), rxChecksumType); + + // Read file size + U32 rxFileSize; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.deserializeTo(rxFileSize)); + ASSERT_EQ(fileSize, rxFileSize); + + // Read source filename LV + U8 srcFilenameLen; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.deserializeTo(srcFilenameLen)); + ASSERT_EQ(strlen(sourceFilename), srcFilenameLen); + U8 srcFilenameBuf[256]; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.popBytes(srcFilenameBuf, srcFilenameLen)); + ASSERT_EQ(0, memcmp(sourceFilename, srcFilenameBuf, srcFilenameLen)); + + // Read dest filename LV + U8 dstFilenameLen; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.deserializeTo(dstFilenameLen)); + ASSERT_EQ(strlen(destFilename), dstFilenameLen); + U8 dstFilenameBuf[256]; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, serialBuffer.popBytes(dstFilenameBuf, dstFilenameLen)); + ASSERT_EQ(0, memcmp(destFilename, dstFilenameBuf, dstFilenameLen)); +} + +TEST_F(PduTest, MetadataEmptyFilenames) { + MetadataPdu pdu; + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, 0, "", "", + CHECKSUM_TYPE_NULL_CHECKSUM, 0); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Should encode successfully even with empty filenames + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, pdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); +} + +TEST_F(PduTest, MetadataLongFilenames) { + MetadataPdu pdu; + // Test with maximum allowed filename length (MaxFilePathSize = 200) + const char* longSrc = "/very/long/path/to/source/file/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bin"; + const char* longDst = "/another/very/long/path/to/destination/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.dat"; + + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, 4096, longSrc, longDst, + CHECKSUM_TYPE_MODULAR, 1); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, pdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); +} + +TEST_F(PduTest, MetadataDeserializeFrom) { + // Test that MetadataPdu::deserializeFrom() works correctly + MetadataPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_RECEIVER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 100; + const TransactionSeq transactionSeq = 200; + const EntityId destEid = 300; + const FileSize fileSize = 2048; + const char* sourceFilename = "/ground/test_source.bin"; + const char* destFilename = "test/ut/output/test_dest.bin"; + const U8 closureRequested = 1; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + fileSize, sourceFilename, destFilename, CHECKSUM_TYPE_MODULAR, closureRequested); + + // Serialize to buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Deserialize from buffer using deserializeFrom() + MetadataPdu rxPdu; + const Fw::Buffer rxBuffer(buffer1, txBuffer.getSize()); + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header fields + const PduHeader& header = rxPdu.asHeader(); + EXPECT_EQ(T_METADATA, header.getType()); + EXPECT_EQ(direction, header.getDirection()); + EXPECT_EQ(txmMode, header.getTxmMode()); + EXPECT_EQ(sourceEid, header.getSourceEid()); + EXPECT_EQ(transactionSeq, header.getTransactionSeq()); + EXPECT_EQ(destEid, header.getDestEid()); + + // Verify metadata fields + EXPECT_EQ(fileSize, rxPdu.getFileSize()); + EXPECT_STREQ(sourceFilename, rxPdu.getSourceFilename().toChar()); + EXPECT_STREQ(destFilename, rxPdu.getDestFilename().toChar()); + EXPECT_EQ(closureRequested, rxPdu.getClosureRequested()); + EXPECT_EQ(CHECKSUM_TYPE_MODULAR, rxPdu.getChecksumType()); +} + +// ====================================================================== +// File Data PDU Tests +// ====================================================================== + +TEST_F(PduTest, FileDataBufferSize) { + FileDataPdu pdu; + const U8 testData[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, 100, sizeof(testData), testData); + + U32 size = pdu.getBufferSize(); + // Should include header + offset(4) + data(5) + ASSERT_GT(size, 0U); + // Verify expected size + U32 expectedSize = pdu.asHeader().getBufferSize() + 4 + sizeof(testData); + ASSERT_EQ(expectedSize, size); +} + +TEST_F(PduTest, FileDataRoundTrip) { + // Arrange - Create transmit PDU with test data + FileDataPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_RECEIVER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_1; + const EntityId sourceEid = 50; + const TransactionSeq transactionSeq = 100; + const EntityId destEid = 75; + const U32 fileOffset = 1024; + const U8 testData[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE}; + const U16 dataSize = sizeof(testData); + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + fileOffset, dataSize, testData); + + // Serialize to buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Deserialize from buffer + FileDataPdu rxPdu; + const Fw::Buffer rxBuffer(buffer1, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header fields + const PduHeader& header = rxPdu.asHeader(); + EXPECT_EQ(T_FILE_DATA, header.getType()); + EXPECT_EQ(direction, header.getDirection()); + EXPECT_EQ(txmMode, header.getTxmMode()); + EXPECT_EQ(sourceEid, header.getSourceEid()); + EXPECT_EQ(transactionSeq, header.getTransactionSeq()); + EXPECT_EQ(destEid, header.getDestEid()); + + // Verify file data fields + EXPECT_EQ(fileOffset, rxPdu.getOffset()); + EXPECT_EQ(dataSize, rxPdu.getDataSize()); + ASSERT_NE(nullptr, rxPdu.getData()); + EXPECT_EQ(0, memcmp(testData, rxPdu.getData(), dataSize)); +} + +TEST_F(PduTest, FileDataEmptyPayload) { + // Test with zero-length data + FileDataPdu pdu; + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, 0, 0, nullptr); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Should encode successfully even with no data + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, pdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); +} + +TEST_F(PduTest, FileDataLargePayload) { + // Test with maximum reasonable payload + const U16 largeSize = 1024; + U8 largeData[largeSize]; + for (U16 i = 0; i < largeSize; ++i) { + largeData[i] = static_cast(i & 0xFF); + } + + FileDataPdu pdu; + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, 999999, largeSize, largeData); + + U8 buffer[2048]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, pdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + FileDataPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(largeSize, rxPdu.getDataSize()); + EXPECT_EQ(0, memcmp(largeData, rxPdu.getData(), largeSize)); +} + +// ====================================================================== +// EOF PDU Tests +// ====================================================================== + +TEST_F(PduTest, EofBufferSize) { + EofPdu pdu; + pdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, 0x12345678, 4096); + + U32 size = pdu.getBufferSize(); + // Should include header + directive(1) + condition(1) + checksum(4) + filesize(sizeof(FileSize)) + ASSERT_GT(size, 0U); + U32 expectedSize = pdu.asHeader().getBufferSize() + sizeof(U8) + sizeof(U8) + sizeof(U32) + sizeof(FileSize); + ASSERT_EQ(expectedSize, size); +} + +TEST_F(PduTest, EofRoundTrip) { + // Arrange - Create transmit PDU + EofPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_RECEIVER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_1; + const EntityId sourceEid = 50; + const TransactionSeq transactionSeq = 100; + const EntityId destEid = 75; + const ConditionCode conditionCode = CONDITION_CODE_NO_ERROR; + const U32 checksum = 0xDEADBEEF; + const FileSize fileSize = 65536; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + conditionCode, checksum, fileSize); + + // Serialize to buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Deserialize from buffer + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer1, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header fields + const PduHeader& header = rxPdu.asHeader(); + EXPECT_EQ(T_EOF, header.getType()); + EXPECT_EQ(direction, header.getDirection()); + EXPECT_EQ(txmMode, header.getTxmMode()); + EXPECT_EQ(sourceEid, header.getSourceEid()); + EXPECT_EQ(transactionSeq, header.getTransactionSeq()); + EXPECT_EQ(destEid, header.getDestEid()); + + // Verify EOF-specific fields + EXPECT_EQ(conditionCode, rxPdu.getConditionCode()); + EXPECT_EQ(checksum, rxPdu.getChecksum()); + EXPECT_EQ(fileSize, rxPdu.getFileSize()); +} + +TEST_F(PduTest, EofWithError) { + // Test with error condition code + EofPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILE_CHECKSUM_FAILURE, 0, 0); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Should encode successfully even with error condition + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(CONDITION_CODE_FILE_CHECKSUM_FAILURE, rxPdu.getConditionCode()); +} + +TEST_F(PduTest, EofZeroValues) { + // Test with all zero values + EofPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, 0, 0); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(0U, rxPdu.getChecksum()); + EXPECT_EQ(0U, rxPdu.getFileSize()); +} + +TEST_F(PduTest, EofLargeValues) { + // Test with maximum U32 values + EofPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, 0xFFFFFFFF, 0xFFFFFFFF); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(0xFFFFFFFFU, rxPdu.getChecksum()); + EXPECT_EQ(0xFFFFFFFFU, rxPdu.getFileSize()); +} + +// ====================================================================== +// FIN PDU Tests +// ====================================================================== + +TEST_F(PduTest, FinBufferSize) { + FinPdu pdu; + pdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_RETAINED); + + U32 size = pdu.getBufferSize(); + // Should include header + directive(1) + flags(1) = header + 2 + ASSERT_GT(size, 0U); + U32 expectedSize = pdu.asHeader().getBufferSize() + 2; + ASSERT_EQ(expectedSize, size); +} + +TEST_F(PduTest, FinRoundTrip) { + // Arrange - Create transmit PDU + FinPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_SENDER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 50; + const TransactionSeq transactionSeq = 100; + const EntityId destEid = 75; + const ConditionCode conditionCode = CONDITION_CODE_NO_ERROR; + const FinDeliveryCode deliveryCode = FIN_DELIVERY_CODE_COMPLETE; + const FinFileStatus fileStatus = FIN_FILE_STATUS_RETAINED; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + conditionCode, deliveryCode, fileStatus); + + // Serialize to buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Deserialize from buffer + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer1, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header fields + const PduHeader& header = rxPdu.asHeader(); + EXPECT_EQ(T_FIN, header.getType()); + EXPECT_EQ(direction, header.getDirection()); + EXPECT_EQ(txmMode, header.getTxmMode()); + EXPECT_EQ(sourceEid, header.getSourceEid()); + EXPECT_EQ(transactionSeq, header.getTransactionSeq()); + EXPECT_EQ(destEid, header.getDestEid()); + + // Verify FIN-specific fields + EXPECT_EQ(conditionCode, rxPdu.getConditionCode()); + EXPECT_EQ(deliveryCode, rxPdu.getDeliveryCode()); + EXPECT_EQ(fileStatus, rxPdu.getFileStatus()); +} + +TEST_F(PduTest, FinWithError) { + // Test with error condition code + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILE_CHECKSUM_FAILURE, + FIN_DELIVERY_CODE_INCOMPLETE, FIN_FILE_STATUS_DISCARDED); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Should encode successfully even with error condition + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(CONDITION_CODE_FILE_CHECKSUM_FAILURE, rxPdu.getConditionCode()); + EXPECT_EQ(FIN_DELIVERY_CODE_INCOMPLETE, rxPdu.getDeliveryCode()); + EXPECT_EQ(FIN_FILE_STATUS_DISCARDED, rxPdu.getFileStatus()); +} + +TEST_F(PduTest, FinDeliveryIncomplete) { + // Test with incomplete delivery + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_INCOMPLETE, FIN_FILE_STATUS_RETAINED); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(FIN_DELIVERY_CODE_INCOMPLETE, rxPdu.getDeliveryCode()); + EXPECT_EQ(FIN_FILE_STATUS_RETAINED, rxPdu.getFileStatus()); +} + +TEST_F(PduTest, FinFileStatusDiscarded) { + // Test with file discarded + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_DISCARDED); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(FIN_DELIVERY_CODE_COMPLETE, rxPdu.getDeliveryCode()); + EXPECT_EQ(FIN_FILE_STATUS_DISCARDED, rxPdu.getFileStatus()); +} + +TEST_F(PduTest, FinFileStatusDiscardedFilestore) { + // Test with file discarded by filestore + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILESTORE_REJECTION, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_DISCARDED_FILESTORE); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(CONDITION_CODE_FILESTORE_REJECTION, rxPdu.getConditionCode()); + EXPECT_EQ(FIN_DELIVERY_CODE_COMPLETE, rxPdu.getDeliveryCode()); + EXPECT_EQ(FIN_FILE_STATUS_DISCARDED_FILESTORE, rxPdu.getFileStatus()); +} + +TEST_F(PduTest, FinBitPackingValidation) { + // Test all combinations to verify bit packing is correct + const FinDeliveryCode deliveryCodes[] = {FIN_DELIVERY_CODE_COMPLETE, FIN_DELIVERY_CODE_INCOMPLETE}; + const FinFileStatus fileStatuses[] = {FIN_FILE_STATUS_DISCARDED, FIN_FILE_STATUS_DISCARDED_FILESTORE, + FIN_FILE_STATUS_RETAINED, FIN_FILE_STATUS_UNREPORTED}; + + for (const auto& deliveryCode : deliveryCodes) { + for (const auto& fileStatus : fileStatuses) { + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, deliveryCode, fileStatus); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(deliveryCode, rxPdu.getDeliveryCode()) + << "Delivery code mismatch for combination: delivery=" + << static_cast(deliveryCode) << " fileStatus=" << static_cast(fileStatus); + EXPECT_EQ(fileStatus, rxPdu.getFileStatus()) + << "File status mismatch for combination: delivery=" + << static_cast(deliveryCode) << " fileStatus=" << static_cast(fileStatus); + } + } +} + +// ====================================================================== +// ACK PDU Tests +// ====================================================================== + +TEST_F(PduTest, AckBufferSize) { + AckPdu pdu; + pdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, FILE_DIRECTIVE_END_OF_FILE, 0, + CONDITION_CODE_NO_ERROR, ACK_TXN_STATUS_ACTIVE); + + U32 size = pdu.getBufferSize(); + // Should include header + directive(1) + directive_and_subtype(1) + cc_and_status(1) = header + 3 + ASSERT_GT(size, 0U); + U32 expectedSize = pdu.asHeader().getBufferSize() + 3; + ASSERT_EQ(expectedSize, size); +} + +TEST_F(PduTest, AckRoundTrip) { + // Arrange - Create transmit PDU + AckPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_SENDER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 50; + const TransactionSeq transactionSeq = 100; + const EntityId destEid = 75; + const FileDirective directiveCode = FILE_DIRECTIVE_END_OF_FILE; + const U8 directiveSubtypeCode = 0; + const ConditionCode conditionCode = CONDITION_CODE_NO_ERROR; + const AckTxnStatus transactionStatus = ACK_TXN_STATUS_ACTIVE; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + directiveCode, directiveSubtypeCode, conditionCode, transactionStatus); + + // Serialize to buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Deserialize from buffer + AckPdu rxPdu; + const Fw::Buffer rxBuffer(buffer1, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header fields + const PduHeader& header = rxPdu.asHeader(); + EXPECT_EQ(T_ACK, header.getType()); + EXPECT_EQ(direction, header.getDirection()); + EXPECT_EQ(txmMode, header.getTxmMode()); + EXPECT_EQ(sourceEid, header.getSourceEid()); + EXPECT_EQ(transactionSeq, header.getTransactionSeq()); + EXPECT_EQ(destEid, header.getDestEid()); + + // Verify ACK-specific fields + EXPECT_EQ(directiveCode, rxPdu.getDirectiveCode()); + EXPECT_EQ(directiveSubtypeCode, rxPdu.getDirectiveSubtypeCode()); + EXPECT_EQ(conditionCode, rxPdu.getConditionCode()); + EXPECT_EQ(transactionStatus, rxPdu.getTransactionStatus()); +} + +TEST_F(PduTest, AckForEof) { + // Test ACK for EOF directive + AckPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, FILE_DIRECTIVE_END_OF_FILE, 0, + CONDITION_CODE_NO_ERROR, ACK_TXN_STATUS_ACTIVE); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + AckPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(FILE_DIRECTIVE_END_OF_FILE, rxPdu.getDirectiveCode()); + EXPECT_EQ(CONDITION_CODE_NO_ERROR, rxPdu.getConditionCode()); + EXPECT_EQ(ACK_TXN_STATUS_ACTIVE, rxPdu.getTransactionStatus()); +} + +TEST_F(PduTest, AckForFin) { + // Test ACK for FIN directive + AckPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, FILE_DIRECTIVE_FIN, 0, + CONDITION_CODE_NO_ERROR, ACK_TXN_STATUS_TERMINATED); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + AckPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(FILE_DIRECTIVE_FIN, rxPdu.getDirectiveCode()); + EXPECT_EQ(ACK_TXN_STATUS_TERMINATED, rxPdu.getTransactionStatus()); +} + +TEST_F(PduTest, AckWithError) { + // Test ACK with error condition code + AckPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, FILE_DIRECTIVE_END_OF_FILE, 0, + CONDITION_CODE_FILE_CHECKSUM_FAILURE, ACK_TXN_STATUS_TERMINATED); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + AckPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(CONDITION_CODE_FILE_CHECKSUM_FAILURE, rxPdu.getConditionCode()); + EXPECT_EQ(ACK_TXN_STATUS_TERMINATED, rxPdu.getTransactionStatus()); +} + +TEST_F(PduTest, AckWithSubtype) { + // Test ACK with non-zero subtype code + AckPdu txPdu; + const U8 subtypeCode = 5; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, FILE_DIRECTIVE_FIN, subtypeCode, + CONDITION_CODE_NO_ERROR, ACK_TXN_STATUS_ACTIVE); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + AckPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(subtypeCode, rxPdu.getDirectiveSubtypeCode()); +} + +TEST_F(PduTest, AckBitPackingValidation) { + // Test various combinations to verify bit packing is correct + const FileDirective directives[] = {FILE_DIRECTIVE_END_OF_FILE, FILE_DIRECTIVE_FIN}; + const AckTxnStatus statuses[] = {ACK_TXN_STATUS_UNDEFINED, ACK_TXN_STATUS_ACTIVE, + ACK_TXN_STATUS_TERMINATED, ACK_TXN_STATUS_UNRECOGNIZED}; + const ConditionCode conditions[] = {CONDITION_CODE_NO_ERROR, CONDITION_CODE_FILE_CHECKSUM_FAILURE}; + + for (const auto& directive : directives) { + for (const auto& status : statuses) { + for (const auto& condition : conditions) { + AckPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, directive, 0, condition, status); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + AckPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(directive, rxPdu.getDirectiveCode()) + << "Directive mismatch for combination: dir=" + << static_cast(directive) << " status=" << static_cast(status) + << " condition=" << static_cast(condition); + EXPECT_EQ(status, rxPdu.getTransactionStatus()) + << "Status mismatch for combination: dir=" + << static_cast(directive) << " status=" << static_cast(status) + << " condition=" << static_cast(condition); + EXPECT_EQ(condition, rxPdu.getConditionCode()) + << "Condition mismatch for combination: dir=" + << static_cast(directive) << " status=" << static_cast(status) + << " condition=" << static_cast(condition); + } + } + } +} + +// ====================================================================== +// NAK PDU Tests +// ====================================================================== + +TEST_F(PduTest, NakBufferSize) { + NakPdu pdu; + pdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, 100, 500); + + U32 size = pdu.getBufferSize(); + // Should include header + directive(1) + scope_start(4) + scope_end(4) = header + 9 + ASSERT_GT(size, 0U); + U32 expectedSize = pdu.asHeader().getBufferSize() + 9; + ASSERT_EQ(expectedSize, size); +} + +TEST_F(PduTest, NakRoundTrip) { + // Arrange - Create transmit PDU + NakPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_SENDER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 50; + const TransactionSeq transactionSeq = 100; + const EntityId destEid = 75; + const FileSize scopeStart = 1024; + const FileSize scopeEnd = 8192; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + scopeStart, scopeEnd); + + // Serialize to buffer + U8 buffer1[512]; + Fw::Buffer txBuffer(buffer1, sizeof(buffer1)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Deserialize from buffer + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer1, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header fields + const PduHeader& header = rxPdu.asHeader(); + EXPECT_EQ(T_NAK, header.getType()); + EXPECT_EQ(direction, header.getDirection()); + EXPECT_EQ(txmMode, header.getTxmMode()); + EXPECT_EQ(sourceEid, header.getSourceEid()); + EXPECT_EQ(transactionSeq, header.getTransactionSeq()); + EXPECT_EQ(destEid, header.getDestEid()); + + // Verify NAK-specific fields + EXPECT_EQ(scopeStart, rxPdu.getScopeStart()); + EXPECT_EQ(scopeEnd, rxPdu.getScopeEnd()); +} + +TEST_F(PduTest, NakZeroScope) { + // Test NAK with zero scope (start of file) + NakPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, 0, 1024); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(0U, rxPdu.getScopeStart()); + EXPECT_EQ(1024U, rxPdu.getScopeEnd()); +} + +TEST_F(PduTest, NakLargeScope) { + // Test NAK with large file offsets + NakPdu txPdu; + const FileSize largeStart = 0xFFFF0000; + const FileSize largeEnd = 0xFFFFFFFF; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, largeStart, largeEnd); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + ASSERT_GT(txBuffer.getSize(), 0U); + + // Verify round-trip + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(largeStart, rxPdu.getScopeStart()); + EXPECT_EQ(largeEnd, rxPdu.getScopeEnd()); +} + +TEST_F(PduTest, NakSingleByte) { + // Test NAK for single byte gap + NakPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, 1000, 1001); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(1000U, rxPdu.getScopeStart()); + EXPECT_EQ(1001U, rxPdu.getScopeEnd()); +} + +TEST_F(PduTest, NakMultipleCombinations) { + // Test various scope combinations + const FileSize testScopes[][2] = { + {0, 100}, + {512, 1024}, + {4096, 8192}, + {0x10000, 0x20000}, + {0x80000000, 0x90000000} + }; + + for (const auto& scope : testScopes) { + NakPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 10, 20, 30, scope[0], scope[1]); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(scope[0], rxPdu.getScopeStart()) + << "Scope start mismatch for range: " << scope[0] << "-" << scope[1]; + EXPECT_EQ(scope[1], rxPdu.getScopeEnd()) + << "Scope end mismatch for range: " << scope[0] << "-" << scope[1]; + } +} + +TEST_F(PduTest, NakWithSingleSegment) { + // Test NAK PDU with one segment request + NakPdu txPdu; + const FileSize scopeStart = 0; + const FileSize scopeEnd = 4096; + const FileSize segStart = 1024; + const FileSize segEnd = 2048; + + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, scopeStart, scopeEnd); + + ASSERT_TRUE(txPdu.addSegment(segStart, segEnd)); + EXPECT_EQ(1, txPdu.getNumSegments()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(scopeStart, rxPdu.getScopeStart()); + EXPECT_EQ(scopeEnd, rxPdu.getScopeEnd()); + EXPECT_EQ(1, rxPdu.getNumSegments()); + EXPECT_EQ(segStart, rxPdu.getSegment(0).offsetStart); + EXPECT_EQ(segEnd, rxPdu.getSegment(0).offsetEnd); +} + +TEST_F(PduTest, NakWithMultipleSegments) { + // Test NAK PDU with multiple segment requests + NakPdu txPdu; + const FileSize scopeStart = 0; + const FileSize scopeEnd = 10000; + + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, scopeStart, scopeEnd); + + // Add 5 segments representing gaps in received data + ASSERT_TRUE(txPdu.addSegment(100, 200)); + ASSERT_TRUE(txPdu.addSegment(500, 750)); + ASSERT_TRUE(txPdu.addSegment(1000, 1500)); + ASSERT_TRUE(txPdu.addSegment(3000, 4000)); + ASSERT_TRUE(txPdu.addSegment(8000, 9000)); + EXPECT_EQ(5, txPdu.getNumSegments()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(scopeStart, rxPdu.getScopeStart()); + EXPECT_EQ(scopeEnd, rxPdu.getScopeEnd()); + EXPECT_EQ(5, rxPdu.getNumSegments()); + + // Verify each segment + EXPECT_EQ(100, rxPdu.getSegment(0).offsetStart); + EXPECT_EQ(200, rxPdu.getSegment(0).offsetEnd); + EXPECT_EQ(500, rxPdu.getSegment(1).offsetStart); + EXPECT_EQ(750, rxPdu.getSegment(1).offsetEnd); + EXPECT_EQ(1000, rxPdu.getSegment(2).offsetStart); + EXPECT_EQ(1500, rxPdu.getSegment(2).offsetEnd); + EXPECT_EQ(3000, rxPdu.getSegment(3).offsetStart); + EXPECT_EQ(4000, rxPdu.getSegment(3).offsetEnd); + EXPECT_EQ(8000, rxPdu.getSegment(4).offsetStart); + EXPECT_EQ(9000, rxPdu.getSegment(4).offsetEnd); +} + +TEST_F(PduTest, NakWithMaxSegments) { + // Test NAK PDU with maximum number of segments (58) + NakPdu txPdu; + const FileSize scopeStart = 0; + const FileSize scopeEnd = 100000; + + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, scopeStart, scopeEnd); + + // Add 58 segments (CFDP_NAK_MAX_SEGMENTS) + for (U8 i = 0; i < 58; i++) { + FileSize start = i * 1000; + FileSize end = start + 500; + ASSERT_TRUE(txPdu.addSegment(start, end)) << "Failed to add segment " << static_cast(i); + } + EXPECT_EQ(58, txPdu.getNumSegments()); + + // Try to add one more - should fail + EXPECT_FALSE(txPdu.addSegment(60000, 61000)); + EXPECT_EQ(58, txPdu.getNumSegments()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + NakPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(scopeStart, rxPdu.getScopeStart()); + EXPECT_EQ(scopeEnd, rxPdu.getScopeEnd()); + EXPECT_EQ(58, rxPdu.getNumSegments()); + + // Spot check a few segments + EXPECT_EQ(0, rxPdu.getSegment(0).offsetStart); + EXPECT_EQ(500, rxPdu.getSegment(0).offsetEnd); + EXPECT_EQ(10000, rxPdu.getSegment(10).offsetStart); + EXPECT_EQ(10500, rxPdu.getSegment(10).offsetEnd); + EXPECT_EQ(57000, rxPdu.getSegment(57).offsetStart); + EXPECT_EQ(57500, rxPdu.getSegment(57).offsetEnd); +} + +TEST_F(PduTest, NakClearSegments) { + // Test clearSegments() functionality + NakPdu pdu; + pdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, 0, 4096); + + // Add segments + ASSERT_TRUE(pdu.addSegment(100, 200)); + ASSERT_TRUE(pdu.addSegment(300, 400)); + EXPECT_EQ(2, pdu.getNumSegments()); + + // Clear and verify + pdu.clearSegments(); + EXPECT_EQ(0, pdu.getNumSegments()); + + // Should be able to add new segments + ASSERT_TRUE(pdu.addSegment(500, 600)); + EXPECT_EQ(1, pdu.getNumSegments()); +} + +TEST_F(PduTest, NakBufferSizeWithSegments) { + // Test that bufferSize() correctly accounts for segments + NakPdu pdu; + pdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, 0, 4096); + + U32 baseSizeNoSegments = pdu.getBufferSize(); + + // Add one segment + ASSERT_TRUE(pdu.addSegment(100, 200)); + U32 sizeWithOneSegment = pdu.getBufferSize(); + EXPECT_EQ(baseSizeNoSegments + 8, sizeWithOneSegment); // 2 * sizeof(FileSize) = 8 + + // Add another segment + ASSERT_TRUE(pdu.addSegment(300, 400)); + U32 sizeWithTwoSegments = pdu.getBufferSize(); + EXPECT_EQ(baseSizeNoSegments + 16, sizeWithTwoSegments); // 4 * sizeof(FileSize) = 16 +} + +// ====================================================================== +// TLV Tests +// ====================================================================== + +TEST_F(PduTest, TlvCreateWithEntityId) { + // Test creating TLV with entity ID + Tlv tlv; + const EntityId testEid = 42; + + tlv.initialize(testEid); + + EXPECT_EQ(TLV_TYPE_ENTITY_ID, tlv.getType()); + EXPECT_EQ(sizeof(EntityId), tlv.getData().getLength()); + EXPECT_EQ(testEid, tlv.getData().getEntityId()); +} + +TEST_F(PduTest, TlvCreateWithRawData) { + // Test creating TLV with raw data + Tlv tlv; + const U8 testData[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + const U8 testDataLen = sizeof(testData); + + tlv.initialize(TLV_TYPE_MESSAGE_TO_USER, testData, testDataLen); + + EXPECT_EQ(TLV_TYPE_MESSAGE_TO_USER, tlv.getType()); + EXPECT_EQ(testDataLen, tlv.getData().getLength()); + EXPECT_EQ(0, memcmp(testData, tlv.getData().getData(), testDataLen)); +} + +TEST_F(PduTest, TlvEncodedSize) { + // Test TLV encoded size calculation + Tlv tlv; + const U8 testData[] = {0xAA, 0xBB, 0xCC}; + + tlv.initialize(TLV_TYPE_FLOW_LABEL, testData, sizeof(testData)); + + // Type(1) + Length(1) + Data(3) = 5 + EXPECT_EQ(5U, tlv.getEncodedSize()); +} + +TEST_F(PduTest, TlvEncodeDecodeEntityId) { + // Test encoding and decoding entity ID TLV + Tlv txTlv; + const EntityId testEid = 123; + txTlv.initialize(testEid); + + U8 buffer[256]; + Fw::SerialBuffer serialBuffer(buffer, sizeof(buffer)); + + // Encode + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txTlv.toSerialBuffer(serialBuffer)); + + // Decode + serialBuffer.resetSer(); + serialBuffer.fill(); + Tlv rxTlv; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxTlv.fromSerialBuffer(serialBuffer)); + + // Verify + EXPECT_EQ(TLV_TYPE_ENTITY_ID, rxTlv.getType()); + EXPECT_EQ(testEid, rxTlv.getData().getEntityId()); +} + +TEST_F(PduTest, TlvEncodeDecodeRawData) { + // Test encoding and decoding raw data TLV + Tlv txTlv; + const U8 testData[] = {0xDE, 0xAD, 0xBE, 0xEF}; + txTlv.initialize(TLV_TYPE_MESSAGE_TO_USER, testData, sizeof(testData)); + + U8 buffer[256]; + Fw::SerialBuffer serialBuffer(buffer, sizeof(buffer)); + + // Encode + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txTlv.toSerialBuffer(serialBuffer)); + + // Decode + serialBuffer.resetSer(); + serialBuffer.fill(); + Tlv rxTlv; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxTlv.fromSerialBuffer(serialBuffer)); + + // Verify + EXPECT_EQ(TLV_TYPE_MESSAGE_TO_USER, rxTlv.getType()); + EXPECT_EQ(sizeof(testData), rxTlv.getData().getLength()); + EXPECT_EQ(0, memcmp(testData, rxTlv.getData().getData(), sizeof(testData))); +} + +TEST_F(PduTest, TlvEncodeDecodeMaxData) { + // Test TLV with maximum data length (255 bytes) + Tlv txTlv; + U8 testData[255]; + for (U16 i = 0; i < 255; i++) { + testData[i] = static_cast(i); + } + txTlv.initialize(TLV_TYPE_MESSAGE_TO_USER, testData, 255); + + U8 buffer[512]; + Fw::SerialBuffer serialBuffer(buffer, sizeof(buffer)); + + // Encode + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txTlv.toSerialBuffer(serialBuffer)); + + // Decode + serialBuffer.resetSer(); + serialBuffer.fill(); + Tlv rxTlv; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxTlv.fromSerialBuffer(serialBuffer)); + + // Verify + EXPECT_EQ(255, rxTlv.getData().getLength()); + EXPECT_EQ(0, memcmp(testData, rxTlv.getData().getData(), 255)); +} + +// ====================================================================== +// TlvList Tests +// ====================================================================== + +TEST_F(PduTest, TlvListAppendUpToMax) { + // Test appending TLVs up to maximum (4) + TlvList list; + + for (U8 i = 0; i < CFDP_MAX_TLV; i++) { + Tlv tlv; + tlv.initialize(static_cast(100 + i)); + ASSERT_TRUE(list.appendTlv(tlv)) << "Failed to append TLV " << static_cast(i); + } + + EXPECT_EQ(CFDP_MAX_TLV, list.getNumTlv()); +} + +TEST_F(PduTest, TlvListRejectWhenFull) { + // Test that appending fails when list is full + TlvList list; + + // Fill the list + for (U8 i = 0; i < CFDP_MAX_TLV; i++) { + Tlv tlv; + tlv.initialize(static_cast(i)); + ASSERT_TRUE(list.appendTlv(tlv)); + } + + // Try to add one more - should fail + Tlv extraTlv; + extraTlv.initialize(999); + EXPECT_FALSE(list.appendTlv(extraTlv)); + EXPECT_EQ(CFDP_MAX_TLV, list.getNumTlv()); +} + +TEST_F(PduTest, TlvListClear) { + // Test clearing TLV list + TlvList list; + + // Add some TLVs + for (U8 i = 0; i < 3; i++) { + Tlv tlv; + tlv.initialize(static_cast(i)); + ASSERT_TRUE(list.appendTlv(tlv)); + } + EXPECT_EQ(3, list.getNumTlv()); + + // Clear and verify + list.clear(); + EXPECT_EQ(0, list.getNumTlv()); + + // Should be able to add new TLVs + Tlv tlv; + tlv.initialize(100); + ASSERT_TRUE(list.appendTlv(tlv)); + EXPECT_EQ(1, list.getNumTlv()); +} + +TEST_F(PduTest, TlvListEncodedSize) { + // Test TLV list encoded size calculation + TlvList list; + + // Add TLVs of different sizes + Tlv tlv1; + tlv1.initialize(42); // Entity ID TLV + ASSERT_TRUE(list.appendTlv(tlv1)); + + const U8 data[] = {0x01, 0x02, 0x03}; + Tlv tlv2; + tlv2.initialize(TLV_TYPE_MESSAGE_TO_USER, data, sizeof(data)); + ASSERT_TRUE(list.appendTlv(tlv2)); + + U32 expectedSize = tlv1.getEncodedSize() + tlv2.getEncodedSize(); + EXPECT_EQ(expectedSize, list.getEncodedSize()); +} + +TEST_F(PduTest, TlvListEncodeDecode) { + // Test encoding and decoding TLV list + TlvList txList; + + // Add multiple TLVs + Tlv tlv1; + tlv1.initialize(123); + ASSERT_TRUE(txList.appendTlv(tlv1)); + + const U8 data2[] = {0xAA, 0xBB}; + Tlv tlv2; + tlv2.initialize(TLV_TYPE_MESSAGE_TO_USER, data2, sizeof(data2)); + ASSERT_TRUE(txList.appendTlv(tlv2)); + + const U8 data3[] = {0xDE, 0xAD, 0xBE, 0xEF}; + Tlv tlv3; + tlv3.initialize(TLV_TYPE_FLOW_LABEL, data3, sizeof(data3)); + ASSERT_TRUE(txList.appendTlv(tlv3)); + + U8 buffer[512]; + Fw::SerialBuffer serialBuffer(buffer, sizeof(buffer)); + + // Encode + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txList.toSerialBuffer(serialBuffer)); + U32 encodedSize = static_cast(serialBuffer.getSize()); + + // Decode + Fw::SerialBuffer decodeBuffer(buffer, encodedSize); + decodeBuffer.fill(); + TlvList rxList; + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxList.fromSerialBuffer(decodeBuffer)); + + // Verify + EXPECT_EQ(3, rxList.getNumTlv()); + EXPECT_EQ(TLV_TYPE_ENTITY_ID, rxList.getTlv(0).getType()); + EXPECT_EQ(123, rxList.getTlv(0).getData().getEntityId()); + EXPECT_EQ(TLV_TYPE_MESSAGE_TO_USER, rxList.getTlv(1).getType()); + EXPECT_EQ(TLV_TYPE_FLOW_LABEL, rxList.getTlv(2).getType()); +} + +// ====================================================================== +// EOF PDU with TLV Tests +// ====================================================================== + +TEST_F(PduTest, EofWithNoTlvs) { + // Verify existing EOF tests work with TLV support (backward compatible) + EofPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, 0x12345678, 4096); + + EXPECT_EQ(0, txPdu.getNumTlv()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(0, rxPdu.getNumTlv()); +} + +TEST_F(PduTest, EofWithOneTlv) { + // Test EOF PDU with one TLV + EofPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILE_CHECKSUM_FAILURE, 0, 0); + + // Add entity ID TLV + Tlv tlv; + tlv.initialize(42); + ASSERT_TRUE(txPdu.appendTlv(tlv)); + EXPECT_EQ(1, txPdu.getNumTlv()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(CONDITION_CODE_FILE_CHECKSUM_FAILURE, rxPdu.getConditionCode()); + EXPECT_EQ(1, rxPdu.getNumTlv()); + EXPECT_EQ(TLV_TYPE_ENTITY_ID, rxPdu.getTlvList().getTlv(0).getType()); + EXPECT_EQ(42, rxPdu.getTlvList().getTlv(0).getData().getEntityId()); +} + +TEST_F(PduTest, EofWithMultipleTlvs) { + // Test EOF PDU with multiple TLVs + EofPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILESTORE_REJECTION, 0xABCDEF, 2048); + + // Add entity ID TLV + Tlv tlv1; + tlv1.initialize(123); + ASSERT_TRUE(txPdu.appendTlv(tlv1)); + + // Add message to user TLV + const U8 message[] = "Error: File rejected"; + Tlv tlv2; + tlv2.initialize(TLV_TYPE_MESSAGE_TO_USER, message, sizeof(message) - 1); + ASSERT_TRUE(txPdu.appendTlv(tlv2)); + + EXPECT_EQ(2, txPdu.getNumTlv()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(2, rxPdu.getNumTlv()); + EXPECT_EQ(TLV_TYPE_ENTITY_ID, rxPdu.getTlvList().getTlv(0).getType()); + EXPECT_EQ(TLV_TYPE_MESSAGE_TO_USER, rxPdu.getTlvList().getTlv(1).getType()); +} + +TEST_F(PduTest, EofTlvBufferSize) { + // Verify buffer size calculation includes TLVs + EofPdu pdu1, pdu2; + pdu1.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, 0, 0); + pdu2.initialize(DIRECTION_TOWARD_RECEIVER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, 0, 0); + + U32 sizeWithoutTlv = pdu1.getBufferSize(); + + // Add TLV to second PDU + Tlv tlv; + tlv.initialize(42); + ASSERT_TRUE(pdu2.appendTlv(tlv)); + + U32 sizeWithTlv = pdu2.getBufferSize(); + EXPECT_EQ(sizeWithoutTlv + tlv.getEncodedSize(), sizeWithTlv); +} + +TEST_F(PduTest, EofTlvRoundTripComplete) { + // Comprehensive round-trip test with TLVs + EofPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_RECEIVER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 10; + const TransactionSeq transactionSeq = 20; + const EntityId destEid = 30; + const ConditionCode conditionCode = CONDITION_CODE_FILE_SIZE_ERROR; + const U32 checksum = 0xDEADBEEF; + const FileSize fileSize = 8192; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + conditionCode, checksum, fileSize); + + // Add TLVs + Tlv tlv1; + tlv1.initialize(sourceEid); + ASSERT_TRUE(txPdu.appendTlv(tlv1)); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Decode + EofPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header + EXPECT_EQ(direction, rxPdu.asHeader().getDirection()); + EXPECT_EQ(txmMode, rxPdu.asHeader().getTxmMode()); + EXPECT_EQ(sourceEid, rxPdu.asHeader().getSourceEid()); + EXPECT_EQ(transactionSeq, rxPdu.asHeader().getTransactionSeq()); + EXPECT_EQ(destEid, rxPdu.asHeader().getDestEid()); + + // Verify EOF fields + EXPECT_EQ(conditionCode, rxPdu.getConditionCode()); + EXPECT_EQ(checksum, rxPdu.getChecksum()); + EXPECT_EQ(fileSize, rxPdu.getFileSize()); + + // Verify TLVs + EXPECT_EQ(1, rxPdu.getNumTlv()); + EXPECT_EQ(sourceEid, rxPdu.getTlvList().getTlv(0).getData().getEntityId()); +} + +// ====================================================================== +// FIN PDU with TLV Tests +// ====================================================================== + +TEST_F(PduTest, FinWithNoTlvs) { + // Verify existing FIN tests work with TLV support (backward compatible) + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_RETAINED); + + EXPECT_EQ(0, txPdu.getNumTlv()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + EXPECT_EQ(0, rxPdu.getNumTlv()); +} + +TEST_F(PduTest, FinWithOneTlv) { + // Test FIN PDU with one TLV + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILE_CHECKSUM_FAILURE, + FIN_DELIVERY_CODE_INCOMPLETE, FIN_FILE_STATUS_DISCARDED); + + // Add entity ID TLV + Tlv tlv; + tlv.initialize(99); + ASSERT_TRUE(txPdu.appendTlv(tlv)); + EXPECT_EQ(1, txPdu.getNumTlv()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(CONDITION_CODE_FILE_CHECKSUM_FAILURE, rxPdu.getConditionCode()); + EXPECT_EQ(FIN_DELIVERY_CODE_INCOMPLETE, rxPdu.getDeliveryCode()); + EXPECT_EQ(FIN_FILE_STATUS_DISCARDED, rxPdu.getFileStatus()); + EXPECT_EQ(1, rxPdu.getNumTlv()); + EXPECT_EQ(TLV_TYPE_ENTITY_ID, rxPdu.getTlvList().getTlv(0).getType()); + EXPECT_EQ(99, rxPdu.getTlvList().getTlv(0).getData().getEntityId()); +} + +TEST_F(PduTest, FinWithMultipleTlvs) { + // Test FIN PDU with multiple TLVs + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_FILESTORE_REJECTION, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_DISCARDED_FILESTORE); + + // Add entity ID TLV + Tlv tlv1; + tlv1.initialize(456); + ASSERT_TRUE(txPdu.appendTlv(tlv1)); + + // Add message to user TLV + const U8 message[] = "Transaction failed"; + Tlv tlv2; + tlv2.initialize(TLV_TYPE_MESSAGE_TO_USER, message, sizeof(message) - 1); + ASSERT_TRUE(txPdu.appendTlv(tlv2)); + + // Add flow label TLV + const U8 flowLabel[] = {0x01, 0x02}; + Tlv tlv3; + tlv3.initialize(TLV_TYPE_FLOW_LABEL, flowLabel, sizeof(flowLabel)); + ASSERT_TRUE(txPdu.appendTlv(tlv3)); + + EXPECT_EQ(3, txPdu.getNumTlv()); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Verify round-trip + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(3, rxPdu.getNumTlv()); + EXPECT_EQ(TLV_TYPE_ENTITY_ID, rxPdu.getTlvList().getTlv(0).getType()); + EXPECT_EQ(TLV_TYPE_MESSAGE_TO_USER, rxPdu.getTlvList().getTlv(1).getType()); + EXPECT_EQ(TLV_TYPE_FLOW_LABEL, rxPdu.getTlvList().getTlv(2).getType()); +} + +TEST_F(PduTest, FinTlvBufferSize) { + // Verify buffer size calculation includes TLVs + FinPdu pdu1, pdu2; + pdu1.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_RETAINED); + pdu2.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_RETAINED); + + U32 sizeWithoutTlv = pdu1.getBufferSize(); + + // Add TLV to second PDU + Tlv tlv; + tlv.initialize(789); + ASSERT_TRUE(pdu2.appendTlv(tlv)); + + U32 sizeWithTlv = pdu2.getBufferSize(); + EXPECT_EQ(sizeWithoutTlv + tlv.getEncodedSize(), sizeWithTlv); +} + +TEST_F(PduTest, FinTlvRoundTripComplete) { + // Comprehensive round-trip test with TLVs + FinPdu txPdu; + const PduDirection direction = DIRECTION_TOWARD_SENDER; + const Cfdp::Class::T txmMode = Cfdp::Class::CLASS_2; + const EntityId sourceEid = 50; + const TransactionSeq transactionSeq = 100; + const EntityId destEid = 75; + const ConditionCode conditionCode = CONDITION_CODE_INACTIVITY_DETECTED; + const FinDeliveryCode deliveryCode = FIN_DELIVERY_CODE_INCOMPLETE; + const FinFileStatus fileStatus = FIN_FILE_STATUS_RETAINED; + + txPdu.initialize(direction, txmMode, sourceEid, transactionSeq, destEid, + conditionCode, deliveryCode, fileStatus); + + // Add TLVs + Tlv tlv1; + tlv1.initialize(destEid); + ASSERT_TRUE(txPdu.appendTlv(tlv1)); + + const U8 msg[] = "Timeout"; + Tlv tlv2; + tlv2.initialize(TLV_TYPE_MESSAGE_TO_USER, msg, sizeof(msg) - 1); + ASSERT_TRUE(txPdu.appendTlv(tlv2)); + + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + // Decode + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + // Verify header + EXPECT_EQ(direction, rxPdu.asHeader().getDirection()); + EXPECT_EQ(txmMode, rxPdu.asHeader().getTxmMode()); + EXPECT_EQ(sourceEid, rxPdu.asHeader().getSourceEid()); + EXPECT_EQ(transactionSeq, rxPdu.asHeader().getTransactionSeq()); + EXPECT_EQ(destEid, rxPdu.asHeader().getDestEid()); + + // Verify FIN fields + EXPECT_EQ(conditionCode, rxPdu.getConditionCode()); + EXPECT_EQ(deliveryCode, rxPdu.getDeliveryCode()); + EXPECT_EQ(fileStatus, rxPdu.getFileStatus()); + + // Verify TLVs + EXPECT_EQ(2, rxPdu.getNumTlv()); + EXPECT_EQ(destEid, rxPdu.getTlvList().getTlv(0).getData().getEntityId()); + EXPECT_EQ(0, memcmp(msg, rxPdu.getTlvList().getTlv(1).getData().getData(), sizeof(msg) - 1)); +} + +TEST_F(PduTest, FinWithMaxTlvs) { + // Test FIN PDU with maximum number of TLVs (4) + FinPdu txPdu; + txPdu.initialize(DIRECTION_TOWARD_SENDER, Cfdp::Class::CLASS_2, + 1, 2, 3, CONDITION_CODE_NO_ERROR, + FIN_DELIVERY_CODE_COMPLETE, FIN_FILE_STATUS_RETAINED); + + // Add 4 TLVs + for (U8 i = 0; i < CFDP_MAX_TLV; i++) { + Tlv tlv; + tlv.initialize(static_cast(100 + i)); + ASSERT_TRUE(txPdu.appendTlv(tlv)) << "Failed to append TLV " << static_cast(i); + } + EXPECT_EQ(CFDP_MAX_TLV, txPdu.getNumTlv()); + + // Try to add one more - should fail + Tlv extraTlv; + extraTlv.initialize(999); + EXPECT_FALSE(txPdu.appendTlv(extraTlv)); + + // Verify round-trip with 4 TLVs + U8 buffer[512]; + Fw::Buffer txBuffer(buffer, sizeof(buffer)); + // Serialize using SerialBuffer wrapper + Fw::SerialBuffer sb_txBuffer(txBuffer.getData(), txBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, txPdu.serializeTo(sb_txBuffer)); + txBuffer.setSize(sb_txBuffer.getSize()); + + FinPdu rxPdu; + const Fw::Buffer rxBuffer(buffer, txBuffer.getSize()); + // Deserialize using SerialBuffer wrapper + Fw::SerialBuffer sb_rxBuffer(const_cast(rxBuffer.getData()), rxBuffer.getSize()); + sb_rxBuffer.setBuffLen(rxBuffer.getSize()); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, rxPdu.deserializeFrom(sb_rxBuffer)); + + EXPECT_EQ(CFDP_MAX_TLV, rxPdu.getNumTlv()); + for (U8 i = 0; i < CFDP_MAX_TLV; i++) { + EXPECT_EQ(100 + i, rxPdu.getTlvList().getTlv(i).getData().getEntityId()); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Types/test/ut/data/test_file.bin b/Svc/Ccsds/CfdpManager/Types/test/ut/data/test_file.bin new file mode 100644 index 00000000000..4c8a59e735e --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Types/test/ut/data/test_file.bin @@ -0,0 +1,5 @@ +This is a test file for CFDP file data PDU testing. +It contains multiple lines of text data. +Line 3: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +Line 4: The quick brown fox jumps over the lazy dog. +Line 5: 0123456789ABCDEF0123456789ABCDEF0123456789 diff --git a/Svc/Ccsds/CfdpManager/Utils.cpp b/Svc/Ccsds/CfdpManager/Utils.cpp new file mode 100644 index 00000000000..0396bbb4eac --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Utils.cpp @@ -0,0 +1,191 @@ +// ====================================================================== +// \title Utils.cpp +// \brief CFDP utility functions +// +// This file is a port of the cf_utils.c file from the +// NASA Core Flight System (cFS) CFDP (CF) Application, +// version 3.0.0, adapted for use within the F-Prime (F') framework. +// +// The CFDP general utility functions source file +// +// Various odds and ends are put here. +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +AckTxnStatus GetTxnStatus(Transaction *txn) +{ + AckTxnStatus LocalStatus; + + // check if this is still an active Tx (not in holdover or drop etc) + // in theory this should never be called on S1 because there is no fin-ack to send, + // but including it for completeness (because it is an active txn) + if (txn == NULL) + { + LocalStatus = ACK_TXN_STATUS_UNRECOGNIZED; + } + else + switch (txn->getState()) + { + case TXN_STATE_S1: + case TXN_STATE_R1: + case TXN_STATE_S2: + case TXN_STATE_R2: + LocalStatus = ACK_TXN_STATUS_ACTIVE; + break; + + case TXN_STATE_DROP: + case TXN_STATE_HOLD: + LocalStatus = ACK_TXN_STATUS_TERMINATED; + break; + + default: + LocalStatus = ACK_TXN_STATUS_INVALID; + break; + } + + return LocalStatus; +} + +// Static member function - can access private members +CListTraverseStatus Transaction::findBySequenceNumberCallback(CListNode *node, void *context) +{ + Transaction *txn = container_of_cpp(node, &Transaction::m_cl_node); + CListTraverseStatus ret = CLIST_TRAVERSE_CONTINUE; + CfdpTraverseTransSeqArg* seqContext = static_cast(context); + + if (txn->m_history && (txn->m_history->src_eid == seqContext->src_eid) && + (txn->m_history->seq_num == seqContext->transaction_sequence_number)) + { + seqContext->txn = txn; + ret = CLIST_TRAVERSE_EXIT; // exit early + } + + return ret; +} + +// Static member function - can access private members +CListTraverseStatus Transaction::prioritySearchCallback(CListNode *node, void *context) +{ + Transaction * txn = container_of_cpp(node, &Transaction::m_cl_node); + CfdpTraversePriorityArg *arg = static_cast(context); + + if (txn->m_priority <= arg->priority) + { + // found it! + // + // the current transaction's prio is less than desired (higher) + arg->txn = txn; + return CLIST_TRAVERSE_EXIT; + } + + return CLIST_TRAVERSE_CONTINUE; +} + +// Legacy wrappers for backward compatibility +CListTraverseStatus FindTransactionBySequenceNumberImpl(CListNode *node, void *context) +{ + return Transaction::findBySequenceNumberCallback(node, context); +} + +CListTraverseStatus PrioSearch(CListNode *node, void *context) +{ + return Transaction::prioritySearchCallback(node, context); +} + +bool TxnStatusIsError(TxnStatus txn_stat) +{ + // The value of TXN_STATUS_UNDEFINED (-1) indicates a transaction is in progress and no error + // has occurred yet. This will be set to TXN_STATUS_NO_ERROR (0) after successful completion + // of the transaction (FIN/EOF). Anything else indicates a problem has occurred. + return (txn_stat > TXN_STATUS_NO_ERROR); +} + +ConditionCode TxnStatusToConditionCode(TxnStatus txn_stat) +{ + ConditionCode result; + + if (!TxnStatusIsError(txn_stat)) + { + // If no status has been set (TXN_STATUS_UNDEFINED), treat that as NO_ERROR for + // the purpose of CFDP CC. This can occur e.g. when sending ACK PDUs and no errors + // have happened yet, but the transaction is not yet complete and thus not final. + result = CONDITION_CODE_NO_ERROR; + } + else + { + switch (txn_stat) + { + // The definition of TxnStatus is such that the 4-bit codes (0-15) share the same + // numeric values as the CFDP condition codes, and can be put directly into the 4-bit + // CC field of a FIN/ACK/EOF PDU. Extended codes use the upper bits (>15) to differentiate + case TXN_STATUS_NO_ERROR: + case TXN_STATUS_POS_ACK_LIMIT_REACHED: + case TXN_STATUS_KEEP_ALIVE_LIMIT_REACHED: + case TXN_STATUS_INVALID_TRANSMISSION_MODE: + case TXN_STATUS_FILESTORE_REJECTION: + case TXN_STATUS_FILE_CHECKSUM_FAILURE: + case TXN_STATUS_FILE_SIZE_ERROR: + case TXN_STATUS_NAK_LIMIT_REACHED: + case TXN_STATUS_INACTIVITY_DETECTED: + case TXN_STATUS_INVALID_FILE_STRUCTURE: + case TXN_STATUS_CHECK_LIMIT_REACHED: + case TXN_STATUS_UNSUPPORTED_CHECKSUM_TYPE: + case TXN_STATUS_SUSPEND_REQUEST_RECEIVED: + case TXN_STATUS_CANCEL_REQUEST_RECEIVED: + result = static_cast(txn_stat); + break; + + // Extended status codes below here --- + // There are no CFDP CCs to directly represent these status codes. Normally this should + // not happen as the engine should not be sending a CFDP CC (FIN/ACK/EOF PDU) for a + // transaction that is not in a valid CFDP-defined state. This should be translated + // to the closest CFDP CC per the intent/meaning of the transaction status code. + + case TXN_STATUS_ACK_LIMIT_NO_FIN: + case TXN_STATUS_ACK_LIMIT_NO_EOF: + // this is similar to the inactivity timeout (no fin-ack) + result = CONDITION_CODE_INACTIVITY_DETECTED; + break; + + default: + // Catch-all: any invalid protocol state will cancel the transaction, and thus this + // is the closest CFDP CC in practice for all other unhandled errors. + result = CONDITION_CODE_CANCEL_REQUEST_RECEIVED; + break; + } + } + + return result; +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/Utils.hpp b/Svc/Ccsds/CfdpManager/Utils.hpp new file mode 100644 index 00000000000..1ceb276ed95 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/Utils.hpp @@ -0,0 +1,146 @@ +// ====================================================================== +// \title Utils.hpp +// \brief CFDP utilities header +// +// This file is a port of CFDP utility functions from the following files +// from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0, +// adapted for use within the F-Prime (F') framework: +// - cf_utils.h (CFDP utility function declarations) +// +// CFDP utils header file +// +// ====================================================================== +// +// NASA Docket No. GSC-18,447-1 +// +// Copyright (c) 2019 United States Government as represented by the +// Administrator of the National Aeronautics and Space Administration. +// All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ====================================================================== + +#ifndef CFDP_UTILS_HPP +#define CFDP_UTILS_HPP + +#include + +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +/** + * @brief Argument structure for use with CList_Traverse() + * + * This identifies a specific transaction sequence number and entity ID + * The transaction pointer is set by the implementation + */ +struct CfdpTraverseTransSeqArg +{ + TransactionSeq transaction_sequence_number; + EntityId src_eid; + Transaction * txn; /**< \brief output transaction pointer */ +}; + +/** + * @brief Argument structure for use with Channel::traverseAllTransactions() + * + * This basically allows for running a traversal on several lists at once + */ +struct CfdpTraverseAllArg +{ + CfdpTraverseAllTransactionsFunc fn; /**< \brief internal callback to use for each CList_Traverse */ + void * context; /**< \brief opaque object to pass to internal callback */ + I32 counter; /**< \brief Running tally of all nodes traversed from all lists */ +}; + +/** + * @brief Argument structure for use with CfdpCListTraverseR() + * + * This is for searching for transactions of a specific priority + */ +struct CfdpTraversePriorityArg +{ + Transaction *txn; /**< \brief OUT: holds value of transaction with which to call CfdpCListInsertAfter on */ + U8 priority; /**< \brief seeking this priority */ +}; + +/************************************************************************/ +/** @brief List traversal function to check if the desired sequence number matches. + * + * @param node Pointer to node currently being traversed + * @param context Pointer to state object passed through from initial call + * + * @retval 1 when it's found, which terminates list traversal + * @retval 0 when it isn't found, which causes list traversal to continue + */ +CListTraverseStatus FindTransactionBySequenceNumberImpl(CListNode *node, void *context); + +/************************************************************************/ +/** @brief Searches for the first transaction with a lower priority than given. + * + * @param node Node being currently traversed + * @param context Pointer to CfdpTraversePriorityArg object indicating the priority to search for + * + * @retval CFDP_CLIST_EXIT when it's found, which terminates list traversal + * @retval CFDP_CLIST_CONT when it isn't found, which causes list traversal to continue + */ +CListTraverseStatus PrioSearch(CListNode *node, void *context); + +/************************************************************************/ +/** @brief Converts the internal transaction status to a CFDP condition code + * + * Transaction status is a superset of condition codes, and includes + * other error conditions for which CFDP will not send FIN/ACK/EOF + * and thus there is no corresponding condition code. + * + * @param txn_stat Transaction status + * + * @returns CFDP protocol condition code + */ +ConditionCode TxnStatusToConditionCode(TxnStatus txn_stat); + +/************************************************************************/ +/** @brief Check if the internal transaction status represents an error + * + * Transaction status is a superset of condition codes, and includes + * other error conditions for which CFDP will not send FIN/ACK/EOF + * and thus there is no corresponding condition code. + * + * @param txn_stat Transaction status + * + * @returns Boolean value indicating if the transaction is in an errored state + * @retval true if an error has occurred during the transaction + * @retval false if no error has occurred during the transaction yet + */ +bool TxnStatusIsError(TxnStatus txn_stat); + +/************************************************************************/ +/** @brief Gets the status of this transaction + * + * Determines if the transaction is ACTIVE or TERMINATED. + * (By definition if it has a txn object then it is not UNRECOGNIZED) + * + * @param txn Transaction + * @returns AckTxnStatus value corresponding to transaction + */ +AckTxnStatus GetTxnStatus(Transaction *txn); + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif /* !CFDP_UTILS_HPP */ diff --git a/Svc/Ccsds/CfdpManager/docs/img/CfdpManager.drawio b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager.drawio new file mode 100644 index 00000000000..90d80999992 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager.drawio @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/docs/img/CfdpManager.png b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager.png new file mode 100644 index 00000000000..7e25c507100 Binary files /dev/null and b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager.png differ diff --git a/Svc/Ccsds/CfdpManager/docs/img/CfdpManager_usage.drawio b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager_usage.drawio new file mode 100644 index 00000000000..ebf66319bcd --- /dev/null +++ b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager_usage.drawio @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Svc/Ccsds/CfdpManager/docs/img/CfdpManager_usage.png b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager_usage.png new file mode 100644 index 00000000000..6d8805439a7 Binary files /dev/null and b/Svc/Ccsds/CfdpManager/docs/img/CfdpManager_usage.png differ diff --git a/Svc/Ccsds/CfdpManager/docs/sdd.md b/Svc/Ccsds/CfdpManager/docs/sdd.md new file mode 100644 index 00000000000..a80bee35faf --- /dev/null +++ b/Svc/Ccsds/CfdpManager/docs/sdd.md @@ -0,0 +1,551 @@ +# Ccsds::CfdpManager + +## CFDP Introduction + +The CCSDS File Delivery Protocol (CFDP) is a space communication standard designed for reliable, autonomous file transfer in space missions. CFDP provides a robust mechanism for transferring files between ground systems and spacecraft even in environments with long propagation delays, intermittent connectivity, or high error rates. + +CFDP is particularly well-suited for: +- Spacecraft-to-ground file transfers: Downlinking F' event logs, telemetry data files, science data products, and diagnostic files +- Ground-to-spacecraft file transfers: Uplinking F' flight software updates, parameter files, and command sequences +- Delay-tolerant and disruption-tolerant delivery: Automatic retry and recovery mechanisms for challenging communication links + +The protocol supports two operational modes: +- Class 1 (Unacknowledged): Unreliable transfer with no acknowledgments, suitable for real-time or non-critical data where speed is prioritized +- Class 2 (Acknowledged): Reliable transfer with acknowledgments, retransmissions, and gap detection, ensuring complete and verified file delivery + +### Protocol Data Units (PDUs) + +CFDP uses Protocol Data Units (PDUs) - structured messages with a common header and type-specific payloads: + + - Metadata: Initiates transfer with filenames, file size, and options + - File Data: Carries file content segments with offset information + - EOF: Signals completion of file transmission with checksum + - FIN: Reports final delivery status (Class 2 only) + - ACK: Confirms receipt of EOF or FIN (Class 2 only) + - NAK: Requests retransmission of missing segments (Class 2 only) + +For complete protocol details, refer to the [CCSDS 727.0-B-5 - CCSDS File Delivery Protocol (CFDP)](https://ccsds.org/Pubs/727x0b5e1.pdf) Blue Book specification. + +## CFDP as an F' Component + +The CfdpManager component provides an F' implementation of the CFDP protocol and is designed to replace the standard F' [FileUplink](../../../FileUplink/docs/sdd.md) and [FileDownlink](../../../FileDownlink/docs/sdd.md) components with the addition of guaranteed file delivery. CfdpManager implements both CFDP Class 1 and Class 2 protocols, providing options for both unacknowledged and acknowledged transfers with retransmissions, gap detection, and reliable file delivery even over lossy or intermittent communication links. + +Substantial portions of this implementation were ported from [NASA's CF (CFDP) Application in the Core Flight System (cFS) version 3.0.0](https://github.com/nasa/CF/releases/tag/v3.0.0). The ported code includes: +- Core CFDP engine and transaction management logic +- Protocol state machines for transmit and receive operations +- Utility functions for file handling and resource management +- Chunk and gap tracking for Class 2 transfers + +The F' implementation adds new components built specifically for the F' ecosystem: +- CfdpManager component wrapper: Integrates CFDP into F' architecture with standard port interfaces, commands, events, telemetry, and parameters +- Object-oriented PDU encoding/decoding: Type-safe PDU classes based on F' `Serializable` interface for consistent serialization +- F' timer implementation: Uses F' time primitives for protocol timers + +For detailed attribution, licensing information, and a breakdown of ported vs. new code, see [ATTRIBUTION.md](../ATTRIBUTION.md). + +## Class Diagram + +The CfdpManager component diagram shows the port organization by functional grouping: + +![CfdpManager Component Diagram](img/CfdpManager.png) + +Ports are organized as follows: +- **Top (System Ports)**: Scheduling and system health - `run1Hz`, `pingIn`, `pingOut` +- **Left (Uplink Ports)**: Receive CFDP PDUs from remote entities - `dataIn`, `dataInReturn` +- **Right (Downlink Ports)**: Send CFDP PDUs to remote entities - `dataOut`, `dataReturnIn`, `bufferAllocate`, `bufferDeallocate` +- **Bottom (File Transfer Ports)**: Port-based file send interface - `fileIn`, `fileDoneOut` + +### Port Descriptions + +#### System Ports + +| Name | Type | Port Type | Description | +|------|------|-----------|-------------| +| run1Hz | async input | `Svc.Sched` | Scheduler port that must be invoked at 1 Hz to drive CFDP protocol timer logic, transaction processing, and state machine execution | +| pingIn | async input | `Svc.Ping` | Health check input port for liveness monitoring | +| pingOut | output | `Svc.Ping` | Health check output port for responding to pings | + +#### Downlink Ports + +| Name | Type | Port Type | Description | +|------|------|-----------|-------------| +| dataOut | output array[N] | `Fw.BufferSend` | Send encoded CFDP PDU data buffers to downstream components. One port (`N`) per CFDP channel. | +| dataReturnIn | async input array[N] | `Svc.ComDataWithContext` | Receive buffers previously sent via `dataOut` after downstream processing is complete. One port per CFDP channel. | +| bufferAllocate | output array[N] | `Fw.BufferGet` | Request allocation of buffers for constructing outgoing CFDP PDUs. One port (`N`) per CFDP channel. | +| bufferDeallocate | output array[N] | `Fw.BufferSend` | Return/deallocate buffers that were allocated but not sent (e.g., due to errors). One port (`N`) per CFDP channel. | + +#### Uplink Ports + +| Name | Type | Port Type | Description | +|------|------|-----------|-------------| +| dataIn | async input array[N] | `Fw.BufferSend` | Receive incoming CFDP PDU data buffers from upstream components (e.g., deframing, radio). One port (`N`) per CFDP channel. | +| dataInReturn | output array[N] | `Fw.BufferSend` | Return buffers received via `dataIn` after PDU processing is complete. One port (`N`) per CFDP channel. | + +#### File Transfer Ports + +| Name | Type | Port Type | Description | +|------|------|-----------|-------------| +| fileIn | guarded input | `Svc.SendFileRequest` | Programmatic file send request interface. Allows other components to initiate CFDP file transfers without using commands. Transaction arguments are populated from component parameters: `FileInDefaultChannel`, `FileInDefaultDestEntityId`, `FileInDefaultClass`, `FileInDefaultKeep`, and `FileInDefaultPriority`. The `offset` and `length` parameters are currently unsupported and must be `0`, or `STATUS_INVALID` is returned| +| fileDoneOut | output | `Svc.SendFileComplete` | Asynchronous notification of file transfer completion for transfers initiated via `fileIn` port. Provides final transfer status. Only invoked for port-initiated transactions (not command-initiated). | + +## Usage Examples + +The following diagram shows typical CfdpManager port connections with other F' components: + +![CfdpManager Usage Example](img/CfdpManager_usage.png) + +This example demonstrates: +- **Uplink data flow**: FprimeRouter deframes incoming CFDP PDUs and sends them to CfdpManager via `dataIn` +- **Downlink data flow**: CfdpManager sends outgoing CFDP PDUs to ComQueue via `dataOut` for transmission +- **Port-based file transfers**: DpCatalog initiates file transfers via CfdpManager's `fileIn` port and receives completion notifications via `fileDoneOut` + +## Component Design + +### Assumptions + +The design of `CfdpManager` assumes the following: + +1. File transfers occur by exchanging CFDP Protocol Data Units (PDUs) as defined in CCSDS 727.0-B-5. + +2. PDUs are transported in buffers provided by downstream components via the `bufferAllocate` port for transmission and received via the `dataIn` port from upstream components. + +3. Multiple file transfers can occur simultaneously, managed across configurable channels with independent transaction pools. + +4. Files are stored on non-volatile storage accessible via standard file I/O operations. + +5. The `run1Hz` port is invoked periodically at 1 Hz to drive protocol timers and state machine execution. + +6. For Class 2 transfers, the remote entity implements the CFDP protocol correctly and responds to PDUs according to the specification. + +7. Received files are written to a temporary directory (`ChannelConfig.tmp_dir` per-channel parameter) during transfer and moved to their final destination upon successful completion. + +8. Port-initiated file transfers (via `fileIn`) use default configuration parameters (`FileInDefaultChannel`, `FileInDefaultDestEntityId`, `FileInDefaultClass`, `FileInDefaultKeep`, and `FileInDefaultPriority`). + +### Main Class Hierarchy + +CfdpManager ([CfdpManager.hpp](../CfdpManager.hpp)) +- Top-level F' component that integrates CFDP into the F' framework +- Provides F' port handlers for commands, data input/output, and periodic execution +- Owns a single Engine instance and delegates all protocol operations to it +- Manages component parameters and provides events/telemetry to the F' system + +Engine ([Engine.hpp](../Engine.hpp)) +- Core protocol engine that manages CFDP lifecycle and operations +- Owns multiple Channel instances (one per configured CFDP channel) +- Handles PDU routing and dispatching to appropriate transactions +- Manages transaction creation, initialization, and cleanup +- Implements top-level protocol state machine coordination + +Channel ([Channel.hpp](../Channel.hpp)) +- Encapsulates channel-specific operations and configuration +- Owns a pool of Transaction instances for that channel +- Manages playback directories and polling directories +- Handles transaction queuing with priority-based scheduling +- Controls flow state (normal/frozen) and PDU throttling + +Transaction ([Transaction.hpp](../Transaction.hpp)) +- Represents individual file transfer operations +- Implements both TX (transmit) and RX (receive) state machines +- Handles Class 1 (unacknowledged) and Class 2 (acknowledged) protocol states +- Implementation split across [TransactionTx.cpp](../TransactionTx.cpp) and [TransactionRx.cpp](../TransactionRx.cpp) +- Manages file I/O, checksums, timers, and retry logic for each transaction + +### PDU Type Hierarchy + +PduBase ([Types/PduBase.hpp](../Types/PduBase.hpp)) +- Abstract base class for all CFDP Protocol Data Units +- Inherits from F' `Fw::Serializable` for consistent encoding/decoding +- Contains common `PduHeader` with transaction identification + +Concrete PDU types (all in [Types/](../Types/) directory): +- MetadataPdu ([MetadataPdu.hpp](../Types/MetadataPdu.hpp)): Initiates file transfer with filename, size, and options +- FileDataPdu ([FileDataPdu.hpp](../Types/FileDataPdu.hpp)): Carries file data segments with offset information +- EofPdu ([EofPdu.hpp](../Types/EofPdu.hpp)): Signals end of file transmission with checksum and final size +- FinPdu ([FinPdu.hpp](../Types/FinPdu.hpp)): Indicates transaction completion with delivery status (Class 2 only) +- AckPdu ([AckPdu.hpp](../Types/AckPdu.hpp)): Acknowledges receipt of EOF or FIN directives (Class 2 only) +- NakPdu ([NakPdu.hpp](../Types/NakPdu.hpp)): Requests retransmission of missing file segments (Class 2 only) + +### Supporting Types and Utilities + +**Classes:** +- Timer ([Timer.hpp](../Timer.hpp)): CFDP timer implementation using F' time primitives for ACK timeouts and inactivity detection +- CfdpChunkList ([Chunk.hpp](../Chunk.hpp)): Gap tracking for Class 2 transfers; tracks received file segments and identifies missing data for NAK generation +- Clist ([Clist.hpp](../Clist.hpp)): Intrusive circular linked list for efficient transaction queue management + +**Structs (defined in [Types.hpp](../Types/Types.hpp)):** +- History: Transaction history records for completed transfers; stores filenames, direction, status, and entity IDs +- Playback: Playback request state for directory playback and polling operations; manages directory iteration and transaction parameters +- CfdpChunkWrapper: Wrapper around CfdpChunkList for pooling and reuse across transactions + +**Utilities:** +- Utils ([Utils.hpp](../Utils.hpp)): Utility functions for transaction traversal, status conversion, and protocol helpers + +### Transmission and Receive Throttling + +#### Transmission Throttling + +Transmission throttling governs how many outgoing PDUs can be sent in a single execution cycle of the component. This mechanism prevents the CFDP engine from overwhelming downstream components (such as communication queues or radio interfaces) with excessive PDU traffic in a single scheduler invocation. + +**Configuration:** + +Transmission throttling is controlled by the `ChannelConfig.max_outgoing_pdus_per_cycle` parameter, which specifies the maximum number of outgoing PDUs that can be transmitted per channel per execution cycle. This limit applies to all outgoing PDU types including Metadata, File Data, EOF, ACK, NAK, and FIN PDUs. + +**Implementation:** + +The transmission throttling mechanism is implemented through a per-channel outgoing PDU counter that is reset at the beginning of each execution cycle. When a transaction requests a buffer to send a PDU, the implementation checks if the counter has reached the configured limit. If under the limit, the buffer is allocated and the counter is incremented. If the limit is reached, buffer allocation is denied and the transaction is deferred to the next cycle, with processing resuming from where it left off. + +**Buffer Management:** + +The transmission throttling mechanism works in conjunction with buffer allocation from downstream components. Two failure modes can occur: throttling limit reached (the `max_outgoing_pdus_per_cycle` limit is reached and no buffer allocation is attempted) or buffer exhaustion (the downstream buffer pool is exhausted and buffer allocation fails even when under the throttling limit). In both cases, the transaction defers PDU transmission until the next cycle by returning to a pending state and resuming processing in the next execution cycle. For Class 2 transactions, protocol timers (ACK, NAK, inactivity) continue running and will eventually trigger retransmissions or transaction abandonment if PDUs cannot be sent. + +#### Receive Throttling + +Unlike transmit operations that are driven by the periodic `run1Hz` scheduler port, receive operations in CfdpManager are driven by the `dataIn` async input port. Incoming CFDP PDUs arrive via this port and are processed immediately by the component's thread when the port handler is invoked, without per-cycle limits. Receive throttling was implemented in NASA's CF (CFDP) application because CF processes received PDUs during scheduled execution cycles. In contrast, CfdpManager processes incoming PDUs asynchronously as they arrive, so there is no architectural reason to throttle incoming PDUs. + +## Sequence Diagrams + +The following sequence diagrams illustrate the external protocol exchanges between spacecraft and ground systems during CFDP transactions. These diagrams focus on the PDU-level interactions and do not depict the internal state machine transitions or detailed transaction processing logic within the CfdpManager component. + +### Class 1 TX Transaction (Unacknowledged) + +This diagram shows a Class 1 file transmission from spacecraft to ground. Class 1 is unacknowledged and provides no retransmission or delivery guarantees. + +```mermaid +sequenceDiagram + participant Ground + participant Spacecraft + + Ground->>Spacecraft: SendFile command
(source file, destination file) + + Note over Spacecraft: Initialize transaction + + Spacecraft->>Ground: Metadata PDU
(filename, size) + + loop File Data Transfer + Spacecraft->>Ground: File Data PDU
(offset, data segment) + end + + Spacecraft->>Ground: EOF PDU
(checksum, file size) + + Note over Spacecraft: Transaction complete
(no acknowledgment) + Note over Ground: Verify checksum
Keep or discard file +``` + +**Key characteristics:** +- No acknowledgments (ACK, NAK, or FIN PDUs) +- No retransmissions or gap detection +- Sender completes immediately after sending EOF +- Receiver validates checksum and keeps/discards file independently + +### Class 2 TX Transaction (Acknowledged) + +This diagram shows a Class 2 file transmission from spacecraft to ground with gap detection and retransmission. The scenario includes a missing File Data PDU that is detected and retransmitted via NAK. + +```mermaid +sequenceDiagram + participant G_ACK as Ground
ACK Timer + participant G_NACK as Ground
NACK Timer + participant Ground + participant Spacecraft + participant S_ACK as Spacecraft
ACK Timer + + Ground->>Spacecraft: SendFile command
(source file, destination file) + + Note over Spacecraft: Initialize transaction + + Spacecraft->>Ground: Metadata PDU
(filename, size) + + Spacecraft->>Ground: File Data PDU (1) + Spacecraft--xGround: File Data PDU (2) [LOST] + Spacecraft->>Ground: File Data PDU (3) + + Spacecraft->>Ground: EOF PDU
(checksum, file size) + + activate S_ACK + Note over S_ACK: Armed on
EOF send + + Ground->>Spacecraft: ACK(EOF) + + deactivate S_ACK + Note over S_ACK: Cancelled on
ACK(EOF) received + + Note over Ground: Gap detected
(missing PDU (2)) + + Ground->>Spacecraft: NAK
(request PDU (2)) + + activate G_NACK + Note over G_NACK: Armed on
NAK send + + Spacecraft->>Ground: File Data PDU (2) [RETRANSMIT] + + deactivate G_NACK + Note over G_NACK: Cancelled on
gap fill + + Note over Ground: All data received
Verify checksum + + Ground->>Spacecraft: FIN PDU
(delivery complete, file retained) + Note over Ground: File saved and
ready for use + + activate G_ACK + Note over G_ACK: Armed on
FIN send + + Spacecraft->>Ground: ACK(FIN) + Note over Spacecraft: Transaction complete + + deactivate G_ACK + Note over G_ACK: Cancelled on
ACK(FIN) received + + Note over Ground: Transaction complete +``` + +**Key characteristics:** +- Full acknowledgment and retransmission support +- EOF is acknowledged to confirm reception +- Ground detects missing data and sends NAK with gap information +- Spacecraft retransmits requested segments +- NAK processing during file data transmission: + - NAKs received during file data transmission (before EOF is sent) are processed immediately + - Requested gap segments are queued and retransmitted with priority over new file data + - This allows gaps to be filled immediately upon detection, rather than waiting for EOF acknowledgment +- FIN PDU from receiver confirms final delivery status +- Timers ensure protocol progress and detect failures + - Spacecraft ACK timer: Armed when EOF is sent with duration `ChannelConfig.ack_timer`, cancelled when ACK(EOF) or FIN is received. If the timer expires before receiving acknowledgment, the spacecraft retransmits EOF and rearms the timer. After `ChannelConfig.ack_limit` retries without acknowledgment, the transaction is abandoned with status `ACK_LIMIT_NO_EOF` +- Transaction completes only after FIN/ACK exchange + +### Class 2 RX Transaction (Acknowledged) + +This diagram shows a Class 2 file reception at the spacecraft from ground with gap detection and retransmission. The scenario includes a missing File Data PDU that is detected and retransmitted via NAK. + +```mermaid +sequenceDiagram + participant G_ACK as Ground
ACK Timer + participant Ground + participant Spacecraft + participant S_NAK as Spacecraft
NAK Timer + participant S_ACK as Spacecraft
ACK Timer + + Note over Ground: Initialize transaction + + Ground->>Spacecraft: Metadata PDU
(filename, size) + + Ground--xSpacecraft: File Data PDU (1) [LOST] + Ground->>Spacecraft: File Data PDU (2) + Ground--xSpacecraft: File Data PDU (3) [LOST] + Ground->>Spacecraft: File Data PDU (4) + + Ground->>Spacecraft: EOF PDU
(checksum, file size) + + activate G_ACK + Note over G_ACK: Armed on
EOF send + + Spacecraft->>Ground: ACK(EOF) + + deactivate G_ACK + Note over G_ACK: Cancelled on
ACK(EOF) received + + Note over Spacecraft: Gaps detected
(missing PDUs (1) and (3)) + + Spacecraft->>Ground: NAK
(request PDUs (1) and (3)) + + activate S_NAK + Note over S_NAK: Armed on
NAK send + + Ground->>Spacecraft: File Data PDU (1) [RETRANSMIT] + Ground->>Spacecraft: File Data PDU (3) [RETRANSMIT] + + deactivate S_NAK + Note over S_NAK: Cancelled on
gaps filled + + Note over Spacecraft: All data received
Verify checksum + + Spacecraft->>Ground: FIN PDU
(delivery complete, file retained) + Note over Spacecraft: File saved and
ready for use + + activate S_ACK + Note over S_ACK: Armed on
FIN send + + Ground->>Spacecraft: ACK(FIN) + Note over Ground: Transaction complete + + deactivate S_ACK + Note over S_ACK: Cancelled on
ACK(FIN) received + + Note over Spacecraft: Transaction complete +``` + +**Key characteristics:** +- Full acknowledgment and retransmission support +- EOF is acknowledged to confirm reception +- Spacecraft detects missing data and sends NAK with gap information +- Ground retransmits requested segments +- FIN PDU from receiver confirms final delivery status +- Timers ensure protocol progress and detect failures + - Spacecraft NAK timer: Armed when NAK is sent with duration `ChannelConfig.ack_timer`, cancelled when **all** requested data is received. If the timer expires before receiving retransmitted data, the spacecraft sends another NAK and rearms the timer. After `ChannelConfig.nack_limit` retries without data, the transaction is abandoned with status `NAK_LIMIT_REACHED` + - Spacecraft ACK timer: Armed when FIN is sent with duration `ChannelConfig.ack_timer`, cancelled when ACK(FIN) is received. If the timer expires, the spacecraft retransmits FIN and rearms the timer. After `ChannelConfig.ack_limit` retries without ACK(FIN), the transaction is abandoned +- Transaction completes only after FIN/ACK exchange + +## Configuration + +`CfdpManager` uses compile-time configuration defined in two files: +- **[CfdpCfg.fpp](../../../../default/config/CfdpCfg.fpp)**: FPP constants and types visible to both FPP and C++ code +- **[CfdpCfg.hpp](../../../../default/config/CfdpCfg.hpp)**: C++ preprocessor definitions for implementation details + +### FPP Constants (CfdpCfg.fpp) + +These constants are defined in the `Svc.Ccsds.Cfdp` module and must be configured at compile time: + +| Constant | Purpose | +|----------|---------| +| `NumChannels` | Number of CFDP channels to instantiate. Determines the size of channel-specific port arrays and the number of independent CFDP channel instances. Each channel has its own transaction pool, configuration, and state. | +| `MaxFilePathSize` | Maximum length for file path strings. Used to size string parameters (`ChannelConfig.tmp_dir`, `ChannelConfig.fail_dir`, `ChannelConfig.move_dir`) and internal file path buffers. | +| `MaxPduSize` | Maximum PDU size in bytes. Limits the maximum possible TX PDU size. Must respect any CCSDS packet size limits on the system. | + +### FPP Types (CfdpCfg.fpp) + +These types define the size of CFDP protocol fields: + +| Type | Purpose | +|------|---------| +| `EntityId` | Entity ID size. Maximum size of entity IDs in CFDP packets. The protocol supports variable-size entity IDs at runtime, but this establishes the maximum. Must be one of: U8, U16, U32, U64. | +| `TransactionSeq` | Transaction sequence number size. Maximum size of transaction sequence numbers in CFDP packets. The protocol supports variable sizes at runtime, but this establishes the maximum. Must be one of: U8, U16, U32, U64. | +| `FileSize` | File size and offset type. Used for file sizes and offsets in CFDP operations. The protocol permits 64-bit values, but the current implementation uses 32-bit. Must be one of: U8, U16, U32, U64. | + +### C++ Configuration Constants (CfdpCfg.hpp) + +#### Protocol Configuration + +| Constant | Purpose | +|----------|---------| +| `CFDP_NAK_MAX_SEGMENTS` | Maximum NAK segments supported in a NAK PDU. When sending or receiving NAK PDUs, this is the maximum number of segment requests supported. Should match ground CFDP engine configuration. | +| `CFDP_MAX_TLV` | Maximum TLVs (Type-Length-Value) per PDU. Limits the number of TLV metadata fields in EOF and FIN PDUs for diagnostic information (entity IDs, fault handler overrides, messages). | +| `CFDP_R2_CRC_CHUNK_SIZE` | Class 2 CRC calculation chunk size. Buffer size for CRC calculation upon file completion. Larger values use more stack but complete faster. Total bytes per scheduler cycle controlled by `RxCrcCalcBytesPerCycle` parameter. | +| `CFDP_CHANNEL_NUM_RX_CHUNKS_PER_TRANSACTION` | RX chunks per transaction per channel (array). For Class 2 receive transactions, each chunk tracks a contiguous received file segment. Used for gap detection and NAK generation. Array size must match `NumChannels`. | +| `CFDP_CHANNEL_NUM_TX_CHUNKS_PER_TRANSACTION` | TX chunks per transaction per channel (array). For Class 2 transmit transactions, each chunk tracks a gap requested via NAK that needs retransmission. Array size must match `NumChannels`. | + +#### Resource Pool Configuration + +| Constant | Purpose | +|----------|---------| +| `CFDP_MAX_SIMULTANEOUS_RX` | Maximum simultaneous file receives. Each channel can support this many active/concurrent receive transactions. Contributes to total transaction pool size. | +| `CFDP_MAX_COMMANDED_PLAYBACK_FILES_PER_CHAN` | Maximum commanded playback files per channel. Maximum number of outstanding ground-commanded file transmits per channel. | +| `CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN` | Maximum commanded playback directories per channel. Each channel can support this many ground-commanded directory playbacks. | +| `CFDP_MAX_POLLING_DIR_PER_CHAN` | Maximum polling directories per channel. Determines the size of the per-channel polling directory array. | +| `CFDP_NUM_TRANSACTIONS_PER_PLAYBACK` | Number of transactions per playback directory. Each playback/polling directory operation can have this many active transfers pending or active at once. | +| `CFDP_NUM_HISTORIES_PER_CHANNEL` | Number of history entries per channel. Each channel maintains a circular buffer of completed transaction records for debugging and reference. Maximum value is 65536. | + +## Commands + +| Name | Description | +|---|---| +| SendFile | Initiates a CFDP file transaction to send a file to a remote entity. Specifies channel, destination entity ID, CFDP class (1 or 2), file retention policy, priority, source filename, and destination filename. | +| PlaybackDirectory | Starts a directory playback operation to send all files from a source directory to a destination directory on a remote entity. Files are sent sequentially as individual CFDP transactions. Completes when all files in the directory have been processed. | +| PollDirectory | Establishes a recurring directory poll that periodically checks a source directory for new files and automatically sends them to a destination directory on a remote entity. Poll interval is configurable in seconds. | +| StopPollDirectory | Stops an active directory poll operation identified by channel ID and poll ID. | +| SetChannelFlow | Sets the flow control state for a specific CFDP channel. Can freeze (pause) or resume PDU transmission on the channel. | +| SuspendResumeTransaction | Suspend or resume a transaction. When suspended, the transaction remains in memory but stops making progress (no PDUs sent or processed, no timers tick). Useful during critical spacecraft operations. Takes an action parameter (SUSPEND or RESUME). Transactions are identified by channel ID, transaction sequence number, and entity ID. | +| CancelTransaction | Gracefully cancel a transaction with protocol close-out. Sends FIN/ACK PDUs as appropriate for the transaction type and state. Transaction is removed from memory. Transactions are identified by channel ID, transaction sequence number, and entity ID. | +| AbandonTransaction | Immediately terminate a transaction without protocol close-out. No FIN/ACK sent. Transaction is immediately removed from memory. Used for stuck or unresponsive transactions. Transactions are identified by channel ID, transaction sequence number, and entity ID. | + +## Parameters + +| Name | Description | +|---|---| +| LocalEid | Local CFDP entity ID used in PDU headers to identify this node in the CFDP network | +| OutgoingFileChunkSize | Maximum number of bytes to include in each File Data PDU. Limits PDU size for transmission | +| RxCrcCalcBytesPerCycle | Maximum number of received file bytes to process for CRC calculation in a single scheduler cycle. Prevents blocking during large file verification | +| FileInDefaultChannel | CFDP channel ID used for file transfers initiated via the `fileIn` port interface (not commands) | +| FileInDefaultDestEntityId | Destination entity ID used for file transfers initiated via the `fileIn` port interface | +| FileInDefaultClass | CFDP class (CLASS_1 or CLASS_2) for file transfers initiated via the `fileIn` port interface | +| FileInDefaultKeep | File retention policy (KEEP or DELETE) for file transfers initiated via the `fileIn` port interface | +| FileInDefaultPriority | Priority (0-255, where 0 is highest) for file transfers initiated via the `fileIn` port interface | +| ChannelConfig.ack_limit | Maximum number of ACK retransmission attempts before abandoning a transaction. Applies when waiting for ACK(EOF) or ACK(FIN) acknowledgments | +| ChannelConfig.nack_limit | Maximum number of NAK retransmission attempts before abandoning a transaction. Applies when waiting for retransmitted file data after sending NAK | +| ChannelConfig.ack_timer | ACK timeout duration in seconds. Determines how long to wait for ACK(EOF) or ACK(FIN) before retransmitting | +| ChannelConfig.inactivity_timer | Inactivity timeout duration in seconds. Transaction is abandoned if no PDUs are received within this period | +| ChannelConfig.dequeue_enabled | Enable or disable transaction dequeuing and processing for this channel. Can be used to pause channel activity | +| ChannelConfig.move_dir | Directory path to move source files after successful TX (transmit) transactions when keep is set to DELETE. If set, provides an archive mechanism to preserve files instead of deleting them. If empty or if the move fails, source files are deleted from the filesystem. Only applies to sending files, not receiving | +| ChannelConfig.max_outgoing_pdus_per_cycle | Maximum number of outgoing PDUs to transmit per execution cycle. Throttles transmission rate to prevent overwhelming downstream components | +| ChannelConfig.tmp_dir | Directory path for storing temporary files during receive (RX) transactions. Files are written here during transfer and moved to their final destination upon successful completion | +| ChannelConfig.fail_dir | Directory path for storing files from polling operations that failed to transfer successfully. If empty or if the move fails, files are deleted from the filesystem | + +### Deep Space Timer Configuration + +The timer parameters (`ack_timer`, `inactivity_timer`, `ack_limit`, `nack_limit`) must be configured appropriately for the communication delay environment: + +- **Near-Earth Operations**: Default values (ack_timer=3s, inactivity_timer=30s) are appropriate for round-trip light times of 1-2 seconds +- **Lunar Operations**: Modest increases recommended (ack_timer=5-10s, inactivity_timer=60-120s) for ~2.5 second round-trip light times +- **Deep Space Operations**: Significant increases required (ack_timer and inactivity_timer scaled to mission-specific round-trip light times, which can range from minutes to hours) + +**Critical Relationship**: The `ack_timer` must be **longer than the round-trip light time** to avoid premature retransmissions. The `inactivity_timer` should be **several times larger than ack_timer** to account for file segmentation and processing delays. + +CfdpManager's per-channel parameter architecture supports multiple mission profiles simultaneously. Different channels can be configured for near-Earth, lunar, and deep space operations, allowing the system to communicate with multiple destinations concurrently. + +## Telemetry + +**Note:** Telemetry channels are currently **proposals** defined in [Telemetry.fppi](../Telemetry.fppi) but not yet implemented. Proposals are based on the CF implementation. + +### ChannelTelemetry + +An array of telemetry structures, one per CFDP channel. Each element is a `ChannelTelemetry` struct containing the following fields: + +#### Receive Counters +| Field | Type | Description | +|---|---|---| +| recvErrors | U32 | Number of PDU receive errors. Incremented when malformed or invalid PDUs are received | +| recvDropped | U32 | Number of PDUs dropped due to lack of resources (buffers, transactions) | +| recvSpurious | U32 | Number of spurious PDUs received (PDUs for nonexistent or completed transactions) | +| recvFileDataBytes | U64 | Total file data bytes received across all transactions | +| recvNakSegmentRequests | U32 | Number of NAK segment requests received from peer entity | + +#### Sent Counters +| Field | Type | Description | +|---|---|---| +| sentNakSegmentRequests | U32 | Number of NAK segment requests sent to peer entity | + +#### Fault Counters +| Field | Type | Description | +|---|---|---| +| faultAckLimit | U32 | Number of transactions abandoned due to ACK limit exceeded (no ACK(EOF) or ACK(FIN) received) | +| faultNakLimit | U32 | Number of transactions abandoned due to NAK limit exceeded (retransmitted data not received) | +| faultInactivityTimer | U32 | Number of transactions abandoned due to inactivity timeout | +| faultCrcMismatch | U32 | Number of CRC mismatches detected in received files | +| faultFileSizeMismatch | U32 | Number of file size mismatches detected (EOF size vs actual received size) | +| faultFileOpen | U32 | Number of file open failures | +| faultFileRead | U32 | Number of file read failures | +| faultFileWrite | U32 | Number of file write failures | +| faultFileSeek | U32 | Number of file seek failures | +| faultFileRename | U32 | Number of file rename failures | +| faultDirectoryRead | U32 | Number of directory read failures during playback/poll operations | + +#### Queue Depths +| Field | Type | Description | +|---|---|---| +| queueFree | U16 | Number of transactions in FREE queue (available for allocation) | +| queueTxActive | U16 | Number of transactions in active transmit queue (TXA) | +| queueTxWaiting | U16 | Number of transactions in waiting transmit queue (TXW) | +| queueRx | U16 | Number of transactions in receive queue (RX) | +| queueHistory | U16 | Number of completed transactions in history queue | + +#### Activity Counters +| Field | Type | Description | +|---|---|---| +| playbackCounter | U8 | Number of active directory playback operations | +| pollCounter | U8 | Number of active directory poll operations | + +## Requirements + +| Requirement | Description | Rationale | Verification Method | +|---|---|---|---| +| CFDP-001 | `CfdpManager` shall support CFDP Class 1 (unacknowledged) file transfers | Provides unreliable but low-overhead file transfer for non-critical data where speed is prioritized over guaranteed delivery | Unit Test, System Test | +| CFDP-002 | `CfdpManager` shall support CFDP Class 2 (acknowledged) file transfers with automatic retransmission | Ensures reliable file delivery with guaranteed completion even over lossy communication links | Unit Test, System Test | +| CFDP-003 | `CfdpManager` shall detect missing file segments using gap tracking and request retransmission via NAK PDUs | Provides the mechanism to recover from lost file data PDUs in Class 2 transfers | Unit Test | +| CFDP-004 | `CfdpManager` shall verify file integrity using CRC checksums and reject files with checksum mismatches | Ensures data corruption is detected and prevents accepting corrupted files | Unit Test | +| CFDP-005 | `CfdpManager` shall support multiple simultaneous file transfers across configurable channels | Allows concurrent file operations to maximize throughput and operational flexibility | Unit Test, System Test | +| CFDP-006 | `CfdpManager` shall support directory playback operations to transfer all files from a specified directory | Provides batch file transfer capability for operational efficiency | Unit Test | +| CFDP-007 | `CfdpManager` shall support directory polling operations to automatically detect and transfer new files at configurable intervals | Enables autonomous file downlink without ground intervention | Unit Test | +| CFDP-008 | `CfdpManager` shall enforce configurable ACK and NAK retry limits and abandon transactions that exceed these limits | Prevents infinite retry loops and ensures forward progress when peer becomes unresponsive | Unit Test | +| CFDP-009 | `CfdpManager` shall detect transaction inactivity using configurable timeout values and abandon inactive transactions | Reclaims resources from stalled transactions and prevents resource exhaustion | Unit Test | +| CFDP-010 | `CfdpManager` shall support configurable file archiving to move completed files instead of deletion | Preserves files for audit trails and operational analysis while managing storage | Unit Test | +| CFDP-011 | `CfdpManager` shall support both command-initiated and port-initiated file transfers | Allows both ground operators and onboard components to initiate file transfers | Unit Test, System Test | +| CFDP-012 | `CfdpManager` shall support flow control to freeze and resume channel operations | Provides mechanism to temporarily halt file transfers during critical spacecraft operations | Unit Test | + diff --git a/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTestMain.cpp b/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTestMain.cpp new file mode 100644 index 00000000000..c81b79417b4 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTestMain.cpp @@ -0,0 +1,102 @@ +// ====================================================================== +// \title CfdpManagerTestMain.cpp +// \author Brian Campuzano +// \brief cpp file for CfdpManager component test main function +// ====================================================================== + +#include "CfdpManagerTester.hpp" + +TEST(Pdu, MetaDataPdu) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testMetaDataPdu(); + delete tester; +} + +TEST(Pdu, FileDataPdu) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testFileDataPdu(); + delete tester; +} + +TEST(Pdu, EofPdu) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testEofPdu(); + delete tester; +} + +TEST(Pdu, FinPdu) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testFinPdu(); + delete tester; +} + +TEST(Pdu, AckPdu) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testAckPdu(); + delete tester; +} + +TEST(Pdu, NakPdu) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testNakPdu(); + delete tester; +} + +TEST(Transaction, Class1TxNominal) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass1TxNominal(); + delete tester; +} + +TEST(Transaction, Class2TxNominal) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass2TxNominal(); + delete tester; +} + +TEST(Transaction, Class2TxNack) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass2TxNack(); + delete tester; +} + +TEST(Transaction, Class1RxNominal) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass1RxNominal(); + delete tester; +} + +TEST(Transaction, Class2RxNominal) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass2RxNominal(); + delete tester; +} + +TEST(Transaction, Class2RxNack) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass2RxNack(); + delete tester; +} + +TEST(Transaction, Class1TxPortBased) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testClass2TxPortBased(); + delete tester; +} + +TEST(Transaction, MultipleTransactionsInSeries) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testMultipleTransactionsInSeries(); + delete tester; +} + +TEST(Miscellaneous, Ping) { + Svc::Ccsds::Cfdp::CfdpManagerTester* tester = new Svc::Ccsds::Cfdp::CfdpManagerTester(); + tester->testPing(); + delete tester; +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.cpp b/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.cpp new file mode 100644 index 00000000000..2ae71dddbc3 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.cpp @@ -0,0 +1,1188 @@ +// ====================================================================== +// \title CfdpManagerTester.cpp +// \author Brian Campuzano +// \brief cpp file for CfdpManager component test harness implementation class +// ====================================================================== + +#include "CfdpManagerTester.hpp" +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ---------------------------------------------------------------------- +// Static member definitions +// ---------------------------------------------------------------------- + +constexpr FwSizeType CfdpManagerTester::MAX_PDU_COPIES; + +// ---------------------------------------------------------------------- +// Construction and destruction +// ---------------------------------------------------------------------- + +CfdpManagerTester ::CfdpManagerTester() + : CfdpManagerGTestBase("CfdpManagerTester", MAX_HISTORY_SIZE), + component("CfdpManager"), + m_pduCopyCount(0) { + this->connectPorts(); + this->initComponents(); + this->component.loadParameters(); + + // Configure CFDP engine after parameters are loaded + this->component.configure(); +} + +CfdpManagerTester ::~CfdpManagerTester() { } + +// ---------------------------------------------------------------------- +// Handler implementations for typed from ports +// ---------------------------------------------------------------------- + +Fw::Buffer CfdpManagerTester::from_bufferAllocate_handler( + FwIndexType portNum, + FwSizeType size +) { + EXPECT_LT(size, MaxPduSize) << "Buffer size request is too large"; + if (size >= MaxPduSize) { + return Fw::Buffer(); + } + return Fw::Buffer(this->m_internalDataBuffer, size); +} + +void CfdpManagerTester::from_dataOut_handler( + FwIndexType portNum, + Fw::Buffer& fwBuffer +) { + // Make a copy of the PDU data to avoid buffer reuse issues + EXPECT_LT(m_pduCopyCount, MAX_PDU_COPIES) << "Too many PDUs sent"; + if (m_pduCopyCount < MAX_PDU_COPIES) { + FwSizeType copySize = fwBuffer.getSize(); + if (copySize > MaxPduSize) { + copySize = MaxPduSize; + } + memcpy(m_pduCopyStorage[m_pduCopyCount], fwBuffer.getData(), copySize); + + // Create a new buffer pointing to our copy + Fw::Buffer copyBuffer(m_pduCopyStorage[m_pduCopyCount], copySize); + m_pduCopyCount++; + + // Call base class handler with the copy + CfdpManagerTesterBase::from_dataOut_handler(portNum, copyBuffer); + } +} + +void CfdpManagerTester::from_fileDoneOut_handler( + FwIndexType portNum, + const Svc::SendFileResponse& response +) { + // Push to port history + CfdpManagerGTestBase::from_fileDoneOut_handler(portNum, response); +} + +// ---------------------------------------------------------------------- +// Transaction Test Helper Implementations +// ---------------------------------------------------------------------- + +Transaction* CfdpManagerTester::findTransaction(U8 chanNum, TransactionSeq seqNum) { + // Grab requested channel + Channel* chan = component.m_engine->m_channels[chanNum]; + + // Search through all transaction queues (PEND, TXA, TXW, RX, FREE) + // Skip HIST and HIST_FREE as they contain History, not Transaction + for (U8 qIdx = 0; qIdx < Cfdp::QueueId::NUM; qIdx++) { + // Skip history queues (HIST=4, HIST_FREE=5) + if (qIdx == Cfdp::QueueId::HIST || qIdx == Cfdp::QueueId::HIST_FREE) { + continue; + } + + CListNode* head = chan->m_qs[qIdx]; + if (head == nullptr) { + continue; + } + + // Traverse circular linked list, stopping when we loop back to head + CListNode* node = head; + do { + Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node); + if (txn->m_history && txn->m_history->seq_num == seqNum) { + return txn; + } + node = node->next; + } while (node != nullptr && node != head); + } + return nullptr; +} + +// ---------------------------------------------------------------------- +// Test Helper Function Implementations +// ---------------------------------------------------------------------- + +void CfdpManagerTester::createAndVerifyTestFile(const char* filePath, FwSizeType expectedFileSize, FwSizeType& actualFileSize) { + Os::File::Status fileStatus; + Os::File testFile; + + // Create file with repeating 0-255 pattern + fileStatus = testFile.open(filePath, Os::File::OPEN_CREATE, Os::File::OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Should create test file"; + + U8 writeBuffer[256]; + for (U16 i = 0; i < 256; i++) { + writeBuffer[i] = static_cast(i); + } + + FwSizeType bytesWritten = 0; + while (bytesWritten < expectedFileSize) { + FwSizeType chunkSize = (expectedFileSize - bytesWritten > 256) ? 256 : (expectedFileSize - bytesWritten); + FwSizeType writeSize = chunkSize; + fileStatus = testFile.write(writeBuffer, writeSize, Os::File::WAIT); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Should write to test file"; + ASSERT_EQ(chunkSize, writeSize) << "Should write requested bytes"; + bytesWritten += writeSize; + } + testFile.close(); + + // Verify file and get size + fileStatus = testFile.open(filePath, Os::File::OPEN_READ); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Test file should exist"; + fileStatus = testFile.size(actualFileSize); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Should get file size"; + testFile.close(); + + EXPECT_EQ(expectedFileSize, actualFileSize) << "File size should match expected size"; +} + +void CfdpManagerTester::setupTxTransaction( + const char* srcFile, + const char* dstFile, + U8 channelId, + EntityId destEid, + Cfdp::Class cfdpClass, + U8 priority, + TxnState expectedState, + TransactionSetup& setup) +{ + const U32 initialSeqNum = component.m_engine->m_seqNum; + + this->sendCmd_SendFile(0, 0, channelId, destEid, cfdpClass, + Cfdp::Keep::KEEP, priority, + Fw::CmdStringArg(srcFile), Fw::CmdStringArg(dstFile)); + this->component.doDispatch(); + + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, CfdpManager::OPCODE_SENDFILE, 0, Fw::CmdResponse::OK); + + setup.expectedSeqNum = initialSeqNum + 1; + EXPECT_EQ(setup.expectedSeqNum, component.m_engine->m_seqNum) << "Sequence number should increment"; + + setup.txn = findTransaction(channelId, setup.expectedSeqNum); + ASSERT_NE(nullptr, setup.txn) << "Transaction should exist"; + + // Now verify initial state + EXPECT_EQ(expectedState, setup.txn->m_state) << "Should be in expected state"; + EXPECT_EQ(0, setup.txn->m_foffs) << "File offset should be 0 initially"; + EXPECT_EQ(TX_SUB_STATE_METADATA, setup.txn->m_state_data.send.sub_state) << "Should start in METADATA sub-state"; + EXPECT_EQ(channelId, setup.txn->m_chan_num) << "Channel number should match"; + EXPECT_EQ(priority, setup.txn->m_priority) << "Priority should match"; + + EXPECT_EQ(setup.expectedSeqNum, setup.txn->m_history->seq_num) << "History seq_num should match"; + EXPECT_EQ(component.getLocalEidParam(), setup.txn->m_history->src_eid) << "Source EID should match local EID"; + EXPECT_EQ(destEid, setup.txn->m_history->peer_eid) << "Peer EID should match dest EID"; + EXPECT_STREQ(srcFile, setup.txn->m_history->fnames.src_filename.toChar()) << "Source filename should match"; + EXPECT_STREQ(dstFile, setup.txn->m_history->fnames.dst_filename.toChar()) << "Destination filename should match"; +} + +void CfdpManagerTester::setupTxPortTransaction( + const char* srcFile, + const char* dstFile, + U8 channelId, + TxnState expectedState, + TransactionSetup& setup) +{ + // Capture current sequence number before initiating + const U32 initialSeqNum = component.m_engine->m_seqNum; + + // Initiate via port (synchronous - no dispatch needed) + Svc::SendFileResponse response = invokeSendFilePort(srcFile, dstFile); + + ASSERT_EQ(Svc::SendFileStatus::STATUS_OK, response.get_status()) + << "Port-based file send should succeed"; + + // Find the transaction that was created + setup.expectedSeqNum = initialSeqNum + 1; + EXPECT_EQ(setup.expectedSeqNum, component.m_engine->m_seqNum) << "Sequence number should increment"; + + setup.txn = findTransaction(channelId, setup.expectedSeqNum); + ASSERT_NE(nullptr, setup.txn) << "Transaction should exist after port invocation"; + + // Verify initial transaction state + ASSERT_EQ(expectedState, setup.txn->m_state) << "Should be in expected state"; + ASSERT_EQ(INIT_BY_PORT, setup.txn->m_initType) << "Should be marked as port-initiated"; + EXPECT_EQ(0, setup.txn->m_foffs) << "File offset should be 0 initially"; + EXPECT_EQ(TX_SUB_STATE_METADATA, setup.txn->m_state_data.send.sub_state) << "Should start in METADATA sub-state"; + EXPECT_EQ(channelId, setup.txn->m_chan_num) << "Channel number should match"; + + // Verify transaction history + EXPECT_EQ(setup.expectedSeqNum, setup.txn->m_history->seq_num) << "History seq_num should match"; + EXPECT_EQ(component.getLocalEidParam(), setup.txn->m_history->src_eid) << "Source EID should match local EID"; + EXPECT_STREQ(srcFile, setup.txn->m_history->fnames.src_filename.toChar()) << "Source filename should match"; + EXPECT_STREQ(dstFile, setup.txn->m_history->fnames.dst_filename.toChar()) << "Destination filename should match"; +} + +void CfdpManagerTester::setupRxTransaction( + const char* srcFile, + const char* dstFile, + U8 channelId, + EntityId sourceEid, + Cfdp::Class::T cfdpClass, + U32 fileSize, + U32 transactionSeq, + TxnState expectedState, + TransactionSetup& setup) +{ + // Send Metadata PDU to initiate RX transaction + U8 closureRequested = (cfdpClass == Cfdp::Class::CLASS_1) ? 0 : 1; + + this->sendMetadataPdu( + channelId, + sourceEid, + component.getLocalEidParam(), + transactionSeq, + fileSize, + srcFile, + dstFile, + cfdpClass, + closureRequested + ); + this->component.doDispatch(); + + // Find the created transaction + setup.expectedSeqNum = transactionSeq; + setup.txn = findTransaction(channelId, transactionSeq); + ASSERT_NE(nullptr, setup.txn) << "RX transaction should be created after Metadata PDU"; + + // Verify transaction state + EXPECT_EQ(expectedState, setup.txn->m_state) << "Should be in expected RX state"; + EXPECT_EQ(RX_SUB_STATE_FILEDATA, setup.txn->m_state_data.receive.sub_state) << "Should start in FILEDATA sub-state"; + EXPECT_EQ(channelId, setup.txn->m_chan_num) << "Channel number should match"; + EXPECT_TRUE(setup.txn->m_flags.rx.md_recv) << "md_recv flag should be set after Metadata PDU"; + + // Verify transaction history + EXPECT_EQ(transactionSeq, setup.txn->m_history->seq_num) << "History seq_num should match"; + EXPECT_EQ(sourceEid, setup.txn->m_history->src_eid) << "Source EID should match ground EID (sender)"; + EXPECT_EQ(sourceEid, setup.txn->m_history->peer_eid) << "Peer EID should match ground EID (the remote peer)"; + EXPECT_STREQ(srcFile, setup.txn->m_history->fnames.src_filename.toChar()) << "Source filename should match"; + EXPECT_STREQ(dstFile, setup.txn->m_history->fnames.dst_filename.toChar()) << "Destination filename should match"; +} + +void CfdpManagerTester::waitForTransactionRecycle(U8 channelId, U32 expectedSeqNum) { + this->clearHistory(); + this->m_pduCopyCount = 0; + + U32 inactivityTimer = this->component.getInactivityTimerParam(channelId); + U32 cyclesToRun = inactivityTimer + 1; + for (U32 i = 0; i < cyclesToRun; ++i) { + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + } + + Transaction* txn = findTransaction(channelId, expectedSeqNum); + EXPECT_EQ(nullptr, txn) << "Transaction should be recycled after inactivity timeout"; +} + +void CfdpManagerTester::completeClass2Handshake( + U8 channelId, + EntityId destEid, + U32 expectedSeqNum, + Transaction* txn) +{ + // Send EOF-ACK + this->sendAckPdu( + channelId, + component.getLocalEidParam(), + destEid, + expectedSeqNum, + Cfdp::FILE_DIRECTIVE_END_OF_FILE, + 0, + Cfdp::CONDITION_CODE_NO_ERROR, + Cfdp::ACK_TXN_STATUS_ACTIVE + ); + this->component.doDispatch(); + + EXPECT_TRUE(txn->m_flags.tx.eof_ack_recv) << "eof_ack_recv flag should be set after EOF-ACK received"; + EXPECT_FALSE(txn->m_flags.com.ack_timer_armed) << "ack_timer_armed should be cleared after EOF-ACK"; + EXPECT_EQ(TXN_STATE_S2, txn->m_state) << "Should remain in S2 state waiting for FIN"; + EXPECT_EQ(TX_SUB_STATE_CLOSEOUT_SYNC, txn->m_state_data.send.sub_state) << "Should remain in CLOSEOUT_SYNC waiting for FIN"; + + // Send FIN + this->sendFinPdu( + channelId, + component.getLocalEidParam(), + destEid, + expectedSeqNum, + Cfdp::CONDITION_CODE_NO_ERROR, + Cfdp::FIN_DELIVERY_CODE_COMPLETE, + Cfdp::FIN_FILE_STATUS_RETAINED + ); + this->component.doDispatch(); + + EXPECT_TRUE(txn->m_flags.tx.fin_recv) << "fin_recv flag should be set after FIN received"; + EXPECT_EQ(TXN_STATE_HOLD, txn->m_state) << "Should move to HOLD state after FIN received"; + EXPECT_TRUE(txn->m_flags.tx.send_fin_ack) << "send_fin_ack flag should be set"; + + // Run cycle to send FIN-ACK + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); +} + +void CfdpManagerTester::verifyFinAckPdu( + FwIndexType pduIndex, + EntityId sourceEid, + EntityId destEid, + U32 expectedSeqNum) +{ + Fw::Buffer finAckPduBuffer = this->getSentPduBuffer(pduIndex); + ASSERT_GT(finAckPduBuffer.getSize(), 0) << "FIN-ACK PDU should be sent"; + + verifyAckPdu(finAckPduBuffer, + sourceEid, + destEid, + expectedSeqNum, + Cfdp::FILE_DIRECTIVE_FIN, + 1, + Cfdp::CONDITION_CODE_NO_ERROR, + Cfdp::ACK_TXN_STATUS_TERMINATED + ); +} + +void CfdpManagerTester::verifyMetadataPduAtIndex( + FwIndexType pduIndex, + const TransactionSetup& setup, + FwSizeType fileSize, + const char* srcFile, + const char* dstFile, + Cfdp::Class::T cfdpClass) +{ + Fw::Buffer metadataPduBuffer = this->getSentPduBuffer(pduIndex); + ASSERT_GT(metadataPduBuffer.getSize(), 0) << "Metadata PDU should be sent"; + EXPECT_EQ(fileSize, setup.txn->m_fsize) << "File size should be set after file is opened"; + verifyMetadataPdu(metadataPduBuffer, component.getLocalEidParam(), TEST_GROUND_EID, + setup.expectedSeqNum, static_cast(fileSize), srcFile, dstFile, cfdpClass); +} + +void CfdpManagerTester::verifyMultipleFileDataPdus( + FwIndexType startIndex, + U8 numPdus, + const TransactionSetup& setup, + U16 dataPerPdu, + const char* srcFile, + Cfdp::Class::T cfdpClass) +{ + for (U8 pduIdx = 0; pduIdx < numPdus; pduIdx++) { + Fw::Buffer fileDataPduBuffer = this->getSentPduBuffer(startIndex + pduIdx); + ASSERT_GT(fileDataPduBuffer.getSize(), 0) << "File data PDU " << static_cast(pduIdx) << " should be sent"; + verifyFileDataPdu(fileDataPduBuffer, component.getLocalEidParam(), TEST_GROUND_EID, + setup.expectedSeqNum, pduIdx * dataPerPdu, dataPerPdu, srcFile, cfdpClass); + } +} + +void CfdpManagerTester::cleanupTestFile(const char* filePath) { + Os::FileSystem::Status fsStatus = Os::FileSystem::removeFile(filePath); + // File may already be deleted by CFDP (keep=DELETE), which is acceptable + EXPECT_TRUE(fsStatus == Os::FileSystem::OP_OK || fsStatus == Os::FileSystem::DOESNT_EXIST) + << "Should remove test file or file already deleted"; +} + +void CfdpManagerTester::verifyReceivedFile( + const char* filePath, + const U8* expectedData, + FwSizeType expectedSize) +{ + // Read destination file + U8* receivedData = new U8[expectedSize]; + Os::File file; + Os::File::Status fileStatus = file.open(filePath, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Received file should exist"; + + FwSizeType bytesRead = expectedSize; + fileStatus = file.read(receivedData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Should read received file successfully"; + ASSERT_EQ(expectedSize, bytesRead) << "Received file size should match expected size"; + + // Compare content byte-by-byte + for (FwSizeType i = 0; i < expectedSize; ++i) { + EXPECT_EQ(expectedData[i], receivedData[i]) << "File content mismatch at byte " << i; + } + + // Clean up buffer + delete[] receivedData; +} + +// ---------------------------------------------------------------------- +// Refactored Test Helper Implementations +// ---------------------------------------------------------------------- + +Svc::SendFileResponse CfdpManagerTester::invokeSendFilePort( + const char* srcFile, + const char* dstFile +) { + Fw::String source(srcFile); + Fw::String dest(dstFile); + Svc::SendFileResponse response = this->invoke_to_fileIn( + 0, // portNum + source, // sourceFileName + dest, // destFileName + 0, // offset (unused) + 0 // length (0 = entire file) + ); + return response; +} + +void CfdpManagerTester::sendAndVerifyClass1Tx( + const char* srcFile, + const char* dstFile, + FwSizeType expectedFileSize +) { + // Create and verify test file + FwSizeType fileSize; + createAndVerifyTestFile(srcFile, expectedFileSize, fileSize); + + // Setup transaction and verify initial state (command-based only) + TransactionSetup setup; + setupTxTransaction(srcFile, dstFile, TEST_CHANNEL_ID_0, TEST_GROUND_EID, + Cfdp::Class::CLASS_1, TEST_PRIORITY, TXN_STATE_S1, setup); + + // Run first engine cycle - should send Metadata + FileData PDUs + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(2); + + // Verify Metadata PDU + verifyMetadataPduAtIndex(0, setup, fileSize, srcFile, dstFile, Cfdp::Class::CLASS_1); + + // Verify FileData PDU + Fw::Buffer fileDataPduBuffer = this->getSentPduBuffer(1); + ASSERT_GT(fileDataPduBuffer.getSize(), 0) << "File data PDU should be sent"; + verifyFileDataPdu(fileDataPduBuffer, component.getLocalEidParam(), TEST_GROUND_EID, + setup.expectedSeqNum, 0, static_cast(fileSize), srcFile, Cfdp::Class::CLASS_1); + + EXPECT_EQ(fileSize, setup.txn->m_foffs) << "Should have read entire file"; + EXPECT_EQ(TX_SUB_STATE_EOF, setup.txn->m_state_data.send.sub_state) << "Should progress to EOF sub-state"; + + // Run second engine cycle - should send EOF PDU + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(3); + + // Verify EOF PDU + Fw::Buffer eofPduBuffer = this->getSentPduBuffer(2); + ASSERT_GT(eofPduBuffer.getSize(), 0) << "EOF PDU should be sent"; + verifyEofPdu(eofPduBuffer, component.getLocalEidParam(), TEST_GROUND_EID, + setup.expectedSeqNum, Cfdp::CONDITION_CODE_NO_ERROR, static_cast(fileSize), srcFile); + + // Verify telemetry was emitted (should be emitted at end of each run1Hz) + // We called run1Hz twice, so expect at least 2 telemetry emissions + ASSERT_GE(this->tlmHistory_ChannelTelemetry->size(), 1u); + + // Get the LATEST telemetry value (last emission has cumulative counts) + U32 tlmIndex = static_cast(this->tlmHistory_ChannelTelemetry->size() - 1); + Cfdp::ChannelTelemetryArray tlm = + this->tlmHistory_ChannelTelemetry->at(tlmIndex).arg; + + // Verify TX counters incremented (we sent Metadata + FileData + EOF PDUs = 3 total) + EXPECT_EQ(3u, tlm[TEST_CHANNEL_ID_0].get_sentPdu()) + << "sentPdu should be 3 (Metadata + FileData + EOF)"; + EXPECT_GT(tlm[TEST_CHANNEL_ID_0].get_sentFileDataBytes(), 0u) + << "sentFileDataBytes should increment when file data is sent"; + EXPECT_EQ(fileSize, tlm[TEST_CHANNEL_ID_0].get_sentFileDataBytes()) + << "sentFileDataBytes should equal file size"; + + // Verify no receive counters incremented (this is TX only) + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_recvPdu()) + << "recvPdu should be 0 for TX-only transaction"; + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_recvFileDataBytes()) + << "recvFileDataBytes should be 0 for TX-only transaction"; + + // Verify no errors occurred + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_recvErrors()) + << "No receive errors should occur"; + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_faultAckLimit()) + << "No ACK limit faults should occur"; + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_faultNakLimit()) + << "No NAK limit faults should occur"; + + // Verify completion event was emitted + ASSERT_EVENTS_TxFileTransferCompleted_SIZE(1); + ASSERT_EVENTS_TxFileTransferCompleted( + 0, // index + Cfdp::Class::CLASS_1, + component.getLocalEidParam(), + setup.expectedSeqNum, + srcFile, + dstFile, + static_cast(fileSize) + ); + + // Wait for transaction recycle + waitForTransactionRecycle(TEST_CHANNEL_ID_0, setup.expectedSeqNum); +} + +void CfdpManagerTester::sendAndVerifyClass1Rx( + const char* srcFile, + const char* dstFile, + const char* groundSrcFile, + FwSizeType expectedFileSize +) { + const U32 transactionSeq = 100; + + // Create test data file dynamically + FwSizeType actualFileSize; + createAndVerifyTestFile(srcFile, expectedFileSize, actualFileSize); + + // Uplink Metadata PDU and setup RX transaction + TransactionSetup setup; + setupRxTransaction(groundSrcFile, dstFile, TEST_CHANNEL_ID_0, TEST_GROUND_EID, + Cfdp::Class::CLASS_1, static_cast(actualFileSize), transactionSeq, TXN_STATE_R1, setup); + + // Read test data from source file + U8* testData = new U8[actualFileSize]; + Os::File file; + Os::File::Status fileStatus = file.open(srcFile, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to open source file for reading"; + + FwSizeType bytesRead = actualFileSize; + fileStatus = file.read(testData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to read source file"; + ASSERT_EQ(actualFileSize, bytesRead) << "Should read entire file"; + + // Send FileData PDU + sendFileDataPdu( + TEST_CHANNEL_ID_0, + TEST_GROUND_EID, + component.getLocalEidParam(), + transactionSeq, + 0, // offset + static_cast(actualFileSize), // size + testData, + Cfdp::Class::CLASS_1 + ); + component.doDispatch(); + + // Verify FileData processed + EXPECT_EQ(TXN_STATE_R1, setup.txn->m_state) << "Should remain in R1 state after FileData"; + EXPECT_EQ(RX_SUB_STATE_FILEDATA, setup.txn->m_state_data.receive.sub_state) << "Should remain in FILEDATA sub-state"; + + // Compute CRC for EOF PDU + CFDP::Checksum crc; + crc.update(testData, 0, static_cast(actualFileSize)); + U32 expectedCrc = crc.getValue(); + + // Send EOF PDU + sendEofPdu( + TEST_CHANNEL_ID_0, + TEST_GROUND_EID, + component.getLocalEidParam(), + transactionSeq, + Cfdp::CONDITION_CODE_NO_ERROR, + expectedCrc, + static_cast(actualFileSize), + Cfdp::Class::CLASS_1 + ); + component.doDispatch(); + + // Verify transaction completed + EXPECT_EQ(TXN_STATE_HOLD, setup.txn->m_state) << "Should be in HOLD state after EOF processing"; + + // Verify file written to disk + verifyReceivedFile(dstFile, testData, actualFileSize); + + // Emit telemetry by calling run1Hz (RX tests don't automatically call this) + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + + // Verify telemetry for RX transaction + ASSERT_GE(this->tlmHistory_ChannelTelemetry->size(), 1u); + U32 tlmIndex = static_cast(this->tlmHistory_ChannelTelemetry->size() - 1); + Cfdp::ChannelTelemetryArray tlm = this->tlmHistory_ChannelTelemetry->at(tlmIndex).arg; + + // Verify RX counters (received Metadata + FileData + EOF = 3 PDUs minimum) + // Note: Counters are cumulative, so use >= for multi-transaction tests + EXPECT_GE(tlm[TEST_CHANNEL_ID_0].get_recvPdu(), 3u) + << "recvPdu should be at least 3 (Metadata + FileData + EOF)"; + EXPECT_GE(tlm[TEST_CHANNEL_ID_0].get_recvFileDataBytes(), actualFileSize) + << "recvFileDataBytes should be at least file size"; + + // Class1 RX doesn't send responses, but sentPdu may have values from previous transactions + // So we just log the values without strict assertions for Class1 RX + + // Verify no errors occurred + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_recvErrors()) + << "No receive errors should occur"; + + // Verify completion event was emitted + ASSERT_EVENTS_RxFileTransferCompleted_SIZE(1); + ASSERT_EVENTS_RxFileTransferCompleted( + 0, // index + Cfdp::Class::CLASS_1, + TEST_GROUND_EID, + transactionSeq, + groundSrcFile, + dstFile, + static_cast(actualFileSize) + ); + + // Clean up + delete[] testData; + waitForTransactionRecycle(TEST_CHANNEL_ID_0, transactionSeq); + cleanupTestFile(dstFile); + cleanupTestFile(srcFile); +} + +void CfdpManagerTester::sendAndVerifyClass2Rx( + const char* srcFile, + const char* dstFile, + const char* groundSrcFile, + FwSizeType expectedFileSize, + bool simulateNak +) { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + const U32 transactionSeq = simulateNak ? 300 : 200; + + // Create test data file + FwSizeType actualFileSize; + createAndVerifyTestFile(srcFile, expectedFileSize, actualFileSize); + + // Setup RX transaction + TransactionSetup setup; + setupRxTransaction(groundSrcFile, dstFile, TEST_CHANNEL_ID_0, TEST_GROUND_EID, + Cfdp::Class::CLASS_2, static_cast(actualFileSize), transactionSeq, TXN_STATE_R2, setup); + + // Read test data + U8* testData = new U8[actualFileSize]; + Os::File file; + Os::File::Status fileStatus = file.open(srcFile, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus); + + FwSizeType bytesRead = actualFileSize; + fileStatus = file.read(testData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus); + ASSERT_EQ(actualFileSize, bytesRead); + + // Send FileData PDUs + if (simulateNak) { + // Send only PDUs 0 and 3 (skip 1, 2, 4 to create gaps) + U8 pduIndices[] = {0, 3}; + for (U8 i = 0; i < 2; i++) { + U8 pduIdx = pduIndices[i]; + U32 offset = pduIdx * dataPerPdu; + sendFileDataPdu(TEST_CHANNEL_ID_0, TEST_GROUND_EID, component.getLocalEidParam(), + transactionSeq, offset, dataPerPdu, testData + offset, Cfdp::Class::CLASS_2); + component.doDispatch(); + } + } else { + // Send all PDUs + U8 numPdus = static_cast(actualFileSize / dataPerPdu); + for (U8 pduIdx = 0; pduIdx < numPdus; pduIdx++) { + U32 offset = pduIdx * dataPerPdu; + sendFileDataPdu(TEST_CHANNEL_ID_0, TEST_GROUND_EID, component.getLocalEidParam(), + transactionSeq, offset, dataPerPdu, testData + offset, Cfdp::Class::CLASS_2); + component.doDispatch(); + } + } + + // Verify FileData processed + EXPECT_EQ(TXN_STATE_R2, setup.txn->m_state); + EXPECT_EQ(RX_SUB_STATE_FILEDATA, setup.txn->m_state_data.receive.sub_state); + + // Compute CRC and send EOF + CFDP::Checksum crc; + crc.update(testData, 0, static_cast(actualFileSize)); + U32 expectedCrc = crc.getValue(); + + FwSizeType pduCountBeforeEof = this->fromPortHistory_dataOut->size(); + + sendEofPdu(TEST_CHANNEL_ID_0, TEST_GROUND_EID, component.getLocalEidParam(), + transactionSeq, Cfdp::CONDITION_CODE_NO_ERROR, expectedCrc, + static_cast(actualFileSize), Cfdp::Class::CLASS_2); + component.doDispatch(); + + // Verify EOF processed + EXPECT_EQ(TXN_STATE_R2, setup.txn->m_state); + EXPECT_TRUE(setup.txn->m_flags.rx.eof_recv); + EXPECT_TRUE(setup.txn->m_flags.rx.send_eof_ack); + + if (simulateNak) { + EXPECT_FALSE(setup.txn->m_flags.rx.send_fin); + EXPECT_TRUE(setup.txn->m_flags.rx.send_nak); + } else { + EXPECT_TRUE(setup.txn->m_flags.rx.send_fin); + } + + // Run cycle to send EOF-ACK + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + + // Verify EOF-ACK sent + FwSizeType pduCountAfterTick = this->fromPortHistory_dataOut->size(); + EXPECT_EQ(pduCountBeforeEof + 1, pduCountAfterTick); + Fw::Buffer eofAckPduBuffer = this->getSentPduBuffer(static_cast(pduCountBeforeEof)); + ASSERT_GT(eofAckPduBuffer.getSize(), 0); + verifyAckPdu(eofAckPduBuffer, TEST_GROUND_EID, component.getLocalEidParam(), + transactionSeq, Cfdp::FILE_DIRECTIVE_END_OF_FILE, 1, + Cfdp::CONDITION_CODE_NO_ERROR, Cfdp::ACK_TXN_STATUS_ACTIVE); + + // Handle NAK if simulated + if (simulateNak) { + // Wait for NAK + U32 maxCycles = 20; + bool foundNak = false; + + for (U32 cycle = 0; cycle < maxCycles && !foundNak; ++cycle) { + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + + if (this->fromPortHistory_dataOut->size() > pduCountAfterTick) { + FwIndexType lastIndex = static_cast(this->fromPortHistory_dataOut->size() - 1); + Fw::Buffer lastPdu = this->getSentPduBuffer(lastIndex); + Cfdp::NakPdu nakPdu; + Fw::SerialBuffer sb(const_cast(lastPdu.getData()), lastPdu.getSize()); + sb.setBuffLen(lastPdu.getSize()); + if (nakPdu.deserializeFrom(sb) == Fw::FW_SERIALIZE_OK) { + foundNak = true; + } + } + } + + ASSERT_TRUE(foundNak); + + // Send missing PDUs 1, 2, and 4 + FwSizeType pduCountBeforeRetransmit = this->fromPortHistory_dataOut->size(); + U8 missingPduIndices[] = {1, 2, 4}; + for (U8 i = 0; i < 3; i++) { + U8 pduIdx = missingPduIndices[i]; + U32 offset = pduIdx * dataPerPdu; + sendFileDataPdu(TEST_CHANNEL_ID_0, TEST_GROUND_EID, component.getLocalEidParam(), + transactionSeq, offset, dataPerPdu, testData + offset, Cfdp::Class::CLASS_2); + component.doDispatch(); + } + + EXPECT_TRUE(setup.txn->m_flags.rx.complete); + + // Wait for FIN after retransmission + bool foundFin = false; + + for (U32 cycle = 0; cycle < maxCycles && !foundFin; ++cycle) { + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + + if (this->fromPortHistory_dataOut->size() > pduCountBeforeRetransmit) { + FwIndexType lastIndex = static_cast(this->fromPortHistory_dataOut->size() - 1); + Fw::Buffer lastPdu = this->getSentPduBuffer(lastIndex); + Cfdp::FinPdu finPdu; + Fw::SerialBuffer sb(const_cast(lastPdu.getData()), lastPdu.getSize()); + sb.setBuffLen(lastPdu.getSize()); + if (finPdu.deserializeFrom(sb) == Fw::FW_SERIALIZE_OK) { + foundFin = true; + } + } + } + + ASSERT_TRUE(foundFin); + } else { + // Wait for FIN (no NAK) + U32 maxCycles = 20; + bool foundFin = false; + + for (U32 cycle = 0; cycle < maxCycles && !foundFin; ++cycle) { + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + + if (this->fromPortHistory_dataOut->size() > 1) { + FwIndexType lastIndex = static_cast(this->fromPortHistory_dataOut->size() - 1); + Fw::Buffer lastPdu = this->getSentPduBuffer(lastIndex); + Cfdp::FinPdu finPdu; + Fw::SerialBuffer sb(const_cast(lastPdu.getData()), lastPdu.getSize()); + sb.setBuffLen(lastPdu.getSize()); + if (finPdu.deserializeFrom(sb) == Fw::FW_SERIALIZE_OK) { + foundFin = true; + } + } + } + + ASSERT_TRUE(foundFin); + } + + // Verify transaction state before FIN-ACK + EXPECT_EQ(TXN_STATE_R2, setup.txn->m_state); + EXPECT_EQ(RX_SUB_STATE_CLOSEOUT_SYNC, setup.txn->m_state_data.receive.sub_state); + + // Send FIN-ACK + this->sendAckPdu(TEST_CHANNEL_ID_0, TEST_GROUND_EID, component.getLocalEidParam(), + transactionSeq, Cfdp::FILE_DIRECTIVE_FIN, 1, + Cfdp::CONDITION_CODE_NO_ERROR, Cfdp::ACK_TXN_STATUS_TERMINATED); + this->component.doDispatch(); + + // Verify transaction completed + EXPECT_EQ(TXN_STATE_HOLD, setup.txn->m_state); + + // Verify completion event was emitted + ASSERT_EVENTS_RxFileTransferCompleted_SIZE(1); + ASSERT_EVENTS_RxFileTransferCompleted( + 0, // index + Cfdp::Class::CLASS_2, + TEST_GROUND_EID, + transactionSeq, + groundSrcFile, + dstFile, + static_cast(actualFileSize) + ); + + // Wait for transaction recycle + waitForTransactionRecycle(TEST_CHANNEL_ID_0, transactionSeq); + + // Verify file + verifyReceivedFile(dstFile, testData, actualFileSize); + + // Verify telemetry for Class2 RX transaction + ASSERT_GE(this->tlmHistory_ChannelTelemetry->size(), 1u); + U32 tlmIndex = static_cast(this->tlmHistory_ChannelTelemetry->size() - 1); + Cfdp::ChannelTelemetryArray tlm = this->tlmHistory_ChannelTelemetry->at(tlmIndex).arg; + + // Verify RX counters (cumulative across all transactions on this channel) + U8 numFileDataPdus = static_cast(actualFileSize / dataPerPdu); + U32 expectedRecvPdus = 1 + numFileDataPdus + 1 + 1; // Metadata + FileData PDUs + EOF + FIN-ACK + if (simulateNak) { + expectedRecvPdus += 3; // Add 3 retransmitted FileData PDUs + } + EXPECT_GT(tlm[TEST_CHANNEL_ID_0].get_recvPdu(), numFileDataPdus) + << "recvPdu should include Metadata + FileData + EOF + FIN-ACK"; + EXPECT_GE(tlm[TEST_CHANNEL_ID_0].get_recvFileDataBytes(), actualFileSize) + << "recvFileDataBytes should be at least file size (cumulative)"; + + // Verify TX counters (Class2 RX sends EOF-ACK + FIN) + EXPECT_GE(tlm[TEST_CHANNEL_ID_0].get_sentPdu(), 2u) + << "sentPdu should be at least 2 (EOF-ACK + FIN)"; + if (simulateNak) { + EXPECT_GT(tlm[TEST_CHANNEL_ID_0].get_sentNakSegmentRequests(), 0u) + << "NAK segment requests should be sent when gaps detected"; + } + + // Verify no errors occurred + EXPECT_EQ(0u, tlm[TEST_CHANNEL_ID_0].get_recvErrors()) + << "No receive errors should occur"; + + // Clean up + delete[] testData; + cleanupTestFile(dstFile); + cleanupTestFile(srcFile); +} + +void CfdpManagerTester::sendAndVerifyClass2Tx( + TransactionInitType initType, + const char* srcFile, + const char* dstFile, + FwSizeType expectedFileSize, + bool simulateNak +) { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + const U8 channelId = (initType == INIT_BY_COMMAND) ? TEST_CHANNEL_ID_1 : TEST_CHANNEL_ID_0; + + // Create and verify test file + FwSizeType fileSize; + createAndVerifyTestFile(srcFile, expectedFileSize, fileSize); + + // Setup transaction + TransactionSetup setup; + + if (initType == INIT_BY_COMMAND) { + setupTxTransaction(srcFile, dstFile, channelId, TEST_GROUND_EID, + Cfdp::Class::CLASS_2, TEST_PRIORITY, TXN_STATE_S2, setup); + } else { + // Initiate via port + setupTxPortTransaction(srcFile, dstFile, channelId, TXN_STATE_S2, setup); + } + + // Run engine cycle - Metadata + FileData PDUs + U8 numFileDataPdus = static_cast(fileSize / dataPerPdu); + if (fileSize % dataPerPdu != 0) { + numFileDataPdus++; + } + + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + ASSERT_FROM_PORT_HISTORY_SIZE(1 + numFileDataPdus); + + verifyMetadataPduAtIndex(0, setup, expectedFileSize, srcFile, dstFile, Cfdp::Class::CLASS_2); + verifyMultipleFileDataPdus(1, numFileDataPdus, setup, dataPerPdu, srcFile, Cfdp::Class::CLASS_2); + + EXPECT_EQ(expectedFileSize, setup.txn->m_foffs); + EXPECT_EQ(TX_SUB_STATE_CLOSEOUT_SYNC, setup.txn->m_state_data.send.sub_state); + EXPECT_TRUE(setup.txn->m_flags.tx.send_eof); + + // Run cycle - EOF PDU + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + FwIndexType firstEofIndex = 1 + numFileDataPdus; + ASSERT_FROM_PORT_HISTORY_SIZE(firstEofIndex + 1); + + Fw::Buffer firstEofPduBuffer = this->getSentPduBuffer(firstEofIndex); + ASSERT_GT(firstEofPduBuffer.getSize(), 0); + verifyEofPdu(firstEofPduBuffer, component.getLocalEidParam(), TEST_GROUND_EID, + setup.expectedSeqNum, Cfdp::CONDITION_CODE_NO_ERROR, static_cast(expectedFileSize), srcFile); + + EXPECT_FALSE(setup.txn->m_flags.tx.send_eof); + + // Handle NAK if requested + if (simulateNak) { + // Clear history for retransmission + this->clearHistory(); + this->m_pduCopyCount = 0; + + // Send NAK requesting retransmission of PDUs 2 and 5 + Cfdp::SegmentRequest segments[2]; + segments[0].offsetStart = dataPerPdu; + segments[0].offsetEnd = 2 * dataPerPdu; + segments[1].offsetStart = 4 * dataPerPdu; + segments[1].offsetEnd = 5 * dataPerPdu; + + this->sendNakPdu(channelId, component.getLocalEidParam(), TEST_GROUND_EID, + setup.expectedSeqNum, 0, static_cast(expectedFileSize), + 2, segments); + this->component.doDispatch(); + + // Run cycles until second EOF + U32 maxCycles = 10; + bool foundSecondEof = false; + + for (U32 cycle = 0; cycle < maxCycles && !foundSecondEof; ++cycle) { + this->invoke_to_run1Hz(0, 0); + this->component.doDispatch(); + + if (this->fromPortHistory_dataOut->size() > 0) { + FwIndexType lastIndex = static_cast(this->fromPortHistory_dataOut->size() - 1); + Fw::Buffer lastPdu = this->getSentPduBuffer(lastIndex); + Cfdp::EofPdu eofPdu; + Fw::SerialBuffer sb(const_cast(lastPdu.getData()), lastPdu.getSize()); + sb.setBuffLen(lastPdu.getSize()); + if (eofPdu.deserializeFrom(sb) == Fw::FW_SERIALIZE_OK) { + foundSecondEof = true; + } + } + } + + ASSERT_TRUE(foundSecondEof) << "Second EOF should be sent after NAK retransmission"; + } + + // Complete Class 2 handshake + completeClass2Handshake(channelId, TEST_GROUND_EID, setup.expectedSeqNum, setup.txn); + + // If port-initiated, verify fileDoneOut callback BEFORE clearing history + if (initType == INIT_BY_PORT) { + ASSERT_EQ(1u, this->fromPortHistory_fileDoneOut->size()) + << "fileDoneOut port should be invoked once for port-initiated transfer"; + Svc::SendFileResponse completionResp = + this->fromPortHistory_fileDoneOut->at(0).resp; + ASSERT_EQ(Svc::SendFileStatus::STATUS_OK, completionResp.get_status()) + << "fileDoneOut should indicate success"; + } + + // Verify completion event was emitted + ASSERT_EVENTS_TxFileTransferCompleted_SIZE(1); + ASSERT_EVENTS_TxFileTransferCompleted( + 0, // index + Cfdp::Class::CLASS_2, + component.getLocalEidParam(), + setup.expectedSeqNum, + srcFile, + dstFile, + static_cast(expectedFileSize) + ); + + // Wait for transaction recycle + waitForTransactionRecycle(channelId, setup.expectedSeqNum); + + // Verify telemetry for Class2 TX transaction + ASSERT_GE(this->tlmHistory_ChannelTelemetry->size(), 1u); + U32 tlmIndex = static_cast(this->tlmHistory_ChannelTelemetry->size() - 1); + Cfdp::ChannelTelemetryArray tlm = this->tlmHistory_ChannelTelemetry->at(tlmIndex).arg; + + // Verify TX counters (Metadata + FileData PDUs + EOF(s) + FIN-ACK) + U32 expectedSentPdus = 1 + numFileDataPdus + 1 + 1; // Metadata + FileData + EOF + FIN-ACK + U64 expectedSentBytes = fileSize; + if (simulateNak) { + expectedSentPdus += 3; // Add 2 retransmitted FileData PDUs + second EOF + expectedSentBytes += 2 * dataPerPdu; // Retransmitted bytes for 2 PDUs + } + EXPECT_GE(tlm[channelId].get_sentPdu(), expectedSentPdus - 1) + << "sentPdu should include Metadata + FileData + EOF + FIN-ACK"; + EXPECT_GE(tlm[channelId].get_sentFileDataBytes(), fileSize) + << "sentFileDataBytes should be at least file size (may include retransmissions)"; + + // Verify RX counters (received EOF-ACK + FIN) + EXPECT_GE(tlm[channelId].get_recvPdu(), 2u) + << "recvPdu should be at least 2 (EOF-ACK + FIN)"; + if (simulateNak) { + EXPECT_GT(tlm[channelId].get_recvNakSegmentRequests(), 0u) + << "NAK segment requests should be received when peer requests retransmission"; + } + + // Verify no errors occurred + EXPECT_EQ(0u, tlm[channelId].get_recvErrors()) + << "No receive errors should occur"; + + // Clean up + cleanupTestFile(srcFile); +} + +// ---------------------------------------------------------------------- +// Command based Transaction Tests +// ---------------------------------------------------------------------- + +void CfdpManagerTester::testClass1TxNominal() { + sendAndVerifyClass1Tx( + "test/ut/output/test_class1_tx.bin", + "test/ut/output/test_class1_tx_dst.dat", + component.getOutgoingFileChunkSizeParam() + ); +} + +void CfdpManagerTester::testClass2TxNominal() { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + const FwSizeType expectedFileSize = 5 * dataPerPdu; + + sendAndVerifyClass2Tx( + INIT_BY_COMMAND, + "test/ut/output/test_class2_tx_5pdu.bin", + "test/ut/output/test_class2_tx_dst.dat", + expectedFileSize, + false // No NAK simulation + ); +} + +void CfdpManagerTester::testClass2TxNack() { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + const FwSizeType expectedFileSize = 5 * dataPerPdu; + + sendAndVerifyClass2Tx( + INIT_BY_COMMAND, + "test/ut/output/test_c2_tx_nak.bin", + "test/ut/output/test_c2_nak_dst.dat", + expectedFileSize, + true // Simulate NAK + ); +} + +void CfdpManagerTester::testClass1RxNominal() { + const U16 fileDataSize = static_cast(this->component.getOutgoingFileChunkSizeParam()); + + sendAndVerifyClass1Rx( + "test/ut/output/test_rx_source.bin", + "test/ut/output/test_rx_received.bin", + "/ground/test_rx_source.bin", + fileDataSize + ); +} + +void CfdpManagerTester::testClass2RxNominal() { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + const FwSizeType expectedFileSize = 5 * dataPerPdu; + + sendAndVerifyClass2Rx( + "test/ut/output/test_class2_rx_source.bin", + "test/ut/output/test_class2_rx_received.bin", + "/ground/test_class2_rx_source.bin", + expectedFileSize, + false // No NAK simulation + ); +} + +void CfdpManagerTester::testClass2RxNack() { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + const FwSizeType expectedFileSize = 5 * dataPerPdu; + + sendAndVerifyClass2Rx( + "test/ut/output/test_class2_rx_nack_source.bin", + "test/ut/output/test_class2_rx_nack_received.bin", + "/ground/test_class2_rx_nack_source.bin", + expectedFileSize, + true // Simulate NAK + ); +} + +// ---------------------------------------------------------------------- +// Port-Based Transaction Tests +// ---------------------------------------------------------------------- + +void CfdpManagerTester::testClass2TxPortBased() { + // Port-initiated transfers use Class 2 for reliability + sendAndVerifyClass2Tx( + INIT_BY_PORT, + "test/ut/output/test_class1_tx_port.bin", + "test/ut/output/test_class1_tx_port_dst.dat", + component.getOutgoingFileChunkSizeParam(), + false // No NAK simulation + ); +} + +// ---------------------------------------------------------------------- +// Multi-Transactions Tests +// ---------------------------------------------------------------------- + +void CfdpManagerTester::testMultipleTransactionsInSeries() { + const U16 dataPerPdu = static_cast(this->component.getOutgoingFileChunkSizeParam()); + + // Transaction 1: Class 1 TX (command-based) + sendAndVerifyClass1Tx( + "test/ut/output/series_c1_tx.bin", + "test/ut/output/series_c1_tx_dst.dat", + dataPerPdu + ); + + // Transaction 2: Class 1 RX + sendAndVerifyClass1Rx( + "test/ut/output/series_c1_rx_src.bin", + "test/ut/output/series_c1_rx_dst.bin", + "/ground/series_c1_rx_src.bin", + dataPerPdu + ); + + // Transaction 3: Class 2 TX (port-based) + sendAndVerifyClass2Tx( + INIT_BY_PORT, + "test/ut/output/series_c2_tx.bin", + "test/ut/output/series_c2_tx_dst.dat", + 5 * dataPerPdu, + false // No NAK simulation + ); + + // Transaction 4: Class 2 RX + sendAndVerifyClass2Rx( + "test/ut/output/series_c2_rx_src.bin", + "test/ut/output/series_c2_rx_dst.bin", + "/ground/series_c2_rx_src.bin", + 5 * dataPerPdu, + false // No NAK simulation + ); +} + +// ---------------------------------------------------------------------- +// Miscellaneous Tests +// ---------------------------------------------------------------------- + +void CfdpManagerTester ::testPing() { + const U32 key = 1234; + this->invoke_to_pingIn(0, key); + this->component.doDispatch(); + ASSERT_from_pingOut_SIZE(1); + ASSERT_from_pingOut(0, key); +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.hpp b/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.hpp new file mode 100644 index 00000000000..23362ca99a5 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/test/ut/CfdpManagerTester.hpp @@ -0,0 +1,604 @@ +// ====================================================================== +// \title CfdpManagerTester.hpp +// \author Brian Campuzano +// \brief hpp file for CfdpManager component test harness implementation class +// ====================================================================== + +#ifndef Svc_Ccsds_CfdpManagerTester_HPP +#define Svc_Ccsds_CfdpManagerTester_HPP + +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +class CfdpManagerTester final : public CfdpManagerGTestBase { + public: + // ---------------------------------------------------------------------- + // Constants + // ---------------------------------------------------------------------- + + // Maximum size of histories storing events, telemetry, and port outputs + static const FwSizeType MAX_HISTORY_SIZE = 100; + + // Instance ID supplied to the component instance under test + static const FwEnumStoreType TEST_INSTANCE_ID = 0; + + // Queue depth supplied to the component instance under test + static const FwSizeType TEST_INSTANCE_QUEUE_DEPTH = 10; + + public: + // ---------------------------------------------------------------------- + // Construction and destruction + // ---------------------------------------------------------------------- + + //! Construct object CfdpManagerTester + CfdpManagerTester(); + + //! Destroy object CfdpManagerTester + ~CfdpManagerTester(); + + public: + // ---------------------------------------------------------------------- + // White Box PDU Tests + // ---------------------------------------------------------------------- + + //! Test generating a Metadata PDU + void testMetaDataPdu(); + + //! Test generating a File Data PDU + void testFileDataPdu(); + + //! Test generating an EOF PDU + void testEofPdu(); + + //! Test generating a FIN PDU + void testFinPdu(); + + //! Test generating an ACK PDU + void testAckPdu(); + + //! Test generating a NAK PDU + void testNakPdu(); + + private: + // ---------------------------------------------------------------------- + // Helper functions + // ---------------------------------------------------------------------- + + //! Connect ports + void connectPorts(); + + //! Initialize components + void initComponents(); + + // ---------------------------------------------------------------------- + // PDU Downlink Test Helper Functions + // ---------------------------------------------------------------------- + + //! Helper to create a minimal transaction for testing + //! @param state Transaction state (S1, S2, R1, R2, etc.) + //! @param channelId CFDP channel number (0-based) + //! @param srcFilename Source filename for the transfer + //! @param dstFilename Destination filename for the transfer + //! @param fileSize File size in octets + //! @param sequenceId Transaction sequence number + //! @param peerId Peer entity ID + //! @return Pointer to configured transaction (owned by component) + Transaction* setupTestTransaction( + TxnState state, + U8 channelId, + const char* srcFilename, + const char* dstFilename, + U32 fileSize, + U32 sequenceId, + U32 peerId + ); + + //! Helper to get PDU buffer from dataOut port history + //! @param index History index (0 for most recent) + //! @return Reference to the buffer + const Fw::Buffer& getSentPduBuffer(FwIndexType index); + + //! Helper to verify Metadata PDU (deserialize + validate) + //! @param pduBuffer Buffer containing complete PDU bytes (header + body) + //! @param expectedSourceEid Expected source entity ID + //! @param expectedDestEid Expected destination entity ID + //! @param expectedTransactionSeq Expected transaction sequence number + //! @param expectedFileSize Expected file size + //! @param expectedSourceFilename Expected source filename + //! @param expectedDestFilename Expected destination filename + //! @param expectedClass Expected CFDP class (CLASS_1 or CLASS_2) + void verifyMetadataPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + FileSize expectedFileSize, + const char* expectedSourceFilename, + const char* expectedDestFilename, + Svc::Ccsds::Cfdp::Class::T expectedClass + ); + + //! Helper to verify File Data PDU (deserialize + validate) + //! @param pduBuffer Buffer containing complete PDU bytes (header + body) + //! @param expectedSourceEid Expected source entity ID + //! @param expectedDestEid Expected destination entity ID + //! @param expectedTransactionSeq Expected transaction sequence number + //! @param expectedOffset Expected file offset + //! @param expectedDataSize Expected data size + //! @param filename Source file to read expected data from + void verifyFileDataPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + U32 expectedOffset, + U16 expectedDataSize, + const char* filename, + Svc::Ccsds::Cfdp::Class::T expectedClass + ); + + //! Helper to verify EOF PDU (deserialize + validate) + //! @param pduBuffer Buffer containing complete PDU bytes (header + body) + //! @param expectedSourceEid Expected source entity ID + //! @param expectedDestEid Expected destination entity ID + //! @param expectedTransactionSeq Expected transaction sequence number + //! @param expectedConditionCode Expected condition code + //! @param expectedFileSize Expected file size + //! @param sourceFilename Source file path to compute CRC for validation + void verifyEofPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + Cfdp::ConditionCode expectedConditionCode, + FileSize expectedFileSize, + const char* sourceFilename + ); + + //! Helper to verify FIN PDU (deserialize + validate) + //! @param pduBuffer Buffer containing complete PDU bytes (header + body) + //! @param expectedSourceEid Expected source entity ID + //! @param expectedDestEid Expected destination entity ID + //! @param expectedTransactionSeq Expected transaction sequence number + //! @param expectedConditionCode Expected condition code + //! @param expectedDeliveryCode Expected delivery code + //! @param expectedFileStatus Expected file status + void verifyFinPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + Cfdp::ConditionCode expectedConditionCode, + Cfdp::FinDeliveryCode expectedDeliveryCode, + Cfdp::FinFileStatus expectedFileStatus + ); + + //! Helper to verify ACK PDU (deserialize + validate) + //! @param pduBuffer Buffer containing complete PDU bytes (header + body) + //! @param expectedSourceEid Expected source entity ID + //! @param expectedDestEid Expected destination entity ID + //! @param expectedTransactionSeq Expected transaction sequence number + //! @param expectedDirectiveCode Expected directive code being acknowledged + //! @param expectedDirectiveSubtypeCode Expected directive subtype code + //! @param expectedConditionCode Expected condition code + //! @param expectedTransactionStatus Expected transaction status + void verifyAckPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + Cfdp::FileDirective expectedDirectiveCode, + U8 expectedDirectiveSubtypeCode, + Cfdp::ConditionCode expectedConditionCode, + Cfdp::AckTxnStatus expectedTransactionStatus + ); + + //! Helper to verify NAK PDU (deserialize + validate) + //! @param pduBuffer Buffer containing complete PDU bytes (header + body) + //! @param expectedSourceEid Expected source entity ID + //! @param expectedDestEid Expected destination entity ID + //! @param expectedTransactionSeq Expected transaction sequence number + //! @param expectedScopeStart Expected scope start offset + //! @param expectedScopeEnd Expected scope end offset + //! @param expectedNumSegments Expected number of segment requests (0 = skip segment validation) + //! @param expectedSegments Optional array of expected segment requests (only used if expectedNumSegments > 0) + void verifyNakPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + FileSize expectedScopeStart, + FileSize expectedScopeEnd, + U8 expectedNumSegments, + const Cfdp::SegmentRequest* expectedSegments + ); + + //! Helper to find transaction by sequence number + //! @param chanNum Channel number to search + //! @param seqNum Transaction sequence number + //! @return Pointer to transaction or nullptr if not found + Transaction* findTransaction(U8 chanNum, TransactionSeq seqNum); + + // ---------------------------------------------------------------------- + // PDU Uplink Helper Functions + // ---------------------------------------------------------------------- + + //! Helper to send a Metadata PDU to CfdpManager via dataIn + //! @param channelId CFDP channel number + //! @param sourceEid Source entity ID (ground) + //! @param destEid Destination entity ID (FSW) + //! @param transactionSeq Transaction sequence number + //! @param fileSize File size in octets + //! @param sourceFilename Source filename + //! @param destFilename Destination filename + //! @param class Transmission mode (Class 1 or Class 2) + //! @param closureRequested Closure requested flag (typically 0 for Class 1, 1 for Class 2) + void sendMetadataPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + FileSize fileSize, + const char* sourceFilename, + const char* destFilename, + Cfdp::Class::T txmMode, + U8 closureRequested + ); + + //! Helper to send a File Data PDU to CfdpManager via dataIn + //! @param channelId CFDP channel number + //! @param sourceEid Source entity ID (ground) + //! @param destEid Destination entity ID (FSW) + //! @param transactionSeq Transaction sequence number + //! @param offset File offset + //! @param dataSize Data size in octets + //! @param data Pointer to file data + //! @param class Transmission mode (Class 1 or Class 2) + void sendFileDataPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + FileSize offset, + U16 dataSize, + const U8* data, + Cfdp::Class::T txmMode + ); + + //! Helper to send an EOF PDU to CfdpManager via dataIn + //! @param channelId CFDP channel number + //! @param sourceEid Source entity ID (ground) + //! @param destEid Destination entity ID (FSW) + //! @param transactionSeq Transaction sequence number + //! @param conditionCode Condition code + //! @param checksum File checksum + //! @param fileSize File size in octets + //! @param class Transmission mode (Class 1 or Class 2) + void sendEofPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + Cfdp::ConditionCode conditionCode, + U32 checksum, + FileSize fileSize, + Cfdp::Class::T txmMode + ); + + //! Helper to send a FIN PDU to CfdpManager via dataIn + //! @param channelId CFDP channel number + //! @param sourceEid Source entity ID (ground as receiver) + //! @param destEid Destination entity ID (FSW as sender) + //! @param transactionSeq Transaction sequence number + //! @param conditionCode Condition code + //! @param deliveryCode Delivery code + //! @param fileStatus File status + void sendFinPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + Cfdp::ConditionCode conditionCode, + Cfdp::FinDeliveryCode deliveryCode, + Cfdp::FinFileStatus fileStatus + ); + + //! Helper to send an ACK PDU to CfdpManager via dataIn + //! @param channelId CFDP channel number + //! @param sourceEid Source entity ID (ground as receiver) + //! @param destEid Destination entity ID (FSW as sender) + //! @param transactionSeq Transaction sequence number + //! @param directiveCode Directive being acknowledged + //! @param directiveSubtypeCode Directive subtype code + //! @param conditionCode Condition code + //! @param transactionStatus Transaction status + void sendAckPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + Cfdp::FileDirective directiveCode, + U8 directiveSubtypeCode, + Cfdp::ConditionCode conditionCode, + Cfdp::AckTxnStatus transactionStatus + ); + + //! Helper to send a NAK PDU to CfdpManager via dataIn + //! @param channelId CFDP channel number + //! @param sourceEid Source entity ID (ground as receiver) + //! @param destEid Destination entity ID (FSW as sender) + //! @param transactionSeq Transaction sequence number + //! @param scopeStart Scope start offset + //! @param scopeEnd Scope end offset + //! @param numSegments Number of segment requests (0 to CF_NAK_MAX_SEGMENTS) + //! @param segments Array of segment requests (can be nullptr if numSegments is 0) + void sendNakPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + FileSize scopeStart, + FileSize scopeEnd, + U8 numSegments, + const Cfdp::SegmentRequest* segments + ); + + public: + // ---------------------------------------------------------------------- + // Transaction Tests + // ---------------------------------------------------------------------- + + //! Test nominal Class 1 TX file transfer + void testClass1TxNominal(); + + //! Test nominal Class 2 TX file transfer + void testClass2TxNominal(); + + //! Test Class 2 TX file transfer with NAK handling + void testClass2TxNack(); + + //! Test nominal Class 1 RX file transfer + void testClass1RxNominal(); + + //! Test nominal Class 2 RX file transfer + void testClass2RxNominal(); + + //! Test Class 2 RX file transfer with NAK retransmission + void testClass2RxNack(); + + //! Test nominal Class 2 TX file transfer (port-based) + void testClass2TxPortBased(); + + //! Test Class 2 TX file transfer with NAK handling (port-based) + void testClass2TxPortBasedNack(); + + //! Test multiple transactions in series + void testMultipleTransactionsInSeries(); + + public: + // ---------------------------------------------------------------------- + // Miscellaneous Tests + // ---------------------------------------------------------------------- + + //! Test ping port functionality + void testPing(); + + private: + // ---------------------------------------------------------------------- + // Test Harness: output port overrides + // ---------------------------------------------------------------------- + + //! Handler for from_bufferAllocate - allocates test buffers + Fw::Buffer from_bufferAllocate_handler( + FwIndexType portNum, + FwSizeType size + ) override; + + //! Handler for from_dataOut - copies PDU data to avoid buffer reuse issues + void from_dataOut_handler( + FwIndexType portNum, + Fw::Buffer& fwBuffer + ) override; + + //! Handler for from_fileDoneOut - just push port history for testing + void from_fileDoneOut_handler( + FwIndexType portNum, + const Svc::SendFileResponse& response + ) override; + + private: + // ---------------------------------------------------------------------- + // Test helper functions + // ---------------------------------------------------------------------- + + //! Test configuration constants + static constexpr U8 TEST_CHANNEL_ID_0 = 0; + static constexpr U8 TEST_CHANNEL_ID_1 = 1; + static constexpr EntityId TEST_GROUND_EID = 100; + static constexpr U8 TEST_PRIORITY = 0; + + //! Helper struct for transaction setup results + struct TransactionSetup { + U32 expectedSeqNum; + Transaction* txn; + }; + + //! Create test file and verify size matches expected + void createAndVerifyTestFile( + const char* filePath, + FwSizeType expectedFileSize, + FwSizeType& actualFileSize + ); + + //! Setup TX transaction and verify initial state (command-based) + void setupTxTransaction( + const char* srcFile, + const char* dstFile, + U8 channelId, + EntityId destEid, + Cfdp::Class cfdpClass, + U8 priority, + TxnState expectedState, + TransactionSetup& setup + ); + + //! Setup TX transaction and verify initial state (port-based) + void setupTxPortTransaction( + const char* srcFile, + const char* dstFile, + U8 channelId, + TxnState expectedState, + TransactionSetup& setup + ); + + //! Setup RX transaction via Metadata PDU and verify initial state + void setupRxTransaction( + const char* srcFile, + const char* dstFile, + U8 channelId, + EntityId sourceEid, + Cfdp::Class::T cfdpClass, + U32 fileSize, + U32 transactionSeq, + TxnState expectedState, + TransactionSetup& setup + ); + + //! Wait for transaction to be recycled by inactivity timer + void waitForTransactionRecycle(U8 channelId, U32 expectedSeqNum); + + //! Complete Class 2 transaction handshake (EOF-ACK, FIN, FIN-ACK) + void completeClass2Handshake( + U8 channelId, + EntityId destEid, + U32 expectedSeqNum, + Transaction* txn + ); + + //! Initiate a file transfer via SendFile port + //! @param srcFile Source file path + //! @param dstFile Destination file path + //! @return SendFileResponse from port + Svc::SendFileResponse invokeSendFilePort( + const char* srcFile, + const char* dstFile + ); + + //! Send and verify a Class 1 TX transaction (command-based only) + //! @param srcFile Source file path + //! @param dstFile Destination file path + //! @param expectedFileSize Expected size of file to transfer + void sendAndVerifyClass1Tx( + const char* srcFile, + const char* dstFile, + FwSizeType expectedFileSize + ); + + //! Send and verify a Class 2 TX transaction + //! @param initType How to initiate the transaction (command or port) + //! @param srcFile Source file path + //! @param dstFile Destination file path + //! @param expectedFileSize Expected size of file to transfer + //! @param simulateNak If true, simulate NAK response (optional, default false) + void sendAndVerifyClass2Tx( + TransactionInitType initType, + const char* srcFile, + const char* dstFile, + FwSizeType expectedFileSize, + bool simulateNak = false + ); + + //! Receive and verify a Class 1 RX transaction + //! @param srcFile Local file to read test data from + //! @param dstFile Destination file path where received file will be written + //! @param groundSrcFile Source filename from ground perspective + //! @param expectedFileSize Expected size of file to receive + void sendAndVerifyClass1Rx( + const char* srcFile, + const char* dstFile, + const char* groundSrcFile, + FwSizeType expectedFileSize + ); + + //! Receive and verify a Class 2 RX transaction + //! @param srcFile Local file to read test data from + //! @param dstFile Destination file path where received file will be written + //! @param groundSrcFile Source filename from ground perspective + //! @param expectedFileSize Expected size of file to receive + //! @param simulateNak If true, simulate missing FileData to trigger NAK (optional, default false) + void sendAndVerifyClass2Rx( + const char* srcFile, + const char* dstFile, + const char* groundSrcFile, + FwSizeType expectedFileSize, + bool simulateNak = false + ); + + //! Verify FIN-ACK PDU at given index + void verifyFinAckPdu( + FwIndexType pduIndex, + EntityId sourceEid, + EntityId destEid, + U32 expectedSeqNum + ); + + //! Verify Metadata PDU at specific index in port history + void verifyMetadataPduAtIndex( + FwIndexType pduIndex, + const TransactionSetup& setup, + FwSizeType fileSize, + const char* srcFile, + const char* dstFile, + Cfdp::Class::T cfdpClass + ); + + //! Verify multiple FileData PDUs in sequence + void verifyMultipleFileDataPdus( + FwIndexType startIndex, + U8 numPdus, + const TransactionSetup& setup, + U16 dataPerPdu, + const char* srcFile, + Cfdp::Class::T cfdpClass + ); + + //! Clean up test file (remove and verify) + void cleanupTestFile(const char* filePath); + + //! Verify received file matches expected data + void verifyReceivedFile( + const char* filePath, + const U8* expectedData, + FwSizeType expectedSize + ); + + private: + // ---------------------------------------------------------------------- + // Member variables + // ---------------------------------------------------------------------- + + //! The component under test + CfdpManager component; + + //! Reusable buffer for allocation handler + U8 m_internalDataBuffer[MaxPduSize]; + + //! Storage for PDU copies (to avoid buffer reuse issues) + static constexpr FwSizeType MAX_PDU_COPIES = 100; + U8 m_pduCopyStorage[MAX_PDU_COPIES][MaxPduSize]; + FwSizeType m_pduCopyCount; +}; + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc + +#endif diff --git a/Svc/Ccsds/CfdpManager/test/ut/PduTester.cpp b/Svc/Ccsds/CfdpManager/test/ut/PduTester.cpp new file mode 100644 index 00000000000..34a3a31a649 --- /dev/null +++ b/Svc/Ccsds/CfdpManager/test/ut/PduTester.cpp @@ -0,0 +1,1001 @@ +// ====================================================================== +// \title PduTester.cpp +// \author Brian Campuzano +// \brief cpp file for PDU test implementations +// +// This file contains PDU test function implementations for CfdpManagerTester. +// The declarations remain in CfdpManagerTester.hpp. +// ====================================================================== + +#include "CfdpManagerTester.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ---------------------------------------------------------------------- +// PDU Test Helper Implementations +// ---------------------------------------------------------------------- + +Transaction* CfdpManagerTester::setupTestTransaction( + TxnState state, + U8 channelId, + const char* srcFilename, + const char* dstFilename, + U32 fileSize, + U32 sequenceId, + U32 peerId +) { + // For white box testing, directly use the first transaction for the specified channel + Channel* chan = component.m_engine->m_channels[channelId]; + FW_ASSERT(chan != nullptr); + + Transaction* txn = chan->getTransaction(0); // Use first transaction for channel + Cfdp::History* history = chan->getHistory(0); // Use first history for channel + + // Initialize transaction state + txn->m_state = state; + txn->m_fsize = fileSize; + txn->m_chan_num = channelId; + txn->m_cfdpManager = &this->component; + txn->m_history = history; + + // Set transaction class based on state + // S2/R2 are Class 2, S1/R1 are Class 1 + if ((state == TXN_STATE_S2) || (state == TXN_STATE_R2)) { + txn->m_txn_class = Cfdp::Class::CLASS_2; + } else { + txn->m_txn_class = Cfdp::Class::CLASS_1; + } + + // Initialize history + history->peer_eid = peerId; + history->seq_num = sequenceId; + history->fnames.src_filename = Fw::String(srcFilename); + history->fnames.dst_filename = Fw::String(dstFilename); + history->dir = DIRECTION_TX; + + return txn; +} + +const Fw::Buffer& CfdpManagerTester::getSentPduBuffer(FwIndexType index) { + // Retrieve PDU buffer from dataOut port history + EXPECT_GT(this->fromPortHistory_dataOut->size(), index); + static Fw::Buffer emptyBuffer; + if (this->fromPortHistory_dataOut->size() <= static_cast(index)) { + return emptyBuffer; + } + + // Extract buffer from history entry + const FromPortEntry_dataOut& entry = + this->fromPortHistory_dataOut->at(index); + return entry.fwBuffer; +} + +// ---------------------------------------------------------------------- +// PDU Verify Functions +// ---------------------------------------------------------------------- + +void CfdpManagerTester::verifyMetadataPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + FileSize expectedFileSize, + const char* expectedSourceFilename, + const char* expectedDestFilename, + Svc::Ccsds::Cfdp::Class::T expectedClass +) { + // Deserialize PDU + Cfdp::MetadataPdu metadataPdu; + Fw::SerialBuffer sb(const_cast(pduBuffer.getData()), pduBuffer.getSize()); + sb.setBuffLen(pduBuffer.getSize()); + Fw::SerializeStatus status = metadataPdu.deserializeFrom(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to deserialize Metadata PDU"; + + // Validate header fields + const Cfdp::PduHeader& header = metadataPdu.asHeader(); + EXPECT_EQ(Cfdp::T_METADATA, header.getType()) << "Expected T_METADATA type"; + EXPECT_EQ(Cfdp::DIRECTION_TOWARD_RECEIVER, header.getDirection()) << "Expected direction toward receiver"; + EXPECT_EQ(expectedClass, header.getTxmMode()) << "TX mode mismatch"; + EXPECT_EQ(expectedSourceEid, header.getSourceEid()) << "Source EID mismatch"; + EXPECT_EQ(expectedDestEid, header.getDestEid()) << "Destination EID mismatch"; + EXPECT_EQ(expectedTransactionSeq, header.getTransactionSeq()) << "Transaction sequence mismatch"; + + // Validate metadata-specific fields + EXPECT_EQ(expectedFileSize, metadataPdu.getFileSize()) << "File size mismatch"; + EXPECT_EQ(Cfdp::CHECKSUM_TYPE_MODULAR, metadataPdu.getChecksumType()) << "Expected modular checksum type"; + + // Closure requested should be 0 for Class 1, 1 for Class 2 + U8 expectedClosureRequested = (expectedClass == Cfdp::Class::CLASS_2) ? 1 : 0; + EXPECT_EQ(expectedClosureRequested, metadataPdu.getClosureRequested()) + << "Closure requested mismatch for class " << static_cast(expectedClass); + + // Validate source filename + const char* rxSrcFilename = metadataPdu.getSourceFilename().toChar(); + ASSERT_NE(nullptr, rxSrcFilename) << "Source filename is null"; + FwSizeType srcLen = strlen(expectedSourceFilename); + EXPECT_EQ(0, memcmp(rxSrcFilename, expectedSourceFilename, srcLen)) << "Source filename mismatch"; + + // Validate destination filename + const char* rxDstFilename = metadataPdu.getDestFilename().toChar(); + ASSERT_NE(nullptr, rxDstFilename) << "Destination filename is null"; + FwSizeType dstLen = strlen(expectedDestFilename); + EXPECT_EQ(0, memcmp(rxDstFilename, expectedDestFilename, dstLen)) << "Destination filename mismatch"; +} + +void CfdpManagerTester::verifyFileDataPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + U32 expectedOffset, + U16 expectedDataSize, + const char* filename, + Svc::Ccsds::Cfdp::Class::T expectedClass +) { + // Deserialize PDU + Cfdp::FileDataPdu fileDataPdu; + Fw::SerialBuffer sb(const_cast(pduBuffer.getData()), pduBuffer.getSize()); + sb.setBuffLen(pduBuffer.getSize()); + Fw::SerializeStatus status = fileDataPdu.deserializeFrom(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to deserialize File Data PDU"; + + // Validate header fields + const Cfdp::PduHeader& header = fileDataPdu.asHeader(); + EXPECT_EQ(Cfdp::T_FILE_DATA, header.getType()) << "Expected T_FILE_DATA type"; + EXPECT_EQ(Cfdp::DIRECTION_TOWARD_RECEIVER, header.getDirection()) << "Expected direction toward receiver"; + EXPECT_EQ(expectedClass, header.getTxmMode()) << "TX mode mismatch"; + EXPECT_EQ(expectedSourceEid, header.getSourceEid()) << "Source EID mismatch"; + EXPECT_EQ(expectedDestEid, header.getDestEid()) << "Destination EID mismatch"; + EXPECT_EQ(expectedTransactionSeq, header.getTransactionSeq()) << "Transaction sequence mismatch"; + + // Validate file data fields + U32 offset = fileDataPdu.getOffset(); + U16 dataSize = fileDataPdu.getDataSize(); + const U8* pduData = fileDataPdu.getData(); + + EXPECT_EQ(expectedOffset, offset) << "File offset mismatch"; + EXPECT_EQ(expectedDataSize, dataSize) << "Data size mismatch"; + ASSERT_NE(nullptr, pduData) << "Data pointer is null"; + ASSERT_GT(dataSize, 0U) << "Data size is zero"; + + // Read expected data from file at the offset specified in the PDU + U8* expectedData = new U8[dataSize]; + Os::File file; + + Os::File::Status fileStatus = file.open(filename, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to open file: " << filename; + + fileStatus = file.seek(static_cast(offset), Os::File::ABSOLUTE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to seek in file"; + + FwSizeType bytesRead = dataSize; + fileStatus = file.read(expectedData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to read from file"; + ASSERT_EQ(dataSize, bytesRead) << "Failed to read expected data from file"; + + // Validate data content + EXPECT_EQ(0, memcmp(expectedData, pduData, dataSize)) + << "Data content mismatch at offset " << offset; + + delete[] expectedData; +} + +void CfdpManagerTester::verifyEofPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + Cfdp::ConditionCode expectedConditionCode, + FileSize expectedFileSize, + const char* sourceFilename +) { + // Deserialize PDU + Cfdp::EofPdu eofPdu; + Fw::SerialBuffer sb(const_cast(pduBuffer.getData()), pduBuffer.getSize()); + sb.setBuffLen(pduBuffer.getSize()); + Fw::SerializeStatus status = eofPdu.deserializeFrom(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to deserialize EOF PDU"; + + // Validate header fields + const Cfdp::PduHeader& header = eofPdu.asHeader(); + EXPECT_EQ(Cfdp::T_EOF, header.getType()) << "Expected T_EOF type"; + EXPECT_EQ(Cfdp::DIRECTION_TOWARD_RECEIVER, header.getDirection()) << "Expected direction toward receiver"; + // Note: Can be either acknowledged or unacknowledged depending on class + EXPECT_EQ(expectedSourceEid, header.getSourceEid()) << "Source EID mismatch"; + EXPECT_EQ(expectedDestEid, header.getDestEid()) << "Destination EID mismatch"; + EXPECT_EQ(expectedTransactionSeq, header.getTransactionSeq()) << "Transaction sequence mismatch"; + + // Validate EOF-specific fields + EXPECT_EQ(expectedConditionCode, eofPdu.getConditionCode()) << "Condition code mismatch"; + EXPECT_EQ(expectedFileSize, eofPdu.getFileSize()) << "File size mismatch"; + + // For Class 1 (unacknowledged), checksum validation is optional + // For Class 2 (acknowledged), validate checksum if non-zero + U32 rxChecksum = eofPdu.getChecksum(); + if (rxChecksum != 0) { + // Compute file CRC and validate against EOF PDU checksum + // Open and read the source file to compute CRC + Os::File file; + Os::File::Status fileStatus = file.open(sourceFilename, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to open source file: " << sourceFilename; + + // Allocate buffer for file content + U8* fileData = new U8[expectedFileSize]; + FwSizeType bytesRead = expectedFileSize; + fileStatus = file.read(fileData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to read source file"; + ASSERT_EQ(expectedFileSize, bytesRead) << "Failed to read complete file"; + + // Compute CRC using CFDP Checksum + CFDP::Checksum computedChecksum; + computedChecksum.update(fileData, 0, expectedFileSize); + U32 expectedCrc = computedChecksum.getValue(); + + delete[] fileData; + + // Validate checksum matches + EXPECT_EQ(expectedCrc, rxChecksum) << "File CRC mismatch"; + } +} + +void CfdpManagerTester::verifyFinPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + Cfdp::ConditionCode expectedConditionCode, + Cfdp::FinDeliveryCode expectedDeliveryCode, + Cfdp::FinFileStatus expectedFileStatus +) { + // Deserialize PDU + Cfdp::FinPdu finPdu; + Fw::SerialBuffer sb(const_cast(pduBuffer.getData()), pduBuffer.getSize()); + sb.setBuffLen(pduBuffer.getSize()); + Fw::SerializeStatus status = finPdu.deserializeFrom(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to deserialize FIN PDU"; + + // Validate header fields + const Cfdp::PduHeader& header = finPdu.asHeader(); + EXPECT_EQ(Cfdp::T_FIN, header.getType()) << "Expected T_FIN type"; + EXPECT_EQ(Cfdp::DIRECTION_TOWARD_SENDER, header.getDirection()) << "Expected direction toward sender"; + EXPECT_EQ(Cfdp::Class::CLASS_2, header.getTxmMode()) << "Expected acknowledged mode for class 2"; + EXPECT_EQ(expectedSourceEid, header.getSourceEid()) << "Source EID mismatch"; + EXPECT_EQ(expectedDestEid, header.getDestEid()) << "Destination EID mismatch"; + EXPECT_EQ(expectedTransactionSeq, header.getTransactionSeq()) << "Transaction sequence mismatch"; + + // Validate FIN-specific fields + EXPECT_EQ(expectedConditionCode, finPdu.getConditionCode()) << "Condition code mismatch"; + EXPECT_EQ(expectedDeliveryCode, finPdu.getDeliveryCode()) << "Delivery code mismatch"; + EXPECT_EQ(expectedFileStatus, finPdu.getFileStatus()) << "File status mismatch"; +} + +void CfdpManagerTester::verifyAckPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + Cfdp::FileDirective expectedDirectiveCode, + U8 expectedDirectiveSubtypeCode, + Cfdp::ConditionCode expectedConditionCode, + Cfdp::AckTxnStatus expectedTransactionStatus +) { + // Deserialize PDU + Cfdp::AckPdu ackPdu; + Fw::SerialBuffer sb(const_cast(pduBuffer.getData()), pduBuffer.getSize()); + sb.setBuffLen(pduBuffer.getSize()); + Fw::SerializeStatus status = ackPdu.deserializeFrom(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to deserialize ACK PDU"; + + // Validate header fields + const Cfdp::PduHeader& header = ackPdu.asHeader(); + EXPECT_EQ(Cfdp::T_ACK, header.getType()) << "Expected T_ACK type"; + EXPECT_EQ(Cfdp::Class::CLASS_2, header.getTxmMode()) << "Expected acknowledged mode for class 2"; + EXPECT_EQ(expectedSourceEid, header.getSourceEid()) << "Source EID mismatch"; + EXPECT_EQ(expectedDestEid, header.getDestEid()) << "Destination EID mismatch"; + EXPECT_EQ(expectedTransactionSeq, header.getTransactionSeq()) << "Transaction sequence mismatch"; + + // Validate ACK-specific fields + EXPECT_EQ(expectedDirectiveCode, ackPdu.getDirectiveCode()) << "Directive code mismatch"; + EXPECT_EQ(expectedDirectiveSubtypeCode, ackPdu.getDirectiveSubtypeCode()) << "Directive subtype code mismatch"; + EXPECT_EQ(expectedConditionCode, ackPdu.getConditionCode()) << "Condition code mismatch"; + EXPECT_EQ(expectedTransactionStatus, ackPdu.getTransactionStatus()) << "Transaction status mismatch"; +} + +void CfdpManagerTester::verifyNakPdu( + const Fw::Buffer& pduBuffer, + U32 expectedSourceEid, + U32 expectedDestEid, + U32 expectedTransactionSeq, + FileSize expectedScopeStart, + FileSize expectedScopeEnd, + U8 expectedNumSegments, + const Cfdp::SegmentRequest* expectedSegments +) { + // Deserialize PDU + Cfdp::NakPdu nakPdu; + Fw::SerialBuffer sb(const_cast(pduBuffer.getData()), pduBuffer.getSize()); + sb.setBuffLen(pduBuffer.getSize()); + Fw::SerializeStatus status = nakPdu.deserializeFrom(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to deserialize NAK PDU"; + + // Validate header fields + const Cfdp::PduHeader& header = nakPdu.asHeader(); + EXPECT_EQ(Cfdp::T_NAK, header.getType()) << "Expected T_NAK type"; + EXPECT_EQ(Cfdp::Class::CLASS_2, header.getTxmMode()) << "Expected acknowledged mode for class 2"; + EXPECT_EQ(expectedSourceEid, header.getSourceEid()) << "Source EID mismatch"; + EXPECT_EQ(expectedDestEid, header.getDestEid()) << "Destination EID mismatch"; + EXPECT_EQ(expectedTransactionSeq, header.getTransactionSeq()) << "Transaction sequence mismatch"; + + // Validate NAK-specific fields + EXPECT_EQ(expectedScopeStart, nakPdu.getScopeStart()) << "Scope start mismatch"; + EXPECT_EQ(expectedScopeEnd, nakPdu.getScopeEnd()) << "Scope end mismatch"; + + // Validate segment requests if expectedNumSegments > 0 + if (expectedNumSegments > 0) { + EXPECT_EQ(expectedNumSegments, nakPdu.getNumSegments()) + << "Expected " << static_cast(expectedNumSegments) << " segment requests"; + + // Validate each segment if expectedSegments array is provided + if (expectedSegments != nullptr) { + for (U8 i = 0; i < expectedNumSegments; i++) { + EXPECT_EQ(expectedSegments[i].offsetStart, nakPdu.getSegment(i).offsetStart) + << "Segment " << static_cast(i) << " start offset mismatch"; + EXPECT_EQ(expectedSegments[i].offsetEnd, nakPdu.getSegment(i).offsetEnd) + << "Segment " << static_cast(i) << " end offset mismatch"; + } + } + } +} + +// ---------------------------------------------------------------------- +// PDU Uplink Helper Functions +// ---------------------------------------------------------------------- + +void CfdpManagerTester::sendMetadataPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + FileSize fileSize, + const char* sourceFilename, + const char* destFilename, + Cfdp::Class::T txmMode, + U8 closureRequested +) { + // Create and initialize Metadata PDU + Cfdp::MetadataPdu metadataPdu; + metadataPdu.initialize( + Cfdp::DIRECTION_TOWARD_RECEIVER, + txmMode, + sourceEid, + transactionSeq, + destEid, + fileSize, + sourceFilename, + destFilename, + Cfdp::CHECKSUM_TYPE_MODULAR, + closureRequested + ); + + // Allocate buffer for PDU + U32 pduSize = metadataPdu.getBufferSize(); + Fw::Buffer pduBuffer(m_internalDataBuffer, pduSize); + + // Serialize PDU to buffer + Fw::SerialBuffer sb(pduBuffer.getData(), pduBuffer.getSize()); + Fw::SerializeStatus status = metadataPdu.serializeTo(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to serialize Metadata PDU"; + pduBuffer.setSize(sb.getSize()); + + // Send PDU to CfdpManager via dataIn port + invoke_to_dataIn(channelId, pduBuffer); +} + +void CfdpManagerTester::sendFileDataPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + FileSize offset, + U16 dataSize, + const U8* data, + Cfdp::Class::T txmMode +) { + // Create and initialize File Data PDU + Cfdp::FileDataPdu fileDataPdu; + fileDataPdu.initialize( + Cfdp::DIRECTION_TOWARD_RECEIVER, + txmMode, + sourceEid, + transactionSeq, + destEid, + offset, + dataSize, + data + ); + + // Allocate buffer for PDU + U32 pduSize = fileDataPdu.getBufferSize(); + Fw::Buffer pduBuffer(m_internalDataBuffer, pduSize); + + // Serialize PDU to buffer + Fw::SerialBuffer sb(pduBuffer.getData(), pduBuffer.getSize()); + Fw::SerializeStatus status = fileDataPdu.serializeTo(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to serialize File Data PDU"; + pduBuffer.setSize(sb.getSize()); + + // Send PDU to CfdpManager via dataIn port + invoke_to_dataIn(channelId, pduBuffer); +} + +void CfdpManagerTester::sendEofPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + Cfdp::ConditionCode conditionCode, + U32 checksum, + FileSize fileSize, + Cfdp::Class::T txmMode +) { + // Create and initialize EOF PDU + Cfdp::EofPdu eofPdu; + eofPdu.initialize( + Cfdp::DIRECTION_TOWARD_RECEIVER, + txmMode, + sourceEid, + transactionSeq, + destEid, + conditionCode, + checksum, + fileSize + ); + + // Allocate buffer for PDU + U32 pduSize = eofPdu.getBufferSize(); + Fw::Buffer pduBuffer(m_internalDataBuffer, pduSize); + + // Serialize PDU to buffer + Fw::SerialBuffer sb(pduBuffer.getData(), pduBuffer.getSize()); + Fw::SerializeStatus status = eofPdu.serializeTo(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to serialize EOF PDU"; + pduBuffer.setSize(sb.getSize()); + + // Send PDU to CfdpManager via dataIn port + invoke_to_dataIn(channelId, pduBuffer); +} + +void CfdpManagerTester::sendFinPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + Cfdp::ConditionCode conditionCode, + Cfdp::FinDeliveryCode deliveryCode, + Cfdp::FinFileStatus fileStatus +) { + // Create and initialize FIN PDU + Cfdp::FinPdu finPdu; + finPdu.initialize( + Cfdp::DIRECTION_TOWARD_SENDER, // FIN is sent from receiver to sender + Cfdp::Class::CLASS_2, // FIN is only used in Class 2 + sourceEid, + transactionSeq, + destEid, + conditionCode, + deliveryCode, + fileStatus + ); + + // Allocate buffer for PDU + U32 pduSize = finPdu.getBufferSize(); + Fw::Buffer pduBuffer(m_internalDataBuffer, pduSize); + + // Serialize PDU to buffer + Fw::SerialBuffer sb(pduBuffer.getData(), pduBuffer.getSize()); + Fw::SerializeStatus status = finPdu.serializeTo(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to serialize FIN PDU"; + pduBuffer.setSize(sb.getSize()); + + // Send PDU to CfdpManager via dataIn port + invoke_to_dataIn(channelId, pduBuffer); +} + +void CfdpManagerTester::sendAckPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + Cfdp::FileDirective directiveCode, + U8 directiveSubtypeCode, + Cfdp::ConditionCode conditionCode, + Cfdp::AckTxnStatus transactionStatus +) { + // Create and initialize ACK PDU + Cfdp::AckPdu ackPdu; + ackPdu.initialize( + Cfdp::DIRECTION_TOWARD_SENDER, // ACK is sent from receiver to sender + Cfdp::Class::CLASS_2, // ACK is only used in Class 2 + sourceEid, + transactionSeq, + destEid, + directiveCode, + directiveSubtypeCode, + conditionCode, + transactionStatus + ); + + // Allocate buffer for PDU + U32 pduSize = ackPdu.getBufferSize(); + Fw::Buffer pduBuffer(m_internalDataBuffer, pduSize); + + // Serialize PDU to buffer + Fw::SerialBuffer sb(pduBuffer.getData(), pduBuffer.getSize()); + Fw::SerializeStatus status = ackPdu.serializeTo(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to serialize ACK PDU"; + pduBuffer.setSize(sb.getSize()); + + // Send PDU to CfdpManager via dataIn port + invoke_to_dataIn(channelId, pduBuffer); +} + +void CfdpManagerTester::sendNakPdu( + U8 channelId, + EntityId sourceEid, + EntityId destEid, + TransactionSeq transactionSeq, + FileSize scopeStart, + FileSize scopeEnd, + U8 numSegments, + const Cfdp::SegmentRequest* segments +) { + // Create and initialize NAK PDU + Cfdp::NakPdu nakPdu; + nakPdu.initialize( + Cfdp::DIRECTION_TOWARD_SENDER, // NAK is sent from receiver to sender + Cfdp::Class::CLASS_2, // NAK is only used in Class 2 + sourceEid, + transactionSeq, + destEid, + scopeStart, + scopeEnd + ); + + // Verify segment count does not exceed maximum + ASSERT_LE(numSegments, CFDP_NAK_MAX_SEGMENTS) << "Number of segments exceeds CFDP_NAK_MAX_SEGMENTS"; + + // Add segment requests if provided + for (U8 i = 0; i < numSegments; i++) { + bool success = nakPdu.addSegment(segments[i].offsetStart, segments[i].offsetEnd); + ASSERT_TRUE(success) << "Failed to add segment " << static_cast(i) << " to NAK PDU"; + } + + // Allocate buffer for PDU + U32 pduSize = nakPdu.getBufferSize(); + Fw::Buffer pduBuffer(m_internalDataBuffer, pduSize); + + // Serialize PDU to buffer + Fw::SerialBuffer sb(pduBuffer.getData(), pduBuffer.getSize()); + Fw::SerializeStatus status = nakPdu.serializeTo(sb); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status) << "Failed to serialize NAK PDU"; + pduBuffer.setSize(sb.getSize()); + + // Send PDU to CfdpManager via dataIn port + invoke_to_dataIn(channelId, pduBuffer); +} + +// ---------------------------------------------------------------------- +// PDU Tests +// ---------------------------------------------------------------------- + +void CfdpManagerTester::testMetaDataPdu() { + // Test pattern: + // 1. Setup transaction + // 2. Invoke Engine->sendMd() + // 3. Capture PDU from dataOut + // 4. Deserialize and validate + + // Configure transaction for Metadata PDU emission + const char* srcFile = "/tmp/test_source.bin"; + const char* dstFile = "/tmp/test_dest.bin"; + const FileSize fileSize = 1024; + const U8 channelId = 0; + const U32 testSequenceId = 98; + const U32 testPeerId = 100; + + Transaction* txn = setupTestTransaction( + TXN_STATE_S1, // Sender, class 1 + channelId, + srcFile, + dstFile, + fileSize, + testSequenceId, + testPeerId + ); + ASSERT_NE(txn, nullptr) << "Failed to create test transaction"; + + // Clear port history before test + this->clearHistory(); + + // Invoke sender to emit Metadata PDU using refactored API + Cfdp::Status::T status = component.m_engine->sendMd(txn); + ASSERT_EQ(status, Cfdp::Status::SUCCESS) << "sendMd failed"; + + // Verify PDU was sent through dataOut port + ASSERT_FROM_PORT_HISTORY_SIZE(1); + + // Get encoded PDU buffer + const Fw::Buffer& pduBuffer = getSentPduBuffer(0); + ASSERT_GT(pduBuffer.getSize(), 0) << "PDU size is zero"; + + // Verify Metadata PDU + verifyMetadataPdu(pduBuffer, component.getLocalEidParam(), testPeerId, + testSequenceId, fileSize, srcFile, dstFile, Cfdp::Class::CLASS_1); +} + +void CfdpManagerTester::testFileDataPdu() { + // Test pattern: + // 1. Setup transaction + // 2. Read test file and construct File Data PDU + // 3. Invoke Engine->sendFd() + // 4. Capture PDU from dataOut and validate + + // Test file configuration + const char* testFilePath = "Types/test/ut/data/test_file.bin"; + const U32 fileOffset = 50; // Read from offset 50 + const U16 readSize = 64; // Read 64 bytes + + // Configure transaction for File Data PDU emission + const char* srcFile = testFilePath; + const char* dstFile = "/tmp/dest_file.bin"; + const U32 fileSize = 256; // Approximate file size + const U8 channelId = 0; + const U32 testSequenceId = 42; + const U32 testPeerId = 200; + + Transaction* txn = setupTestTransaction( + TXN_STATE_S1, // Sender, class 1 + channelId, + srcFile, + dstFile, + fileSize, + testSequenceId, + testPeerId + ); + ASSERT_NE(txn, nullptr) << "Failed to create test transaction"; + + // Clear port history before test + this->clearHistory(); + + // Read test data from file + U8 testData[readSize]; + Os::File file; + + Os::File::Status fileStatus = file.open(testFilePath, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to open test file: " << testFilePath; + + fileStatus = file.seek(static_cast(fileOffset), Os::File::ABSOLUTE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to seek in test file"; + + FwSizeType bytesRead = readSize; + fileStatus = file.read(testData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to read from test file"; + ASSERT_EQ(readSize, bytesRead) << "Failed to read test data from file"; + + // Create File Data PDU with test data + Cfdp::FileDataPdu fdPdu; + Cfdp::PduDirection direction = Cfdp::DIRECTION_TOWARD_RECEIVER; + + fdPdu.initialize( + direction, + Cfdp::Class::CLASS_1, // transmission mode + component.getLocalEidParam(), // source EID + testSequenceId, // transaction sequence number + testPeerId, // destination EID + fileOffset, // file offset + readSize, // data size + testData // data pointer + ); + + // Invoke sendFd using refactored API + Cfdp::Status::T status = component.m_engine->sendFd(txn, fdPdu); + ASSERT_EQ(status, Cfdp::Status::SUCCESS) << "sendFd failed"; + + // Verify PDU was sent through dataOut port + ASSERT_FROM_PORT_HISTORY_SIZE(1); + + // Get encoded PDU buffer + const Fw::Buffer& pduBuffer = getSentPduBuffer(0); + ASSERT_GT(pduBuffer.getSize(), 0) << "PDU size is zero"; + + // Verify File Data PDU + verifyFileDataPdu(pduBuffer, component.getLocalEidParam(), testPeerId, + testSequenceId, fileOffset, readSize, testFilePath, Cfdp::Class::CLASS_1); +} + +void CfdpManagerTester::testEofPdu() { + // Test pattern: + // 1. Setup transaction + // 2. Invoke Engine->sendEof() + // 3. Capture PDU from dataOut + // 4. Deserialize and validate + + // Configure transaction for EOF PDU emission + const char* srcFile = "Types/test/ut/data/test_file.bin"; + const char* dstFile = "/tmp/dest_eof.bin"; + const FileSize fileSize = 242; // Actual size of test_file.bin + const U8 channelId = 0; + const U32 testSequenceId = 55; + const U32 testPeerId = 150; + + Transaction* txn = setupTestTransaction( + TXN_STATE_S2, // Sender, class 2 (acknowledged mode) + channelId, + srcFile, + dstFile, + fileSize, + testSequenceId, + testPeerId + ); + ASSERT_NE(txn, nullptr) << "Failed to create test transaction"; + + // Setup transaction to simulate file transfer complete + const Cfdp::ConditionCode testConditionCode = Cfdp::CONDITION_CODE_NO_ERROR; + txn->m_state_data.send.cached_pos = fileSize; // Simulate file transfer complete + + // Read test file and compute CRC + Os::File file; + Os::File::Status fileStatus = file.open(srcFile, Os::File::OPEN_READ, Os::File::NO_OVERWRITE); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to open test file: " << srcFile; + + U8* fileData = new U8[fileSize]; + FwSizeType bytesRead = fileSize; + fileStatus = file.read(fileData, bytesRead, Os::File::WAIT); + file.close(); + ASSERT_EQ(Os::File::OP_OK, fileStatus) << "Failed to read test file"; + ASSERT_EQ(fileSize, bytesRead) << "Failed to read complete test file"; + + // Compute and set CRC in transaction (matches what sendEof expects) + txn->m_crc.update(fileData, 0, fileSize); + delete[] fileData; + + // Clear port history before test + this->clearHistory(); + + // Invoke sender to emit EOF PDU using refactored API + Cfdp::Status::T status = component.m_engine->sendEof(txn); + ASSERT_EQ(status, Cfdp::Status::SUCCESS) << "sendEof failed"; + + // Verify PDU was sent through dataOut port + ASSERT_FROM_PORT_HISTORY_SIZE(1); + + // Get encoded PDU buffer + const Fw::Buffer& pduBuffer = getSentPduBuffer(0); + ASSERT_GT(pduBuffer.getSize(), 0) << "PDU size is zero"; + + // Verify EOF PDU + verifyEofPdu(pduBuffer, component.getLocalEidParam(), testPeerId, + testSequenceId, testConditionCode, fileSize, srcFile); +} + +void CfdpManagerTester::testFinPdu() { + // Test pattern: + // 1. Setup transaction + // 2. Invoke Engine->sendFin() + // 3. Capture PDU from dataOut + // 4. Deserialize and validate + + // Configure transaction for FIN PDU emission + const char* srcFile = "/tmp/test_fin.bin"; + const char* dstFile = "/tmp/dest_fin.bin"; + const FileSize fileSize = 8192; + const U8 channelId = 0; + const U32 testSequenceId = 77; + const U32 testPeerId = 200; + + Transaction* txn = setupTestTransaction( + TXN_STATE_R2, // Receiver, class 2 (acknowledged mode) + channelId, + srcFile, + dstFile, + fileSize, + testSequenceId, + testPeerId + ); + ASSERT_NE(txn, nullptr) << "Failed to create test transaction"; + + // Setup transaction to simulate file reception complete + const ConditionCode testConditionCode = CONDITION_CODE_NO_ERROR; + const FinDeliveryCode testDeliveryCode = FIN_DELIVERY_CODE_COMPLETE; + const FinFileStatus testFileStatus = FIN_FILE_STATUS_RETAINED; + + // Clear port history before test + this->clearHistory(); + + // Invoke receiver to emit FIN PDU using refactored API + Cfdp::Status::T status = component.m_engine->sendFin(txn, testDeliveryCode, testFileStatus, + static_cast(testConditionCode)); + ASSERT_EQ(status, Cfdp::Status::SUCCESS) << "sendFin failed"; + + // Verify PDU was sent through dataOut port + ASSERT_FROM_PORT_HISTORY_SIZE(1); + + // Get encoded PDU buffer + const Fw::Buffer& pduBuffer = getSentPduBuffer(0); + ASSERT_GT(pduBuffer.getSize(), 0) << "PDU size is zero"; + + // Verify FIN PDU + // FIN PDU is sent from receiver (testPeerId) to sender (component.getLocalEidParam()) + // So source=testPeerId, dest=component.getLocalEidParam() + verifyFinPdu(pduBuffer, testPeerId, component.getLocalEidParam(), + testSequenceId, + static_cast(testConditionCode), + static_cast(testDeliveryCode), + static_cast(testFileStatus)); +} + +void CfdpManagerTester::testAckPdu() { + // Test pattern: + // 1. Setup transaction + // 2. Invoke Engine->sendAck() + // 3. Capture PDU from dataOut + // 4. Deserialize and validate + + // Configure transaction for ACK PDU emission + const char* srcFile = "/tmp/test_ack.bin"; + const char* dstFile = "/tmp/dest_ack.bin"; + const FileSize fileSize = 2048; + const U8 channelId = 0; + const U32 testSequenceId = 88; + const U32 testPeerId = 175; + + Transaction* txn = setupTestTransaction( + TXN_STATE_R2, // Receiver, class 2 (acknowledged mode) + channelId, + srcFile, + dstFile, + fileSize, + testSequenceId, + testPeerId + ); + ASSERT_NE(txn, nullptr) << "Failed to create test transaction"; + + // Setup test parameters for ACK PDU + const Cfdp::AckTxnStatus testTransactionStatus = Cfdp::ACK_TXN_STATUS_ACTIVE; + const Cfdp::FileDirective testDirectiveCode = Cfdp::FILE_DIRECTIVE_END_OF_FILE; + const Cfdp::ConditionCode testConditionCode = Cfdp::CONDITION_CODE_NO_ERROR; + + // Clear port history before test + this->clearHistory(); + + // Invoke sendAck using refactored API + Cfdp::Status::T status = component.m_engine->sendAck(txn, testTransactionStatus, testDirectiveCode, + testConditionCode, testPeerId, testSequenceId); + ASSERT_EQ(status, Cfdp::Status::SUCCESS) << "sendAck failed"; + + // Verify PDU was sent through dataOut port + ASSERT_FROM_PORT_HISTORY_SIZE(1); + + // Get encoded PDU buffer + const Fw::Buffer& pduBuffer = getSentPduBuffer(0); + ASSERT_GT(pduBuffer.getSize(), 0) << "PDU size is zero"; + + // Verify ACK PDU + // ACK PDU is sent from receiver (component.getLocalEidParam()) to sender (testPeerId) + // acknowledging the EOF directive + const U8 expectedSubtypeCode = 1; + verifyAckPdu(pduBuffer, component.getLocalEidParam(), testPeerId, + testSequenceId, + static_cast(testDirectiveCode), + expectedSubtypeCode, + static_cast(testConditionCode), + static_cast(testTransactionStatus)); +} + +void CfdpManagerTester::testNakPdu() { + // Test pattern: + // 1. Setup transaction + // 2. Construct NAK PDU with scope_start and scope_end + // 3. Invoke Engine->sendNak() + // 4. Capture PDU from dataOut and validate + + // Configure transaction for NAK PDU emission + const char* srcFile = "/tmp/test_nak.bin"; + const char* dstFile = "/tmp/dest_nak.bin"; + const FileSize fileSize = 4096; + const U8 channelId = 0; + const U32 testSequenceId = 99; + const U32 testPeerId = 200; + + Transaction* txn = setupTestTransaction( + TXN_STATE_R2, // Receiver, class 2 (acknowledged mode) + channelId, + srcFile, + dstFile, + fileSize, + testSequenceId, + testPeerId + ); + ASSERT_NE(txn, nullptr) << "Failed to create test transaction"; + + // Clear port history before test + this->clearHistory(); + + // Create and initialize NAK PDU + Cfdp::NakPdu nakPdu; + Cfdp::PduDirection direction = Cfdp::DIRECTION_TOWARD_SENDER; + const FileSize testScopeStart = 0; // Scope covers entire file + const FileSize testScopeEnd = fileSize; // Scope covers entire file + + // NAK PDU is sent from receiver (component.getLocalEidParam()) to sender (testPeerId) + // requesting retransmission of missing data + + nakPdu.initialize( + direction, + Cfdp::Class::CLASS_2, // Class 2 (acknowledged mode) + component.getLocalEidParam(), // source EID (receiver/local) + testSequenceId, // transaction sequence number + testPeerId, // destination EID (sender/peer) + testScopeStart, // scope start + testScopeEnd // scope end + ); + + // Add segment requests indicating specific missing data ranges + // Simulates receiver requesting retransmission of 3 gaps + + // Gap 1: Missing data from 512-1024 + nakPdu.addSegment(512, 1024); + + // Gap 2: Missing data from 2048-2560 + nakPdu.addSegment(2048, 2560); + + // Gap 3: Missing data from 3584-4096 + nakPdu.addSegment(3584, 4096); + + // Invoke sendNak using refactored API + Cfdp::Status::T status = component.m_engine->sendNak(txn, nakPdu); + ASSERT_EQ(status, Cfdp::Status::SUCCESS) << "sendNak failed"; + + // Verify PDU was sent through dataOut port + ASSERT_FROM_PORT_HISTORY_SIZE(1); + + // Get encoded PDU buffer + const Fw::Buffer& pduBuffer = getSentPduBuffer(0); + ASSERT_GT(pduBuffer.getSize(), 0) << "PDU size is zero"; + + // Verify NAK PDU + + // Define expected segment requests + Cfdp::SegmentRequest expectedSegments[3]; + expectedSegments[0].offsetStart = 512; + expectedSegments[0].offsetEnd = 1024; + expectedSegments[1].offsetStart = 2048; + expectedSegments[1].offsetEnd = 2560; + expectedSegments[2].offsetStart = 3584; + expectedSegments[2].offsetEnd = 4096; + + // Verify all fields including segments + verifyNakPdu(pduBuffer, component.getLocalEidParam(), testPeerId, + testSequenceId, testScopeStart, testScopeEnd, + 3, expectedSegments); +} + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/CfdpManager/test/ut/output/.gitignore b/Svc/Ccsds/CfdpManager/test/ut/output/.gitignore new file mode 100644 index 00000000000..7f25d4e744b --- /dev/null +++ b/Svc/Ccsds/CfdpManager/test/ut/output/.gitignore @@ -0,0 +1,2 @@ +# Ignore all files in the output folder +* \ No newline at end of file diff --git a/Svc/Subtopologies/CMakeLists.txt b/Svc/Subtopologies/CMakeLists.txt index e821318ab18..6b8ec2cbb73 100644 --- a/Svc/Subtopologies/CMakeLists.txt +++ b/Svc/Subtopologies/CMakeLists.txt @@ -2,6 +2,7 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/CdhCore/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ComCcsds/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ComFprime/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/FileHandling/") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/FileHandlingCfdp/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/DataProducts/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ComLoggerTee/") @@ -16,6 +17,8 @@ add_custom_target( Svc_Subtopologies_ComFprime_ComFprimeConfig Svc_Subtopologies_FileHandling Svc_Subtopologies_FileHandling_FileHandlingConfig + Svc_Subtopologies_FileHandlingCfdp + Svc_Subtopologies_FileHandlingCfdp_FileHandlingCfdpConfig Svc_Subtopologies_DataProducts Svc_Subtopologies_DataProducts_DataProductsConfig Svc_Subtopologies_ComLoggerTee diff --git a/Svc/Subtopologies/FileHandlingCfdp/CMakeLists.txt b/Svc/Subtopologies/FileHandlingCfdp/CMakeLists.txt new file mode 100644 index 00000000000..b53a7ebcf2e --- /dev/null +++ b/Svc/Subtopologies/FileHandlingCfdp/CMakeLists.txt @@ -0,0 +1,13 @@ +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/FileHandlingCfdpConfig/") + +register_fprime_module( + EXCLUDE_FROM_ALL + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/FileHandlingCfdp.fpp" + HEADERS + "${CMAKE_CURRENT_LIST_DIR}/SubtopologyTopologyDefs.hpp" + "${CMAKE_CURRENT_LIST_DIR}/PingEntries.hpp" + INTERFACE + DEPENDS + Svc_Subtopologies_FileHandlingCfdp_FileHandlingCfdpConfig +) diff --git a/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdp.fpp b/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdp.fpp new file mode 100644 index 00000000000..e126a00108d --- /dev/null +++ b/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdp.fpp @@ -0,0 +1,41 @@ +module FileHandlingCfdp { + + # ---------------------------------------------------------------------- + # Active Components + # ---------------------------------------------------------------------- + instance cfdpManager: Svc.Ccsds.CfdpManager base id FileHandlingCfdpConfig.BASE_ID + 0x00000 \ + queue size FileHandlingCfdpConfig.QueueSizes.cfdpManager \ + stack size FileHandlingCfdpConfig.StackSizes.cfdpManager \ + priority FileHandlingCfdpConfig.Priorities.cfdpManager \ + { + phase Fpp.ToCpp.Phases.configComponents """ + FileHandlingCfdp::cfdpManager.configure(); + """ + } + + instance fileManager: Svc.FileManager base id FileHandlingCfdpConfig.BASE_ID + 0x01000 \ + queue size FileHandlingCfdpConfig.QueueSizes.fileManager \ + stack size FileHandlingCfdpConfig.StackSizes.fileManager \ + priority FileHandlingCfdpConfig.Priorities.fileManager + + instance prmDb: Svc.PrmDb base id FileHandlingCfdpConfig.BASE_ID + 0x02000 \ + queue size FileHandlingCfdpConfig.QueueSizes.prmDb \ + stack size FileHandlingCfdpConfig.StackSizes.prmDb \ + priority FileHandlingCfdpConfig.Priorities.prmDb \ + { + phase Fpp.ToCpp.Phases.configComponents """ + FileHandlingCfdp::prmDb.configure("PrmDb.dat"); + """ + phase Fpp.ToCpp.Phases.readParameters """ + FileHandlingCfdp::prmDb.readParamFile(); + """ + } + + topology Subtopology { + # Active Components + instance cfdpManager + instance fileManager + instance prmDb + + } # end topology +} # end FileHandlingCfdp Subtopology diff --git a/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/CMakeLists.txt b/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/CMakeLists.txt new file mode 100644 index 00000000000..1af98b79163 --- /dev/null +++ b/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/CMakeLists.txt @@ -0,0 +1,6 @@ +register_fprime_module( + EXCLUDE_FROM_ALL + INTERFACE + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/FileHandlingCfdpConfig.fpp" +) diff --git a/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/FileHandlingCfdpConfig.fpp b/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/FileHandlingCfdpConfig.fpp new file mode 100644 index 00000000000..e26ea3a4dc8 --- /dev/null +++ b/Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/FileHandlingCfdpConfig.fpp @@ -0,0 +1,22 @@ +module FileHandlingCfdpConfig { + # Base ID for the FileHandlingCfdp Subtopology, all components are offsets from this base ID + constant BASE_ID = 0x06000000 + + module QueueSizes { + constant cfdpManager = 30 + constant fileManager = 10 + constant prmDb = 10 + } + + module StackSizes { + constant cfdpManager = 128 * 1024 + constant fileManager = 64 * 1024 + constant prmDb = 64 * 1024 + } + + module Priorities { + constant cfdpManager = 24 + constant fileManager = 22 + constant prmDb = 21 + } +} diff --git a/Svc/Subtopologies/FileHandlingCfdp/PingEntries.hpp b/Svc/Subtopologies/FileHandlingCfdp/PingEntries.hpp new file mode 100644 index 00000000000..147c8e1238e --- /dev/null +++ b/Svc/Subtopologies/FileHandlingCfdp/PingEntries.hpp @@ -0,0 +1,14 @@ +#ifndef FILEHANDLINGCFDP_PINGENTRIES_HPP +#define FILEHANDLINGCFDP_PINGENTRIES_HPP + +namespace FileHandling_cfdpManager { + enum { WARN = 3, FATAL = 5 }; +} +namespace FileHandling_fileManager { + enum { WARN = 3, FATAL = 5 }; +} +namespace FileHandling_prmDb { + enum { WARN = 3, FATAL = 5 }; +} + +#endif diff --git a/Svc/Subtopologies/FileHandlingCfdp/SubtopologyTopologyDefs.hpp b/Svc/Subtopologies/FileHandlingCfdp/SubtopologyTopologyDefs.hpp new file mode 100644 index 00000000000..f49afb74319 --- /dev/null +++ b/Svc/Subtopologies/FileHandlingCfdp/SubtopologyTopologyDefs.hpp @@ -0,0 +1,16 @@ +#ifndef FILEHANDLINGCFDPSUBTOPOLOGY_DEFS_HPP +#define FILEHANDLINGCFDPSUBTOPOLOGY_DEFS_HPP + +#include "Svc/Subtopologies/FileHandlingCfdp/FileHandlingCfdpConfig/FppConstantsAc.hpp" + +namespace FileHandlingCfdp { +struct SubtopologyState { + // Empty - no external state needed for FileHandlingCfdp subtopology +}; + +struct TopologyState { + SubtopologyState fileHandlingCfdp; +}; +} // namespace FileHandlingCfdp + +#endif diff --git a/default/config/CMakeLists.txt b/default/config/CMakeLists.txt index 5e012aa8363..95483688e52 100644 --- a/default/config/CMakeLists.txt +++ b/default/config/CMakeLists.txt @@ -6,6 +6,7 @@ register_fprime_config( AUTOCODER_INPUTS "${CMAKE_CURRENT_LIST_DIR}/AcConstants.fpp" + "${CMAKE_CURRENT_LIST_DIR}/CfdpCfg.fpp" "${CMAKE_CURRENT_LIST_DIR}/ComCfg.fpp" "${CMAKE_CURRENT_LIST_DIR}/DpCfg.fpp" "${CMAKE_CURRENT_LIST_DIR}/FileDispatcherCfg.fpp" @@ -25,6 +26,7 @@ register_fprime_config( "${CMAKE_CURRENT_LIST_DIR}/ActiveRateGroupCfg.hpp" "${CMAKE_CURRENT_LIST_DIR}/ActiveTextLoggerCfg.hpp" "${CMAKE_CURRENT_LIST_DIR}/BufferManagerComponentImplCfg.hpp" + "${CMAKE_CURRENT_LIST_DIR}/CfdpCfg.hpp" "${CMAKE_CURRENT_LIST_DIR}/CommandDispatcherImplCfg.hpp" "${CMAKE_CURRENT_LIST_DIR}/DpCatalogCfg.hpp" "${CMAKE_CURRENT_LIST_DIR}/DpCfg.hpp" diff --git a/default/config/CfdpCfg.fpp b/default/config/CfdpCfg.fpp new file mode 100644 index 00000000000..f7f49f7a89b --- /dev/null +++ b/default/config/CfdpCfg.fpp @@ -0,0 +1,68 @@ +# ====================================================================== +# CfdpCfg.fpp +# F Prime CFDP configuration constants +# ====================================================================== + +module Svc { + module Ccsds { + module Cfdp { + @ Number of CFDP channels + constant NumChannels = 2 + + @ File path size used for CFDP file system operations + constant MaxFilePathSize = 200 + + @ @brief Entity id size + @ + @ @par Description: + @ The maximum size of the entity id as expected for all CFDP packets. + @ CF supports the spec's variable size of EID, where the actual size is + @ selected at runtime, and therefore the size in CFDP PDUs may be smaller + @ than the size specified here. This type only establishes the maximum + @ size (and therefore maximum value) that an EID may be. + @ + @ @note This type is used in several commands, and so changing the size + @ of this type will affect various command structures. + @ + @ @par Limits + @ Must be one of U8, U16, U32, U64. + type EntityId = U32 + + @ @brief transaction sequence number size + @ + @ @par Description: + @ The max size of the transaction sequence number as expected for all CFDP packets. + @ CF supports the spec's variable size of TSN, where the actual size is + @ selected at runtime, and therefore the size in CFDP PDUs may be smaller + @ than the size specified here. This type only establishes the maximum + @ size (and therefore maximum value) that a TSN may be. + @ + @ @note This type is used in several commands, and so changing the size + @ of this type will affect various command structures. + @ + @ @par Limits + @ Must be one of U8, U16, U32, U64. + type TransactionSeq = U32 + + @ @brief File size and offset type + @ + @ @par Description: + @ The type used for file sizes and offsets in CFDP operations. + @ The CFDP protocol permits use of 64-bit values for file size/offsets, + @ although the current implementation supports 32-bit values. + @ + @ @par Limits + @ Must be one of U8, U16, U32, U64. + type FileSize = U32 + + @ @brief Maximum PDU size in bytes + @ + @ @par Description: + @ Limits the maximum possible Tx PDU size. + @ + @ @par Limits: + @ Must respect any CCSDS packet size limits on the system. + constant MaxPduSize = 1024 + } + } +} diff --git a/default/config/CfdpCfg.hpp b/default/config/CfdpCfg.hpp new file mode 100644 index 00000000000..7a08f841ff5 --- /dev/null +++ b/default/config/CfdpCfg.hpp @@ -0,0 +1,216 @@ +// ====================================================================== +// \title CfdpCfg.hpp +// \author Brian Campuzano +// \brief F Prime CFDP configuration constants +// ====================================================================== + +namespace Svc { +namespace Ccsds { +namespace Cfdp { + +// ================================================================== +// Protocol Configuration +// ================================================================== + +/** + * @brief Max NAK segments supported in a NAK PDU + * + * @par Description: + * When a NAK PDU is sent or received, this is the max number of + * segment requests supported. This number should match the ground + * CFDP engine configuration as well. + * + * @par Limits: + * + */ +#define CFDP_NAK_MAX_SEGMENTS (58) + +/** + * @brief Maximum TLVs (Type-Length-Value) per PDU + * + * @par Description: + * Maximum number of TLV (Type-Length-Value) tuples that can be + * included in a single CFDP PDU. TLVs are optional metadata fields + * used in EOF and FIN PDUs to convey diagnostic information. + * + * Per CCSDS 727.0-B-5 section 5.4, TLVs are variable-length fields + * that encode information such as entity IDs, fault handler overrides, + * or messages to the user. The most common use is the Entity ID TLV + * (type 6), automatically added to EOF and FIN PDUs on error conditions + * to aid in debugging. + * + * This value sets an upper bound on TLV storage per PDU to prevent + * unbounded memory growth. The limit of 4 is sufficient for typical + * CFDP operations: + * - 1 for Entity ID TLV + * - 3 additional for filestore requests/responses or messages + * + * @par Limits: + * Must be > 0. + * Larger values consume more memory per PDU but allow more metadata. + * + * @reference + * CCSDS 727.0-B-5, section 5.4, table 5-3 + */ +#define CFDP_MAX_TLV (4) + +/** + * @brief R2 CRC calc chunk size + * + * @par Description + * R2 performs CRC calculation upon file completion in chunks. This is the size + * of the buffer. The larger the size the more stack will be used, but + * the faster it can go. The overall number of bytes calculated per wakeup + * is set in the configuration table. + * + * @par Limits: + * + */ +#define CFDP_R2_CRC_CHUNK_SIZE (1024) + +/** + * @brief RX chunks per transaction (per channel) + * + * @par Description: + * Number of chunks per transaction per channel (RX). + * + * RX CHUNKS - + * For Class 2 CFDP receive transactions, the receiver must track which file segments + * have been successfully received. A chunk represents a contiguous range (offset, size) + * of received file data. By tracking received chunks, the receiver can identify gaps + * in the file data and generate NAK PDUs to request retransmission of missing segments. + * + * (array size must be NumChannels) + * CFDP_CHANNEL_NUM_RX_CHUNKS_PER_TRANSACTION is an array for each channel indicating + * the number of chunks per transaction to track received file segments. This enables + * gap detection and NAK generation for reliable Class 2 transfers. + * + * @par Limits: + * + */ +#define CFDP_CHANNEL_NUM_RX_CHUNKS_PER_TRANSACTION \ + { \ + CFDP_NAK_MAX_SEGMENTS, CFDP_NAK_MAX_SEGMENTS \ + } + +/** + * @brief TX chunks per transaction (per channel) + * + * @par Description: + * Number of chunks per transaction per channel (TX). + * + * TX CHUNKS - + * For Class 2 CFDP transmit transactions, the sender must track which file segments + * the receiver has requested via NAK PDUs. Each chunk represents a gap (offset, size) + * that needs to be retransmitted. + * + * (array size must be NumChannels) + * CFDP_CHANNEL_NUM_TX_CHUNKS_PER_TRANSACTION is an array for each channel indicating + * the number of chunks to track NAK segment requests from the receiver per transaction. + * This allows the sender to queue and retransmit the requested missing file data. + * + * @par Limits: + * + */ +#define CFDP_CHANNEL_NUM_TX_CHUNKS_PER_TRANSACTION \ + { \ + CFDP_NAK_MAX_SEGMENTS, CFDP_NAK_MAX_SEGMENTS \ + } + +// ================================================================== +// Resource Pool Configuration +// ================================================================== + +/** + * @brief Max number of simultaneous file receives. + * + * @par Description: + * Each channel can support this number of active/concurrent file receive + * transactions. This contributes to the total transaction pool size and + * limits how many incoming files can be received simultaneously. + * + * @par Limits: + * + */ +#define CFDP_MAX_SIMULTANEOUS_RX (5) + +/** + * @brief Number of max commanded playback files per chan. + * + * @par Description: + * This is the max number of outstanding ground commanded file transmits per channel. + * + * @par Limits: + * + */ +#define CFDP_MAX_COMMANDED_PLAYBACK_FILES_PER_CHAN (10) + +/** + * @brief Max number of commanded playback directories per channel. + * + * @par Description: + * Each channel can support this number of ground commanded directory playbacks. + * + * @par Limits: + * + */ +#define CFDP_MAX_COMMANDED_PLAYBACK_DIRECTORIES_PER_CHAN (2) + +/** + * @brief Max number of polling directories per channel. + * + * @par Description: + * This affects the configuration table. There must be an entry (can + * be empty) for each of these polling directories per channel. + * + * @par Limits: + * + */ +#define CFDP_MAX_POLLING_DIR_PER_CHAN (5) + +/** + * @brief Number of transactions per playback directory. + * + * @par Description: + * Each playback/polling directory operation will be able to have this + * many active transfers at a time pending or active. + * + * @par Limits: + * + */ +#define CFDP_NUM_TRANSACTIONS_PER_PLAYBACK (5) + +/** + * @brief Number of histories per channel + * + * @par Description: + * Each channel maintains a circular buffer of completed transaction records + * (history entries) for debugging and reference. This defines the maximum + * number of completed transactions to keep in the history buffer. + * + * @par Limits: + * 65536 is the current max. + */ +#define CFDP_NUM_HISTORIES_PER_CHANNEL (256) + +// ================================================================== +// Miscellaneous +// ================================================================== + +/** + * @brief Macro type for Entity id that is used in printf style formatting + * + * @note This must match the size of CfdpEntityId as defined in CfdpCfg.fpp + */ +#define CFDP_PRI_ENTITY_ID PRIu32 + +/** + * @brief Macro type for transaction sequences that is used in printf style formatting + * + * @note This must match the size of CfdpTransactionSeq as defined in CfdpCfg.fpp + */ +#define CFDP_PRI_TRANSACTION_SEQ PRIu32 + +} // namespace Cfdp +} // namespace Ccsds +} // namespace Svc \ No newline at end of file