aboutsummaryrefslogtreecommitdiffstats
path: root/src/fig/FIGCarouselPriority.cpp
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2026-01-09 09:39:35 +0100
committerMatthias P. Braendli <matthias.braendli@mpb.li>2026-01-09 09:39:35 +0100
commit5972c0042396e8445b34916fa7edd984cc76e115 (patch)
treec8fe833392419e6c1fc673599260fbc1aba81eb1 /src/fig/FIGCarouselPriority.cpp
parent9d3d0dc592771a704f2a399318ab876753656e13 (diff)
parentb084bd07570cd031cbba4cc0617418883d82a9c7 (diff)
downloaddabmux-5972c0042396e8445b34916fa7edd984cc76e115.tar.gz
dabmux-5972c0042396e8445b34916fa7edd984cc76e115.tar.bz2
dabmux-5972c0042396e8445b34916fa7edd984cc76e115.zip
Merge 'shunt010/CarouselPriority' into next
Diffstat (limited to 'src/fig/FIGCarouselPriority.cpp')
-rw-r--r--src/fig/FIGCarouselPriority.cpp585
1 files changed, 585 insertions, 0 deletions
diff --git a/src/fig/FIGCarouselPriority.cpp b/src/fig/FIGCarouselPriority.cpp
new file mode 100644
index 0000000..e18a075
--- /dev/null
+++ b/src/fig/FIGCarouselPriority.cpp
@@ -0,0 +1,585 @@
+/*
+ Copyright (C) 2026
+ Samuel Hunt, Maxxwave Ltd. sam@maxxwave.co.uk
+
+ Implementation of a priority-based FIG carousel scheduler.
+ */
+/*
+ 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/>.
+*/
+
+#include "fig/FIGCarouselPriority.h"
+#include "fig/FIG0_20.h"
+#include "crc.h"
+
+#include <algorithm>
+#include <sstream>
+#include <limits>
+#include <cstring>
+
+namespace FIC {
+
+/**************** PriorityLevel ****************/
+
+FIGEntryPriority* PriorityLevel::find_must_send()
+{
+ for (auto* entry : carousel) {
+ if (entry->must_send) {
+ return entry;
+ }
+ }
+ return nullptr;
+}
+
+FIGEntryPriority* PriorityLevel::find_can_send()
+{
+ for (auto* entry : carousel) {
+ // A FIG can send if it has data (even if must_send is false)
+ // We always have something to send - the FIG will return 0 bytes if nothing
+ return entry;
+ }
+ return nullptr;
+}
+
+void PriorityLevel::move_to_back(FIGEntryPriority* entry)
+{
+ carousel.remove(entry);
+ carousel.push_back(entry);
+}
+
+bool PriorityLevel::has_must_send() const
+{
+ for (const auto* entry : carousel) {
+ if (entry->must_send) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool PriorityLevel::has_can_send() const
+{
+ return !carousel.empty();
+}
+
+/**************** FIGCarouselPriority ****************/
+
+FIGCarouselPriority::FIGCarouselPriority(
+ std::shared_ptr<dabEnsemble> ensemble,
+ FIGRuntimeInformation::get_time_func_t getTimeFunc) :
+ m_rti(ensemble, getTimeFunc),
+ m_fig0_0(&m_rti),
+ m_fig0_1(&m_rti),
+ m_fig0_2(&m_rti),
+ m_fig0_3(&m_rti),
+ m_fig0_5(&m_rti),
+ m_fig0_6(&m_rti),
+ m_fig0_7(&m_rti),
+ m_fig0_8(&m_rti),
+ m_fig0_9(&m_rti),
+ m_fig0_10(&m_rti),
+ m_fig0_13(&m_rti),
+ m_fig0_14(&m_rti),
+ m_fig0_17(&m_rti),
+ m_fig0_18(&m_rti),
+ m_fig0_19(&m_rti),
+ m_fig0_20(&m_rti),
+ m_fig0_21(&m_rti),
+ m_fig0_24(&m_rti),
+ m_fig1_0(&m_rti),
+ m_fig1_1(&m_rti),
+ m_fig1_4(&m_rti),
+ m_fig1_5(&m_rti),
+ m_fig2_0(&m_rti),
+ m_fig2_1(&m_rti, true),
+ m_fig2_5(&m_rti, false),
+ m_fig2_4(&m_rti)
+{
+ // Initialize priority levels
+ for (int i = 0; i < NUM_PRIORITIES; i++) {
+ m_priorities[i].priority = i;
+ // Priority 0 is handled specially (0/0 and 0/7)
+ // Other priorities have reset values tuned to meet rate requirements:
+ // - Priority 1 (Rate A, 100ms): Must send frequently
+ // - Lower priorities can wait longer
+ switch (i) {
+ case 0: m_priorities[i].poll_reset_value = 1; break; // Special
+ case 1: m_priorities[i].poll_reset_value = 1; break; // Rate A - every opportunity
+ case 2: m_priorities[i].poll_reset_value = 2; break; // Rate A
+ case 3: m_priorities[i].poll_reset_value = 4; break; // Rate B (1 sec)
+ case 4: m_priorities[i].poll_reset_value = 8; break; // Rate B
+ case 5: m_priorities[i].poll_reset_value = 16; break; // Rate B
+ case 6: m_priorities[i].poll_reset_value = 32; break; // Rate B
+ case 7: m_priorities[i].poll_reset_value = 64; break; // Rate C (10 sec)
+ case 8: m_priorities[i].poll_reset_value = 128; break; // Rate E
+ default: m_priorities[i].poll_reset_value = 256; break;
+ }
+ m_priorities[i].poll_counter = m_priorities[i].poll_reset_value;
+ }
+
+ // Initialize priority stack (all priorities except 0)
+ for (int i = 1; i < NUM_PRIORITIES; i++) {
+ m_priority_stack.push_back(i);
+ }
+
+ // Assign FIGs to priorities
+ assign_figs_to_priorities();
+}
+
+void FIGCarouselPriority::assign_figs_to_priorities()
+{
+ /*
+ * Priority assignments:
+ *
+ * Priority 0 (every frame): 0/0, 0/7 - Critical MCI
+ * Priority 1 (reset 2): 0/1, 0/2 - Core MCI (subchannel/service organisation)
+ * Priority 2 (reset 4): 0/3 - Service component in packet mode
+ * Priority 3 (reset 8): 1/1 - Programme service labels
+ * Priority 4 (reset 16): 0/8, 0/13 - Service component global definition, user apps
+ * Priority 5 (reset 32): 0/5, 0/9, 0/10, 0/17, 0/18 - Metadata
+ * Priority 6 (reset 64): 1/0, 1/4, 1/5, 2/0, 2/1, 2/4, 2/5 - Labels
+ * Priority 7 (reset 128): 0/6, 0/14, 0/19 - Service linking, FEC, announcements
+ * Priority 8 (reset 256): 0/21, 0/24 - Frequency info, OE services
+ * Priority 9 (reset 512): (reserved for future/dynamic use)
+ */
+
+ // Priority 0: Critical (every frame)
+ add_fig_to_priority(m_fig0_0, 0);
+ add_fig_to_priority(m_fig0_7, 0);
+
+ // Priority 1: Core MCI
+ add_fig_to_priority(m_fig0_1, 1);
+ add_fig_to_priority(m_fig0_2, 1);
+
+ // Priority 2: Packet mode
+ add_fig_to_priority(m_fig0_3, 2);
+
+ // Priority 3: Service labels
+ add_fig_to_priority(m_fig1_1, 3);
+
+ // Priority 4: Component details
+ add_fig_to_priority(m_fig0_8, 4);
+ add_fig_to_priority(m_fig0_13, 4);
+
+ // Priority 5: Metadata
+ add_fig_to_priority(m_fig0_5, 5);
+ add_fig_to_priority(m_fig0_9, 5);
+ add_fig_to_priority(m_fig0_10, 5);
+ add_fig_to_priority(m_fig0_17, 5);
+ add_fig_to_priority(m_fig0_18, 5);
+ add_fig_to_priority(m_fig0_20, 5); // SCI - Service Component Information
+
+ // Priority 6: Labels (ensemble, component, data, extended)
+ add_fig_to_priority(m_fig1_0, 6);
+ add_fig_to_priority(m_fig1_4, 6);
+ add_fig_to_priority(m_fig1_5, 6);
+ add_fig_to_priority(m_fig2_0, 6);
+ add_fig_to_priority(m_fig2_1, 6);
+ add_fig_to_priority(m_fig2_4, 6);
+ add_fig_to_priority(m_fig2_5, 6);
+
+ // Priority 7: Linking and announcements
+ add_fig_to_priority(m_fig0_6, 7);
+ add_fig_to_priority(m_fig0_14, 7);
+ add_fig_to_priority(m_fig0_19, 7);
+
+ // Priority 8: Frequency/OE
+ add_fig_to_priority(m_fig0_21, 8);
+ add_fig_to_priority(m_fig0_24, 8);
+
+ // Priority 9: Reserved for dynamic adjustment
+}
+
+void FIGCarouselPriority::add_fig_to_priority(IFIG& fig, int priority)
+{
+ std::unique_ptr<FIGEntryPriority> entry(new FIGEntryPriority());
+ entry->fig = &fig;
+ entry->assigned_priority = priority;
+ entry->base_priority = priority;
+ entry->init_deadline();
+
+ FIGEntryPriority* entry_ptr = entry.get();
+ m_all_entries.push_back(std::move(entry));
+
+ m_priorities[priority].carousel.push_back(entry_ptr);
+}
+
+void FIGCarouselPriority::tick_all_deadlines(int elapsed_ms)
+{
+ for (auto& entry : m_all_entries) {
+ entry->tick_deadline(elapsed_ms);
+
+ // Check if a new cycle should start
+ if (!entry->must_send && entry->deadline_ms <= 0) {
+ entry->start_new_cycle();
+ entry->deadline_ms = entry->rate_ms; // Reset for next cycle
+ }
+ }
+}
+
+void FIGCarouselPriority::check_and_log_deadlines(uint64_t current_frame)
+{
+ if ((current_frame % 250) != 0) {
+ return;
+ }
+
+ std::stringstream ss;
+ bool any_violated = false;
+
+ for (auto& entry : m_all_entries) {
+ if (entry->deadline_violated) {
+ ss << " " << entry->name();
+ entry->deadline_violated = false;
+ any_violated = true;
+ }
+ }
+
+ if (any_violated) {
+ etiLog.level(info) << "FIGCarouselPriority: Could not respect repetition rates for FIGs:" << ss.str();
+ }
+}
+
+size_t FIGCarouselPriority::write_fibs(uint8_t* buf, uint64_t current_frame, bool fib3_present)
+{
+ m_rti.currentFrame = current_frame;
+
+ const int fibCount = fib3_present ? 4 : 3;
+ const int framephase = current_frame % 4;
+
+#if PRIORITY_CAROUSEL_DEBUG
+ if ((current_frame % 50) == 0) { // Log every 50 frames (~1.2 sec)
+ std::stringstream ss;
+ ss << "Frame " << current_frame << " (phase " << framephase << ") FIG deadlines: ";
+ for (auto& entry : m_all_entries) {
+ if (entry->must_send || entry->deadline_ms < 50) {
+ ss << entry->name() << "(" << entry->deadline_ms << "ms"
+ << (entry->must_send ? ",MUST" : "")
+ << (entry->deadline_violated ? ",VIOL" : "") << ") ";
+ }
+ }
+ etiLog.level(debug) << ss.str();
+ }
+#endif
+
+ for (int fib = 0; fib < fibCount; fib++) {
+ memset(buf, 0x00, 30);
+ size_t figSize = fill_fib(buf, 30, framephase);
+
+ if (figSize < 30) {
+ buf[figSize] = 0xff; // End marker
+ }
+ else if (figSize > 30) {
+ std::stringstream ss;
+ ss << "FIB" << fib << " overload (" << figSize << " > 30)";
+ throw std::runtime_error(ss.str());
+ }
+
+ // Calculate and append CRC
+ uint16_t crc = 0xffff;
+ crc = crc16(crc, buf, 30);
+ crc ^= 0xffff;
+
+ buf += 30;
+ *(buf++) = (crc >> 8) & 0xFF;
+ *(buf++) = crc & 0xFF;
+ }
+
+ // Tick all deadline monitors AFTER sending (24ms per frame)
+ tick_all_deadlines(24);
+
+ // Periodically log missed deadlines
+ check_and_log_deadlines(current_frame);
+
+ return 32 * fibCount;
+}
+
+size_t FIGCarouselPriority::fill_fib(uint8_t* buf, size_t max_size, int framephase)
+{
+ size_t written = 0;
+
+#if PRIORITY_CAROUSEL_DEBUG
+ std::stringstream fib_log;
+ fib_log << "FIB fill (phase " << framephase << "): ";
+#endif
+
+ // Step 1: Priority 0 always first (FIG 0/0, 0/7)
+ size_t p0_written = send_priority_zero(buf, max_size, framephase);
+ written += p0_written;
+
+#if PRIORITY_CAROUSEL_DEBUG
+ if (p0_written > 0) {
+ fib_log << "P0:" << p0_written << "B ";
+ }
+#endif
+
+ size_t remaining = max_size - written;
+ uint8_t* pbuf = buf + written;
+
+ // Step 2: Must-send pass - send FIGs that are due
+ int attempts = 0;
+ const int max_attempts = NUM_PRIORITIES * 2; // Prevent infinite loop
+
+ while (remaining > 2 && attempts < max_attempts) {
+ attempts++;
+
+ // Find any priority with must_send FIGs
+ FIGEntryPriority* entry = nullptr;
+ int send_prio = -1;
+
+ // First try the selected priority
+ int prio = select_priority();
+ if (prio >= 0) {
+ entry = m_priorities[prio].find_must_send();
+ if (entry) {
+ send_prio = prio;
+ }
+ }
+
+ // If selected priority has nothing, search all priorities
+ if (!entry) {
+ for (int p = 1; p < NUM_PRIORITIES; p++) {
+ entry = m_priorities[p].find_must_send();
+ if (entry) {
+ send_prio = p;
+ break;
+ }
+ }
+ }
+
+ if (!entry) break; // No more must_send anywhere
+
+ size_t bytes = try_send_fig(entry, pbuf, remaining);
+ if (bytes > 0) {
+#if PRIORITY_CAROUSEL_DEBUG
+ fib_log << entry->name() << ":" << bytes << "B ";
+#endif
+ written += bytes;
+ remaining -= bytes;
+ pbuf += bytes;
+ m_priorities[send_prio].move_to_back(entry);
+ on_fig_sent(send_prio);
+ } else {
+ // FIG couldn't write (no space or no data), move to back to try others
+ m_priorities[send_prio].move_to_back(entry);
+ }
+ }
+
+ // Step 3: Can-send pass (fill remaining space opportunistically)
+ attempts = 0;
+ while (remaining > 2 && attempts < max_attempts) {
+ attempts++;
+
+ int prio = select_priority();
+ if (prio < 0) break;
+
+ FIGEntryPriority* entry = m_priorities[prio].find_can_send();
+
+ if (!entry) {
+ // Try other priorities
+ for (int p = 1; p < NUM_PRIORITIES; p++) {
+ if (!m_priorities[p].carousel.empty()) {
+ entry = m_priorities[p].carousel.front();
+ prio = p;
+ break;
+ }
+ }
+ }
+
+ if (!entry) break;
+
+ size_t bytes = try_send_fig(entry, pbuf, remaining);
+ if (bytes > 0) {
+#if PRIORITY_CAROUSEL_DEBUG
+ fib_log << entry->name() << ":" << bytes << "B(opt) ";
+#endif
+ written += bytes;
+ remaining -= bytes;
+ pbuf += bytes;
+ m_priorities[prio].move_to_back(entry);
+ on_fig_sent(prio);
+ } else {
+ // FIG wrote nothing, move on
+ m_priorities[prio].move_to_back(entry);
+ break; // No more space or nothing to send
+ }
+ }
+
+#if PRIORITY_CAROUSEL_DEBUG
+ fib_log << "= " << written << "/" << max_size;
+ etiLog.level(debug) << fib_log.str();
+#endif
+
+ return written;
+}
+
+size_t FIGCarouselPriority::send_priority_zero(uint8_t* buf, size_t max_size, int framephase)
+{
+ size_t written = 0;
+
+ // FIG 0/0 and 0/7 only in framephase 0 (every 96ms in mode I)
+ if (framephase != 0) {
+ return 0;
+ }
+
+#if PRIORITY_CAROUSEL_DEBUG
+ etiLog.level(debug) << "send_priority_zero: framephase=0, sending 0/0 and 0/7";
+#endif
+
+ // Find and send FIG 0/0
+ for (auto* entry : m_priorities[0].carousel) {
+ if (entry->fig->figtype() == 0 && entry->fig->figextension() == 0) {
+ FillStatus status = entry->fig->fill(buf + written, max_size - written);
+#if PRIORITY_CAROUSEL_DEBUG
+ etiLog.level(debug) << " FIG 0/0: wrote " << status.num_bytes_written
+ << " bytes, complete=" << status.complete_fig_transmitted
+ << ", deadline was " << entry->deadline_ms << "ms";
+#endif
+ if (status.num_bytes_written > 0) {
+ written += status.num_bytes_written;
+ if (status.complete_fig_transmitted) {
+ entry->on_cycle_complete();
+ }
+ }
+ else {
+ throw std::logic_error("Failed to write FIG 0/0");
+ }
+ break;
+ }
+ }
+
+ // FIG 0/7 must directly follow FIG 0/0
+ for (auto* entry : m_priorities[0].carousel) {
+ if (entry->fig->figtype() == 0 && entry->fig->figextension() == 7) {
+#if PRIORITY_CAROUSEL_DEBUG
+ etiLog.level(debug) << " FIG 0/7: deadline=" << entry->deadline_ms
+ << "ms, must_send=" << entry->must_send
+ << ", violated=" << entry->deadline_violated;
+#endif
+ FillStatus status = entry->fig->fill(buf + written, max_size - written);
+#if PRIORITY_CAROUSEL_DEBUG
+ etiLog.level(debug) << " FIG 0/7: wrote " << status.num_bytes_written
+ << " bytes, complete=" << status.complete_fig_transmitted;
+#endif
+ if (status.num_bytes_written > 0) {
+ written += status.num_bytes_written;
+ }
+ // If complete (even with 0 bytes - means nothing to send), reset the cycle
+ if (status.complete_fig_transmitted) {
+ entry->on_cycle_complete();
+#if PRIORITY_CAROUSEL_DEBUG
+ etiLog.level(debug) << " FIG 0/7: cycle complete, new deadline=" << entry->deadline_ms;
+#endif
+ }
+ break;
+ }
+ }
+
+ return written;
+}
+
+int FIGCarouselPriority::select_priority()
+{
+ if (m_priority_stack.empty()) {
+ return -1;
+ }
+
+ // First: find any priority with counter == 0, highest in stack (front)
+ for (int prio : m_priority_stack) {
+ if (m_priorities[prio].poll_counter == 0) {
+ return prio;
+ }
+ }
+
+ // None at 0: find lowest weighted score
+ // Score = poll_counter * stack_position
+ // Lower score wins; ties broken by stack position (higher = wins)
+
+ int best_prio = -1;
+ int best_score = std::numeric_limits<int>::max();
+ int position = 1;
+
+ for (int prio : m_priority_stack) {
+ int score = m_priorities[prio].poll_counter * position;
+ if (score < best_score) {
+ best_score = score;
+ best_prio = prio;
+ }
+ position++;
+ }
+
+ return best_prio;
+}
+
+void FIGCarouselPriority::on_fig_sent(int priority)
+{
+ // Decrement all counters
+ decrement_all_counters();
+
+ // Reset the priority that sent
+ m_priorities[priority].poll_counter = m_priorities[priority].poll_reset_value;
+
+ // Move to bottom of priority stack
+ m_priority_stack.remove(priority);
+ m_priority_stack.push_back(priority);
+}
+
+void FIGCarouselPriority::decrement_all_counters()
+{
+ for (int i = 1; i < NUM_PRIORITIES; i++) { // Skip priority 0
+ if (m_priorities[i].poll_counter > 0) {
+ m_priorities[i].poll_counter--;
+ }
+ // Clamp at 0 (don't go negative)
+ }
+}
+
+size_t FIGCarouselPriority::try_send_fig(FIGEntryPriority* entry, uint8_t* buf, size_t max_size)
+{
+ FillStatus status = entry->fig->fill(buf, max_size);
+ size_t written = status.num_bytes_written;
+
+ // Validation: FIG should write at least 3 bytes or nothing
+ if (written == 1 || written == 2) {
+ std::stringstream ss;
+ ss << "Assertion error: FIG " << entry->name()
+ << " wrote only " << written << " bytes (minimum is 3)";
+ throw std::logic_error(ss.str());
+ }
+
+ if (written > max_size) {
+ std::stringstream ss;
+ ss << "Assertion error: FIG " << entry->name()
+ << " wrote " << written << " bytes but only "
+ << max_size << " available";
+ throw std::logic_error(ss.str());
+ }
+
+#if PRIORITY_CAROUSEL_DEBUG
+ if (written > 0) {
+ etiLog.level(debug) << "FIGCarouselPriority: " << entry->name()
+ << " wrote " << written << " bytes"
+ << (status.complete_fig_transmitted ? " (complete)" : " (partial)");
+ }
+#endif
+
+ if (status.complete_fig_transmitted) {
+ entry->on_cycle_complete();
+ }
+
+ return written;
+}
+
+} // namespace FIC \ No newline at end of file