GCC Code Coverage Report


Directory: lib/fprime-samd/fprime-samd/
File: Svc/TmFramer/TmFramer.cpp
Date: 2026-09-26 00:05:09
Exec Total Coverage
Lines: 0 163 0.0%
Functions: 0 11 0.0%
Branches: 0 222 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title TmFramer.cpp
3 // \brief cpp file for CCSDS TM Transfer Frame downlink framer implementation class
4 // ======================================================================
5
6 #include "fprime-samd/Svc/TmFramer/TmFramer.hpp"
7 #include "Drv/ByteStreamDriverModel/ByteStreamStatusEnumAc.hpp"
8 #include "Fw/Com/ComBuffer.hpp"
9 #include "Fw/Types/Assert.hpp"
10 #include "Svc/Ccsds/Utils/CRC16.hpp"
11 #include "samd-config/FramerConfig.hpp"
12
13 namespace Samd21 {
14
15 // Out-of-class definition required pre-C++17 for any ODR-use of a static constexpr member
16 constexpr U8 TmFramer::IDLE_DATA_PATTERN;
17
18 static_assert(ComCfg::TmFrameFixedSize > Svc::Ccsds::TMHeader::SERIALIZED_SIZE + Svc::Ccsds::TMTrailer::SERIALIZED_SIZE,
19 "ComCfg::TmFrameFixedSize too small to hold TMHeader + TMTrailer");
20
21 static constexpr FwSizeType TM_DATA_FIELD_CAPACITY =
22 ComCfg::TmFrameFixedSize - (Svc::Ccsds::TMHeader::SERIALIZED_SIZE + Svc::Ccsds::TMTrailer::SERIALIZED_SIZE);
23
24 static constexpr FwSizeType IDLE_SPACE_PACKET_MIN_SIZE = Svc::Ccsds::SpacePacketHeader::SERIALIZED_SIZE + 1;
25 static constexpr FwSizeType MAX_SINGLE_SPACE_PACKET_SIZE =
26 Svc::Ccsds::SpacePacketHeader::SERIALIZED_SIZE + FW_COM_BUFFER_MAX_SIZE;
27 static_assert(TM_DATA_FIELD_CAPACITY < MAX_SINGLE_SPACE_PACKET_SIZE ||
28 (TM_DATA_FIELD_CAPACITY - MAX_SINGLE_SPACE_PACKET_SIZE) == 0 ||
29 (TM_DATA_FIELD_CAPACITY - MAX_SINGLE_SPACE_PACKET_SIZE) >= IDLE_SPACE_PACKET_MIN_SIZE,
30 "FW_COM_BUFFER_MAX_SIZE vs ComCfg::TmFrameFixedSize can leave an unfillable "
31 "single-packet remainder on an empty TM frame buffer");
32
33 // ----------------------------------------------------------------------
34 // Component construction and destruction
35 // ----------------------------------------------------------------------
36
37 ✗ TmFramer ::TmFramer(const char* const compName)
38 : TmFramerComponentBase(compName),
39 ✗ m_driverConnected(false),
40 ✗ m_activeBufferIdx(0),
41 ✗ m_droppedPackets(0),
42 ✗ m_apidOverflowCount(0),
43 ✗ m_numApidsTracked(0),
44 ✗ m_masterFrameCount(0),
45 ✗ m_virtualFrameCount(0) {
46 ✗ for (FwIndexType i = 0; i < 2; i++) {
47 ✗ m_buffers[i].dataFieldSize = 0;
48 ✗ m_buffers[i].state = IDLE;
49 }
50 ✗ }
51
52 ✗ TmFramer ::~TmFramer() {}
53
54 // ----------------------------------------------------------------------
55 // Handler implementations for typed input ports
56 // ----------------------------------------------------------------------
57
58 // Unlike stock Svc::Ccsds::TmFramer (one buffer per frame), this component accumulates
59 // multiple Space Packets per frame
60
61 ✗ void TmFramer ::comPacketQueueIn_handler(FwIndexType portNum, Fw::ComBuffer& data, U32 context) {
62 ✗ TxBuffer* activeBuf = &m_buffers[m_activeBufferIdx];
63
64 // Space Packet header (6B) + payload, no idle padding included yet.
65 ✗ FwSizeType spaceNeeded = Svc::Ccsds::SpacePacketHeader::SERIALIZED_SIZE + data.getSize();
66
67 // If both buffers are in flight (backpressure), drop this packet
68 ✗ if (activeBuf->state == TRANSMITTING) {
69 ✗ this->m_droppedPackets++;
70 ✗ return;
71 }
72
73 // Flush first if adding this Space Packet would overflow the data field, OR would fit but
74 // leave a remainder too small for closeFrame()'s idle pad (the dead zone described above).
75 ✗ FwSizeType afterAppend = activeBuf->dataFieldSize + spaceNeeded;
76 ✗ bool wouldOverflow = afterAppend > TM_DATA_FIELD_CAPACITY;
77 ✗ bool wouldLeaveDeadZone = false;
78 ✗ if (!wouldOverflow) {
79 ✗ FwSizeType remainderAfterAppend = TM_DATA_FIELD_CAPACITY - afterAppend;
80 ✗ wouldLeaveDeadZone = remainderAfterAppend > 0 && remainderAfterAppend < IDLE_SPACE_PACKET_MIN_SIZE;
81 }
82
83 ✗ if (wouldOverflow || wouldLeaveDeadZone) {
84 ✗ flushActiveBuffer();
85
86 // flushActiveBuffer() swapped the active buffer -- re-acquire it
87 ✗ activeBuf = &m_buffers[m_activeBufferIdx];
88
89 // If both buffers are in flight (backpressure), drop this packet
90 ✗ if (activeBuf->state == TRANSMITTING) {
91 ✗ this->m_droppedPackets++;
92 ✗ return;
93 }
94
95 ✗ FW_ASSERT(activeBuf->state == IDLE);
96 ✗ FW_ASSERT(activeBuf->dataFieldSize == 0);
97 }
98
99 // A single ComBuffer + Space Packet header must never exceed the entire data field
100 // capacity -- otherwise it could never fit in any frame regardless of flushing.
101 ✗ FW_ASSERT(spaceNeeded <= TM_DATA_FIELD_CAPACITY, static_cast<FwAssertArgType>(spaceNeeded));
102
103 ✗ if (activeBuf->state == IDLE) {
104 ✗ activeBuf->state = ACTIVE;
105 }
106
107 // Data field starts after the (not-yet-written) TMHeader; append at that offset.
108 ✗ U8* dataFieldStart = activeBuf->data + Svc::Ccsds::TMHeader::SERIALIZED_SIZE;
109 ✗ FwSizeType written = appendSpacePacket(dataFieldStart + activeBuf->dataFieldSize,
110 ✗ TM_DATA_FIELD_CAPACITY - activeBuf->dataFieldSize, data);
111 ✗ activeBuf->dataFieldSize += written;
112 ✗ FW_ASSERT(activeBuf->dataFieldSize <= TM_DATA_FIELD_CAPACITY,
113 static_cast<FwAssertArgType>(activeBuf->dataFieldSize));
114
115 // dataFieldSize must end up either exactly full or with room for a valid idle pad.
116 ✗ FwSizeType finalRemainder = TM_DATA_FIELD_CAPACITY - activeBuf->dataFieldSize;
117 ✗ FW_ASSERT(finalRemainder == 0 || finalRemainder >= IDLE_SPACE_PACKET_MIN_SIZE,
118 static_cast<FwAssertArgType>(finalRemainder));
119 }
120
121 ✗ ComCfg::Apid TmFramer ::apidForComBuffer(const Fw::ComBuffer& data) const {
122 // Every ComBuffer from Fw::TlmPacket/Fw::LogPacket leads with a FwPacketDescriptorType
123 // (Fw::ComPacket::serializeBase()) that is bit-identical to the ComCfg::Apid value we
124 // want. Peek it via a temporary ExternalSerializeBuffer over the same memory so `data`'s
125 // own cursor is untouched -- setBuffLen() marks its bytes readable without copying.
126 ✗ Fw::ExternalSerializeBuffer peek(const_cast<U8*>(data.getBuffAddr()), data.getSize());
127 ✗ Fw::SerializeStatus status = peek.setBuffLen(data.getSize());
128 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
129 ✗ FwPacketDescriptorType descriptor = 0;
130 ✗ status = peek.deserializeTo(descriptor);
131 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
132 ✗ return static_cast<ComCfg::Apid>(descriptor);
133 ✗ }
134
135 ✗ U16 TmFramer ::nextApidSequenceCount(ComCfg::Apid apid) {
136 ✗ FwIndexType slotIdx = -1;
137 ✗ for (FwIndexType i = 0; i < this->m_numApidsTracked; i++) {
138 ✗ if (this->m_apidSequenceCounts[i].apid == apid) {
139 ✗ slotIdx = i;
140 ✗ break;
141 }
142 }
143
144 ✗ if (slotIdx == -1) {
145 // First time seeing this APID -- claim the next free slot if one is available. If
146 // every downlink packet source is accounted for in Samd21::FramerConfig::MAX_TRACKED_APIDS,
147 // This path must stay non-fatal because it can be reached while handling a
148 // fatal/assert packet, and a crash here would prevent that packet from ever being sent.
149 ✗ if (this->m_numApidsTracked >= Samd21::FramerConfig::MAX_TRACKED_APIDS) {
150 ✗ this->m_apidOverflowCount++;
151 ✗ return 0;
152 }
153 ✗ slotIdx = this->m_numApidsTracked;
154 ✗ this->m_apidSequenceCounts[slotIdx].apid = apid;
155 ✗ this->m_apidSequenceCounts[slotIdx].count = 0;
156 ✗ this->m_numApidsTracked++;
157 }
158
159 ✗ U16 count = this->m_apidSequenceCounts[slotIdx].count;
160 // Sequence count is 14 bits, mod-16384 per APID (CCSDS 133.0-B-2 4.1.3.4).
161 ✗ this->m_apidSequenceCounts[slotIdx].count =
162 ✗ static_cast<U16>((count + 1) & Svc::Ccsds::SpacePacketSubfields::SeqCountMask);
163 ✗ return count;
164 }
165
166 ✗ FwSizeType TmFramer ::appendSpacePacket(U8* dest, FwSizeType destCapacity, Fw::ComBuffer& data) {
167 ✗ FW_ASSERT(destCapacity >= Svc::Ccsds::SpacePacketHeader::SERIALIZED_SIZE + data.getSize());
168
169 ✗ Fw::Buffer bufWrapper(dest, destCapacity);
170 ✗ Fw::ExternalSerializeBuffer serializer(bufWrapper.getData(), bufWrapper.getSize());
171
172 // Space Packet primary header (CCSDS 133.0-B-2): Packet Type = 0 (TM), Secondary Header
173 // Flag = 0, Sequence Flags = 0b11 (unsegmented) for every packet this framer builds.
174 ✗ ComCfg::Apid apid = apidForComBuffer(data);
175
176 ✗ Svc::Ccsds::SpacePacketHeader header;
177 ✗ U16 packetIdentification = static_cast<U16>(apid) & Svc::Ccsds::SpacePacketSubfields::ApidMask;
178 ✗ header.set_packetIdentification(packetIdentification);
179
180 ✗ U16 packetSequenceControl = static_cast<U16>(0x3 << Svc::Ccsds::SpacePacketSubfields::SeqFlagsOffset);
181 ✗ packetSequenceControl |= (nextApidSequenceCount(apid) & Svc::Ccsds::SpacePacketSubfields::SeqCountMask);
182 ✗ header.set_packetSequenceControl(packetSequenceControl);
183
184 // Packet Data Length = size of user data field minus one (CCSDS 133.0-B-2 4.1.3.5.3)
185 ✗ FW_ASSERT(data.getSize() > 0, static_cast<FwAssertArgType>(data.getSize()));
186 ✗ header.set_packetDataLength(static_cast<U16>(data.getSize() - 1));
187
188 ✗ Fw::SerializeStatus status = serializer.serializeFrom(header);
189 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
190
191 ✗ status = serializer.serializeFrom(data.getBuffAddr(), data.getSize(), Fw::Serialization::OMIT_LENGTH);
192 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status, data.getSize());
193
194 ✗ return serializer.getSize();
195 ✗ }
196
197 ✗ void TmFramer ::drvConnected_handler(FwIndexType portNum) {
198 ✗ this->m_driverConnected = true;
199 ✗ }
200
201 ✗ void TmFramer ::drvReturnIn_handler(FwIndexType portNum, Fw::Buffer& fwBuffer, const Drv::ByteStreamStatus& status) {
202 ✗ for (FwIndexType i = 0; i < 2; i++) {
203 ✗ if (m_buffers[i].state == TRANSMITTING && fwBuffer.getData() == m_buffers[i].data) {
204 ✗ m_buffers[i].state = IDLE;
205 ✗ m_buffers[i].dataFieldSize = 0;
206 ✗ return;
207 }
208 }
209 ✗ FW_ASSERT(false); // Buffer returned that we didn't send
210 }
211
212 ✗ void TmFramer ::closeFrame(U8* frameData, FwSizeType dataFieldUsed) {
213 ✗ U8* dataField = frameData + Svc::Ccsds::TMHeader::SERIALIZED_SIZE;
214
215 // Pad the remainder of the data field with a CCSDS idle Space Packet
216 // (CCSDS 132.0-B-3 4.2.2.5).
217 ✗ FwSizeType remaining = TM_DATA_FIELD_CAPACITY - dataFieldUsed;
218 ✗ if (remaining > 0) {
219 ✗ FW_ASSERT(remaining >= Svc::Ccsds::SpacePacketHeader::SERIALIZED_SIZE + 1,
220 static_cast<FwAssertArgType>(remaining));
221
222 ✗ Fw::Buffer idleWrapper(dataField + dataFieldUsed, remaining);
223 ✗ Fw::ExternalSerializeBuffer idleSerializer(idleWrapper.getData(), idleWrapper.getSize());
224
225 ✗ Svc::Ccsds::SpacePacketHeader idleHeader;
226 ✗ idleHeader.set_packetIdentification(static_cast<U16>(ComCfg::Apid::SPP_IDLE_PACKET) &
227 Svc::Ccsds::SpacePacketSubfields::ApidMask);
228 ✗ idleHeader.set_packetSequenceControl(static_cast<U16>(0x3 << Svc::Ccsds::SpacePacketSubfields::SeqFlagsOffset));
229 ✗ FwSizeType idleDataLen = remaining - Svc::Ccsds::SpacePacketHeader::SERIALIZED_SIZE;
230 ✗ idleHeader.set_packetDataLength(static_cast<U16>(idleDataLen - 1));
231
232 ✗ Fw::SerializeStatus status = idleSerializer.serializeFrom(idleHeader);
233 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
234 ✗ for (FwSizeType i = 0; i < idleDataLen; i++) {
235 ✗ status = idleSerializer.serializeFrom(IDLE_DATA_PATTERN);
236 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
237 }
238 ✗ }
239
240 // -----------------------------------------------
241 // Header (TM Transfer Frame Primary Header, CCSDS 132.0-B-3 4.1.2)
242 // -----------------------------------------------
243 ✗ Svc::Ccsds::TMHeader header;
244 ✗ U16 globalVcId = static_cast<U16>(
245 1 << Svc::Ccsds::TMSubfields::virtualChannelIdOffset); // VCID=1, this project's downlink virtual channel
246 ✗ globalVcId |= static_cast<U16>(ComCfg::SpacecraftId << Svc::Ccsds::TMSubfields::spacecraftIdOffset);
247 ✗ globalVcId |= 0x0; // Operational Control Field: Flag = 0 (no OCF present)
248
249 ✗ U16 dataFieldStatus = static_cast<U16>(0x3 << Svc::Ccsds::TMSubfields::segLengthOffset); // Seg Length Id = 0b11
250 // First Header Pointer = 0: the first Space Packet in every frame starts at data-field
251 // offset 0 (this framer never spans a packet across frames).
252
253 ✗ header.set_globalVcId(globalVcId);
254 ✗ header.set_masterFrameCount(this->m_masterFrameCount);
255 ✗ header.set_virtualFrameCount(this->m_virtualFrameCount);
256 ✗ header.set_dataFieldStatus(dataFieldStatus);
257
258 // Single Master Channel / Virtual Channel in this deployment, so both counters
259 // track together.
260 ✗ this->m_masterFrameCount++; // U8 wraps mod-256
261 ✗ this->m_virtualFrameCount++; // U8 wraps mod-256
262
263 ✗ Fw::Buffer headerWrapper(frameData, Svc::Ccsds::TMHeader::SERIALIZED_SIZE);
264 ✗ Fw::ExternalSerializeBuffer headerSerializer(headerWrapper.getData(), headerWrapper.getSize());
265 ✗ Fw::SerializeStatus status = headerSerializer.serializeFrom(header);
266 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
267
268 // -----------------------------------------------
269 // Trailer (FECF, CRC-16/CCITT-FALSE per CCSDS 132.0-B-3 4.1.6)
270 // -----------------------------------------------
271 U16 crc =
272 ✗ Svc::Ccsds::Utils::CRC16::compute(frameData, ComCfg::TmFrameFixedSize - Svc::Ccsds::TMTrailer::SERIALIZED_SIZE);
273 ✗ Svc::Ccsds::TMTrailer trailer;
274 ✗ trailer.set_fecf(crc);
275
276 ✗ Fw::Buffer trailerWrapper(frameData + ComCfg::TmFrameFixedSize - Svc::Ccsds::TMTrailer::SERIALIZED_SIZE,
277 ✗ Svc::Ccsds::TMTrailer::SERIALIZED_SIZE);
278 ✗ Fw::ExternalSerializeBuffer trailerSerializer(trailerWrapper.getData(), trailerWrapper.getSize());
279 ✗ status = trailerSerializer.serializeFrom(trailer);
280 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
281 ✗ }
282
283 ✗ void TmFramer ::flushActiveBuffer() {
284 ✗ FW_ASSERT(this->m_driverConnected);
285
286 ✗ TxBuffer& activeBuf = m_buffers[m_activeBufferIdx];
287
288 // Nothing to flush if the buffer hasn't been touched since its last return.
289 ✗ if (activeBuf.state == IDLE) {
290 ✗ return;
291 }
292
293 ✗ FW_ASSERT(activeBuf.state == ACTIVE);
294 ✗ FW_ASSERT(activeBuf.dataFieldSize > 0);
295
296 ✗ closeFrame(activeBuf.data, activeBuf.dataFieldSize);
297
298 ✗ activeBuf.state = TRANSMITTING;
299 ✗ m_activeBufferIdx = (m_activeBufferIdx + 1) % 2;
300
301 ✗ Fw::Buffer txBuffer(activeBuf.data, ComCfg::TmFrameFixedSize);
302 ✗ drvSendOut_out(0, txBuffer);
303 ✗ }
304
305 ✗ void TmFramer ::schedIn_handler(FwIndexType portNum, U32 context) {
306 ✗ if (this->m_driverConnected) {
307 ✗ FwIndexType nextBufferIdx = (m_activeBufferIdx + 1) % 2;
308 ✗ TxBuffer& nextBuf = m_buffers[nextBufferIdx];
309
310 ✗ this->tlmWrite_DroppedPackets(this->m_droppedPackets);
311 ✗ this->tlmWrite_ApidOverflowCount(this->m_apidOverflowCount);
312
313 ✗ if (nextBuf.state == TRANSMITTING) {
314 // Can't flush because both buffers would be in flight
315 ✗ return;
316 }
317
318 ✗ flushActiveBuffer();
319 }
320 }
321
322 } // namespace Samd21
323