GCC Code Coverage Report


Directory: ./
File: Svc/Ccsds/CfdpManager/Channel.cpp
Date: 2026-09-23 22:11:34
Exec Total Coverage
Lines: 0 418 0.0%
Functions: 0 30 0.0%
Branches: 0 234 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title Channel.cpp
3 // \brief CFDP Channel operations implementation
4 //
5 // This file is a port of channel-specific functions from the following files
6 // from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0,
7 // adapted for use within the F-Prime (F') framework:
8 // - cf_cfdp.c (channel processing functions)
9 // - cf_utils.c (channel transaction and resource management)
10 //
11 // ======================================================================
12 //
13 // NASA Docket No. GSC-18,447-1
14 //
15 // Copyright (c) 2019 United States Government as represented by the
16 // Administrator of the National Aeronautics and Space Administration.
17 // All Rights Reserved.
18 //
19 // Licensed under the Apache License, Version 2.0 (the "License");
20 // you may not use this file except in compliance with the License.
21 // You may obtain a copy of the License at
22 //
23 // http://www.apache.org/licenses/LICENSE-2.0
24 //
25 // Unless required by applicable law or agreed to in writing, software
26 // distributed under the License is distributed on an "AS IS" BASIS,
27 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28 // See the License for the specific language governing permissions and
29 // limitations under the License.
30 //
31 // ======================================================================
32
33 #include <string.h>
34 #include <new>
35
36 #include <Fw/FPrimeBasicTypes.hpp>
37
38 #include <Svc/Ccsds/CfdpManager/CfdpManager.hpp>
39 #include <Svc/Ccsds/CfdpManager/Channel.hpp>
40 #include <Svc/Ccsds/CfdpManager/Engine.hpp>
41 #include <Svc/Ccsds/CfdpManager/Utils.hpp>
42
43 namespace Svc {
44 namespace Ccsds {
45 namespace Cfdp {
46
47 // ----------------------------------------------------------------------
48 // Construction
49 // ----------------------------------------------------------------------
50
51 ✗ Channel::Channel(Engine* engine,
52 U8 channelId,
53 CfdpManager* cfdpManager,
54 Fw::MemAllocator& allocator,
55 ✗ FwEnumStoreType memId)
56 ✗ : m_engine(engine),
57 ✗ m_numCmdTx(0),
58 ✗ m_currentTxn(nullptr),
59 ✗ m_cfdpManager(cfdpManager),
60 ✗ m_tickType(0),
61 ✗ m_channelId(channelId),
62 ✗ m_flowState(Cfdp::Flow::NOT_FROZEN),
63 ✗ m_outgoingCounter(0),
64 ✗ m_transactions(nullptr),
65 ✗ m_histories(nullptr),
66 ✗ m_chunks(nullptr),
67 ✗ m_chunkMem(nullptr) {
68 ✗ FW_ASSERT(engine != nullptr);
69 ✗ FW_ASSERT(cfdpManager != nullptr);
70
71 // Initialize queue pointers
72 ✗ for (U32 i = 0; i < QueueId::NUM; i++) {
73 ✗ m_qs[i] = nullptr;
74 }
75
76 // Initialize command/history lists
77 ✗ for (U32 i = 0; i < static_cast<U32>(Direction::DIRECTION_NUM); i++) {
78 ✗ m_cs[i] = nullptr;
79 }
80
81 // Initialize poll directory playback state
82 ✗ for (U32 i = 0; i < MaxPollingDirPerChan; i++) {
83 ✗ m_polldir[i].enabled = Fw::Enabled::DISABLED;
84 ✗ m_polldir[i].pb.busy = false;
85 ✗ m_polldir[i].pb.diropen = false;
86 ✗ m_polldir[i].pb.counted = false;
87 ✗ m_polldir[i].pb.num_ts = 0;
88 ✗ m_polldir[i].pb.pending_file = "";
89 }
90
91 // Initialize playback structures
92 ✗ for (U32 i = 0; i < MaxCommandedPlaybackDirectoriesPerChan; i++) {
93 ✗ m_playback[i].busy = false;
94 ✗ m_playback[i].diropen = false;
95 ✗ m_playback[i].counted = false;
96 ✗ m_playback[i].num_ts = 0;
97 ✗ m_playback[i].pending_file = "";
98 }
99
100 // Allocate and initialize per-channel resources
101 U32 j, k;
102 History* history;
103 Transaction* txn;
104 CfdpChunkWrapper* cw;
105 CListNode** list_head;
106 ✗ U32 chunk_mem_offset = 0;
107 U32 total_chunks_needed;
108
109 // Initialize chunk configuration for this channel
110 ✗ const U32 rxChunksPerChannel[] = CFDP_CHANNEL_NUM_RX_CHUNKS_PER_TRANSACTION;
111 ✗ const U32 txChunksPerChannel[] = CFDP_CHANNEL_NUM_TX_CHUNKS_PER_TRANSACTION;
112 ✗ m_dirMaxChunks[static_cast<U32>(Direction::DIRECTION_RX)] = rxChunksPerChannel[m_channelId];
113 ✗ m_dirMaxChunks[static_cast<U32>(Direction::DIRECTION_TX)] = txChunksPerChannel[m_channelId];
114
115 // Calculate total chunks needed for this channel
116 ✗ total_chunks_needed = 0;
117 ✗ for (k = 0; k < static_cast<U32>(Direction::DIRECTION_NUM); ++k) {
118 ✗ total_chunks_needed += m_dirMaxChunks[k] * CFDP_NUM_TRANSACTIONS_PER_CHANNEL;
119 }
120
121 // Allocate arrays using the provided allocator
122 ✗ FwSizeType transactionsSize = CFDP_NUM_TRANSACTIONS_PER_CHANNEL * sizeof(Transaction);
123 ✗ m_transactions = static_cast<Transaction*>(allocator.allocate(memId, transactionsSize));
124 ✗ FW_ASSERT(m_transactions != nullptr);
125
126 ✗ FwSizeType chunksSize =
127 (CFDP_NUM_TRANSACTIONS_PER_CHANNEL * static_cast<U32>(Direction::DIRECTION_NUM)) * sizeof(CfdpChunkWrapper);
128 ✗ m_chunks = static_cast<CfdpChunkWrapper*>(allocator.allocate(memId, chunksSize));
129 ✗ FW_ASSERT(m_chunks != nullptr);
130
131 ✗ FwSizeType historiesSize = NumHistoriesPerChannel * sizeof(History);
132 ✗ m_histories = static_cast<History*>(allocator.allocate(memId, historiesSize));
133 ✗ FW_ASSERT(m_histories != nullptr);
134
135 ✗ FwSizeType chunkMemSize = total_chunks_needed * sizeof(Chunk);
136 ✗ m_chunkMem = static_cast<Chunk*>(allocator.allocate(memId, chunkMemSize));
137 ✗ FW_ASSERT(m_chunkMem != nullptr);
138
139 // Initialize transactions using placement new with parameterized constructor
140 ✗ cw = m_chunks;
141 ✗ for (j = 0; j < CFDP_NUM_TRANSACTIONS_PER_CHANNEL; ++j) {
142 // Construct transaction in-place with parameterized constructor
143 ✗ txn = new (&m_transactions[j]) Transaction(this, m_channelId, m_engine, m_cfdpManager);
144
145 // Put transaction on free list
146 ✗ this->freeTransaction(txn);
147
148 // Initialize chunk wrappers for this transaction (TX and RX)
149 ✗ for (k = 0; k < static_cast<U32>(Direction::DIRECTION_NUM); ++k, ++cw) {
150 ✗ list_head = this->getChunkListHead(static_cast<U8>(k));
151
152 // Use placement new to construct CfdpChunkWrapper with the new class-based interface
153 ✗ new (cw) CfdpChunkWrapper(static_cast<ChunkIdx>(m_dirMaxChunks[k]), &m_chunkMem[chunk_mem_offset]);
154 ✗ chunk_mem_offset += m_dirMaxChunks[k];
155 ✗ CfdpCListInitNode(&cw->cl_node);
156 ✗ CfdpCListInsertBack(list_head, &cw->cl_node);
157 }
158 }
159
160 // Initialize histories using placement new (History contains Fw::String which needs proper construction)
161 ✗ for (j = 0; j < NumHistoriesPerChannel; ++j) {
162 ✗ history = new (&m_histories[j]) History(); // Use placement new with default constructor
163 ✗ CfdpCListInitNode(&history->cl_node);
164 ✗ this->insertBackInQueue(QueueId::HIST_FREE, &history->cl_node);
165 }
166 ✗ }
167
168 ✗ Channel::~Channel() {
169 // Cleanup should have been called before destruction
170 // This is enforced by Engine::~Engine()
171 ✗ }
172
173 ✗ void Channel::cleanup(Fw::MemAllocator& allocator, FwEnumStoreType memId) {
174 // Call destructors and deallocate all internal arrays
175 ✗ if (m_transactions != nullptr) {
176 // Manually call destructors since we used placement new
177 ✗ for (U32 j = 0; j < CFDP_NUM_TRANSACTIONS_PER_CHANNEL; ++j) {
178 ✗ m_transactions[j].~Transaction();
179 }
180 ✗ allocator.deallocate(memId, m_transactions);
181 ✗ m_transactions = nullptr;
182 }
183
184 ✗ if (m_chunks != nullptr) {
185 // Manually call destructors since we used placement new
186 ✗ for (U32 j = 0; j < (CFDP_NUM_TRANSACTIONS_PER_CHANNEL * static_cast<U32>(Direction::DIRECTION_NUM)); ++j) {
187 ✗ m_chunks[j].~CfdpChunkWrapper();
188 }
189 ✗ allocator.deallocate(memId, m_chunks);
190 ✗ m_chunks = nullptr;
191 }
192
193 ✗ if (m_histories != nullptr) {
194 // Call destructors on History objects
195 ✗ for (U32 j = 0; j < NumHistoriesPerChannel; ++j) {
196 ✗ m_histories[j].~History();
197 }
198 ✗ allocator.deallocate(memId, m_histories);
199 ✗ m_histories = nullptr;
200 }
201
202 ✗ if (m_chunkMem != nullptr) {
203 ✗ allocator.deallocate(memId, m_chunkMem);
204 ✗ m_chunkMem = nullptr;
205 }
206 ✗ }
207
208 // ----------------------------------------------------------------------
209 // Channel Processing
210 // ----------------------------------------------------------------------
211
212 ✗ void Channel::cycleTx() {
213 Transaction* txn;
214 CycleTxArgs args;
215
216 ✗ if (m_cfdpManager->getDequeueEnabledParam(m_channelId)) {
217 ✗ args.chan = this;
218 ✗ args.ran_one = 0;
219
220 // loop through as long as there are pending transactions, and a message buffer to send their PDUs on
221
222 // NOTE: tick processing is higher priority than sending new filedata PDUs, so only send however many
223 // PDUs that can be sent once we get to here
224 ✗ if (!this->m_currentTxn) { // don't enter if currentTxn is set, since we need to pick up where we left off on
225 // tick processing next scheduler cycle
226
227 // Process pending transactions until queue is empty or something runs
228 while (true) {
229 // Context for static wrapper: pass both Channel* and CycleTxArgs*
230 struct CycleTxContext {
231 Channel* channel;
232 CycleTxArgs* args;
233 ✗ } cycleTxCtx = {this, &args};
234
235 // Attempt to run something on TXA
236 ✗ CfdpCListTraverse(m_qs[QueueId::TXA], &Channel::cycleTxFirstActiveWrapper, &cycleTxCtx);
237
238 // Keep going until QueueId::PEND is empty or something is run
239 ✗ if (args.ran_one || m_qs[QueueId::PEND] == nullptr) {
240 break;
241 }
242
243 ✗ txn = container_of_cpp(m_qs[QueueId::PEND], &Transaction::m_cl_node);
244
245 // Class 2 transactions need a chunklist for NAK processing, get one now.
246 // Class 1 transactions don't need chunks since they don't support NAKs.
247 ✗ if (txn->getClass() == Cfdp::Class::CLASS_2) {
248 ✗ if (txn->m_chunks == nullptr) {
249 ✗ txn->m_chunks = this->findUnusedChunks(Direction::DIRECTION_TX);
250 }
251 ✗ if (txn->m_chunks == nullptr) {
252 // Chunklist unavailable - EVR already emitted by Engine
253 // Leave transaction pending until a chunklist is available.
254 ✗ break;
255 }
256 }
257
258 ✗ m_engine->armInactTimer(txn);
259 ✗ this->moveTransaction(txn, QueueId::TXA);
260 ✗ }
261 }
262
263 // in case the loop exited due to no message buffers, clear it and start from the top next time
264 ✗ this->m_currentTxn = nullptr;
265 }
266 ✗ }
267
268 ✗ void Channel::tickTransactions() {
269 ✗ bool reset = true;
270
271 ✗ void (Transaction::* fns[static_cast<U8>(CfdpTickType::CFDP_TICK_TYPE_NUM_TYPES)])(I32*) = {
272 &Transaction::rTick, &Transaction::sTick, &Transaction::sTickNak};
273 ✗ I32 qs[static_cast<U8>(CfdpTickType::CFDP_TICK_TYPE_NUM_TYPES)] = {QueueId::RX, QueueId::TXW, QueueId::TXW};
274
275 ✗ FW_ASSERT(m_tickType < static_cast<U8>(CfdpTickType::CFDP_TICK_TYPE_NUM_TYPES), m_tickType);
276
277 ✗ for (; m_tickType < static_cast<U8>(CfdpTickType::CFDP_TICK_TYPE_NUM_TYPES); ++m_tickType) {
278 ✗ TickArgs args = {this, fns[m_tickType], 0, 0};
279
280 // Safety bound: retry loop should not exceed the number of transactions in the queue
281 // Each retry processes one transaction that may request continuation
282 ✗ constexpr U32 maxRetries = MaxSimultaneousRx + MaxCommandedPlaybackFilesPerChan +
283 (MaxCommandedPlaybackDirectoriesPerChan * NumTransactionsPerPlayback) +
284 (MaxPollingDirPerChan * NumTransactionsPerPlayback);
285
286 ✗ for (U32 retry = 0; retry < maxRetries; ++retry) {
287 ✗ args.cont = 0;
288
289 // Context for static wrapper: pass both Channel* and TickArgs*
290 struct TickContext {
291 Channel* channel;
292 TickArgs* args;
293 ✗ } tickCtx = {this, &args};
294
295 ✗ CfdpCListTraverse(m_qs[qs[m_tickType]], &Channel::doTickWrapper, &tickCtx);
296
297 ✗ if (args.early_exit) {
298 // early exit means we ran out of available outgoing messages this scheduler cycle.
299 // If current tick type is NAK response, then reset tick type. It would be
300 // bad to let NAK response starve out RX or TXW ticks on the next cycle.
301 //
302 // If RX ticks use up all available messages, then we pick up where we left
303 // off on the next cycle. (This causes some RX tick counts to be missed,
304 // but that's ok. Precise timing isn't required.)
305 //
306 // This scheme allows the following priority for use of outgoing messages:
307 //
308 // RX state messages
309 // TXW state messages
310 // NAK response (could be many)
311 //
312 // New file data on TXA
313 ✗ if (m_tickType != static_cast<U8>(CfdpTickType::CFDP_TICK_TYPE_TXW_NAK)) {
314 ✗ reset = false;
315 }
316
317 ✗ break;
318 }
319
320 ✗ if (!args.cont) {
321 ✗ break; // No continuation requested, exit retry loop
322 }
323 }
324
325 ✗ if (!reset) {
326 ✗ break;
327 }
328 }
329
330 ✗ if (reset) {
331 ✗ m_tickType = static_cast<U8>(CfdpTickType::CFDP_TICK_TYPE_RX); // reset tick type
332 }
333 ✗ }
334
335 ✗ void Channel::processPlaybackDirectories() {
336 U32 i;
337 ✗ U8 playback_count = 0;
338
339 ✗ for (i = 0; i < MaxCommandedPlaybackDirectoriesPerChan; ++i) {
340 ✗ this->processPlaybackDirectory(&m_playback[i]);
341 // Count active playback operations
342 ✗ if (m_playback[i].busy) {
343 ✗ playback_count++;
344 }
345 }
346
347 // Update playback counter telemetry
348 ✗ Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId);
349 ✗ tlm.set_playbackCounter(playback_count);
350 ✗ }
351
352 ✗ void Channel::processPollingDirectories() {
353 CfdpPollDir* pd;
354 U32 i;
355 ✗ U8 poll_count = 0;
356
357 ✗ for (i = 0; i < MaxPollingDirPerChan; ++i) {
358 ✗ pd = &m_polldir[i];
359
360 ✗ if (pd->enabled) {
361 ✗ poll_count++;
362
363 ✗ if ((pd->pb.busy == false) && (pd->pb.num_ts == 0)) {
364 ✗ if (pd->intervalTimer.getStatus() == Timer::Status::EXPIRED) {
365 // the timer has expired, so initiate a playback of the directory.
366 // The return status is intentionally ignored: playbackDirInitiate
367 // already emits an event on failure and the timer is re-armed
368 // below regardless so polling retries after the interval.
369 ✗ (void)m_engine->playbackDirInitiate(&pd->pb, pd->srcDir, pd->dstDir, pd->cfdpClass,
370 ✗ Cfdp::Keep::DELETE, m_channelId, pd->priority, pd->destEid);
371 // re-arm the timer for the next interval. The timer only ticks
372 // down while the playback is not busy.
373 ✗ if (pd->intervalSec > 0) {
374 ✗ pd->intervalTimer.setTimer(pd->intervalSec);
375 }
376 } else {
377 ✗ pd->intervalTimer.run();
378 }
379 } else {
380 // playback is active, so step it
381 ✗ this->processPlaybackDirectory(&pd->pb);
382 }
383 }
384 }
385
386 // Update poll counter telemetry
387 ✗ Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId);
388 ✗ tlm.set_pollCounter(poll_count);
389 ✗ }
390
391 // ----------------------------------------------------------------------
392 // Transaction Management
393 // ----------------------------------------------------------------------
394
395 ✗ Transaction* Channel::findUnusedTransaction(Direction direction) {
396 CListNode* node;
397 Transaction* txn;
398 QueueId::T q_index; // initialized below in if
399
400 ✗ if (m_qs[QueueId::FREE]) {
401 ✗ node = m_qs[QueueId::FREE];
402 ✗ txn = container_of_cpp(node, &Transaction::m_cl_node);
403
404 ✗ this->removeFromQueue(QueueId::FREE, &txn->m_cl_node);
405
406 // now that a transaction is acquired, must also acquire a history slot to go along with it
407 ✗ if (m_qs[QueueId::HIST_FREE]) {
408 ✗ q_index = QueueId::HIST_FREE;
409 } else {
410 // no free history, so take the oldest one from the channel's history queue
411 ✗ FW_ASSERT(m_qs[QueueId::HIST]);
412 ✗ q_index = QueueId::HIST;
413 }
414
415 ✗ txn->m_history = container_of_cpp(m_qs[q_index], &History::cl_node);
416
417 ✗ this->removeFromQueue(q_index, &txn->m_history->cl_node);
418
419 // Reset all history fields to initial state (matches constructor zero-init)
420 // This is necessary when recycling from HIST queue to clear stale data
421 ✗ txn->m_history->txn_stat = TxnStatus::TXN_STATUS_UNDEFINED; // Critical: prevents error status inheritance
422 ✗ txn->m_history->src_eid = 0;
423 ✗ txn->m_history->peer_eid = 0;
424 ✗ txn->m_history->seq_num = 0;
425 ✗ txn->m_history->fnames.src_filename = "";
426 ✗ txn->m_history->fnames.dst_filename = "";
427 // Note: cl_node is managed by queue operations (already handled by removeFromQueue)
428 // Note: dir is explicitly set below (already handled)
429
430 // Indicate that this was freshly pulled from the free list
431 // notably this state is distinguishable from items still on the free list
432 ✗ txn->m_state = TxnState::TXN_STATE_INIT;
433
434 // Clear the FREE tag now that this transaction has been taken off the FREE
435 // list. freeTransaction() marks q_index == QueueId::FREE for anything sitting
436 // on the free list; leaving that tag set on an acquired-but-not-yet-enqueued
437 // transaction would break the invariant relied on by
438 // Engine::finishTransaction()'s double-free guard (a live txn must never look
439 // FREE). The caller (startRxTransaction / txFileInitiate) will assign the real
440 // queue via insertSortPrio()/direct assignment; until then PEND (0) is the
441 // neutral, not-on-FREE-list default that matches reset()'s zeroed m_flags.
442 ✗ txn->m_flags.com.q_index = QueueId::PEND;
443
444 ✗ txn->m_history->dir = direction;
445 ✗ txn->m_chan = this; // Set channel pointer
446
447 // Re-initialize the linked list node to clear stale pointers from FREE list
448 ✗ CfdpCListInitNode(&txn->m_cl_node);
449 } else {
450 ✗ txn = nullptr;
451 }
452
453 ✗ return txn;
454 }
455
456 ✗ Transaction* Channel::findTransactionBySequenceNumber(TransactionSeq transaction_sequence_number, EntityId src_eid) {
457 // need to find transaction by sequence number. It will either be the active transaction (front of Q_PEND),
458 // or on Q_TX or Q_RX. Once a transaction moves to history, then it's done.
459 //
460 // Let's put QueueId::RX up front, because most RX packets will be file data PDUs
461 ✗ CfdpTraverseTransSeqArg ctx = {transaction_sequence_number, src_eid, nullptr};
462 ✗ CListNode* ptrs[] = {m_qs[QueueId::RX], m_qs[QueueId::PEND], m_qs[QueueId::TXA], m_qs[QueueId::TXW]};
463 ✗ Transaction* ret = nullptr;
464
465 ✗ for (CListNode* head : ptrs) {
466 ✗ CfdpCListTraverse(head, Transaction::findBySequenceNumberCallback, &ctx);
467 ✗ if (ctx.txn) {
468 ✗ ret = ctx.txn;
469 ✗ break;
470 }
471 }
472
473 ✗ return ret;
474 }
475
476 ✗ I32 Channel::traverseAllTransactions(CfdpTraverseAllTransactionsFunc fn, void* context) {
477 ✗ I32 counter = 0;
478
479 // Context for static wrapper
480 struct TraverseAllContext {
481 CfdpTraverseAllTransactionsFunc fn;
482 void* userContext;
483 I32* counter;
484 ✗ } ctx = {fn, context, &counter};
485
486 ✗ for (I32 queueidx = QueueId::PEND; queueidx <= QueueId::RX; ++queueidx) {
487 ✗ CfdpCListTraverse(m_qs[queueidx], &Channel::traverseAllTransactionsWrapper, &ctx);
488 }
489
490 ✗ return counter;
491 }
492
493 ✗ void Channel::resetHistory(History* history) {
494 ✗ this->removeFromQueue(QueueId::HIST, &history->cl_node);
495 ✗ this->insertBackInQueue(QueueId::HIST_FREE, &history->cl_node);
496 ✗ }
497
498 // ----------------------------------------------------------------------
499 // Transaction Queue Management
500 // ----------------------------------------------------------------------
501
502 ✗ void Channel::dequeueTransaction(Transaction* txn) {
503 ✗ FW_ASSERT(txn);
504 ✗ CfdpCListRemove(&m_qs[txn->m_flags.com.q_index], &txn->m_cl_node);
505
506 // Update queue depth telemetry
507 ✗ Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId);
508 ✗ switch (txn->m_flags.com.q_index) {
509 ✗ case Cfdp::QueueId::FREE:
510
511 ✗ tlm.set_queueFree(static_cast<U16>(tlm.get_queueFree() - 1));
512 ✗ break;
513 ✗ case Cfdp::QueueId::TXA:
514
515 ✗ tlm.set_queueTxActive(static_cast<U16>(tlm.get_queueTxActive() - 1));
516 ✗ break;
517 ✗ case Cfdp::QueueId::TXW:
518
519 ✗ tlm.set_queueTxWaiting(static_cast<U16>(tlm.get_queueTxWaiting() - 1));
520 ✗ break;
521 ✗ case Cfdp::QueueId::RX:
522
523 ✗ tlm.set_queueRx(static_cast<U16>(tlm.get_queueRx() - 1));
524 ✗ break;
525 ✗ case Cfdp::QueueId::HIST:
526
527 ✗ tlm.set_queueHistory(static_cast<U16>(tlm.get_queueHistory() - 1));
528 ✗ break;
529 ✗ case Cfdp::QueueId::PEND:
530 case Cfdp::QueueId::HIST_FREE:
531 // PEND and HIST_FREE queues are not tracked in telemetry
532 ✗ break;
533 ✗ default:
534 ✗ FW_ASSERT(0, txn->m_flags.com.q_index);
535 }
536 ✗ }
537
538 ✗ void Channel::moveTransaction(Transaction* txn, QueueId::T queue) {
539 ✗ FW_ASSERT(txn);
540 ✗ Cfdp::ChannelTelemetry& tlm = m_engine->getChannelTelemetryRef(m_channelId);
541
542 // Decrement old queue
543 ✗ CfdpCListRemove(&m_qs[txn->m_flags.com.q_index], &txn->m_cl_node);
544 ✗ switch (txn->m_flags.com.q_index) {
545 ✗ case Cfdp::QueueId::FREE:
546
547 ✗ tlm.set_queueFree(static_cast<U16>(tlm.get_queueFree() - 1));
548 ✗ break;
549 ✗ case Cfdp::QueueId::TXA:
550
551 ✗ tlm.set_queueTxActive(static_cast<U16>(tlm.get_queueTxActive() - 1));
552 ✗ break;
553 ✗ case Cfdp::QueueId::TXW:
554
555 ✗ tlm.set_queueTxWaiting(static_cast<U16>(tlm.get_queueTxWaiting() - 1));
556 ✗ break;
557 ✗ case Cfdp::QueueId::RX:
558
559 ✗ tlm.set_queueRx(static_cast<U16>(tlm.get_queueRx() - 1));
560 ✗ break;
561 ✗ case Cfdp::QueueId::HIST:
562
563 ✗ tlm.set_queueHistory(static_cast<U16>(tlm.get_queueHistory() - 1));
564 ✗ break;
565 ✗ case Cfdp::QueueId::PEND:
566 case Cfdp::QueueId::HIST_FREE:
567 // PEND and HIST_FREE queues are not tracked in telemetry
568 ✗ break;
569 ✗ default:
570 ✗ FW_ASSERT(0, txn->m_flags.com.q_index);
571 }
572
573 // Increment new queue
574 ✗ CfdpCListInsertBack(&m_qs[queue], &txn->m_cl_node);
575 ✗ txn->m_flags.com.q_index = queue;
576 ✗ switch (queue) {
577 ✗ case Cfdp::QueueId::FREE:
578 ✗ tlm.set_queueFree(static_cast<U16>(tlm.get_queueFree() + 1));
579 ✗ break;
580 ✗ case Cfdp::QueueId::TXA:
581 ✗ tlm.set_queueTxActive(static_cast<U16>(tlm.get_queueTxActive() + 1));
582 ✗ break;
583 ✗ case Cfdp::QueueId::TXW:
584 ✗ tlm.set_queueTxWaiting(static_cast<U16>(tlm.get_queueTxWaiting() + 1));
585 ✗ break;
586 ✗ case Cfdp::QueueId::RX:
587 ✗ tlm.set_queueRx(static_cast<U16>(tlm.get_queueRx() + 1));
588 ✗ break;
589 ✗ case Cfdp::QueueId::HIST:
590 ✗ tlm.set_queueHistory(static_cast<U16>(tlm.get_queueHistory() + 1));
591 ✗ break;
592 ✗ case Cfdp::QueueId::PEND:
593 case Cfdp::QueueId::HIST_FREE:
594 // PEND and HIST_FREE queues are not tracked in telemetry
595 ✗ break;
596 ✗ default:
597 ✗ FW_ASSERT(0, queue);
598 }
599 ✗ }
600
601 ✗ void Channel::freeTransaction(Transaction* txn) {
602 // Reset transaction to default state (preserves channel context)
603 ✗ txn->reset();
604
605 // Initialize the linked list node for the FREE queue
606 ✗ CfdpCListInitNode(&txn->m_cl_node);
607 ✗ this->insertBackInQueue(QueueId::FREE, &txn->m_cl_node);
608
609 // Mark the transaction as residing on the FREE list. insertBackInQueue() only
610 // performs the list insertion (unlike insertSortPrio(), which also updates
611 // q_index), and txn->reset() zeroes m_flags so q_index would otherwise be left
612 // at 0 (== QueueId::PEND). Without this, a freed transaction is never tagged
613 // FREE, and Engine::finishTransaction()'s double-free guard
614 // (q_index == QueueId::FREE) can never fire. Setting it here upholds the
615 // invariant: "a transaction on the FREE list has q_index == FREE".
616 ✗ txn->m_flags.com.q_index = QueueId::FREE;
617 ✗ }
618
619 ✗ void Channel::recycleTransaction(Transaction* txn) {
620 CListNode** chunklist_head;
621 QueueId::T hist_destq;
622
623 // File should have been closed by the state machine, but if
624 // it still hanging open at this point, close it now so its not leaked.
625 // This is not normal/expected so log it if this happens.
626 ✗ if (true == txn->m_fd.isOpen()) {
627 ✗ this->m_cfdpManager->log_WARNING_LO_DanglingFileHandleClosed(txn->getChannelId(), txn->m_history->seq_num);
628 ✗ txn->m_fd.close();
629 }
630
631 ✗ this->dequeueTransaction(txn); // this makes it "float" (not in any queue)
632
633 // this should always be
634 ✗ if (txn->m_history != nullptr) {
635 ✗ if (txn->m_chunks != nullptr) {
636 ✗ chunklist_head = this->getChunkListHead(static_cast<U8>(txn->m_history->dir));
637 ✗ if (chunklist_head != nullptr) {
638 // Reset chunk list to clear stale data from previous transaction
639 ✗ txn->m_chunks->chunks.reset();
640 ✗ CfdpCListInsertBack(chunklist_head, &txn->m_chunks->cl_node);
641 ✗ txn->m_chunks = nullptr;
642 }
643 }
644
645 ✗ if (txn->m_flags.com.keep_history) {
646 // move transaction history to history queue
647 ✗ hist_destq = QueueId::HIST;
648 } else {
649 ✗ hist_destq = QueueId::HIST_FREE;
650 }
651 ✗ this->insertBackInQueue(hist_destq, &txn->m_history->cl_node);
652 ✗ txn->m_history = nullptr;
653 }
654
655 // this wipes it and puts it back onto the list to be found by
656 // Channel::findUnusedTransaction(). Need to preserve the chan_num
657 // and keep it associated with this channel, though.
658 ✗ this->freeTransaction(txn);
659 ✗ }
660
661 ✗ void Channel::insertSortPrio(Transaction* txn, QueueId::T queue) {
662 ✗ bool insert_back = false;
663
664 ✗ FW_ASSERT(txn);
665
666 // look for proper position on PEND queue for this transaction.
667 // This is a simple priority sort.
668
669 ✗ if (!m_qs[queue]) {
670 // list is empty, so just insert
671 ✗ insert_back = true;
672 } else {
673 ✗ CfdpTraversePriorityArg arg = {nullptr, txn->getPriority()};
674 ✗ CfdpCListTraverseR(m_qs[queue], Transaction::prioritySearchCallback, &arg);
675 ✗ if (arg.txn) {
676 ✗ this->insertAfterInQueue(queue, &arg.txn->m_cl_node, &txn->m_cl_node);
677 } else {
678 ✗ insert_back = true;
679 }
680 }
681
682 ✗ if (insert_back) {
683 ✗ this->insertBackInQueue(queue, &txn->m_cl_node);
684 }
685 ✗ txn->m_flags.com.q_index = queue;
686 ✗ }
687
688 // ----------------------------------------------------------------------
689 // Channel State Management
690 // ----------------------------------------------------------------------
691
692 ✗ void Channel::decrementCmdTxCounter() {
693 ✗ FW_ASSERT(m_numCmdTx); // sanity check
694 ✗ --m_numCmdTx;
695 ✗ }
696
697 ✗ void Channel::clearCurrentIfMatch(Transaction* txn) {
698 // Done with this TX transaction
699 ✗ if (this->m_currentTxn == txn) {
700 ✗ this->m_currentTxn = nullptr;
701 }
702 ✗ }
703
704 ✗ void Channel::setCurrentTxn(const Transaction* txn) {
705 ✗ this->m_currentTxn = txn;
706 ✗ }
707
708 // ----------------------------------------------------------------------
709 // Resource Management
710 // ----------------------------------------------------------------------
711
712 ✗ CListNode** Channel::getChunkListHead(U8 direction) {
713 CListNode** result;
714
715 ✗ if (direction < static_cast<U32>(Direction::DIRECTION_NUM)) {
716 ✗ result = &m_cs[direction];
717 } else {
718 ✗ result = nullptr;
719 }
720
721 ✗ return result;
722 }
723
724 ✗ CfdpChunkWrapper* Channel::findUnusedChunks(Direction dir) {
725 ✗ CfdpChunkWrapper* ret = nullptr;
726 CListNode* node;
727 CListNode** chunklist_head;
728
729 ✗ chunklist_head = this->getChunkListHead(static_cast<U8>(dir));
730
731 // this should never be null
732 ✗ FW_ASSERT(chunklist_head);
733
734 ✗ if (*chunklist_head != nullptr) {
735 ✗ node = CfdpCListPop(chunklist_head);
736 ✗ if (node != nullptr) {
737 ✗ ret = container_of_cpp(node, &CfdpChunkWrapper::cl_node);
738 }
739 }
740
741 ✗ return ret;
742 }
743
744 // ----------------------------------------------------------------------
745 // Private helper methods
746 // ----------------------------------------------------------------------
747
748 ✗ void Channel::processPlaybackDirectory(Playback* pb) {
749 Transaction* txn;
750 ✗ Fw::StringTemplate<MaxFilePathSize> path;
751 Os::Directory::Status status;
752
753 // either there's no transaction (first one) or the last one was finished, so check for a new one
754
755 ✗ while (pb->diropen && (pb->num_ts < NumTransactionsPerPlayback)) {
756 ✗ if (pb->pending_file.length() == 0) {
757 ✗ status = pb->dir.read(path);
758 ✗ if (status == Os::Directory::NO_MORE_FILES) {
759 // Directory playback complete - success reported via TxFileTransferCompleted EVR
760 ✗ pb->dir.close();
761 ✗ pb->diropen = false;
762 ✗ break;
763 }
764 ✗ if (status != Os::Directory::OP_OK) {
765 // Directory read error - emit EVR and close playback
766 ✗ this->m_cfdpManager->log_WARNING_LO_PlaybackDirReadFailed(pb->fnames.src_filename,
767 static_cast<I32>(status));
768 ✗ pb->dir.close();
769 ✗ pb->diropen = false;
770 ✗ break;
771 }
772
773 ✗ pb->pending_file = path;
774 } else {
775 ✗ txn = this->findUnusedTransaction(Direction::DIRECTION_TX);
776 ✗ if (txn == nullptr) {
777 // while not expected this can certainly happen, because
778 // rx transactions consume in these as well.
779 // should not need to do anything special, will come back next tick
780 ✗ break;
781 }
782
783 // Append file name to source/destination folders
784 ✗ txn->m_history->fnames.src_filename = pb->fnames.src_filename;
785 ✗ txn->m_history->fnames.src_filename += "/";
786 ✗ txn->m_history->fnames.src_filename += pb->pending_file;
787
788 ✗ txn->m_history->fnames.dst_filename = pb->fnames.dst_filename;
789 ✗ txn->m_history->fnames.dst_filename += "/";
790 ✗ txn->m_history->fnames.dst_filename += pb->pending_file;
791
792 ✗ m_engine->txFileInitiate(txn, pb->cfdp_class, pb->keep, m_channelId, pb->priority, pb->dest_id);
793
794 ✗ txn->m_pb = pb;
795 ✗ ++pb->num_ts;
796
797 ✗ pb->pending_file = ""; // continue reading dir
798 }
799 }
800
801 ✗ if (!pb->diropen && !pb->num_ts) {
802 // the directory has been exhausted, and there are no more active transactions
803 // for this playback -- so mark it as not busy
804 ✗ pb->busy = false;
805 }
806 ✗ }
807
808 ✗ void Channel::updatePollPbCounted(Playback* pb, I32 up, U8* counter) {
809 ✗ if (pb->counted != up) {
810 // only handle on state change
811 ✗ pb->counted = !!up; // !! ensure 0 or 1, should be optimized out
812
813 ✗ if (up) {
814 ✗ ++*counter;
815 } else {
816 ✗ FW_ASSERT(*counter); // sanity check it isn't zero
817 ✗ --*counter;
818 }
819 }
820 ✗ }
821
822 ✗ CListTraverseStatus Channel::cycleTxFirstActive(CListNode* node, void* context) {
823 ✗ CycleTxArgs* args = static_cast<CycleTxArgs*>(context);
824 ✗ Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node);
825 ✗ CListTraverseStatus ret = CLIST_TRAVERSE_EXIT; // default option is exit traversal
826
827 ✗ if (txn->m_flags.com.suspended) {
828 ✗ ret = CLIST_TRAVERSE_CONTINUE; // suspended, so move on to next
829 } else {
830 ✗ FW_ASSERT(txn->m_flags.com.q_index == QueueId::TXA); // huh?
831
832 // if no more messages, then chan->m_currentTxn will be set.
833 // If the transaction sent the last filedata PDU and EOF, it will move itself
834 // off the active queue. Run until either of these occur.
835 ✗ while (!this->m_currentTxn && txn->m_flags.com.q_index == QueueId::TXA) {
836 ✗ m_engine->dispatchTx(txn);
837 }
838
839 ✗ args->ran_one = 1;
840 }
841
842 ✗ return ret;
843 }
844
845 ✗ CListTraverseStatus Channel::doTick(CListNode* node, void* context) {
846 ✗ CListTraverseStatus ret =
847 CLIST_TRAVERSE_CONTINUE; // CLIST_TRAVERSE_CONTINUE means don't tick one, keep looking for currentTxn
848 ✗ TickArgs* args = static_cast<TickArgs*>(context);
849 ✗ Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node);
850 ✗ if (!this->m_currentTxn || (this->m_currentTxn == txn)) {
851 // found where we left off, so clear that and move on
852 ✗ this->m_currentTxn = nullptr;
853 ✗ if (!txn->m_flags.com.suspended) {
854 ✗ (txn->*args->fn)(&args->cont);
855 }
856
857 // if this->m_currentTxn was set to not-nullptr above, then exit early
858 // NOTE: if channel is frozen, then tick processing won't have been entered.
859 // so there is no need to check it here
860 ✗ if (this->m_currentTxn) {
861 ✗ ret = CLIST_TRAVERSE_EXIT;
862 ✗ args->early_exit = true;
863 }
864 }
865
866 ✗ return ret; // don't tick one, keep looking for currentTxn
867 }
868
869 ✗ Transaction* Channel::getTransaction(U32 index) {
870 ✗ FW_ASSERT(index < CFDP_NUM_TRANSACTIONS_PER_CHANNEL);
871 ✗ return &m_transactions[index];
872 }
873
874 ✗ History* Channel::getHistory(U32 index) {
875 ✗ FW_ASSERT(index < NumHistoriesPerChannel);
876 ✗ return &m_histories[index];
877 }
878
879 // ----------------------------------------------------------------------
880 // Static callback wrapper implementations
881 // ----------------------------------------------------------------------
882
883 ✗ CListTraverseStatus Channel::cycleTxFirstActiveWrapper(CListNode* node, void* context) {
884 struct CycleTxContext {
885 Channel* channel;
886 CycleTxArgs* args;
887 };
888 ✗ CycleTxContext* ctx = static_cast<CycleTxContext*>(context);
889 ✗ return ctx->channel->cycleTxFirstActive(node, ctx->args);
890 }
891
892 ✗ CListTraverseStatus Channel::doTickWrapper(CListNode* node, void* context) {
893 struct TickContext {
894 Channel* channel;
895 TickArgs* args;
896 };
897 ✗ TickContext* ctx = static_cast<TickContext*>(context);
898 ✗ return ctx->channel->doTick(node, ctx->args);
899 }
900
901 ✗ CListTraverseStatus Channel::traverseAllTransactionsWrapper(CListNode* node, void* context) {
902 struct TraverseAllContext {
903 CfdpTraverseAllTransactionsFunc fn;
904 void* userContext;
905 I32* counter;
906 };
907 ✗ TraverseAllContext* ctx = static_cast<TraverseAllContext*>(context);
908 ✗ Transaction* txn = container_of_cpp(node, &Transaction::m_cl_node);
909 ✗ ctx->fn(txn, ctx->userContext);
910 ✗ ++(*ctx->counter);
911 ✗ return CLIST_TRAVERSE_CONTINUE;
912 }
913
914 } // namespace Cfdp
915 } // namespace Ccsds
916 } // namespace Svc
917