F´ Flight Software - C/C++ Documentation
A framework for building embedded system applications to NASA flight quality standards.
TlmChan.cpp
Go to the documentation of this file.
1 
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 TlmChan::TlmChan(const char* name)
40  : TlmChanComponentBase(name), m_procCapCount(0), m_activeBuffer(ActiveBuffer::Buffer_0) {
41  FW_ASSERT(name != nullptr);
42 
43  // clear slot pointers
44  for (FwChanIdType entry = 0; entry < TLMCHAN_NUM_TLM_HASH_SLOTS; entry++) {
45  this->m_tlmEntries[0].slots[entry] = nullptr;
46  this->m_tlmEntries[1].slots[entry] = nullptr;
47  }
48  // clear buckets
49  for (FwChanIdType entry = 0; entry < TLMCHAN_HASH_BUCKETS; entry++) {
50  this->m_tlmEntries[0].buckets[entry].used = false;
51  this->m_tlmEntries[0].buckets[entry].updated = false;
52  this->m_tlmEntries[0].buckets[entry].bucketNo = entry;
53  this->m_tlmEntries[0].buckets[entry].next = nullptr;
54  this->m_tlmEntries[0].buckets[entry].id = 0;
55  this->m_tlmEntries[1].buckets[entry].used = false;
56  this->m_tlmEntries[1].buckets[entry].updated = false;
57  this->m_tlmEntries[1].buckets[entry].bucketNo = entry;
58  this->m_tlmEntries[1].buckets[entry].next = nullptr;
59  this->m_tlmEntries[1].buckets[entry].id = 0;
60  }
61  // clear free index
62  this->m_tlmEntries[0].free = 0;
63  this->m_tlmEntries[1].free = 0;
64 
65  // determine deployed channel size
66  this->m_chanIdSize = static_cast<U32>(sizeof(FwChanIdType));
67 
68  // ------- Set random telemetry hash seed -------
69  U32 seed = 0;
70 
71  // get current time and use as non-deterministic source for seed
72  Os::RawTime rawTime;
73  (void)rawTime.now();
75  Fw::ExternalSerializeBuffer serBuf(timeBuf, sizeof(timeBuf));
76  (void)rawTime.serializeTo(serBuf);
77 
78  U32 foldedTime = 0;
79  const U32 timeSize = static_cast<U32>(serBuf.getSize());
80  for (U32 i = 0; i < timeSize; i++) {
81  // Rotate-and-XOR each byte to avoid cancellation when bytes are equal
82  foldedTime = (foldedTime << 8) | (foldedTime >> 24);
83  foldedTime ^= static_cast<U32>(timeBuf[i]);
84  }
85 
86  // read stack-address - address varies per boot
87  const U64 raw = reinterpret_cast<U64>(&seed);
88  const U32 foldedStack = static_cast<U32>(raw ^ (raw >> 32));
89 
90  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  if (seed == 0) {
100  seed = 0xDEADBEEFU;
101  }
102 
103  this->m_hashSeed = seed;
104 }
105 
107 
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  if (this->m_chanIdSize >= 4) {
117  // Verify id fits in U16 before narrowing cast
118  FW_ASSERT(id <= static_cast<FwChanIdType>(std::numeric_limits<U32>::max()), static_cast<FwAssertArgType>(id));
119 
120  U32 h = static_cast<U32>(id) ^ static_cast<U32>(this->m_hashSeed);
121 
122  // Murmur3 32-bit
123  h ^= (h >> 16);
124  h *= MURMUR3_C1;
125  h ^= (h >> 13);
126  h *= MURMUR3_C2;
127  h ^= (h >> 16);
128 
129  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  return result;
154 }
155 
156 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  this->pingOut_out(0, key);
160 }
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 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  FwChanIdType index = this->doHash(id);
233  TlmEntry* entryToUse = nullptr;
234  TlmEntry* prevEntry = nullptr;
235 
236  // Search to see if channel has already been stored or a bucket needs to be added
237  if (this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index]) {
238  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  for (FwChanIdType bucket = 0; bucket < TLMCHAN_HASH_BUCKETS + 1; bucket++) {
241  if (entryToUse) {
242  if (entryToUse->id == id) {
243  break;
244  } else {
245  prevEntry = entryToUse;
246  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  if (this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free >= TLMCHAN_HASH_BUCKETS) {
253  return;
254  }
255  // add new bucket from free list
256  entryToUse = &this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)]
257  .buckets[this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free++];
258  FW_ASSERT(prevEntry != nullptr);
259  prevEntry->next = entryToUse;
260  entryToUse->next = nullptr;
261  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  if (this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free >= TLMCHAN_HASH_BUCKETS) {
269  return;
270  }
271  this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index] =
272  &this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)]
273  .buckets[this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].free++];
274  entryToUse = this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].slots[index];
275  entryToUse->next = nullptr;
276  }
277 
278  FW_ASSERT(entryToUse != nullptr);
279  entryToUse->used = true;
280  entryToUse->id = id;
281  entryToUse->updated = true;
282  entryToUse->lastUpdate = timeTag;
283  entryToUse->buffer = val;
284 }
285 
286 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  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  this->lock();
296  this->m_activeBuffer =
297  (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  for (U32 entry = 0; entry < TLMCHAN_HASH_BUCKETS; entry++) {
304  this->m_tlmEntries[static_cast<U8>(this->m_activeBuffer)].buckets[entry].updated = false;
305  }
306  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  U32 entriesProcessed = 0;
319  U32 entriesDeferred = 0;
320 
321  Fw::TlmPacket pkt;
322  Fw::SerializeStatus resetStat = pkt.resetPktSer();
323  FW_ASSERT(Fw::FW_SERIALIZE_OK == resetStat, static_cast<FwAssertArgType>(resetStat));
324 
325  for (U32 entry = 0; entry < TLMCHAN_HASH_BUCKETS; entry++) {
326  TlmEntry* p_entry = &this->m_tlmEntries[1 - static_cast<U8>(this->m_activeBuffer)].buckets[entry];
327  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  if (entriesProcessed >= TLMCHAN_MAX_ENTRIES_PER_RUN) {
339  entriesDeferred++;
340  continue;
341  }
342 
343  Fw::SerializeStatus stat = pkt.addValue(p_entry->id, p_entry->lastUpdate, p_entry->buffer);
344 
345  if (Fw::FW_SERIALIZE_NO_ROOM_LEFT == stat) {
346  this->PktSend_out(0, pkt.getBuffer(), 0);
347  resetStat = pkt.resetPktSer();
348  FW_ASSERT(Fw::FW_SERIALIZE_OK == resetStat, static_cast<FwAssertArgType>(resetStat));
349  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  FW_ASSERT(Fw::FW_SERIALIZE_OK == stat, static_cast<FwAssertArgType>(stat));
353  } 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  this->lock();
360  p_entry->updated = false;
361  this->unLock();
362  entriesProcessed++;
363  }
364  }
365 
366  // send remnant entries
367  if (pkt.getNumEntries() > 0) {
368  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  if (entriesDeferred > 0) {
375  this->m_procCapCount++;
376  this->log_WARNING_HI_TlmChanEpochProcessingCapReached(entriesDeferred, this->m_procCapCount);
377  }
378 }
379 
380 } // namespace Svc
Serialization/Deserialization operation was successful.
Serializable::SizeType getSize() const override
Get current buffer size.
Status now() override
Get the current time.
TlmChan(const char *compName)
Definition: TlmChan.cpp:39
No room left in the buffer to serialize data.
bool isConnected_PktSend_OutputPort(FwIndexType portNum) const
Fw::ComBuffer & getBuffer()
get buffer to send to the ground
Definition: TlmPacket.cpp:55
Auto-generated base for TlmChan component.
Component that stores telemetry channel values.
FwSizeType getNumEntries()
get the number of packets added via addValue()
Definition: TlmPacket.cpp:51
virtual ~TlmChan()
Definition: TlmChan.cpp:106
SerializeStatus
forward declaration for string
SerializeStatus addValue(FwChanIdType id, Time &timeTag, TlmBuffer &buffer)
Add telemetry value to buffer.
Definition: TlmPacket.cpp:63
void PktSend_out(FwIndexType portNum, Fw::ComBuffer &data, U32 context) const
Invoke output port PktSend.
virtual void lock()
Lock the guarded mutex.
void pingOut_out(FwIndexType portNum, U32 key) const
Invoke output port pingOut.
External serialize buffer with no copy semantics.
FwIdType FwChanIdType
The type of a telemetry channel identifier.
SerializeStatus resetPktSer()
Reset serialization of values. This should be done when starting to accumulate a new set of values...
Definition: TlmPacket.cpp:20
void log_WARNING_HI_TlmChanEpochProcessingCapReached(U32 numDeferred, U32 numTimesDeferredCountReached) const
void resetSer() override
Reset serialization pointer to beginning of buffer.
uint8_t U8
8-bit unsigned integer
Definition: BasicTypes.h:54
static TimeComparison compare(const Time &time1, const Time &time2)
Definition: Time.cpp:149
Fw::SerializeStatus serializeTo(Fw::SerialBufferBase &buffer, Fw::Endianness mode=Fw::Endianness::BIG) const override
Serialize the contents of the RawTimeInterface object into a buffer.
PlatformIndexType FwIndexType
virtual void unLock()
Unlock the guarded mutex.
RateGroupDivider component implementation.
#define FW_ASSERT(...)
Definition: Assert.hpp:14
FwChanIdType doHash(FwChanIdType id) const
Definition: TlmChan.cpp:108
void log_WARNING_HI_TlmChanBucketPoolExhausted(FwChanIdType Id)