diff options
| author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2026-01-09 09:39:35 +0100 |
|---|---|---|
| committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2026-01-09 09:39:35 +0100 |
| commit | 5972c0042396e8445b34916fa7edd984cc76e115 (patch) | |
| tree | c8fe833392419e6c1fc673599260fbc1aba81eb1 /src | |
| parent | 9d3d0dc592771a704f2a399318ab876753656e13 (diff) | |
| parent | b084bd07570cd031cbba4cc0617418883d82a9c7 (diff) | |
| download | dabmux-5972c0042396e8445b34916fa7edd984cc76e115.tar.gz dabmux-5972c0042396e8445b34916fa7edd984cc76e115.tar.bz2 dabmux-5972c0042396e8445b34916fa7edd984cc76e115.zip | |
Merge 'shunt010/CarouselPriority' into next
Diffstat (limited to 'src')
| -rw-r--r-- | src/ConfigParser.cpp | 5 | ||||
| -rw-r--r-- | src/DabMultiplexer.cpp | 29 | ||||
| -rw-r--r-- | src/DabMultiplexer.h | 15 | ||||
| -rw-r--r-- | src/MuxElements.h | 3 | ||||
| -rw-r--r-- | src/fig/FIGCarouselPriority.cpp | 585 | ||||
| -rw-r--r-- | src/fig/FIGCarouselPriority.h | 257 | ||||
| -rw-r--r-- | src/fig/FIGSchedulerType.cpp | 60 | ||||
| -rw-r--r-- | src/fig/FIGSchedulerType.h | 49 | ||||
| -rw-r--r-- | src/utils.cpp | 3 |
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; |
