GCC Code Coverage Report


Directory: /home/runner/work/fprime/fprime/Svc/TlmChan/
File: TlmChan.cpp
Date: 2026-09-03 22:14:00
Exec Total Coverage
Lines: 116 182 63.7%
Functions: 6 7 85.7%
Branches: 60 119 50.4%

Line Branch Exec Source
1 /**
2 * \file
3 * \author T. Canham
4 * \brief Implementation file for channelized telemetry storage component
5 *
6 * \copyright
7 * Copyright 2009-2015, by the California Institute of Technology.
8 * ALL RIGHTS RESERVED. United States Government Sponsorship
9 * acknowledged.
10 * <br /><br />
11 */
12 #include <Fw/Com/ComBuffer.hpp>
13 #include <Fw/FPrimeBasicTypes.hpp>
14 #include <Fw/Time/Time.hpp>
15 #include <Fw/Types/Assert.hpp>
16 #include <Os/RawTime.hpp>
17 #include <Svc/TlmChan/TlmChan.hpp>
18
19 namespace Svc {
20
21 // Definition of TLMCHAN_HASH_BUCKETS is >= number of telemetry ids
22 static_assert(std::numeric_limits<FwChanIdType>::max() >= TLMCHAN_HASH_BUCKETS,
23 "Cannot have more hash buckets than maximum telemetry ids in the system");
24 // TLMCHAN_HASH_BUCKETS >= TLMCHAN_NUM_TLM_HASH_SLOTS >= 0
25 static_assert(std::numeric_limits<FwChanIdType>::max() >= TLMCHAN_NUM_TLM_HASH_SLOTS,
26 "Cannot have more hash slots than maximum telemetry ids in the system");
27
28 // TLMCHAN_MAX_ENTRIES_PER_RUN must be defined in TlmChanImplCfg.hpp.
29 // It caps the number of updated telemetry entries that Run_handler will
30 // serialize and downlink in a single invocation. Any entries beyond this
31 // limit are skipped (deferred) for the current cycle and will be cleared
32 // by the next buffer swap, so they are effectively dropped rather than
33 // queued. Choose a value that keeps Run_handler's worst-case execution
34 // time within its rate-group budget.
35 static_assert(TLMCHAN_MAX_ENTRIES_PER_RUN > 0, "TLMCHAN_MAX_ENTRIES_PER_RUN must be greater than zero");
36 static_assert(TLMCHAN_MAX_ENTRIES_PER_RUN <= TLMCHAN_HASH_BUCKETS,
37 "TLMCHAN_MAX_ENTRIES_PER_RUN cannot exceed TLMCHAN_HASH_BUCKETS");
38
39 1 TlmChan::TlmChan(const char* name)
40
3/9
✓ Branch 2 taken 2 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
3 : TlmChanComponentBase(name), m_procCapCount(0), m_activeBuffer(ActiveBuffer::Buffer_0) {
41 1 FW_ASSERT(name != nullptr);
42
43 // clear slot pointers
44
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1 times.
16 for (FwChanIdType entry = 0; entry < TLMCHAN_NUM_TLM_HASH_SLOTS; entry++) {
45 15 this->m_tlmEntries[0].slots[entry] = nullptr;
46 15 this->m_tlmEntries[1].slots[entry] = nullptr;
47 }
48 // clear buckets
49
2/2
✓ Branch 0 taken 500 times.
✓ Branch 1 taken 1 times.
501 for (FwChanIdType entry = 0; entry < TLMCHAN_HASH_BUCKETS; entry++) {
50 500 this->m_tlmEntries[0].buckets[entry].used = false;
51 500 this->m_tlmEntries[0].buckets[entry].updated = false;
52 500 this->m_tlmEntries[0].buckets[entry].bucketNo = entry;
53 500 this->m_tlmEntries[0].buckets[entry].next = nullptr;
54 500 this->m_tlmEntries[0].buckets[entry].id = 0;
55 500 this->m_tlmEntries[1].buckets[entry].used = false;
56 500 this->m_tlmEntries[1].buckets[entry].updated = false;
57 500 this->m_tlmEntries[1].buckets[entry].bucketNo = entry;
58 500 this->m_tlmEntries[1].buckets[entry].next = nullptr;
59 500 this->m_tlmEntries[1].buckets[entry].id = 0;
60 }
61 // clear free index
62 1 this->m_tlmEntries[0].free = 0;
63 1 this->m_tlmEntries[1].free = 0;
64
65 // determine deployed channel size
66 1 this->m_chanIdSize = static_cast<U32>(sizeof(FwChanIdType));
67
68 // ------- Set random telemetry hash seed -------
69 1 U32 seed = 0;
70
71 // get current time and use as non-deterministic source for seed
72
1/1
✓ Branch 1 taken 1 times.
1 Os::RawTime rawTime;
73
1/1
✓ Branch 1 taken 1 times.
1 (void)rawTime.now();
74 1 U8 timeBuf[FW_RAW_TIME_SERIALIZATION_MAX_SIZE] = {};
75
1/1
✓ Branch 1 taken 1 times.
1 Fw::ExternalSerializeBuffer serBuf(timeBuf, sizeof(timeBuf));
76
1/1
✓ Branch 1 taken 1 times.
1 (void)rawTime.serializeTo(serBuf);
77
78 1 U32 foldedTime = 0;
79
1/1
✓ Branch 1 taken 1 times.
1 const U32 timeSize = static_cast<U32>(serBuf.getSize());
80
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
9 for (U32 i = 0; i < timeSize; i++) {
81 // Rotate-and-XOR each byte to avoid cancellation when bytes are equal
82 8 foldedTime = (foldedTime << 8) | (foldedTime >> 24);
83 8 foldedTime ^= static_cast<U32>(timeBuf[i]);
84 }
85
86 // read stack-address - address varies per boot
87 1 const U64 raw = reinterpret_cast<U64>(&seed);
88 1 const U32 foldedStack = static_cast<U32>(raw ^ (raw >> 32));
89
90 1 seed = foldedTime ^ foldedStack;
91
92 // Force a non-zero seed. Of the three hash paths, only the narrow
93 // (<16-bit) path actually loses its keying at seed == 0: it reverts to the
94 // original linear (id % MOD) % SLOTS reduction, re-exposing the predictable
95 // collision pattern this change removes. The Murmur3 and Wang paths still
96 // diffuse a zero seed correctly, so this guard is conservative for them.
97 // Substituting a known non-zero constant keeps every branch keyed and the
98 // seed uniform to reason about across platforms.
99
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (seed == 0) {
100 seed = 0xDEADBEEFU;
101 }
102
103 1 this->m_hashSeed = seed;
104 1 }
105
106
3/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
6 TlmChan::~TlmChan() {}
107
108 3068 FwChanIdType TlmChan::doHash(FwChanIdType id) const {
109 // Validate input before use.
110 static_assert(std::is_unsigned<FwChanIdType>::value, "FwChanIdType must be unsigned");
111 static_assert(sizeof(FwChanIdType) <= sizeof(U32), "FwChanIdType must fit within U32 for safe hash cast");
112 static_assert(TLMCHAN_NUM_TLM_HASH_SLOTS > 0, "TLMCHAN_NUM_TLM_HASH_SLOTS must be greater than zero");
113
114 FwChanIdType result;
115
116
1/2
✓ Branch 0 taken 3068 times.
✗ Branch 1 not taken.
3068 if (this->m_chanIdSize >= 4) {
117 // Verify id fits in U16 before narrowing cast
118 3068 FW_ASSERT(id <= static_cast<FwChanIdType>(std::numeric_limits<U32>::max()), static_cast<FwAssertArgType>(id));
119
120 3068 U32 h = static_cast<U32>(id) ^ static_cast<U32>(this->m_hashSeed);
121
122 // Murmur3 32-bit
123 3068 h ^= (h >> 16);
124 3068 h *= MURMUR3_C1;
125 3068 h ^= (h >> 13);
126 3068 h *= MURMUR3_C2;
127 3068 h ^= (h >> 16);
128
129 3068 result = static_cast<FwChanIdType>(h % TLMCHAN_NUM_TLM_HASH_SLOTS);
130 } else if (this->m_chanIdSize == 2) {
131 // Verify id fits in U16 before narrowing cast
132 FW_ASSERT(id <= static_cast<FwChanIdType>(std::numeric_limits<U16>::max()), static_cast<FwAssertArgType>(id));
133
134 U16 h = (static_cast<U16>(id)) ^ (static_cast<U16>(this->m_hashSeed) & static_cast<U16>(0xFFFFU));
135
136 // Wang 16-bit
137 h = static_cast<U16>(h ^ (h >> 7));
138 h = static_cast<U16>(h * WANG16_C1);
139 h = static_cast<U16>(h ^ (h >> 5));
140 h = static_cast<U16>(h * WANG16_C2);
141 h = static_cast<U16>(h ^ (h >> 3));
142
143 result = static_cast<FwChanIdType>(h % TLMCHAN_NUM_TLM_HASH_SLOTS);
144 } else {
145 // Verify id fits in U8 before narrowing cast
146 FW_ASSERT(id <= static_cast<FwChanIdType>(std::numeric_limits<U8>::max()), static_cast<FwAssertArgType>(id));
147
148 // FwChanIdType is smaller than 16 bits (at most 255 distinct channel IDs).
149 // XOR with the low byte of the seed before reduction to maintain consistency
150 const U8 h = static_cast<U8>(id) ^ (static_cast<U8>(this->m_hashSeed) & static_cast<U8>(0xFFU));
151 result = static_cast<FwChanIdType>((h % TLMCHAN_HASH_MOD_VALUE) % TLMCHAN_NUM_TLM_HASH_SLOTS);
152 }
153 3068 return result;
154 }
155
156 59 void TlmChan::pingIn_handler(const FwIndexType portNum, U32 key) {
157 static_assert(NUM_PINGIN_INPUT_PORTS == 1, "pingIn_handler expects exactly one input port");
158 // return key
159 59 this->pingOut_out(0, key);
160 59 }
161
162 Fw::TlmValid TlmChan::TlmGet_handler(FwIndexType portNum, FwChanIdType id, Fw::Time& timeTag, Fw::TlmBuffer& val) {
163 static_assert(NUM_TLMGET_INPUT_PORTS == 1, "TlmGet_handler expects exactly one input port");
164 FwChanIdType index = this->doHash(id);
165
166 // Search to see if channel has been stored
167 // check both buffers
168 // don't need to lock because this port is guarded
169 TlmEntry* activeEntry = this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index];
170 for (FwChanIdType bucket = 0; bucket < TLMCHAN_HASH_BUCKETS; bucket++) {
171 if (activeEntry) {
172 if (activeEntry->id == id) {
173 break;
174 } else {
175 activeEntry = activeEntry->next;
176 }
177 } else {
178 break;
179 }
180 }
181
182 TlmEntry* inactiveEntry = this->m_tlmEntries[1 - static_cast<U8>(this->m_activeBuffer)].slots[index];
183 for (FwChanIdType bucket = 0; bucket < TLMCHAN_HASH_BUCKETS; bucket++) {
184 if (inactiveEntry) {
185 if (inactiveEntry->id == id) {
186 break;
187 } else {
188 inactiveEntry = inactiveEntry->next;
189 }
190 } else {
191 break;
192 }
193 }
194
195 if (activeEntry && inactiveEntry) {
196 Fw::TimeComparison cmp = Fw::Time::compare(inactiveEntry->lastUpdate, activeEntry->lastUpdate);
197 if (cmp == Fw::TimeComparison::GT) {
198 val = inactiveEntry->buffer;
199 timeTag = inactiveEntry->lastUpdate;
200 return Fw::TlmValid::VALID;
201 } else if (cmp != Fw::TimeComparison::INCOMPARABLE) {
202 val = activeEntry->buffer;
203 timeTag = activeEntry->lastUpdate;
204 return Fw::TlmValid::VALID;
205 } else {
206 if (inactiveEntry->updated) {
207 val = inactiveEntry->buffer;
208 timeTag = inactiveEntry->lastUpdate;
209 return Fw::TlmValid::VALID;
210 } else {
211 val = activeEntry->buffer;
212 timeTag = activeEntry->lastUpdate;
213 return Fw::TlmValid::VALID;
214 }
215 }
216 } else if (activeEntry) {
217 val = activeEntry->buffer;
218 timeTag = activeEntry->lastUpdate;
219 return Fw::TlmValid::VALID;
220 } else if (inactiveEntry) {
221 val = inactiveEntry->buffer;
222 timeTag = inactiveEntry->lastUpdate;
223 return Fw::TlmValid::VALID;
224 } else {
225 val.resetSer();
226 }
227 return Fw::TlmValid::INVALID;
228 }
229
230 3068 void TlmChan::TlmRecv_handler(FwIndexType portNum, FwChanIdType id, Fw::Time& timeTag, Fw::TlmBuffer& val) {
231 static_assert(NUM_TLMRECV_INPUT_PORTS == 1, "TlmRecv_handler expects exactly one input port");
232 3068 FwChanIdType index = this->doHash(id);
233 3068 TlmEntry* entryToUse = nullptr;
234 3068 TlmEntry* prevEntry = nullptr;
235
236 // Search to see if channel has already been stored or a bucket needs to be added
237
2/2
✓ Branch 0 taken 3039 times.
✓ Branch 1 taken 29 times.
3068 if (this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index]) {
238 3039 entryToUse = this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index];
239 // Loop one extra time so that we don't inadvertently fall through the end of the loop early.
240
1/2
✓ Branch 0 taken 6621 times.
✗ Branch 1 not taken.
6621 for (FwChanIdType bucket = 0; bucket < TLMCHAN_HASH_BUCKETS + 1; bucket++) {
241
2/2
✓ Branch 0 taken 6558 times.
✓ Branch 1 taken 63 times.
6621 if (entryToUse) {
242
2/2
✓ Branch 0 taken 2976 times.
✓ Branch 1 taken 3582 times.
6558 if (entryToUse->id == id) {
243 2976 break;
244 } else {
245 3582 prevEntry = entryToUse;
246 3582 entryToUse = entryToUse->next;
247 }
248 } else {
249 // Out of buckets: drop the new channel rather than asserting, since
250 // telemetry IDs may arrive from external sources (e.g. a hub)
251
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 63 times.
63 if (this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free >= TLMCHAN_HASH_BUCKETS) {
252 this->log_WARNING_HI_TlmChanBucketPoolExhausted(id);
253 return;
254 }
255 // add new bucket from free list
256 63 entryToUse = &this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)]
257 63 .buckets[this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free++];
258 63 FW_ASSERT(prevEntry != nullptr);
259 63 prevEntry->next = entryToUse;
260 63 entryToUse->next = nullptr;
261 63 break;
262 }
263 }
264 } else {
265 // Out of buckets: drop the new channel rather than asserting, since
266 // telemetry IDs may arrive from external sources (e.g. a hub)
267
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free >= TLMCHAN_HASH_BUCKETS) {
268 this->log_WARNING_HI_TlmChanBucketPoolExhausted(id);
269 return;
270 }
271 29 this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index] =
272 29 &this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)]
273 29 .buckets[this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free++];
274 29 entryToUse = this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index];
275 29 entryToUse->next = nullptr;
276 }
277
278 3068 FW_ASSERT(entryToUse != nullptr);
279 3068 entryToUse->used = true;
280 3068 entryToUse->id = id;
281 3068 entryToUse->updated = true;
282 3068 entryToUse->lastUpdate = timeTag;
283 3068 entryToUse->buffer = val;
284 }
285
286 235 void TlmChan::Run_handler(FwIndexType portNum, U32 context) {
287 static_assert(NUM_RUN_INPUT_PORTS == 1, "Run_handler expects exactly one input port");
288 // Only write packets if connected
289
2/3
✓ Branch 1 taken 235 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 235 times.
235 if (not this->isConnected_PktSend_OutputPort(0)) {
290 return;
291 }
292
293 // Lock mutex long enough to swap the active buffer so the inactive buffer
294 // can be read without worrying about concurrent updates.
295
1/1
✓ Branch 1 taken 235 times.
235 this->lock();
296 235 this->m_activeBuffer =
297
2/2
✓ Branch 0 taken 118 times.
✓ Branch 1 taken 117 times.
235 (this->m_activeBuffer == ActiveBuffer::Buffer_0) ? ActiveBuffer::Buffer_1 : ActiveBuffer::Buffer_0;
298 // Clear the new active buffer's updated flags so it is clean for incoming
299 // writes. Any entries that were deferred (skipped) in the previous cycle
300 // and still carry updated=true in this buffer are also cleared here.
301 // This is intentional: deferred entries are dropped rather than re-queued,
302 // which preserves Run_handler's bounded execution-time guarantee.
303
2/2
✓ Branch 0 taken 117500 times.
✓ Branch 1 taken 235 times.
117735 for (U32 entry = 0; entry < TLMCHAN_HASH_BUCKETS; entry++) {
304 117500 this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].buckets[entry].updated = false;
305 }
306
1/1
✓ Branch 1 taken 235 times.
235 this->unLock();
307
308 // -----------------------------------------------------------------------
309 // CPU processing guard
310 //
311 // entriesProcessed — updated entries serialized into downlink packets this
312 // invocation. Hard-capped at TLMCHAN_MAX_ENTRIES_PER_RUN.
313 // entriesDeferred — updated entries skipped because the cap was already
314 // reached. These samples are dropped for this cycle.
315 // A non-zero value means the system is producing
316 // telemetry faster than Run_handler can drain it.
317 // -----------------------------------------------------------------------
318 235 U32 entriesProcessed = 0;
319 235 U32 entriesDeferred = 0;
320
321
1/1
✓ Branch 1 taken 235 times.
235 Fw::TlmPacket pkt;
322
1/1
✓ Branch 1 taken 235 times.
235 Fw::SerializeStatus resetStat = pkt.resetPktSer();
323 235 FW_ASSERT(Fw::FW_SERIALIZE_OK == resetStat, static_cast<FwAssertArgType>(resetStat));
324
325
2/2
✓ Branch 0 taken 117500 times.
✓ Branch 1 taken 235 times.
117735 for (U32 entry = 0; entry < TLMCHAN_HASH_BUCKETS; entry++) {
326 117500 TlmEntry* p_entry = &this->m_tlmEntries[1 - static_cast<U8>(this->m_activeBuffer)].buckets[entry];
327
3/4
✓ Branch 0 taken 2875 times.
✓ Branch 1 taken 114625 times.
✓ Branch 2 taken 2875 times.
✗ Branch 3 not taken.
117500 if ((p_entry->updated) && (p_entry->used)) {
328 // ------------------------------------------------------------------
329 // CPU guard check: once the per-run cap is reached, count this entry
330 // as deferred and skip serialization. The entry's updated flag will
331 // be cleared by the next buffer swap (see lock section above), so
332 // the sample is intentionally dropped for this cycle. This bounds
333 // Run_handler's worst-case execution time and prevents it from
334 // starving higher-priority tasks during a telemetry burst caused by
335 // a hardware anomaly, runaway component, software fault, or
336 // cyber-attack.
337 // ------------------------------------------------------------------
338
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2875 times.
2875 if (entriesProcessed >= TLMCHAN_MAX_ENTRIES_PER_RUN) {
339 entriesDeferred++;
340 continue;
341 }
342
343
1/1
✓ Branch 1 taken 2875 times.
2875 Fw::SerializeStatus stat = pkt.addValue(p_entry->id, p_entry->lastUpdate, p_entry->buffer);
344
345
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2874 times.
2875 if (Fw::FW_SERIALIZE_NO_ROOM_LEFT == stat) {
346
2/2
✓ Branch 1 taken 1 times.
✓ Branch 4 taken 1 times.
1 this->PktSend_out(0, pkt.getBuffer(), 0);
347
1/1
✓ Branch 1 taken 1 times.
1 resetStat = pkt.resetPktSer();
348 1 FW_ASSERT(Fw::FW_SERIALIZE_OK == resetStat, static_cast<FwAssertArgType>(resetStat));
349
1/1
✓ Branch 1 taken 1 times.
1 stat = pkt.addValue(p_entry->id, p_entry->lastUpdate, p_entry->buffer);
350 // If a single channel doesn't fit in an empty packet the packet
351 // is misconfigured; assert so the error is visible immediately.
352 1 FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, static_cast<FwAssertArgType>(stat));
353
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2874 times.
2874 } else if (Fw::FW_SERIALIZE_OK == stat) {
354 // room available, continue filling packet
355 } else {
356 FW_ASSERT(false, static_cast<FwAssertArgType>(stat));
357 }
358
359
1/1
✓ Branch 1 taken 2875 times.
2875 this->lock();
360 2875 p_entry->updated = false;
361
1/1
✓ Branch 1 taken 2875 times.
2875 this->unLock();
362 2875 entriesProcessed++;
363 }
364 }
365
366 // send remnant entries
367
3/3
✓ Branch 1 taken 235 times.
✓ Branch 3 taken 225 times.
✓ Branch 4 taken 10 times.
235 if (pkt.getNumEntries() > 0) {
368
2/2
✓ Branch 1 taken 225 times.
✓ Branch 4 taken 225 times.
225 this->PktSend_out(0, pkt.getBuffer(), 0);
369 }
370
371 // Emit a WARNING_HI event when the processing cap was reached this cycle.
372 // Using an event rather than injecting a reserved telemetry channel into
373 // the downlink stream is the correct F-Prime anomaly reporting mechanism.
374
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 235 times.
235 if (entriesDeferred > 0) {
375 this->m_procCapCount++;
376 this->log_WARNING_HI_TlmChanEpochProcessingCapReached(entriesDeferred, this->m_procCapCount);
377 }
378 235 }
379
380 } // namespace Svc
381