aboutsummaryrefslogtreecommitdiffstats
path: root/src
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
parent9d3d0dc592771a704f2a399318ab876753656e13 (diff)
parentb084bd07570cd031cbba4cc0617418883d82a9c7 (diff)
downloaddabmux-5972c0042396e8445b34916fa7edd984cc76e115.tar.gz
dabmux-5972c0042396e8445b34916fa7edd984cc76e115.tar.bz2
dabmux-5972c0042396e8445b34916fa7edd984cc76e115.zip
Merge 'shunt010/CarouselPriority' into next
Diffstat (limited to 'src')
-rw-r--r--src/ConfigParser.cpp5
-rw-r--r--src/DabMultiplexer.cpp29
-rw-r--r--src/DabMultiplexer.h15
-rw-r--r--src/MuxElements.h3
-rw-r--r--src/fig/FIGCarouselPriority.cpp585
-rw-r--r--src/fig/FIGCarouselPriority.h257
-rw-r--r--src/fig/FIGSchedulerType.cpp60
-rw-r--r--src/fig/FIGSchedulerType.h49
-rw-r--r--src/utils.cpp3
9 files changed, 1003 insertions, 3 deletions
diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp
index 241ae58..b2e285b 100644
--- a/src/ConfigParser.cpp
+++ b/src/ConfigParser.cpp
@@ -55,6 +55,7 @@
#include <memory>
#include <string>
#include <vector>
+#include "fig/FIGSchedulerType.h"
using namespace std;
using boost::property_tree::ptree;
@@ -531,6 +532,10 @@ static void parse_general(ptree& pt,
ensemble->reconfig_counter = pt_ensemble.get("reconfig-counter", ensemble->reconfig_counter);
}
+ // FIG scheduler type selection
+ std::string fic_scheduler_str = pt_general.get("fic-scheduler", "classic");
+ ensemble->fic_scheduler = FIC::parse_scheduler_type(fic_scheduler_str);
+
string lto_auto = pt_ensemble.get("local-time-offset", "");
if (lto_auto == "auto") {
ensemble->lto_auto = true;
diff --git a/src/DabMultiplexer.cpp b/src/DabMultiplexer.cpp
index 9a06cfe..dc19751 100644
--- a/src/DabMultiplexer.cpp
+++ b/src/DabMultiplexer.cpp
@@ -166,6 +166,20 @@ void DabMultiplexer::prepare(bool require_tai_clock)
{
parse_ptree(m_config.pt, ensemble);
+ /* Create the appropriate FIG carousel based on config.
+ * This must happen after parse_ptree() which sets ensemble->fic_scheduler
+ */
+ m_scheduler_type = ensemble->fic_scheduler;
+ auto time_func = [&]() { return m_time.get_milliseconds_seconds(); };
+
+ if (m_scheduler_type == FIC::FIGSchedulerType::Priority) {
+ etiLog.level(info) << "Using priority-based FIG scheduler";
+ m_fig_carousel_priority.reset(new FIC::FIGCarouselPriority(ensemble, time_func));
+ } else {
+ etiLog.level(info) << "Using classic FIG scheduler";
+ m_fig_carousel_classic.reset(new FIC::FIGCarousel(ensemble, time_func));
+ }
+
rcs.enrol(this);
rcs.enrol(ensemble.get());
@@ -512,6 +526,17 @@ void DabMultiplexer::reload_linking()
}
}
+/* Helper method for FIG carousel write_fibs - abstracts the scheduler type */
+size_t DabMultiplexer::fig_carousel_write_fibs(uint8_t* buf, uint64_t current_frame, bool fib3_present)
+{
+ if (m_fig_carousel_priority) {
+ return m_fig_carousel_priority->write_fibs(buf, current_frame, fib3_present);
+ } else if (m_fig_carousel_classic) {
+ return m_fig_carousel_classic->write_fibs(buf, current_frame, fib3_present);
+ }
+ return 0;
+}
+
/* Each call creates one ETI frame */
void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs)
{
@@ -730,9 +755,9 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
edi_tagDETI.fic_data = &etiFrame[index];
edi_tagDETI.fic_length = FICL * 4;
- // Insert all FIBs
+ // Insert all FIBs using the selected scheduler
const bool fib3_present = (ensemble->transmission_mode == TransmissionMode_e::TM_III);
- index += fig_carousel.write_fibs(&etiFrame[index], currentFrame, fib3_present);
+ index += fig_carousel_write_fibs(&etiFrame[index], currentFrame, fib3_present);
/**********************************************************************
****** Input Data Reading *******************************************
diff --git a/src/DabMultiplexer.h b/src/DabMultiplexer.h
index 7f12868..50e2585 100644
--- a/src/DabMultiplexer.h
+++ b/src/DabMultiplexer.h
@@ -32,6 +32,8 @@
#include "dabOutput/dabOutput.h"
#include "edioutput/Transport.h"
#include "fig/FIGCarousel.h"
+#include "fig/FIGCarouselPriority.h"
+#include "fig/FIGSchedulerType.h"
#include "MuxElements.h"
#include "RemoteControl.h"
#include "ClockTAI.h"
@@ -127,5 +129,16 @@ class DabMultiplexer : public RemoteControllable {
bool m_tai_clock_required = false;
ClockTAI& m_clock_tai;
- FIC::FIGCarousel fig_carousel;
+ /* FIG Carousel - supports classic and priority schedulers
+ *
+ * Only one of these will be instantiated based on config.
+ * The scheduler type is determined by ensemble->fic_scheduler
+ * which is set during config parsing in prepare().
+ */
+ FIC::FIGSchedulerType m_scheduler_type = FIC::FIGSchedulerType::Classic;
+ std::unique_ptr<FIC::FIGCarousel> m_fig_carousel_classic;
+ std::unique_ptr<FIC::FIGCarouselPriority> m_fig_carousel_priority;
+
+ /* Helper method for FIG carousel write_fibs */
+ size_t fig_carousel_write_fibs(uint8_t* buf, uint64_t current_frame, bool fib3_present);
};
diff --git a/src/MuxElements.h b/src/MuxElements.h
index 802c4fc..12d2848 100644
--- a/src/MuxElements.h
+++ b/src/MuxElements.h
@@ -40,6 +40,7 @@
#include "dabOutput/dabOutput.h"
#include "input/inputs.h"
#include "RemoteControl.h"
+#include "fig/FIGSchedulerType.h" // For FIGSchedulerType enum
// Protection levels and bitrates for UEP.
const unsigned char ProtectionLevelTable[64] = {
@@ -413,6 +414,8 @@ class dabEnsemble : public RemoteControllable {
std::vector<ServiceOtherEnsembleInfo> get_service_other_ensemble() const;
vec_sp_sci sci_entries;
+ FIC::FIGSchedulerType fic_scheduler = FIC::FIGSchedulerType::Classic;
+
void set_linking_config(
std::vector<std::shared_ptr<LinkageSet> >& new_linkage_sets,
std::vector<FrequencyInformation>& new_frequency_information,
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
diff --git a/src/fig/FIGCarouselPriority.h b/src/fig/FIGCarouselPriority.h
new file mode 100644
index 0000000..a9ae827
--- /dev/null
+++ b/src/fig/FIGCarouselPriority.h
@@ -0,0 +1,257 @@
+/*
+ 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
+#define PRIORITY_CAROUSEL_DEBUG 0
+
+/*
+ * FIGEntryPriority - Holds a FIG and its scheduling state
+ *
+ * Each FIG has:
+ * - must_send: Set when a new cycle is due, cleared when cycle completes
+ * - deadline_ms: Independent countdown for monitoring repetition rate compliance
+ * - rate_ms: The required repetition rate from the FIG's specification
+ */
+struct FIGEntryPriority {
+ IFIG* fig = nullptr;
+
+ // Scheduling state
+ bool must_send = false; // Cycle is due, not yet complete
+
+ // Deadline monitoring (independent of scheduling)
+ int deadline_ms = 0; // Countdown timer
+ int rate_ms = 0; // Reset value (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
+
+ std::string name() const {
+ if (fig) {
+ return fig->name();
+ }
+ return "unknown";
+ }
+
+ 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
+ }
+
+ void tick_deadline(int elapsed_ms) {
+ deadline_ms -= elapsed_ms;
+ if (deadline_ms <= 0 && must_send && !deadline_violated) {
+ // Deadline expired but cycle not complete (only flag once)
+ deadline_violated = true;
+ }
+ }
+
+ void on_cycle_complete() {
+ // 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;
+ }
+};
+
+/*
+ * 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 that fits in available space
+ FIGEntryPriority* find_must_send();
+
+ // Find first FIG that can send (has data)
+ FIGEntryPriority* find_can_send();
+
+ // Move entry to back of carousel (after sending)
+ void move_to_back(FIGEntryPriority* entry);
+
+ // Check if any FIG in this priority has must_send
+ bool has_must_send() const;
+
+ // Check if any FIG in this priority can send
+ bool has_can_send() const;
+};
+
+/*
+ * FIGCarouselPriority - Priority-based FIG scheduler
+ *
+ * Scheduling algorithm:
+ * 1. Priority 0 (0/0, 0/7) always sends first every frame
+ * 2. Other priorities are polled based on weighted counters
+ * 3. Within each priority, FIGs rotate via round-robin carousel
+ * 4. must_send FIGs are prioritised over can_send (opportunistic)
+ * 5. Lower priorities can get early turns if higher has nothing due
+ *
+ * Counter mechanism:
+ * - All counters decrement when ANY FIG is sent
+ * - When a priority sends, its counter resets to poll_reset_value
+ * - Priority with counter=0 (or lowest weighted score) is selected
+ *
+ * Priority stack:
+ * - Tracks which priority sent most recently
+ * - Used for tie-breaking when multiple priorities are due
+ */
+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
+ size_t fill_fib(uint8_t* buf, size_t max_size, int framephase);
+
+ // Send priority 0 FIGs (0/0, 0/7)
+ size_t send_priority_zero(uint8_t* buf, size_t max_size, int framephase);
+
+ // Select which priority to poll next
+ int select_priority();
+
+ // Called after a FIG is sent from a priority
+ void on_fig_sent(int priority);
+
+ // Decrement all poll counters (called on each 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);
+
+ // Try to send a FIG, returns bytes written
+ 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;
+
+ // 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 \ No newline at end of file
diff --git a/src/fig/FIGSchedulerType.cpp b/src/fig/FIGSchedulerType.cpp
new file mode 100644
index 0000000..dc2c952
--- /dev/null
+++ b/src/fig/FIGSchedulerType.cpp
@@ -0,0 +1,60 @@
+/*
+ Copyright (C) 2026
+ Samuel Hunt, Maxxwave Ltd. sam@maxxwave.co.uk
+ */
+/*
+ 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/FIGSchedulerType.h"
+#include "Log.h"
+#include <algorithm>
+#include <cctype>
+
+namespace FIC {
+
+FIGSchedulerType parse_scheduler_type(const std::string& type_str)
+{
+ std::string lower = type_str;
+ std::transform(lower.begin(), lower.end(), lower.begin(),
+ [](unsigned char c){ return std::tolower(c); });
+
+ if (lower == "priority") {
+ return FIGSchedulerType::Priority;
+ }
+ else if (lower == "classic" || lower == "default" || lower.empty()) {
+ return FIGSchedulerType::Classic;
+ }
+ else {
+ etiLog.level(warn) << "Unknown FIC scheduler type '" << type_str
+ << "', defaulting to classic";
+ return FIGSchedulerType::Classic;
+ }
+}
+
+std::string scheduler_type_to_string(FIGSchedulerType type)
+{
+ switch (type) {
+ case FIGSchedulerType::Classic:
+ return "classic";
+ case FIGSchedulerType::Priority:
+ return "priority";
+ default:
+ return "unknown";
+ }
+}
+
+} // namespace FIC
diff --git a/src/fig/FIGSchedulerType.h b/src/fig/FIGSchedulerType.h
new file mode 100644
index 0000000..b8b029d
--- /dev/null
+++ b/src/fig/FIGSchedulerType.h
@@ -0,0 +1,49 @@
+/*
+ Copyright (C) 2026
+ Samuel Hunt, Maxxwave Ltd. sam@maxxwave.co.uk
+
+ FIG Scheduler type definitions.
+ Separated into own header to avoid circular dependencies with MuxElements.h
+ */
+/*
+ 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 <string>
+
+namespace FIC {
+
+/*
+ * Scheduler type selection.
+ *
+ * Classic: Original ODR-DabMux deadline-based scheduler
+ * Priority: New priority-based scheduler with weighted round-robin
+ */
+enum class FIGSchedulerType {
+ Classic,
+ Priority
+};
+
+// Parse scheduler type from config string
+// Returns Classic if string is unrecognised
+FIGSchedulerType parse_scheduler_type(const std::string& type_str);
+
+// Convert scheduler type to string for logging
+std::string scheduler_type_to_string(FIGSchedulerType type);
+
+} // namespace FIC
diff --git a/src/utils.cpp b/src/utils.cpp
index 63ad32c..3c65e58 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -31,6 +31,7 @@
#include <boost/algorithm/string/join.hpp>
#include "utils.h"
#include "fig/FIG0structs.h"
+#include "fig/FIGSchedulerType.h"
using namespace std;
@@ -492,6 +493,8 @@ void printEnsemble(const shared_ptr<dabEnsemble>& ensemble)
break;
}
+ etiLog.level(info) << " FIC sched: " << FIC::scheduler_type_to_string(ensemble->fic_scheduler);
+
if (ensemble->lto_auto) {
time_t now = time(nullptr);
struct tm ltime;