diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e192dd09113..f185f4e96ac 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -14,6 +14,7 @@ alphanums ampcs ANamespace Aos +aosmpdu apid APIDOCS APPENDFILE @@ -217,6 +218,7 @@ endmacro endraw enduml EPP +epp ERRORCHECK errornum ert @@ -242,6 +244,7 @@ ffff Ffs fge fgt +fhp FILEDISPATCHER FILEDISPATCHERCFG FILEDOWNLINK @@ -407,6 +410,7 @@ llu LOCALSTATEDIR LOGGERRULES LOGPACKET +lol Lsb lseek LTK @@ -439,6 +443,7 @@ MMAPALLOCATOR MML modbus MOVEFILE +mpdu Mpu Msb msc @@ -763,6 +768,7 @@ VCA vcid VCP vcs +VCx VFILE VID vla diff --git a/Svc/Ccsds/AosDeframer/AosDeframer.cpp b/Svc/Ccsds/AosDeframer/AosDeframer.cpp new file mode 100644 index 00000000000..0206bd956c4 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/AosDeframer.cpp @@ -0,0 +1,530 @@ +// ====================================================================== +// \title AosDeframer.cpp +// \author Will MacCormack +// \brief cpp file for AosDeframer component implementation class +// +// Deframer for the AOS Space Data Link Protocol per CCSDS 732.0-B-5. +// Supports M_PDU data field service with: +// - Frame Error Control Field (FECF) validation (Section 4.1.6) +// - Space Packet Protocol (SPP) extraction (CCSDS 133.0-B-2) +// - Encapsulation Packet Protocol (EPP) extraction (CCSDS 133.1-B-3) +// ====================================================================== + +#include "Svc/Ccsds/AosDeframer/AosDeframer.hpp" +#include "Svc/Ccsds/Types/EppLengthOfLengthEnumAc.hpp" +#include "Svc/Ccsds/Types/EppProtocolIdEnumAc.hpp" +#include "Svc/Ccsds/Types/SpacePacketHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Utils/CRC16.hpp" +#include "config/FppConstantsAc.hpp" + +namespace Svc { +namespace Ccsds { + +// ---------------------------------------------------------------------- +// Component construction and destruction +// ---------------------------------------------------------------------- + +AosDeframer::AosDeframer(const char* const compName) + : AosDeframerComponentBase(compName), + m_fixedFrameSize(ComCfg::AosMaxFrameFixedSize), + m_fecfEnabled(true), + m_spacecraftId(ComCfg::SpacecraftId), + m_crcErrorCount(0) { + // Initialize VC struct + for (U8 vcInd = 0; vcInd < AosDeframer_NumVcs; vcInd++) { + m_vcs[vcInd].vcStructIndex = vcInd; + } +} + +AosDeframer::~AosDeframer() {} + +void AosDeframer::configure(U32 fixedFrameSize, bool frameErrorControlField, U16 spacecraftId, U8 vcId, U8 pvnMask) { + // Validate frame size is within bounds + FW_ASSERT(fixedFrameSize <= ComCfg::AosMaxFrameFixedSize, static_cast(fixedFrameSize), + static_cast(ComCfg::AosMaxFrameFixedSize)); + + // Frame must be large enough for header + M_PDU header + optional trailer + const FwSizeType minSize = AOSHeader::SERIALIZED_SIZE + M_PDUHeader::SERIALIZED_SIZE + + (frameErrorControlField ? AOSTrailer::SERIALIZED_SIZE : 0); + FW_ASSERT(fixedFrameSize > minSize, static_cast(fixedFrameSize), + static_cast(minSize)); + + // Spacecraft ID is 10 bits (per CCSDS 732.0-B-5 Section 4.1.2.2) + FW_ASSERT((spacecraftId & 0xFC00) == 0, static_cast(spacecraftId)); + + // Virtual Channel ID is 6 bits (per CCSDS 732.0-B-5 Section 4.1.2.3) + FW_ASSERT((vcId & 0xC0) == 0, static_cast(vcId)); + + // pvnMask must only contain valid PVN bits and at least one must be set + FW_ASSERT((pvnMask & PvnBitfield::VALID_MASK) != 0, static_cast(pvnMask)); + FW_ASSERT((pvnMask & ~PvnBitfield::VALID_MASK) == 0, static_cast(pvnMask)); + + // Spanning packet reassembly requires dynamic backing via allocator ports + FW_ASSERT(this->isConnected_allocate_OutputPort(0)); + FW_ASSERT(this->isConnected_deallocate_OutputPort(0)); + + m_fixedFrameSize = fixedFrameSize; + m_fecfEnabled = frameErrorControlField; + m_spacecraftId = spacecraftId; + + // Zero out FECF error counter on (re)configure + m_crcErrorCount = 0; + + // Populate the (single) VC struct + m_vcs[0].virtualChannelId = vcId; + m_vcs[0].pvnMask = pvnMask; + + // Clear out all VC stats + for (U8 vcInd = 0; vcInd < AosDeframer_NumVcs; vcInd++) { + m_vcs[vcInd].framesProcessed = 0; + m_vcs[vcInd].packetsExtracted = 0; + m_vcs[vcInd].vcFrameCount = 0; + + // Clear out the spanningPacket + this->abandonSpanningPacket(m_vcs[vcInd]); + } +} + +// ---------------------------------------------------------------------- +// Handler implementations for user-defined typed input ports +// ---------------------------------------------------------------------- + +void AosDeframer::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, const ComCfg::FrameContext& context) { + // Per CCSDS 732.0-B-5, AOS frames are fixed-size + // Verify we have received a complete frame + FW_ASSERT(m_fixedFrameSize > 0, static_cast(m_fixedFrameSize)); + + if (data.getSize() < m_fixedFrameSize) { + this->log_WARNING_HI_InvalidFrameLength(data.getSize(), m_fixedFrameSize); + this->notifyErrorIfConnected(Ccsds::FrameError::AOS_INVALID_LENGTH); + this->dataReturnOut_out(0, data, context); + return; + } + + // Validate FECF if enabled (Section 4.1.6) + // FrameDetector + FrameAccumulator or Lower Protocol Layer should enforce whole AOS Frames + if (m_fecfEnabled && !this->validateFecf(data)) { + this->dataReturnOut_out(0, data, context); + return; + } + + // Create a mutable context for extracted packet info + ComCfg::FrameContext packetContext = context; + // Start null, and is set by the parse step + AosDeframerVc* vc; + + // Parse and validate the AOS Primary Header (Section 4.1.2) + // Note: parseAndValidateHeader handles warning events and errorNotify for header failures. + if ((vc = this->parseAndValidateHeader(data, packetContext)) == nullptr) { + this->dataReturnOut_out(0, data, context); + return; + } + + // Set the default context only if we haven't for this packet already + // Otherwise our PVN tracker gets overwritten + if (!vc->spanningPacket.buffer.isValid()) { + vc->spanningPacket.context = packetContext; + } + + // Update telemetry + this->tlmWrite_FramesProcessed(++vc->framesProcessed); + + // Extract packets from the M_PDU data zone + this->extractPackets(*vc, data); + + // Return the frame buffer + this->dataReturnOut_out(0, data, context); +} + +void AosDeframer::dataReturnIn_handler(FwIndexType portNum, Fw::Buffer& fwBuffer, const ComCfg::FrameContext& context) { + // Deallocate this dynamically allocated packet + this->deallocate_out(0, fwBuffer); +} + +// ---------------------------------------------------------------------- +// Private helper methods +// ---------------------------------------------------------------------- + +void AosDeframer::notifyErrorIfConnected(Ccsds::FrameError error) { + if (this->isConnected_errorNotify_OutputPort(0)) { + this->errorNotify_out(0, error); + } +} + +void AosDeframer::abandonSpanningPacket(AosDeframerVc& vc) { + if (vc.spanningPacket.buffer.isValid()) { + this->log_WARNING_HI_SpanningPacketAbandoned(vc.virtualChannelId, vc.spanningPacket.context.get_pvn(), + vc.spanningPacket.bytesReceived, + vc.spanningPacket.buffer.getSize()); + this->deallocate_out(0, vc.spanningPacket.buffer); + } + vc.spanningPacket.buffer = Fw::Buffer(); + vc.spanningPacket.bytesReceived = 0; + vc.spanningPacket.context.set_pvn(ComCfg::Pvn::INVALID_UNINITIALIZED); +} + +AosDeframer::AosDeframerVc* AosDeframer::getVcStruct(const U8 vcId) { + for (U8 vcInd = 0; vcInd < AosDeframer_NumVcs; vcInd++) { + if (m_vcs[vcInd].virtualChannelId == vcId) { + return &m_vcs[vcInd]; + } + } + + return nullptr; +} + +AosDeframer::AosDeframerVc* AosDeframer::parseAndValidateHeader(Fw::Buffer& data, ComCfg::FrameContext& context) { + // Deserialize the AOS Primary Header (per CCSDS 732.0-B-5 Section 4.1.2) + AOSHeader header; + Fw::SerializeStatus status = data.getDeserializer().deserializeTo(header); + // We already checked that a header fits into fixedFrameSize & that this frame is >= fixedFrameSize + FW_ASSERT(status == Fw::FW_SERIALIZE_OK, static_cast(status)); + + // Extract Transfer Frame Version Number (Section 4.1.2.2.2) + // AOS uses Tfvn::AOS = 0x1 ('01' binary) + U8 tfvn = static_cast((header.get_globalVcId() & AOSHeaderSubfields::frameVersionMask) >> + AOSHeaderSubfields::frameVersionOffset); + if (tfvn != static_cast(Tfvn::AOS)) { + this->log_WARNING_HI_InvalidTfvn(tfvn, static_cast(Tfvn::AOS)); + this->notifyErrorIfConnected(Ccsds::FrameError::AOS_INVALID_VERSION); + return nullptr; + } + + // Extract Spacecraft ID (Section 4.1.2.2) + // SCID is split: 8 LS bits in globalVcId, 2 MS bits in signaling field + U16 spacecraftId = static_cast(((header.get_globalVcId() & AOSHeaderSubfields::spacecraftIdLsbMask) >> + AOSHeaderSubfields::spacecraftIdLsbOffset)); + spacecraftId |= static_cast((header.get_frameCountAndSignaling() & AOSHeaderSubfields::spacecraftIdMsbMask) + << (8 - AOSHeaderSubfields::spacecraftIdMsbOffset)); + + if (spacecraftId != m_spacecraftId) { + this->log_WARNING_LO_InvalidSpacecraftId(spacecraftId, m_spacecraftId); + this->notifyErrorIfConnected(Ccsds::FrameError::AOS_INVALID_SCID); + return nullptr; + } + + // Extract Virtual Channel ID (Section 4.1.2.3) + U8 vcId = static_cast(header.get_globalVcId() & AOSHeaderSubfields::virtualChannelIdMask); + AosDeframerVc* vc = this->getVcStruct(vcId); + + if (vc == nullptr) { + // TODO: Multi VC | Handle logging all valid vcIds + this->log_ACTIVITY_LO_InvalidVcId(vcId, m_vcs[0].virtualChannelId); + this->notifyErrorIfConnected(Ccsds::FrameError::AOS_INVALID_VCID); + return vc; + } + + // Extract Virtual Channel Frame Count (Section 4.1.2.4) + // 24 bits in the upper 3 bytes of frameCountAndSignaling + U32 vcFrameCount = (header.get_frameCountAndSignaling() & AOSHeaderSubfields::vcFrameCountMask) >> + AOSHeaderSubfields::vcFrameCountOffset; + + // Extract VC Frame Count Cycle if in use (Section 4.1.2.5.3) + if ((header.get_frameCountAndSignaling() & AOSHeaderSubfields::cycleCountFlagMask) != 0) { + const U8 vcFrameCountCycle = header.get_frameCountAndSignaling() & AOSHeaderSubfields::vcFrameCountCycleMask; + // Extend the 24-bit frame count with the 4-bit cycle count + vcFrameCount |= static_cast(vcFrameCountCycle) << 24; + } + + // Gap detect after the first accepted frame on a VC + if (vc->framesProcessed > 0U) { + const U32 expectedVcFrameCount = vc->vcFrameCount + 1U; + if (vcFrameCount != expectedVcFrameCount) { + this->log_WARNING_HI_VcFrameCountGap(vcId, vcFrameCount, expectedVcFrameCount); + this->notifyErrorIfConnected(Ccsds::FrameError::AOS_VC_FRAME_COUNT_GAP); + // Other errors will implicitly drop their spanning packet once we finally lock back onto a valid frame + this->abandonSpanningPacket(*vc); + } + } + + // Store VC frame count in the VC struct for reference (e.g., gap detection) + this->tlmWrite_LatestVcFrameCount(vc->vcFrameCount = vcFrameCount); + + // Update context with extracted values + context.set_vcId(vcId); + + return vc; +} + +bool AosDeframer::validateFecf(Fw::Buffer& data) { + // Per CCSDS 732.0-B-5 Section 4.1.6, FECF is a 16-bit CRC + // computed over all preceding bits in the frame + + const FwSizeType crcDataLen = m_fixedFrameSize - AOSTrailer::SERIALIZED_SIZE; + U16 computedCrc = Ccsds::Utils::CRC16::compute(data.getData(), static_cast(crcDataLen)); + + // Deserialize the trailer + AOSTrailer trailer; + auto deserializer = data.getDeserializer(); + deserializer.moveDeserToOffset(crcDataLen); + Fw::SerializeStatus status = deserializer.deserializeTo(trailer); + FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status); + + U16 transmittedCrc = trailer.get_fecf(); + if (transmittedCrc != computedCrc) { + this->log_WARNING_HI_InvalidFecf(transmittedCrc, computedCrc); + this->notifyErrorIfConnected(Ccsds::FrameError::AOS_INVALID_CRC); + this->tlmWrite_CrcErrorCount(++m_crcErrorCount); + return false; + } + + return true; +} + +FwSizeType AosDeframer::appendToSpanningPacket(AosDeframerVc& vc, U8* data, FwSizeType size) { + FW_ASSERT(size > 0, static_cast(size)); + + // Seek amount + FwSizeType seekForward = 0; + + if (!vc.spanningPacket.buffer.isValid()) { + // Fill the tmp header buff w/ what we got + const FwSizeType headerCap = AosDeframerVc::SpanningPacketState::HEADER_BUF_SIZE; + const FwSizeType toHeader = FW_MIN(size, headerCap - vc.spanningPacket.bytesReceived); + if (toHeader > 0) { + ::memcpy(vc.spanningPacket.headerBuf + vc.spanningPacket.bytesReceived, data, toHeader); + vc.spanningPacket.bytesReceived += toHeader; + + // We'll work w/ everything past the copied header if we get a clean parse + data += toHeader; + size -= toHeader; + seekForward += toHeader; + } + + // Attempt to find a size w/ what we have (zero means this frame is over) + const FwSizeType packetSize = sizePacket(vc, vc.spanningPacket.headerBuf, vc.spanningPacket.bytesReceived); + if (packetSize == 0) { + return 0; + } + + // Try to allocate a buffer for the whole packet + vc.spanningPacket.buffer = this->allocate_out(0, packetSize); + if (vc.spanningPacket.buffer.getSize() < packetSize) { + this->log_WARNING_HI_SpanningPacketAllocFailed(vc.virtualChannelId, vc.spanningPacket.context.get_pvn(), + packetSize); + // Save before abandon clears it — needed for the correct seek offset below + const FwSizeType remainingBody = packetSize - vc.spanningPacket.bytesReceived; + this->abandonSpanningPacket(vc); + + // Seek past the failed packet (header bytes already consumed + remaining body) + const FwSizeType remainingLength = seekForward + remainingBody; + if (remainingLength > size) { + return 0; + } else { + return remainingLength; + } + } + + // Load the header into the dynamic buffer + ::memcpy(vc.spanningPacket.buffer.getData(), vc.spanningPacket.headerBuf, vc.spanningPacket.bytesReceived); + } + + // Already have the dynamic buffer, so fill away + const FwSizeType spaceLeft = vc.spanningPacket.buffer.getSize() - vc.spanningPacket.bytesReceived; + // Copy what we got + const FwSizeType toBody = FW_MIN(size, spaceLeft); + if (toBody > 0) { + ::memcpy(vc.spanningPacket.buffer.getData() + vc.spanningPacket.bytesReceived, data, toBody); + vc.spanningPacket.bytesReceived += toBody; + seekForward += toBody; + } + + // Check if the spanning packet is now complete + if (vc.spanningPacket.buffer.getSize() > 0 && + vc.spanningPacket.bytesReceived >= vc.spanningPacket.buffer.getSize()) { + this->dataOut_out(0, vc.spanningPacket.buffer, vc.spanningPacket.context); + this->tlmWrite_PacketsExtracted(++vc.packetsExtracted); + + // Ownership of the buffer has transferred downstream; clear local handle before consolidating state reset. + vc.spanningPacket.buffer = Fw::Buffer(); + // Buffer won't be returned now since we cleared the handle + this->abandonSpanningPacket(vc); + } + + return seekForward; +} + +void AosDeframer::extractPackets(AosDeframerVc& vc, Fw::Buffer& data) { + // Parse M_PDU header (per CCSDS 732.0-B-5 Section 4.1.4.2.2) + M_PDUHeader mpduHeader; + auto deserializer = data.getDeserializer(); + deserializer.moveDeserToOffset(AOSHeader::SERIALIZED_SIZE); + Fw::SerializeStatus status = deserializer.deserializeTo(mpduHeader); + FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status); + + U16 firstHeaderPointer = mpduHeader.get_firstHeaderPointer(); + + // Calculate data zone boundaries + const FwSizeType dataZoneStart = AOSHeader::SERIALIZED_SIZE + M_PDUHeader::SERIALIZED_SIZE; + const FwSizeType dataZoneEnd = m_fixedFrameSize - (m_fecfEnabled ? AOSTrailer::SERIALIZED_SIZE : 0); + const FwSizeType dataZoneSize = dataZoneEnd - dataZoneStart; + U8* dataZone = data.getData() + dataZoneStart; + + // Handle special First Header Pointer values (Section 4.1.4.2.2.4) + if (firstHeaderPointer == M_PDUSubfields::FHP_IDLE_DATA_ONLY) { + // Frame contains only idle data - abandon any in-progress spanning packet + this->log_ACTIVITY_LO_IdleFrame(vc.virtualChannelId); + this->abandonSpanningPacket(vc); + return; + } + // Handle continuation data (data before First Header Pointer) + else if (firstHeaderPointer == M_PDUSubfields::FHP_NO_PACKET_START) { + // Entire data zone is continuation of previous packet + if (vc.spanningPacket.bytesReceived > 0) { + (void)this->appendToSpanningPacket(vc, dataZone, dataZoneSize); + } + // If no spanning packet active, this continuation data cannot be used + return; + } + + // There is continuation data before the first packet header + if (firstHeaderPointer > 0 && vc.spanningPacket.bytesReceived > 0) { + (void)this->appendToSpanningPacket(vc, dataZone, static_cast(firstHeaderPointer)); + // We must be done w/ the prior packet since we have a FHP + this->abandonSpanningPacket(vc); + } + + // Move to first packet header + FwSizeType currentOffset = firstHeaderPointer; + + // Max Bound is a sequence of 1 byte EPP Idle Packets + const FwIndexType maxIters = static_cast(dataZoneSize - firstHeaderPointer); + + // Extract packets starting at First Header Pointer + // (All fresh packets from here on out) + for (FwIndexType iter = 0; iter < maxIters && currentOffset < dataZoneSize; iter++) { + // Clear out any prior packet data + this->abandonSpanningPacket(vc); + + U8* packetStart = dataZone + currentOffset; + FwSizeType remainingBytes = dataZoneSize - currentOffset; + + FwSizeType packetSize = this->appendToSpanningPacket(vc, packetStart, remainingBytes); + + if (packetSize == 0) { + // Break out of loop since we ran out of data + return; + } + + currentOffset += packetSize; + } +} + +FwSizeType AosDeframer::sizePacket(AosDeframerVc& vc, U8* packetStart, FwSizeType remainingBytes) { + FW_ASSERT(remainingBytes > 0, static_cast(remainingBytes)); + + // Determine packet type from PVN (upper 3 bits of first byte) + U8 pvn = getPacketVersion(packetStart[0]); + // Default to invalid, override if valid (non-idle) packet + vc.spanningPacket.context.set_pvn(ComCfg::Pvn::INVALID_UNINITIALIZED); + + // Size the Packet (so we can alloc a buffer) + switch (ComCfg::Pvn pvnEnum = static_cast(pvn)) { + case ComCfg::Pvn::SPACE_PACKET_PROTOCOL: + // Intentionally fallthrough since logic is more condensed this way + case ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL: + if (vc.pvnMask & (1 << pvn)) { + vc.spanningPacket.context.set_pvn(pvnEnum); + if (pvnEnum == ComCfg::Pvn::SPACE_PACKET_PROTOCOL) { + return sizeSppPacket(packetStart, remainingBytes); + } else { + return sizeEppPacket(packetStart, remainingBytes); + } + } else { + this->log_WARNING_HI_DisabledPvn(vc.virtualChannelId, pvnEnum); + return 0; + } + break; + default: + this->log_WARNING_HI_InvalidPvn(vc.virtualChannelId, pvn); + return 0; + } +} + +FwSizeType AosDeframer::sizeSppPacket(U8* payloadStart, FwSizeType payloadSize) { + SpacePacketHeader header; + + Fw::Buffer data(payloadStart, payloadSize); + Fw::SerializeStatus status = data.getDeserializer().deserializeTo(header); + + if (status != Fw::FW_SERIALIZE_OK) { + return 0; // Incomplete header - spans to next frame + } + + // Per CCSDS 133.0-B-2 Section 4.1.3.5.2, packet data length = (actual length - 1) + FwSizeType totalPacketSize = SpacePacketHeader::SERIALIZED_SIZE + header.get_packetDataLength() + 1; + + // TODO: Unify Deframers | bring the whole spp processing into this component + // since we're only missing seq count logic? + + // Check for idle packet (APID = 0x7FF per CCSDS 133.0-B-2) + U16 apid = static_cast(header.get_packetIdentification() & SpacePacketSubfields::ApidMask); + + // Idle means this is the last packet in the frame + if (apid == static_cast(ComCfg::Apid::SPP_IDLE_PACKET)) { + return 0; + } + + return totalPacketSize; +} + +FwSizeType AosDeframer::sizeEppPacket(const U8* const payloadStart, FwSizeType payloadSize) { + // Per CCSDS 133.1-B-3 Section 4.1.2.1.1, EPP minimum header is 1 byte + // Since we identified this as an EPP we had the 1 byte to read the PVN already + FW_ASSERT(payloadSize > 0, static_cast(payloadSize)); + + // Parse first byte + U8 firstByte = payloadStart[0]; + U8 protocolId = static_cast((firstByte & EPPSubfields::protocolIdMask) >> EPPSubfields::protocolIdOffset); + + FwSizeType totalPacketSize = 0; + + // Idle means this is the last packet in the frame + if (protocolId == static_cast(EppProtocolId::Idle)) { + return 0; + } + + // Encapsulation Idle Packet per CCSDS 133.1-B-3 Section 4.1.3.2 + U8 lengthOfLength = firstByte & EPPSubfields::lengthOfLengthMask; + + U8 lengthOffset = 1U; + + // If length of length is 2 or more then there's an extra byte of extension/user defined (4.1.2.1.1) + if (lengthOfLength >= EppLengthOfLength::Two) { + lengthOffset += 1U; + } + + // If length of length is 4 then we add 2 bytes for the ccsds reserved field (4.1.2.1.1) + if (lengthOfLength == EppLengthOfLength::Four) { + lengthOffset += 2U; + // '0d3' on the wire, but means 4 + lengthOfLength = 4; + } + + // Bytes to get to length + length of length + const U8 headerLength = lengthOffset + lengthOfLength; + + // Validate and read length field + if (payloadSize < headerLength) { + return 0; // Incomplete + } + + // Read length field (big-endian) + U32 packetDataLength = 0; + for (U8 i = 0; i < lengthOfLength; i++) { + packetDataLength = (packetDataLength << 8) | payloadStart[lengthOffset + i]; + } + + totalPacketSize = headerLength + packetDataLength; + + return totalPacketSize; +} + +U8 AosDeframer::getPacketVersion(U8 firstByte) { + // PVN is the upper 3 bits per both CCSDS 133.0-B-2 and 133.1-B-3 + // EPP's Subfield array is done in bytes + return firstByte >> EPPSubfields::packetVersionOffset; +} + +} // namespace Ccsds +} // namespace Svc diff --git a/Svc/Ccsds/AosDeframer/AosDeframer.fpp b/Svc/Ccsds/AosDeframer/AosDeframer.fpp new file mode 100644 index 00000000000..11f75e10ad5 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/AosDeframer.fpp @@ -0,0 +1,51 @@ +module Svc { +module Ccsds { + @ Deframer for the AOS Space Data Link Protocol + @ Per CCSDS 732.0-B-5 (5th Edition) - AOS Space Data Link Protocol + @ Supports M_PDU (Multiplexing PDU) data field service with optional: + @ - Frame Error Control Field (FECF) per Section 4.1.6 + @ - Space Packet Protocol (SPP) extraction per CCSDS 133.0-B-2 + @ - Encapsulation Packet Protocol (EPP) extraction per CCSDS 133.1-B-3 + passive component AosDeframer { + + constant NumVcs = 1 + + # TODO: Multi VC | figure out storage and telemetry round up of per VC stat counters + type VCxU32 = U32 + type VCxU8 = U8 + + import Deframer + + @ Port to notify of a deframing error + output port errorNotify: Ccsds.ErrorNotify + + @ Buffer allocation and deallocation for packets that span across multiple AOS frames + import Svc.BufferAllocation + + include "AosDeframerEvents.fppi" + include "AosDeframerTelem.fppi" + + ############################################################################### + # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # + ############################################################################### + @ Port for requesting the current time + time get port timeCaller + + @ Port for sending textual representation of events + text event port logTextOut + + @ Port for sending events to downlink + event port logOut + + @ Port for sending telemetry channels to downlink + telemetry port tlmOut + + @ Port to return the value of a parameter + param get port prmGetOut + + @Port to set the value of a parameter + param set port prmSetOut + + } +} +} diff --git a/Svc/Ccsds/AosDeframer/AosDeframer.hpp b/Svc/Ccsds/AosDeframer/AosDeframer.hpp new file mode 100644 index 00000000000..5fceda2acc0 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/AosDeframer.hpp @@ -0,0 +1,193 @@ +// ====================================================================== +// \title AosDeframer.hpp +// \author Auto-generated +// \brief hpp file for AosDeframer component implementation class +// +// Deframer for the AOS Space Data Link Protocol per CCSDS 732.0-B-5. +// Supports M_PDU data field service with: +// - Frame Error Control Field (FECF) validation +// - Space Packet Protocol (SPP) extraction +// - Encapsulation Packet Protocol (EPP) extraction per CCSDS 133.1-B-3 +// ====================================================================== + +#ifndef Svc_Ccsds_AosDeframer_HPP +#define Svc_Ccsds_AosDeframer_HPP + +#include "Svc/Ccsds/AosDeframer/AosDeframerComponentAc.hpp" +#include "Svc/Ccsds/AosDeframer/FppConstantsAc.hpp" +#include "Svc/Ccsds/Types/AOSHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Types/AOSTrailerSerializableAc.hpp" +#include "Svc/Ccsds/Types/FppConstantsAc.hpp" +#include "Svc/Ccsds/Types/M_PDUHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Types/TfvnEnumAc.hpp" +#include "config/PvnEnumAc.hpp" + +namespace Svc { +namespace Ccsds { + +class AosDeframer : public AosDeframerComponentBase { + public: + // ---------------------------------------------------------------------- + // Component construction and destruction + // ---------------------------------------------------------------------- + + //! Construct AosDeframer object + AosDeframer(const char* const compName //!< The component name + ); + + //! Destroy AosDeframer object + ~AosDeframer(); + + //! \brief Configure the AosDeframer with mission-specific parameters + //! + //! Must be called before any frames are processed. Configures the deframer + //! for the expected AOS frame format per CCSDS 732.0-B-5. + //! + //! \param fixedFrameSize Fixed size of AOS frames in bytes (per Section 4.1.1) + //! \param frameErrorControlField Whether FECF is present (per Section 4.1.6) + //! \param spacecraftId The spacecraft ID to accept (10 bits, per Section 4.1.2.2) + //! \param vcId The virtual channel ID to accept (6 bits, per Section 4.1.2.3) + //! \param pvnMask Bitmask of Packet Version Numbers to extract (SPP=0x01, EPP=0x80) + //! + void configure(U32 fixedFrameSize, + bool frameErrorControlField, + U16 spacecraftId = ComCfg::SpacecraftId, + U8 vcId = 0, + U8 pvnMask = PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK); + + private: + // Forward declaration for helper method signatures that reference the nested VC state type + struct AosDeframerVc; + + // ---------------------------------------------------------------------- + // Handler implementations for user-defined typed input ports + // ---------------------------------------------------------------------- + + //! Handler implementation for dataIn + //! + //! Port to receive framed AOS data. This is essentially the CCSDS AOS + //! VC_RECEIVE.indication Service Primitive (per Section 3.4.3.2) + //! Note: parseAndValidateHeader handles warning events and errorNotify for + //! header failures; this function is responsible for data return. + void dataIn_handler(FwIndexType portNum, //!< The port number + Fw::Buffer& data, + const ComCfg::FrameContext& context) override; + + //! Handler implementation for dataReturnIn + //! + //! Port receiving back ownership of sent packet buffers + void dataReturnIn_handler(FwIndexType portNum, //!< The port number + Fw::Buffer& data, //!< The buffer + const ComCfg::FrameContext& context) override; + + // ---------------------------------------------------------------------- + // Private helper methods + // ---------------------------------------------------------------------- + + //! Parse the AOS Primary Header per CCSDS 732.0-B-5 Section 4.1.2 + //! \param data The frame buffer + //! \param context The frame context to update + //! \return pointer to the vc struct if header is valid, nullptr otherwise + AosDeframerVc* parseAndValidateHeader(Fw::Buffer& data, ComCfg::FrameContext& context); + + //! Validate the Frame Error Control Field (CRC) per CCSDS 732.0-B-5 Section 4.1.6 + //! Increments the global m_crcErrorCount on failure (FECF is a physical-channel concern). + //! \param data The frame buffer + //! \return true if FECF is valid, false otherwise + bool validateFecf(Fw::Buffer& data); + + //! Emit an errorNotify port message if the port is connected + void notifyErrorIfConnected(Ccsds::FrameError error); + + //! Abandon an in-progress spanning packet, deallocating backing storage if needed + void abandonSpanningPacket(AosDeframerVc& vc); + + //! Parse the M_PDU header and extract packets per CCSDS 732.0-B-5 Section 4.1.4.2 + //! \param vc The virtual channel state + //! \param data The frame buffer (positioned after AOS primary header) + void extractPackets(AosDeframerVc& vc, Fw::Buffer& data); + + //! Determine the validity and size, and idle status of a packet + //! \param vc The virtual channel state + //! \param packetStart Pointer to start of packet data within the incoming frame buffer + //! \param remainingBytes Available bytes in the data zone + //! \return Number of bytes the packet spans, or 0 if not yet known/idle + FwSizeType sizePacket(AosDeframerVc& vc, U8* packetStart, FwSizeType remainingBytes); + + //! Attempt to parse a Space Packet header from the M_PDU data zone per CCSDS 133.0-B-2 + //! \param payloadStart Pointer to start of packet data within the incoming frame buffer + //! \param payloadSize Available bytes in the data zone + //! \return Number of bytes the packet spans, or 0 not yet known + FwSizeType sizeSppPacket(U8* payloadStart, FwSizeType payloadSize); + + //! Attempt to parse an Encapsulation Packet header from the M_PDU data zone per CCSDS 133.1-B-3 + //! \param payloadStart Pointer to start of packet data within the incoming frame buffer + //! \param payloadSize Available bytes in the data zone + //! \return Number of bytes the packet spans, or 0 not yet known + FwSizeType sizeEppPacket(const U8* const payloadStart, FwSizeType payloadSize); + + //! Determine packet type from first byte (PVN field) + //! \param firstByte First byte of packet + //! \return Packet Version Number (0 for SPP, 7 for EPP) + static U8 getPacketVersion(U8 firstByte); + + //! Append data to the active spanning packet buffer, completing it if possible + //! \param vc The virtual channel state + //! \param data Pointer to data bytes to append + //! \param size Number of bytes to append + //! \return Number of bytes to seek forward, or zero if done w/ frame + FwSizeType appendToSpanningPacket(AosDeframerVc& vc, U8* data, FwSizeType size); + + //! Map frame context onto the appropriate virtual channel struct + //! \param vcId the virtual channel id to lookup + //! \return pointer to the vc struct if vcId is known, nullptr otherwise + //! TODO: Implement multi-VC support; currently always returns &m_vcs[0] or nullptr + AosDeframerVc* getVcStruct(const U8 vcId); + + //! Per-virtual-channel state, mirroring the AosFramer::AosVc pattern for future multi-VC support + struct AosDeframerVc { + U8 vcStructIndex = 0xFF; //!< Index into VC array for this vc struct + U8 virtualChannelId = 0; //!< VCID for this virtual channel + U8 pvnMask = PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK; //!< Bitmask of enabled PVNs + + // Telemetry counters (per-VC) + U32 framesProcessed = 0; //!< Total frames received on this VC + U32 packetsExtracted = 0; //!< Total packets extracted from this VC + U32 vcFrameCount = 0; //!< Last received virtual channel frame count from header + + // Spanning packet state (for packets that span multiple frames) + // Per CCSDS 732.0-B-5 Section 4.1.4.2.2.3 + struct SpanningPacketState { + static constexpr FwSizeType HEADER_BUF_SIZE = + 8; //!< Max header bytes needed to determine size (8 bytes is largest EPP Header) + U8 headerBuf[HEADER_BUF_SIZE]; //!< Header bytes accumulated before allocation + + Fw::Buffer buffer; //!< Dynamically-allocated packet buffer + + FwSizeType bytesReceived = 0; //!< Bytes received so far + // Context to be sent w/ the spanning packet + ComCfg::FrameContext context; + } spanningPacket; + }; + + private: + // ---------------------------------------------------------------------- + // Member variables + // ---------------------------------------------------------------------- + + // Frame-level configuration parameters (set via configure()) + U32 m_fixedFrameSize = 0; //!< Fixed frame size in bytes + bool m_fecfEnabled = true; //!< Whether FECF is enabled + U16 m_spacecraftId = 0; //!< Expected spacecraft ID (10 bits) + + //! FECF CRC error counter - per physical channel (not per-VC) + U32 m_crcErrorCount = 0; + + //! TODO: Multi VC | Implement multiple VCs - currently always returns &m_vcs[0] + AosDeframerVc m_vcs[AosDeframer_NumVcs]; //!< Our one AOS Virtual Channel (for now) +}; + +} // namespace Ccsds +} // namespace Svc + +#endif diff --git a/Svc/Ccsds/AosDeframer/AosDeframerEvents.fppi b/Svc/Ccsds/AosDeframer/AosDeframerEvents.fppi new file mode 100644 index 00000000000..ccdb2401877 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/AosDeframerEvents.fppi @@ -0,0 +1,54 @@ + @ Deframing received an invalid SCID (CCSDS 732.0-B-5 Section 4.1.2.2) + event InvalidSpacecraftId(transmitted: U16, configured: U16) \ + severity warning low \ + format "Invalid Spacecraft ID Received. Received: {} | Deframer configured with: {}" + + @ Deframing received an invalid frame length + event InvalidFrameLength(actual: FwSizeType, expected: U32) \ + severity warning high \ + format "Frame length mismatch. Received: {} | Expected: {} " + + @ Deframing received a VCID not in the accepted set (CCSDS 732.0-B-5 Section 4.1.2.3) + event InvalidVcId(transmitted: U8, configured: VCxU8) \ + severity activity low \ + format "Invalid Virtual Channel ID. Frame contained: {} | Accepted VCIDs: {}" + + @ Deframing received an invalid checksum (CCSDS 732.0-B-5 Section 4.1.6) + event InvalidFecf(transmitted: U16, computed: U16) \ + severity warning high \ + format "Invalid FECF (CRC) received. Trailer specified: {} | Computed on board: {}" + + @ Deframing received an invalid CCSDS Transfer Frame Version Number (CCSDS 732.0-B-5 Section 4.1.2.2.2) + event InvalidTfvn(transmitted: U8, expected: U8) \ + severity warning high \ + format "Invalid CCSDS Transfer Frame Version Number. Received: {} | Expected: {}" + + @ Deframing encountered an invalid/unsupported packet version number in the M_PDU data zone + event InvalidPvn(vcId: U8, pvn: U8) \ + severity warning high \ + format "Invalid packet version number {} encountered on VC {}" + + @ Deframing encountered a valid packet version number that is disabled by configuration + event DisabledPvn(vcId: U8, pvn: ComCfg.Pvn) \ + severity warning high \ + format "Valid packet version number {} encountered on VC {} but it is disabled by configuration" + + @ Frame was received with idle-only data on a virtual channel (CCSDS 732.0-B-5 Section 4.1.4.2.2.4) + event IdleFrame(vcId: U8) \ + severity activity low \ + format "Received frame containing only idle data on VC {}" + + @ Spanning packet buffer allocation failed; packet dropped + event SpanningPacketAllocFailed(vcId: U8, pvn: ComCfg.Pvn, packetSize: FwSizeType) \ + severity warning high \ + format "Spanning packet allocation failed on VC {} for {} packet of {} bytes; packet dropped" + + @ Deframing detected a gap/discontinuity in the AOS VC frame count sequence + event VcFrameCountGap(vcId: U8, received: U32, expected: U32) \ + severity warning high \ + format "VC frame count gap on VC {}. Received {} | Expected next count {}" + + @ A spanning packet was abandoned before receiving all expected bytes + event SpanningPacketAbandoned(vcId: U8, pvn: ComCfg.Pvn, bytesReceived: FwSizeType, bytesExpected: FwSizeType) \ + severity warning high \ + format "Spanning packet on VC {} ({}) abandoned after receiving {} of {} expected bytes" diff --git a/Svc/Ccsds/AosDeframer/AosDeframerTelem.fppi b/Svc/Ccsds/AosDeframer/AosDeframerTelem.fppi new file mode 100644 index 00000000000..7b45a8a1912 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/AosDeframerTelem.fppi @@ -0,0 +1,14 @@ + @ Latest VC Frame Received from header + telemetry LatestVcFrameCount: VCxU32 + + @ Processed frames per Virtual Channel + telemetry FramesProcessed: VCxU32 \ + format "{} frames processed" + + @ Packets extracted per Virtual Channel + telemetry PacketsExtracted: VCxU32 \ + format "{} packets extracted" + + @ Frame Error Control Field (FECF) errors (per physical channel) + telemetry CrcErrorCount: U32 \ + format "{} FECF errors" diff --git a/Svc/Ccsds/AosDeframer/CMakeLists.txt b/Svc/Ccsds/AosDeframer/CMakeLists.txt new file mode 100644 index 00000000000..ab53d798643 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/CMakeLists.txt @@ -0,0 +1,37 @@ +#### +# 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/devel/docs/reference/api/cmake/API/ +# +#### + +register_fprime_library( + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/AosDeframer.cpp" + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/AosDeframer.fpp" + "${CMAKE_CURRENT_LIST_DIR}/AosDeframerEvents.fppi" + "${CMAKE_CURRENT_LIST_DIR}/AosDeframerTelem.fppi" + DEPENDS + Svc_Ccsds_Types +) + +register_fprime_ut( + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/AosDeframerTestMain.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/AosDeframerTester.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/AosDeframerTestSupport.cpp" + AUTOCODER_INPUTS + "${CMAKE_CURRENT_LIST_DIR}/AosDeframer.fpp" + "${CMAKE_CURRENT_LIST_DIR}/AosDeframerEvents.fppi" + "${CMAKE_CURRENT_LIST_DIR}/AosDeframerTelem.fppi" + DEPENDS + Svc_Ccsds_Types + STest + UT_AUTO_HELPERS +) diff --git a/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTestMain.cpp b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTestMain.cpp new file mode 100644 index 00000000000..75a94abc41b --- /dev/null +++ b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTestMain.cpp @@ -0,0 +1,202 @@ +// ====================================================================== +// \title AosDeframerTestMain.cpp +// \author Auto-generated +// \brief cpp file for AosDeframer component test main function +// ====================================================================== + +#include "AosDeframerTester.hpp" + +// ---------------------------------------------------------------------- +// Tests - Basic Validation +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testNominalDeframing) { + Svc::Ccsds::AosDeframerTester tester; + tester.testNominalDeframing(); +} + +TEST(AosDeframer, testDataReturn) { + Svc::Ccsds::AosDeframerTester tester; + tester.testDataReturn(); +} + +TEST(AosDeframer, testInvalidScId) { + Svc::Ccsds::AosDeframerTester tester; + tester.testInvalidScId(); +} + +TEST(AosDeframer, testInvalidVcId) { + Svc::Ccsds::AosDeframerTester tester; + tester.testInvalidVcId(); +} + +TEST(AosDeframer, testInvalidFrameLength) { + Svc::Ccsds::AosDeframerTester tester; + tester.testInvalidFrameLength(); +} + +TEST(AosDeframer, testInvalidFecf) { + Svc::Ccsds::AosDeframerTester tester; + tester.testInvalidFecf(); +} + +TEST(AosDeframer, testInvalidTfvn) { + Svc::Ccsds::AosDeframerTester tester; + tester.testInvalidTfvn(); +} + +TEST(AosDeframer, testVcFrameCountGap) { + Svc::Ccsds::AosDeframerTester tester; + tester.testVcFrameCountGap(); +} + +// testAcceptAllVcid removed: accept-all-VCID mode is not supported. + +// ---------------------------------------------------------------------- +// Tests - M_PDU Processing +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testFhpAtOffset) { + Svc::Ccsds::AosDeframerTester tester; + tester.testFhpAtOffset(); +} + +TEST(AosDeframer, testFhpNoPacketStart) { + Svc::Ccsds::AosDeframerTester tester; + tester.testFhpNoPacketStart(); +} + +TEST(AosDeframer, testFhpIdleDataOnly) { + Svc::Ccsds::AosDeframerTester tester; + tester.testFhpIdleDataOnly(); +} + +TEST(AosDeframer, testMultiplePacketsInFrame) { + Svc::Ccsds::AosDeframerTester tester; + tester.testMultiplePacketsInFrame(); +} + +// ---------------------------------------------------------------------- +// Tests - Spanning Packets +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testSpanningPacketTwoFrames) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketTwoFrames(); +} + +TEST(AosDeframer, testSpanningPacketFourFrames) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketFourFrames(); +} + +TEST(AosDeframer, testSpanningPacketContinuation) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketContinuation(); +} + +TEST(AosDeframer, testSpanningPacketAllocFailureEvent) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketAllocFailureEvent(); +} + +TEST(AosDeframer, testSpanningPacketAbandonedOnVcGap) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketAbandonedOnVcGap(); +} + +TEST(AosDeframer, testSpanningPacketAbandonedOnIdleFrame) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketAbandonedOnIdleFrame(); +} + +TEST(AosDeframer, testSpanningPacketAbandonedOnPrematureFhp) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSpanningPacketAbandonedOnPrematureFhp(); +} + +TEST(AosDeframer, testSppHeaderSpansFrame) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSppHeaderSpansFrame(); +} + +TEST(AosDeframer, testEppHeaderSpansFrame) { + Svc::Ccsds::AosDeframerTester tester; + tester.testEppHeaderSpansFrame(); +} + +TEST(AosDeframer, testAllocFailureNextPacketExtracted) { + Svc::Ccsds::AosDeframerTester tester; + tester.testAllocFailureNextPacketExtracted(); +} + +// ---------------------------------------------------------------------- +// Tests - SPP Extraction +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testSppIdlePacketFiltering) { + Svc::Ccsds::AosDeframerTester tester; + tester.testSppIdlePacketFiltering(); +} + +// ---------------------------------------------------------------------- +// Tests - EPP Extraction +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testEppExtraction) { + Svc::Ccsds::AosDeframerTester tester; + tester.testEppExtraction(); +} + +TEST(AosDeframer, testEppLengthOfLength) { + Svc::Ccsds::AosDeframerTester tester; + tester.testEppLengthOfLength(); +} + +TEST(AosDeframer, testEppIdlePacket) { + Svc::Ccsds::AosDeframerTester tester; + tester.testEppIdlePacket(); +} + +TEST(AosDeframer, testEppFillPacket) { + Svc::Ccsds::AosDeframerTester tester; + tester.testEppFillPacket(); +} + +TEST(AosDeframer, testInvalidPvnVersion) { + Svc::Ccsds::AosDeframerTester tester; + tester.testInvalidPvnVersion(); +} + +// ---------------------------------------------------------------------- +// Tests - Configuration +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testFecfDisabled) { + Svc::Ccsds::AosDeframerTester tester; + tester.testFecfDisabled(); +} + +TEST(AosDeframer, testPvnMaskSppOnly) { + Svc::Ccsds::AosDeframerTester tester; + tester.testPvnMaskSppOnly(); +} + +TEST(AosDeframer, testPvnMaskEppOnly) { + Svc::Ccsds::AosDeframerTester tester; + tester.testPvnMaskEppOnly(); +} + +// ---------------------------------------------------------------------- +// Tests - Telemetry +// ---------------------------------------------------------------------- + +TEST(AosDeframer, testFrameCountTelemetry) { + Svc::Ccsds::AosDeframerTester tester; + tester.testFrameCountTelemetry(); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTestSupport.cpp b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTestSupport.cpp new file mode 100644 index 00000000000..e36ef66e28f --- /dev/null +++ b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTestSupport.cpp @@ -0,0 +1,163 @@ +// ====================================================================== +// \title AosDeframerTestSupport.cpp +// \author Codex +// \brief helper function definitions for AosDeframer unit tests +// ====================================================================== + +#include "AosDeframerTester.hpp" +#include "Svc/Ccsds/Types/AOSHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Types/AOSTrailerSerializableAc.hpp" +#include "Svc/Ccsds/Types/M_PDUHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Types/SpacePacketHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Utils/CRC16.hpp" + +namespace Svc { + +namespace Ccsds { + +Fw::Buffer AosDeframerTester::from_allocate_handler(FwIndexType portNum, FwSizeType size) { + (void)portNum; + if (m_failNextAlloc) { + m_failNextAlloc = false; + return Fw::Buffer(); + } + if (size <= ALLOC_BUF_SIZE) { + return Fw::Buffer(this->m_allocBuf, size); + } + return Fw::Buffer(); +} + +void AosDeframerTester::configureDefault() { + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, 0, + PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK); +} + +Fw::Buffer AosDeframerTester::assembleFrameBuffer(U8* payload, + FwSizeType payloadLength, + U16 fhp, + U16 scid, + U8 vcid, + U32 vcCount, + U8 tfvn, + bool includeFecf) { + const U32 frameSize = includeFecf ? TEST_FRAME_SIZE : (TEST_FRAME_SIZE - AOSTrailer::SERIALIZED_SIZE); + ::memset(this->m_frameData, 0, sizeof(this->m_frameData)); + + // Build AOS Primary Header (6 bytes) + // Byte 0-1: globalVcId (2b TFVN | 8b SCID LSB | 6b VCID) + U16 globalVcId = static_cast((tfvn & 0x3) << AOSHeaderSubfields::frameVersionOffset); + globalVcId |= static_cast((scid & 0xFF) << AOSHeaderSubfields::spacecraftIdLsbOffset); + globalVcId |= static_cast(vcid & 0x3F); + this->m_frameData[0] = static_cast(globalVcId >> 8); + this->m_frameData[1] = static_cast(globalVcId & 0xFF); + + // Byte 2-4: VC Frame Count (24 bits) + this->m_frameData[2] = static_cast((vcCount >> 16) & 0xFF); + this->m_frameData[3] = static_cast((vcCount >> 8) & 0xFF); + this->m_frameData[4] = static_cast(vcCount & 0xFF); + + // Byte 5: Signaling field (replay | cycle use | SCID MSB | VC cycle) + U8 signaling = 0; + signaling |= static_cast(1 << AOSHeaderSubfields::cycleCountFlagOffset); // Cycle count in use + signaling |= static_cast(((scid >> 8) & 0x3) << AOSHeaderSubfields::spacecraftIdMsbOffset); + signaling |= static_cast((vcCount >> 24) & 0x0F); // Cycle count + this->m_frameData[5] = signaling; + + // Byte 6-7: M_PDU Header (First Header Pointer) + this->m_frameData[6] = static_cast(fhp >> 8); + this->m_frameData[7] = static_cast(fhp & 0xFF); + + // Copy payload to data zone + const FwSizeType dataZoneStart = AOSHeader::SERIALIZED_SIZE + M_PDUHeader::SERIALIZED_SIZE; + const FwSizeType dataZoneEnd = frameSize - (includeFecf ? AOSTrailer::SERIALIZED_SIZE : 0); + FwSizeType maxPayload = dataZoneEnd - dataZoneStart; + FwSizeType copyLen = FW_MIN(payloadLength, maxPayload); + ::memcpy(this->m_frameData + dataZoneStart, payload, copyLen); + + // Fill remaining data zone with an EPP fill packet (protocolId=0, lengthOfLength=0) + // This prevents interpretation of zeros as valid SPP packets + FwSizeType fillStart = dataZoneStart + copyLen; + if (fillStart < dataZoneEnd) { + this->createEppPacket(this->m_frameData + fillStart, EppProtocolId::Idle, EppLengthOfLength::Zero, 0); + } + + // Add FECF if enabled + if (includeFecf) { + U16 crc = Ccsds::Utils::CRC16::compute(this->m_frameData, frameSize - AOSTrailer::SERIALIZED_SIZE); + this->m_frameData[frameSize - 2] = static_cast(crc >> 8); + this->m_frameData[frameSize - 1] = static_cast(crc & 0xFF); + } + + return Fw::Buffer(this->m_frameData, frameSize); +} + +FwSizeType AosDeframerTester::createSppPacket(U8* buffer, U16 apid, U16 dataLength, U16 seqCount) { + const U16 packetIdentification = + static_cast((ComCfg::Pvn::SPACE_PACKET_PROTOCOL << SpacePacketSubfields::PvnOffset) | + (apid & SpacePacketSubfields::ApidMask)); + const U16 packetSequenceControl = static_cast((0x3 << SpacePacketSubfields::SeqFlagsOffset) | + (seqCount & SpacePacketSubfields::SeqCountMask)); + + const U16 packetDataLength = static_cast(dataLength - 1); + + SpacePacketHeader header(packetIdentification, packetSequenceControl, packetDataLength); + Fw::ExternalSerializeBuffer serializer(buffer, SpacePacketHeader::SERIALIZED_SIZE); + FW_ASSERT(header.serializeTo(serializer) == Fw::FW_SERIALIZE_OK); + + for (U16 i = 0; i < dataLength; i++) { + buffer[SpacePacketHeader::SERIALIZED_SIZE + i] = static_cast(i & 0xFF); + } + + return SpacePacketHeader::SERIALIZED_SIZE + dataLength; +} + +FwSizeType AosDeframerTester::createEppPacket(U8* buffer, + U8 protocolId, + EppLengthOfLength lengthOfLength, + const FwSizeType dataLength) { + // EPP Packet per CCSDS 133.1-B-3 Section 4.1.2 / 4.1.3.2 + // Byte 0: 3b PVN=7 | 3b protocolId | 2b lengthOfLength + // protocolId=0 (EppProtocolId::Idle) produces an EPP idle/fill packet + buffer[0] = static_cast((ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL << EPPSubfields::packetVersionOffset)); + buffer[0] |= ((protocolId & EPPSubfields::protocolIdMask) << EPPSubfields::protocolIdOffset); + buffer[0] |= (lengthOfLength & EPPSubfields::lengthOfLengthMask); + + if (lengthOfLength == EppLengthOfLength::Zero) { + return 1; + } + + // Numerical meaning (not wire version) of length of length + U8 lol = lengthOfLength; + + FwSizeType offset = 1; + + // Extension byte for lengthOfLength >= 2 (per CCSDS 133.1-B-3 Section 4.1.2.1.1) + if (lengthOfLength >= EppLengthOfLength::Two) { + buffer[offset++] = 0x00; + } + + // Two CCSDS reserved bytes for lengthOfLength == 4 + if (lengthOfLength == EppLengthOfLength::Four) { + buffer[offset++] = 0x00; + buffer[offset++] = 0x00; + + // 3 on the wire, but means 4 + lol = 4; + } + + // Length field (big-endian) + for (U8 i = 0; i < lol; i++) { + buffer[offset++] = static_cast(dataLength >> (8 * (lol - i - 1)) & 0xFF); + } + + // Fill data + for (FwSizeType i = 0; i < dataLength; i++) { + buffer[offset++] = 0x55; + } + + return offset; +} + +} // namespace Ccsds + +} // namespace Svc diff --git a/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTester.cpp b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTester.cpp new file mode 100644 index 00000000000..0c5a19056a9 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTester.cpp @@ -0,0 +1,990 @@ +// ====================================================================== +// \title AosDeframerTester.cpp +// \author Auto-generated +// \brief cpp file for AosDeframer component test harness implementation class +// ====================================================================== + +#include "AosDeframerTester.hpp" +#include "STest/Random/Random.hpp" +#include "Svc/Ccsds/Types/AOSHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Types/AOSTrailerSerializableAc.hpp" +#include "Svc/Ccsds/Types/M_PDUHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Types/SpacePacketHeaderSerializableAc.hpp" +#include "Svc/Ccsds/Utils/CRC16.hpp" + +namespace Svc { + +namespace Ccsds { + +// ---------------------------------------------------------------------- +// Construction and destruction +// ---------------------------------------------------------------------- + +AosDeframerTester::AosDeframerTester() + : AosDeframerGTestBase("AosDeframerTester", AosDeframerTester::MAX_HISTORY_SIZE), component("AosDeframer") { + this->initComponents(); + this->connectPorts(); +} + +AosDeframerTester::~AosDeframerTester() {} + +void AosDeframerTester::assertDataOutVcId(const U8 expectedVcId) const { + const U32 dataOutSize = static_cast(this->fromPortHistory_dataOut->size()); + for (U32 i = 0; i < dataOutSize; i++) { + ASSERT_EQ(this->fromPortHistory_dataOut->at(i).context.get_vcId(), expectedVcId); + } +} + +// ---------------------------------------------------------------------- +// Tests - Basic Validation +// ---------------------------------------------------------------------- + +void AosDeframerTester::testNominalDeframing() { + const U8 testVcId = 7; + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, testVcId, + PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK); + + // Create a simple SPP packet + U8 payload[100]; + FwSizeType sppSize = this->createSppPacket(payload, 0x001, 50); // APID 1, 50 bytes data + + // Assemble frame with FHP=0 + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, testVcId); + ComCfg::FrameContext context; + + // Invoke the deframer + this->invoke_to_dataIn(0, buffer, context); + + // Should output one packet + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(testVcId); + ASSERT_from_dataReturnOut_SIZE(1); // Frame buffer returned + + // Verify packet content and context + Fw::Buffer outBuffer = this->fromPortHistory_dataOut->at(0).data; + ASSERT_EQ(outBuffer.getSize(), sppSize); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).context.get_pvn(), ComCfg::Pvn::SPACE_PACKET_PROTOCOL); + + // Verify telemetry + ASSERT_TLM_SIZE(3); // LatestVcFrameCount, FramesProcessed, and PacketsExtracted + ASSERT_TLM_FramesProcessed_SIZE(1); + ASSERT_TLM_FramesProcessed(0, 1); + ASSERT_TLM_PacketsExtracted_SIZE(1); + ASSERT_TLM_PacketsExtracted(0, 1); + ASSERT_TLM_LatestVcFrameCount_SIZE(1); + ASSERT_TLM_LatestVcFrameCount(0, 0); // vcCount=0 (assembleFrameBuffer default) +} + +void AosDeframerTester::testDataReturn() { + this->configureDefault(); + + U8 data[1] = {0}; + Fw::Buffer buffer(data, sizeof(data)); + ComCfg::FrameContext context; + + this->invoke_to_dataReturnIn(0, buffer, context); + + // dataReturnIn receives back dynamically allocated packet buffers from downstream + // and deallocates them via the deallocate port. + ASSERT_from_deallocate_SIZE(1); + ASSERT_FROM_PORT_HISTORY_SIZE(1); + ASSERT_EQ(this->fromPortHistory_deallocate->at(0).fwBuffer.getData(), data); +} + +void AosDeframerTester::testInvalidScId() { + this->configureDefault(); + + U8 payload[50]; + FwSizeType sppSize = this->createSppPacket(payload, 0x001, 20); + + // Use wrong spacecraft ID + U16 wrongScid = static_cast((ComCfg::SpacecraftId + 1) & 0x3FF); + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0, wrongScid); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // No packets should be output + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); // Frame returned + ASSERT_from_errorNotify_SIZE(1); + ASSERT_from_errorNotify(0, Ccsds::FrameError::AOS_INVALID_SCID); + ASSERT_EVENTS_SIZE(1); + ASSERT_EVENTS_InvalidSpacecraftId_SIZE(1); +} + +void AosDeframerTester::testInvalidVcId() { + // Configure to accept only VCID 0 (always filtered - no accept-all-vcid mode) + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, 0); + + U8 payload[50]; + FwSizeType sppSize = this->createSppPacket(payload, 0x001, 20); + + // Use wrong VCID (1 instead of 0) + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, 1); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_from_errorNotify_SIZE(1); + ASSERT_from_errorNotify(0, Ccsds::FrameError::AOS_INVALID_VCID); + ASSERT_EVENTS_SIZE(1); + ASSERT_EVENTS_InvalidVcId_SIZE(1); +} + +void AosDeframerTester::testInvalidFrameLength() { + this->configureDefault(); + + // Send a buffer smaller than expected frame size + U8 shortBuffer[50]; + ::memset(shortBuffer, 0, sizeof(shortBuffer)); + Fw::Buffer buffer(shortBuffer, sizeof(shortBuffer)); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_from_errorNotify_SIZE(1); + ASSERT_from_errorNotify(0, Ccsds::FrameError::AOS_INVALID_LENGTH); + ASSERT_EVENTS_SIZE(1); + ASSERT_EVENTS_InvalidFrameLength_SIZE(1); +} + +void AosDeframerTester::testInvalidFecf() { + this->configureDefault(); + + U8 payload[50]; + FwSizeType sppSize = this->createSppPacket(payload, 0x001, 20); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0); + + // Corrupt the CRC (last 2 bytes) + buffer.getData()[TEST_FRAME_SIZE - 1] ^= 0xFF; + + ComCfg::FrameContext context; + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_from_errorNotify_SIZE(1); + ASSERT_from_errorNotify(0, Ccsds::FrameError::AOS_INVALID_CRC); + ASSERT_EVENTS_SIZE(1); + ASSERT_EVENTS_InvalidFecf_SIZE(1); + ASSERT_TLM_CrcErrorCount_SIZE(1); + ASSERT_TLM_CrcErrorCount(0, 1); + + // Second corrupt frame - verify counter accumulates + this->clearHistory(); + Fw::Buffer buffer2 = this->assembleFrameBuffer(payload, sppSize, 0); + buffer2.getData()[TEST_FRAME_SIZE - 1] ^= 0xFF; + this->invoke_to_dataIn(0, buffer2, context); + ASSERT_TLM_CrcErrorCount(0, 2); +} + +void AosDeframerTester::testInvalidTfvn() { + this->configureDefault(); + + U8 payload[50]; + FwSizeType sppSize = this->createSppPacket(payload, 0x001, 20); + + // Use wrong TFVN (0 instead of 1) + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, 0, 0, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_from_errorNotify_SIZE(1); + ASSERT_from_errorNotify(0, Ccsds::FrameError::AOS_INVALID_VERSION); + ASSERT_EVENTS_SIZE(1); + ASSERT_EVENTS_InvalidTfvn_SIZE(1); +} + +void AosDeframerTester::testVcFrameCountGap() { + this->configureDefault(); + + U8 payload[50]; + FwSizeType sppSize = this->createSppPacket(payload, 0x001, 20); + ComCfg::FrameContext context; + + // First valid frame initializes the tracked VC frame count and should not report a gap. + Fw::Buffer buffer1 = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, 0, 0); + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_errorNotify_SIZE(0); + ASSERT_EVENTS_VcFrameCountGap_SIZE(0); + + this->clearHistory(); + + // Skip VC frame count 1 to force a discontinuity. + Fw::Buffer buffer2 = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, 0, 2); + this->invoke_to_dataIn(0, buffer2, context); + + ASSERT_from_dataOut_SIZE(1); // Frame is still processed + this->assertDataOutVcId(0); + ASSERT_from_dataReturnOut_SIZE(1); // Frame buffer returned + ASSERT_from_errorNotify_SIZE(1); + ASSERT_from_errorNotify(0, Ccsds::FrameError::AOS_VC_FRAME_COUNT_GAP); + ASSERT_EVENTS_VcFrameCountGap_SIZE(1); + ASSERT_EVENTS_VcFrameCountGap(0, 0, 2, 1); + ASSERT_TLM_LatestVcFrameCount_SIZE(1); + ASSERT_TLM_LatestVcFrameCount(0, 2); +} + +// ---------------------------------------------------------------------- +// Tests - M_PDU Processing +// ---------------------------------------------------------------------- + +void AosDeframerTester::testFhpAtOffset() { + this->configureDefault(); + + // Create payload with junk data followed by an SPP packet + U8 payload[150]; + const FwSizeType junkOffset = 30; + ::memset(payload, 0xAA, junkOffset); // Junk data (continuation) + + FwSizeType sppSize = this->createSppPacket(payload + junkOffset, 0x003, 50); + + // FHP points to where packet starts + Fw::Buffer buffer = this->assembleFrameBuffer(payload, junkOffset + sppSize, static_cast(junkOffset)); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), sppSize); +} + +void AosDeframerTester::testFhpNoPacketStart() { + this->configureDefault(); + + // Scenario 1: FHP_NO_PACKET_START with NO active spanning packet (orphan continuation) + // This covers the "continuation data cannot be used" path in extractPackets. + { + U8 orphanData[TEST_DATA_ZONE_SIZE]; + ::memset(orphanData, 0xAA, sizeof(orphanData)); + Fw::Buffer orphanFrame = + this->assembleFrameBuffer(orphanData, TEST_DATA_ZONE_SIZE, M_PDUSubfields::FHP_NO_PACKET_START); + ComCfg::FrameContext context; + this->invoke_to_dataIn(0, orphanFrame, context); + // Data silently dropped, no error events, no output + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_errorNotify_SIZE(0); + this->clearHistory(); + } + + // Scenario 2: FHP_NO_PACKET_START with an active spanning packet (normal continuation) + // For TEST_FRAME_SIZE=256 with FECF: data zone = 256 - 6 - 2 - 2 = 246 bytes + // Create a packet that spans two frames: header (6) + data (250) = 256 bytes + U8 payload1[300]; + FwSizeType sppSize = this->createSppPacket(payload1, 0x004, 250); // Packet that will span two frames + + // First frame - send as much of the packet as fits (up to data zone size) + Fw::Buffer buffer1 = this->assembleFrameBuffer(payload1, TEST_DATA_ZONE_SIZE, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer1, context); + + // No complete packet yet (packet spans into next frame) + ASSERT_from_dataOut_SIZE(0); + this->clearHistory(); + + // Now send continuation frame with FHP = 0x7FE (no packet start) + U8 payload2[256]; + FwSizeType remainingSize = sppSize - TEST_DATA_ZONE_SIZE; + ::memcpy(payload2, payload1 + TEST_DATA_ZONE_SIZE, remainingSize); + + Fw::Buffer buffer2 = this->assembleFrameBuffer(payload2, remainingSize, M_PDUSubfields::FHP_NO_PACKET_START, + ComCfg::SpacecraftId, 0, 1); + + this->invoke_to_dataIn(0, buffer2, context); + + // Should now have complete packet + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); +} + +void AosDeframerTester::testFhpIdleDataOnly() { + this->configureDefault(); + + U8 payload[100]; + ::memset(payload, 0x55, sizeof(payload)); // Idle pattern + + // FHP = 0x7FF means idle data only + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sizeof(payload), M_PDUSubfields::FHP_IDLE_DATA_ONLY); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // No packets output, but frame was processed + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_EVENTS_SIZE(1); + ASSERT_EVENTS_IdleFrame_SIZE(1); + ASSERT_EVENTS_IdleFrame(0, 0); // vcId=0 (the configured VC) + ASSERT_TLM_FramesProcessed_SIZE(1); + ASSERT_TLM_FramesProcessed(0, 1); +} + +void AosDeframerTester::testMultiplePacketsInFrame() { + this->configureDefault(); + + U8 payload[200]; + FwSizeType offset = 0; + + // Create 3 small packets + offset += this->createSppPacket(payload + offset, 0x010, 20); + offset += this->createSppPacket(payload + offset, 0x011, 25); + offset += this->createSppPacket(payload + offset, 0x012, 30); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, offset, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(3); + this->assertDataOutVcId(0); + ASSERT_TLM_PacketsExtracted_SIZE(3); + ASSERT_TLM_PacketsExtracted(2, 3); // Final count is 3 +} + +// ---------------------------------------------------------------------- +// Tests - Spanning Packets +// ---------------------------------------------------------------------- + +void AosDeframerTester::testSpanningPacketTwoFrames() { + this->configureDefault(); + + // Create a packet larger than one frame's data zone + const FwSizeType packetDataLen = TEST_DATA_ZONE_SIZE + 50; // Spans into second frame + + U8 fullPacket[512]; + FwSizeType totalPacketSize = this->createSppPacket(fullPacket, 0x020, static_cast(packetDataLen)); + + // First frame - partial packet + Fw::Buffer buffer1 = this->assembleFrameBuffer(fullPacket, TEST_DATA_ZONE_SIZE, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_dataOut_SIZE(0); // Packet not complete yet + this->clearHistory(); + + // Second frame - rest of packet with FHP pointing to next packet + FwSizeType remainingBytes = totalPacketSize - TEST_DATA_ZONE_SIZE; + U8 payload2[200]; + ::memcpy(payload2, fullPacket + TEST_DATA_ZONE_SIZE, remainingBytes); + + // Add another packet after the spanning one + FwSizeType nextPacketSize = this->createSppPacket(payload2 + remainingBytes, 0x021, 20); + + Fw::Buffer buffer2 = this->assembleFrameBuffer(payload2, remainingBytes + nextPacketSize, + static_cast(remainingBytes), ComCfg::SpacecraftId, 0, 1); + + this->invoke_to_dataIn(0, buffer2, context); + + // Should have both packets now + ASSERT_from_dataOut_SIZE(2); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), totalPacketSize); +} + +void AosDeframerTester::testSpanningPacketFourFrames() { + this->configureDefault(); + + // Header (6) + data (900) = 906 bytes, which spans four ~246-byte data zones + const FwSizeType packetDataLen = 900; + U8 fullPacket[1024]; + FwSizeType totalPacketSize = this->createSppPacket(fullPacket, 0x031, static_cast(packetDataLen)); + + ASSERT_TRUE(totalPacketSize > (3 * TEST_DATA_ZONE_SIZE)); + ASSERT_TRUE(totalPacketSize <= (4 * TEST_DATA_ZONE_SIZE)); + + ComCfg::FrameContext context; + + Fw::Buffer buffer1 = + this->assembleFrameBuffer(fullPacket, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, 0, 0, 1, true); + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_dataOut_SIZE(0); + this->clearHistory(); + + Fw::Buffer buffer2 = + this->assembleFrameBuffer(fullPacket + TEST_DATA_ZONE_SIZE, TEST_DATA_ZONE_SIZE, + M_PDUSubfields::FHP_NO_PACKET_START, ComCfg::SpacecraftId, 0, 1, 1, true); + this->invoke_to_dataIn(0, buffer2, context); + ASSERT_from_dataOut_SIZE(0); + this->clearHistory(); + + Fw::Buffer buffer3 = + this->assembleFrameBuffer(fullPacket + (2 * TEST_DATA_ZONE_SIZE), TEST_DATA_ZONE_SIZE, + M_PDUSubfields::FHP_NO_PACKET_START, ComCfg::SpacecraftId, 0, 2, 1, true); + this->invoke_to_dataIn(0, buffer3, context); + ASSERT_from_dataOut_SIZE(0); + this->clearHistory(); + + FwSizeType remainingBytes = totalPacketSize - (3 * TEST_DATA_ZONE_SIZE); + Fw::Buffer buffer4 = + this->assembleFrameBuffer(fullPacket + (3 * TEST_DATA_ZONE_SIZE), remainingBytes, + static_cast(remainingBytes), ComCfg::SpacecraftId, 0, 3, 1, true); + this->invoke_to_dataIn(0, buffer4, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), totalPacketSize); +} + +void AosDeframerTester::testSpanningPacketContinuation() { + this->configureDefault(); + + // For spanning, packet must be larger than data zone size (246 bytes) + // Create packet with header (6) + data (280) = 286 bytes + const U16 packetDataLen = 280; + + U8 payload1[300]; + FwSizeType sppSize = this->createSppPacket(payload1, 0x040, packetDataLen); + + // First frame - send full data zone (partial packet) + Fw::Buffer buffer1 = this->assembleFrameBuffer(payload1, TEST_DATA_ZONE_SIZE, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_dataOut_SIZE(0); // Packet not complete yet + this->clearHistory(); + + // Second frame has continuation + FHP at correct offset + U8 payload2[150]; + FwSizeType continuation = sppSize - TEST_DATA_ZONE_SIZE; + ::memcpy(payload2, payload1 + TEST_DATA_ZONE_SIZE, continuation); + + // Add new packet after continuation + FwSizeType nextSize = this->createSppPacket(payload2 + continuation, 0x041, 30); + + Fw::Buffer buffer2 = this->assembleFrameBuffer(payload2, continuation + nextSize, static_cast(continuation), + ComCfg::SpacecraftId, 0, 1); + + this->invoke_to_dataIn(0, buffer2, context); + + ASSERT_from_dataOut_SIZE(2); // Both packets + this->assertDataOutVcId(0); +} + +void AosDeframerTester::testSpanningPacketAllocFailureEvent() { + this->configureDefault(); + + U8 payload[64] = {}; + // Start an EPP packet that declares a payload large enough to exceed the test allocator buffer. + payload[0] = (ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL << EPPSubfields::packetVersionOffset); + payload[0] |= EppProtocolId::MissionSpecific << EPPSubfields::protocolIdOffset; + payload[0] |= 0x02 & EPPSubfields::lengthOfLengthMask; + + payload[1] = 0x00; // Ext Field + + payload[2] = 0xFF; + payload[3] = 0xFF; // dataLength = 65535 -> total packet size = 65539 (> ALLOC_BUF_SIZE=65536) + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sizeof(payload), 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(0); + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_EVENTS_SpanningPacketAllocFailed_SIZE(1); +} + +void AosDeframerTester::testSpanningPacketAbandonedOnVcGap() { + this->configureDefault(); + + // Create a packet that spans two frames (6 + 280 = 286 bytes > 246-byte data zone) + U8 fullPacket[300]; + this->createSppPacket(fullPacket, 0x050, 280); + + ComCfg::FrameContext context; + + // Frame 0 (vcCount=0): send only the first data zone's worth — spanning in progress + Fw::Buffer buffer1 = this->assembleFrameBuffer(fullPacket, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, 0, 0); + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_dataOut_SIZE(0); + this->clearHistory(); + + // Frame 2 (vcCount=2, gap): spanning packet abandoned; fresh complete packet still extracted + U8 freshPayload[50]; + FwSizeType freshSize = this->createSppPacket(freshPayload, 0x051, 20); + Fw::Buffer buffer2 = this->assembleFrameBuffer(freshPayload, freshSize, 0, ComCfg::SpacecraftId, 0, 2); + this->invoke_to_dataIn(0, buffer2, context); + + ASSERT_from_dataOut_SIZE(1); // Only the fresh packet — partial spanning packet was dropped + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), freshSize); + ASSERT_EVENTS_VcFrameCountGap_SIZE(1); + ASSERT_EVENTS_SpanningPacketAbandoned_SIZE(1); + ASSERT_EVENTS_SpanningPacketAbandoned(0, 0, ComCfg::Pvn::SPACE_PACKET_PROTOCOL, TEST_DATA_ZONE_SIZE, 286); +} + +void AosDeframerTester::testSpanningPacketAbandonedOnIdleFrame() { + this->configureDefault(); + + // Create a packet that spans two frames (6 + 280 = 286 bytes > 246-byte data zone) + U8 fullPacket[300]; + this->createSppPacket(fullPacket, 0x060, 280); + + ComCfg::FrameContext context; + + // Frame 0 (vcCount=0): spanning in progress + Fw::Buffer buffer1 = this->assembleFrameBuffer(fullPacket, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, 0, 0); + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_dataOut_SIZE(0); + this->clearHistory(); + + // Frame 1 (vcCount=1): idle frame — spanning packet abandoned + U8 idlePayload[1] = {0}; + Fw::Buffer buffer2 = this->assembleFrameBuffer(idlePayload, sizeof(idlePayload), M_PDUSubfields::FHP_IDLE_DATA_ONLY, + ComCfg::SpacecraftId, 0, 1); + this->invoke_to_dataIn(0, buffer2, context); + + ASSERT_from_dataOut_SIZE(0); // Partial spanning packet dropped + ASSERT_EVENTS_IdleFrame_SIZE(1); + ASSERT_EVENTS_IdleFrame(0, 0); + ASSERT_EVENTS_SpanningPacketAbandoned_SIZE(1); + ASSERT_EVENTS_SpanningPacketAbandoned(0, 0, ComCfg::Pvn::SPACE_PACKET_PROTOCOL, TEST_DATA_ZONE_SIZE, 286); +} + +void AosDeframerTester::testSpanningPacketAbandonedOnPrematureFhp() { + this->configureDefault(); + + // Packet A: 400 bytes total (6-byte SPP header + 394-byte data), exceeds data zone by 154 bytes + U8 packetA[400]; + this->createSppPacket(packetA, 0x070, 394); + + ComCfg::FrameContext context; + + // Frame 0 (vcCount=0): FHP=0, first 246 bytes of packet A accumulated in spanning packet + Fw::Buffer buffer1 = this->assembleFrameBuffer(packetA, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, 0, 0); + this->invoke_to_dataIn(0, buffer1, context); + ASSERT_from_dataOut_SIZE(0); // Packet A incomplete + this->clearHistory(); + + // Frame 1 (vcCount=1): FHP=50, only 50 continuation bytes precede packet B + // Packet A needs 154 more bytes but only gets 50 — spanning packet is abandoned + U8 payload2[TEST_DATA_ZONE_SIZE]; + const FwSizeType fhp = 50; + ::memcpy(payload2, packetA + TEST_DATA_ZONE_SIZE, fhp); // 50 bytes of packet A's tail + FwSizeType sizeB = this->createSppPacket(payload2 + fhp, 0x071, 20); + Fw::Buffer buffer2 = + this->assembleFrameBuffer(payload2, fhp + sizeB, static_cast(fhp), ComCfg::SpacecraftId, 0, 1); + this->invoke_to_dataIn(0, buffer2, context); + + // Packet A abandoned at the FHP boundary; only packet B extracted + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), sizeB); + ASSERT_EVENTS_SpanningPacketAbandoned_SIZE(1); + ASSERT_EVENTS_SpanningPacketAbandoned(0, 0, ComCfg::Pvn::SPACE_PACKET_PROTOCOL, TEST_DATA_ZONE_SIZE + fhp, 400); +} + +void AosDeframerTester::testSppHeaderSpansFrame() { + this->configureDefault(); + + // Fill most of the data zone with a complete packet, leaving only 3 bytes for the + // next packet's header (SPP header is 6 bytes, so 3 bytes are insufficient to size it). + U8 payload1[TEST_DATA_ZONE_SIZE]; + FwSizeType firstSize = this->createSppPacket(payload1, 0x072, 237); // 6 + 237 = 243 bytes + + // Second packet: first 3 bytes go in frame 0, last 3 header bytes + data go in frame 1 + U8 secondPacket[50]; + FwSizeType secondSize = this->createSppPacket(secondPacket, 0x073, 20); // 26 bytes + const FwSizeType splitAt = TEST_DATA_ZONE_SIZE - firstSize; // = 3 + ::memcpy(payload1 + firstSize, secondPacket, splitAt); + + ComCfg::FrameContext context; + + // Frame 0: first packet complete + 3-byte partial SPP header + Fw::Buffer buffer1 = this->assembleFrameBuffer(payload1, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, 0, 0); + this->invoke_to_dataIn(0, buffer1, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), firstSize); + this->clearHistory(); + + // Frame 1: remaining 23 bytes of second packet (3 header + 20 data) + Fw::Buffer buffer2 = this->assembleFrameBuffer(secondPacket + splitAt, secondSize - splitAt, + M_PDUSubfields::FHP_NO_PACKET_START, ComCfg::SpacecraftId, 0, 1); + this->invoke_to_dataIn(0, buffer2, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), secondSize); +} + +void AosDeframerTester::testEppHeaderSpansFrame() { + this->configureDefault(); + + // Fill most of the data zone with a complete packet, leaving only 2 bytes for the + // next packet's header (EPP lol=2 header is 4 bytes, so 2 bytes are insufficient to size it). + U8 payload1[TEST_DATA_ZONE_SIZE]; + FwSizeType firstSize = this->createSppPacket(payload1, 0x074, 238); // 6 + 238 = 244 bytes + + // EPP packet lol=2: 1-byte first field + 1-byte extension + 2-byte length + 20 data = 24 bytes + U8 eppPacket[50]; + FwSizeType eppSize = this->createEppPacket(eppPacket, EppProtocolId::MissionSpecific, EppLengthOfLength::Two, 20); + const FwSizeType splitAt = TEST_DATA_ZONE_SIZE - firstSize; // = 2 + ::memcpy(payload1 + firstSize, eppPacket, splitAt); + + ComCfg::FrameContext context; + + // Frame 0: SPP complete + 2-byte partial EPP header + Fw::Buffer buffer1 = this->assembleFrameBuffer(payload1, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, 0, 0); + this->invoke_to_dataIn(0, buffer1, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), firstSize); + this->clearHistory(); + + // Frame 1: remaining 22 bytes of EPP packet (2 length bytes + 20 data bytes) + Fw::Buffer buffer2 = this->assembleFrameBuffer(eppPacket + splitAt, eppSize - splitAt, + M_PDUSubfields::FHP_NO_PACKET_START, ComCfg::SpacecraftId, 0, 1); + this->invoke_to_dataIn(0, buffer2, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), eppSize); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).context.get_pvn(), ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL); +} + +void AosDeframerTester::testAllocFailureNextPacketExtracted() { + this->configureDefault(); + + U8 payload[100]; + FwSizeType offset = 0; + + // Packet A: alloc will be forced to fail + FwSizeType sizeA = this->createSppPacket(payload + offset, 0x080, 20); + offset += sizeA; + + // Packet B: should be extracted cleanly after A's failure + FwSizeType sizeB = this->createSppPacket(payload + offset, 0x081, 20); + offset += sizeB; + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, offset, 0); + ComCfg::FrameContext context; + + m_failNextAlloc = true; + this->invoke_to_dataIn(0, buffer, context); + + // Packet A was dropped; packet B extracted cleanly + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), sizeB); + ASSERT_EVENTS_SpanningPacketAllocFailed_SIZE(1); +} + +// ---------------------------------------------------------------------- +// Tests - SPP Extraction +// ---------------------------------------------------------------------- + +void AosDeframerTester::testSppIdlePacketFiltering() { + this->configureDefault(); + + U8 payload[200]; + FwSizeType offset = 0; + + // Real packet + FwSizeType sppSize0 = this->createSppPacket(payload + offset, 0x101, 20); + offset += sppSize0; + + // Another real packet + FwSizeType sppSize1 = this->createSppPacket(payload + offset, 0x102, 25); + offset += sppSize1; + + // Idle packet (APID 0x7FF) + offset += this->createSppPacket(payload + offset, ComCfg::Apid::SPP_IDLE_PACKET, 30); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, offset, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // Only 2 packets output (idle filtered) + ASSERT_from_dataOut_SIZE(2); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), sppSize0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).context.get_pvn(), ComCfg::Pvn::SPACE_PACKET_PROTOCOL); + ASSERT_EQ(this->fromPortHistory_dataOut->at(1).data.getSize(), sppSize1); + ASSERT_EQ(this->fromPortHistory_dataOut->at(1).context.get_pvn(), ComCfg::Pvn::SPACE_PACKET_PROTOCOL); +} + +// ---------------------------------------------------------------------- +// Tests - EPP Extraction +// ---------------------------------------------------------------------- + +void AosDeframerTester::testEppExtraction() { + const U8 testVcId = 11; + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, testVcId, + PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK); + + U8 payload[100]; + FwSizeType eppSize = + this->createEppPacket(payload, EppProtocolId::MissionSpecific, EppLengthOfLength::One, 50); // Protocol ID 2 + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, eppSize, 0, ComCfg::SpacecraftId, testVcId); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(testVcId); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), eppSize); + ComCfg::FrameContext outContext = this->fromPortHistory_dataOut->at(0).context; + ASSERT_EQ(outContext.get_pvn(), ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL); +} + +void AosDeframerTester::testEppLengthOfLength() { + const U8 testVcId = 11; + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, testVcId, + PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK); + + // lol=1: 1 first byte + 1 length byte + 11 data = 13 bytes + U8 packet1[13]; + FwSizeType size1 = this->createEppPacket(packet1, EppProtocolId::MissionSpecific, EppLengthOfLength::One, 11); + + // lol=2: 1 first byte + 1 extension byte + 2 length bytes + 277 data = 281 bytes + U8 packet2[281]; + FwSizeType size2 = this->createEppPacket(packet2, EppProtocolId::MissionSpecific, EppLengthOfLength::Two, 277); + + // lol=4: 1 first byte + 1 extension byte + 2 reserved bytes + 4 length bytes + 1367 data = 1375 bytes + U8 packet4[1375]; + FwSizeType size4 = this->createEppPacket(packet4, EppProtocolId::MissionSpecific, EppLengthOfLength::Four, 1367); + + ComCfg::FrameContext context; + U32 vcCount = 0; + + // --- lol=1: fits in a single frame --- + Fw::Buffer frame0 = this->assembleFrameBuffer(packet1, size1, 0, ComCfg::SpacecraftId, testVcId, vcCount++); + this->invoke_to_dataIn(0, frame0, context); + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(testVcId); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), size1); + this->clearHistory(); + + // --- lol=2: spans two frames (246 + 35 bytes) --- + Fw::Buffer frame1 = + this->assembleFrameBuffer(packet2, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, testVcId, vcCount++); + this->invoke_to_dataIn(0, frame1, context); + ASSERT_from_dataOut_SIZE(0); + + FwSizeType lol2Remaining = size2 - TEST_DATA_ZONE_SIZE; + Fw::Buffer frame2 = + this->assembleFrameBuffer(packet2 + TEST_DATA_ZONE_SIZE, lol2Remaining, M_PDUSubfields::FHP_NO_PACKET_START, + ComCfg::SpacecraftId, testVcId, vcCount++); + this->invoke_to_dataIn(0, frame2, context); + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(testVcId); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), size2); + this->clearHistory(); + + // --- lol=4: spans six frames (246 bytes x5 + 145 bytes) --- + Fw::Buffer frame3 = + this->assembleFrameBuffer(packet4, TEST_DATA_ZONE_SIZE, 0, ComCfg::SpacecraftId, testVcId, vcCount++); + this->invoke_to_dataIn(0, frame3, context); + ASSERT_from_dataOut_SIZE(0); + + for (FwSizeType i = 1; i <= 4; i++) { + Fw::Buffer frameN = + this->assembleFrameBuffer(packet4 + i * TEST_DATA_ZONE_SIZE, TEST_DATA_ZONE_SIZE, + M_PDUSubfields::FHP_NO_PACKET_START, ComCfg::SpacecraftId, testVcId, vcCount++); + this->invoke_to_dataIn(0, frameN, context); + ASSERT_from_dataOut_SIZE(0); + } + + FwSizeType lol4Remaining = size4 - 5 * TEST_DATA_ZONE_SIZE; + Fw::Buffer frame8 = + this->assembleFrameBuffer(packet4 + 5 * TEST_DATA_ZONE_SIZE, lol4Remaining, M_PDUSubfields::FHP_NO_PACKET_START, + ComCfg::SpacecraftId, testVcId, vcCount++); + this->invoke_to_dataIn(0, frame8, context); + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(testVcId); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).data.getSize(), size4); +} + +void AosDeframerTester::testEppIdlePacket() { + this->configureDefault(); + + U8 payload[100]; + FwSizeType offset = 0; + + // Real SPP packet + offset += this->createSppPacket(payload + offset, 0x104, 20); + + // Another real packet + offset += this->createSppPacket(payload + offset, 0x105, 15); + + // EPP idle packet with length + offset += this->createEppPacket(payload + offset, EppProtocolId::Idle, EppLengthOfLength::Two, 10); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, offset, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // Only 2 packets (EPP idle filtered) + ASSERT_from_dataOut_SIZE(2); + this->assertDataOutVcId(0); +} + +void AosDeframerTester::testEppFillPacket() { + this->configureDefault(); + + U8 payload[150]; + FwSizeType offset = 0; + + // Real packet + offset += this->createSppPacket(payload + offset, 0x106, 30); + + // EPP fill packet (length of length = 0) - consumes rest + offset += this->createEppPacket(payload + offset, EppProtocolId::Idle, EppLengthOfLength::Zero, 0); + + // Fill rest with pattern + ::memset(payload + offset, 0x55, sizeof(payload) - offset); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sizeof(payload) - 50, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // Only 1 packet (fill consumed rest) + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); +} + +void AosDeframerTester::testInvalidPvnVersion() { + this->configureDefault(); + + U8 payload[100]; + + // Create a packet with unrecognized PVN (not 0/SPP and not 7/EPP) + // PVN=3 (0b011) means first byte upper 3 bits = 011 + // 0x60 = 0b01100000 -> PVN = 3 + payload[0] = 0x60 | 0x02; // Version 3, Protocol ID 2 + payload[1] = 0x00; // Length high byte + payload[2] = 0x10; // Length low byte (16) + ::memset(payload + 3, 0xAA, 16); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, 19, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // Unrecognized PVN should result in no packets output and an explicit event. + ASSERT_from_dataOut_SIZE(0); + // Frame should still be returned + ASSERT_from_dataReturnOut_SIZE(1); + ASSERT_EVENTS_InvalidPvn_SIZE(1); + // Telemetry should still be updated for frame count + ASSERT_TLM_FramesProcessed_SIZE(1); +} + +// ---------------------------------------------------------------------- +// Tests - Configuration +// ---------------------------------------------------------------------- + +void AosDeframerTester::testFecfDisabled() { + // When FECF is disabled, frame size is reduced by trailer size + const U32 frameSizeNoFecf = TEST_FRAME_SIZE - AOSTrailer::SERIALIZED_SIZE; + + // Configure without FECF + this->component.configure(frameSizeNoFecf, false, ComCfg::SpacecraftId, 0); + + U8 payload[100]; + FwSizeType sppSize = this->createSppPacket(payload, 0x200, 50); + + // Assemble frame without CRC + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, 0, 0, 1, false); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + // No FECF error events + ASSERT_EVENTS_InvalidFecf_SIZE(0); +} + +void AosDeframerTester::testPvnMaskSppOnly() { + // Configure for SPP only + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, 0, PvnBitfield::SPP_MASK); + + U8 payload[150]; + FwSizeType offset = 0; + + // SPP packet - should be extracted + offset += this->createSppPacket(payload + offset, 0x201, 20); + + // EPP packet - should be ignored + offset += this->createEppPacket(payload + offset, 0x02, EppLengthOfLength::One, 20); + + // Another SPP + offset += this->createSppPacket(payload + offset, 0x202, 15); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, offset, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + // Only the first SPP extracted; EPP triggers DisabledPvn and stops extraction + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EVENTS_DisabledPvn_SIZE(1); + ASSERT_EVENTS_DisabledPvn(0, 0, ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL); +} + +void AosDeframerTester::testPvnMaskEppOnly() { + // Configure for EPP only + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, 0, PvnBitfield::EPP_MASK); + + U8 payload[100]; + FwSizeType eppSize = this->createEppPacket(payload, EppProtocolId::MissionSpecific, EppLengthOfLength::One, 30); + + Fw::Buffer buffer = this->assembleFrameBuffer(payload, eppSize, 0); + ComCfg::FrameContext context; + + this->invoke_to_dataIn(0, buffer, context); + + ASSERT_from_dataOut_SIZE(1); + this->assertDataOutVcId(0); + ASSERT_EQ(this->fromPortHistory_dataOut->at(0).context.get_pvn(), ComCfg::Pvn::ENCAPSULATION_PACKET_PROTOCOL); +} + +// ---------------------------------------------------------------------- +// Tests - Telemetry +// ---------------------------------------------------------------------- + +void AosDeframerTester::testFrameCountTelemetry() { + const U8 testVcId = 5; + this->component.configure(TEST_FRAME_SIZE, true, ComCfg::SpacecraftId, testVcId, + PvnBitfield::SPP_MASK | PvnBitfield::EPP_MASK); + + U8 payload[50]; + FwSizeType sppSize = this->createSppPacket(payload, 0x300, 20); + + ComCfg::FrameContext context; + + // Send 3 frames with incrementing vcCount to avoid gap detection + for (U32 i = 0; i < 3; i++) { + this->clearHistory(); + Fw::Buffer buffer = this->assembleFrameBuffer(payload, sppSize, 0, ComCfg::SpacecraftId, testVcId, i); + this->invoke_to_dataIn(0, buffer, context); + this->assertDataOutVcId(testVcId); + ASSERT_TLM_FramesProcessed(0, i + 1); + ASSERT_TLM_PacketsExtracted(0, i + 1); + ASSERT_TLM_LatestVcFrameCount(0, i); + } +} + +} // namespace Ccsds + +} // namespace Svc diff --git a/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTester.hpp b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTester.hpp new file mode 100644 index 00000000000..96f43ad7bc1 --- /dev/null +++ b/Svc/Ccsds/AosDeframer/test/ut/AosDeframerTester.hpp @@ -0,0 +1,260 @@ +// ====================================================================== +// \title AosDeframerTester.hpp +// \author Auto-generated +// \brief hpp file for AosDeframer component test harness implementation class +// ====================================================================== + +#ifndef Svc_Ccsds_AosDeframerTester_HPP +#define Svc_Ccsds_AosDeframerTester_HPP + +#include "Svc/Ccsds/AosDeframer/AosDeframer.hpp" +#include "Svc/Ccsds/AosDeframer/AosDeframerGTestBase.hpp" +#include "Svc/Ccsds/Types/EppLengthOfLengthEnumAc.hpp" +#include "Svc/Ccsds/Types/EppProtocolIdEnumAc.hpp" + +namespace Svc { + +namespace Ccsds { + +class AosDeframerTester final : public AosDeframerGTestBase { + public: + // ---------------------------------------------------------------------- + // Constants + // ---------------------------------------------------------------------- + + // Maximum size of histories storing events, telemetry, and port outputs + static const FwSizeType MAX_HISTORY_SIZE = 20; + + // Instance ID supplied to the component instance under test + static const FwEnumStoreType TEST_INSTANCE_ID = 0; + + // Test frame sizes + static const U32 TEST_FRAME_SIZE = 256; + static const U32 TEST_FRAME_SIZE_LARGE = 1024; + static const FwSizeType ALLOC_BUF_SIZE = 65536; + + // Data zone size within TEST_FRAME_SIZE frames (header + M_PDU header + FECF trailer) + static const FwSizeType TEST_DATA_ZONE_SIZE = + TEST_FRAME_SIZE - AOSHeader::SERIALIZED_SIZE - M_PDUHeader::SERIALIZED_SIZE - AOSTrailer::SERIALIZED_SIZE; + + public: + // ---------------------------------------------------------------------- + // Construction and destruction + // ---------------------------------------------------------------------- + + //! Construct object AosDeframerTester + AosDeframerTester(); + + //! Destroy object AosDeframerTester + ~AosDeframerTester(); + + public: + // ---------------------------------------------------------------------- + // Tests - Basic Validation + // ---------------------------------------------------------------------- + + //! Test nominal deframing with single SPP packet + void testNominalDeframing(); + + //! Test data return passthrough + void testDataReturn(); + + //! Test invalid spacecraft ID handling + void testInvalidScId(); + + //! Test invalid virtual channel ID handling + void testInvalidVcId(); + + //! Test invalid frame length handling + void testInvalidFrameLength(); + + //! Test invalid FECF (CRC) handling + void testInvalidFecf(); + + //! Test invalid transfer frame version number + void testInvalidTfvn(); + + //! Test VC frame count gap detection emits event + errorNotify + void testVcFrameCountGap(); + + // Accept-all-VCID is not supported: each VC struct maps to exactly one VCID, + // enabling per-packet spanning state tracking. + + // ---------------------------------------------------------------------- + // Tests - M_PDU Processing + // ---------------------------------------------------------------------- + + //! Test First Header Pointer at non-zero offset + void testFhpAtOffset(); + + //! Test FHP_NO_PACKET_START (0x7FE) - continuation only + void testFhpNoPacketStart(); + + //! Test FHP_IDLE_DATA_ONLY (0x7FF) - idle frame + void testFhpIdleDataOnly(); + + //! Test multiple packets in single frame + void testMultiplePacketsInFrame(); + + // ---------------------------------------------------------------------- + // Tests - Spanning Packets + // ---------------------------------------------------------------------- + + //! Test packet spanning across two frames + void testSpanningPacketTwoFrames(); + + //! Test packet spanning across four frames (explicit 3+ frame coverage) + void testSpanningPacketFourFrames(); + + //! Test spanning packet with continuation frame + void testSpanningPacketContinuation(); + + //! Test spanning packet allocation failure emits an event and drops the packet + void testSpanningPacketAllocFailureEvent(); + + //! Test spanning packet dropped when a VC frame count gap is detected mid-reassembly + void testSpanningPacketAbandonedOnVcGap(); + + //! Test spanning packet dropped when an idle frame arrives mid-reassembly + void testSpanningPacketAbandonedOnIdleFrame(); + + //! Test spanning packet silently dropped when FHP arrives before the packet's expected end + void testSpanningPacketAbandonedOnPrematureFhp(); + + //! Test SPP packet whose header is split across a frame boundary + void testSppHeaderSpansFrame(); + + //! Test EPP packet whose header is split across a frame boundary + void testEppHeaderSpansFrame(); + + //! Test alloc failure for a packet that fits in one frame; next packet still extracted + void testAllocFailureNextPacketExtracted(); + + // ---------------------------------------------------------------------- + // Tests - SPP Extraction + // ---------------------------------------------------------------------- + + //! Test SPP idle packet filtering + void testSppIdlePacketFiltering(); + + // ---------------------------------------------------------------------- + // Tests - EPP Extraction + // ---------------------------------------------------------------------- + + //! Test Encapsulation Packet Protocol extraction + void testEppExtraction(); + + //! Test EPP extraction for all length-of-length variants (lol=1, lol=2, lol=4) + void testEppLengthOfLength(); + + //! Test EPP idle packet handling + void testEppIdlePacket(); + + //! Test EPP fill packet handling + void testEppFillPacket(); + + //! Test invalid packet version + void testInvalidPvnVersion(); + + // ---------------------------------------------------------------------- + // Tests - Configuration + // ---------------------------------------------------------------------- + + //! Test FECF disabled mode + void testFecfDisabled(); + + //! Test PVN mask filtering (SPP only) + void testPvnMaskSppOnly(); + + //! Test PVN mask filtering (EPP only) + void testPvnMaskEppOnly(); + + // ---------------------------------------------------------------------- + // Tests - Telemetry + // ---------------------------------------------------------------------- + + //! Test frame count telemetry + void testFrameCountTelemetry(); + + private: + // ---------------------------------------------------------------------- + // From-port handlers (allocator support for spanning packets) + // ---------------------------------------------------------------------- + + Fw::Buffer from_allocate_handler(FwIndexType portNum, FwSizeType size) override; + + private: + // ---------------------------------------------------------------------- + // Helper functions + // ---------------------------------------------------------------------- + + //! Connect ports + void connectPorts(); + + //! Initialize components + void initComponents(); + + //! Configure the component with default test settings + void configureDefault(); + + //! Assert that all emitted packet contexts carry the expected VCID + void assertDataOutVcId(U8 expectedVcId) const; + + //! Assemble an AOS frame buffer with the given parameters + //! \param payload Pointer to M_PDU payload data + //! \param payloadLength Length of payload data + //! \param fhp First Header Pointer value + //! \param scid Spacecraft ID + //! \param vcid Virtual Channel ID + //! \param vcCount Virtual Channel Frame Count + //! \param tfvn Transfer Frame Version Number + //! \param includeFecf Whether to include FECF + //! \return Assembled frame buffer + Fw::Buffer assembleFrameBuffer(U8* payload, + FwSizeType payloadLength, + U16 fhp = 0, + U16 scid = ComCfg::SpacecraftId, + U8 vcid = 0, + U32 vcCount = 0, + U8 tfvn = 1, + bool includeFecf = true); + + //! Create an SPP packet in the buffer + //! \param buffer Destination buffer + //! \param apid Application Process ID + //! \param dataLength Packet data length (not including header) + //! \param seqCount Sequence count + //! \return Total packet size + FwSizeType createSppPacket(U8* buffer, U16 apid, U16 dataLength, U16 seqCount = 0); + + //! Create an EPP packet in the buffer + //! \param buffer Destination buffer + //! \param protocolId Protocol ID (0 = Idle per EppProtocolId::Idle) + //! \param lengthOfLength EPP length of length enum + //! \param dataLength Packet data length + //! \return Total packet size + FwSizeType createEppPacket(U8* buffer, U8 protocolId, EppLengthOfLength lengthOfLength, FwSizeType dataLength); + + private: + // ---------------------------------------------------------------------- + // Member variables + // ---------------------------------------------------------------------- + + //! The component under test + AosDeframer component; + + //! Data buffer used to produce test frames + U8 m_frameData[ComCfg::AosMaxFrameFixedSize]; + + //! Static backing storage returned by the allocate port in unit tests + U8 m_allocBuf[ALLOC_BUF_SIZE]; + + //! When true, the next allocate call returns an invalid buffer (simulates alloc failure) + bool m_failNextAlloc = false; +}; + +} // namespace Ccsds + +} // namespace Svc + +#endif diff --git a/Svc/Ccsds/CMakeLists.txt b/Svc/Ccsds/CMakeLists.txt index fa3326c2521..991c486ee76 100644 --- a/Svc/Ccsds/CMakeLists.txt +++ b/Svc/Ccsds/CMakeLists.txt @@ -5,4 +5,5 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SpacePacketFramer/") 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}/AosDeframer/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ApidManager/") diff --git a/Svc/Ccsds/Types/Types.fpp b/Svc/Ccsds/Types/Types.fpp index db4761fe1b1..a51f32e26c8 100644 --- a/Svc/Ccsds/Types/Types.fpp +++ b/Svc/Ccsds/Types/Types.fpp @@ -2,12 +2,20 @@ module Svc { module Ccsds { @ Enum representing an error during framing/deframing in the CCSDS protocols + @ TODO: Decide whether AOS errors should be merged with TC errors into a unified enum enum FrameError: U8 { SP_INVALID_LENGTH = 0 TC_INVALID_SCID = 1 TC_INVALID_LENGTH = 2 TC_INVALID_VCID = 3 TC_INVALID_CRC = 4 + AOS_INVALID_SCID = 5 @< CCSDS 732.0-B-5: Spacecraft ID mismatch (4.1.2.2) + AOS_INVALID_LENGTH = 6 @< CCSDS 732.0-B-5: Frame length insufficient + AOS_INVALID_VCID = 7 @< CCSDS 732.0-B-5: Virtual Channel ID mismatch (4.1.2.3) + AOS_INVALID_CRC = 8 @< CCSDS 732.0-B-5: Frame Error Control Field CRC mismatch (4.1.6) + AOS_INVALID_VERSION = 9 @< CCSDS 732.0-B-5: Transfer Frame Version Number mismatch (4.1.2.2.2) + AOS_INVALID_EPP = 10 @< CCSDS 133.1-B-3: Encapsulation Packet Protocol error + AOS_VC_FRAME_COUNT_GAP = 11 @< CCSDS 732.0-B-5: AOS VC frame count discontinuity detected } # ------------------------------------------------ @@ -38,6 +46,36 @@ module Ccsds { constant SeqCountWidth = 14 } + + # ------------------------------------------------ + # Encapsulation Packet Protocol + # ------------------------------------------------ + @ Bit masks and offsets for Encapsulation Packet Protocol first byte + @ Per CCSDS 133.1-B-3 section 4.1.2.1 + module EPPSubfields { + # First octet masks (8 bits) + constant packetVersionMask = 0xE0 @< 0b11100000 - bits [7:5] + constant protocolIdMask = 0x1C @< 0b00011100 - bits [4:2] + constant lengthOfLengthMask = 0x03 @< 0b00000011 - bits [1:0] + + constant packetVersionOffset = 5 + constant protocolIdOffset = 2 + } + + @ Protocol IDs for EPP encapsulation packets per CCSDS 133.1-B-3 Section 4.1.2.3.3 + dictionary enum EppProtocolId : U8 { + Idle = 0x00 @< 0b000 - Encapsulation Idle Packet + Extended = 0x06 @< 0b110 - Extended Protocol ID Field Driven + MissionSpecific = 0x07 @< Mission-specific + } default MissionSpecific + + dictionary enum EppLengthOfLength : U8 { + Zero = 0x00 @< 0b00 - Single Byte Idle Packet + One = 0x01 @< 0b01 - Two Byte Header + Two = 0x02 @< 0b10 - Four Byte Header + Four = 0x03 @< 0b11 - Eight Byte Header + } + # ------------------------------------------------ # TC # ------------------------------------------------ @@ -98,25 +136,45 @@ module Ccsds { @< 1 bit replay flag | 1 bit VC frame count cycle flag | 2 most significant bits of spacecraft ID | 4 bits VC frame count cycle } - @ Offsets for serializing individual sub-fields in AOS headers + @ Offsets and masks for deserializing individual sub-fields in AOS headers + @ Per CCSDS 732.0-B-5 Section 4.1.2 module AOSHeaderSubfields { - # globalVcId offsets + # globalVcId offsets and masks (16 bits) constant frameVersionOffset = 14 constant spacecraftIdLsbOffset = 6 constant virtualChannelIdOffset = 0 - # signaling field offsets + constant frameVersionMask = 0xC000 @< 0b1100000000000000 - bits [15:14] + constant spacecraftIdLsbMask = 0x3FC0 @< 0b0011111111000000 - bits [13:6] + constant virtualChannelIdMask = 0x003F @< 0b0000000000111111 - bits [5:0] + + # signaling field offsets and masks (lower 8 bits of frameCountAndSignaling) constant vcFrameCountOffset = 8 constant replayFlagOffset = 7 constant cycleCountFlagOffset = 6 constant spacecraftIdMsbOffset = 4 + constant vcFrameCountCycleOffset = 0 + + constant vcFrameCountMask = 0xFFFFFF00 @< 24 bits of frame count [31:8] + constant replayFlagMask = 0x00000080 @< 0b10000000 - bit [7] + constant cycleCountFlagMask = 0x00000040 @< 0b01000000 - bit [6] + constant spacecraftIdMsbMask = 0x00000030 @< 0b00110000 - bits [5:4] + constant vcFrameCountCycleMask = 0x0000000F @< 0b00001111 - bits [3:0] + } + + @ Special values for AOS M_PDU First Header Pointer + @ Per CCSDS 732.0-B-5 Section 4.1.4.2.2 + module M_PDUSubfields { + # Special First Header Pointer values per CCSDS 732.0-B-5 Section 4.1.4.2.2.4 & 4.1.4.2.2.5 + constant FHP_NO_PACKET_START = 0xFFFF @< No packet starts in this frame + constant FHP_IDLE_DATA_ONLY = 0xFFFE @< Frame contains only idle data } @ Describes the header format for a Advanced Orbiting Systems (AOS) Space Data Link (SDL) multiplex protocol data unit (M_PDU) struct M_PDUHeader { firstHeaderPointer: U16 @< bytes to the header of the first new CCSDS Packet } default { - firstHeaderPointer = 0xFFFF # Set first header pointer to all ones to mean no packet starts here (4.1.4.2.2.4) + firstHeaderPointer = M_PDUSubfields.FHP_NO_PACKET_START # Set first header pointer to all ones to mean no packet starts here (4.1.4.2.2.4) } @ Describes the frame trailer format for a Advanced Orbiting Systems (AOS) Space Data Link (SDL) Transfer Frame @@ -124,10 +182,17 @@ module Ccsds { fecf: U16 @< 16 bit Frame Error Control Field (CRC16) } + # ------------------------------------------------ + # CCSDS Enums + # ------------------------------------------------ + @ Bitmask of enabled Packet Version Numbers (PVN) + @ Each bit position corresponds to a PVN value (bit N set = PVN N is enabled) + @ Used to selectively enable one or multiple protocols for Aos Framers/Deframers + @ SPP PVN = 0, EPP PVN = 7 per CCSDS 133.0-B-2 / 133.1-B-3 module PvnBitfield { - constant SPP_MASK = 0x1 @< 1 << 0x0 - constant EPP_MASK = 0x8 @< 1 << 0x3 - constant VALID_MASK = SPP_MASK + EPP_MASK + constant SPP_MASK = 0x01 @< 1 << 0 (SPP PVN = 0) + constant EPP_MASK = 0x80 @< 1 << 7 (EPP PVN = 7) + constant VALID_MASK = 0x81 @< SPP_MASK | EPP_MASK } @ Transfer Frame Version Numbers are 4 bits long diff --git a/default/config/ComCfg.fpp b/default/config/ComCfg.fpp index f782ba2d2e5..89d12548c15 100644 --- a/default/config/ComCfg.fpp +++ b/default/config/ComCfg.fpp @@ -20,6 +20,13 @@ module ComCfg { @ Aggregation buffer for ComAggregator component constant AggregationSize = TmFrameFixedSize - 6 - 6 - 1 - 2 # 2 header (6) + 1 idle byte + 2 trailer bytes + @ Packet Version Numbers are 3 bits with only 2 currently valid values + dictionary enum Pvn : U8 { + SPACE_PACKET_PROTOCOL = 0x0 @< Fully Featured CCSDS Space Packet Protocol + ENCAPSULATION_PACKET_PROTOCOL = 0x7 @< Bare-bones CCSDS Encapsulation Packet Protocol + INVALID_UNINITIALIZED = 0x8 @< Anything equal or higher value is invalid and should not be used + } default INVALID_UNINITIALIZED + @ APIDs are 11 bits in the Space Packet protocol, so we use U16. Max value 7FF dictionary enum Apid : FwPacketDescriptorType { # APIDs prefixed with FW are reserved for F Prime and need to be present @@ -43,6 +50,7 @@ module ComCfg { apid: Apid @< 11 bits APID in CCSDS sequenceCount: U16 @< 14 bit Sequence count - sequence count is incremented per APID vcId: U8 @< 6 bit Virtual Channel ID - used for AOS, TC, and TM Protocols + pvn: Pvn @< Packet Version Number - used for AOS deframing to identify packet type sendNow: bool @< Flag to AOS Framer that the Frame this packet goes into should be sent ASAP } default { @@ -50,6 +58,7 @@ module ComCfg { apid = Apid.FW_PACKET_UNKNOWN sequenceCount = 0 vcId = 1 + pvn = Pvn.INVALID_UNINITIALIZED sendNow = false }