/*
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
#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()
{
// Return front of carousel if non-empty
// Any FIG can potentially send - it will return 0 bytes if nothing to send
if (!carousel.empty()) {
return carousel.front();
}
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 based on WorldDAB "Guidance on FIC generation
* for high service count ensembles":
*
* Priority 0 (every frame): 0/0, 0/7 - Fixed insertion (first FIB of every frame)
* Priority 1 (reset 1): 0/1, 0/2 - Core MCI (fixed share, ~50% of FIC)
* Priority 2 (reset 2): 1/1, 1/0 - Service labels + ensemble label (fixed share)
* Critical for scan completion - receivers timeout!
* Priority 3 (reset 4): 0/9 - ECC (timing critical, once/sec)
* Priority 4 (reset 8): 0/3, 0/8, 0/13 - Packet mode, component details
* Priority 5 (reset 16): 0/5, 0/10, 0/17, 0/18, 0/20 - Scaled insertion
* 1/4, 1/5, 2/0, 2/1, 2/4, 2/5 - Other labels (match label cycle)
* PTY (0/17) is NOT essential for scan, just nice-to-have
* Priority 6 (reset 32): 0/14, 0/19 - Announcements, FEC
* Priority 7 (reset 64): 0/6 - Service linking (short form)
* Priority 8 (reset 128): 0/21, 0/24 - Frequency info, OE (best effort, 2 mins)
* Priority 9 (reserved): (future/dynamic use)
*
* Key insight from WorldDAB: For >20 services, service labels (1/1) are
* critical for scan completion. Receivers abort scan if labels timeout.
* PTY and other metadata should be "scaled insertion" - match label cycle,
* not compete with it.
*/
// Priority 0: Fixed insertion (every frame at framephase 0)
add_fig_to_priority(m_fig0_0, 0);
add_fig_to_priority(m_fig0_7, 0);
// Priority 1: Core MCI (fixed share - ~50% of FIC capacity)
add_fig_to_priority(m_fig0_1, 1);
add_fig_to_priority(m_fig0_2, 1);
// Priority 2: Service labels (fixed share - critical for scan completion!)
// WorldDAB: "labels should get 2 FIGs per 96ms frame"
// Ensemble label should match service label rate
add_fig_to_priority(m_fig1_1, 2); // Programme service labels - PROMOTED
add_fig_to_priority(m_fig1_0, 2); // Ensemble label - PROMOTED
// Priority 3: ECC (timing critical - once per second)
add_fig_to_priority(m_fig0_9, 3);
// Priority 4: Component details
add_fig_to_priority(m_fig0_3, 4); // Packet mode
add_fig_to_priority(m_fig0_8, 4); // Service component global def
add_fig_to_priority(m_fig0_13, 4); // User applications
// Priority 5: Scaled insertion (should match label cycle time)
// These are important but NOT critical for scan completion
add_fig_to_priority(m_fig0_5, 5); // Language
add_fig_to_priority(m_fig0_10, 5); // Date and time
add_fig_to_priority(m_fig0_17, 5); // PTY - DEMOTED (not essential for scan)
add_fig_to_priority(m_fig0_18, 5); // Announcement support
add_fig_to_priority(m_fig0_20, 5); // SCI - Service Component Information
// Other labels (component, data service) - less critical than service labels
add_fig_to_priority(m_fig1_4, 5); // Component labels
add_fig_to_priority(m_fig1_5, 5); // Data service labels
add_fig_to_priority(m_fig2_0, 5); // Extended ensemble label
add_fig_to_priority(m_fig2_1, 5); // Extended service labels
add_fig_to_priority(m_fig2_4, 5); // Extended component labels
add_fig_to_priority(m_fig2_5, 5); // Extended data service labels
// Priority 6: Announcements and FEC
add_fig_to_priority(m_fig0_14, 6); // FEC subchannel organisation
add_fig_to_priority(m_fig0_19, 6); // Announcement switching
// Priority 7: Service linking
add_fig_to_priority(m_fig0_6, 7); // Service linking (short form)
// Priority 8: Frequency/OE (best effort - within 2 minutes)
add_fig_to_priority(m_fig0_21, 8); // Frequency information
add_fig_to_priority(m_fig0_24, 8); // OE services
// 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) {
bool start_new = entry->tick_deadline(elapsed_ms);
// Check if a new cycle should start
if (start_new) {
entry->start_new_cycle();
}
}
}
void FIGCarouselPriority::check_and_log_deadlines(uint64_t current_frame)
{
// Log every ~6 seconds (250 frames at 24ms)
if ((current_frame % 250) != 0) {
return;
}
std::stringstream ss_slow; // 1x-2x nominal (acceptable per WorldDAB)
std::stringstream ss_warning; // 2x-3x nominal (approaching limit)
std::stringstream ss_critical; // >3x nominal (unacceptable)
bool any_slow = false;
bool any_warning = false;
bool any_critical = false;
for (auto& entry : m_all_entries) {
if (entry->deadline_violated && entry->cycle_count > 0) {
// Check how severe the violation is based on average cycle time
uint64_t avg = entry->avg_cycle_time_ms();
uint64_t required = entry->rate_ms;
if (avg > required * 3) {
ss_critical << " " << entry->name();
any_critical = true;
} else if (avg > required * 2) {
ss_warning << " " << entry->name();
any_warning = true;
} else if (avg > required) {
ss_slow << " " << entry->name();
any_slow = true;
}
entry->deadline_violated = false;
}
}
// Log critical issues first (these violate WorldDAB guidance)
if (any_critical) {
etiLog.level(warn) << "FIGCarouselPriority: UNACCEPTABLE repetition rates (>3x nominal) for FIGs:" << ss_critical.str();
}
// Log warnings (approaching limit)
if (any_warning) {
etiLog.level(info) << "FIGCarouselPriority: Slow repetition rates (2x-3x nominal) for FIGs:" << ss_warning.str();
}
// Log slow but acceptable (only if no worse issues, to avoid log spam)
if (any_slow && !any_warning && !any_critical) {
etiLog.level(info) << "FIGCarouselPriority: Repetition rates slightly exceeded for FIGs:" << ss_slow.str();
}
}
void FIGCarouselPriority::log_rate_statistics(uint64_t current_frame)
{
#if PRIORITY_RATE_STATS_DEBUG
// Log every ~10 seconds (approx 417 frames)
m_stats_log_counter++;
if (m_stats_log_counter < 417) {
return;
}
m_stats_log_counter = 0;
etiLog.level(info) << "=== FIG Repetition Rate Statistics ===";
etiLog.level(info) << "FIG Required Avg Min Max Cycles";
etiLog.level(info) << "------------ -------- -------- -------- -------- --------";
for (auto& entry : m_all_entries) {
if (entry->cycle_count > 0) {
std::stringstream ss;
ss << std::left << std::setw(12) << entry->name()
<< " " << std::right << std::setw(7) << entry->rate_ms << "ms"
<< " " << std::setw(7) << entry->avg_cycle_time_ms() << "ms"
<< " " << std::setw(7) << entry->min_cycle_time_ms << "ms"
<< " " << std::setw(7) << entry->max_cycle_time_ms << "ms"
<< " " << std::setw(8) << entry->cycle_count;
// WorldDAB guidance: worst case is 3x nominal rate
// - Within spec: no marker
// - SLOW (1x-2x): exceeds nominal but well within WorldDAB limits
// - [!] (2x-3x): approaching WorldDAB worst-case limit
// - UNACCEPTABLE (>3x): violates WorldDAB guidance
uint64_t avg = entry->avg_cycle_time_ms();
uint64_t required = entry->rate_ms;
if (avg > required * 3) {
ss << " UNACCEPTABLE"; // >3x nominal - violates WorldDAB
} else if (avg > required * 2) {
ss << " [!]"; // 2x-3x nominal - approaching limit
} else if (avg > required) {
ss << " [SLOW]"; // 1x-2x nominal - exceeds but acceptable
}
etiLog.level(info) << ss.str();
}
// Reset stats for next interval
entry->reset_stats();
}
etiLog.level(info) << "======================================";
#else
(void)current_frame;
#endif
}
size_t FIGCarouselPriority::write_fibs(uint8_t* buf, uint64_t current_frame, bool fib3_present)
{
m_rti.currentFrame = current_frame;
// Tick all deadline monitors (24ms per frame)
tick_all_deadlines(24);
// Periodically log missed deadlines
check_and_log_deadlines(current_frame);
// Periodically log rate statistics (if enabled)
log_rate_statistics(current_frame);
const int fibCount = fib3_present ? 4 : 3;
const int framephase = current_frame % 4;
for (int fib = 0; fib < fibCount; fib++) {
memset(buf, 0x00, 30);
size_t figSize = fill_fib(buf, 30, fib, 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;
}
return 32 * fibCount;
}
size_t FIGCarouselPriority::fill_fib(uint8_t* buf, size_t max_size, int fib_index, int framephase)
{
size_t written = 0;
#if PRIORITY_CAROUSEL_DEBUG
std::stringstream fib_log;
fib_log << "FIB" << fib_index << " fp" << framephase << ": ";
#endif
// Step 1: Priority 0 - FIG 0/0 and 0/7 (only in FIB 0 at framephase 0)
written += send_priority_zero(buf, max_size, fib_index, framephase);
size_t remaining = max_size - written;
uint8_t* pbuf = buf + written;
// Track which FIGs we've tried this pass to avoid infinite loops
std::unordered_set tried_this_fib;
// Step 2: Must-send pass - clear all urgent FIGs using cascading priority
// We iterate through priorities and try each must_send FIG.
// A FIG might return 0 bytes if it doesn't fit, so we try all of them.
bool made_progress = true;
while (remaining > 2 && made_progress) {
made_progress = false;
// Try each priority, starting from the selected one
int start_prio = select_priority();
if (start_prio < 0) {
start_prio = 1;
}
// Look for must_send FIGs in priority order
for (int prio_offset = 0; prio_offset < NUM_PRIORITIES - 1 && remaining > 2; prio_offset++) {
int prio = start_prio + prio_offset;
if (prio >= NUM_PRIORITIES) {
prio = 1 + (prio - NUM_PRIORITIES); // Wrap around, skip 0
}
if (!m_priorities[prio].has_must_send()) {
continue;
}
// Try all must_send FIGs in this priority
size_t carousel_size = m_priorities[prio].carousel.size();
for (size_t fig_attempt = 0; fig_attempt < carousel_size && remaining > 2; fig_attempt++) {
FIGEntryPriority* entry = m_priorities[prio].find_must_send();
if (!entry) {
break; // No more must_send in this priority
}
// Skip if we've already tried this FIG this FIB
if (tried_this_fib.count(entry)) {
m_priorities[prio].move_to_back(entry);
continue;
}
tried_this_fib.insert(entry);
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[prio].move_to_back(entry);
on_fig_sent(prio);
made_progress = true;
} else {
// FIG couldn't write (doesn't fit), move to back and try next
m_priorities[prio].move_to_back(entry);
}
}
}
}
// Step 3: Can-send pass - fill remaining space opportunistically
// By now, there should be no must_send FIGs (cleared in step 2)
// Any FIG can send to fill the FIB and improve repetition rates
//
// TWO-PASS APPROACH to avoid over-serving FIGs that are ahead of schedule:
// Pass 1: Only try FIGs that are past 50% of their deadline (actually need bandwidth)
// Pass 2: If still space, try any FIG that can_send (avoid wasting FIB space)
//
// This ensures bandwidth goes to FIGs that need it (like 1/1 at ~1200ms vs 960ms target)
// rather than FIGs already well ahead of schedule (like 0/9 at ~500ms vs 960ms target)
//
// NOTE: We do NOT update the priority counters here. The counter system
// is for bandwidth allocation in must_send. In can_send, we just fill space.
tried_this_fib.clear();
// Pass 1: FIGs that are past 50% of their deadline (need bandwidth more urgently)
for (int prio = 1; prio < NUM_PRIORITIES && remaining > 2; prio++) {
if (m_priorities[prio].carousel.empty()) {
continue;
}
size_t carousel_size = m_priorities[prio].carousel.size();
for (size_t fig_attempt = 0; fig_attempt < carousel_size && remaining > 2; fig_attempt++) {
FIGEntryPriority* entry = m_priorities[prio].find_can_send();
if (!entry) {
break;
}
// Skip if we've already tried this FIG this FIB
if (tried_this_fib.count(entry)) {
m_priorities[prio].move_to_back(entry);
continue;
}
// Pass 1: Only try FIGs past 50% of their deadline
if (!entry->past_deadline_percent(50)) {
tried_this_fib.insert(entry);
m_priorities[prio].move_to_back(entry);
continue;
}
tried_this_fib.insert(entry);
size_t bytes = try_send_fig(entry, pbuf, remaining);
if (bytes > 0) {
#if PRIORITY_CAROUSEL_DEBUG
fib_log << entry->name() << ":" << bytes << "B(urg) ";
#endif
written += bytes;
remaining -= bytes;
pbuf += bytes;
}
m_priorities[prio].move_to_back(entry);
}
}
// Pass 2: Any remaining FIGs that can send (to fill unused space)
// Reset tried set - we want to try everything again, but only those we skipped
std::set tried_pass1 = tried_this_fib;
for (int prio = 1; prio < NUM_PRIORITIES && remaining > 2; prio++) {
if (m_priorities[prio].carousel.empty()) {
continue;
}
size_t carousel_size = m_priorities[prio].carousel.size();
for (size_t fig_attempt = 0; fig_attempt < carousel_size && remaining > 2; fig_attempt++) {
FIGEntryPriority* entry = m_priorities[prio].find_can_send();
if (!entry) {
break;
}
// Skip if already sent in pass 1
if (tried_pass1.count(entry) && tried_this_fib.count(entry)) {
// Check if it was actually sent (not just tried and skipped)
// If it was skipped due to not being urgent, we can try it now
if (entry->past_deadline_percent(50)) {
// Was tried and is urgent - was actually attempted, skip
m_priorities[prio].move_to_back(entry);
continue;
}
}
tried_this_fib.insert(entry);
size_t bytes = try_send_fig(entry, pbuf, remaining);
if (bytes > 0) {
#if PRIORITY_CAROUSEL_DEBUG
fib_log << entry->name() << ":" << bytes << "B(fill) ";
#endif
written += bytes;
remaining -= bytes;
pbuf += bytes;
}
m_priorities[prio].move_to_back(entry);
}
}
#if PRIORITY_CAROUSEL_DEBUG
fib_log << "= " << written << "/" << max_size;
etiLog.level(info) << fib_log.str();
#endif
return written;
}
size_t FIGCarouselPriority::send_priority_zero(uint8_t* buf, size_t max_size, int fib_index, int framephase)
{
size_t written = 0;
// EN 300 401 Clause 6.4.1: FIG 0/0 shall only be transmitted in the first
// FIB of the first CIF of each FIC (once per 96ms in Mode I)
// It must be the first FIG in that FIB
// Only send in FIB 0 at framephase 0
if (fib_index != 0 || framephase != 0) {
return 0;
}
#if PRIORITY_CAROUSEL_DEBUG
etiLog.level(info) << "send_priority_zero: FIB0, 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(info) << " 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;
}
// Mark cycle complete if the FIG says so
if (status.complete_fig_transmitted) {
entry->on_cycle_complete(status.num_bytes_written > 0);
}
if (status.num_bytes_written == 0) {
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(info) << " 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(info) << " 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, reset the cycle - pass whether data was sent
if (status.complete_fig_transmitted) {
entry->on_cycle_complete(status.num_bytes_written > 0);
#if PRIORITY_CAROUSEL_DEBUG
etiLog.level(info) << " 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;
}
int FIGCarouselPriority::cascade_find_priority(int start, bool must_send_only)
{
// Cascade from start priority down to 8, then wrap to 1 and continue
// until we reach start again (full circle)
// First pass: start -> NUM_PRIORITIES-1
for (int p = start; p < NUM_PRIORITIES; p++) {
if (must_send_only) {
if (m_priorities[p].has_must_send()) {
return p;
}
} else {
if (m_priorities[p].has_can_send()) {
return p;
}
}
}
// Second pass: 1 -> start-1 (wrap around, skip priority 0)
for (int p = 1; p < start; p++) {
if (must_send_only) {
if (m_priorities[p].has_must_send()) {
return p;
}
} else {
if (m_priorities[p].has_can_send()) {
return p;
}
}
}
return -1; // Nothing found in any priority
}
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());
}
// Handle cycle completion
// Pass whether data was actually sent - only record stats if we transmitted something
if (status.complete_fig_transmitted) {
entry->on_cycle_complete(written > 0);
}
return written;
}
} // namespace FIC