/* 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 . */ #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 #include #include #include #include #include #include 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 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 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 m_priorities; // Priority stack: front = least recently sent from std::list m_priority_stack; // All FIG entries (owns the FIGEntryPriority objects) std::vector> m_all_entries; // Track missed deadlines for periodic logging std::unordered_set 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