1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
|
/*
Copyright (C) 2026
Samuel Hunt, Maxxwave Ltd. sam@maxxwave.co.uk
Implementation of a priority-based FIG carousel scheduler.
This scheduler uses weighted priority classes with round-robin
within each class to provide fair bandwidth allocation and
prevent starvation.
*/
/*
This file is part of ODR-DabMux.
ODR-DabMux is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
ODR-DabMux is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ODR-DabMux. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "fig/FIG.h"
#include "fig/FIG0.h"
#include "fig/FIG0_20.h"
#include "fig/FIG1.h"
#include "fig/FIG2.h"
#include "fig/FIGSchedulerType.h"
#include "MuxElements.h"
#include "Log.h"
#include <list>
#include <vector>
#include <array>
#include <map>
#include <unordered_set>
#include <memory>
#include <string>
namespace FIC {
// Number of priority levels (0-9)
constexpr int NUM_PRIORITIES = 10;
// Priority 0 is special (always sent every frame)
constexpr int PRIORITY_CRITICAL = 0;
// Debug flag for carousel tracing - set to 1 to enable verbose logging
// Note: Uses 'info' level so output is visible without -v flag
#define PRIORITY_CAROUSEL_DEBUG 0
// Debug flag for repetition rate statistics - logs actual vs required rates
#define PRIORITY_RATE_STATS_DEBUG 0
/* Helper function to calculate the deadline for the next transmission, in milliseconds.
* All values are multiples of 24ms (ETI frame duration) for easier reasoning.
*/
inline int rate_increment_ms(FIG_rate rate)
{
switch (rate) {
case FIG_rate::FIG0_0: return 96; // Special case for FIG 0/0
case FIG_rate::A: return 240; // At least 10 times per second
case FIG_rate::A_B: return 480; // Between 10 times and once per second
case FIG_rate::B: return 960; // Once per second
case FIG_rate::C: return 24000; // Once every 10 seconds
case FIG_rate::D: return 30000; // Less than once every 10 seconds
case FIG_rate::E: return 120000; // All in two minutes
}
return 960; // Default to rate B if unknown
}
/*
* FIGEntryPriority - Holds a FIG and its scheduling state
*
* Deadline/Cycle Model:
* ---------------------
* - deadline_ms: Countdown timer, reset ONLY when cycle completes
* - must_send: Flag indicating a cycle is in progress and not yet complete
* - rate_ms: The required repetition rate from the FIG's specification
*
* Lifecycle:
* 1. Counter starts at rate_ms, must_send = true (cycle in progress)
* 2. Counter ticks down every 24ms
* 3. When FIG completes its cycle (complete_fig_transmitted = true):
* - must_send = false
* - deadline_ms = rate_ms (reset for next cycle)
* 4. When deadline_ms reaches 0:
* - If must_send == true: VIOLATION (cycle didn't complete in time)
* - If must_send == false: Start new cycle (must_send = true)
* Note: DON'T reset deadline_ms here - it continues counting to track lateness
*
* In a lightly loaded mux:
* - can_send completes cycles well before deadline
* - deadline_ms gets reset early, must_send is briefly true then false
* - When deadline_ms hits 0, must_send is false, so we just start new cycle
*
* In a heavily loaded mux:
* - must_send stays true longer as cycle takes time to complete
* - If deadline_ms hits 0 while must_send is true, violation is logged
*
* Repetition rate tracking (for debug/verification):
* - last_cycle_complete_ms: Timestamp when last cycle completed
* - cycle_count: Number of completed cycles
* - total_cycle_time_ms: Sum of all cycle times (for averaging)
* - min_cycle_time_ms / max_cycle_time_ms: Range tracking
*/
struct FIGEntryPriority {
IFIG* fig = nullptr;
// Scheduling state
bool must_send = false; // Cycle is in progress, not yet complete
// Deadline monitoring
int deadline_ms = 0; // Countdown timer - ONLY reset on cycle complete
int rate_ms = 0; // Required repetition rate (from FIG_rate)
bool deadline_violated = false; // Set if deadline expires before cycle completes
// For future dynamic priority adjustment
int assigned_priority = 0; // Current priority assignment
int base_priority = 0; // Original priority assignment
// Repetition rate statistics (for verification)
uint64_t last_cycle_complete_ms = 0; // Timestamp of last completion
uint64_t cycle_count = 0; // Number of completed cycles
uint64_t total_cycle_time_ms = 0; // For calculating average
uint64_t min_cycle_time_ms = UINT64_MAX;
uint64_t max_cycle_time_ms = 0;
uint64_t current_time_ms = 0; // Updated each frame
std::string name() const {
if (fig) {
return fig->name();
}
return "unknown";
}
// Returns true if this FIG has used more than the given percentage of its deadline
// Used to prioritize FIGs that actually need bandwidth over those running ahead
bool past_deadline_percent(int percent) const {
// deadline_ms counts down from rate_ms
// If deadline_ms < rate_ms * (100 - percent) / 100, we're past that percent
// e.g., for 50%: if deadline_ms < rate_ms/2, we're past 50%
int threshold = rate_ms * (100 - percent) / 100;
return deadline_ms < threshold;
}
void init_deadline() {
rate_ms = rate_increment_ms(fig->repetition_rate());
// FIG 0/7 has special timing - only sent at framephase 0
// Give it an extra frame margin to avoid false violation warnings
if (fig->figtype() == 0 && fig->figextension() == 7) {
deadline_ms = rate_ms + 24; // Extra frame margin
} else {
deadline_ms = rate_ms;
}
must_send = true; // Start with cycle due
}
// Called every frame (24ms). Returns true if a new cycle should start.
bool tick_deadline(int elapsed_ms) {
current_time_ms += elapsed_ms;
deadline_ms -= elapsed_ms;
if (deadline_ms <= 0) {
if (must_send) {
// Deadline expired but cycle not complete - VIOLATION
if (!deadline_violated) {
deadline_violated = true;
}
// Don't start new cycle - current one isn't done yet
// Let deadline go negative to track how late we are
return false;
} else {
// Deadline expired and cycle is complete - start new cycle
// Note: We do NOT reset deadline_ms here. It was already reset
// when the cycle completed. If we're here, it means the cycle
// completed exactly on time or the counter wrapped.
// Actually, if must_send is false and deadline <= 0, the cycle
// completed and deadline was reset, then counted down again.
// So we should start a new cycle.
return true;
}
}
return false;
}
void on_cycle_complete(bool data_was_sent = true) {
// Track repetition rate statistics only if data was actually sent
// FIGs that return 0 bytes (nothing to send) shouldn't count as "cycles"
// as they don't consume bandwidth and skew the statistics
if (data_was_sent && last_cycle_complete_ms > 0) {
uint64_t cycle_time = current_time_ms - last_cycle_complete_ms;
// Only count if meaningful time has passed (avoid artifacts from
// multiple completions in same frame due to timing granularity)
if (cycle_time > 0) {
cycle_count++;
total_cycle_time_ms += cycle_time;
if (cycle_time < min_cycle_time_ms) min_cycle_time_ms = cycle_time;
if (cycle_time > max_cycle_time_ms) max_cycle_time_ms = cycle_time;
}
}
if (data_was_sent) {
last_cycle_complete_ms = current_time_ms;
}
// Reset deadline for next cycle
// FIG 0/7 needs extra margin for framephase timing
if (fig->figtype() == 0 && fig->figextension() == 7) {
deadline_ms = rate_ms + 24;
} else {
deadline_ms = rate_ms;
}
must_send = false;
// Note: deadline_violated is NOT cleared here
// It will be logged and cleared by the monitoring system
}
void start_new_cycle() {
must_send = true;
// Note: Do NOT reset deadline_ms here!
// The deadline was reset when the previous cycle completed.
// If we reset here, we lose track of timing.
}
// Get average cycle time in ms (0 if no data)
uint64_t avg_cycle_time_ms() const {
return (cycle_count > 0) ? (total_cycle_time_ms / cycle_count) : 0;
}
// Reset statistics (call periodically to get recent averages)
void reset_stats() {
cycle_count = 0;
total_cycle_time_ms = 0;
min_cycle_time_ms = UINT64_MAX;
max_cycle_time_ms = 0;
// Don't reset last_cycle_complete_ms - need it for next cycle measurement
}
};
/*
* PriorityLevel - A priority class containing multiple FIGs
*
* Each priority has:
* - poll_counter: Decrements on each FIG send, determines when this priority is due
* - poll_reset_value: Value to reset counter to (2^priority for priorities 1+)
* - carousel: Round-robin list of FIGs, front = least recently sent
*/
struct PriorityLevel {
int priority = 0;
int poll_counter = 0;
int poll_reset_value = 1;
std::list<FIGEntryPriority*> carousel;
// Find first FIG with must_send set
FIGEntryPriority* find_must_send();
// Find first FIG in carousel (for can_send - any FIG can potentially send)
FIGEntryPriority* find_can_send();
// Move entry to back of carousel (only call after successful send!)
void move_to_back(FIGEntryPriority* entry);
// Check if any FIG in this priority has must_send
bool has_must_send() const;
// Check if carousel is non-empty
bool has_can_send() const;
};
/*
* FIGCarouselPriority - Priority-based FIG scheduler
*
* Scheduling algorithm:
*
* 1. Priority 0 (FIG 0/0, 0/7) handled specially:
* - Only sent in FIB 0 at framephase 0 (once per 96ms)
* - FIG 0/0 must be first FIG in FIB per EN 300 401 clause 6.4.1
*
* 2. Must-send pass (clear urgent FIGs):
* - Select priority via weighted counter system
* - If selected priority has must_send FIG, try to send it
* - If not, CASCADE: try lower priorities, then wrap to 1 and continue
* - Only move FIG to back of carousel if it actually wrote bytes
* - Continue until no must_send FIGs remain or FIB full
*
* 3. Can-send pass (fill remaining space opportunistically):
* - Same cascading logic as must-send
* - Any FIG can send even if not "due" - improves receiver acquisition
* - Continue until FIB full or all FIGs tried
*
* Counter mechanism:
* - All counters decrement when ANY FIG successfully sends
* - When a priority sends, its counter resets to poll_reset_value
* - Priority with counter=0 (or lowest weighted score) is selected
*
* Carousel ordering:
* - Front of carousel = least recently sent
* - FIGs only move to back when they actually write bytes
* - FIGs that write 0 bytes stay at front (get another chance)
*/
class FIGCarouselPriority {
public:
FIGCarouselPriority(
std::shared_ptr<dabEnsemble> ensemble,
FIGRuntimeInformation::get_time_func_t getTimeFunc);
// Write all FIBs to buffer, returns bytes written
size_t write_fibs(uint8_t* buf, uint64_t current_frame, bool fib3_present);
private:
// Fill a single FIB with FIG data
// fib_index: 0-3, which FIB we're filling (needed for FIG 0/0 placement)
size_t fill_fib(uint8_t* buf, size_t max_size, int fib_index, int framephase);
// Send priority 0 FIGs (0/0, 0/7) - only in FIB 0 at framephase 0
size_t send_priority_zero(uint8_t* buf, size_t max_size, int fib_index, int framephase);
// Select which priority to poll next based on weighted counters
int select_priority();
// Cascade through priorities starting from 'start', wrapping around
// Returns priority that has a FIG matching the predicate, or -1 if none
// For must_send: predicate checks must_send flag
// For can_send: predicate checks carousel non-empty
int cascade_find_priority(int start, bool must_send_only);
// Called after a FIG successfully sends from a priority
void on_fig_sent(int priority);
// Decrement all poll counters (called on each successful FIG send)
void decrement_all_counters();
// Tick all deadline monitors (called each frame)
void tick_all_deadlines(int elapsed_ms);
// Check and log any deadline violations
void check_and_log_deadlines(uint64_t current_frame);
// Log repetition rate statistics (when PRIORITY_RATE_STATS_DEBUG enabled)
void log_rate_statistics(uint64_t current_frame);
// Try to send a FIG, returns bytes written
// Does NOT move FIG in carousel - caller must do that only if bytes > 0
size_t try_send_fig(FIGEntryPriority* entry, uint8_t* buf, size_t max_size);
// Assign FIGs to priority levels (hardcoded assignments)
void assign_figs_to_priorities();
// Add a FIG to a priority level
void add_fig_to_priority(IFIG& fig, int priority);
// Runtime information shared with FIGs
FIGRuntimeInformation m_rti;
// Priority levels array
std::array<PriorityLevel, NUM_PRIORITIES> m_priorities;
// Priority stack: front = least recently sent from
std::list<int> m_priority_stack;
// All FIG entries (owns the FIGEntryPriority objects)
std::vector<std::unique_ptr<FIGEntryPriority>> m_all_entries;
// Track missed deadlines for periodic logging
std::unordered_set<std::string> m_missed_deadlines;
// Frame counter for rate statistics logging interval
uint64_t m_stats_log_counter = 0;
// FIG instances
FIG0_0 m_fig0_0;
FIG0_1 m_fig0_1;
FIG0_2 m_fig0_2;
FIG0_3 m_fig0_3;
FIG0_5 m_fig0_5;
FIG0_6 m_fig0_6;
FIG0_7 m_fig0_7;
FIG0_8 m_fig0_8;
FIG0_9 m_fig0_9;
FIG0_10 m_fig0_10;
FIG0_13 m_fig0_13;
FIG0_14 m_fig0_14;
FIG0_17 m_fig0_17;
FIG0_18 m_fig0_18;
FIG0_19 m_fig0_19;
FIG0_20 m_fig0_20;
FIG0_21 m_fig0_21;
FIG0_24 m_fig0_24;
FIG1_0 m_fig1_0;
FIG1_1 m_fig1_1;
FIG1_4 m_fig1_4;
FIG1_5 m_fig1_5;
FIG2_0 m_fig2_0;
FIG2_1_and_5 m_fig2_1;
FIG2_1_and_5 m_fig2_5;
FIG2_4 m_fig2_4;
};
} // namespace FIC
|