diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c index 21930d1..d8bc33f 100644 --- a/misrc_tools/common/ddd_protocol.c +++ b/misrc_tools/common/ddd_protocol.c @@ -53,6 +53,13 @@ static bool ddd_read_register(const ddd_control_ops_t *ops, return ddd_control_in(ops, DDD_REQUEST_REGISTER_READ, address, value, 1); } +static uint16_t ddd_read_little_endian_word(const uint8_t *block, + size_t offset) +{ + return (uint16_t)(block[offset] | + ((uint16_t)block[offset + 1u] << 8)); +} + bool ddd_is_known_device_id(uint16_t vendor_id, uint16_t product_id) { return (vendor_id == DDD_LEGACY_VENDOR_ID && @@ -351,6 +358,143 @@ void ddd_collection_state_init(ddd_collection_state_t *state) state->profile = DDD_DEVICE_NOT_DDD; } +void ddd_fifo_telemetry_init(ddd_fifo_telemetry_t *telemetry) +{ + if (telemetry) memset(telemetry, 0, sizeof(*telemetry)); +} + +bool ddd_fifo_telemetry_parse(const uint8_t *block, + size_t block_length, + ddd_fifo_telemetry_t *telemetry) +{ + uint8_t status; + uint8_t format; + uint16_t depth; + uint16_t packet; + + if (!telemetry) return false; + ddd_fifo_telemetry_init(telemetry); + if (!block || block_length < DDD_FIFO_TELEMETRY_LENGTH || + block[0] != DDD_FIFO_TELEMETRY_ID) { + return false; + } + + status = block[DDD_FIFO_OFFSET_STATUS]; + format = status & DDD_FIFO_TELEMETRY_FORMAT_MASK; + if (format != DDD_FIFO_TELEMETRY_FORMAT) return false; + + depth = ddd_read_little_endian_word(block, DDD_FIFO_OFFSET_DEPTH); + packet = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_PACKET_WORDS); + if (packet == 0 || depth <= packet) return false; + + telemetry->present = true; + telemetry->format = format; + telemetry->overflow_seen = + (status & DDD_FIFO_FLAG_OVERFLOW_SEEN) != 0; + telemetry->saturated = (status & DDD_FIFO_FLAG_SATURATED) != 0; + telemetry->latch_count = block[DDD_FIFO_OFFSET_LATCH_COUNT]; + telemetry->used_now = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_USED_NOW); + telemetry->peak = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_PEAK); + telemetry->peak_since_open = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_PEAK_LIFETIME); + telemetry->overflow_events = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_OVERFLOWS); + telemetry->dropped_words = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_DROPPED); + telemetry->packets_read = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_PACKETS); + telemetry->near_full_units = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_NEAR_FULL); + telemetry->depth_words = depth; + telemetry->packet_words = packet; + telemetry->near_full_words = ddd_read_little_endian_word( + block, DDD_FIFO_OFFSET_NEAR_FULL_WORDS); + return true; +} + +int ddd_fifo_backpressure_percent(const ddd_fifo_telemetry_t *telemetry) +{ + int headroom; + int excursion; + int percent; + + if (!telemetry || !telemetry->present) return 0; + if (telemetry->overflow_events > 0) return 100; + if (telemetry->peak <= telemetry->packet_words) return 0; + + headroom = (int)telemetry->depth_words - (int)telemetry->packet_words; + if (headroom <= 0) return 0; + excursion = (int)telemetry->peak - (int)telemetry->packet_words; + percent = excursion * 100 / headroom; + if (percent < 0) return 0; + return percent > 100 ? 100 : percent; +} + +int ddd_fifo_peak_percent(const ddd_fifo_telemetry_t *telemetry) +{ + int percent; + if (!telemetry || !telemetry->present || telemetry->depth_words == 0) { + return 0; + } + percent = (int)telemetry->peak * 100 / (int)telemetry->depth_words; + if (percent < 0) return 0; + return percent > 100 ? 100 : percent; +} + +int ddd_fifo_used_percent(const ddd_fifo_telemetry_t *telemetry) +{ + int percent; + if (!telemetry || !telemetry->present || telemetry->depth_words == 0) { + return 0; + } + percent = (int)telemetry->used_now * 100 / + (int)telemetry->depth_words; + if (percent < 0) return 0; + return percent > 100 ? 100 : percent; +} + +void ddd_fifo_telemetry_totals_init( + ddd_fifo_telemetry_totals_t *totals) +{ + if (!totals) return; + memset(totals, 0, sizeof(*totals)); + totals->interval_coverage_complete = true; +} + +bool ddd_fifo_telemetry_totals_add( + ddd_fifo_telemetry_totals_t *totals, + const ddd_fifo_telemetry_t *telemetry) +{ + uint8_t latch_delta; + int backpressure; + + if (!totals || !telemetry || !telemetry->present) return false; + if (totals->latch_seen) { + latch_delta = (uint8_t)(telemetry->latch_count - + totals->last_latch_count); + if (latch_delta == 0) return false; + if (latch_delta != 1) totals->interval_coverage_complete = false; + } + + totals->latch_seen = true; + totals->last_latch_count = telemetry->latch_count; + totals->saturated = totals->saturated || telemetry->saturated; + totals->overflow_events += telemetry->overflow_events; + totals->dropped_words += telemetry->dropped_words; + totals->near_full_units += telemetry->near_full_units; + if (telemetry->peak > totals->peak_words) { + totals->peak_words = telemetry->peak; + } + backpressure = ddd_fifo_backpressure_percent(telemetry); + if (backpressure > totals->peak_backpressure_percent) { + totals->peak_backpressure_percent = backpressure; + } + return true; +} + static ddd_protocol_result_t ddd_restore_safe_defaults( const ddd_control_ops_t *ops) { diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h index f06db1d..265a59e 100644 --- a/misrc_tools/common/ddd_protocol.h +++ b/misrc_tools/common/ddd_protocol.h @@ -43,6 +43,30 @@ extern "C" { #define DDD_REGISTER_TEST_MODE UINT8_C(0x10) #define DDD_REGISTER_DECIMATION UINT8_C(0x12) +/* Self-described FIFO telemetry block. Reading address 0x40 latches a + * coherent snapshot and clears the interval counters in the gateware. */ +#define DDD_REGISTER_FIFO_TELEMETRY UINT8_C(0x40) +#define DDD_FIFO_TELEMETRY_ID UINT8_C(0xBD) +#define DDD_FIFO_TELEMETRY_LENGTH 23u +#define DDD_FIFO_TELEMETRY_FORMAT UINT8_C(1) +#define DDD_FIFO_TELEMETRY_FORMAT_MASK UINT8_C(0x0F) +#define DDD_FIFO_FLAG_OVERFLOW_SEEN UINT8_C(0x10) +#define DDD_FIFO_FLAG_SATURATED UINT8_C(0x20) +#define DDD_FIFO_NEAR_FULL_PRESCALE 256u + +#define DDD_FIFO_OFFSET_STATUS 1u +#define DDD_FIFO_OFFSET_LATCH_COUNT 2u +#define DDD_FIFO_OFFSET_USED_NOW 3u +#define DDD_FIFO_OFFSET_PEAK 5u +#define DDD_FIFO_OFFSET_PEAK_LIFETIME 7u +#define DDD_FIFO_OFFSET_OVERFLOWS 9u +#define DDD_FIFO_OFFSET_DROPPED 11u +#define DDD_FIFO_OFFSET_PACKETS 13u +#define DDD_FIFO_OFFSET_NEAR_FULL 15u +#define DDD_FIFO_OFFSET_DEPTH 17u +#define DDD_FIFO_OFFSET_PACKET_WORDS 19u +#define DDD_FIFO_OFFSET_NEAR_FULL_WORDS 21u + #define DDD_DECIMATION_FULL_RATE UINT8_C(1) #define DDD_DECIMATION_HALF_RATE UINT8_C(2) #define DDD_CONVERTER_SAMPLE_RATE_HZ UINT32_C(40000000) @@ -174,6 +198,36 @@ typedef struct ddd_control_ops { void *context; } ddd_control_ops_t; +typedef struct ddd_fifo_telemetry { + bool present; + uint8_t format; + bool overflow_seen; + bool saturated; + uint8_t latch_count; + uint16_t used_now; + uint16_t peak; + uint16_t peak_since_open; + uint16_t overflow_events; + uint16_t dropped_words; + uint16_t packets_read; + uint16_t near_full_units; + uint16_t depth_words; + uint16_t packet_words; + uint16_t near_full_words; +} ddd_fifo_telemetry_t; + +typedef struct ddd_fifo_telemetry_totals { + bool latch_seen; + uint8_t last_latch_count; + bool interval_coverage_complete; + bool saturated; + uint64_t overflow_events; + uint64_t dropped_words; + uint64_t near_full_units; + uint16_t peak_words; + int peak_backpressure_percent; +} ddd_fifo_telemetry_totals_t; + typedef enum ddd_protocol_result { DDD_PROTOCOL_OK = 0, DDD_PROTOCOL_INVALID_ARGUMENT, @@ -199,6 +253,19 @@ typedef struct ddd_collection_state { void ddd_collection_state_init(ddd_collection_state_t *state); +void ddd_fifo_telemetry_init(ddd_fifo_telemetry_t *telemetry); +bool ddd_fifo_telemetry_parse(const uint8_t *block, + size_t block_length, + ddd_fifo_telemetry_t *telemetry); +int ddd_fifo_backpressure_percent(const ddd_fifo_telemetry_t *telemetry); +int ddd_fifo_peak_percent(const ddd_fifo_telemetry_t *telemetry); +int ddd_fifo_used_percent(const ddd_fifo_telemetry_t *telemetry); +void ddd_fifo_telemetry_totals_init( + ddd_fifo_telemetry_totals_t *totals); +bool ddd_fifo_telemetry_totals_add( + ddd_fifo_telemetry_totals_t *totals, + const ddd_fifo_telemetry_t *telemetry); + /* Protocol-v1 order: B7(identity), B8(test), B8(decimation), B7(test), * B7(decimation), B5(start). Every partial start is rolled back to B5(stop), * test=0 and decimation=1 with verified readback. */ diff --git a/misrc_tools/meson.build b/misrc_tools/meson.build index bdefa96..99cd38b 100644 --- a/misrc_tools/meson.build +++ b/misrc_tools/meson.build @@ -438,6 +438,7 @@ if raylib_dep.found() sources_gui += 'misrc_gui/input/gui_ddd_v1.c' sources_gui += 'misrc_gui/input/gui_ddd_async.c' sources_gui += 'misrc_gui/input/gui_ddd_clockgen.c' + sources_gui += 'misrc_gui/ui/gui_ddd_fifo_status.c' endif # Add RTL-SDR support to GUI if available @@ -569,6 +570,15 @@ ddd_protocol_test = executable('ddd_protocol_test', ) test('ddd_protocol', ddd_protocol_test) +ddd_fifo_status_test = executable('gui_ddd_fifo_status_test', + ['test/gui_ddd_fifo_status_test.c', + 'misrc_gui/ui/gui_ddd_fifo_status.c', + 'common/ddd_protocol.c'], + dependencies: [], + c_args: cflags, +) +test('gui_ddd_fifo_status', ddd_fifo_status_test) + ddd_async_policy_test = executable('gui_ddd_async_policy_test', 'test/gui_ddd_async_policy_test.c', dependencies: [], @@ -579,7 +589,8 @@ test('gui_ddd_async_policy', ddd_async_policy_test) if ddd_enabled ddd_async_fault_test = executable('gui_ddd_async_fault_test', ['test/gui_ddd_async_fault_test.c', - 'misrc_gui/input/gui_ddd_async.c'], + 'misrc_gui/input/gui_ddd_async.c', + 'common/ddd_protocol.c'], dependencies: [libusb_common_dep], c_args: cflags, ) diff --git a/misrc_tools/misrc_gui/input/gui_ddd_async.c b/misrc_tools/misrc_gui/input/gui_ddd_async.c index 7eb8e0e..7fdced2 100644 --- a/misrc_tools/misrc_gui/input/gui_ddd_async.c +++ b/misrc_tools/misrc_gui/input/gui_ddd_async.c @@ -5,8 +5,10 @@ #include #include #include +#include #include "../../common/libusb_compat.h" +#include "../../common/ddd_protocol.h" #include "../../common/threading.h" #include "gui_ddd_async.h" @@ -44,6 +46,13 @@ struct gui_ddd_async_orphan { size_t submitted_count; atomic_size_t in_flight; atomic_size_t callbacks_active; + struct libusb_transfer *telemetry_transfer; + uint8_t *telemetry_buffer; + atomic_bool telemetry_in_flight; + uint64_t telemetry_due_ms; + unsigned telemetry_failures; + bool telemetry_primed; + bool telemetry_cancel_requested; uint64_t cancel_deadline_ms; bool accepting_submissions; bool stopping; @@ -74,7 +83,8 @@ static bool gui_ddd_async_engine_has_pending( { if (!engine) return false; return gui_ddd_async_policy_has_pending( - atomic_load(&engine->in_flight), + atomic_load(&engine->in_flight) + + (atomic_load(&engine->telemetry_in_flight) ? 1u : 0u), atomic_load(&engine->callbacks_active)); } @@ -210,6 +220,144 @@ static int gui_ddd_async_submit_slot(gui_ddd_async_engine_t *engine, return 0; } +static void gui_ddd_async_note_telemetry_failure( + gui_ddd_async_engine_t *engine) +{ + if (!engine) return; + if (engine->telemetry_failures < UINT_MAX) { + engine->telemetry_failures++; + } + if (engine->result.telemetry_failures < UINT_MAX) { + engine->result.telemetry_failures++; + } + if (engine->telemetry_failures == + GUI_DDD_ASYNC_TELEMETRY_MAX_FAILURES && + !engine->stopping && engine->config->telemetry) { + /* Remove a stale instrument after the bounded retry budget. The + * backend callback is deliberately parse/publish-only, so this cannot + * stall the USB event pump. */ + engine->config->telemetry(engine->config->telemetry_context, NULL, 0); + } +} + +static void LIBUSB_CALL gui_ddd_async_telemetry_callback( + struct libusb_transfer *transfer) +{ + gui_ddd_async_engine_t *engine; + ddd_fifo_telemetry_t validated; + const uint8_t *data; + + if (!transfer || !transfer->user_data) return; + engine = (gui_ddd_async_engine_t *)transfer->user_data; + atomic_fetch_add(&engine->callbacks_active, 1); + atomic_store(&engine->telemetry_in_flight, false); + + if (transfer->status == LIBUSB_TRANSFER_COMPLETED) { + if (transfer->actual_length < (int)DDD_FIFO_TELEMETRY_LENGTH) { + gui_ddd_async_note_telemetry_failure(engine); + } else { + data = libusb_control_transfer_get_data(transfer); + if (!ddd_fifo_telemetry_parse( + data, (size_t)transfer->actual_length, &validated)) { + /* Older or incompatible protocol-v1 firmware may return a + * full-length placeholder for an unknown register. Count it + * against the same bounded retry budget as a refused read. */ + gui_ddd_async_note_telemetry_failure(engine); + } else { + engine->result.telemetry_readings++; + if (!engine->stopping && engine->config->telemetry && + gui_ddd_async_policy_telemetry_should_publish( + &engine->telemetry_primed)) { + engine->config->telemetry( + engine->config->telemetry_context, + data, DDD_FIFO_TELEMETRY_LENGTH); + } + } + } + } else if (transfer->status != LIBUSB_TRANSFER_CANCELLED) { + /* Telemetry is diagnostic. A refusal disables only the instrument + * after the bounded retry count; it never fails the RF queue. */ + gui_ddd_async_note_telemetry_failure(engine); + } + + atomic_fetch_sub(&engine->callbacks_active, 1); +} + +static void gui_ddd_async_maybe_submit_telemetry( + gui_ddd_async_engine_t *engine, + uint64_t now_ms) +{ + int rc; + bool enabled; + + if (!engine) return; + enabled = engine->config->telemetry != NULL && + engine->telemetry_transfer != NULL && + engine->telemetry_buffer != NULL && + !engine->stopping && !engine->failed; + if (!gui_ddd_async_policy_telemetry_should_submit( + enabled, + atomic_load(&engine->telemetry_in_flight), + engine->telemetry_failures, + now_ms, + engine->telemetry_due_ms)) { + return; + } + engine->telemetry_due_ms = + now_ms + GUI_DDD_ASYNC_TELEMETRY_INTERVAL_MS; + engine->telemetry_cancel_requested = false; + + libusb_fill_control_setup( + engine->telemetry_buffer, + DDD_USB_REQUEST_VENDOR_IN, + DDD_REQUEST_REGISTER_READ, + DDD_REGISTER_FIFO_TELEMETRY, + 0, + DDD_FIFO_TELEMETRY_LENGTH); + libusb_fill_control_transfer( + engine->telemetry_transfer, + engine->config->device_handle, + engine->telemetry_buffer, + gui_ddd_async_telemetry_callback, + engine, + GUI_DDD_ASYNC_TELEMETRY_TIMEOUT_MS); + + if (engine->config->submit_override) { + rc = engine->config->submit_override( + engine->config->submit_context, + engine->telemetry_transfer); + } else { + rc = libusb_submit_transfer(engine->telemetry_transfer); + } + if (rc < 0) { + gui_ddd_async_note_telemetry_failure(engine); + return; + } + atomic_store(&engine->telemetry_in_flight, true); +} + +static void gui_ddd_async_cancel_telemetry( + gui_ddd_async_engine_t *engine) +{ + int rc; + + if (!engine || !atomic_load(&engine->telemetry_in_flight) || + engine->telemetry_cancel_requested) { + return; + } + engine->telemetry_cancel_requested = true; + if (engine->config->cancel_override) { + rc = engine->config->cancel_override( + engine->config->cancel_context, + engine->telemetry_transfer); + } else { + rc = libusb_cancel_transfer(engine->telemetry_transfer); + } + /* NOT_FOUND means completion won the race. Other errors remain bounded by + * the same orphan deadline as the RF transfers. */ + (void)rc; +} + static void gui_ddd_async_cancel_in_flight(gui_ddd_async_engine_t *engine) { size_t i; @@ -219,6 +367,7 @@ static void gui_ddd_async_cancel_in_flight(gui_ddd_async_engine_t *engine) engine->cancel_deadline_ms = gui_ddd_async_now_ms(engine) + GUI_DDD_ASYNC_CANCEL_REAP_TIMEOUT_MS; engine->accepting_submissions = false; + gui_ddd_async_cancel_telemetry(engine); for (i = 0; i < GUI_DDD_ASYNC_TRANSFER_COUNT; i++) { gui_ddd_async_slot_t *slot = &engine->slots[i]; gui_ddd_async_policy_slot_t *policy_slot = @@ -351,6 +500,24 @@ static int gui_ddd_async_allocate(gui_ddd_async_engine_t *engine) return -1; } } + + if (engine->config->telemetry) { + engine->telemetry_transfer = libusb_alloc_transfer(0); + engine->telemetry_buffer = (uint8_t *)calloc( + 1, LIBUSB_CONTROL_SETUP_SIZE + DDD_FIFO_TELEMETRY_LENGTH); + if (!engine->telemetry_transfer || !engine->telemetry_buffer) { + if (engine->telemetry_transfer) { + libusb_free_transfer(engine->telemetry_transfer); + } + free(engine->telemetry_buffer); + engine->telemetry_transfer = NULL; + engine->telemetry_buffer = NULL; + engine->telemetry_failures = + GUI_DDD_ASYNC_TELEMETRY_MAX_FAILURES; + engine->result.telemetry_failures = + GUI_DDD_ASYNC_TELEMETRY_MAX_FAILURES; + } + } return 0; } @@ -360,8 +527,7 @@ static void gui_ddd_async_destroy(gui_ddd_async_engine_t *engine) if (!engine) return; /* Never release transfer/user-data storage while a callback is pending. */ - if (atomic_load(&engine->in_flight) != 0 || - atomic_load(&engine->callbacks_active) != 0) return; + if (gui_ddd_async_engine_has_pending(engine)) return; if (engine->slots) { for (i = 0; i < GUI_DDD_ASYNC_TRANSFER_COUNT; i++) { if (engine->slots[i].transfer) { @@ -371,8 +537,14 @@ static void gui_ddd_async_destroy(gui_ddd_async_engine_t *engine) } free(engine->buffer_pool); free(engine->slots); + if (engine->telemetry_transfer) { + libusb_free_transfer(engine->telemetry_transfer); + } + free(engine->telemetry_buffer); engine->buffer_pool = NULL; engine->slots = NULL; + engine->telemetry_transfer = NULL; + engine->telemetry_buffer = NULL; free(engine); } @@ -417,6 +589,7 @@ int gui_ddd_async_run(const gui_ddd_async_config_t *config, engine->accepting_submissions = true; atomic_init(&engine->in_flight, 0); atomic_init(&engine->callbacks_active, 0); + atomic_init(&engine->telemetry_in_flight, false); gui_ddd_async_order_policy_init(&engine->order, GUI_DDD_ASYNC_TRANSFER_COUNT); @@ -444,12 +617,19 @@ int gui_ddd_async_run(const gui_ddd_async_config_t *config, bool capture_running = atomic_load(config->capture_running); uint64_t now_ms = gui_ddd_async_now_ms(engine); size_t in_flight; + size_t total_in_flight; + bool telemetry_in_flight; if (!capture_running && !engine->stopping) { engine->stopping = true; engine->accepting_submissions = false; stop_deadline_ms = now_ms + GUI_DDD_ASYNC_STOP_DRAIN_TIMEOUT_MS; + gui_ddd_async_cancel_telemetry(engine); + } + + if (capture_running) { + gui_ddd_async_maybe_submit_telemetry(engine, now_ms); } if (!engine->failed && @@ -458,9 +638,11 @@ int gui_ddd_async_run(const gui_ddd_async_config_t *config, } in_flight = atomic_load(&engine->in_flight); + telemetry_in_flight = atomic_load(&engine->telemetry_in_flight); + total_in_flight = in_flight + (telemetry_in_flight ? 1u : 0u); if (engine->failed) { gui_ddd_async_cancel_in_flight(engine); - } else if (engine->stopping && in_flight > 0 && + } else if (engine->stopping && total_in_flight > 0 && now_ms >= stop_deadline_ms) { gui_ddd_async_latch_failure( engine, GUI_DDD_ASYNC_RESULT_DRAIN_TIMEOUT, 0, 0, 0, @@ -469,19 +651,22 @@ int gui_ddd_async_run(const gui_ddd_async_config_t *config, } in_flight = atomic_load(&engine->in_flight); - if (engine->failed && in_flight == 0) break; + telemetry_in_flight = atomic_load(&engine->telemetry_in_flight); + total_in_flight = in_flight + (telemetry_in_flight ? 1u : 0u); + if (engine->failed && total_in_flight == 0) break; if (!engine->failed && gui_ddd_async_policy_stop_drain_complete( capture_running, in_flight, engine->result.completed_transfers, - engine->result.consumed_transfers)) { + engine->result.consumed_transfers) && + !telemetry_in_flight) { break; } if (gui_ddd_async_policy_reap_action( engine->cancel_issued, now_ms, engine->cancel_deadline_ms, - in_flight) == GUI_DDD_ASYNC_REAP_ORPHAN) { + total_in_flight) == GUI_DDD_ASYNC_REAP_ORPHAN) { break; } if (!engine->failed && capture_running && in_flight == 0) { @@ -503,7 +688,8 @@ int gui_ddd_async_run(const gui_ddd_async_config_t *config, if (gui_ddd_async_engine_has_pending(engine)) { engine->result.transfers_unreaped = true; engine->result.unreaped_transfers = - atomic_load(&engine->in_flight); + atomic_load(&engine->in_flight) + + (atomic_load(&engine->telemetry_in_flight) ? 1u : 0u); engine->result.active_callbacks = atomic_load(&engine->callbacks_active); engine->result.orphan = engine; @@ -528,8 +714,7 @@ bool gui_ddd_async_orphan_has_unreaped( bool gui_ddd_async_orphan_try_reclaim(gui_ddd_async_orphan_t *orphan) { - if (!orphan || atomic_load(&orphan->in_flight) != 0 || - atomic_load(&orphan->callbacks_active) != 0) return false; + if (!orphan || gui_ddd_async_engine_has_pending(orphan)) return false; gui_ddd_async_destroy(orphan); return true; } diff --git a/misrc_tools/misrc_gui/input/gui_ddd_async.h b/misrc_tools/misrc_gui/input/gui_ddd_async.h index ca71fad..dbbec61 100644 --- a/misrc_tools/misrc_gui/input/gui_ddd_async.h +++ b/misrc_tools/misrc_gui/input/gui_ddd_async.h @@ -27,6 +27,9 @@ struct libusb_transfer; #define GUI_DDD_ASYNC_STOP_DRAIN_TIMEOUT_MS UINT64_C(1000) #define GUI_DDD_ASYNC_CANCEL_REAP_TIMEOUT_MS UINT64_C(1000) #define GUI_DDD_ASYNC_ABANDONED_CAPACITY 1u +#define GUI_DDD_ASYNC_TELEMETRY_INTERVAL_MS UINT64_C(250) +#define GUI_DDD_ASYNC_TELEMETRY_TIMEOUT_MS 1000u +#define GUI_DDD_ASYNC_TELEMETRY_MAX_FAILURES 2u typedef struct gui_ddd_async_orphan gui_ddd_async_orphan_t; @@ -164,6 +167,30 @@ static inline bool gui_ddd_async_policy_has_pending( return in_flight != 0 || callbacks_active != 0; } +static inline bool gui_ddd_async_policy_telemetry_should_submit( + bool enabled, + bool in_flight, + unsigned failures, + uint64_t now_ms, + uint64_t due_ms) +{ + return enabled && !in_flight && + failures < GUI_DDD_ASYNC_TELEMETRY_MAX_FAILURES && + now_ms >= due_ms; +} + +/* The first successful read only establishes the start of this capture's + * interval. The read itself clears the gateware's interval counters. */ +static inline bool gui_ddd_async_policy_telemetry_should_publish(bool *primed) +{ + if (!primed) return false; + if (!*primed) { + *primed = true; + return false; + } + return true; +} + /* A synchronous libusb control transfer also depends on the context event * pump. Once an async queue has timed out with callbacks still pending, a * synchronous B5/rollback may wait forever and must not be attempted. */ @@ -188,6 +215,11 @@ typedef gui_ddd_async_consume_result_t (*gui_ddd_async_consume_fn)( const uint8_t *data, size_t size); +typedef void (*gui_ddd_async_telemetry_fn)( + void *context, + const uint8_t *data, + size_t size); + /* Optional fault-injection seams. Production leaves all of them NULL. A test may * supply an event pump that returns a permanent error and a deterministic * clock to exercise the bounded orphan transition. */ @@ -219,6 +251,8 @@ typedef struct { uint64_t submission_id; uint64_t completed_transfers; uint64_t consumed_transfers; + uint64_t telemetry_readings; + unsigned telemetry_failures; bool ready_signalled; bool transfers_unreaped; size_t unreaped_transfers; @@ -235,6 +269,11 @@ typedef struct { atomic_bool *startup_failed; gui_ddd_async_consume_fn consume; void *consume_context; + /* Optional DdD FIFO instrument. It is driven by the same event thread as + * the RF queue; no second event handler or synchronous control transfer is + * introduced while capture is active. */ + gui_ddd_async_telemetry_fn telemetry; + void *telemetry_context; gui_ddd_async_event_pump_fn event_pump_override; void *event_pump_context; gui_ddd_async_now_ms_fn now_ms_override; @@ -257,6 +296,8 @@ static inline void gui_ddd_async_detach_external_context( config->startup_failed = NULL; config->consume = NULL; config->consume_context = NULL; + config->telemetry = NULL; + config->telemetry_context = NULL; config->event_pump_override = NULL; config->event_pump_context = NULL; config->now_ms_override = NULL; diff --git a/misrc_tools/misrc_gui/input/gui_ddd_v1.c b/misrc_tools/misrc_gui/input/gui_ddd_v1.c index 23b2b6e..f4b1b89 100644 --- a/misrc_tools/misrc_gui/input/gui_ddd_v1.c +++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.c @@ -77,6 +77,37 @@ static ddd_v1_capture_result_t s_result = DDD_V1_RESULT_SUCCESS; static ddd_v1_lock_phase_t s_lock_phase; static char s_locked_path[DDD_STABLE_ID_MAX]; +typedef struct { + atomic_uint revision; + atomic_bool present; + atomic_uint format; + atomic_bool overflow_seen; + atomic_bool saturated; + atomic_uint latch_count; + atomic_uint used_now; + atomic_uint peak; + atomic_uint peak_since_open; + atomic_uint overflow_events; + atomic_uint dropped_words; + atomic_uint packets_read; + atomic_uint near_full_units; + atomic_uint depth_words; + atomic_uint packet_words; + atomic_uint near_full_words; + atomic_bool totals_latch_seen; + atomic_uint totals_last_latch_count; + atomic_bool interval_coverage_complete; + atomic_bool totals_saturated; + atomic_ullong total_overflow_events; + atomic_ullong total_dropped_words; + atomic_ullong total_near_full_units; + atomic_uint run_peak_words; + atomic_int peak_backpressure_percent; +} ddd_v1_fifo_store_t; + +static ddd_v1_fifo_store_t s_fifo_store; +static ddd_fifo_telemetry_totals_t s_fifo_totals; + _Static_assert(GUI_DDD_ASYNC_TRANSFER_BYTES == (size_t)DDD_SEQUENCE_SAMPLES_PER_MARKER * sizeof(uint16_t), "DDD 3.1 transfer must contain one sequence-marker block"); @@ -111,6 +142,162 @@ static const char *ddd_v1_protocol_result_name(ddd_protocol_result_t result) return "Unknown"; } +static void ddd_v1_fifo_publish( + const ddd_fifo_telemetry_t *latest, + const ddd_fifo_telemetry_totals_t *totals) +{ + if (!latest || !totals) return; + + /* Sequential consistency keeps a validated revision from spanning two + * telemetry generations while remaining non-blocking in the USB loop. */ + atomic_fetch_add_explicit(&s_fifo_store.revision, 1, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.present, latest->present, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.format, latest->format, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.overflow_seen, + latest->overflow_seen, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.saturated, latest->saturated, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.latch_count, latest->latch_count, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.used_now, latest->used_now, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.peak, latest->peak, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.peak_since_open, + latest->peak_since_open, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.overflow_events, + latest->overflow_events, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.dropped_words, + latest->dropped_words, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.packets_read, + latest->packets_read, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.near_full_units, + latest->near_full_units, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.depth_words, latest->depth_words, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.packet_words, latest->packet_words, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.near_full_words, + latest->near_full_words, memory_order_seq_cst); + + atomic_store_explicit(&s_fifo_store.totals_latch_seen, + totals->latch_seen, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.totals_last_latch_count, + totals->last_latch_count, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.interval_coverage_complete, + totals->interval_coverage_complete, + memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.totals_saturated, + totals->saturated, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.total_overflow_events, + totals->overflow_events, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.total_dropped_words, + totals->dropped_words, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.total_near_full_units, + totals->near_full_units, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.run_peak_words, + totals->peak_words, memory_order_seq_cst); + atomic_store_explicit(&s_fifo_store.peak_backpressure_percent, + totals->peak_backpressure_percent, + memory_order_seq_cst); + atomic_fetch_add_explicit(&s_fifo_store.revision, 1, + memory_order_seq_cst); +} + +static void ddd_v1_fifo_reset(void) +{ + ddd_fifo_telemetry_t latest; + + ddd_fifo_telemetry_init(&latest); + ddd_fifo_telemetry_totals_init(&s_fifo_totals); + ddd_v1_fifo_publish(&latest, &s_fifo_totals); +} + +static void ddd_v1_fifo_receive(void *context, + const uint8_t *data, + size_t size) +{ + ddd_fifo_telemetry_t latest; + + (void)context; + + (void)ddd_fifo_telemetry_parse(data, size, &latest); + (void)ddd_fifo_telemetry_totals_add(&s_fifo_totals, &latest); + ddd_v1_fifo_publish(&latest, &s_fifo_totals); +} + +bool gui_ddd_v1_get_fifo_snapshot(gui_ddd_v1_fifo_snapshot_t *snapshot) +{ + unsigned before; + unsigned after; + + if (!snapshot) return false; + do { + before = atomic_load_explicit(&s_fifo_store.revision, + memory_order_seq_cst); + if ((before & 1u) != 0) continue; + + snapshot->latest.present = atomic_load_explicit( + &s_fifo_store.present, memory_order_seq_cst); + snapshot->latest.format = (uint8_t)atomic_load_explicit( + &s_fifo_store.format, memory_order_seq_cst); + snapshot->latest.overflow_seen = atomic_load_explicit( + &s_fifo_store.overflow_seen, memory_order_seq_cst); + snapshot->latest.saturated = atomic_load_explicit( + &s_fifo_store.saturated, memory_order_seq_cst); + snapshot->latest.latch_count = (uint8_t)atomic_load_explicit( + &s_fifo_store.latch_count, memory_order_seq_cst); + snapshot->latest.used_now = (uint16_t)atomic_load_explicit( + &s_fifo_store.used_now, memory_order_seq_cst); + snapshot->latest.peak = (uint16_t)atomic_load_explicit( + &s_fifo_store.peak, memory_order_seq_cst); + snapshot->latest.peak_since_open = (uint16_t)atomic_load_explicit( + &s_fifo_store.peak_since_open, memory_order_seq_cst); + snapshot->latest.overflow_events = (uint16_t)atomic_load_explicit( + &s_fifo_store.overflow_events, memory_order_seq_cst); + snapshot->latest.dropped_words = (uint16_t)atomic_load_explicit( + &s_fifo_store.dropped_words, memory_order_seq_cst); + snapshot->latest.packets_read = (uint16_t)atomic_load_explicit( + &s_fifo_store.packets_read, memory_order_seq_cst); + snapshot->latest.near_full_units = (uint16_t)atomic_load_explicit( + &s_fifo_store.near_full_units, memory_order_seq_cst); + snapshot->latest.depth_words = (uint16_t)atomic_load_explicit( + &s_fifo_store.depth_words, memory_order_seq_cst); + snapshot->latest.packet_words = (uint16_t)atomic_load_explicit( + &s_fifo_store.packet_words, memory_order_seq_cst); + snapshot->latest.near_full_words = (uint16_t)atomic_load_explicit( + &s_fifo_store.near_full_words, memory_order_seq_cst); + + snapshot->totals.latch_seen = atomic_load_explicit( + &s_fifo_store.totals_latch_seen, memory_order_seq_cst); + snapshot->totals.last_latch_count = (uint8_t)atomic_load_explicit( + &s_fifo_store.totals_last_latch_count, memory_order_seq_cst); + snapshot->totals.interval_coverage_complete = atomic_load_explicit( + &s_fifo_store.interval_coverage_complete, + memory_order_seq_cst); + snapshot->totals.saturated = atomic_load_explicit( + &s_fifo_store.totals_saturated, memory_order_seq_cst); + snapshot->totals.overflow_events = atomic_load_explicit( + &s_fifo_store.total_overflow_events, memory_order_seq_cst); + snapshot->totals.dropped_words = atomic_load_explicit( + &s_fifo_store.total_dropped_words, memory_order_seq_cst); + snapshot->totals.near_full_units = atomic_load_explicit( + &s_fifo_store.total_near_full_units, memory_order_seq_cst); + snapshot->totals.peak_words = (uint16_t)atomic_load_explicit( + &s_fifo_store.run_peak_words, memory_order_seq_cst); + snapshot->totals.peak_backpressure_percent = atomic_load_explicit( + &s_fifo_store.peak_backpressure_percent, + memory_order_seq_cst); + + after = atomic_load_explicit(&s_fifo_store.revision, + memory_order_seq_cst); + } while (before != after || (after & 1u) != 0); + return snapshot->latest.present; +} + static bool ddd_v1_format_usb_path(libusb_device *device, char *path, size_t path_size) @@ -265,6 +452,7 @@ static void ddd_v1_close(void) s_interface_claimed = false; s_usb_path[0] = '\0'; ddd_collection_state_init(&s_collection); + ddd_v1_fifo_reset(); } int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) @@ -478,7 +666,9 @@ static int ddd_v1_capture_thread(void *context) .transfer_ready = &s_queue_ready, .startup_failed = &s_startup_failed, .consume = ddd_v1_consume, - .consume_context = &consumer + .consume_context = &consumer, + .telemetry = ddd_v1_fifo_receive, + .telemetry_context = NULL }; gui_ddd_async_result_t async_result = {0}; int result; @@ -524,6 +714,7 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) thrd_t thread; if (!app) return -1; + ddd_v1_fifo_reset(); if (!s_handle) { gui_app_set_status(app, "DdD device is not open"); return -1; diff --git a/misrc_tools/misrc_gui/input/gui_ddd_v1.h b/misrc_tools/misrc_gui/input/gui_ddd_v1.h index a8fa2a5..69e05cd 100644 --- a/misrc_tools/misrc_gui/input/gui_ddd_v1.h +++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.h @@ -9,13 +9,20 @@ #include #include "../../common/device_enum.h" +#include "../../common/ddd_protocol.h" typedef struct gui_app gui_app_t; +typedef struct gui_ddd_v1_fifo_snapshot { + ddd_fifo_telemetry_t latest; + ddd_fifo_telemetry_totals_t totals; +} gui_ddd_v1_fifo_snapshot_t; + int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path); int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode); void gui_ddd_v1_stop(gui_app_t *app); bool gui_ddd_v1_is_active(void); +bool gui_ddd_v1_get_fifo_snapshot(gui_ddd_v1_fifo_snapshot_t *snapshot); /* A failed/unverified B5 cleanup locks only the exact physical DDD 3.1 path. * Two complete enumerations must observe disappearance followed by reappearance diff --git a/misrc_tools/misrc_gui/ui/gui_ddd_fifo_status.c b/misrc_tools/misrc_gui/ui/gui_ddd_fifo_status.c new file mode 100644 index 0000000..8be8f53 --- /dev/null +++ b/misrc_tools/misrc_gui/ui/gui_ddd_fifo_status.c @@ -0,0 +1,85 @@ +/* MISRC GUI - DdD FIFO adapter for the generic hardware-buffer view. */ + +#include "gui_ddd_fifo_status.h" + +#include +#include +#include + +bool gui_ddd_fifo_status_visible(ddd_device_profile_t profile, + bool capture_active, + bool telemetry_present) +{ + return profile == DDD_DEVICE_PROTOCOL_V1 && capture_active && + telemetry_present; +} + +void gui_ddd_fifo_make_buffer_view( + const ddd_fifo_telemetry_t *latest, + const ddd_fifo_telemetry_totals_t *totals, + gui_device_buffer_layout_t layout, + gui_device_buffer_view_t *view) +{ + const char *floor_marker; + int backpressure_percent; + + if (!view) return; + memset(view, 0, sizeof(*view)); + if (!latest || !totals || !latest->present) return; + + view->visible = true; + view->meter_percent = ddd_fifo_peak_percent(latest); + backpressure_percent = ddd_fifo_backpressure_percent(latest); + floor_marker = !totals->interval_coverage_complete ? "+" : ""; + + if (totals->overflow_events > 0 || totals->dropped_words > 0) { + view->severity = GUI_DEVICE_BUFFER_ERROR; + if (layout == GUI_DEVICE_BUFFER_LAYOUT_TINY) { + snprintf(view->caption, sizeof(view->caption), + totals->saturated ? "L%" PRIu64 "%s SAT" + : "L%" PRIu64 "%s", + totals->dropped_words, floor_marker); + } else if (layout == GUI_DEVICE_BUFFER_LAYOUT_COMPACT) { + snprintf(view->caption, sizeof(view->caption), + totals->saturated + ? "O%" PRIu64 "%s L%" PRIu64 "%s SAT" + : "O%" PRIu64 "%s L%" PRIu64 "%s", + totals->overflow_events, floor_marker, + totals->dropped_words, floor_marker); + } else { + snprintf(view->caption, sizeof(view->caption), + totals->saturated + ? "%" PRIu64 "%s ovf, %" PRIu64 "%s lost (sat)" + : "%" PRIu64 "%s ovf, %" PRIu64 "%s lost", + totals->overflow_events, floor_marker, + totals->dropped_words, floor_marker); + } + } else if (latest->peak == 0 && latest->packets_read == 0) { + snprintf(view->caption, sizeof(view->caption), "Idle"); + } else if (backpressure_percent > 0) { + view->severity = GUI_DEVICE_BUFFER_WARNING; + if (layout == GUI_DEVICE_BUFFER_LAYOUT_TINY) { + snprintf(view->caption, sizeof(view->caption), "B%d%%", + backpressure_percent); + } else if (layout == GUI_DEVICE_BUFFER_LAYOUT_COMPACT) { + snprintf(view->caption, sizeof(view->caption), "BP %d%%", + backpressure_percent); + } else { + snprintf(view->caption, sizeof(view->caption), "%d%% pressure", + backpressure_percent); + } + } else if (layout == GUI_DEVICE_BUFFER_LAYOUT_TINY) { + snprintf(view->caption, sizeof(view->caption), "P%d%%", + view->meter_percent); + } else if (layout == GUI_DEVICE_BUFFER_LAYOUT_COMPACT) { + snprintf(view->caption, sizeof(view->caption), "%u/%u peak", + (unsigned)latest->peak, + (unsigned)latest->depth_words); + } else { + snprintf(view->caption, sizeof(view->caption), + "now %u, peak %u/%u", + (unsigned)latest->used_now, + (unsigned)latest->peak, + (unsigned)latest->depth_words); + } +} diff --git a/misrc_tools/misrc_gui/ui/gui_ddd_fifo_status.h b/misrc_tools/misrc_gui/ui/gui_ddd_fifo_status.h new file mode 100644 index 0000000..09fb933 --- /dev/null +++ b/misrc_tools/misrc_gui/ui/gui_ddd_fifo_status.h @@ -0,0 +1,20 @@ +/* MISRC GUI - DdD FIFO adapter for the generic hardware-buffer view. */ + +#ifndef GUI_DDD_FIFO_STATUS_H +#define GUI_DDD_FIFO_STATUS_H + +#include + +#include "../../common/ddd_protocol.h" +#include "gui_device_buffer_status.h" + +bool gui_ddd_fifo_status_visible(ddd_device_profile_t profile, + bool capture_active, + bool telemetry_present); +void gui_ddd_fifo_make_buffer_view( + const ddd_fifo_telemetry_t *latest, + const ddd_fifo_telemetry_totals_t *totals, + gui_device_buffer_layout_t layout, + gui_device_buffer_view_t *view); + +#endif /* GUI_DDD_FIFO_STATUS_H */ diff --git a/misrc_tools/misrc_gui/ui/gui_device_buffer_status.h b/misrc_tools/misrc_gui/ui/gui_device_buffer_status.h new file mode 100644 index 0000000..a9c165c --- /dev/null +++ b/misrc_tools/misrc_gui/ui/gui_device_buffer_status.h @@ -0,0 +1,30 @@ +/* MISRC GUI - device-neutral hardware-buffer status view. */ + +#ifndef GUI_DEVICE_BUFFER_STATUS_H +#define GUI_DEVICE_BUFFER_STATUS_H + +#include + +typedef enum gui_device_buffer_severity { + GUI_DEVICE_BUFFER_NORMAL = 0, + GUI_DEVICE_BUFFER_WARNING, + GUI_DEVICE_BUFFER_ERROR +} gui_device_buffer_severity_t; + +typedef enum gui_device_buffer_layout { + GUI_DEVICE_BUFFER_LAYOUT_FULL = 0, + GUI_DEVICE_BUFFER_LAYOUT_COMPACT, + GUI_DEVICE_BUFFER_LAYOUT_TINY +} gui_device_buffer_layout_t; + +/* Backends interpret their own telemetry and provide only presentation + * semantics. This deliberately does not assume FIFO geometry, packet + * thresholds, counter units, or any particular transport. */ +typedef struct gui_device_buffer_view { + bool visible; + int meter_percent; + gui_device_buffer_severity_t severity; + char caption[64]; +} gui_device_buffer_view_t; + +#endif /* GUI_DEVICE_BUFFER_STATUS_H */ diff --git a/misrc_tools/misrc_gui/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c index 579ab3d..0a3b38d 100644 --- a/misrc_tools/misrc_gui/ui/gui_ui.c +++ b/misrc_tools/misrc_gui/ui/gui_ui.c @@ -14,7 +14,10 @@ #include "../input/gui_cxadc.h" #ifdef ENABLE_DDD #include "../input/gui_ddd_clockgen.h" +#include "../input/gui_ddd_v1.h" +#include "gui_ddd_fifo_status.h" #endif +#include "gui_device_buffer_status.h" #include "../output/gui_audio.h" #include "../output/gui_record.h" #include "../input/gui_capture.h" // Support hsdoah-rp2350 Error & stats @@ -302,6 +305,39 @@ static bool gui_ui_selected_device_is_ddd_clockgen(const gui_app_t *app) } #endif +/* Translate the active backend's telemetry into one device-neutral status-bar + * view. Unsupported devices return false and do not consume any UI space. */ +static bool gui_ui_get_device_buffer_view( + gui_app_t *app, + gui_device_buffer_layout_t layout, + gui_device_buffer_view_t *view) +{ + if (!app || !view) return false; + memset(view, 0, sizeof(*view)); + (void)layout; + +#ifdef ENABLE_DDD + if (gui_ui_selected_device_is_ddd_v1(app)) { + gui_ddd_v1_fifo_snapshot_t snapshot; + bool capture_active = atomic_load(&app->ddd_running); + bool telemetry_present = capture_active && + gui_ddd_v1_get_fifo_snapshot(&snapshot); + ddd_device_profile_t profile = + app->devices[app->selected_device].ddd_profile; + + if (!gui_ddd_fifo_status_visible(profile, capture_active, + telemetry_present)) { + return false; + } + gui_ddd_fifo_make_buffer_view(&snapshot.latest, &snapshot.totals, + layout, view); + return view->visible; + } +#endif + + return false; +} + #ifdef ENABLE_FX3 // FX3 is a distinct USB backend; showing its name as the mode label avoids // confusion with the hsdaoh-specific MISRC/HSDAOH A/B-swap toggle. @@ -1158,6 +1194,7 @@ static char status_aud_buf_display[16]; static char status_free_space_display[120]; static char status_message_display[192]; static char status_record_timer_display[16]; +static gui_device_buffer_view_t status_device_buffer_view; static char record_limit_state_display[96]; static char record_limit_timecode_display[20]; static bool s_status_free_space_valid = false; @@ -5424,6 +5461,15 @@ static void render_status_bar(gui_app_t *app) { bool show_frame_count = !status_narrow; bool show_missed_count = !status_narrow; bool show_error_count = !status_narrow; + bool device_buffer_tiny = status_tiny || status_minimal || + status_width < GUI_UI_STATUS_RECORDING_NARROW_BREAKPOINT; + gui_device_buffer_layout_t device_buffer_layout = device_buffer_tiny + ? GUI_DEVICE_BUFFER_LAYOUT_TINY + : (status_compact + ? GUI_DEVICE_BUFFER_LAYOUT_COMPACT + : GUI_DEVICE_BUFFER_LAYOUT_FULL); + bool show_device_buffer_status = gui_ui_get_device_buffer_view( + app, device_buffer_layout, &status_device_buffer_view); // Detect an error/denied/failed or critical capture-stop status so the bar can // yield space to it: when an error is being shown, hide the free-space // readout (and widen the message budget) so the actual error text isn't @@ -5769,6 +5815,66 @@ static void render_status_bar(gui_app_t *app) { } } + /* Device-side hardware-buffer health is separate from the host RF + * and audio rings below. Backends that cannot report it return no + * view, so this slot consumes no space for those devices. */ + if (show_device_buffer_status) { + const char *device_buffer_label = + device_buffer_layout == GUI_DEVICE_BUFFER_LAYOUT_FULL + ? "HW Buffer:" + : "HW:"; + int device_buffer_bar_width = device_buffer_tiny + ? 20 + : (status_compact ? 36 : 48); + int device_buffer_percent = + status_device_buffer_view.meter_percent; + if (device_buffer_percent < 0) device_buffer_percent = 0; + if (device_buffer_percent > 100) device_buffer_percent = 100; + int device_buffer_fill_width = + device_buffer_bar_width * device_buffer_percent / 100; + Color device_buffer_color = COLOR_SYNC_GREEN; + if (status_device_buffer_view.severity == + GUI_DEVICE_BUFFER_ERROR) { + device_buffer_color = COLOR_CLIP_RED; + } else if (status_device_buffer_view.severity == + GUI_DEVICE_BUFFER_WARNING) { + device_buffer_color = COLOR_METER_YELLOW; + } + + CLAY(CLAY_ID("DeviceBufferStatus"), { + .layout = { + .sizing = { CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0) }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + .childGap = device_buffer_tiny ? 2 : 4 + } + }) { + CLAY_TEXT(make_string(device_buffer_label), + CLAY_TEXT_CONFIG({ .fontSize = status_font_size, .textColor = to_clay_color(COLOR_TEXT_DIM), .wrapMode = CLAY_TEXT_WRAP_NONE })); + CLAY(CLAY_ID("DeviceBufferMeter"), { + .layout = { + .sizing = { CLAY_SIZING_FIXED(device_buffer_bar_width), CLAY_SIZING_FIXED(10) }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + .childGap = 0 + }, + .backgroundColor = to_clay_color(COLOR_METER_BG), + .cornerRadius = CLAY_CORNER_RADIUS(2) + }) { + if (device_buffer_fill_width > 0) { + CLAY(CLAY_ID("DeviceBufferMeterFill"), { + .layout = { + .sizing = { CLAY_SIZING_FIXED(device_buffer_fill_width), CLAY_SIZING_GROW(0) } + }, + .backgroundColor = to_clay_color(device_buffer_color), + .cornerRadius = CLAY_CORNER_RADIUS(2) + }) {} + } + } + CLAY_TEXT(make_string(status_device_buffer_view.caption), + CLAY_TEXT_CONFIG({ .fontSize = status_font_size, .fontId = 1, .textColor = to_clay_color(device_buffer_color), .wrapMode = CLAY_TEXT_WRAP_NONE })); + } + } + // RF Buffer usage size_t rf_head = atomic_load(&app->buffers.buffers[BUF_CAPTURE_RF].head); size_t rf_tail = atomic_load(&app->buffers.buffers[BUF_CAPTURE_RF].tail); diff --git a/misrc_tools/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c index 43a3a9e..527ad40 100644 --- a/misrc_tools/test/ddd_protocol_test.c +++ b/misrc_tools/test/ddd_protocol_test.c @@ -330,6 +330,136 @@ static bool test_lifecycle(void) return true; } +static void put_word(uint8_t *block, size_t offset, uint16_t value) +{ + block[offset] = (uint8_t)(value & 0xffu); + block[offset + 1u] = (uint8_t)(value >> 8); +} + +static void make_fifo_block(uint8_t *block, + uint16_t peak, + uint16_t overflows, + uint16_t dropped) +{ + memset(block, 0, DDD_FIFO_TELEMETRY_LENGTH); + block[0] = DDD_FIFO_TELEMETRY_ID; + block[DDD_FIFO_OFFSET_STATUS] = DDD_FIFO_TELEMETRY_FORMAT; + block[DDD_FIFO_OFFSET_LATCH_COUNT] = 7; + put_word(block, DDD_FIFO_OFFSET_USED_NOW, 4000); + put_word(block, DDD_FIFO_OFFSET_PEAK, peak); + put_word(block, DDD_FIFO_OFFSET_PEAK_LIFETIME, 12288); + put_word(block, DDD_FIFO_OFFSET_OVERFLOWS, overflows); + put_word(block, DDD_FIFO_OFFSET_DROPPED, dropped); + put_word(block, DDD_FIFO_OFFSET_PACKETS, 1221); + put_word(block, DDD_FIFO_OFFSET_NEAR_FULL, 12); + put_word(block, DDD_FIFO_OFFSET_DEPTH, 16384); + put_word(block, DDD_FIFO_OFFSET_PACKET_WORDS, 8192); + put_word(block, DDD_FIFO_OFFSET_NEAR_FULL_WORDS, 12288); +} + +static bool test_fifo_telemetry(void) +{ + uint8_t block[DDD_FIFO_TELEMETRY_LENGTH]; + ddd_fifo_telemetry_t telemetry; + + make_fifo_block(block, 12288, 0, 0); + block[DDD_FIFO_OFFSET_STATUS] |= + DDD_FIFO_FLAG_OVERFLOW_SEEN | DDD_FIFO_FLAG_SATURATED; + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(telemetry.present); + CHECK(telemetry.format == DDD_FIFO_TELEMETRY_FORMAT); + CHECK(telemetry.overflow_seen); + CHECK(telemetry.saturated); + CHECK(telemetry.latch_count == 7); + CHECK(telemetry.used_now == 4000); + CHECK(telemetry.peak == 12288); + CHECK(telemetry.peak_since_open == 12288); + CHECK(telemetry.packets_read == 1221); + CHECK(telemetry.near_full_units == 12); + CHECK(telemetry.depth_words == 16384); + CHECK(telemetry.packet_words == 8192); + CHECK(telemetry.near_full_words == 12288); + CHECK(ddd_fifo_backpressure_percent(&telemetry) == 50); + CHECK(ddd_fifo_peak_percent(&telemetry) == 75); + CHECK(ddd_fifo_used_percent(&telemetry) == 24); + + make_fifo_block(block, 8192, 0, 0); + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(ddd_fifo_backpressure_percent(&telemetry) == 0); + + make_fifo_block(block, 1000, 1, 4200); + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(ddd_fifo_backpressure_percent(&telemetry) == 100); + CHECK(telemetry.dropped_words == 4200); + + memset(block, 0, sizeof(block)); + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(!telemetry.present); + memset(block, 0xff, sizeof(block)); + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + make_fifo_block(block, 9000, 0, 0); + CHECK(!ddd_fifo_telemetry_parse( + block, DDD_FIFO_TELEMETRY_LENGTH - 1u, &telemetry)); + block[DDD_FIFO_OFFSET_STATUS] = DDD_FIFO_TELEMETRY_FORMAT + 1u; + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + make_fifo_block(block, 9000, 0, 0); + put_word(block, DDD_FIFO_OFFSET_PACKET_WORDS, 16385); + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + make_fifo_block(block, 9000, 0, 0); + put_word(block, DDD_FIFO_OFFSET_PACKET_WORDS, 0); + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + make_fifo_block(block, 9000, 0, 0); + put_word(block, DDD_FIFO_OFFSET_PACKET_WORDS, 16384); + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + + make_fifo_block(block, UINT16_MAX, 0, 0); + put_word(block, DDD_FIFO_OFFSET_USED_NOW, UINT16_MAX); + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(ddd_fifo_peak_percent(&telemetry) == 100); + CHECK(ddd_fifo_used_percent(&telemetry) == 100); + CHECK(!ddd_fifo_telemetry_parse(block, sizeof(block), NULL)); + CHECK(!ddd_fifo_telemetry_parse(NULL, sizeof(block), &telemetry)); + return true; +} + +static bool test_fifo_telemetry_totals(void) +{ + uint8_t block[DDD_FIFO_TELEMETRY_LENGTH]; + ddd_fifo_telemetry_t telemetry; + ddd_fifo_telemetry_totals_t totals; + + ddd_fifo_telemetry_totals_init(&totals); + CHECK(totals.interval_coverage_complete); + make_fifo_block(block, 9000, 1, 100); + block[DDD_FIFO_OFFSET_LATCH_COUNT] = 255; + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(ddd_fifo_telemetry_totals_add(&totals, &telemetry)); + CHECK(totals.overflow_events == 1); + CHECK(totals.dropped_words == 100); + CHECK(totals.near_full_units == 12); + CHECK(totals.peak_words == 9000); + CHECK(totals.peak_backpressure_percent == 100); + + CHECK(!ddd_fifo_telemetry_totals_add(&totals, &telemetry)); + CHECK(totals.overflow_events == 1); + + make_fifo_block(block, 12288, 0, 0); + block[DDD_FIFO_OFFSET_LATCH_COUNT] = 0; + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(ddd_fifo_telemetry_totals_add(&totals, &telemetry)); + CHECK(totals.interval_coverage_complete); + CHECK(totals.peak_words == 12288); + + block[DDD_FIFO_OFFSET_LATCH_COUNT] = 2; + CHECK(ddd_fifo_telemetry_parse(block, sizeof(block), &telemetry)); + CHECK(ddd_fifo_telemetry_totals_add(&totals, &telemetry)); + CHECK(!totals.interval_coverage_complete); + CHECK(!ddd_fifo_telemetry_totals_add(NULL, &telemetry)); + CHECK(!ddd_fifo_telemetry_totals_add(&totals, NULL)); + ddd_fifo_telemetry_totals_init(NULL); + return true; +} + static bool test_validators(void) { ddd_sequence_validator_t sequence; @@ -386,6 +516,8 @@ int main(void) !test_reconnect_path_selection() || !test_topology_and_endpoint() || !test_lifecycle() || + !test_fifo_telemetry() || + !test_fifo_telemetry_totals() || !test_validators()) { return 1; } diff --git a/misrc_tools/test/gui_ddd_async_fault_test.c b/misrc_tools/test/gui_ddd_async_fault_test.c index 60f3078..082b7cc 100644 --- a/misrc_tools/test/gui_ddd_async_fault_test.c +++ b/misrc_tools/test/gui_ddd_async_fault_test.c @@ -1,6 +1,8 @@ #include "../misrc_gui/input/gui_ddd_async.h" +#include "../common/ddd_protocol.h" #include +#include #include #include #include @@ -12,6 +14,17 @@ typedef struct { size_t submit_calls; size_t cancel_calls; size_t event_pump_calls; + struct libusb_transfer *bulk_transfers[GUI_DDD_ASYNC_TRANSFER_COUNT]; + size_t bulk_transfer_count; + struct libusb_transfer *telemetry_transfer; + atomic_bool *capture_running; + size_t telemetry_failure_callbacks; + size_t telemetry_success_callbacks; + size_t telemetry_disable_notices; + size_t telemetry_publish_calls; + size_t telemetry_last_size; + uint8_t telemetry_last_first_byte; + size_t consume_calls; } async_fault_state_t; static uint64_t advancing_now_ms(void *context) @@ -36,6 +49,11 @@ static int accept_fake_submit( async_fault_state_t *state = (async_fault_state_t *)context; assert(transfer != NULL); state->submit_calls++; + if (transfer->type == LIBUSB_TRANSFER_TYPE_CONTROL) { + state->telemetry_transfer = transfer; + } else if (state->bulk_transfer_count < GUI_DDD_ASYNC_TRANSFER_COUNT) { + state->bulk_transfers[state->bulk_transfer_count++] = transfer; + } return 0; } @@ -61,6 +79,171 @@ static gui_ddd_async_consume_result_t reject_unexpected_consume( return GUI_DDD_ASYNC_CONSUME_FAILED; } +static void reject_unexpected_telemetry( + void *context, + const uint8_t *data, + size_t size) +{ + (void)context; + (void)data; + (void)size; + assert(!"permanent event failure must not publish telemetry"); +} + +static gui_ddd_async_consume_result_t accept_expected_consume( + void *context, + const uint8_t *data, + size_t size) +{ + async_fault_state_t *state = (async_fault_state_t *)context; + assert(data != NULL); + assert(size == GUI_DDD_ASYNC_TRANSFER_BYTES); + state->consume_calls++; + return GUI_DDD_ASYNC_CONSUME_CONTINUE; +} + +static void record_telemetry_notice( + void *context, + const uint8_t *data, + size_t size) +{ + async_fault_state_t *state = (async_fault_state_t *)context; + if (!data && size == 0) { + state->telemetry_disable_notices++; + } else { + assert(data != NULL); + state->telemetry_publish_calls++; + state->telemetry_last_size = size; + state->telemetry_last_first_byte = data[0]; + } +} + +static void complete_all_bulk_transfers(async_fault_state_t *state) +{ + assert(state != NULL); + assert(state->bulk_transfer_count == GUI_DDD_ASYNC_TRANSFER_COUNT); + for (size_t i = 0; i < state->bulk_transfer_count; ++i) { + struct libusb_transfer *bulk = state->bulk_transfers[i]; + assert(bulk != NULL); + assert(bulk->callback != NULL); + bulk->status = LIBUSB_TRANSFER_COMPLETED; + bulk->actual_length = (int)GUI_DDD_ASYNC_TRANSFER_BYTES; + bulk->callback(bulk); + } +} + +static void write_valid_telemetry_block(uint8_t *data) +{ + assert(data != NULL); + memset(data, 0, DDD_FIFO_TELEMETRY_LENGTH); + data[0] = DDD_FIFO_TELEMETRY_ID; + data[DDD_FIFO_OFFSET_STATUS] = DDD_FIFO_TELEMETRY_FORMAT; + data[DDD_FIFO_OFFSET_DEPTH] = 0x00; + data[DDD_FIFO_OFFSET_DEPTH + 1u] = 0x40; + data[DDD_FIFO_OFFSET_PACKET_WORDS] = 0x00; + data[DDD_FIFO_OFFSET_PACKET_WORDS + 1u] = 0x20; +} + +static int drive_two_telemetry_successes(void *context, long timeout_us) +{ + async_fault_state_t *state = (async_fault_state_t *)context; + struct libusb_transfer *telemetry = state->telemetry_transfer; + struct libusb_control_setup *setup; + uint8_t *data; + + (void)timeout_us; + state->event_pump_calls++; + assert(telemetry != NULL); + assert(telemetry->callback != NULL); + assert(state->telemetry_success_callbacks < 2); + + setup = libusb_control_transfer_get_setup(telemetry); + assert(setup != NULL); + assert(setup->bmRequestType == DDD_USB_REQUEST_VENDOR_IN); + assert(setup->bRequest == DDD_REQUEST_REGISTER_READ); + assert(libusb_le16_to_cpu(setup->wValue) == + DDD_REGISTER_FIFO_TELEMETRY); + assert(libusb_le16_to_cpu(setup->wIndex) == 0); + assert(libusb_le16_to_cpu(setup->wLength) == + DDD_FIFO_TELEMETRY_LENGTH); + + data = libusb_control_transfer_get_data(telemetry); + assert(data != NULL); + write_valid_telemetry_block(data); + telemetry->status = LIBUSB_TRANSFER_COMPLETED; + telemetry->actual_length = (int)DDD_FIFO_TELEMETRY_LENGTH; + telemetry->callback(telemetry); + state->telemetry_success_callbacks++; + + if (state->telemetry_success_callbacks == 2) { + assert(state->capture_running != NULL); + atomic_store(state->capture_running, false); + complete_all_bulk_transfers(state); + } + return 0; +} + +static int drive_two_invalid_telemetry_blocks(void *context, long timeout_us) +{ + async_fault_state_t *state = (async_fault_state_t *)context; + struct libusb_transfer *telemetry = state->telemetry_transfer; + uint8_t *data; + + (void)timeout_us; + state->event_pump_calls++; + assert(telemetry != NULL); + assert(telemetry->callback != NULL); + + if (state->telemetry_failure_callbacks == 2) { + /* Leave capture active for one extra loop iteration. A correct retry + * policy must not submit a third telemetry read after two invalid + * full-length responses. */ + assert(state->submit_calls == + GUI_DDD_ASYNC_TRANSFER_COUNT + 2u); + assert(state->capture_running != NULL); + atomic_store(state->capture_running, false); + complete_all_bulk_transfers(state); + return 0; + } + + data = libusb_control_transfer_get_data(telemetry); + assert(data != NULL); + memset(data, 0, DDD_FIFO_TELEMETRY_LENGTH); + if (state->telemetry_failure_callbacks == 1) { + write_valid_telemetry_block(data); + data[DDD_FIFO_OFFSET_STATUS] = DDD_FIFO_TELEMETRY_FORMAT + 1u; + } + telemetry->status = LIBUSB_TRANSFER_COMPLETED; + telemetry->actual_length = (int)DDD_FIFO_TELEMETRY_LENGTH; + telemetry->callback(telemetry); + state->telemetry_failure_callbacks++; + return 0; +} + +static int drive_two_telemetry_timeouts(void *context, long timeout_us) +{ + async_fault_state_t *state = (async_fault_state_t *)context; + struct libusb_transfer *telemetry = state->telemetry_transfer; + + (void)timeout_us; + state->event_pump_calls++; + assert(telemetry != NULL); + assert(telemetry->callback != NULL); + assert(state->telemetry_failure_callbacks < 2); + + telemetry->status = LIBUSB_TRANSFER_TIMED_OUT; + telemetry->actual_length = 0; + telemetry->callback(telemetry); + state->telemetry_failure_callbacks++; + + if (state->telemetry_failure_callbacks == 2) { + assert(state->capture_running != NULL); + atomic_store(state->capture_running, false); + complete_all_bulk_transfers(state); + } + return 0; +} + static gui_ddd_async_config_t make_fault_config( async_fault_state_t *state, atomic_bool *capture_running, @@ -82,6 +265,8 @@ static gui_ddd_async_config_t make_fault_config( config.startup_failed = startup_failed; config.consume = reject_unexpected_consume; config.consume_context = state; + config.telemetry = reject_unexpected_telemetry; + config.telemetry_context = state; config.event_pump_override = permanent_event_error; config.event_pump_context = state; config.now_ms_override = advancing_now_ms; @@ -93,6 +278,122 @@ static gui_ddd_async_config_t make_fault_config( return config; } +static void test_telemetry_success_primes_then_publishes(void) +{ + async_fault_state_t state = {0}; + atomic_bool capture_running = true; + atomic_bool transfer_ready = false; + atomic_bool startup_failed = false; + int usb_context_marker = 1; + int device_handle_marker = 2; + gui_ddd_async_config_t config = make_fault_config( + &state, &capture_running, &transfer_ready, &startup_failed, + &usb_context_marker, &device_handle_marker); + gui_ddd_async_result_t result; + + state.capture_running = &capture_running; + config.consume = accept_expected_consume; + config.telemetry = record_telemetry_notice; + config.event_pump_override = drive_two_telemetry_successes; + memset(&result, 0, sizeof(result)); + + assert(gui_ddd_async_run(&config, &result) == 0); + assert(result.code == GUI_DDD_ASYNC_RESULT_SUCCESS); + assert(result.ready_signalled); + assert(!result.transfers_unreaped); + assert(result.completed_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.consumed_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.telemetry_readings == 2); + assert(result.telemetry_failures == 0); + assert(state.submit_calls == GUI_DDD_ASYNC_TRANSFER_COUNT + 2u); + assert(state.cancel_calls == 0); + assert(state.telemetry_success_callbacks == 2); + assert(state.telemetry_disable_notices == 0); + assert(state.telemetry_publish_calls == 1); + assert(state.telemetry_last_size == DDD_FIFO_TELEMETRY_LENGTH); + assert(state.telemetry_last_first_byte == DDD_FIFO_TELEMETRY_ID); + assert(state.consume_calls == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(!atomic_load(&capture_running)); + assert(!atomic_load(&transfer_ready)); + assert(!atomic_load(&startup_failed)); +} + +static void test_telemetry_failures_do_not_fail_rf_capture(void) +{ + async_fault_state_t state = {0}; + atomic_bool capture_running = true; + atomic_bool transfer_ready = false; + atomic_bool startup_failed = false; + int usb_context_marker = 1; + int device_handle_marker = 2; + gui_ddd_async_config_t config = make_fault_config( + &state, &capture_running, &transfer_ready, &startup_failed, + &usb_context_marker, &device_handle_marker); + gui_ddd_async_result_t result; + + state.capture_running = &capture_running; + config.consume = accept_expected_consume; + config.telemetry = record_telemetry_notice; + config.event_pump_override = drive_two_telemetry_timeouts; + memset(&result, 0, sizeof(result)); + + assert(gui_ddd_async_run(&config, &result) == 0); + assert(result.code == GUI_DDD_ASYNC_RESULT_SUCCESS); + assert(result.ready_signalled); + assert(!result.transfers_unreaped); + assert(result.completed_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.consumed_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.telemetry_readings == 0); + assert(result.telemetry_failures == 2); + assert(state.submit_calls == GUI_DDD_ASYNC_TRANSFER_COUNT + 2u); + assert(state.cancel_calls == 0); + assert(state.telemetry_failure_callbacks == 2); + assert(state.telemetry_disable_notices == 1); + assert(state.telemetry_publish_calls == 0); + assert(state.consume_calls == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(!atomic_load(&capture_running)); + assert(!atomic_load(&transfer_ready)); + assert(!atomic_load(&startup_failed)); +} + +static void test_invalid_telemetry_blocks_disable_instrument(void) +{ + async_fault_state_t state = {0}; + atomic_bool capture_running = true; + atomic_bool transfer_ready = false; + atomic_bool startup_failed = false; + int usb_context_marker = 1; + int device_handle_marker = 2; + gui_ddd_async_config_t config = make_fault_config( + &state, &capture_running, &transfer_ready, &startup_failed, + &usb_context_marker, &device_handle_marker); + gui_ddd_async_result_t result; + + state.capture_running = &capture_running; + config.consume = accept_expected_consume; + config.telemetry = record_telemetry_notice; + config.event_pump_override = drive_two_invalid_telemetry_blocks; + memset(&result, 0, sizeof(result)); + + assert(gui_ddd_async_run(&config, &result) == 0); + assert(result.code == GUI_DDD_ASYNC_RESULT_SUCCESS); + assert(result.ready_signalled); + assert(!result.transfers_unreaped); + assert(result.completed_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.consumed_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.telemetry_readings == 0); + assert(result.telemetry_failures == 2); + assert(state.submit_calls == GUI_DDD_ASYNC_TRANSFER_COUNT + 2u); + assert(state.cancel_calls == 0); + assert(state.telemetry_failure_callbacks == 2); + assert(state.telemetry_disable_notices == 1); + assert(state.telemetry_publish_calls == 0); + assert(state.consume_calls == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(!atomic_load(&capture_running)); + assert(!atomic_load(&transfer_ready)); + assert(!atomic_load(&startup_failed)); +} + static void test_result_is_required_for_orphan_ownership(void) { async_fault_state_t state = {0}; @@ -132,11 +433,12 @@ static void test_permanent_event_error_returns_bounded_orphan(void) assert(result.libusb_error == TEST_EVENT_ERROR); assert(result.ready_signalled); assert(result.transfers_unreaped); - assert(result.unreaped_transfers == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(result.unreaped_transfers == + GUI_DDD_ASYNC_TRANSFER_COUNT + 1u); assert(result.active_callbacks == 0); assert(result.orphan != NULL); - assert(state.submit_calls == GUI_DDD_ASYNC_TRANSFER_COUNT); - assert(state.cancel_calls == GUI_DDD_ASYNC_TRANSFER_COUNT); + assert(state.submit_calls == GUI_DDD_ASYNC_TRANSFER_COUNT + 1u); + assert(state.cancel_calls == GUI_DDD_ASYNC_TRANSFER_COUNT + 1u); assert(state.event_pump_calls > 0); assert(state.event_pump_calls <= 4); assert(state.now_ms <= @@ -155,6 +457,9 @@ static void test_permanent_event_error_returns_bounded_orphan(void) int main(void) { test_result_is_required_for_orphan_ownership(); + test_telemetry_success_primes_then_publishes(); + test_telemetry_failures_do_not_fail_rf_capture(); + test_invalid_telemetry_blocks_disable_instrument(); test_permanent_event_error_returns_bounded_orphan(); puts("gui_ddd_async_fault_test: OK"); return 0; diff --git a/misrc_tools/test/gui_ddd_async_policy_test.c b/misrc_tools/test/gui_ddd_async_policy_test.c index a6ebecd..a2cc468 100644 --- a/misrc_tools/test/gui_ddd_async_policy_test.c +++ b/misrc_tools/test/gui_ddd_async_policy_test.c @@ -13,6 +13,7 @@ int main(void) { gui_ddd_async_order_policy_t policy; + bool telemetry_primed = false; gui_ddd_async_policy_slot_t slots[2] = { {.submission_id = 0, .state = GUI_DDD_ASYNC_SLOT_COMPLETE}, {.submission_id = 1, .state = GUI_DDD_ASYNC_SLOT_SUBMITTED} @@ -39,6 +40,21 @@ int main(void) GUI_DDD_ASYNC_TRANSFER_BYTES)); CHECK(!gui_ddd_async_policy_exact_length( GUI_DDD_ASYNC_TRANSFER_BYTES - 1)); + CHECK(gui_ddd_async_policy_telemetry_should_submit( + true, false, 0, 1000, 1000)); + CHECK(!gui_ddd_async_policy_telemetry_should_submit( + true, true, 0, 1000, 1000)); + CHECK(!gui_ddd_async_policy_telemetry_should_submit( + true, false, 0, 999, 1000)); + CHECK(!gui_ddd_async_policy_telemetry_should_submit( + true, false, GUI_DDD_ASYNC_TELEMETRY_MAX_FAILURES, + 1000, 1000)); + CHECK(!gui_ddd_async_policy_telemetry_should_publish( + &telemetry_primed)); + CHECK(telemetry_primed); + CHECK(gui_ddd_async_policy_telemetry_should_publish( + &telemetry_primed)); + CHECK(!gui_ddd_async_policy_telemetry_should_publish(NULL)); CHECK(gui_ddd_async_policy_abandon_slot_available(0)); CHECK(!gui_ddd_async_policy_abandon_slot_available( GUI_DDD_ASYNC_ABANDONED_CAPACITY)); diff --git a/misrc_tools/test/gui_ddd_fifo_status_test.c b/misrc_tools/test/gui_ddd_fifo_status_test.c new file mode 100644 index 0000000..f3d393b --- /dev/null +++ b/misrc_tools/test/gui_ddd_fifo_status_test.c @@ -0,0 +1,91 @@ +#include "../misrc_gui/ui/gui_ddd_fifo_status.h" + +#include +#include + +#define CHECK(condition) do { \ + if (!(condition)) { \ + fprintf(stderr, "CHECK failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #condition); \ + return 1; \ + } \ +} while (0) + +int main(void) +{ + ddd_fifo_telemetry_t latest; + ddd_fifo_telemetry_totals_t totals; + gui_device_buffer_view_t view; + + CHECK(gui_ddd_fifo_status_visible( + DDD_DEVICE_PROTOCOL_V1, true, true)); + CHECK(!gui_ddd_fifo_status_visible( + DDD_DEVICE_LEGACY, true, true)); + CHECK(!gui_ddd_fifo_status_visible( + DDD_DEVICE_PROTOCOL_V1, false, true)); + CHECK(!gui_ddd_fifo_status_visible( + DDD_DEVICE_PROTOCOL_V1, true, false)); + + ddd_fifo_telemetry_init(&latest); + ddd_fifo_telemetry_totals_init(&totals); + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_FULL, &view); + CHECK(!view.visible); + + latest.present = true; + latest.used_now = 4000; + latest.peak = 8192; + latest.depth_words = 16384; + latest.packet_words = 8192; + latest.packets_read = 100; + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_FULL, &view); + CHECK(view.visible); + CHECK(view.meter_percent == 50); + CHECK(view.severity == GUI_DEVICE_BUFFER_NORMAL); + CHECK(strcmp(view.caption, "now 4000, peak 8192/16384") == 0); + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_TINY, &view); + CHECK(strcmp(view.caption, "P50%") == 0); + + latest.peak = 12288; + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, &view); + CHECK(view.meter_percent == 75); + CHECK(view.severity == GUI_DEVICE_BUFFER_WARNING); + CHECK(strcmp(view.caption, "BP 50%") == 0); + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_TINY, &view); + CHECK(strcmp(view.caption, "B50%") == 0); + + totals.overflow_events = 2; + totals.dropped_words = 128; + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, &view); + CHECK(view.severity == GUI_DEVICE_BUFFER_ERROR); + CHECK(strcmp(view.caption, "O2 L128") == 0); + + totals.saturated = true; + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, &view); + CHECK(strcmp(view.caption, "O2 L128 SAT") == 0); + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_TINY, &view); + CHECK(strcmp(view.caption, "L128 SAT") == 0); + totals.saturated = false; + totals.interval_coverage_complete = false; + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, &view); + CHECK(strcmp(view.caption, "O2+ L128+") == 0); + gui_ddd_fifo_make_buffer_view( + NULL, &totals, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, &view); + CHECK(!view.visible); + gui_ddd_fifo_make_buffer_view( + &latest, NULL, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, &view); + CHECK(!view.visible); + gui_ddd_fifo_make_buffer_view( + &latest, &totals, GUI_DEVICE_BUFFER_LAYOUT_COMPACT, NULL); + + puts("DDD FIFO status presentation tests passed"); + return 0; +}