GCC Code Coverage Report


Directory: /home/runner/work/fprime/fprime/Svc/Ccsds/AosFramer/
File: AosFramer.cpp
Date: 2026-09-23 22:12:35
Exec Total Coverage
Lines: 0 189 0.0%
Functions: 0 17 0.0%
Branches: 0 116 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title AosFramer.cpp
3 // \author Will MacCormack (Aos Modifications)
4 // \brief cpp file for AosFramer component implementation class
5 // \details modified from thomas-bc's TmFramer
6 // ======================================================================
7
8 #include "Svc/Ccsds/AosFramer/AosFramer.hpp"
9 #include "Svc/Ccsds/Utils/CRC16.hpp"
10 #include "config/FppConstantsAc.hpp"
11
12 namespace Svc {
13
14 namespace Ccsds {
15 // ----------------------------------------------------------------------
16 // Component construction and destruction
17 // ----------------------------------------------------------------------
18
19 ✗ AosFramer ::AosFramer(const char* const compName) : AosFramerComponentBase(compName) {
20 // Default to FECF on, Max Sized if you don't override w/ another configure call
21 ✗ configure(ComCfg::AosMaxFrameFixedSize, true);
22 ✗ }
23
24 ✗ AosFramer ::~AosFramer() {}
25
26 ✗ void AosFramer::configure(const U32 fixedFrameSize,
27 const bool frameErrorControlField,
28 const U16 spacecraftId,
29 const U8 vcId,
30 const U8 idlePvns) {
31 // fixedFrameSize must be less than or equal to the maximum defined in ComCfg.fpp
32 ✗ FW_ASSERT(fixedFrameSize <= ComCfg::AosMaxFrameFixedSize, static_cast<FwAssertArgType>(fixedFrameSize));
33
34 // AOS Frame Fixed Size must be at least large enough to hold header, trailer and data
35 ✗ FW_ASSERT(fixedFrameSize > AOSHeader::SERIALIZED_SIZE + M_PDUHeader::SERIALIZED_SIZE +
36 (frameErrorControlField ? AOSTrailer::SERIALIZED_SIZE : 0),
37 static_cast<FwAssertArgType>(fixedFrameSize));
38
39 // Spacecraft ID is 10 bits (per CCSDS 732.0-B-5 Section 4.1.2.2)
40 ✗ FW_ASSERT((spacecraftId & 0xFC00) == 0, static_cast<FwAssertArgType>(spacecraftId));
41
42 // Virtual Channel ID is 6 bits (per CCSDS 732.0-B-5 Section 4.1.2.3)
43 ✗ FW_ASSERT((vcId & 0xC0) == 0, static_cast<FwAssertArgType>(vcId));
44
45 // AOS Framer must be provided with a protocol to use for Idle Packets
46 // Currently, only SPP idle packing is supported
47 // EPP is more optimal, however
48 ✗ FW_ASSERT((idlePvns & PvnBitfield::SPP_MASK) != 0, static_cast<FwAssertArgType>(idlePvns));
49
50 // FECF is constant for a given Physical Channel during a Mission Phase (4.1.6.1.3)
51 ✗ this->m_fecf = frameErrorControlField;
52 ✗ this->m_spacecraftId = spacecraftId;
53
54 // For each vc, init the buffer objects
55 ✗ for (U8 ind = 0; ind < sizeof(this->m_vcs) / sizeof(AosVc); ind++) {
56 ✗ AosVc& currentVc = this->m_vcs[ind];
57
58 // Write the index for mapping a vc struct onto an output port
59 ✗ currentVc.vc_struct_index = ind;
60 ✗ currentVc.virtualChannelId = vcId;
61 ✗ currentVc.frame.buffer = {currentVc.frame.backer, fixedFrameSize};
62 // Set the bitmask of PVNs to use for idle packets
63 ✗ currentVc.idle_packet_types = idlePvns;
64 }
65 ✗ }
66
67 // ----------------------------------------------------------------------
68 // Handler implementations for typed input ports
69 // ----------------------------------------------------------------------
70
71 ✗ void AosFramer ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, const ComCfg::FrameContext& context) {
72 // Get the VC Struct for this buffer
73 ✗ const U8 vcId = context.get_vcId();
74 ✗ AosVc& currentVc = this->get_vc_struct(context);
75 ✗ FW_ASSERT(currentVc.virtualChannelId == vcId);
76
77 // Ensure rest of the com stack is complying with the [communications adapter
78 // interface](docs/reference/communication-adapter-interface.md)
79 ✗ FW_ASSERT(currentVc.frame.state == BufferOwnershipState::OWNED,
80 static_cast<FwAssertArgType>(currentVc.frame.state));
81
82 // AOS Header & M_PDU Header
83 // If this is the first fresh packet, set the M_PDU firstHeaderPointer
84 ✗ if (!currentVc.past_first_fresh_packet) {
85 // setup our headers if we have a packet + context ready to go
86 ✗ setup_header(context);
87 ✗ setup_m_pdu_header(context);
88
89 // We now will only work with fresh packets for the rest of this frame
90 ✗ currentVc.past_first_fresh_packet = true;
91 }
92
93 // Pack this packet
94 ✗ pack_pad_send(data, context);
95 ✗ }
96
97 ✗ void AosFramer::compute_and_inject_fecf(AosVc& currentVc) {
98 // -------------------------------------------------
99 // Trailer (CRC)
100 // -------------------------------------------------
101 ✗ AOSTrailer trailer;
102
103 ✗ const FwSizeType fecfStart = currentVc.frame.buffer.getSize() - AOSTrailer::SERIALIZED_SIZE;
104 // Compute CRC over the entire frame buffer minus the FECF trailer (Standard 4.1.6)
105 ✗ U16 crc = Ccsds::Utils::CRC16::compute(currentVc.frame.buffer.getData(), static_cast<U32>(fecfStart));
106 // Set the Frame Error Control Field (FECF)
107 ✗ trailer.set_fecf(crc);
108 // Move the serializer pointer to the end of the location where the trailer will be
109 // serialized
110 ✗ auto frameSerializer = currentVc.frame.buffer.getSerializer();
111 ✗ Fw::SerializeStatus status = frameSerializer.moveSerToOffset(fecfStart);
112 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
113
114 ✗ status = frameSerializer.serializeFrom(trailer);
115 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
116 ✗ }
117
118 ✗ void AosFramer ::comStatusIn_handler(FwIndexType portNum, Fw::Success& condition) {
119 // We just ask upstream for more packets
120 // comQueue decides to which VCs to allocate comStatus success
121 ✗ if (this->isConnected_comStatusOut_OutputPort(portNum)) {
122 // Forward the comStatus upstream
123 ✗ this->comStatusOut_out(portNum, condition);
124 }
125 ✗ }
126
127 ✗ void AosFramer ::dataReturnIn_handler(FwIndexType portNum,
128 Fw::Buffer& frameBuffer,
129 const ComCfg::FrameContext& context) {
130 // Get the VC Struct for this buffer
131 ✗ AosVc& currentVc = this->get_vc_struct(context);
132
133 // Assert that the returned buffer is the member, and set ownership state
134 ✗ FW_ASSERT(buffer_belongs(frameBuffer, currentVc.frame.backer, sizeof(currentVc.frame.backer)));
135 ✗ currentVc.frame.state = BufferOwnershipState::OWNED;
136
137 // If we have an outstanding packet from the prior frame, pack it
138 ✗ if (currentVc.outstanding.packet.isValid()) {
139 ✗ this->pack_pad_send(currentVc.outstanding.packet, currentVc.outstanding.context, currentVc.outstanding.offset);
140 }
141 ✗ }
142
143 ✗ AosFramer::AosVc& AosFramer ::get_vc_struct(const ComCfg::FrameContext& context) {
144 // MultiVc support would require looking up a vc struct ind given the vcIndex
145 // (unless you force them to be continuous and start at zero)
146 ✗ const U8 ind = 0;
147 ✗ AosVc& currentVc = this->m_vcs[ind];
148
149 // Ensure configure was called
150 ✗ FW_ASSERT(currentVc.vc_struct_index == ind);
151 ✗ FW_ASSERT(currentVc.virtualChannelId == context.get_vcId());
152 ✗ return currentVc;
153 }
154
155 ✗ void AosFramer ::setup_header(const ComCfg::FrameContext& context) {
156 // Get the VC Struct for this vc
157 ✗ AosVc& currentVc = this->get_vc_struct(context);
158
159 // -----------------------------------------------
160 // Header
161 // -----------------------------------------------
162 ✗ AOSHeader header;
163
164 // GVCID (Global Virtual Channel ID) (Standard 4.1.2.2 and 4.1.2.3)
165 ✗ U16 globalVcId = static_cast<U16>((context.get_vcId() << AOSHeaderSubfields::virtualChannelIdOffset) |
166 ✗ ((m_spacecraftId & 0x00FF) << AOSHeaderSubfields::spacecraftIdLsbOffset) |
167 ✗ ((Tfvn::AOS & 0x3) << AOSHeaderSubfields::frameVersionOffset));
168
169 // Virtual Channel Frame Count (4.1.2.4)
170 ✗ U32 frameCountAndSignaling = static_cast<U32>((currentVc.virtualFrameCount & 0x00FFFFFFU)
171 << static_cast<FwSizeType>(AOSHeaderSubfields::vcFrameCountOffset));
172
173 // Virtual Channel Frame Count Cycle Use Flag (4.1.2.5.3)
174 ✗ frameCountAndSignaling |= static_cast<U32>(1 << AOSHeaderSubfields::cycleCountFlagOffset);
175
176 // Spacecraft ID MSB (4.1.2.5.4)
177 ✗ frameCountAndSignaling |=
178 ✗ static_cast<U32>((m_spacecraftId & 0x0300) >> (8 - AOSHeaderSubfields::spacecraftIdMsbOffset));
179
180 // Virtual Channel Frame Cycle Count (4.1.2.5.5)
181 ✗ frameCountAndSignaling |= static_cast<U32>((currentVc.virtualFrameCount & 0x0F000000) >> 24);
182
183 ✗ header.set_globalVcId(globalVcId);
184 ✗ header.set_frameCountAndSignaling(frameCountAndSignaling);
185
186 // Perform the modulo at serialization time makes vc cycle count easier to make optional
187 ✗ currentVc.virtualFrameCount++; // U24 base Frame Count
188 // Extended by 4 bit VC Cycle Count
189 // intended to wrap around (modulo 268,435,456)
190
191 // -----------------------------------------------
192 // Write Header
193 // -----------------------------------------------
194 Fw::SerializeStatus status;
195 // Use our member Fw::Buffer
196 ✗ auto frameSerializer = currentVc.frame.buffer.getSerializer();
197
198 ✗ status = frameSerializer.moveSerToOffset(0);
199 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
200
201 ✗ status = frameSerializer.serializeFrom(header);
202 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
203 ✗ }
204
205 ✗ void AosFramer ::setup_m_pdu_header(const ComCfg::FrameContext& context, bool no_fresh) {
206 // Get the VC Struct for this vc
207 ✗ AosVc& currentVc = this->get_vc_struct(context);
208
209 ✗ M_PDUHeader muxedPdu;
210 ✗ if (no_fresh) {
211 // All Ones since no packets start in this frame (no fresh packets)
212 ✗ muxedPdu.set_firstHeaderPointer(0xFFFF);
213 } else {
214 // Called at first fresh packet (not a packet tail)
215 // So current payload offset is the pointer to first packet header
216 ✗ muxedPdu.set_firstHeaderPointer(currentVc.current_payload_offset);
217 }
218
219 ✗ auto frameSerializer = currentVc.frame.buffer.getSerializer();
220 ✗ Fw::SerializeStatus status = frameSerializer.moveSerToOffset(AOSHeader::SERIALIZED_SIZE);
221 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
222
223 ✗ status = frameSerializer.serializeFrom(muxedPdu);
224 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
225 ✗ }
226
227 ✗ FwSizeType AosFramer::get_min_size() {
228 ✗ return AOSHeader::SERIALIZED_SIZE + M_PDUHeader::SERIALIZED_SIZE + (this->m_fecf ? AOSTrailer::SERIALIZED_SIZE : 0);
229 }
230
231 ✗ bool AosFramer::buffer_belongs(const Fw::Buffer& buffer, U8 const* start, FwSizeType size) {
232 ✗ return (buffer.getData() >= start && buffer.getData() < start + size);
233 }
234
235 ✗ void AosFramer::check_and_send_vc(AosFramer::AosVc& currentVc) {
236 ✗ const FwSizeType maxPayload = currentVc.frame.buffer.getSize() - get_min_size();
237
238 // Check if we've filled up the M_PDU payload
239 ✗ if (currentVc.current_payload_offset == maxPayload) {
240 // Write the headers if we haven't already (continuing packet only)
241 ✗ if (!currentVc.past_first_fresh_packet) {
242 // Setup the AOS header right before we send with the context cached in outstanding
243 ✗ setup_header(currentVc.outstanding.context);
244
245 // Setup the M_PDU header w/ all zero (only continuing packets)
246 ✗ setup_m_pdu_header(currentVc.outstanding.context, true);
247 }
248
249 // Compute Trailer
250 ✗ if (this->m_fecf) {
251 ✗ compute_and_inject_fecf(currentVc);
252 }
253
254 // Ensure we aren't double sending
255 ✗ FW_ASSERT(currentVc.frame.state == BufferOwnershipState::OWNED);
256 ✗ currentVc.frame.state = BufferOwnershipState::NOT_OWNED;
257
258 // Clean up our per frame vc values
259 ✗ currentVc.current_payload_offset = 0;
260 ✗ currentVc.past_first_fresh_packet = false;
261
262 // Write the completed frame
263 ✗ this->dataOut_out(0, currentVc.frame.buffer, currentVc.outstanding.context);
264 ✗ } else if (this->isConnected_comStatusOut_OutputPort(currentVc.vc_struct_index)) {
265 // We got more room, so ask for more bytes
266 ✗ Fw::Success condition = Fw::Success::SUCCESS;
267 ✗ this->comStatusOut_out(currentVc.vc_struct_index, condition);
268 ✗ }
269 ✗ }
270
271 ✗ void AosFramer ::pack_pad_send(Fw::Buffer& data, const ComCfg::FrameContext& context, FwSizeType dataOffset) {
272 // Pack this packet into the M_PDU
273 ✗ pack_packet(data, context, dataOffset);
274
275 // Get the VC Struct for this buffer
276 ✗ AosVc& currentVc = this->get_vc_struct(context);
277
278 // Pack with idle packets if sendNow and not full already
279 // TODO: Add configurable time elapsed & buffer remaining based idle packing
280 ✗ if (context.get_sendNow()) {
281 // Compute some sizes to check if we've got space left to pad
282 ✗ const FwSizeType min_size = get_min_size();
283 ✗ FW_ASSERT(currentVc.frame.buffer.getSize() >= min_size,
284 static_cast<FwAssertArgType>(currentVc.frame.buffer.getSize()),
285 static_cast<FwAssertArgType>(min_size));
286 ✗ const FwSizeType maxPayload = currentVc.frame.buffer.getSize() - min_size;
287
288 ✗ if (currentVc.current_payload_offset < maxPayload) {
289 // As per TM Standard 4.2.2.5, fill the rest of the data field with an Idle Packet
290 ✗ fill_with_idle_packet(currentVc, context);
291 }
292 }
293
294 // Send the frame if we've filled it
295 ✗ check_and_send_vc(currentVc);
296 ✗ }
297
298 ✗ void AosFramer ::pack_packet(Fw::Buffer& data, const ComCfg::FrameContext& context, FwSizeType dataOffset) {
299 // Ensure the packet is starting within the frame
300 ✗ const FwSizeType min_size = get_min_size();
301
302 // Get the VC Struct for this buffer
303 ✗ AosVc& currentVc = this->get_vc_struct(context);
304
305 ✗ const FwSizeType maxPayload = currentVc.frame.buffer.getSize() - min_size;
306 ✗ const FwSizeType bytesAvailable = maxPayload - currentVc.current_payload_offset;
307 ✗ FW_ASSERT(bytesAvailable < currentVc.frame.buffer.getSize(),
308 static_cast<FwAssertArgType>(min_size + currentVc.current_payload_offset));
309
310 // -------------------------------------------------
311 // Payload Packet
312 // -------------------------------------------------
313 Fw::SerializeStatus status;
314 // Use our member Fw::Buffer
315 ✗ auto frameSerializer = currentVc.frame.buffer.getSerializer();
316 ✗ status = frameSerializer.moveSerToOffset(START_OF_PAYLOAD + currentVc.current_payload_offset);
317 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
318
319 ✗ const U8* dataStart = data.getData() + dataOffset;
320 // min of (remaining bytes in buffer and available bytes in frame)
321 ✗ FwSizeType dataSize = data.getSize() - dataOffset;
322
323 // Determine if we can write the whole packet or not
324 ✗ if (dataSize <= bytesAvailable) {
325 // Whole packet fits, no need to store to outstanding
326 ✗ currentVc.outstanding.offset = 0;
327 } else {
328 // We'll only write a subset (clamp our write to the available size)
329 ✗ dataSize = bytesAvailable;
330
331 // We'll pick up serialization from here later
332 ✗ currentVc.outstanding.offset = dataOffset + dataSize;
333 ✗ currentVc.outstanding.packet = data;
334 }
335
336 ✗ status = frameSerializer.serializeFrom(dataStart, dataSize, Fw::Serialization::OMIT_LENGTH);
337 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
338
339 // Shift our offset into the M_PDU payload region by how many bytes we wrote
340 ✗ currentVc.current_payload_offset = static_cast<U16>(currentVc.current_payload_offset + dataSize);
341
342 // Context must always be present once any packet payload bytes are written
343 ✗ currentVc.outstanding.context = context;
344
345 // Return the buffer if no bytes are outstanding
346 ✗ if (currentVc.outstanding.offset == 0) {
347 // Return the buffer if this isn't the SPP Idle buff
348 ✗ const bool isSppIdleBuffer = buffer_belongs(data, currentVc.spp_idle.backer, sizeof(currentVc.spp_idle.backer));
349 ✗ if (!isSppIdleBuffer) {
350 ✗ this->dataReturnOut_out(0, data,
351 context); // return ownership of the original data buffer
352 }
353
354 ✗ currentVc.outstanding.packet = {};
355 ✗ currentVc.outstanding.offset = 0;
356 }
357 ✗ }
358
359 ✗ void AosFramer ::serialize_idle_spp_packet(Fw::SerialBufferBase& serializer, U16 length) {
360 // APID to use for this Idle Packet
361 ✗ constexpr U16 idleApid = static_cast<U16>(ComCfg::Apid::SPP_IDLE_PACKET);
362
363 // Length token is defined as the number of bytes of payload data minus 1
364 ✗ const U16 lengthToken = static_cast<U16>(length - SpacePacketHeader::SERIALIZED_SIZE - 1);
365
366 ✗ SpacePacketHeader header;
367 ✗ header.set_packetIdentification(idleApid);
368 ✗ header.set_packetSequenceControl(
369 0x3 << SpacePacketSubfields::SeqFlagsOffset); // Sequence Flags = 0b11 (unsegmented) & unused Seq count
370 ✗ header.set_packetDataLength(lengthToken);
371 // Serialize header into frame
372 ✗ Fw::SerializeStatus status = serializer.serializeFrom(header);
373 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
374
375 // Fill with idle pattern
376 ✗ for (U16 i = static_cast<U16>(SpacePacketHeader::SERIALIZED_SIZE); i < length; i++) {
377 ✗ status = serializer.serializeFrom(SPP_IDLE_DATA_PATTERN); // Idle data
378 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
379 }
380 ✗ }
381
382 ✗ void AosFramer ::fill_with_idle_packet(AosVc& vc, const ComCfg::FrameContext& context) {
383 // Bytes that aren't actual PDUs
384 ✗ const U16 overhead = START_OF_PAYLOAD + (m_fecf ? AOSTrailer::SERIALIZED_SIZE : 0);
385 // Bytes for all the PDUs in this VC Frame
386 ✗ FW_ASSERT(vc.frame.buffer.getSize() > 0, static_cast<FwAssertArgType>(vc.frame.buffer.getSize()));
387 ✗ FW_ASSERT(vc.frame.buffer.getSize() < std::numeric_limits<U16>::max(),
388 static_cast<FwAssertArgType>(vc.frame.buffer.getSize()));
389 ✗ const U16 pduSize = static_cast<U16>(vc.frame.buffer.getSize() - overhead);
390
391 // How many bytes are left over
392 ✗ const U16 idlePacketSize = static_cast<U16>(pduSize - vc.current_payload_offset);
393
394 // Grab a serializer @ the current offset
395 ✗ auto frameSerializer = vc.frame.buffer.getSerializer();
396 ✗ Fw::SerializeStatus status = frameSerializer.moveSerToOffset(START_OF_PAYLOAD + vc.current_payload_offset);
397 ✗ FW_ASSERT(status == Fw::FW_SERIALIZE_OK, status);
398
399 // If there wasn't a fresh packet before this
400 // set the M_PDU firstHeaderPointer
401 ✗ if (!vc.past_first_fresh_packet) {
402 // setup our headers if we have a packet + context ready to go
403 ✗ setup_header(context);
404 ✗ setup_m_pdu_header(context);
405
406 // This idle packet is the fresh packet
407 ✗ vc.past_first_fresh_packet = true;
408 }
409
410 // EPP-only idle fill is not yet supported; configure() requires the SPP bit
411 ✗ if (vc.idle_packet_types & PvnBitfield::EPP_MASK) {
412 // TODO: Serialize an EPP of the right size once EPP idle is supported
413 }
414 // While we are using only SPP, we have to comply w/ the min SPP packet size
415 // We'll stripe this packet onto the next frame of this VC if we have to
416 ✗ else if (idlePacketSize < 7) {
417 // Serialize the Idle packet into the spp_idle_backer
418
419 // Make sure we aren't overwriting a packet fragment
420 ✗ FW_ASSERT(!vc.outstanding.packet.isValid());
421
422 // Setup the PDU to point at our idle buffer
423 ✗ vc.outstanding.packet = {vc.spp_idle.backer, MIN_SPP_LENGTH};
424
425 // Write the SPP Idle to the outstanding packet
426 ✗ auto pduSerializer = vc.outstanding.packet.getSerializer();
427 ✗ serialize_idle_spp_packet(pduSerializer, MIN_SPP_LENGTH);
428
429 // Remove the sendNow param
430 // The idle fragment that spills onto the next doesn't need to be sentNow
431 ✗ ComCfg::FrameContext filtered_context = context;
432 ✗ filtered_context.set_sendNow(false);
433
434 // Use the normal pack command since we will have leftovers
435 // There won't be an outstanding packet since we'd use those bytes instead
436 ✗ pack_packet(vc.outstanding.packet, filtered_context);
437 ✗ } else {
438 // Serialize an idle packet right into the frame
439 ✗ serialize_idle_spp_packet(frameSerializer, idlePacketSize);
440
441 // Increment the offset since we serialized directly into the frame
442 ✗ vc.current_payload_offset = static_cast<U16>(vc.current_payload_offset + idlePacketSize);
443 }
444 ✗ }
445
446 } // namespace Ccsds
447 } // namespace Svc
448