From f8b5402727b7e94aecbfb663a601577f97bae5b9 Mon Sep 17 00:00:00 2001 From: Samuel Hunt Date: Tue, 30 Dec 2025 20:39:49 +0000 Subject: Added FIG0/20 --- src/ConfigParser.cpp | 100 ++++++++++++++++++++++ src/MuxElements.h | 59 ++++++++++++- src/fig/FIG0.h | 1 + src/fig/FIG0_20.cpp | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ src/fig/FIG0_20.h | 52 ++++++++++++ src/fig/FIGCarousel.cpp | 2 + src/fig/FIGCarousel.h | 1 + 7 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 src/fig/FIG0_20.cpp create mode 100644 src/fig/FIG0_20.h (limited to 'src') diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp index a845bef..d937166 100644 --- a/src/ConfigParser.cpp +++ b/src/ConfigParser.cpp @@ -373,6 +373,105 @@ static void parse_other_service_linking(ptree& pt, } // if other-services present } +// Parse the service-component-information section (FIG 0/20) +static void parse_service_component_information(ptree& pt, + std::shared_ptr ensemble) +{ + auto pt_sci = pt.get_child_optional("service-component-information"); + if (pt_sci) + { + for (const auto& it_sci : *pt_sci) { + const string sci_uid = it_sci.first; + const ptree pt_entry = it_sci.second; + + auto sci = make_shared(); + + try { + // Required: Service ID + sci->SId = hexparse(pt_entry.get("id")); + + // Required: change type + string change_str = pt_entry.get("change"); + if (change_str == "identity") { + sci->change_flags = SCIChangeFlags::IdentityChange; + } + else if (change_str == "addition") { + sci->change_flags = SCIChangeFlags::Addition; + } + else if (change_str == "local_removal") { + sci->change_flags = SCIChangeFlags::LocalRemoval; + } + else if (change_str == "global_removal") { + sci->change_flags = SCIChangeFlags::GlobalRemoval; + } + else { + throw runtime_error("Invalid change type '" + change_str + + "' for SCI entry " + sci_uid + + " (valid: identity, addition, local_removal, global_removal)"); + } + + // Optional: SCIdS (default 0 = primary component) + sci->SCIdS = pt_entry.get("scids", 0) & 0x0F; + + // Optional: programme flag (default true for audio services) + sci->isProgramme = pt_entry.get("programme", true); + + // Optional: part-time flag + sci->part_time = pt_entry.get("part_time", false); + + // Optional: SC description + auto pt_sc = pt_entry.get_child_optional("sc_description"); + if (pt_sc) { + sci->sc_flag = true; + sci->ca_flag = pt_sc->get("ca", false); + sci->ad_flag = pt_sc->get("data", false); // A/D flag: 0=audio, 1=data + sci->SCTy = pt_sc->get("scty", 0) & 0x3F; + } + + // Optional: date-time of change (default: special value = already occurred) + auto pt_datetime = pt_entry.get_child_optional("datetime"); + if (pt_datetime) { + sci->date = pt_datetime->get("date", 0x1F) & 0x1F; + sci->hour = pt_datetime->get("hour", 0x1F) & 0x1F; + sci->minute = pt_datetime->get("minute", 0x3F) & 0x3F; + sci->second = pt_datetime->get("second", 0x3F) & 0x3F; + } + // else: default values already set to special value (0x1FFFFFF) + + // Optional: transfer SId (for identity changes or service moves) + string transfer_sid_str = pt_entry.get("transfer_sid", ""); + if (not transfer_sid_str.empty()) { + sci->sid_flag = true; + sci->transfer_sid = hexparse(transfer_sid_str); + } + + // Optional: transfer EId (for service moves to another ensemble) + string transfer_eid_str = pt_entry.get("transfer_eid", ""); + if (not transfer_eid_str.empty()) { + sci->eid_flag = true; + sci->transfer_eid = hexparse(transfer_eid_str); + } + + // Optional: active flag (default false - must be enabled via RC or config) + sci->set_active(pt_entry.get("active", false)); + + ensemble->sci_entries.push_back(sci); + + etiLog.level(info) << "SCI entry " << sci_uid << + " configured for SId 0x" << hex << sci->SId << dec; + } + catch (const ptree_error &e) { + throw runtime_error("Invalid configuration for SCI entry " + + sci_uid + ": " + e.what()); + } + catch (const std::exception &e) { + throw runtime_error("Error parsing SCI entry " + + sci_uid + ": " + e.what()); + } + } // for over sci entries + } // if service-component-information present +} + static void parse_general(ptree& pt, std::shared_ptr ensemble) { @@ -906,6 +1005,7 @@ void parse_ptree( parse_linkage(pt, ensemble); parse_freq_info(pt, ensemble); parse_other_service_linking(pt, ensemble); + parse_service_component_information(pt, ensemble); } static Inputs::dab_input_zmq_config_t setup_zmq_input( diff --git a/src/MuxElements.h b/src/MuxElements.h index 1e9b707..bbd87ec 100644 --- a/src/MuxElements.h +++ b/src/MuxElements.h @@ -287,6 +287,63 @@ using vec_sp_service = std::vector >; using vec_sp_subchannel = std::vector >; +/* FIG 0/20 Service Component Information (SCI) + * Change flags values - see ETSI TS 103 176 clause 6 + */ +enum class SCIChangeFlags : uint8_t { + IdentityChange = 0, // 00: Service changing identity or moving ensemble + Addition = 1, // 01: Service being added + LocalRemoval = 2, // 10: Service removed from this ensemble only + GlobalRemoval = 3 // 11: Service removed from all ensembles +}; + +/* Service Component Information entry for FIG 0/20 + * See ETSI TS 103 176 clause 6 + */ +struct ServiceComponentInformation { + uint32_t SId = 0; // Service Identifier (16-bit for audio, 32-bit for data) + uint8_t SCIdS = 0; // Service Component Identifier within Service (4-bit) + SCIChangeFlags change_flags = SCIChangeFlags::Addition; + bool part_time = false; // P-T flag: true if part-time service + bool sc_flag = false; // SC flag: true if SC description present + + // SC description (only present if sc_flag is true) + bool ca_flag = false; // CA flag: Access control applied + bool ad_flag = false; // A/D flag: 0=ASCTy follows, 1=DSCTy follows + uint8_t SCTy = 0; // Service Component Type (6-bit) + + // Date-time of change (special value 0x1FFFFFF means "already occurred" or "unknown") + uint8_t date = 0x1F; // 5-bit, 0x1F = special value + uint8_t hour = 0x1F; // 5-bit, 0x1F = special value + uint8_t minute = 0x3F; // 6-bit, 0x3F = special value + uint8_t second = 0x3F; // 6-bit, 0x3F = special value + + // Transfer information (for service moves/identity changes) + bool sid_flag = false; // SId flag: Transfer SId present + bool eid_flag = false; // EId flag: Transfer EId present + uint32_t transfer_sid = 0; // Transfer Service Identifier + uint16_t transfer_eid = 0; // Transfer Ensemble Identifier + + bool isDateTimeSpecial() const { + return (date == 0x1F && hour == 0x1F && minute == 0x3F && second == 0x3F); + } + + void setDateTimeSpecial() { + date = 0x1F; hour = 0x1F; minute = 0x3F; second = 0x3F; + } + + bool isProgramme = true; + + bool is_active() const { return m_active; } + void set_active(bool a) { m_active = a; } + +private: + bool m_active = false; +}; + +using vec_sp_sci = std::vector >; + + enum class TransmissionMode_e { TM_I, TM_II, @@ -349,6 +406,7 @@ class dabEnsemble : public RemoteControllable { std::vector > linkagesets; std::vector frequency_information; std::vector service_other_ensemble; + vec_sp_sci sci_entries; }; @@ -667,4 +725,3 @@ vec_sp_component::iterator getComponent( vec_sp_service::iterator getService( std::shared_ptr component, vec_sp_service& services); - diff --git a/src/fig/FIG0.h b/src/fig/FIG0.h index a779fa0..26d55ab 100644 --- a/src/fig/FIG0.h +++ b/src/fig/FIG0.h @@ -41,6 +41,7 @@ #include "fig/FIG0_17.h" #include "fig/FIG0_18.h" #include "fig/FIG0_19.h" +#include "fig/FIG0_20.h" #include "fig/FIG0_21.h" #include "fig/FIG0_24.h" diff --git a/src/fig/FIG0_20.cpp b/src/fig/FIG0_20.cpp new file mode 100644 index 0000000..86ac857 --- /dev/null +++ b/src/fig/FIG0_20.cpp @@ -0,0 +1,221 @@ +/* + Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + 2011, 2012 Her Majesty the Queen in Right of Canada (Communications + Research Center Canada) + + Copyright (C) 2024 + Matthias P. Braendli, matthias.braendli@mpb.li + */ +/* + 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 . +*/ + +#include "fig/FIG0structs.h" +#include "fig/FIG0_20.h" +#include "utils.h" + +namespace FIC { + +// FIG 0/20 header structure for programme services (16-bit SId) +struct FIGtype0_20_programme_header { + uint8_t SId_high; + uint8_t SId_low; + uint8_t sc_flag:1; + uint8_t pt_flag:1; + uint8_t change_flags:2; + uint8_t SCIdS:4; +} PACKED; + +// FIG 0/20 header structure for data services (32-bit SId) +struct FIGtype0_20_data_header { + uint8_t SId_byte0; + uint8_t SId_byte1; + uint8_t SId_byte2; + uint8_t SId_byte3; + uint8_t sc_flag:1; + uint8_t pt_flag:1; + uint8_t change_flags:2; + uint8_t SCIdS:4; +} PACKED; + +// SC description structure (1 byte, only present if SC flag = 1) +struct FIGtype0_20_sc_desc { + uint8_t SCTy:6; + uint8_t ad_flag:1; + uint8_t ca_flag:1; +} PACKED; + +FIG0_20::FIG0_20(FIGRuntimeInformation *rti) : + m_rti(rti), + m_initialised(false) +{ } + +FillStatus FIG0_20::fill(uint8_t *buf, size_t max_size) +{ + FillStatus fs; + ssize_t remaining = max_size; + + auto ensemble = m_rti->ensemble; + + if (ensemble->sci_entries.empty()) { + fs.complete_fig_transmitted = true; + fs.num_bytes_written = 0; + return fs; + } + + if (not m_initialised) { + sci_it = ensemble->sci_entries.end(); + m_initialised = true; + } + + FIGtype0* fig0 = NULL; + + for (; sci_it != ensemble->sci_entries.end(); ++sci_it) { + auto& sci = *sci_it; + + if (!sci->is_active()) { + continue; + } + + // Calculate required size for this SCI entry + size_t required_size = 0; + + if (sci->isProgramme) { + required_size += 3; // 2 bytes SId + 1 byte flags + } else { + required_size += 5; // 4 bytes SId + 1 byte flags + } + + if (sci->sc_flag) { + required_size += 1; + } + + // Date-time field: 3 bytes + required_size += 3; + + // Transfer SId + if (sci->sid_flag) { + required_size += sci->isProgramme ? 2 : 4; + } + + // Transfer EId + if (sci->eid_flag) { + required_size += 2; + } + + if (fig0 == NULL) { + if (remaining < 2 + (ssize_t)required_size) { + break; + } + + fig0 = (FIGtype0*)buf; + fig0->FIGtypeNumber = 0; + fig0->Length = 1; + fig0->CN = 0; + fig0->OE = 0; + fig0->PD = sci->isProgramme ? 0 : 1; + fig0->Extension = 20; + + buf += 2; + remaining -= 2; + } + else if (remaining < (ssize_t)required_size) { + break; + } + + uint8_t* entry_start = buf; + + if (sci->isProgramme) { + auto header = (FIGtype0_20_programme_header*)buf; + header->SId_high = (sci->SId >> 8) & 0xFF; + header->SId_low = sci->SId & 0xFF; + header->SCIdS = sci->SCIdS & 0x0F; + header->change_flags = static_cast(sci->change_flags) & 0x03; + header->pt_flag = sci->part_time ? 1 : 0; + header->sc_flag = sci->sc_flag ? 1 : 0; + buf += 3; + } + else { + auto header = (FIGtype0_20_data_header*)buf; + header->SId_byte0 = (sci->SId >> 24) & 0xFF; + header->SId_byte1 = (sci->SId >> 16) & 0xFF; + header->SId_byte2 = (sci->SId >> 8) & 0xFF; + header->SId_byte3 = sci->SId & 0xFF; + header->SCIdS = sci->SCIdS & 0x0F; + header->change_flags = static_cast(sci->change_flags) & 0x03; + header->pt_flag = sci->part_time ? 1 : 0; + header->sc_flag = sci->sc_flag ? 1 : 0; + buf += 5; + } + + if (sci->sc_flag) { + auto sc_desc = (FIGtype0_20_sc_desc*)buf; + sc_desc->ca_flag = sci->ca_flag ? 1 : 0; + sc_desc->ad_flag = sci->ad_flag ? 1 : 0; + sc_desc->SCTy = sci->SCTy & 0x3F; + buf += 1; + } + + // Date-time field (3 bytes) + // Byte 0: date[4:0] + hour[4:2] + // Byte 1: hour[1:0] + minute[5:0] + // Byte 2: second[5:0] + sid_flag + eid_flag + uint8_t date_val = sci->date & 0x1F; + uint8_t hour_val = sci->hour & 0x1F; + uint8_t minute_val = sci->minute & 0x3F; + uint8_t second_val = sci->second & 0x3F; + + buf[0] = (date_val << 3) | (hour_val >> 2); + buf[1] = ((hour_val & 0x03) << 6) | minute_val; + buf[2] = (second_val << 2) | ((sci->sid_flag ? 1 : 0) << 1) | (sci->eid_flag ? 1 : 0); + buf += 3; + + if (sci->sid_flag) { + if (sci->isProgramme) { + buf[0] = (sci->transfer_sid >> 8) & 0xFF; + buf[1] = sci->transfer_sid & 0xFF; + buf += 2; + } + else { + buf[0] = (sci->transfer_sid >> 24) & 0xFF; + buf[1] = (sci->transfer_sid >> 16) & 0xFF; + buf[2] = (sci->transfer_sid >> 8) & 0xFF; + buf[3] = sci->transfer_sid & 0xFF; + buf += 4; + } + } + + if (sci->eid_flag) { + buf[0] = (sci->transfer_eid >> 8) & 0xFF; + buf[1] = sci->transfer_eid & 0xFF; + buf += 2; + } + + size_t entry_size = buf - entry_start; + fig0->Length += entry_size; + remaining -= entry_size; + } + + if (sci_it == ensemble->sci_entries.end()) { + sci_it = ensemble->sci_entries.begin(); + fs.complete_fig_transmitted = true; + } + + fs.num_bytes_written = max_size - remaining; + return fs; +} + +} diff --git a/src/fig/FIG0_20.h b/src/fig/FIG0_20.h new file mode 100644 index 0000000..2ad6c94 --- /dev/null +++ b/src/fig/FIG0_20.h @@ -0,0 +1,52 @@ +/* + Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + 2011, 2012 Her Majesty the Queen in Right of Canada (Communications + Research Center Canada) + + Copyright (C) 2024 + Matthias P. Braendli, matthias.braendli@mpb.li + */ +/* + 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 +#include + +namespace FIC { + +// FIG type 0/20 +// Service Component Information (SCI) +// See ETSI TS 103 176 clause 6 +class FIG0_20 : public IFIG +{ + public: + FIG0_20(FIGRuntimeInformation* rti); + virtual FillStatus fill(uint8_t *buf, size_t max_size); + virtual FIG_rate repetition_rate() const { return FIG_rate::B; } + + virtual int figtype() const { return 0; } + virtual int figextension() const { return 20; } + + private: + FIGRuntimeInformation *m_rti; + bool m_initialised; + vec_sp_sci::iterator sci_it; +}; + +} diff --git a/src/fig/FIGCarousel.cpp b/src/fig/FIGCarousel.cpp index 4c5c04c..3d627c9 100644 --- a/src/fig/FIGCarousel.cpp +++ b/src/fig/FIGCarousel.cpp @@ -89,6 +89,7 @@ FIGCarousel::FIGCarousel(std::shared_ptr ensemble) : m_fig1_5(&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_fig2_0(&m_rti), @@ -130,6 +131,7 @@ FIGCarousel::FIGCarousel(std::shared_ptr ensemble) : load_and_allocate(m_fig1_5, FIBAllocation::FIB_ANY); load_and_allocate(m_fig0_18, FIBAllocation::FIB_ANY); load_and_allocate(m_fig0_19, FIBAllocation::FIB_ANY); + load_and_allocate(m_fig0_20, FIBAllocation::FIB_ANY); load_and_allocate(m_fig0_21, FIBAllocation::FIB_ANY); load_and_allocate(m_fig0_24, FIBAllocation::FIB_ANY); diff --git a/src/fig/FIGCarousel.h b/src/fig/FIGCarousel.h index 00b6d18..b56bec4 100644 --- a/src/fig/FIGCarousel.h +++ b/src/fig/FIGCarousel.h @@ -114,6 +114,7 @@ class FIGCarousel { FIG1_5 m_fig1_5; 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; FIG2_0 m_fig2_0; -- cgit v1.2.3 From 18398455642cf265995aa2988f1f3f610c6baf03 Mon Sep 17 00:00:00 2001 From: Samuel Hunt Date: Thu, 8 Jan 2026 16:03:03 +0000 Subject: Priority based FIC scheduler capable of >40 services --- src/fig/FIGCarouselPriority.cpp | 585 ++++++++++++++++++++++++++++++++++++++++ src/fig/FIGCarouselPriority.h | 257 ++++++++++++++++++ src/fig/FIGSchedulerType.cpp | 60 +++++ src/fig/FIGSchedulerType.h | 49 ++++ 4 files changed, 951 insertions(+) create mode 100644 src/fig/FIGCarouselPriority.cpp create mode 100644 src/fig/FIGCarouselPriority.h create mode 100644 src/fig/FIGSchedulerType.cpp create mode 100644 src/fig/FIGSchedulerType.h (limited to 'src') 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 . +*/ + +#include "fig/FIGCarouselPriority.h" +#include "fig/FIG0_20.h" +#include "crc.h" + +#include +#include +#include +#include + +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 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 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::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 . +*/ + +#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 +#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 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 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 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; + + // 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 . +*/ + +#include "fig/FIGSchedulerType.h" +#include "Log.h" +#include +#include + +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 . +*/ + +#pragma once + +#include + +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 -- cgit v1.2.3 From b084bd07570cd031cbba4cc0617418883d82a9c7 Mon Sep 17 00:00:00 2001 From: Samuel Hunt Date: Thu, 8 Jan 2026 16:13:19 +0000 Subject: Priority based FIC scheduler capable of >40 services --- Makefile.am | 4 ++++ doc/example.mux | 4 ++++ src/ConfigParser.cpp | 5 +++++ src/DabMultiplexer.cpp | 32 ++++++++++++++++++++++++++++---- src/DabMultiplexer.h | 15 ++++++++++++++- src/MuxElements.h | 2 ++ src/utils.cpp | 3 +++ 7 files changed, 60 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/Makefile.am b/Makefile.am index 1cf101a..c1b7731 100644 --- a/Makefile.am +++ b/Makefile.am @@ -138,6 +138,10 @@ odr_dabmux_SOURCES =src/DabMux.cpp \ src/fig/FIG2.h \ src/fig/FIGCarousel.cpp \ src/fig/FIGCarousel.h \ + src/fig/FIGCarouselPriority.cpp \ + src/fig/FIGCarouselPriority.h \ + src/fig/FIGSchedulerType.cpp \ + src/fig/FIGSchedulerType.h \ src/fig/TransitionHandler.h \ src/mpeg.h \ src/mpeg.c \ diff --git a/doc/example.mux b/doc/example.mux index ae12fb2..7a8739b 100644 --- a/doc/example.mux +++ b/doc/example.mux @@ -86,6 +86,10 @@ general { ; system has had time to setup the clock. ;startupcheck "chronyc waitsync 10 0.01" ;startupcheck "ntp-wait -fv" + + ; Enable the Maxxwave priority tree round-robin FIC scheduler that seems to respect repetition rates >40 services + ; FIC-Scheduler default is the default scheduler that seems to fall apart after around 15 services + fic-scheduler priority } remotecontrol { diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp index 8f4052c..4e20dea 100644 --- a/src/ConfigParser.cpp +++ b/src/ConfigParser.cpp @@ -55,6 +55,7 @@ #include #include #include +#include "fig/FIGSchedulerType.h" using namespace std; using boost::property_tree::ptree; @@ -532,6 +533,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 7a8ac97..32ed2b3 100644 --- a/src/DabMultiplexer.cpp +++ b/src/DabMultiplexer.cpp @@ -158,8 +158,7 @@ DabMultiplexer::DabMultiplexer(DabMultiplexerConfig& config) : m_config(config), m_time(), ensemble(std::make_shared()), - m_clock_tai(split_pipe_separated_string(m_config.pt.get("general.tai_clock_bulletins", ""))), - fig_carousel(ensemble, [&]() { return m_time.get_milliseconds_seconds(); }) + m_clock_tai(split_pipe_separated_string(m_config.pt.get("general.tai_clock_bulletins", ""))) { RC_ADD_PARAMETER(frames, "Show number of frames generated [read-only]"); RC_ADD_PARAMETER(tist_offset, "Configured tist-offset"); @@ -185,6 +184,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()); @@ -489,6 +502,17 @@ void DabMultiplexer::reload_linkagesets() } } +/* 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 >& outputs) { @@ -709,9 +733,9 @@ void DabMultiplexer::mux_frame(std::vector >& 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 620e65d..afb308f 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" @@ -130,5 +132,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 m_fig_carousel_classic; + std::unique_ptr 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 8ba103f..ab05ce3 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] = { @@ -410,6 +411,7 @@ class dabEnsemble : public RemoteControllable { std::vector > linkagesets; std::vector frequency_information; std::vector service_other_ensemble; + FIC::FIGSchedulerType fic_scheduler = FIC::FIGSchedulerType::Classic; vec_sp_sci sci_entries; }; diff --git a/src/utils.cpp b/src/utils.cpp index 7ea6293..127ba78 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -31,6 +31,7 @@ #include #include "utils.h" #include "fig/FIG0structs.h" +#include "fig/FIGSchedulerType.h" using namespace std; @@ -496,6 +497,8 @@ void printEnsemble(const shared_ptr& 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; -- cgit v1.2.3