SatCat5
polling.cc
1 // Copyright 2021-2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/datetime.h>
7 #include <satcat5/interrupts.h> // For AtomicLock
8 #include <satcat5/list.h>
9 #include <satcat5/polling.h>
10 
11 namespace poll = satcat5::poll;
16 
17 // Enable runtime checks for severe infrastructure errors?
18 // This aids debugging but has severe performance penalties.
19 // Setting this flag is not recommended for production designs.
20 #ifndef SATCAT5_PARANOIA
21 #define SATCAT5_PARANOIA 0
22 #endif
23 
24 #if SATCAT5_PARANOIA
25  #include <satcat5/log.h>
26  static unsigned panic(const char* label) {
27  static constexpr unsigned* NULLPTR = 0;
28  satcat5::log::Log(satcat5::log::CRITICAL, label);
29  return *NULLPTR; // Intentionally segfault.
30  }
31 #else
32  static unsigned panic(const char* label) {return 0;} // GCOVR_EXCL_LINE
33 #endif
34 
35 // Global linked list for each major polling type:
36 // (Use of global pointer ensures this is initialized before any
37 // individual object constructor, including other globals.)
38 static poll::Always* g_list_always = 0;
39 static poll::OnDemand* g_list_demand = 0;
40 static poll::Timer* g_list_timer = 0;
41 
42 // Placeholder used if no other timer is available.
43 satcat5::util::NullTimer null_timer;
44 
45 // Global pointer to the preferred timer object.
46 // (As above, syntax ensures expected initialization order.)
47 static TimeRef* g_main_timer = &null_timer;
48 
49 // Global helper object for Timers.
51 
52 // Human-readable label for AtomicLock.
53 static const char* const LBL_POLL = "POLL";
54 
65 public:
66  // Constructor is the only public method.
67  OnDemandHelper() : m_item(nullptr) {}
68 
69  // Are we holding on to a working sublist?
70  inline unsigned count() const
71  {return ListCore::len(m_item);}
72 
73  // Sanity check before starting each unit test:
74  // * Confirm this object is the only item in "g_list_always".
75  // * Confirm the global clock is the only item in "g_list_timer".
76  // * Forcibly discard all pending OnDemand objects.
77  bool pre_test_reset() {
78  bool ok = true;
79  if (ListCore::pre_test_reset<poll::Always>(g_list_always, this)) {ok = false;}
80  if (ListCore::pre_test_reset<poll::Timer>(g_list_timer, &clock)) {ok = false;}
81  if (m_item) {m_item = nullptr; ok = false;}
82  if (m_next) {m_next = nullptr; ok = false;}
83  return ok;
84  }
85 
86  // Remove an item from the global list or the working sublist.
87  // (Safe to call "remove" on both lists, no-op if there's no match.)
88  void remove(poll::OnDemand* ptr) {
89  AtomicLock lock(LBL_POLL);
90  ListCore::remove(g_list_demand, ptr);
91  if (m_item) ListCore::remove(m_item, ptr);
92  }
93 
94 private:
95  // In rare cases, such as the wait loop of "cfgbus_remote", the
96  // poll_always() method may be called recursively. To avoid
97  // leaving orphaned leftovers, retain the working pointer.
98  poll::OnDemand *m_item;
99 
100  // Atomically claim the current global list, and create
101  // an empty one in its place for future requests.
102  inline void list_start() {
103  AtomicLock lock(LBL_POLL);
104  m_item = g_list_demand;
105  g_list_demand = 0;
106  }
107 
108  // Atomically pop the current item from the queue, updating
109  // associated pointers and status flags to mark it as idle.
110  inline poll::OnDemand* list_pop() {
111  AtomicLock lock(LBL_POLL);
112  poll::OnDemand* temp = m_item;
113  m_item = m_item->m_next;
114  temp->m_idle = 1;
115  temp->m_next = 0;
116  return temp;
117  }
118 
119  // Poll each block on the "demand" list, resuming work in progress if
120  // possible. Reset the state of each item just before we process it.
121  void poll_always() override {
122  if (!m_item) list_start();
123  if (SATCAT5_PARANOIA && ListCore::has_loop(m_item)) {
124  panic("poll_demand"); return;
125  }
126  while (m_item) {
127  poll::OnDemand* next = list_pop();
128  next->poll_demand();
129  }
130  }
131 } on_demand_helper;
132 
133 // Forcibly reset on_demand_helper and unregister all other global event handlers.
135  bool ok = true;
136  if (!on_demand_helper.pre_test_reset()) ok = false;
137  if (!timekeeper.pre_test_reset()) ok = false;
138  if (g_list_demand) {g_list_demand = 0; ok = false;}
139  satcat5::datetime::clock.reset(true);
140  return ok;
141 }
142 
144  // Optional sanity check before we start.
145  if (SATCAT5_PARANOIA && ListCore::has_loop(g_list_always)) {
146  panic("poll_always"); return;
147  }
148  // Poll each block on the global list exactly once.
149  // (This includes the on_demand_helper defined above.)
150  poll::Always* item = g_list_always;
151  while (item) {
152  item->poll_always();
153  item = item->m_next;
154  }
155 }
156 
157 void poll::service_all(unsigned limit) {
158  // Always poll at least once.
159  poll::service();
160 
161  // Continue until demand list is empty or iteration limit is reached.
162  while (g_list_demand && limit) {
163  poll::service();
164  --limit;
165  }
166 }
167 
168 poll::Always::Always(bool auto_register) {
169  if (auto_register) { poll_register(); }
170 }
171 
172 #if SATCAT5_ALLOW_DELETION
173 poll::Always::~Always() {
174  poll_unregister();
175 }
176 #endif
177 
179  AtomicLock lock(LBL_POLL);
180  return ListCore::len(g_list_always);
181 }
182 
184  // Add this item to the head of the global list.
185  AtomicLock lock(LBL_POLL);
186  ListCore::add_safe(g_list_always, this);
187 }
188 
190  // Remove ourselves from the global linked list.
191  AtomicLock lock(LBL_POLL);
192  ListCore::remove(g_list_always, this);
193 }
194 
195 #if SATCAT5_ALLOW_DELETION
196 poll::OnDemand::~OnDemand() {
197  AtomicLock lock(LBL_POLL);
198 
199  // If we're idle, there's nothing else to do.
200  if (m_idle) return;
201 
202  // Otherwise, remove ourselves from the pending queue.
203  // (This may be either g_list_demand or on_demand_helper.)
204  on_demand_helper.remove(this);
205 }
206 #endif
207 
209  // After safety-check, add this item to the head of the list.
210  // (Re-adding an item creates an infinite loop in the linked-list.)
211  AtomicLock lock(LBL_POLL);
212  if (m_idle) {
213  m_idle = 0;
214  if (SATCAT5_PARANOIA && ListCore::contains(g_list_demand, this)) {
215  panic("poll_request");
216  } else {
217  ListCore::add(g_list_demand, this);
218  }
219  }
220 }
221 
223  // If applicable, remove ourselves from the pending-item list.
224  AtomicLock lock(LBL_POLL);
225  if (!m_idle) {
226  m_idle = 1;
227  on_demand_helper.remove(this);
228  }
229 }
230 
232  AtomicLock lock(LBL_POLL);
233  return ListCore::len(g_list_demand);
234 }
235 
236 poll::Timekeeper::Timekeeper()
237  : m_tref(g_main_timer->now())
238 {
239  // No other initialization required.
240 }
241 
243  return g_main_timer != &null_timer;
244 }
245 
247  return g_main_timer;
248 }
249 
251  // Atomically set the global clock pointer.
252  AtomicLock lock(LBL_POLL);
253  g_main_timer = timer ? timer : &null_timer;
254  m_tref = g_main_timer->now();
255 }
256 
258  // Keep the clock with better resolution
259  AtomicLock lock(LBL_POLL);
260  if (timer && timer->ticks_per_msec() > g_main_timer->ticks_per_msec())
261  set_clock(timer);
262 }
263 
265  // Since timekeeper is global, explicitly purge persistent state.
266  request_cancel(); // Cancel any pending callbacks.
267  set_clock(0); // Reset the reference clock.
268  return true; // All initial states are valid.
269 }
270 
272  // Measure elapsed time if a reference clock is available.
273  unsigned elapsed_msec = 1; // Default = 1 msec
274  if (clock_ready()) {
275  // Measure elapsed time since last call to "elapsed_msec".
276  // ("m_tref" updated to g_main_timer->now(), less fractional leftovers.)
277  AtomicLock lock(LBL_POLL);
278  elapsed_msec = m_tref.increment_msec();
279  if (!elapsed_msec) return; // Less than 1 msec elapsed?
280  }
281  // Optional sanity check before we start.
282  if (SATCAT5_PARANOIA && ListCore::has_loop(g_list_timer)) {
283  panic("poll_timer"); return;
284  }
285  // Check on each of the registered Timer objects.
286  poll::Timer* item = g_list_timer;
287  while(item) {
288  item->query(elapsed_msec);
289  item = item->m_next;
290  }
291 }
292 
294  : m_next(0)
295  , m_trem(0)
296  , m_tnext(0)
297 {
298  // Add this item to the head of the global list.
299  AtomicLock lock(LBL_POLL);
300  ListCore::add(g_list_timer, this);
301 }
302 
303 #if SATCAT5_ALLOW_DELETION
304 poll::Timer::~Timer() {
305  // Remove ourselves from the global linked list.
306  AtomicLock lock(LBL_POLL);
307  ListCore::remove(g_list_timer, this);
308 }
309 #endif
310 
312  AtomicLock lock(LBL_POLL);
313  return ListCore::len(g_list_timer);
314 }
315 
316 void poll::Timer::timer_once(unsigned msec) {
317  m_trem = msec;
318  m_tnext = 0;
319 }
320 
321 void poll::Timer::timer_every(unsigned msec) {
322  m_trem = msec;
323  m_tnext = msec;
324 }
325 
327  m_trem = 0;
328  m_tnext = 0;
329 }
330 
331 void poll::Timer::query(unsigned elapsed_msec) {
332  if (m_trem > elapsed_msec) {
333  // Continue countdown...
334  m_trem -= elapsed_msec;
335  } else if (m_trem) {
336  // Repeating timers adjust next interval to minimize cumulative drift.
337  // (Do this first, since timer_event() may change the configuration.)
338  unsigned ovr = elapsed_msec - m_trem;
339  if (m_tnext > ovr) {
340  // Overshoot is small enough to compensate accurately.
341  // e.g., If timer scheduled every 1000 msec fires 5 msec late,
342  // then next interval should be 995 msec to get back on schedule.
343  m_trem = m_tnext - ovr;
344  } else if (m_tnext) {
345  // Overshoot is too large to fix, minimum delay is 1 msec.
346  m_trem = 1;
347  } else {
348  // Stop after one-time event
349  m_trem = 0;
350  }
351 
352  // Process the timer event notification.
353  // (Any configuration changes overwrite the calculations above.)
354  timer_event();
355  }
356 }
357 
358 poll::TimerAdapter::TimerAdapter(poll::OnDemand* target)
359  : m_target(target)
360 {
361  // Parent should call timer_once(), timer_every(), etc.
362 }
363 
365  m_target->request_poll();
366 }
367 
369  : m_target(obj)
370  , m_interval(usec)
371  , m_tref(SATCAT5_CLOCK->now())
372 {
373  // Nothing else to initialize.
374 }
375 
377  if (m_tref.interval_usec(m_interval))
378  m_target->request_poll();
379 }
380 
void reset(bool full=false)
Reset internals after changes to SATCAT5_CLOCK.
Definition: datetime.cc:49
Automatic lock or mutex.
VirtualTimer(satcat5::poll::OnDemand *obj, unsigned usec=1000)
Poll the designated object once every N microseconds.
Definition: polling.cc:368
void poll_always() override
Child class MUST override this method.
Definition: polling.cc:376
The Log class creates and formats one log message.
Definition: log.h:195
An "Always" object is polled whenever service() is called.
Definition: polling.h:96
virtual void poll_always()=0
Child class MUST override this method.
static unsigned count_always()
Count active objects of this type.
Definition: polling.cc:178
void poll_register()
Register this pollable object, called by the constructor.
Definition: polling.cc:183
void poll_unregister()
Unregister this pollable object, called by the destructor.
Definition: polling.cc:189
Always(bool auto_register=true)
Registers this pollable object unless auto_register = false.
Definition: polling.cc:168
Global helper object that services all OnDemand objects.
Definition: polling.cc:64
void poll_always() override
Child class MUST override this method.
Definition: polling.cc:121
An "OnDemand" object is polled only on request.
Definition: polling.h:131
virtual void poll_demand()=0
Deferred event handler, called after request().
void request_poll()
Call this method to request polling at a later time.
Definition: polling.cc:208
void request_cancel()
Call this method to cancel a previous request_poll().
Definition: polling.cc:222
static unsigned count_ondemand()
Count queued objects of this type (i.e., non-idle).
Definition: polling.cc:231
Global coordinator for multiple Timer objects.
Definition: polling.h:168
void set_clock(satcat5::util::TimeRef *timer)
Immediately set the system time reference.
Definition: polling.cc:250
bool clock_ready() const
Has a system time reference been provided?
Definition: polling.cc:242
void suggest_clock(satcat5::util::TimeRef *timer)
Compare the provided reference to the current TimeRef, and keep whichever is "better" by an internal ...
Definition: polling.cc:257
bool pre_test_reset()
Reset timekeeper state at the start of each unit test.
Definition: polling.cc:264
satcat5::util::TimeRef * get_clock() const
Get the system time reference, if one is set.
Definition: polling.cc:246
void poll_demand() override
Deferred event handler, called after request().
Definition: polling.cc:271
void timer_event() override
Child class MUST override this method.
Definition: polling.cc:364
Timer objects are polled after a fixed delay or at a regular interval.
Definition: polling.h:213
Timer()
Register object in the idle state.
Definition: polling.cc:293
void timer_stop()
Stop all future notifications.
Definition: polling.cc:326
void timer_once(unsigned msec)
Configure a one-time notification after X milliseconds.
Definition: polling.cc:316
static unsigned count_timer()
Count all objects of this type, including idle timers.
Definition: polling.cc:311
void timer_every(unsigned msec)
Configure a repeating notification every X milliseconds.
Definition: polling.cc:321
Helper functions for manipulating singly-linked lists.
Definition: list.h:52
Placeholder used if no timer is available.
Definition: timeref.h:200
The TimeRef API provides access to a monotonic time-counter.
Definition: timeref.h:142
u32 ticks_per_msec() const
Stable accessors for unit conversion.
Definition: timeref.h:179
TimeVal now()
Create a TimeVal object using the tick-count from raw().
Definition: timeref.cc:65
Real-time clock conversion functions.
satcat5::datetime::Clock clock
Global instance of the datetime::Clock class.
Definition: datetime.cc:16
Templated functions for manipulating singly-linked lists.
Diagnostic logging to UART and/or Ethernet ports.
Core event-processing loop for SatCat5 software.
Timekeeper timekeeper
There is a single global instance of the Timekeeper class.
Definition: polling.cc:50
bool pre_test_reset()
Hard-reset of global variables at the start of each unit test.
Definition: polling.cc:134
void service()
Single-pass service loop.
Definition: polling.cc:143
void service_all(unsigned limit=100)
Multi-pass service loop Calling this function regularly is required for SatCat5 operation.
Definition: polling.cc:157
Platform-agnostic API for interrupt management.