diff --git a/README.md b/README.md
index b2ef4e6..d1eff21 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,14 @@ Building from source? See [INSTALLATION.md](INSTALLATION.md).
## Setup With Devices
+
+DdD Setup
+
+
+MISRC GUI supports both legacy DdD firmware and the protocol-v1 firmware family introduced in 3.1. They appear as separate device profiles in the device list.
+
+
+
Install Windows
diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c
new file mode 100644
index 0000000..21930d1
--- /dev/null
+++ b/misrc_tools/common/ddd_protocol.c
@@ -0,0 +1,628 @@
+/* MISRC Common - Domesday Duplicator firmware protocol helpers. */
+
+#include "ddd_protocol.h"
+
+#include
+#include
+
+static bool ddd_control_ops_valid(const ddd_control_ops_t *ops)
+{
+ return ops != NULL && ops->transfer != NULL;
+}
+
+static bool ddd_control_out(const ddd_control_ops_t *ops,
+ uint8_t request,
+ uint16_t value)
+{
+ return ops->transfer(ops->context,
+ DDD_USB_REQUEST_VENDOR_OUT,
+ request,
+ value,
+ 0,
+ NULL,
+ 0) == 0;
+}
+
+static bool ddd_control_in(const ddd_control_ops_t *ops,
+ uint8_t request,
+ uint16_t value,
+ uint8_t *data,
+ uint16_t length)
+{
+ return ops->transfer(ops->context,
+ DDD_USB_REQUEST_VENDOR_IN,
+ request,
+ value,
+ 0,
+ data,
+ length) == (int)length;
+}
+
+static bool ddd_write_register(const ddd_control_ops_t *ops,
+ uint8_t address,
+ uint8_t value)
+{
+ return ddd_control_out(ops, DDD_REQUEST_REGISTER_WRITE,
+ ddd_make_register_write(address, value));
+}
+
+static bool ddd_read_register(const ddd_control_ops_t *ops,
+ uint8_t address,
+ uint8_t *value)
+{
+ return ddd_control_in(ops, DDD_REQUEST_REGISTER_READ, address, value, 1);
+}
+
+bool ddd_is_known_device_id(uint16_t vendor_id, uint16_t product_id)
+{
+ return (vendor_id == DDD_LEGACY_VENDOR_ID &&
+ product_id == DDD_LEGACY_PRODUCT_ID) ||
+ (vendor_id == DDD_CURRENT_VENDOR_ID &&
+ product_id == DDD_CURRENT_PRODUCT_ID);
+}
+
+ddd_device_profile_t ddd_classify_device(uint16_t vendor_id,
+ uint16_t product_id,
+ uint16_t bcd_device)
+{
+ if (vendor_id == DDD_LEGACY_VENDOR_ID &&
+ product_id == DDD_LEGACY_PRODUCT_ID) {
+ return DDD_DEVICE_LEGACY;
+ }
+ if (vendor_id == DDD_CURRENT_VENDOR_ID &&
+ product_id == DDD_CURRENT_PRODUCT_ID) {
+ return (uint8_t)(bcd_device >> 8) == DDD_SUPPORTED_PROTOCOL_VERSION
+ ? DDD_DEVICE_PROTOCOL_V1
+ : DDD_DEVICE_UNSUPPORTED;
+ }
+ return DDD_DEVICE_NOT_DDD;
+}
+
+bool ddd_profile_can_capture(ddd_device_profile_t profile)
+{
+ return profile == DDD_DEVICE_LEGACY ||
+ profile == DDD_DEVICE_PROTOCOL_V1;
+}
+
+bool ddd_profile_requires_usb_path(ddd_device_profile_t profile)
+{
+ return profile == DDD_DEVICE_PROTOCOL_V1;
+}
+
+bool ddd_clockgen_candidate_is_preferred(
+ bool selection_present,
+ ddd_device_profile_t selected_profile,
+ ddd_device_profile_t candidate_profile,
+ bool candidate_capture_supported)
+{
+ if (!candidate_capture_supported ||
+ !ddd_profile_can_capture(candidate_profile)) {
+ return false;
+ }
+ if (!selection_present) return true;
+ return selected_profile == DDD_DEVICE_PROTOCOL_V1 &&
+ candidate_profile == DDD_DEVICE_LEGACY;
+}
+
+bool ddd_reconnect_path_matches(
+ ddd_device_profile_t target_profile,
+ const char *target_usb_path,
+ ddd_device_profile_t candidate_profile,
+ const char *candidate_usb_path)
+{
+ return ddd_profile_requires_usb_path(target_profile) &&
+ target_profile == candidate_profile &&
+ target_usb_path != NULL && target_usb_path[0] != '\0' &&
+ candidate_usb_path != NULL && candidate_usb_path[0] != '\0' &&
+ strcmp(target_usb_path, candidate_usb_path) == 0;
+}
+
+void ddd_profile_index_state_init(ddd_profile_index_state_t *state)
+{
+ if (state) memset(state, 0, sizeof(*state));
+}
+
+int ddd_profile_index_take(ddd_profile_index_state_t *state,
+ ddd_device_profile_t profile)
+{
+ if (!state) return -1;
+ switch (profile) {
+ case DDD_DEVICE_LEGACY: return state->legacy_count++;
+ case DDD_DEVICE_PROTOCOL_V1: return state->protocol_v1_count++;
+ case DDD_DEVICE_UNSUPPORTED: return state->unsupported_count++;
+ case DDD_DEVICE_NOT_DDD: return -1;
+ }
+ return -1;
+}
+
+bool ddd_v1_link_speed_allowed(bool speed_known, bool at_least_superspeed)
+{
+ return !speed_known || at_least_superspeed;
+}
+
+bool ddd_decimation_is_supported(uint8_t factor)
+{
+ return factor == DDD_DECIMATION_FULL_RATE ||
+ factor == DDD_DECIMATION_HALF_RATE;
+}
+
+bool ddd_profile_supports_decimation(ddd_device_profile_t profile,
+ uint8_t factor)
+{
+ if (profile == DDD_DEVICE_LEGACY) {
+ return factor == DDD_DECIMATION_FULL_RATE;
+ }
+ return profile == DDD_DEVICE_PROTOCOL_V1 &&
+ ddd_decimation_is_supported(factor);
+}
+
+uint16_t ddd_make_register_write(uint8_t address, uint8_t value)
+{
+ return (uint16_t)(((uint16_t)address << 8) | value);
+}
+
+uint32_t ddd_sample_rate_hz(uint8_t factor)
+{
+ return ddd_decimation_is_supported(factor)
+ ? DDD_CONVERTER_SAMPLE_RATE_HZ / factor
+ : 0;
+}
+
+uint32_t ddd_sample_rate_khz(uint8_t factor)
+{
+ return ddd_sample_rate_hz(factor) / UINT32_C(1000);
+}
+
+uint32_t ddd_v1_effective_output_rate_khz(
+ bool resample_enabled,
+ uint32_t resample_rate_khz,
+ uint8_t stored_decimation_factor)
+{
+ uint32_t hardware_rate_khz = ddd_sample_rate_khz(
+ stored_decimation_factor);
+ if (hardware_rate_khz == 0) return 0;
+ if (!resample_enabled || resample_rate_khz == 0 ||
+ resample_rate_khz >= hardware_rate_khz) {
+ return hardware_rate_khz;
+ }
+ return resample_rate_khz;
+}
+
+bool ddd_v1_plan_output_rate_khz(uint32_t output_rate_khz,
+ ddd_v1_rate_plan_t *plan)
+{
+ uint8_t factor;
+ uint32_t hardware_rate_khz;
+
+ if (!plan || output_rate_khz == 0 ||
+ output_rate_khz > ddd_sample_rate_khz(
+ DDD_DECIMATION_FULL_RATE)) {
+ return false;
+ }
+
+ factor = output_rate_khz <= ddd_sample_rate_khz(
+ DDD_DECIMATION_HALF_RATE)
+ ? DDD_DECIMATION_HALF_RATE
+ : DDD_DECIMATION_FULL_RATE;
+ hardware_rate_khz = ddd_sample_rate_khz(factor);
+ plan->decimation_factor = factor;
+ plan->hardware_rate_khz = hardware_rate_khz;
+ plan->output_rate_khz = output_rate_khz;
+ plan->software_resample = output_rate_khz < hardware_rate_khz;
+ return true;
+}
+
+bool ddd_identity_is_supported(const uint8_t *identity, size_t length)
+{
+ return identity != NULL && length >= DDD_IDENTITY_LENGTH &&
+ identity[DDD_REGISTER_IDENTITY] == DDD_IDENTITY_VALUE &&
+ identity[DDD_REGISTER_MAP_VERSION] ==
+ DDD_SUPPORTED_REGISTER_MAP &&
+ identity[DDD_REGISTER_IMAGE_ROLE] ==
+ DDD_APPLICATION_IMAGE_ROLE;
+}
+
+bool ddd_format_gateware_commit(const uint8_t *identity,
+ size_t identity_length,
+ char *destination,
+ size_t destination_size)
+{
+ size_t commit_length = 0;
+ bool dirty;
+
+ if (!destination || destination_size == 0) return false;
+ destination[0] = '\0';
+ if (!identity || identity_length < DDD_IDENTITY_LENGTH ||
+ (identity[DDD_REGISTER_BUILD_FLAGS] & DDD_BUILD_COMMIT_FLAG) == 0) {
+ return false;
+ }
+ dirty = (identity[DDD_REGISTER_BUILD_FLAGS] & DDD_BUILD_DIRTY_FLAG) != 0;
+ while (commit_length < DDD_COMMIT_LENGTH) {
+ uint8_t value = identity[DDD_REGISTER_COMMIT + commit_length];
+ bool is_hex = (value >= (uint8_t)'0' && value <= (uint8_t)'9') ||
+ (value >= (uint8_t)'a' && value <= (uint8_t)'f') ||
+ (value >= (uint8_t)'A' && value <= (uint8_t)'F');
+ if (!is_hex) break;
+ ++commit_length;
+ }
+ if (commit_length == 0 ||
+ commit_length + (dirty ? 6u : 0u) + 1u > destination_size) {
+ return false;
+ }
+ memcpy(destination, &identity[DDD_REGISTER_COMMIT], commit_length);
+ if (dirty) {
+ memcpy(destination + commit_length, "-dirty", 6u);
+ commit_length += 6u;
+ }
+ destination[commit_length] = '\0';
+ return true;
+}
+
+bool ddd_format_usb_topology_path(uint8_t bus_number,
+ const uint8_t *ports,
+ int port_count,
+ char *destination,
+ size_t destination_size)
+{
+ int written;
+ size_t used;
+
+ if (!destination || destination_size == 0 || !ports || port_count <= 0) {
+ return false;
+ }
+ written = snprintf(destination, destination_size, "usb:%u-", bus_number);
+ if (written < 0 || (size_t)written >= destination_size) return false;
+ used = (size_t)written;
+ for (int i = 0; i < port_count; ++i) {
+ written = snprintf(destination + used, destination_size - used,
+ i == 0 ? "%u" : ".%u", ports[i]);
+ if (written < 0 || (size_t)written >= destination_size - used) {
+ destination[0] = '\0';
+ return false;
+ }
+ used += (size_t)written;
+ }
+ return true;
+}
+
+void ddd_stream_selector_init(ddd_stream_selector_t *selector,
+ ddd_device_profile_t profile)
+{
+ if (!selector) return;
+ memset(selector, 0, sizeof(*selector));
+ selector->profile = profile;
+ selector->selected.interface_number = DDD_STREAM_INTERFACE_NUMBER;
+ selector->selected.alternate_setting = DDD_STREAM_ALTERNATE_SETTING;
+ selector->selected.endpoint_address = DDD_STREAM_ENDPOINT_ADDRESS;
+}
+
+void ddd_stream_selector_consider(
+ ddd_stream_selector_t *selector,
+ const ddd_stream_endpoint_candidate_t *candidate)
+{
+ if (!selector || !candidate || !candidate->is_bulk || !candidate->is_in) {
+ return;
+ }
+ if (selector->profile == DDD_DEVICE_PROTOCOL_V1) {
+ bool exact = candidate->interface_number == DDD_STREAM_INTERFACE_NUMBER &&
+ candidate->alternate_setting == DDD_STREAM_ALTERNATE_SETTING &&
+ candidate->endpoint_address == DDD_STREAM_ENDPOINT_ADDRESS &&
+ candidate->max_packet_size == DDD_STREAM_MAX_PACKET_SIZE;
+ if (!exact) return;
+ ++selector->protocol_v1_exact_matches;
+ if (selector->protocol_v1_exact_matches == 1) {
+ selector->selected.interface_number = candidate->interface_number;
+ selector->selected.alternate_setting = candidate->alternate_setting;
+ selector->selected.endpoint_address = candidate->endpoint_address;
+ selector->selected.max_packet_size = candidate->max_packet_size;
+ selector->selected.found = true;
+ } else {
+ selector->selected.found = false;
+ }
+ return;
+ }
+ if (selector->profile == DDD_DEVICE_LEGACY &&
+ (!selector->selected.found ||
+ candidate->max_packet_size > selector->selected.max_packet_size)) {
+ selector->selected.interface_number = candidate->interface_number;
+ selector->selected.alternate_setting = candidate->alternate_setting;
+ selector->selected.endpoint_address = candidate->endpoint_address;
+ selector->selected.max_packet_size = candidate->max_packet_size;
+ selector->selected.found = true;
+ }
+}
+
+bool ddd_stream_selector_get(const ddd_stream_selector_t *selector,
+ ddd_stream_path_t *selected)
+{
+ if (!selector || !selected) return false;
+ *selected = selector->selected;
+ if (selector->profile == DDD_DEVICE_PROTOCOL_V1 &&
+ selector->protocol_v1_exact_matches != 1) {
+ selected->found = false;
+ }
+ return selected->found;
+}
+
+void ddd_collection_state_init(ddd_collection_state_t *state)
+{
+ if (!state) return;
+ memset(state, 0, sizeof(*state));
+ state->profile = DDD_DEVICE_NOT_DDD;
+}
+
+static ddd_protocol_result_t ddd_restore_safe_defaults(
+ const ddd_control_ops_t *ops)
+{
+ uint8_t test_mode = UINT8_MAX;
+ uint8_t decimation = UINT8_MAX;
+ bool transfer_failed = false;
+ bool mismatch = false;
+
+ if (!ddd_write_register(ops, DDD_REGISTER_TEST_MODE, 0)) {
+ transfer_failed = true;
+ }
+ if (!ddd_write_register(ops, DDD_REGISTER_DECIMATION,
+ DDD_DECIMATION_FULL_RATE)) {
+ transfer_failed = true;
+ }
+ if (!ddd_read_register(ops, DDD_REGISTER_TEST_MODE, &test_mode)) {
+ transfer_failed = true;
+ } else if (test_mode != 0) {
+ mismatch = true;
+ }
+ if (!ddd_read_register(ops, DDD_REGISTER_DECIMATION, &decimation)) {
+ transfer_failed = true;
+ } else if (decimation != DDD_DECIMATION_FULL_RATE) {
+ mismatch = true;
+ }
+ if (transfer_failed) return DDD_PROTOCOL_CONTROL_FAILURE;
+ return mismatch ? DDD_PROTOCOL_READBACK_MISMATCH : DDD_PROTOCOL_OK;
+}
+
+ddd_protocol_result_t ddd_collection_rollback_v1(
+ const ddd_control_ops_t *ops,
+ ddd_collection_state_t *state)
+{
+ ddd_protocol_result_t result = DDD_PROTOCOL_OK;
+ bool stop_ok = true;
+
+ if (!state || !ddd_control_ops_valid(ops)) {
+ return DDD_PROTOCOL_INVALID_ARGUMENT;
+ }
+ if (state->profile != DDD_DEVICE_PROTOCOL_V1) {
+ return DDD_PROTOCOL_UNSUPPORTED_PROFILE;
+ }
+ state->rollback_attempted = true;
+ if (state->collection_start_attempted || state->collection_active) {
+ stop_ok = ddd_control_out(ops, DDD_REQUEST_COLLECTION, 0);
+ if (!stop_ok) result = DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ ddd_protocol_result_t restore = ddd_restore_safe_defaults(ops);
+ if (result == DDD_PROTOCOL_OK && restore != DDD_PROTOCOL_OK) result = restore;
+ if (stop_ok) {
+ state->collection_start_attempted = false;
+ state->collection_active = false;
+ }
+ state->configured = false;
+ state->rollback_succeeded = result == DDD_PROTOCOL_OK;
+ return result;
+}
+
+ddd_protocol_result_t ddd_collection_start_v1(
+ const ddd_control_ops_t *ops,
+ bool test_mode,
+ uint8_t decimation_factor,
+ ddd_collection_state_t *state)
+{
+ uint8_t identity[DDD_IDENTITY_LENGTH] = {0};
+ uint8_t readback = 0;
+
+ if (!state || !ddd_control_ops_valid(ops)) {
+ return DDD_PROTOCOL_INVALID_ARGUMENT;
+ }
+ ddd_collection_state_init(state);
+ state->profile = DDD_DEVICE_PROTOCOL_V1;
+ state->test_mode = test_mode;
+ state->decimation_factor = decimation_factor;
+ state->sample_rate_hz = ddd_sample_rate_hz(decimation_factor);
+ if (!ddd_decimation_is_supported(decimation_factor)) {
+ return DDD_PROTOCOL_UNSUPPORTED_DECIMATION;
+ }
+ if (!ddd_control_in(ops, DDD_REQUEST_REGISTER_READ,
+ DDD_REGISTER_IDENTITY, identity,
+ DDD_IDENTITY_LENGTH)) {
+ return DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ if (!ddd_identity_is_supported(identity, sizeof(identity))) {
+ return DDD_PROTOCOL_IDENTITY_MISMATCH;
+ }
+ memcpy(state->identity, identity, sizeof(state->identity));
+ if (!ddd_write_register(ops, DDD_REGISTER_TEST_MODE,
+ test_mode ? 1 : 0) ||
+ !ddd_write_register(ops, DDD_REGISTER_DECIMATION,
+ decimation_factor)) {
+ (void)ddd_collection_rollback_v1(ops, state);
+ return DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ if (!ddd_read_register(ops, DDD_REGISTER_TEST_MODE, &readback)) {
+ (void)ddd_collection_rollback_v1(ops, state);
+ return DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ if (readback != (test_mode ? 1 : 0)) {
+ (void)ddd_collection_rollback_v1(ops, state);
+ return DDD_PROTOCOL_READBACK_MISMATCH;
+ }
+ if (!ddd_read_register(ops, DDD_REGISTER_DECIMATION, &readback)) {
+ (void)ddd_collection_rollback_v1(ops, state);
+ return DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ if (readback != decimation_factor) {
+ (void)ddd_collection_rollback_v1(ops, state);
+ return DDD_PROTOCOL_READBACK_MISMATCH;
+ }
+ state->configured = true;
+ state->collection_start_attempted = true;
+ if (!ddd_control_out(ops, DDD_REQUEST_COLLECTION, 1)) {
+ (void)ddd_collection_rollback_v1(ops, state);
+ return DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ state->collection_active = true;
+ return DDD_PROTOCOL_OK;
+}
+
+ddd_protocol_result_t ddd_collection_stop_v1(
+ const ddd_control_ops_t *ops,
+ ddd_collection_state_t *state)
+{
+ if (!state || !ddd_control_ops_valid(ops)) {
+ return DDD_PROTOCOL_INVALID_ARGUMENT;
+ }
+ if (state->profile != DDD_DEVICE_PROTOCOL_V1) {
+ return DDD_PROTOCOL_UNSUPPORTED_PROFILE;
+ }
+ if (!state->collection_start_attempted && !state->collection_active) {
+ return DDD_PROTOCOL_OK;
+ }
+ if (!ddd_control_out(ops, DDD_REQUEST_COLLECTION, 0)) {
+ return DDD_PROTOCOL_CONTROL_FAILURE;
+ }
+ state->collection_start_attempted = false;
+ state->collection_active = false;
+ return DDD_PROTOCOL_OK;
+}
+
+void ddd_sequence_validator_init(ddd_sequence_validator_t *state)
+{
+ if (!state) return;
+ memset(state, 0, sizeof(*state));
+ state->phase = DDD_SEQUENCE_SYNCHRONIZING;
+}
+
+static uint8_t ddd_next_sequence_marker(uint8_t marker)
+{
+ ++marker;
+ return marker == DDD_SEQUENCE_MARKER_COUNT ? 0 : marker;
+}
+
+static ddd_validation_result_t ddd_sequence_fail(
+ ddd_sequence_validator_t *state, uint8_t expected, uint8_t actual)
+{
+ state->phase = DDD_SEQUENCE_FAILED;
+ state->error_sample_index = state->samples_seen;
+ state->expected_marker = expected;
+ state->actual_marker = actual;
+ return DDD_VALIDATION_MISMATCH;
+}
+
+ddd_validation_result_t ddd_sequence_validator_feed(
+ ddd_sequence_validator_t *state,
+ const uint16_t *sample_words,
+ size_t sample_count)
+{
+ if (!state || (sample_count != 0 && !sample_words)) {
+ return DDD_VALIDATION_INVALID_ARGUMENT;
+ }
+ if (state->phase == DDD_SEQUENCE_FAILED) return DDD_VALIDATION_MISMATCH;
+ for (size_t i = 0; i < sample_count; ++i) {
+ uint8_t actual = (uint8_t)((sample_words[i] >> 10) & 0x3Fu);
+ if (actual >= DDD_SEQUENCE_MARKER_COUNT) {
+ return ddd_sequence_fail(state, 0, actual);
+ }
+ if (!state->marker_seen) {
+ state->marker_seen = true;
+ state->marker = actual;
+ state->samples_in_marker = 1;
+ ++state->samples_seen;
+ continue;
+ }
+ if (state->phase == DDD_SEQUENCE_SYNCHRONIZING) {
+ if (actual == state->marker) {
+ if (state->samples_in_marker == DDD_SEQUENCE_SAMPLES_PER_MARKER) {
+ return ddd_sequence_fail(
+ state, ddd_next_sequence_marker(state->marker), actual);
+ }
+ ++state->samples_in_marker;
+ } else {
+ uint8_t expected = ddd_next_sequence_marker(state->marker);
+ if (actual != expected) {
+ return ddd_sequence_fail(state, expected, actual);
+ }
+ state->phase = DDD_SEQUENCE_RUNNING;
+ state->marker = actual;
+ state->samples_in_marker = 1;
+ }
+ ++state->samples_seen;
+ continue;
+ }
+ if (actual == state->marker) {
+ if (state->samples_in_marker == DDD_SEQUENCE_SAMPLES_PER_MARKER) {
+ return ddd_sequence_fail(
+ state, ddd_next_sequence_marker(state->marker), actual);
+ }
+ ++state->samples_in_marker;
+ } else {
+ if (state->samples_in_marker != DDD_SEQUENCE_SAMPLES_PER_MARKER) {
+ return ddd_sequence_fail(state, state->marker, actual);
+ }
+ uint8_t expected = ddd_next_sequence_marker(state->marker);
+ if (actual != expected) {
+ return ddd_sequence_fail(state, expected, actual);
+ }
+ state->marker = actual;
+ state->samples_in_marker = 1;
+ }
+ ++state->samples_seen;
+ }
+ return DDD_VALIDATION_OK;
+}
+
+void ddd_test_ramp_validator_init(ddd_test_ramp_validator_t *state)
+{
+ if (state) memset(state, 0, sizeof(*state));
+}
+
+static ddd_validation_result_t ddd_test_ramp_fail(
+ ddd_test_ramp_validator_t *state, uint16_t expected, uint16_t actual)
+{
+ state->failed = true;
+ state->error_sample_index = state->samples_seen;
+ state->expected_value = expected;
+ state->actual_value = actual;
+ return DDD_VALIDATION_MISMATCH;
+}
+
+ddd_validation_result_t ddd_test_ramp_validator_feed(
+ ddd_test_ramp_validator_t *state,
+ const uint16_t *sample_words,
+ size_t sample_count)
+{
+ if (!state || (sample_count != 0 && !sample_words)) {
+ return DDD_VALIDATION_INVALID_ARGUMENT;
+ }
+ if (state->failed) return DDD_VALIDATION_MISMATCH;
+ for (size_t i = 0; i < sample_count; ++i) {
+ uint16_t actual = sample_words[i] & UINT16_C(0x03FF);
+ if (!state->armed) {
+ state->armed = true;
+ state->expected_next = (uint16_t)(actual + 1u);
+ } else if (!state->wrap_detected && actual == 0 &&
+ (state->expected_next == DDD_TEST_RAMP_NEW_WRAP ||
+ state->expected_next == DDD_TEST_RAMP_LEGACY_WRAP)) {
+ state->wrap_detected = true;
+ state->wrap_value = state->expected_next;
+ state->expected_next = 1;
+ } else {
+ if (actual != state->expected_next) {
+ return ddd_test_ramp_fail(state, state->expected_next, actual);
+ }
+ ++state->expected_next;
+ if (state->wrap_detected &&
+ state->expected_next == state->wrap_value) {
+ state->expected_next = 0;
+ }
+ }
+ ++state->samples_seen;
+ }
+ return DDD_VALIDATION_OK;
+}
diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h
new file mode 100644
index 0000000..f06db1d
--- /dev/null
+++ b/misrc_tools/common/ddd_protocol.h
@@ -0,0 +1,268 @@
+/*
+ * MISRC Common - Domesday Duplicator firmware protocol helpers.
+ *
+ * This module is dependency-free so protocol-v1 can be unit-tested without
+ * libusb or GUI state. Legacy capture remains implemented by gui_ddd.c.
+ */
+
+#ifndef MISRC_DDD_PROTOCOL_H
+#define MISRC_DDD_PROTOCOL_H
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define DDD_LEGACY_VENDOR_ID UINT16_C(0x1D50)
+#define DDD_LEGACY_PRODUCT_ID UINT16_C(0x603B)
+#define DDD_CURRENT_VENDOR_ID UINT16_C(0x1209)
+#define DDD_CURRENT_PRODUCT_ID UINT16_C(0x2347)
+#define DDD_SUPPORTED_PROTOCOL_VERSION UINT8_C(1)
+
+#define DDD_REQUEST_COLLECTION UINT8_C(0xB5)
+#define DDD_REQUEST_REGISTER_READ UINT8_C(0xB7)
+#define DDD_REQUEST_REGISTER_WRITE UINT8_C(0xB8)
+#define DDD_USB_REQUEST_VENDOR_OUT UINT8_C(0x40)
+#define DDD_USB_REQUEST_VENDOR_IN UINT8_C(0xC0)
+
+#define DDD_REGISTER_IDENTITY UINT8_C(0x00)
+#define DDD_IDENTITY_LENGTH 12u
+#define DDD_IDENTITY_VALUE UINT8_C(0x44)
+#define DDD_REGISTER_MAP_VERSION UINT8_C(0x01)
+#define DDD_SUPPORTED_REGISTER_MAP UINT8_C(2)
+#define DDD_REGISTER_BUILD_FLAGS UINT8_C(0x02)
+#define DDD_REGISTER_COMMIT UINT8_C(0x03)
+#define DDD_COMMIT_LENGTH 8u
+#define DDD_BUILD_DIRTY_FLAG UINT8_C(0x01)
+#define DDD_BUILD_COMMIT_FLAG UINT8_C(0x02)
+#define DDD_REGISTER_IMAGE_ROLE UINT8_C(0x0B)
+#define DDD_APPLICATION_IMAGE_ROLE UINT8_C(1)
+#define DDD_REGISTER_TEST_MODE UINT8_C(0x10)
+#define DDD_REGISTER_DECIMATION UINT8_C(0x12)
+
+#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)
+
+#define DDD_STREAM_INTERFACE_NUMBER 0
+#define DDD_STREAM_ALTERNATE_SETTING 0
+#define DDD_STREAM_ENDPOINT_ADDRESS UINT8_C(0x81)
+#define DDD_STREAM_MAX_PACKET_SIZE UINT16_C(1024)
+
+#define DDD_SEQUENCE_MARKER_COUNT UINT8_C(63)
+#define DDD_SEQUENCE_SAMPLES_PER_MARKER UINT32_C(65536)
+#define DDD_TEST_RAMP_NEW_WRAP UINT16_C(1021)
+#define DDD_TEST_RAMP_LEGACY_WRAP UINT16_C(1024)
+#define DDD_STABLE_ID_MAX 128u
+
+typedef enum ddd_device_profile {
+ DDD_DEVICE_NOT_DDD = 0,
+ DDD_DEVICE_LEGACY,
+ DDD_DEVICE_PROTOCOL_V1,
+ DDD_DEVICE_UNSUPPORTED
+} ddd_device_profile_t;
+
+bool ddd_is_known_device_id(uint16_t vendor_id, uint16_t product_id);
+ddd_device_profile_t ddd_classify_device(uint16_t vendor_id,
+ uint16_t product_id,
+ uint16_t bcd_device);
+bool ddd_profile_can_capture(ddd_device_profile_t profile);
+bool ddd_profile_requires_usb_path(ddd_device_profile_t profile);
+bool ddd_clockgen_candidate_is_preferred(
+ bool selection_present,
+ ddd_device_profile_t selected_profile,
+ ddd_device_profile_t candidate_profile,
+ bool candidate_capture_supported);
+bool ddd_reconnect_path_matches(
+ ddd_device_profile_t target_profile,
+ const char *target_usb_path,
+ ddd_device_profile_t candidate_profile,
+ const char *candidate_usb_path);
+
+typedef struct ddd_profile_index_state {
+ int legacy_count;
+ int protocol_v1_count;
+ int unsupported_count;
+} ddd_profile_index_state_t;
+
+void ddd_profile_index_state_init(ddd_profile_index_state_t *state);
+int ddd_profile_index_take(ddd_profile_index_state_t *state,
+ ddd_device_profile_t profile);
+
+bool ddd_v1_link_speed_allowed(bool speed_known, bool at_least_superspeed);
+bool ddd_decimation_is_supported(uint8_t factor);
+bool ddd_profile_supports_decimation(ddd_device_profile_t profile,
+ uint8_t factor);
+uint16_t ddd_make_register_write(uint8_t address, uint8_t value);
+uint32_t ddd_sample_rate_hz(uint8_t factor);
+uint32_t ddd_sample_rate_khz(uint8_t factor);
+
+typedef struct ddd_v1_rate_plan {
+ uint8_t decimation_factor;
+ uint32_t hardware_rate_khz;
+ uint32_t output_rate_khz;
+ bool software_resample;
+} ddd_v1_rate_plan_t;
+
+/* Resolve the output rate represented by the pre-unified settings. With the
+ * resampler disabled, the stored hardware decimation remains authoritative. */
+uint32_t ddd_v1_effective_output_rate_khz(
+ bool resample_enabled,
+ uint32_t resample_rate_khz,
+ uint8_t stored_decimation_factor);
+
+/* Protocol-v1 routes 20 MSPS directly through the FPGA half-rate path. Lower
+ * output rates use that 20 MSPS hardware stream as the software source. */
+bool ddd_v1_plan_output_rate_khz(uint32_t output_rate_khz,
+ ddd_v1_rate_plan_t *plan);
+
+bool ddd_identity_is_supported(const uint8_t *identity, size_t length);
+bool ddd_format_gateware_commit(const uint8_t *identity,
+ size_t identity_length,
+ char *destination,
+ size_t destination_size);
+bool ddd_format_usb_topology_path(uint8_t bus_number,
+ const uint8_t *ports,
+ int port_count,
+ char *destination,
+ size_t destination_size);
+
+typedef struct ddd_stream_endpoint_candidate {
+ int interface_number;
+ int alternate_setting;
+ uint8_t endpoint_address;
+ uint16_t max_packet_size;
+ bool is_bulk;
+ bool is_in;
+} ddd_stream_endpoint_candidate_t;
+
+typedef struct ddd_stream_path {
+ int interface_number;
+ int alternate_setting;
+ uint8_t endpoint_address;
+ uint16_t max_packet_size;
+ bool found;
+} ddd_stream_path_t;
+
+typedef struct ddd_stream_selector {
+ ddd_device_profile_t profile;
+ ddd_stream_path_t selected;
+ size_t protocol_v1_exact_matches;
+} ddd_stream_selector_t;
+
+void ddd_stream_selector_init(ddd_stream_selector_t *selector,
+ ddd_device_profile_t profile);
+void ddd_stream_selector_consider(
+ ddd_stream_selector_t *selector,
+ const ddd_stream_endpoint_candidate_t *candidate);
+bool ddd_stream_selector_get(const ddd_stream_selector_t *selector,
+ ddd_stream_path_t *selected);
+
+typedef int (*ddd_control_transfer_fn)(void *context,
+ uint8_t request_type,
+ uint8_t request,
+ uint16_t value,
+ uint16_t index,
+ uint8_t *data,
+ uint16_t length);
+
+typedef struct ddd_control_ops {
+ ddd_control_transfer_fn transfer;
+ void *context;
+} ddd_control_ops_t;
+
+typedef enum ddd_protocol_result {
+ DDD_PROTOCOL_OK = 0,
+ DDD_PROTOCOL_INVALID_ARGUMENT,
+ DDD_PROTOCOL_UNSUPPORTED_PROFILE,
+ DDD_PROTOCOL_UNSUPPORTED_DECIMATION,
+ DDD_PROTOCOL_CONTROL_FAILURE,
+ DDD_PROTOCOL_IDENTITY_MISMATCH,
+ DDD_PROTOCOL_READBACK_MISMATCH
+} ddd_protocol_result_t;
+
+typedef struct ddd_collection_state {
+ ddd_device_profile_t profile;
+ uint8_t decimation_factor;
+ uint32_t sample_rate_hz;
+ bool test_mode;
+ bool configured;
+ bool collection_start_attempted;
+ bool collection_active;
+ bool rollback_attempted;
+ bool rollback_succeeded;
+ uint8_t identity[DDD_IDENTITY_LENGTH];
+} ddd_collection_state_t;
+
+void ddd_collection_state_init(ddd_collection_state_t *state);
+
+/* 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. */
+ddd_protocol_result_t ddd_collection_start_v1(
+ const ddd_control_ops_t *ops,
+ bool test_mode,
+ uint8_t decimation_factor,
+ ddd_collection_state_t *state);
+ddd_protocol_result_t ddd_collection_stop_v1(
+ const ddd_control_ops_t *ops,
+ ddd_collection_state_t *state);
+ddd_protocol_result_t ddd_collection_rollback_v1(
+ const ddd_control_ops_t *ops,
+ ddd_collection_state_t *state);
+
+typedef enum ddd_validation_result {
+ DDD_VALIDATION_OK = 0,
+ DDD_VALIDATION_MISMATCH,
+ DDD_VALIDATION_INVALID_ARGUMENT
+} ddd_validation_result_t;
+
+typedef enum ddd_sequence_phase {
+ DDD_SEQUENCE_SYNCHRONIZING = 0,
+ DDD_SEQUENCE_RUNNING,
+ DDD_SEQUENCE_FAILED
+} ddd_sequence_phase_t;
+
+typedef struct ddd_sequence_validator {
+ ddd_sequence_phase_t phase;
+ bool marker_seen;
+ uint8_t marker;
+ uint32_t samples_in_marker;
+ uint64_t samples_seen;
+ uint64_t error_sample_index;
+ uint8_t expected_marker;
+ uint8_t actual_marker;
+} ddd_sequence_validator_t;
+
+void ddd_sequence_validator_init(ddd_sequence_validator_t *state);
+ddd_validation_result_t ddd_sequence_validator_feed(
+ ddd_sequence_validator_t *state,
+ const uint16_t *sample_words,
+ size_t sample_count);
+
+typedef struct ddd_test_ramp_validator {
+ bool armed;
+ bool failed;
+ bool wrap_detected;
+ uint16_t expected_next;
+ uint16_t wrap_value;
+ uint64_t samples_seen;
+ uint64_t error_sample_index;
+ uint16_t expected_value;
+ uint16_t actual_value;
+} ddd_test_ramp_validator_t;
+
+void ddd_test_ramp_validator_init(ddd_test_ramp_validator_t *state);
+ddd_validation_result_t ddd_test_ramp_validator_feed(
+ ddd_test_ramp_validator_t *state,
+ const uint16_t *sample_words,
+ size_t sample_count);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MISRC_DDD_PROTOCOL_H */
diff --git a/misrc_tools/common/device_enum.c b/misrc_tools/common/device_enum.c
index c48eee1..9b5a844 100644
--- a/misrc_tools/common/device_enum.c
+++ b/misrc_tools/common/device_enum.c
@@ -26,10 +26,6 @@
#ifdef ENABLE_DDD
#include "libusb_compat.h"
-
-// DdD USB VID/PID (Domesday Duplicator)
-#define DDD_VID 0x1D50
-#define DDD_PID 0x603B
#endif
@@ -42,6 +38,9 @@ void misrc_device_list_init(misrc_device_list_t *list)
list->devices = NULL;
list->count = 0;
list->capacity = 0;
+#ifdef ENABLE_DDD
+ list->ddd_enumeration_complete = false;
+#endif
}
void misrc_device_list_free(misrc_device_list_t *list)
@@ -52,6 +51,9 @@ void misrc_device_list_free(misrc_device_list_t *list)
}
list->count = 0;
list->capacity = 0;
+#ifdef ENABLE_DDD
+ list->ddd_enumeration_complete = false;
+#endif
}
static bool device_list_grow(misrc_device_list_t *list)
@@ -264,6 +266,7 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh,
}
if (!include_ddd) {
+ list->ddd_enumeration_complete = true;
return (int)list->count;
}
@@ -282,11 +285,21 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh,
libusb_device **devlist;
ssize_t num_devices = libusb_get_device_list(ctx, &devlist);
- int ddd_index = 0;
+ if (num_devices < 0) {
+ libusb_exit(ctx);
+ return (int)list->count;
+ }
+
+ ddd_profile_index_state_t ddd_indices;
+ bool enumeration_complete = true;
+ ddd_profile_index_state_init(&ddd_indices);
for (ssize_t i = 0; i < num_devices; i++) {
struct libusb_device_descriptor desc;
- if (libusb_get_device_descriptor(devlist[i], &desc) == 0) {
- if (desc.idVendor == DDD_VID && desc.idProduct == DDD_PID) {
+ int descriptor_result = libusb_get_device_descriptor(devlist[i], &desc);
+ if (descriptor_result == 0) {
+ ddd_device_profile_t profile = ddd_classify_device(
+ desc.idVendor, desc.idProduct, desc.bcdDevice);
+ if (profile != DDD_DEVICE_NOT_DDD) {
misrc_device_info_t *dev = device_list_add(list);
if (!dev) {
libusb_free_device_list(devlist, 1);
@@ -295,9 +308,43 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh,
}
dev->type = MISRC_DEVICE_TYPE_DDD;
- dev->index = ddd_index++;
+ /* The legacy opener counts only legacy VID/PID rows. Keep
+ * indices profile-local so a preceding v3.1/unsupported row
+ * cannot shift the selected legacy physical device. */
+ dev->index = ddd_profile_index_take(&ddd_indices, profile);
+ dev->ddd_profile = profile;
+ dev->ddd_vendor_id = desc.idVendor;
+ dev->ddd_product_id = desc.idProduct;
+ dev->ddd_bcd_device = desc.bcdDevice;
+ dev->ddd_capture_supported = ddd_profile_can_capture(profile);
+
+ if (profile == DDD_DEVICE_LEGACY) {
+ snprintf(dev->name, sizeof(dev->name),
+ "Domesday Duplicator (legacy firmware)");
+ } else if (profile == DDD_DEVICE_PROTOCOL_V1) {
+ snprintf(dev->name, sizeof(dev->name),
+ "Domesday Duplicator");
+ } else {
+ snprintf(dev->name, sizeof(dev->name),
+ "Domesday Duplicator (unsupported protocol %u)",
+ (unsigned)(desc.bcdDevice >> 8));
+ }
- snprintf(dev->name, sizeof(dev->name), "Domesday Duplicator");
+ {
+ uint8_t ports[8];
+ int port_count = libusb_get_port_numbers(
+ devlist[i], ports, (int)sizeof(ports));
+ if (!ddd_format_usb_topology_path(
+ libusb_get_bus_number(devlist[i]), ports,
+ port_count, dev->ddd_usb_path,
+ sizeof(dev->ddd_usb_path))) {
+ dev->ddd_usb_path[0] = '\0';
+ if (ddd_profile_requires_usb_path(profile)) {
+ dev->ddd_capture_supported = false;
+ enumeration_complete = false;
+ }
+ }
+ }
/* Try to get serial number */
dev->device_id[0] = '\0';
@@ -315,11 +362,14 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh,
dev->supports_1080p60 = false; /* N/A for DdD */
}
+ } else {
+ enumeration_complete = false;
}
}
libusb_free_device_list(devlist, 1);
libusb_exit(ctx);
+ list->ddd_enumeration_complete = enumeration_complete;
return (int)list->count;
}
diff --git a/misrc_tools/common/device_enum.h b/misrc_tools/common/device_enum.h
index 3eb182c..c888f3b 100644
--- a/misrc_tools/common/device_enum.h
+++ b/misrc_tools/common/device_enum.h
@@ -12,6 +12,10 @@
#include
#include
+#ifdef ENABLE_DDD
+#include "ddd_protocol.h"
+#endif
+
/*-----------------------------------------------------------------------------
* Device Information
*-----------------------------------------------------------------------------*/
@@ -36,6 +40,14 @@ typedef struct {
char name[DEVICE_NAME_MAX]; /* Human-readable device name */
char device_id[DEVICE_ID_MAX]; /* Device ID (for simple_capture) */
bool supports_1080p60; /* True if device supports 1920x1080 @ 60fps YUYV */
+#ifdef ENABLE_DDD
+ ddd_device_profile_t ddd_profile; /* Firmware/protocol profile for DDD rows */
+ uint16_t ddd_vendor_id;
+ uint16_t ddd_product_id;
+ uint16_t ddd_bcd_device;
+ char ddd_usb_path[DDD_STABLE_ID_MAX]; /* Stable bus/port topology path */
+ bool ddd_capture_supported;
+#endif
} misrc_device_info_t;
/*-----------------------------------------------------------------------------
@@ -46,6 +58,9 @@ typedef struct {
misrc_device_info_t *devices; /* Array of device info */
size_t count; /* Number of devices */
size_t capacity; /* Allocated capacity */
+#ifdef ENABLE_DDD
+ bool ddd_enumeration_complete; /* Safe basis for unplug/replug observation */
+#endif
} misrc_device_list_t;
/* Initialize device list
diff --git a/misrc_tools/meson.build b/misrc_tools/meson.build
index bbb88b5..bdefa96 100644
--- a/misrc_tools/meson.build
+++ b/misrc_tools/meson.build
@@ -262,6 +262,7 @@ if libusb_common_dep.found()
libusb_common_dep_added = true
endif
cflags += ['-DENABLE_DDD=1']
+ sources_capture += 'common/ddd_protocol.c'
ddd_enabled = true
message('libusb-1.0 found, building with DdD support')
else
@@ -432,7 +433,10 @@ if raylib_dep.found()
# Add DdD support to GUI if available
if ddd_enabled
+ sources_gui += 'common/ddd_protocol.c'
sources_gui += 'misrc_gui/input/gui_ddd.c'
+ 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'
endif
@@ -555,3 +559,29 @@ if raylib_dep.found()
else
message('raylib not found, skipping GUI application')
endif
+
+# Dependency-free DDD protocol/policy tests run even when libusb or raylib is
+# unavailable, so the firmware contract remains covered on lean CI hosts.
+ddd_protocol_test = executable('ddd_protocol_test',
+ ['test/ddd_protocol_test.c', 'common/ddd_protocol.c'],
+ dependencies: [],
+ c_args: cflags,
+)
+test('ddd_protocol', ddd_protocol_test)
+
+ddd_async_policy_test = executable('gui_ddd_async_policy_test',
+ 'test/gui_ddd_async_policy_test.c',
+ dependencies: [],
+ c_args: cflags,
+)
+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'],
+ dependencies: [libusb_common_dep],
+ c_args: cflags,
+ )
+ test('gui_ddd_async_fault', ddd_async_fault_test)
+endif
diff --git a/misrc_tools/misrc_gui/core/gui_app.h b/misrc_tools/misrc_gui/core/gui_app.h
index 3e716b6..244e47d 100644
--- a/misrc_tools/misrc_gui/core/gui_app.h
+++ b/misrc_tools/misrc_gui/core/gui_app.h
@@ -9,6 +9,9 @@
#include
#include "raylib.h"
#include "../../common/buffer_manager.h"
+#ifdef ENABLE_DDD
+#include "../../common/ddd_protocol.h"
+#endif
// Forward declarations
typedef struct hsdaoh_dev hsdaoh_dev_t;
@@ -162,6 +165,15 @@ typedef struct {
char serial[64];
device_type_t type;
int index;
+#ifdef ENABLE_DDD
+ ddd_device_profile_t ddd_profile;
+ uint16_t ddd_vendor_id;
+ uint16_t ddd_product_id;
+ uint16_t ddd_bcd_device;
+ char ddd_usb_path[DDD_STABLE_ID_MAX];
+ bool ddd_capture_supported;
+ bool ddd_clockgen;
+#endif
} device_info_t;
// GUI settings (bound to UI controls) - mirrors all CLI options
@@ -232,6 +244,9 @@ typedef struct {
int resample_quality_b; // 0-4
float resample_gain_a; // dB
float resample_gain_b; // dB
+#ifdef ENABLE_DDD
+ uint8_t ddd_decimation; // Firmware 3.1 only: 1=40, 2=20 MSPS
+#endif
// FLAC compression
bool use_flac;
diff --git a/misrc_tools/misrc_gui/core/gui_settings.c b/misrc_tools/misrc_gui/core/gui_settings.c
index 0b9d8f5..696014c 100644
--- a/misrc_tools/misrc_gui/core/gui_settings.c
+++ b/misrc_tools/misrc_gui/core/gui_settings.c
@@ -431,6 +431,9 @@ void gui_settings_init_defaults(gui_settings_t *settings) {
settings->resample_quality_b = 3; // High quality
settings->resample_gain_a = 0.0f; // No gain
settings->resample_gain_b = 0.0f; // No gain
+#ifdef ENABLE_DDD
+ settings->ddd_decimation = DDD_DECIMATION_FULL_RATE;
+#endif
// FLAC defaults
settings->use_flac = true;
@@ -586,6 +589,9 @@ void gui_settings_save(const gui_settings_t *settings) {
fprintf(f, " \"resample_quality_b\": %d,\n", settings->resample_quality_b);
fprintf(f, " \"resample_gain_a\": %.1f,\n", settings->resample_gain_a);
fprintf(f, " \"resample_gain_b\": %.1f,\n", settings->resample_gain_b);
+#ifdef ENABLE_DDD
+ fprintf(f, " \"ddd_decimation\": %u,\n", (unsigned)settings->ddd_decimation);
+#endif
fprintf(f, " \"use_flac\": %s,\n", settings->use_flac ? "true" : "false");
fprintf(f, " \"flac_12bit\": %s,\n", settings->flac_12bit ? "true" : "false");
fprintf(f, " \"flac_level\": %d,\n", settings->flac_level);
@@ -1016,6 +1022,15 @@ void gui_settings_load(gui_settings_t *settings) {
settings->resample_gain_a = (float)atof(value);
}
+#ifdef ENABLE_DDD
+ if ((value = find_value(content, "ddd_decimation")) != NULL) {
+ uint8_t factor = (uint8_t)atoi(value);
+ if (ddd_decimation_is_supported(factor)) {
+ settings->ddd_decimation = factor;
+ }
+ }
+#endif
+
if ((value = find_value(content, "resample_gain_b")) != NULL) {
settings->resample_gain_b = (float)atof(value);
}
diff --git a/misrc_tools/misrc_gui/core/misrc_gui.c b/misrc_tools/misrc_gui/core/misrc_gui.c
index 84769ac..37c5c89 100644
--- a/misrc_tools/misrc_gui/core/misrc_gui.c
+++ b/misrc_tools/misrc_gui/core/misrc_gui.c
@@ -144,6 +144,10 @@ typedef struct {
int index;
char name[64];
char serial[64];
+#ifdef ENABLE_DDD
+ ddd_device_profile_t ddd_profile;
+ char ddd_usb_path[DDD_STABLE_ID_MAX];
+#endif
} gui_reconnect_target_t;
static int gui_find_first_device_of_type(const gui_app_t *app, device_type_t type) {
if (!app) return -1;
@@ -161,6 +165,10 @@ static void gui_set_reconnect_target_from_selected(const gui_app_t *app, gui_rec
target->index = -1;
target->name[0] = '\0';
target->serial[0] = '\0';
+#ifdef ENABLE_DDD
+ target->ddd_profile = DDD_DEVICE_NOT_DDD;
+ target->ddd_usb_path[0] = '\0';
+#endif
if (!app) return;
if (app->selected_device < 0 || app->selected_device >= app->device_count) return;
const device_info_t *dev = &app->devices[app->selected_device];
@@ -169,6 +177,13 @@ static void gui_set_reconnect_target_from_selected(const gui_app_t *app, gui_rec
target->index = dev->index;
snprintf(target->name, sizeof(target->name), "%s", dev->name);
snprintf(target->serial, sizeof(target->serial), "%s", dev->serial);
+#ifdef ENABLE_DDD
+ if (dev->type == DEVICE_TYPE_DDD) {
+ target->ddd_profile = dev->ddd_profile;
+ snprintf(target->ddd_usb_path, sizeof(target->ddd_usb_path), "%s",
+ dev->ddd_usb_path);
+ }
+#endif
}
static int gui_find_reconnect_device(const gui_app_t *app, const gui_reconnect_target_t *target) {
if (!app || !target || !target->valid) return -1;
@@ -220,10 +235,17 @@ static int gui_find_reconnect_device(const gui_app_t *app, const gui_reconnect_t
}
#ifdef ENABLE_DDD
else if (target->type == DEVICE_TYPE_DDD) {
- // Match by name so the synthetic "[DdD] Clockgen" entry reconnects
- // to itself rather than the plain "[DdD] Domesday Duplicator" entry
- // (both share DEVICE_TYPE_DDD; the name disambiguates them).
- if (target->name[0] && strcmp(dev->name, target->name) == 0) {
+ // The name keeps the synthetic Clockgen variant separate. A
+ // protocol-v1 device must also retain its exact USB topology path;
+ // never switch to another same-name DdD during auto-reconnect.
+ if (!target->name[0] || strcmp(dev->name, target->name) != 0 ||
+ dev->ddd_profile != target->ddd_profile) {
+ continue;
+ }
+ if (!ddd_profile_requires_usb_path(target->ddd_profile) ||
+ ddd_reconnect_path_matches(
+ target->ddd_profile, target->ddd_usb_path,
+ dev->ddd_profile, dev->ddd_usb_path)) {
return i;
}
}
@@ -232,6 +254,11 @@ static int gui_find_reconnect_device(const gui_app_t *app, const gui_reconnect_t
return i;
}
}
+#ifdef ENABLE_DDD
+ // DdD profiles are not interchangeable. If the selected profile/path is
+ // absent, wait for it instead of falling back to another DdD device.
+ if (target->type == DEVICE_TYPE_DDD) return -1;
+#endif
return fallback_same_type;
}
diff --git a/misrc_tools/misrc_gui/input/gui_capture.c b/misrc_tools/misrc_gui/input/gui_capture.c
index 9c69cd8..87caaea 100644
--- a/misrc_tools/misrc_gui/input/gui_capture.c
+++ b/misrc_tools/misrc_gui/input/gui_capture.c
@@ -28,6 +28,7 @@
#endif
#ifdef ENABLE_DDD
#include "gui_ddd.h"
+#include "gui_ddd_v1.h"
#include "gui_ddd_clockgen.h"
#endif
#include "../visualization/gui_panel.h"
@@ -364,6 +365,21 @@ static inline uint32_t gui_capture_normalize_sample_rate_hz(uint32_t raw_srate)
return (uint32_t)hz;
}
+#ifdef ENABLE_DDD
+static uint32_t gui_capture_ddd_resample_setting_khz(const gui_app_t *app)
+{
+ float rate_khz;
+ if (!app) return 0;
+ rate_khz = app->settings.resample_rate_a;
+ if (!isfinite(rate_khz) || rate_khz <= 0.0f ||
+ rate_khz > (float)ddd_sample_rate_khz(
+ DDD_DECIMATION_FULL_RATE) + 0.5f) {
+ return 0;
+ }
+ return (uint32_t)lroundf(rate_khz);
+}
+#endif
+
static inline void gui_capture_update_backpressure_counters(gui_app_t *app)
{
if (!app) return;
@@ -1249,6 +1265,7 @@ void gui_app_enumerate_devices(gui_app_t *app) {
#ifdef ENABLE_DDD
bool ddd_device_added = false;
int first_ddd_src_index = -1;
+ device_info_t first_ddd_device = {0};
#endif
// Copy devices to GUI format
@@ -1279,11 +1296,25 @@ void gui_app_enumerate_devices(gui_app_t *app) {
dst->type = DEVICE_TYPE_DDD;
dst->index = src->index;
snprintf(dst->serial, sizeof(dst->serial), "%s", src->device_id);
- // Remember the first DdD device so the synthetic "[DdD] Clockgen"
- // entry below can target the same physical device for its RF path.
- if (!ddd_device_added) {
+ dst->ddd_profile = src->ddd_profile;
+ dst->ddd_vendor_id = src->ddd_vendor_id;
+ dst->ddd_product_id = src->ddd_product_id;
+ dst->ddd_bcd_device = src->ddd_bcd_device;
+ snprintf(dst->ddd_usb_path, sizeof(dst->ddd_usb_path), "%s",
+ src->ddd_usb_path);
+ dst->ddd_capture_supported = src->ddd_capture_supported;
+ dst->ddd_clockgen = false;
+ // Preserve the legacy Clockgen RF path when legacy and protocol-v1
+ // devices coexist. Protocol-v1 remains available when it is the
+ // only supported DdD profile.
+ if (ddd_clockgen_candidate_is_preferred(
+ ddd_device_added,
+ first_ddd_device.ddd_profile,
+ src->ddd_profile,
+ src->ddd_capture_supported)) {
ddd_device_added = true;
first_ddd_src_index = src->index;
+ first_ddd_device = *dst;
}
}
#endif
@@ -1309,6 +1340,9 @@ void gui_app_enumerate_devices(gui_app_t *app) {
app->device_count++;
}
+#ifdef ENABLE_DDD
+ gui_ddd_v1_observe_enumeration(&devices);
+#endif
misrc_device_list_free(&devices);
int cxadc_card_count = gui_cxadc_detect_cards();
@@ -1326,6 +1360,14 @@ void gui_app_enumerate_devices(gui_app_t *app) {
snprintf(dst->serial, sizeof(dst->serial), "%s", DDD_CLOCKGEN_MARKER_SERIAL);
dst->type = DEVICE_TYPE_DDD;
dst->index = first_ddd_src_index;
+ dst->ddd_profile = first_ddd_device.ddd_profile;
+ dst->ddd_vendor_id = first_ddd_device.ddd_vendor_id;
+ dst->ddd_product_id = first_ddd_device.ddd_product_id;
+ dst->ddd_bcd_device = first_ddd_device.ddd_bcd_device;
+ snprintf(dst->ddd_usb_path, sizeof(dst->ddd_usb_path), "%s",
+ first_ddd_device.ddd_usb_path);
+ dst->ddd_capture_supported = first_ddd_device.ddd_capture_supported;
+ dst->ddd_clockgen = true;
app->device_count++;
}
#endif
@@ -1885,20 +1927,47 @@ int gui_app_start_capture(gui_app_t *app) {
#ifdef ENABLE_DDD
// Handle DdD device
if (dev->type == DEVICE_TYPE_DDD) {
+ ddd_v1_rate_plan_t v1_rate_plan = {0};
proc_set_priority(PROC_PRIORITY_ABOVE);
thrd_set_priority(THRD_PRIORITY_CRITICAL);
// gui_ddd_start() launches extraction internally; declare A-only mode
// before startup so channel mapping is correct from first frame.
app->capture_backend_upstream = false;
app->capture_has_channel_b = false;
- // Open DdD device first
- int r = gui_ddd_open(app, dev->index);
+ if (!dev->ddd_capture_supported) {
+ gui_app_set_status(app, "Selected DdD firmware is unsupported");
+ proc_set_priority(PROC_PRIORITY_NORMAL);
+ return -1;
+ }
+ if (dev->ddd_profile == DDD_DEVICE_PROTOCOL_V1) {
+ uint32_t output_rate_khz =
+ ddd_v1_effective_output_rate_khz(
+ app->settings.enable_resample_a,
+ gui_capture_ddd_resample_setting_khz(app),
+ app->settings.ddd_decimation);
+ if (!ddd_v1_plan_output_rate_khz(
+ output_rate_khz, &v1_rate_plan)) {
+ gui_app_set_status(app, "Invalid DdD RF rate settings");
+ proc_set_priority(PROC_PRIORITY_NORMAL);
+ return -1;
+ }
+ }
+ // Legacy and firmware 3.1 deliberately use separate backends. Only
+ // the firmware-3.1 profile sees B5/B7/B8 or the async queue.
+ int r = dev->ddd_profile == DDD_DEVICE_PROTOCOL_V1
+ ? gui_ddd_v1_open(app, dev->ddd_usb_path)
+ : gui_ddd_open(app, dev->index);
if (r < 0) {
- gui_app_set_status(app, "Failed to open DdD device");
+ if (dev->ddd_profile != DDD_DEVICE_PROTOCOL_V1) {
+ gui_app_set_status(app, "Failed to open DdD device");
+ }
proc_set_priority(PROC_PRIORITY_NORMAL);
return -1;
}
- int ddd_rc = gui_ddd_start(app);
+ int ddd_rc = dev->ddd_profile == DDD_DEVICE_PROTOCOL_V1
+ ? gui_ddd_v1_start(app, v1_rate_plan.decimation_factor,
+ gui_ddd_get_test_mode())
+ : gui_ddd_start(app);
if (ddd_rc == 0) {
// Same watchdog fix as FX3: capture_start_time must be set here or
// the auto-reconnect watchdog fires within 2s (DdD streams
@@ -2363,7 +2432,12 @@ void gui_app_stop_capture(gui_app_t *app) {
#ifdef ENABLE_DDD
if (dev->type == DEVICE_TYPE_DDD) {
bool was_clockgen = gui_ddd_clockgen_device_mode(dev);
- gui_ddd_stop(app);
+ if (gui_ddd_v1_is_active() ||
+ dev->ddd_profile == DDD_DEVICE_PROTOCOL_V1) {
+ gui_ddd_v1_stop(app);
+ } else {
+ gui_ddd_stop(app);
+ }
if (was_clockgen) {
// Stop the Clockgen Lite audio capture that ran in parallel with
// the DdD RF capture. gui_ddd_stop already set is_capturing=false,
diff --git a/misrc_tools/misrc_gui/input/gui_ddd.c b/misrc_tools/misrc_gui/input/gui_ddd.c
index 181fa7c..cb3b1dd 100644
--- a/misrc_tools/misrc_gui/input/gui_ddd.c
+++ b/misrc_tools/misrc_gui/input/gui_ddd.c
@@ -72,13 +72,13 @@ static int s_ddd_interface = 0;
static uint8_t s_ddd_bulk_ep = DDD_EP_BULK_IN;
static atomic_bool s_ddd_transfer_ready = false;
-typedef struct ddd_stream_path {
+typedef struct ddd_legacy_stream_path {
int interface_number;
int alternate_setting;
uint8_t endpoint_address;
uint16_t max_packet_size;
bool found;
-} ddd_stream_path_t;
+} ddd_legacy_stream_path_t;
// Sequence-number validation state (capture thread only). The DdD sequence
// number is constant for 65536 samples then advances by 1 (mod 64). We only
@@ -150,8 +150,8 @@ static void gui_ddd_usb_exit(void) {
}
}
-static ddd_stream_path_t gui_ddd_find_stream_path(libusb_device *device) {
- ddd_stream_path_t best = {
+static ddd_legacy_stream_path_t gui_ddd_find_stream_path(libusb_device *device) {
+ ddd_legacy_stream_path_t best = {
.interface_number = 0,
.alternate_setting = 0,
.endpoint_address = DDD_EP_BULK_IN,
@@ -376,7 +376,7 @@ int gui_ddd_open(gui_app_t *app, int device_index) {
"not SuperSpeed. DdD requires USB 3.0 for full 40 MSPS.\n");
}
// Determine the streaming interface/endpoint from descriptors
- ddd_stream_path_t stream_path = gui_ddd_find_stream_path(devlist[i]);
+ ddd_legacy_stream_path_t stream_path = gui_ddd_find_stream_path(devlist[i]);
s_ddd_interface = stream_path.interface_number;
s_ddd_bulk_ep = stream_path.endpoint_address;
if (stream_path.found) {
diff --git a/misrc_tools/misrc_gui/input/gui_ddd_async.c b/misrc_tools/misrc_gui/input/gui_ddd_async.c
new file mode 100644
index 0000000..7eb8e0e
--- /dev/null
+++ b/misrc_tools/misrc_gui/input/gui_ddd_async.c
@@ -0,0 +1,575 @@
+/*
+ * MISRC GUI - DDD firmware 3.1 asynchronous USB capture queue
+ */
+
+#include
+#include
+#include
+
+#include "../../common/libusb_compat.h"
+#include "../../common/threading.h"
+#include "gui_ddd_async.h"
+
+#define GUI_DDD_ASYNC_EVENT_POLL_US 10000L
+
+_Static_assert(GUI_DDD_ASYNC_TRANSFER_COUNT == 96,
+ "DDD 3.1 queue geometry must remain 96 x 128 KiB");
+_Static_assert(GUI_DDD_ASYNC_QUEUE_BYTES ==
+ GUI_DDD_ASYNC_TRANSFER_COUNT *
+ GUI_DDD_ASYNC_TRANSFER_BYTES,
+ "DDD 3.1 queue geometry must divide exactly");
+_Static_assert(GUI_DDD_ASYNC_ABANDONED_CAPACITY == 1,
+ "A process may retain only one unreaped DDD queue");
+
+typedef struct gui_ddd_async_orphan gui_ddd_async_engine_t;
+
+typedef struct {
+ gui_ddd_async_engine_t *owner;
+ size_t slot_index;
+ struct libusb_transfer *transfer;
+ uint8_t *buffer;
+ int actual_length;
+ bool cancel_requested;
+} gui_ddd_async_slot_t;
+
+struct gui_ddd_async_orphan {
+ gui_ddd_async_config_t config_storage;
+ const gui_ddd_async_config_t *config;
+ gui_ddd_async_slot_t *slots;
+ gui_ddd_async_policy_slot_t policy_slots[GUI_DDD_ASYNC_TRANSFER_COUNT];
+ uint8_t *buffer_pool;
+ gui_ddd_async_order_policy_t order;
+ gui_ddd_async_result_t result;
+ uint64_t next_submit_id;
+ size_t submitted_count;
+ atomic_size_t in_flight;
+ atomic_size_t callbacks_active;
+ uint64_t cancel_deadline_ms;
+ bool accepting_submissions;
+ bool stopping;
+ bool cancel_issued;
+ bool failed;
+ bool abandoned;
+};
+
+/* Deliberate process-lifetime ownership for a backend that never returns its
+ * cancellation callbacks. This is bounded (at most one entry per quarantined
+ * capture attempt) and prevents UAF of libusb transfer/user-data storage. */
+static gui_ddd_async_engine_t *s_ddd_async_abandoned = NULL;
+
+static uint64_t gui_ddd_async_now_ms(gui_ddd_async_engine_t *engine)
+{
+ if (engine && engine->config && engine->config->now_ms_override) {
+ return engine->config->now_ms_override(
+ engine->config->now_ms_context);
+ }
+ /* get_time_ms() uses 32-bit GetTickCount on Windows and wraps after
+ * 49.7 days. get_time_us() uses GetTickCount64 there and is monotonic on
+ * every supported platform, so the reap deadline remains truly bounded. */
+ return get_time_us() / UINT64_C(1000);
+}
+
+static bool gui_ddd_async_engine_has_pending(
+ const gui_ddd_async_engine_t *engine)
+{
+ if (!engine) return false;
+ return gui_ddd_async_policy_has_pending(
+ atomic_load(&engine->in_flight),
+ atomic_load(&engine->callbacks_active));
+}
+
+static void gui_ddd_async_signal_failure(gui_ddd_async_engine_t *engine)
+{
+ if (!engine || !engine->config) return;
+ if (engine->config->transfer_ready) {
+ atomic_store(engine->config->transfer_ready, false);
+ }
+ if (engine->config->startup_failed) {
+ atomic_store(engine->config->startup_failed, true);
+ }
+}
+
+static void gui_ddd_async_latch_failure(gui_ddd_async_engine_t *engine,
+ gui_ddd_async_result_code_t code,
+ int libusb_error,
+ int transfer_status,
+ int actual_length,
+ uint64_t submission_id)
+{
+ if (!engine || engine->failed) return;
+ engine->failed = true;
+ engine->accepting_submissions = false;
+ engine->result.code = code;
+ engine->result.libusb_error = libusb_error;
+ engine->result.transfer_status = transfer_status;
+ engine->result.actual_length = actual_length;
+ engine->result.submission_id = submission_id;
+ gui_ddd_async_signal_failure(engine);
+}
+
+static void LIBUSB_CALL gui_ddd_async_transfer_callback(
+ struct libusb_transfer *transfer)
+{
+ gui_ddd_async_slot_t *slot;
+ gui_ddd_async_engine_t *engine;
+ gui_ddd_async_policy_slot_t *policy_slot;
+
+ if (!transfer || !transfer->user_data) return;
+ slot = (gui_ddd_async_slot_t *)transfer->user_data;
+ engine = slot->owner;
+ if (!engine) return;
+ atomic_fetch_add(&engine->callbacks_active, 1);
+ policy_slot = &engine->policy_slots[slot->slot_index];
+
+ if (policy_slot->state != GUI_DDD_ASYNC_SLOT_SUBMITTED ||
+ atomic_load(&engine->in_flight) == 0) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_ORDER_FAILURE, 0,
+ (int)transfer->status, transfer->actual_length,
+ policy_slot->submission_id);
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_FAILED;
+ atomic_fetch_sub(&engine->callbacks_active, 1);
+ return;
+ }
+
+ engine->result.completed_transfers++;
+ slot->actual_length = transfer->actual_length;
+
+ if (transfer->status == LIBUSB_TRANSFER_COMPLETED) {
+ if (!gui_ddd_async_policy_exact_length(
+ (size_t)transfer->actual_length)) {
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_FAILED;
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_SHORT_TRANSFER, 0,
+ (int)transfer->status, transfer->actual_length,
+ policy_slot->submission_id);
+ atomic_fetch_sub(&engine->in_flight, 1);
+ atomic_fetch_sub(&engine->callbacks_active, 1);
+ return;
+ }
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_COMPLETE;
+ atomic_fetch_sub(&engine->in_flight, 1);
+ atomic_fetch_sub(&engine->callbacks_active, 1);
+ return;
+ }
+
+ if (transfer->status == LIBUSB_TRANSFER_CANCELLED &&
+ (slot->cancel_requested || engine->failed)) {
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_CANCELLED;
+ atomic_fetch_sub(&engine->in_flight, 1);
+ atomic_fetch_sub(&engine->callbacks_active, 1);
+ return;
+ }
+
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_FAILED;
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_TRANSFER_FAILURE, 0,
+ (int)transfer->status, transfer->actual_length,
+ policy_slot->submission_id);
+ atomic_fetch_sub(&engine->in_flight, 1);
+ atomic_fetch_sub(&engine->callbacks_active, 1);
+}
+
+static int gui_ddd_async_submit_slot(gui_ddd_async_engine_t *engine,
+ gui_ddd_async_slot_t *slot)
+{
+ gui_ddd_async_policy_slot_t *policy_slot =
+ &engine->policy_slots[slot->slot_index];
+ int rc;
+
+ policy_slot->submission_id = engine->next_submit_id++;
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_SUBMITTED;
+ slot->actual_length = 0;
+ slot->cancel_requested = false;
+ libusb_fill_bulk_transfer(slot->transfer,
+ engine->config->device_handle,
+ engine->config->endpoint,
+ slot->buffer,
+ (int)GUI_DDD_ASYNC_TRANSFER_BYTES,
+ gui_ddd_async_transfer_callback,
+ slot,
+ 0);
+ slot->transfer->flags = LIBUSB_TRANSFER_SHORT_NOT_OK;
+
+ if (engine->config->submit_override) {
+ rc = engine->config->submit_override(
+ engine->config->submit_context, slot->transfer);
+ } else {
+ rc = libusb_submit_transfer(slot->transfer);
+ }
+ if (rc < 0) {
+ policy_slot->state = GUI_DDD_ASYNC_SLOT_FAILED;
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_SUBMIT_FAILURE, rc, 0, 0,
+ policy_slot->submission_id);
+ return -1;
+ }
+
+ engine->submitted_count++;
+ atomic_fetch_add(&engine->in_flight, 1);
+ return 0;
+}
+
+static void gui_ddd_async_cancel_in_flight(gui_ddd_async_engine_t *engine)
+{
+ size_t i;
+
+ if (!engine || engine->cancel_issued) return;
+ engine->cancel_issued = true;
+ engine->cancel_deadline_ms = gui_ddd_async_now_ms(engine) +
+ GUI_DDD_ASYNC_CANCEL_REAP_TIMEOUT_MS;
+ engine->accepting_submissions = false;
+ 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 =
+ &engine->policy_slots[i];
+ int rc;
+ if (policy_slot->state != GUI_DDD_ASYNC_SLOT_SUBMITTED) continue;
+ slot->cancel_requested = true;
+ if (engine->config->cancel_override) {
+ rc = engine->config->cancel_override(
+ engine->config->cancel_context, slot->transfer);
+ } else {
+ rc = libusb_cancel_transfer(slot->transfer);
+ }
+ if (rc < 0 && rc != LIBUSB_ERROR_NOT_FOUND) {
+ /* Preserve the original failure, but keep pumping callbacks. */
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_EVENT_FAILURE, rc, 0, 0,
+ policy_slot->submission_id);
+ }
+ }
+}
+
+static int gui_ddd_async_consume_ready(gui_ddd_async_engine_t *engine)
+{
+ for (;;) {
+ gui_ddd_async_next_state_t next_state;
+ gui_ddd_async_slot_t *slot;
+ size_t slot_index = 0;
+ bool capture_running =
+ atomic_load(engine->config->capture_running);
+
+ if (gui_ddd_async_policy_stop_drain_complete(
+ capture_running,
+ atomic_load(&engine->in_flight),
+ engine->result.completed_transfers,
+ engine->result.consumed_transfers)) {
+ return 0;
+ }
+
+ next_state = gui_ddd_async_order_policy_peek(
+ &engine->order,
+ engine->policy_slots,
+ &slot_index);
+ if (next_state == GUI_DDD_ASYNC_NEXT_WAIT) return 0;
+ if (next_state == GUI_DDD_ASYNC_NEXT_STALE) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_ORDER_FAILURE, 0, 0, 0,
+ engine->order.next_consume_id);
+ return -1;
+ }
+
+ slot = &engine->slots[slot_index];
+ if (engine->config->consume(
+ engine->config->consume_context,
+ slot->buffer,
+ (size_t)slot->actual_length) !=
+ GUI_DDD_ASYNC_CONSUME_CONTINUE) {
+ engine->policy_slots[slot_index].state =
+ GUI_DDD_ASYNC_SLOT_FAILED;
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_CONSUMER_FAILURE, 0, 0,
+ slot->actual_length,
+ engine->policy_slots[slot_index].submission_id);
+ return -1;
+ }
+
+ engine->result.consumed_transfers++;
+ engine->policy_slots[slot_index].state =
+ GUI_DDD_ASYNC_SLOT_RETIRED;
+ gui_ddd_async_order_policy_advance(&engine->order);
+ capture_running = atomic_load(engine->config->capture_running);
+ if (gui_ddd_async_policy_should_resubmit(
+ engine->accepting_submissions,
+ capture_running,
+ engine->failed)) {
+ if (gui_ddd_async_submit_slot(engine, slot) < 0) return -1;
+ }
+ }
+}
+
+static int gui_ddd_async_pump_events(gui_ddd_async_engine_t *engine)
+{
+ struct timeval timeout;
+ int rc;
+
+ if (engine->config->event_pump_override) {
+ rc = engine->config->event_pump_override(
+ engine->config->event_pump_context,
+ GUI_DDD_ASYNC_EVENT_POLL_US);
+ } else {
+ timeout.tv_sec = 0;
+ timeout.tv_usec = GUI_DDD_ASYNC_EVENT_POLL_US;
+ rc = libusb_handle_events_timeout_completed(
+ engine->config->usb_context, &timeout, NULL);
+ }
+ if (rc < 0 && rc != LIBUSB_ERROR_INTERRUPTED) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_EVENT_FAILURE, rc, 0, 0,
+ engine->order.next_consume_id);
+ return -1;
+ }
+ return 0;
+}
+
+static int gui_ddd_async_allocate(gui_ddd_async_engine_t *engine)
+{
+ size_t i;
+
+ engine->slots = (gui_ddd_async_slot_t *)calloc(
+ GUI_DDD_ASYNC_TRANSFER_COUNT, sizeof(*engine->slots));
+ engine->buffer_pool = (uint8_t *)malloc(GUI_DDD_ASYNC_QUEUE_BYTES);
+ if (!engine->slots || !engine->buffer_pool) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_ALLOCATION_FAILURE,
+ LIBUSB_ERROR_NO_MEM, 0, 0, 0);
+ return -1;
+ }
+
+ for (i = 0; i < GUI_DDD_ASYNC_TRANSFER_COUNT; i++) {
+ gui_ddd_async_slot_t *slot = &engine->slots[i];
+ slot->owner = engine;
+ slot->slot_index = i;
+ slot->buffer = engine->buffer_pool +
+ i * GUI_DDD_ASYNC_TRANSFER_BYTES;
+ slot->transfer = libusb_alloc_transfer(0);
+ if (!slot->transfer) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_ALLOCATION_FAILURE,
+ LIBUSB_ERROR_NO_MEM, 0, 0, i);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void gui_ddd_async_destroy(gui_ddd_async_engine_t *engine)
+{
+ size_t i;
+
+ 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 (engine->slots) {
+ for (i = 0; i < GUI_DDD_ASYNC_TRANSFER_COUNT; i++) {
+ if (engine->slots[i].transfer) {
+ libusb_free_transfer(engine->slots[i].transfer);
+ }
+ }
+ }
+ free(engine->buffer_pool);
+ free(engine->slots);
+ engine->buffer_pool = NULL;
+ engine->slots = NULL;
+ free(engine);
+}
+
+int gui_ddd_async_run(const gui_ddd_async_config_t *config,
+ gui_ddd_async_result_t *result)
+{
+ gui_ddd_async_engine_t *engine;
+ gui_ddd_async_result_t local_result;
+ uint64_t stop_deadline_ms = 0;
+ size_t i;
+
+ memset(&local_result, 0, sizeof(local_result));
+ local_result.code = GUI_DDD_ASYNC_RESULT_INVALID_ARGUMENT;
+
+ if (!result || !config || !config->usb_context || !config->device_handle ||
+ !config->capture_running || !config->transfer_ready ||
+ !config->startup_failed || !config->consume) {
+ if (config && config->transfer_ready) {
+ atomic_store(config->transfer_ready, false);
+ }
+ if (config && config->startup_failed) {
+ atomic_store(config->startup_failed, true);
+ }
+ if (result) *result = local_result;
+ return -1;
+ }
+
+ atomic_store(config->transfer_ready, false);
+ atomic_store(config->startup_failed, false);
+
+ engine = (gui_ddd_async_engine_t *)calloc(1, sizeof(*engine));
+ if (!engine) {
+ local_result.code = GUI_DDD_ASYNC_RESULT_ALLOCATION_FAILURE;
+ local_result.libusb_error = LIBUSB_ERROR_NO_MEM;
+ atomic_store(config->startup_failed, true);
+ *result = local_result;
+ return -1;
+ }
+ engine->config_storage = *config;
+ engine->config = &engine->config_storage;
+ engine->result.code = GUI_DDD_ASYNC_RESULT_SUCCESS;
+ engine->accepting_submissions = true;
+ atomic_init(&engine->in_flight, 0);
+ atomic_init(&engine->callbacks_active, 0);
+ gui_ddd_async_order_policy_init(&engine->order,
+ GUI_DDD_ASYNC_TRANSFER_COUNT);
+
+ if (gui_ddd_async_allocate(engine) < 0) goto finished;
+
+ for (i = 0; i < GUI_DDD_ASYNC_TRANSFER_COUNT; i++) {
+ if (gui_ddd_async_submit_slot(engine, &engine->slots[i]) < 0) {
+ break;
+ }
+ }
+
+ if (gui_ddd_async_policy_initial_queue_ready(engine->submitted_count,
+ engine->failed)) {
+ engine->result.ready_signalled = true;
+ atomic_store(config->transfer_ready, true);
+ } else if (!engine->failed) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_SUBMIT_FAILURE,
+ LIBUSB_ERROR_OTHER, 0, 0, engine->next_submit_id);
+ }
+
+ if (engine->failed) gui_ddd_async_cancel_in_flight(engine);
+
+ for (;;) {
+ bool capture_running = atomic_load(config->capture_running);
+ uint64_t now_ms = gui_ddd_async_now_ms(engine);
+ size_t 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;
+ }
+
+ if (!engine->failed &&
+ gui_ddd_async_consume_ready(engine) < 0) {
+ gui_ddd_async_cancel_in_flight(engine);
+ }
+
+ in_flight = atomic_load(&engine->in_flight);
+ if (engine->failed) {
+ gui_ddd_async_cancel_in_flight(engine);
+ } else if (engine->stopping && in_flight > 0 &&
+ now_ms >= stop_deadline_ms) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_DRAIN_TIMEOUT, 0, 0, 0,
+ engine->order.next_consume_id);
+ gui_ddd_async_cancel_in_flight(engine);
+ }
+
+ in_flight = atomic_load(&engine->in_flight);
+ if (engine->failed && 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)) {
+ break;
+ }
+ if (gui_ddd_async_policy_reap_action(
+ engine->cancel_issued,
+ now_ms,
+ engine->cancel_deadline_ms,
+ in_flight) == GUI_DDD_ASYNC_REAP_ORPHAN) {
+ break;
+ }
+ if (!engine->failed && capture_running && in_flight == 0) {
+ gui_ddd_async_latch_failure(
+ engine, GUI_DDD_ASYNC_RESULT_ORDER_FAILURE, 0, 0, 0,
+ engine->order.next_consume_id);
+ gui_ddd_async_cancel_in_flight(engine);
+ continue;
+ }
+
+ if (gui_ddd_async_pump_events(engine) < 0) {
+ gui_ddd_async_cancel_in_flight(engine);
+ thrd_sleep_ms(1);
+ }
+ }
+
+finished:
+ atomic_store(config->transfer_ready, false);
+ if (gui_ddd_async_engine_has_pending(engine)) {
+ engine->result.transfers_unreaped = true;
+ engine->result.unreaped_transfers =
+ atomic_load(&engine->in_flight);
+ engine->result.active_callbacks =
+ atomic_load(&engine->callbacks_active);
+ engine->result.orphan = engine;
+ /* The capture-thread stack and gui_app may go away after return.
+ * Future callbacks may only touch heap-owned engine/slot state. */
+ gui_ddd_async_detach_external_context(&engine->config_storage);
+ *result = engine->result;
+ return -1;
+ }
+
+ local_result = engine->result;
+ gui_ddd_async_destroy(engine);
+ *result = local_result;
+ return local_result.code == GUI_DDD_ASYNC_RESULT_SUCCESS ? 0 : -1;
+}
+
+bool gui_ddd_async_orphan_has_unreaped(
+ const gui_ddd_async_orphan_t *orphan)
+{
+ return gui_ddd_async_engine_has_pending(orphan);
+}
+
+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;
+ gui_ddd_async_destroy(orphan);
+ return true;
+}
+
+bool gui_ddd_async_orphan_abandon(gui_ddd_async_orphan_t *orphan)
+{
+ if (!orphan) return false;
+ if (!gui_ddd_async_engine_has_pending(orphan)) {
+ gui_ddd_async_destroy(orphan);
+ return true;
+ }
+ /* One process-global slot is intentional: after the first unreaped queue,
+ * gui_ddd refuses every further open so retained USB ownership is bounded. */
+ if (gui_ddd_async_policy_abandon_slot_available(
+ s_ddd_async_abandoned ? 1u : 0u)) {
+ s_ddd_async_abandoned = orphan;
+ orphan->abandoned = true;
+ return true;
+ }
+ return s_ddd_async_abandoned == orphan;
+}
+
+bool gui_ddd_async_global_quarantine_active(void)
+{
+ return s_ddd_async_abandoned != NULL;
+}
+
+const char *gui_ddd_async_result_name(gui_ddd_async_result_code_t code)
+{
+ switch (code) {
+ case GUI_DDD_ASYNC_RESULT_SUCCESS: return "Success";
+ case GUI_DDD_ASYNC_RESULT_INVALID_ARGUMENT: return "InvalidArgument";
+ case GUI_DDD_ASYNC_RESULT_ALLOCATION_FAILURE: return "AllocationFailure";
+ case GUI_DDD_ASYNC_RESULT_SUBMIT_FAILURE: return "SubmitFailure";
+ case GUI_DDD_ASYNC_RESULT_TRANSFER_FAILURE: return "TransferFailure";
+ case GUI_DDD_ASYNC_RESULT_SHORT_TRANSFER: return "ShortTransfer";
+ case GUI_DDD_ASYNC_RESULT_EVENT_FAILURE: return "EventFailure";
+ case GUI_DDD_ASYNC_RESULT_DRAIN_TIMEOUT: return "DrainTimeout";
+ case GUI_DDD_ASYNC_RESULT_ORDER_FAILURE: return "OrderFailure";
+ case GUI_DDD_ASYNC_RESULT_CONSUMER_FAILURE: return "ConsumerFailure";
+ default: return "Unknown";
+ }
+}
diff --git a/misrc_tools/misrc_gui/input/gui_ddd_async.h b/misrc_tools/misrc_gui/input/gui_ddd_async.h
new file mode 100644
index 0000000..ca71fad
--- /dev/null
+++ b/misrc_tools/misrc_gui/input/gui_ddd_async.h
@@ -0,0 +1,316 @@
+/*
+ * MISRC GUI - DDD firmware 3.1 asynchronous USB capture queue
+ *
+ * This interface is intentionally independent from gui_app_t. It owns all
+ * libusb transfers and their backing storage for one capture-thread run, and
+ * delivers exact 128 KiB blocks to the caller in submission order. The
+ * smaller completion unit limits the device-FIFO exposure between completion
+ * and resubmission while retaining a 12 MiB in-flight queue.
+ */
+
+#ifndef GUI_DDD_ASYNC_H
+#define GUI_DDD_ASYNC_H
+
+#include
+#include
+#include
+#include
+
+struct libusb_context;
+struct libusb_device_handle;
+struct libusb_transfer;
+
+#define GUI_DDD_ASYNC_TRANSFER_BYTES ((size_t)128 * 1024)
+#define GUI_DDD_ASYNC_QUEUE_BYTES ((size_t)12 * 1024 * 1024)
+#define GUI_DDD_ASYNC_TRANSFER_COUNT \
+ (GUI_DDD_ASYNC_QUEUE_BYTES / GUI_DDD_ASYNC_TRANSFER_BYTES)
+#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
+
+typedef struct gui_ddd_async_orphan gui_ddd_async_orphan_t;
+
+typedef enum {
+ GUI_DDD_ASYNC_SLOT_UNUSED = 0,
+ GUI_DDD_ASYNC_SLOT_SUBMITTED,
+ GUI_DDD_ASYNC_SLOT_COMPLETE,
+ GUI_DDD_ASYNC_SLOT_CANCELLED,
+ GUI_DDD_ASYNC_SLOT_FAILED,
+ GUI_DDD_ASYNC_SLOT_RETIRED,
+} gui_ddd_async_slot_state_t;
+
+/* Small dependency-free ordering model shared by runtime code and unit tests. */
+typedef struct {
+ uint64_t submission_id;
+ gui_ddd_async_slot_state_t state;
+} gui_ddd_async_policy_slot_t;
+
+typedef struct {
+ uint64_t next_consume_id;
+ size_t slot_count;
+} gui_ddd_async_order_policy_t;
+
+typedef enum {
+ GUI_DDD_ASYNC_NEXT_WAIT = 0,
+ GUI_DDD_ASYNC_NEXT_READY,
+ GUI_DDD_ASYNC_NEXT_STALE,
+} gui_ddd_async_next_state_t;
+
+static inline void gui_ddd_async_order_policy_init(
+ gui_ddd_async_order_policy_t *policy,
+ size_t slot_count)
+{
+ if (!policy) return;
+ policy->next_consume_id = 0;
+ policy->slot_count = slot_count;
+}
+
+static inline gui_ddd_async_next_state_t gui_ddd_async_order_policy_peek(
+ const gui_ddd_async_order_policy_t *policy,
+ const gui_ddd_async_policy_slot_t *slots,
+ size_t *slot_index)
+{
+ size_t index;
+ const gui_ddd_async_policy_slot_t *slot;
+
+ if (!policy || !slots || policy->slot_count == 0) {
+ return GUI_DDD_ASYNC_NEXT_STALE;
+ }
+ index = (size_t)(policy->next_consume_id % policy->slot_count);
+ slot = &slots[index];
+ if (slot->submission_id != policy->next_consume_id) {
+ return GUI_DDD_ASYNC_NEXT_STALE;
+ }
+ if (slot->state != GUI_DDD_ASYNC_SLOT_COMPLETE) {
+ return GUI_DDD_ASYNC_NEXT_WAIT;
+ }
+ if (slot_index) *slot_index = index;
+ return GUI_DDD_ASYNC_NEXT_READY;
+}
+
+static inline void gui_ddd_async_order_policy_advance(
+ gui_ddd_async_order_policy_t *policy)
+{
+ if (policy) policy->next_consume_id++;
+}
+
+static inline bool gui_ddd_async_policy_initial_queue_ready(
+ size_t submitted_count,
+ bool failed)
+{
+ return !failed && submitted_count == GUI_DDD_ASYNC_TRANSFER_COUNT;
+}
+
+/* Submitting the USB queue only establishes transport ownership. Recording is
+ * safe after the startup discard has finished and one validated RF block has
+ * been published, while the queue and capture lifetime are still active. */
+static inline bool gui_ddd_async_policy_stream_ready(
+ bool queue_ready,
+ bool verified_block_published,
+ bool capture_running,
+ bool startup_failed)
+{
+ return queue_ready && verified_block_published && capture_running &&
+ !startup_failed;
+}
+
+static inline bool gui_ddd_async_policy_should_resubmit(
+ bool accepting_submissions,
+ bool capture_running,
+ bool failed)
+{
+ return accepting_submissions && capture_running && !failed;
+}
+
+/* Once capture has stopped, every submitted callback has completed, and every
+ * completed block has been consumed, the next ring generation intentionally
+ * does not exist. That terminal gap is a successful drain, not stale-order
+ * corruption. */
+static inline bool gui_ddd_async_policy_stop_drain_complete(
+ bool capture_running,
+ size_t in_flight,
+ uint64_t completed_transfers,
+ uint64_t consumed_transfers)
+{
+ return !capture_running && in_flight == 0 &&
+ completed_transfers == consumed_transfers;
+}
+
+typedef enum {
+ GUI_DDD_ASYNC_REAP_WAIT = 0,
+ GUI_DDD_ASYNC_REAP_COMPLETE,
+ GUI_DDD_ASYNC_REAP_ORPHAN,
+} gui_ddd_async_reap_action_t;
+
+/* Dependency-free cancellation/reap controller. Tests inject a permanently
+ * failing event pump by advancing now_ms without decrementing in_flight. */
+static inline gui_ddd_async_reap_action_t gui_ddd_async_policy_reap_action(
+ bool cancel_started,
+ uint64_t now_ms,
+ uint64_t cancel_deadline_ms,
+ size_t in_flight)
+{
+ if (in_flight == 0) return GUI_DDD_ASYNC_REAP_COMPLETE;
+ if (cancel_started && now_ms >= cancel_deadline_ms) {
+ return GUI_DDD_ASYNC_REAP_ORPHAN;
+ }
+ return GUI_DDD_ASYNC_REAP_WAIT;
+}
+
+static inline bool gui_ddd_async_policy_has_pending(
+ size_t in_flight,
+ size_t callbacks_active)
+{
+ return in_flight != 0 || callbacks_active != 0;
+}
+
+/* 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. */
+static inline bool gui_ddd_async_policy_sync_control_allowed(
+ bool orphan_pending)
+{
+ return !orphan_pending;
+}
+
+static inline bool gui_ddd_async_policy_exact_length(size_t actual_length)
+{
+ return actual_length == GUI_DDD_ASYNC_TRANSFER_BYTES;
+}
+
+typedef enum {
+ GUI_DDD_ASYNC_CONSUME_CONTINUE = 0,
+ GUI_DDD_ASYNC_CONSUME_FAILED,
+} gui_ddd_async_consume_result_t;
+
+typedef gui_ddd_async_consume_result_t (*gui_ddd_async_consume_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. */
+typedef int (*gui_ddd_async_event_pump_fn)(void *context,
+ long timeout_us);
+typedef uint64_t (*gui_ddd_async_now_ms_fn)(void *context);
+typedef int (*gui_ddd_async_transfer_action_fn)(
+ void *context,
+ struct libusb_transfer *transfer);
+
+typedef enum {
+ GUI_DDD_ASYNC_RESULT_SUCCESS = 0,
+ GUI_DDD_ASYNC_RESULT_INVALID_ARGUMENT,
+ GUI_DDD_ASYNC_RESULT_ALLOCATION_FAILURE,
+ GUI_DDD_ASYNC_RESULT_SUBMIT_FAILURE,
+ GUI_DDD_ASYNC_RESULT_TRANSFER_FAILURE,
+ GUI_DDD_ASYNC_RESULT_SHORT_TRANSFER,
+ GUI_DDD_ASYNC_RESULT_EVENT_FAILURE,
+ GUI_DDD_ASYNC_RESULT_DRAIN_TIMEOUT,
+ GUI_DDD_ASYNC_RESULT_ORDER_FAILURE,
+ GUI_DDD_ASYNC_RESULT_CONSUMER_FAILURE,
+} gui_ddd_async_result_code_t;
+
+typedef struct {
+ gui_ddd_async_result_code_t code;
+ int libusb_error;
+ int transfer_status;
+ int actual_length;
+ uint64_t submission_id;
+ uint64_t completed_transfers;
+ uint64_t consumed_transfers;
+ bool ready_signalled;
+ bool transfers_unreaped;
+ size_t unreaped_transfers;
+ size_t active_callbacks;
+ gui_ddd_async_orphan_t *orphan;
+} gui_ddd_async_result_t;
+
+typedef struct {
+ struct libusb_context *usb_context;
+ struct libusb_device_handle *device_handle;
+ uint8_t endpoint;
+ atomic_bool *capture_running;
+ atomic_bool *transfer_ready;
+ atomic_bool *startup_failed;
+ gui_ddd_async_consume_fn consume;
+ void *consume_context;
+ gui_ddd_async_event_pump_fn event_pump_override;
+ void *event_pump_context;
+ gui_ddd_async_now_ms_fn now_ms_override;
+ void *now_ms_context;
+ gui_ddd_async_transfer_action_fn submit_override;
+ void *submit_context;
+ gui_ddd_async_transfer_action_fn cancel_override;
+ void *cancel_context;
+} gui_ddd_async_config_t;
+
+/* Converts a capture-thread config into callback-safe orphan state. Raw USB
+ * identity is retained, while every pointer into caller/thread stack state and
+ * every test hook is cleared. */
+static inline void gui_ddd_async_detach_external_context(
+ gui_ddd_async_config_t *config)
+{
+ if (!config) return;
+ config->capture_running = NULL;
+ config->transfer_ready = NULL;
+ config->startup_failed = NULL;
+ config->consume = NULL;
+ config->consume_context = NULL;
+ config->event_pump_override = NULL;
+ config->event_pump_context = NULL;
+ config->now_ms_override = NULL;
+ config->now_ms_context = NULL;
+ config->submit_override = NULL;
+ config->submit_context = NULL;
+ config->cancel_override = NULL;
+ config->cancel_context = NULL;
+}
+
+static inline bool gui_ddd_async_policy_abandon_slot_available(
+ size_t abandoned_count)
+{
+ return abandoned_count < GUI_DDD_ASYNC_ABANDONED_CAPACITY;
+}
+
+/*
+ * Run one queue lifetime synchronously on the calling capture thread. Usually
+ * returns after every callback is reaped. If cancellation callbacks cannot be
+ * reaped within the bounded deadline, result->orphan owns every pending
+ * transfer and backing buffer and retains the raw USB identity. The caller
+ * must retain the underlying USB interface/handle/context (the engine does
+ * not add libusb references) and either reclaim or abandon the engine.
+ * result is required so orphan ownership can never be dropped silently.
+ *
+ * The supplied usb_context is exclusively owned by this DDD capture while
+ * run is active: no second event handler and no synchronous libusb operation
+ * may use it. Callback state is intentionally non-atomic apart from the
+ * lifetime counters and relies on that single event-pump thread. After run
+ * returns an orphan, capture must be joined before ownership is inspected.
+ * Query/reclaim only after any event-pump call that could dispatch callbacks
+ * has returned. In particular, synchronous recovery is forbidden while
+ * orphan_has_unreaped is true.
+ */
+int gui_ddd_async_run(const gui_ddd_async_config_t *config,
+ gui_ddd_async_result_t *result);
+
+bool gui_ddd_async_orphan_has_unreaped(
+ const gui_ddd_async_orphan_t *orphan);
+
+/* Frees a formerly orphaned engine only when all callbacks were subsequently
+ * delivered by an explicitly serialized event pump. Call only after that
+ * event-pump function has returned. Returns true iff ownership was consumed
+ * and the pointer must be discarded. */
+bool gui_ddd_async_orphan_try_reclaim(gui_ddd_async_orphan_t *orphan);
+
+/* Explicitly retain an unreaped engine until process exit. The caller must
+ * likewise transfer USB lifetime by not releasing its handle/interface/context. */
+bool gui_ddd_async_orphan_abandon(gui_ddd_async_orphan_t *orphan);
+
+/* The first process-lifetime orphan globally blocks further DDD opens. This
+ * bounds retained async queues to GUI_DDD_ASYNC_ABANDONED_CAPACITY. */
+bool gui_ddd_async_global_quarantine_active(void);
+
+const char *gui_ddd_async_result_name(gui_ddd_async_result_code_t code);
+
+#endif /* GUI_DDD_ASYNC_H */
diff --git a/misrc_tools/misrc_gui/input/gui_ddd_v1.c b/misrc_tools/misrc_gui/input/gui_ddd_v1.c
new file mode 100644
index 0000000..23b2b6e
--- /dev/null
+++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.c
@@ -0,0 +1,716 @@
+/* MISRC GUI - isolated Domesday Duplicator firmware 3.1 backend. */
+
+#ifdef ENABLE_DDD
+
+#include
+#include
+#include
+#include
+#include
+
+#if defined(_WIN32)
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#ifndef NOGDI
+#define NOGDI
+#endif
+#ifndef NOUSER
+#define NOUSER
+#endif
+#include "../../common/libusb_compat.h"
+#undef WIN32_LEAN_AND_MEAN
+#undef NOGDI
+#undef NOUSER
+#else
+#include "../../common/libusb_compat.h"
+#endif
+
+#include "gui_ddd_v1.h"
+#include "gui_ddd.h"
+#include "gui_ddd_async.h"
+#include "../core/gui_app.h"
+#include "../output/gui_record.h"
+#include "../processing/gui_display_thread.h"
+#include "../processing/gui_extract.h"
+#include "../../common/buffer_manager.h"
+#include "../../common/threading.h"
+
+#define DDD_V1_CONTROL_TIMEOUT_MS 1000
+#define DDD_V1_STARTUP_DISCARD_BYTES (UINT64_C(8) * 1024u * 1024u)
+
+typedef enum {
+ DDD_V1_RESULT_RUNNING = 0,
+ DDD_V1_RESULT_SUCCESS,
+ DDD_V1_RESULT_USB_FAILURE,
+ DDD_V1_RESULT_SEQUENCE_FAILURE,
+ DDD_V1_RESULT_TEST_FAILURE,
+ DDD_V1_RESULT_BACKPRESSURE
+} ddd_v1_capture_result_t;
+
+typedef enum {
+ DDD_V1_LOCK_NONE = 0,
+ DDD_V1_LOCK_WAIT_DISAPPEARANCE,
+ DDD_V1_LOCK_WAIT_REAPPEARANCE
+} ddd_v1_lock_phase_t;
+
+typedef struct {
+ gui_app_t *app;
+ uint64_t discarded_bytes;
+ uint64_t published_blocks;
+} ddd_v1_consumer_t;
+
+static libusb_context *s_context;
+static libusb_device_handle *s_handle;
+static bool s_interface_claimed;
+static atomic_bool s_queue_ready = ATOMIC_VAR_INIT(false);
+static atomic_bool s_startup_failed = ATOMIC_VAR_INIT(false);
+static gui_ddd_async_orphan_t *s_orphan;
+static ddd_collection_state_t s_collection;
+static ddd_sequence_validator_t s_sequence;
+static ddd_test_ramp_validator_t s_test_ramp;
+static bool s_test_mode;
+static uint8_t s_decimation = DDD_DECIMATION_FULL_RATE;
+static uint32_t s_sample_rate_hz = DDD_CONVERTER_SAMPLE_RATE_HZ;
+static char s_usb_path[DDD_STABLE_ID_MAX];
+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];
+
+_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");
+_Static_assert(DDD_V1_STARTUP_DISCARD_BYTES %
+ GUI_DDD_ASYNC_TRANSFER_BYTES == 0,
+ "DDD 3.1 startup discard must contain whole transfers");
+
+static const char *ddd_v1_result_name(ddd_v1_capture_result_t result)
+{
+ switch (result) {
+ case DDD_V1_RESULT_RUNNING: return "Running";
+ case DDD_V1_RESULT_SUCCESS: return "Success";
+ case DDD_V1_RESULT_USB_FAILURE: return "UsbFailure";
+ case DDD_V1_RESULT_SEQUENCE_FAILURE: return "SequenceFailure";
+ case DDD_V1_RESULT_TEST_FAILURE: return "TestFailure";
+ case DDD_V1_RESULT_BACKPRESSURE: return "Backpressure";
+ }
+ return "Unknown";
+}
+
+static const char *ddd_v1_protocol_result_name(ddd_protocol_result_t result)
+{
+ switch (result) {
+ case DDD_PROTOCOL_OK: return "OK";
+ case DDD_PROTOCOL_INVALID_ARGUMENT: return "InvalidArgument";
+ case DDD_PROTOCOL_UNSUPPORTED_PROFILE: return "UnsupportedProfile";
+ case DDD_PROTOCOL_UNSUPPORTED_DECIMATION: return "UnsupportedDecimation";
+ case DDD_PROTOCOL_CONTROL_FAILURE: return "ControlFailure";
+ case DDD_PROTOCOL_IDENTITY_MISMATCH: return "IdentityMismatch";
+ case DDD_PROTOCOL_READBACK_MISMATCH: return "ReadbackMismatch";
+ }
+ return "Unknown";
+}
+
+static bool ddd_v1_format_usb_path(libusb_device *device,
+ char *path,
+ size_t path_size)
+{
+ uint8_t ports[8];
+ int port_count;
+
+ if (!device || !path || path_size == 0) return false;
+ port_count = libusb_get_port_numbers(device, ports, (int)sizeof(ports));
+ return ddd_format_usb_topology_path(libusb_get_bus_number(device), ports,
+ port_count, path, path_size);
+}
+
+static bool ddd_v1_find_exact_endpoint(libusb_device *device)
+{
+ struct libusb_config_descriptor *config = NULL;
+ ddd_stream_selector_t selector;
+ ddd_stream_path_t selected;
+ int result = libusb_get_active_config_descriptor(device, &config);
+
+ if (result != 0 || !config) {
+ result = libusb_get_config_descriptor(device, 0, &config);
+ }
+ if (result != 0 || !config) return false;
+ ddd_stream_selector_init(&selector, DDD_DEVICE_PROTOCOL_V1);
+ for (int i = 0; i < config->bNumInterfaces; ++i) {
+ const struct libusb_interface *interface = &config->interface[i];
+ for (int j = 0; j < interface->num_altsetting; ++j) {
+ const struct libusb_interface_descriptor *alternate =
+ &interface->altsetting[j];
+ for (int k = 0; k < alternate->bNumEndpoints; ++k) {
+ const struct libusb_endpoint_descriptor *endpoint =
+ &alternate->endpoint[k];
+ ddd_stream_endpoint_candidate_t candidate = {
+ .interface_number = alternate->bInterfaceNumber,
+ .alternate_setting = alternate->bAlternateSetting,
+ .endpoint_address = endpoint->bEndpointAddress,
+ .max_packet_size = endpoint->wMaxPacketSize,
+ .is_bulk = (endpoint->bmAttributes & 0x03) ==
+ LIBUSB_TRANSFER_TYPE_BULK,
+ .is_in = (endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0
+ };
+ ddd_stream_selector_consider(&selector, &candidate);
+ }
+ }
+ }
+ libusb_free_config_descriptor(config);
+ return ddd_stream_selector_get(&selector, &selected);
+}
+
+static int ddd_v1_control_transfer(void *context,
+ uint8_t request_type,
+ uint8_t request,
+ uint16_t value,
+ uint16_t index,
+ uint8_t *data,
+ uint16_t length)
+{
+ return libusb_control_transfer((libusb_device_handle *)context,
+ request_type, request, value, index,
+ data, length, DDD_V1_CONTROL_TIMEOUT_MS);
+}
+
+static ddd_control_ops_t ddd_v1_control_ops(void)
+{
+ ddd_control_ops_t ops = {
+ .transfer = ddd_v1_control_transfer,
+ .context = s_handle
+ };
+ return ops;
+}
+
+static void ddd_v1_lock_active_path(const char *reason)
+{
+ if (s_usb_path[0]) {
+ snprintf(s_locked_path, sizeof(s_locked_path), "%s", s_usb_path);
+ s_lock_phase = DDD_V1_LOCK_WAIT_DISAPPEARANCE;
+ }
+ fprintf(stderr, "[DdD 3.1] Safety lock for %s: %s\n",
+ s_locked_path[0] ? s_locked_path : "unknown path",
+ reason ? reason : "unverified device state");
+}
+
+void gui_ddd_v1_observe_enumeration(const misrc_device_list_t *devices)
+{
+ bool present = false;
+
+ if (!devices || !devices->ddd_enumeration_complete ||
+ s_lock_phase == DDD_V1_LOCK_NONE || !s_locked_path[0]) {
+ return;
+ }
+ for (size_t i = 0; i < devices->count; ++i) {
+ const misrc_device_info_t *device = &devices->devices[i];
+ if (device->type == MISRC_DEVICE_TYPE_DDD &&
+ device->ddd_profile == DDD_DEVICE_PROTOCOL_V1 &&
+ strcmp(device->ddd_usb_path, s_locked_path) == 0) {
+ present = true;
+ break;
+ }
+ }
+ if (s_lock_phase == DDD_V1_LOCK_WAIT_DISAPPEARANCE && !present) {
+ s_lock_phase = DDD_V1_LOCK_WAIT_REAPPEARANCE;
+ fprintf(stderr, "[DdD 3.1] Safety lock observed unplug for %s\n",
+ s_locked_path);
+ } else if (s_lock_phase == DDD_V1_LOCK_WAIT_REAPPEARANCE && present) {
+ fprintf(stderr, "[DdD 3.1] Safety lock cleared after replug for %s\n",
+ s_locked_path);
+ s_lock_phase = DDD_V1_LOCK_NONE;
+ s_locked_path[0] = '\0';
+ }
+}
+
+static bool ddd_v1_path_locked(const char *path)
+{
+ return path && s_lock_phase != DDD_V1_LOCK_NONE &&
+ strcmp(path, s_locked_path) == 0;
+}
+
+static bool ddd_v1_sync_control_allowed(void)
+{
+ if (s_orphan && gui_ddd_async_orphan_has_unreaped(s_orphan)) return false;
+ if (s_orphan) {
+ if (!gui_ddd_async_orphan_try_reclaim(s_orphan)) return false;
+ s_orphan = NULL;
+ }
+ return true;
+}
+
+static void ddd_v1_close(void)
+{
+ bool retain_usb = false;
+
+ if (s_orphan) {
+ if (gui_ddd_async_orphan_try_reclaim(s_orphan)) {
+ s_orphan = NULL;
+ } else if (gui_ddd_async_orphan_abandon(s_orphan)) {
+ s_orphan = NULL;
+ retain_usb = true;
+ } else {
+ return;
+ }
+ }
+ if (!retain_usb && s_handle) {
+ if (s_interface_claimed) {
+ libusb_release_interface(s_handle, DDD_STREAM_INTERFACE_NUMBER);
+ }
+ libusb_close(s_handle);
+ }
+ if (!retain_usb && s_context) libusb_exit(s_context);
+ s_handle = NULL;
+ s_context = NULL;
+ s_interface_claimed = false;
+ s_usb_path[0] = '\0';
+ ddd_collection_state_init(&s_collection);
+}
+
+int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path)
+{
+ libusb_device **devices = NULL;
+ libusb_device *selected = NULL;
+ ssize_t device_count;
+ size_t match_count = 0;
+ int result;
+
+ if (!app) return -1;
+ if (!stable_usb_path || !stable_usb_path[0]) {
+ gui_app_set_status(app, "Selected DdD USB path is unavailable");
+ return -1;
+ }
+ if (gui_ddd_async_global_quarantine_active()) {
+ gui_app_set_status(app, "DdD USB safety lock active; restart MISRC");
+ return -1;
+ }
+ if (ddd_v1_path_locked(stable_usb_path)) {
+ gui_app_set_status(app, "DdD safety lock active; unplug and reconnect it");
+ return -1;
+ }
+#if LIBUSB_API_VERSION >= 0x0100010A
+ result = libusb_init_context(&s_context, NULL, 0);
+#else
+ result = libusb_init(&s_context);
+#endif
+ if (result != 0) {
+ gui_app_set_status(app, "Failed to initialize DdD USB access");
+ return -1;
+ }
+ device_count = libusb_get_device_list(s_context, &devices);
+ if (device_count < 0) {
+ gui_app_set_status(app, "Failed to enumerate DdD USB devices");
+ ddd_v1_close();
+ return -1;
+ }
+ for (ssize_t i = 0; i < device_count; ++i) {
+ struct libusb_device_descriptor descriptor;
+ char candidate_path[DDD_STABLE_ID_MAX];
+ if (libusb_get_device_descriptor(devices[i], &descriptor) != 0 ||
+ ddd_classify_device(descriptor.idVendor, descriptor.idProduct,
+ descriptor.bcdDevice) !=
+ DDD_DEVICE_PROTOCOL_V1 ||
+ !ddd_v1_format_usb_path(devices[i], candidate_path,
+ sizeof(candidate_path)) ||
+ strcmp(candidate_path, stable_usb_path) != 0) {
+ continue;
+ }
+ selected = devices[i];
+ ++match_count;
+ }
+ if (match_count != 1 || !selected) {
+ fprintf(stderr, "[DdD 3.1] Exact USB path match count was %zu\n",
+ match_count);
+ gui_app_set_status(app,
+ "Selected DdD device is no longer at its USB path");
+ libusb_free_device_list(devices, 1);
+ ddd_v1_close();
+ return -1;
+ }
+ {
+ enum libusb_speed speed = libusb_get_device_speed(selected);
+ if (!ddd_v1_link_speed_allowed(speed != LIBUSB_SPEED_UNKNOWN,
+ speed >= LIBUSB_SPEED_SUPER)) {
+ gui_app_set_status(app, "DdD requires USB 3 SuperSpeed");
+ libusb_free_device_list(devices, 1);
+ ddd_v1_close();
+ return -1;
+ }
+ }
+ if (!ddd_v1_find_exact_endpoint(selected)) {
+ gui_app_set_status(app, "DdD USB stream descriptor mismatch");
+ libusb_free_device_list(devices, 1);
+ ddd_v1_close();
+ return -1;
+ }
+ result = libusb_open(selected, &s_handle);
+ libusb_free_device_list(devices, 1);
+ if (result != 0 || !s_handle) {
+ gui_app_set_status(app, "Failed to open DdD device");
+ ddd_v1_close();
+ return -1;
+ }
+#if LIBUSB_API_VERSION >= 0x01000106
+ (void)libusb_set_auto_detach_kernel_driver(s_handle, 1);
+#endif
+ result = libusb_claim_interface(s_handle, DDD_STREAM_INTERFACE_NUMBER);
+ if (result != 0) {
+ gui_app_set_status(app, "Failed to claim DdD USB interface");
+ ddd_v1_close();
+ return -1;
+ }
+ s_interface_claimed = true;
+ snprintf(s_usb_path, sizeof(s_usb_path), "%s", stable_usb_path);
+ fprintf(stderr, "[DdD 3.1] Opened exact device path %s\n", s_usb_path);
+ return 0;
+}
+
+static gui_ddd_async_consume_result_t ddd_v1_fail(
+ gui_app_t *app,
+ ddd_v1_capture_result_t result,
+ gui_dropout_reason_t reason,
+ const char *message)
+{
+ if (app) {
+ fprintf(stderr, "[DdD 3.1] %s\n", message);
+ gui_record_log_capture_event(app, "ERROR", message,
+ GUI_ERROR_CLASS_SYSTEM, 1);
+ atomic_store(&app->dropout_stop_reason, reason);
+ atomic_store(&app->dropout_stop_requested, true);
+ atomic_store(&app->ddd_running, false);
+ atomic_store(&app->stream_synced, false);
+ }
+ s_result = result;
+ return GUI_DDD_ASYNC_CONSUME_FAILED;
+}
+
+static gui_ddd_async_consume_result_t ddd_v1_consume(
+ void *context, const uint8_t *data, size_t size)
+{
+ ddd_v1_consumer_t *consumer = (ddd_v1_consumer_t *)context;
+ gui_app_t *app = consumer ? consumer->app : NULL;
+ const uint16_t *input;
+ uint32_t *output;
+ size_t sample_count;
+ size_t output_size;
+
+ if (!app || !data || !gui_ddd_async_policy_exact_length(size)) {
+ return ddd_v1_fail(app, DDD_V1_RESULT_USB_FAILURE,
+ GUI_DROPOUT_FRAME_ERROR,
+ "Async USB transfer had an invalid length");
+ }
+ if (consumer->discarded_bytes < DDD_V1_STARTUP_DISCARD_BYTES) {
+ consumer->discarded_bytes += size;
+ atomic_store(&app->last_callback_time_ms, get_time_ms());
+ return GUI_DDD_ASYNC_CONSUME_CONTINUE;
+ }
+ input = (const uint16_t *)data;
+ sample_count = size / sizeof(*input);
+ if (ddd_sequence_validator_feed(&s_sequence, input, sample_count) !=
+ DDD_VALIDATION_OK) {
+ char message[192];
+ snprintf(message, sizeof(message),
+ "Sequence mismatch at sample %llu (expected %u, got %u)",
+ (unsigned long long)s_sequence.error_sample_index,
+ (unsigned)s_sequence.expected_marker,
+ (unsigned)s_sequence.actual_marker);
+ atomic_fetch_add(&app->missed_frame_count, 1);
+ return ddd_v1_fail(app, DDD_V1_RESULT_SEQUENCE_FAILURE,
+ GUI_DROPOUT_MISSED_FRAME, message);
+ }
+ if (s_test_mode &&
+ ddd_test_ramp_validator_feed(&s_test_ramp, input, sample_count) !=
+ DDD_VALIDATION_OK) {
+ char message[192];
+ snprintf(message, sizeof(message),
+ "Test ramp mismatch at sample %llu (expected %u, got %u)",
+ (unsigned long long)s_test_ramp.error_sample_index,
+ (unsigned)s_test_ramp.expected_value,
+ (unsigned)s_test_ramp.actual_value);
+ return ddd_v1_fail(app, DDD_V1_RESULT_TEST_FAILURE,
+ GUI_DROPOUT_FRAME_ERROR, message);
+ }
+ output_size = sample_count * sizeof(*output);
+ output = (uint32_t *)bufmgr_write_begin(&app->buffers, BUF_CAPTURE_RF,
+ output_size, NULL);
+ if (!output) {
+ atomic_fetch_add(&app->rb_drop_count, 1);
+ return ddd_v1_fail(app, DDD_V1_RESULT_BACKPRESSURE,
+ GUI_DROPOUT_BACKPRESSURE,
+ "RF capture buffer backpressure");
+ }
+ for (size_t i = 0; i < sample_count; ++i) {
+ uint32_t sample = input[i] & DDD_SAMPLE_MASK;
+ int32_t signed_sample = (int32_t)sample - 512;
+ if (sample == 0) atomic_fetch_add(&app->clip_count_a_neg, 1);
+ if (sample == DDD_SAMPLE_MASK) atomic_fetch_add(&app->clip_count_a_pos, 1);
+ if (signed_sample >= 0) {
+ uint16_t peak = atomic_load(&app->peak_a_pos);
+ if ((uint16_t)signed_sample > peak) {
+ atomic_store(&app->peak_a_pos, (uint16_t)signed_sample);
+ }
+ } else {
+ uint16_t magnitude = (uint16_t)(-signed_sample);
+ uint16_t peak = atomic_load(&app->peak_a_neg);
+ if (magnitude > peak) atomic_store(&app->peak_a_neg, magnitude);
+ }
+ output[i] = DDD_PACK_12BIT(sample);
+ }
+ bufmgr_write_end(&app->buffers, BUF_CAPTURE_RF, output_size);
+ bufmgr_signal_data(&app->buffers, BUF_CAPTURE_RF);
+ atomic_fetch_add(&app->total_samples, sample_count);
+ atomic_fetch_add(&app->samples_a, sample_count);
+ atomic_store(&app->last_callback_time_ms, get_time_ms());
+ ++consumer->published_blocks;
+ if (consumer->published_blocks == 1) atomic_store(&app->stream_synced, true);
+ return GUI_DDD_ASYNC_CONSUME_CONTINUE;
+}
+
+static int ddd_v1_capture_thread(void *context)
+{
+ gui_app_t *app = (gui_app_t *)context;
+ ddd_v1_consumer_t consumer = {.app = app};
+ gui_ddd_async_config_t config = {
+ .usb_context = s_context,
+ .device_handle = s_handle,
+ .endpoint = DDD_STREAM_ENDPOINT_ADDRESS,
+ .capture_running = &app->ddd_running,
+ .transfer_ready = &s_queue_ready,
+ .startup_failed = &s_startup_failed,
+ .consume = ddd_v1_consume,
+ .consume_context = &consumer
+ };
+ gui_ddd_async_result_t async_result = {0};
+ int result;
+
+ thrd_set_priority(THRD_PRIORITY_CRITICAL);
+ result = gui_ddd_async_run(&config, &async_result);
+ if (async_result.transfers_unreaped && async_result.orphan) {
+ s_orphan = async_result.orphan;
+ }
+ if (result < 0 &&
+ async_result.code != GUI_DDD_ASYNC_RESULT_CONSUMER_FAILURE) {
+ char message[224];
+ snprintf(message, sizeof(message),
+ "Async USB queue failed: %s (usb=%d status=%d length=%d)",
+ gui_ddd_async_result_name(async_result.code),
+ async_result.libusb_error, async_result.transfer_status,
+ async_result.actual_length);
+ if (async_result.ready_signalled) {
+ (void)ddd_v1_fail(app, DDD_V1_RESULT_USB_FAILURE,
+ GUI_DROPOUT_DEVICE_ERROR, message);
+ } else {
+ s_result = DDD_V1_RESULT_USB_FAILURE;
+ fprintf(stderr, "[DdD 3.1] %s\n", message);
+ }
+ }
+ if (s_result == DDD_V1_RESULT_RUNNING) s_result = DDD_V1_RESULT_SUCCESS;
+ return result;
+}
+
+static void ddd_v1_stop_workers(gui_app_t *app, bool display_started)
+{
+ if (display_started && app->display_thread) {
+ gui_display_thread_stop(app->display_thread);
+ }
+ gui_extract_stop();
+}
+
+int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode)
+{
+ ddd_control_ops_t ops;
+ ddd_protocol_result_t protocol_result;
+ bool display_started = false;
+ thrd_t thread;
+
+ if (!app) return -1;
+ if (!s_handle) {
+ gui_app_set_status(app, "DdD device is not open");
+ return -1;
+ }
+ if (!ddd_decimation_is_supported(decimation)) {
+ gui_app_set_status(app, "Invalid DdD hardware rate");
+ return -1;
+ }
+ bufmgr_reset_stats(&app->buffers, BUF_COUNT);
+ atomic_store(&app->total_samples, 0);
+ atomic_store(&app->samples_a, 0);
+ atomic_store(&app->samples_b, 0);
+ atomic_store(&app->frame_count, 0);
+ atomic_store(&app->missed_frame_count, 0);
+ atomic_store(&app->error_count, 0);
+ atomic_store(&app->parser_error_count, 0);
+ atomic_store(&app->system_error_count, 0);
+ atomic_store(&app->error_count_a, 0);
+ atomic_store(&app->error_count_b, 0);
+ atomic_store(&app->clip_count_a_pos, 0);
+ atomic_store(&app->clip_count_a_neg, 0);
+ atomic_store(&app->clip_count_b_pos, 0);
+ atomic_store(&app->clip_count_b_neg, 0);
+ atomic_store(&app->rb_wait_count, 0);
+ atomic_store(&app->rb_drop_count, 0);
+ atomic_store(&app->stream_synced, false);
+ atomic_store(&app->dropout_stop_requested, false);
+ atomic_store(&app->dropout_stop_reason, GUI_DROPOUT_NONE);
+ atomic_store(&s_queue_ready, false);
+ atomic_store(&s_startup_failed, false);
+ s_test_mode = test_mode;
+ s_decimation = decimation;
+ s_sample_rate_hz = ddd_sample_rate_hz(decimation);
+ atomic_store(&app->sample_rate, s_sample_rate_hz);
+ atomic_store(&app->last_callback_time_ms, get_time_ms());
+ app->display_samples_available_a = 0;
+ app->display_samples_available_b = 0;
+ if (bufmgr_ensure_init(&app->buffers, BUF_CAPTURE_RF) != 0) {
+ gui_app_set_status(app, "Failed to initialize DdD capture buffer");
+ ddd_v1_close();
+ return -1;
+ }
+ bufmgr_reset(&app->buffers, BUF_CAPTURE_RF);
+ app->is_capturing = true;
+ if (gui_extract_start(app) < 0) {
+ app->is_capturing = false;
+ gui_app_set_status(app, "Failed to start DdD extraction");
+ ddd_v1_close();
+ return -1;
+ }
+ if (app->display_thread &&
+ gui_display_thread_start(app->display_thread, app, &app->buffers) == 0) {
+ display_started = true;
+ }
+ ops = ddd_v1_control_ops();
+ protocol_result = ddd_collection_start_v1(&ops, test_mode, decimation,
+ &s_collection);
+ if (protocol_result != DDD_PROTOCOL_OK) {
+ bool unsafe = s_collection.rollback_attempted &&
+ !s_collection.rollback_succeeded;
+ fprintf(stderr, "[DdD 3.1] Configuration failed: %s\n",
+ ddd_v1_protocol_result_name(protocol_result));
+ if (unsafe) ddd_v1_lock_active_path("startup rollback failed");
+ app->is_capturing = false;
+ ddd_v1_stop_workers(app, display_started);
+ ddd_v1_close();
+ gui_app_set_status(app, unsafe
+ ? "DdD rollback failed; unplug and reconnect it"
+ : "DdD configuration/readback failed");
+ return -1;
+ }
+ ddd_sequence_validator_init(&s_sequence);
+ ddd_test_ramp_validator_init(&s_test_ramp);
+ s_result = DDD_V1_RESULT_RUNNING;
+ atomic_store(&app->ddd_running, true);
+ if (thrd_create_with_priority(&thread, ddd_v1_capture_thread, app,
+ THRD_PRIORITY_CRITICAL) != thrd_success) {
+ bool rollback_failed;
+ atomic_store(&app->ddd_running, false);
+ app->is_capturing = false;
+ ddd_v1_stop_workers(app, display_started);
+ rollback_failed = ddd_collection_rollback_v1(
+ &ops, &s_collection) != DDD_PROTOCOL_OK;
+ if (rollback_failed) {
+ ddd_v1_lock_active_path("thread-start rollback failed");
+ }
+ ddd_v1_close();
+ gui_app_set_status(app, rollback_failed
+ ? "DdD thread-start rollback failed; unplug and reconnect it"
+ : "Failed to start DdD capture thread");
+ return -1;
+ }
+ app->ddd_thread = (void *)(uintptr_t)thread;
+ for (int i = 0; i < 100; ++i) {
+ if (gui_ddd_async_policy_stream_ready(
+ atomic_load(&s_queue_ready),
+ atomic_load(&app->stream_synced),
+ atomic_load(&app->ddd_running),
+ atomic_load(&s_startup_failed))) {
+ char message[224];
+ char commit[DDD_COMMIT_LENGTH + 7] = {0};
+ (void)ddd_format_gateware_commit(
+ s_collection.identity, sizeof(s_collection.identity),
+ commit, sizeof(commit));
+ snprintf(message, sizeof(message),
+ "DdD protocol v1 capture started (path=%s, %u MSPS, test=%s, gateware=%s)",
+ s_usb_path, (unsigned)(s_sample_rate_hz / 1000000u),
+ test_mode ? "on" : "off", commit[0] ? commit : "n/a");
+ gui_record_log_capture_event(app, "INFO", message,
+ GUI_ERROR_CLASS_NONE, 0);
+ gui_app_set_status(app, "DdD capture running");
+ return 0;
+ }
+ if (atomic_load(&s_startup_failed) ||
+ !atomic_load(&app->ddd_running)) break;
+ thrd_sleep_ms(10);
+ }
+ atomic_store(&app->ddd_running, false);
+ app->is_capturing = false;
+ thrd_join(thread, NULL);
+ app->ddd_thread = NULL;
+ ddd_v1_stop_workers(app, display_started);
+ bool restart_required = false;
+ bool replug_required = false;
+ if (!ddd_v1_sync_control_allowed()) {
+ restart_required = true;
+ ddd_v1_lock_active_path("startup callbacks remained unreaped");
+ } else if (ddd_collection_rollback_v1(&ops, &s_collection) !=
+ DDD_PROTOCOL_OK) {
+ replug_required = true;
+ ddd_v1_lock_active_path("readiness rollback failed");
+ }
+ ddd_v1_close();
+ if (restart_required || gui_ddd_async_global_quarantine_active()) {
+ gui_app_set_status(app, "DdD USB callbacks unverified; restart MISRC");
+ } else {
+ gui_app_set_status(app, replug_required
+ ? "DdD stream cleanup failed; unplug and reconnect it"
+ : "DdD stream did not become ready");
+ }
+ return -1;
+}
+
+void gui_ddd_v1_stop(gui_app_t *app)
+{
+ ddd_control_ops_t ops;
+ bool unsafe = false;
+
+ if (!app || (!s_handle && !atomic_load(&app->ddd_running))) return;
+ atomic_store(&s_queue_ready, false);
+ atomic_store(&app->ddd_running, false);
+ if (app->ddd_thread) {
+ thrd_t thread = (thrd_t)(uintptr_t)app->ddd_thread;
+ thrd_join(thread, NULL);
+ app->ddd_thread = NULL;
+ }
+ app->is_capturing = false;
+ if (app->display_thread) gui_display_thread_stop(app->display_thread);
+ gui_extract_stop();
+ if (!ddd_v1_sync_control_allowed()) {
+ unsafe = true;
+ ddd_v1_lock_active_path("callbacks remained unreaped at stop");
+ } else {
+ ops = ddd_v1_control_ops();
+ if (ddd_collection_stop_v1(&ops, &s_collection) != DDD_PROTOCOL_OK &&
+ ddd_collection_rollback_v1(&ops, &s_collection) !=
+ DDD_PROTOCOL_OK) {
+ unsafe = true;
+ ddd_v1_lock_active_path("B5 stop and rollback failed");
+ }
+ }
+ fprintf(stderr, "[DdD 3.1] Capture stopped: %s\n",
+ ddd_v1_result_name(s_result));
+ ddd_v1_close();
+ atomic_store(&app->stream_synced, false);
+ if (gui_ddd_async_global_quarantine_active()) {
+ gui_app_set_status(app, "DdD USB callbacks unverified; restart MISRC");
+ } else {
+ gui_app_set_status(app, unsafe
+ ? "DdD stop unverified; unplug and reconnect it"
+ : "DdD capture stopped");
+ }
+}
+
+bool gui_ddd_v1_is_active(void)
+{
+ return s_handle != NULL || s_usb_path[0] != '\0';
+}
+
+#endif /* ENABLE_DDD */
diff --git a/misrc_tools/misrc_gui/input/gui_ddd_v1.h b/misrc_tools/misrc_gui/input/gui_ddd_v1.h
new file mode 100644
index 0000000..a8fa2a5
--- /dev/null
+++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.h
@@ -0,0 +1,27 @@
+/* MISRC GUI - isolated Domesday Duplicator firmware 3.1 backend. */
+
+#ifndef GUI_DDD_V1_H
+#define GUI_DDD_V1_H
+
+#ifdef ENABLE_DDD
+
+#include
+#include
+
+#include "../../common/device_enum.h"
+
+typedef struct gui_app gui_app_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);
+
+/* A failed/unverified B5 cleanup locks only the exact physical DDD 3.1 path.
+ * Two complete enumerations must observe disappearance followed by reappearance
+ * before that path is usable again. Other devices and DDD paths are untouched. */
+void gui_ddd_v1_observe_enumeration(const misrc_device_list_t *devices);
+
+#endif /* ENABLE_DDD */
+
+#endif /* GUI_DDD_V1_H */
diff --git a/misrc_tools/misrc_gui/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c
index 585dd04..579ab3d 100644
--- a/misrc_tools/misrc_gui/ui/gui_ui.c
+++ b/misrc_tools/misrc_gui/ui/gui_ui.c
@@ -286,6 +286,13 @@ static bool gui_ui_selected_device_is_ddd(const gui_app_t *app)
return app->devices[app->selected_device].type == DEVICE_TYPE_DDD;
}
+static bool gui_ui_selected_device_is_ddd_v1(const gui_app_t *app)
+{
+ if (!gui_ui_selected_device_is_ddd(app)) return false;
+ return app->devices[app->selected_device].ddd_profile ==
+ DDD_DEVICE_PROTOCOL_V1;
+}
+
// True iff the selected device is the synthetic "[DdD] Clockgen" entry.
static bool gui_ui_selected_device_is_ddd_clockgen(const gui_app_t *app)
{
@@ -1669,6 +1676,64 @@ static float cycle_resample_khz(float current_khz, float max_khz) {
return allowed[(idx + 1) % allowed_count];
}
+#ifdef ENABLE_DDD
+static uint32_t gui_ui_ddd_v1_resample_setting_khz(const gui_app_t *app)
+{
+ float rate_khz;
+ if (!app) return 0;
+ rate_khz = app->settings.resample_rate_a;
+ if (!isfinite(rate_khz) || rate_khz <= 0.0f ||
+ rate_khz > (float)ddd_sample_rate_khz(
+ DDD_DECIMATION_FULL_RATE) + 0.5f) {
+ return 0;
+ }
+ return (uint32_t)lroundf(rate_khz);
+}
+
+static bool gui_ui_ddd_v1_rate_plan(const gui_app_t *app,
+ ddd_v1_rate_plan_t *plan)
+{
+ uint32_t output_rate_khz;
+ if (!app || !plan) return false;
+ output_rate_khz = ddd_v1_effective_output_rate_khz(
+ app->settings.enable_resample_a,
+ gui_ui_ddd_v1_resample_setting_khz(app),
+ app->settings.ddd_decimation);
+ return ddd_v1_plan_output_rate_khz(output_rate_khz, plan);
+}
+
+static bool gui_ui_set_ddd_v1_output_rate(gui_app_t *app,
+ uint32_t output_rate_khz)
+{
+ ddd_v1_rate_plan_t plan;
+ char hardware_label[32];
+ char output_label[32];
+ char message[96];
+
+ if (!app || !ddd_v1_plan_output_rate_khz(output_rate_khz, &plan)) {
+ return false;
+ }
+
+ app->settings.ddd_decimation = plan.decimation_factor;
+ app->settings.enable_resample_a = plan.software_resample;
+ app->settings.resample_rate_a = (float)plan.output_rate_khz;
+ gui_settings_save(&app->settings);
+
+ format_msps_label(hardware_label, sizeof(hardware_label),
+ (float)plan.hardware_rate_khz);
+ format_msps_label(output_label, sizeof(output_label),
+ (float)plan.output_rate_khz);
+ if (plan.software_resample) {
+ snprintf(message, sizeof(message), "%s HW -> %s SW",
+ hardware_label, output_label);
+ } else {
+ snprintf(message, sizeof(message), "DdD: %s HW", output_label);
+ }
+ gui_app_set_status(app, message);
+ return true;
+}
+#endif
+
static bool gui_ui_flac_affinity_supported(void) {
#if defined(__linux__)
return true;
@@ -2460,8 +2525,10 @@ static void render_settings_panel(gui_app_t *app) {
bool settings_cxadc_mode = gui_ui_selected_device_is_cxadc(app, &settings_cxadc_has_channel_b);
#ifdef ENABLE_DDD
bool settings_ddd_mode = gui_ui_selected_device_is_ddd(app);
+ bool settings_ddd_v1_mode = gui_ui_selected_device_is_ddd_v1(app);
#else
bool settings_ddd_mode = false;
+ bool settings_ddd_v1_mode = false;
#endif
#ifdef ENABLE_FX3
bool settings_fx3_mode = gui_ui_selected_device_is_fx3(app);
@@ -2880,24 +2947,58 @@ CLAY(CLAY_ID("SettingsOutputPath"), {
}
}
- // Resample section
- CLAY_TEXT(CLAY_STRING("Resample (RF):"),
- CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(COLOR_TEXT_DIM) }));
-
- CLAY(CLAY_ID("ToggleRowResampleA"), { .layout = { .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(28) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .childGap = 10 } }) {
- Color resample_a_toggle_bg = app->settings.enable_resample_a ? COLOR_BUTTON_ACTIVE : COLOR_BUTTON;
- Color resample_a_toggle_fg = COLOR_TEXT;
- CLAY(CLAY_ID("ToggleResampleA"), { .layout = { .sizing = { CLAY_SIZING_FIXED(80), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(resample_a_toggle_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
- CLAY_TEXT(app->settings.enable_resample_a ? CLAY_STRING("ON") : CLAY_STRING("OFF"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_a_toggle_fg) }));
+ if (settings_ddd_v1_mode) {
+ CLAY_TEXT(CLAY_STRING("RF sample rate:"),
+ CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(COLOR_TEXT_DIM) }));
+#ifdef ENABLE_DDD
+ ddd_v1_rate_plan_t rate_plan;
+ if (!gui_ui_ddd_v1_rate_plan(app, &rate_plan)) {
+ (void)ddd_v1_plan_output_rate_khz(
+ ddd_sample_rate_khz(DDD_DECIMATION_FULL_RATE),
+ &rate_plan);
}
- CLAY_TEXT(CLAY_STRING("Resample A"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_a_toggle_fg) }));
-
- // Rate selector (kHz stored; display MSPS)
- format_msps_label(settings_resample_a_display, sizeof(settings_resample_a_display), app->settings.resample_rate_a);
- Color rate_bg = !app->settings.enable_resample_a ? ui_disabled_color(COLOR_BUTTON) : COLOR_BUTTON;
- Color rate_fg = !app->settings.enable_resample_a ? ui_disabled_color(COLOR_TEXT) : COLOR_TEXT;
- CLAY(CLAY_ID("ResampleRateABox"), { .layout = { .sizing = { CLAY_SIZING_FIXED(110), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(rate_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
- CLAY_TEXT(make_string(settings_resample_a_display), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(rate_fg) }));
+ format_msps_label(settings_resample_a_display,
+ sizeof(settings_resample_a_display),
+ (float)rate_plan.output_rate_khz);
+ Color ddd_mode_bg = app->is_capturing
+ ? ui_disabled_color(COLOR_BUTTON_ACTIVE)
+ : COLOR_BUTTON_ACTIVE;
+ Color ddd_rate_bg = app->is_capturing
+ ? ui_disabled_color(COLOR_BUTTON) : COLOR_BUTTON;
+ Color ddd_rate_fg = app->is_capturing
+ ? ui_disabled_color(COLOR_TEXT) : COLOR_TEXT;
+ Color ddd_path_fg = app->is_capturing
+ ? ui_disabled_color(COLOR_TEXT_DIM) : COLOR_TEXT_DIM;
+ CLAY(CLAY_ID("ToggleRowResampleA"), { .layout = { .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(28) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .childGap = 10 } }) {
+ CLAY(CLAY_ID("DddRateModeBadge"), { .layout = { .sizing = { CLAY_SIZING_FIXED(80), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(ddd_mode_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
+ CLAY_TEXT(rate_plan.software_resample ? CLAY_STRING("SW") : CLAY_STRING("HW"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(ddd_rate_fg) }));
+ }
+ CLAY_TEXT(CLAY_STRING("RF ChA"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(ddd_rate_fg) }));
+ CLAY(CLAY_ID("ResampleRateABox"), { .layout = { .sizing = { CLAY_SIZING_FIXED(110), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(ddd_rate_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
+ CLAY_TEXT(make_string(settings_resample_a_display), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(ddd_rate_fg) }));
+ }
+ CLAY_TEXT(rate_plan.software_resample ? CLAY_STRING("from 20 MSPS HW") : CLAY_STRING("hardware"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(ddd_path_fg) }));
+ }
+#endif
+ } else {
+ CLAY_TEXT(CLAY_STRING("Resample (RF):"),
+ CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(COLOR_TEXT_DIM) }));
+
+ CLAY(CLAY_ID("ToggleRowResampleA"), { .layout = { .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(28) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .childGap = 10 } }) {
+ Color resample_a_toggle_bg = app->settings.enable_resample_a ? COLOR_BUTTON_ACTIVE : COLOR_BUTTON;
+ Color resample_a_toggle_fg = COLOR_TEXT;
+ CLAY(CLAY_ID("ToggleResampleA"), { .layout = { .sizing = { CLAY_SIZING_FIXED(80), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(resample_a_toggle_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
+ CLAY_TEXT(app->settings.enable_resample_a ? CLAY_STRING("ON") : CLAY_STRING("OFF"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_a_toggle_fg) }));
+ }
+ CLAY_TEXT(CLAY_STRING("Resample A"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_a_toggle_fg) }));
+
+ // Rate selector (kHz stored; display MSPS)
+ format_msps_label(settings_resample_a_display, sizeof(settings_resample_a_display), app->settings.resample_rate_a);
+ Color rate_bg = !app->settings.enable_resample_a ? ui_disabled_color(COLOR_BUTTON) : COLOR_BUTTON;
+ Color rate_fg = !app->settings.enable_resample_a ? ui_disabled_color(COLOR_TEXT) : COLOR_TEXT;
+ CLAY(CLAY_ID("ResampleRateABox"), { .layout = { .sizing = { CLAY_SIZING_FIXED(110), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(rate_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
+ CLAY_TEXT(make_string(settings_resample_a_display), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(rate_fg) }));
+ }
}
}
@@ -2909,7 +3010,7 @@ CLAY(CLAY_ID("SettingsOutputPath"), {
CLAY(CLAY_ID("ToggleResampleB"), { .layout = { .sizing = { CLAY_SIZING_FIXED(80), CLAY_SIZING_FIXED(28) }, .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER } }, .backgroundColor = to_clay_color(resample_b_toggle_bg), .cornerRadius = CLAY_CORNER_RADIUS(4) }) {
CLAY_TEXT(app->settings.enable_resample_b ? CLAY_STRING("ON") : CLAY_STRING("OFF"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_b_toggle_fg) }));
}
- CLAY_TEXT(CLAY_STRING("Resample B"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_b_toggle_fg) }));
+ CLAY_TEXT(settings_ddd_v1_mode ? CLAY_STRING("RF ChB") : CLAY_STRING("Resample B"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(resample_b_toggle_fg) }));
format_msps_label(settings_resample_b_display, sizeof(settings_resample_b_display), app->settings.resample_rate_b);
Color rate_bg = (settings_b_controls_disabled || !app->settings.enable_resample_b) ? ui_disabled_color(COLOR_BUTTON) : COLOR_BUTTON;
@@ -6875,8 +6976,10 @@ void gui_handle_interactions(gui_app_t *app) {
bool settings_cxadc_mode = gui_ui_selected_device_is_cxadc(app, &settings_cxadc_has_channel_b);
#ifdef ENABLE_DDD
bool settings_ddd_mode = gui_ui_selected_device_is_ddd(app);
+ bool settings_ddd_v1_mode = gui_ui_selected_device_is_ddd_v1(app);
#else
bool settings_ddd_mode = false;
+ bool settings_ddd_v1_mode = false;
#endif
#ifdef ENABLE_FX3
bool settings_fx3_mode = gui_ui_selected_device_is_fx3(app);
@@ -6885,7 +6988,16 @@ void gui_handle_interactions(gui_app_t *app) {
#endif
bool settings_b_disabled = settings_ddd_mode || settings_fx3_mode || (settings_cxadc_mode && !settings_cxadc_has_channel_b);
bool settings_b_controls_disabled = settings_b_disabled || !app->settings.capture_b;
- float settings_base_rate_a_khz = settings_cxadc_mode ? gui_ui_cxadc_base_rate_khz(app, 0) : 40000.0f;
+ float settings_non_cxadc_base_rate_a_khz = 40000.0f;
+#ifdef ENABLE_DDD
+ if (settings_ddd_v1_mode) {
+ settings_non_cxadc_base_rate_a_khz =
+ (float)ddd_sample_rate_khz(app->settings.ddd_decimation);
+ }
+#endif
+ float settings_base_rate_a_khz = settings_cxadc_mode
+ ? gui_ui_cxadc_base_rate_khz(app, 0)
+ : settings_non_cxadc_base_rate_a_khz;
float settings_base_rate_b_khz = settings_cxadc_mode ? gui_ui_cxadc_base_rate_khz(app, settings_cxadc_has_channel_b ? 1 : 0) : 40000.0f;
if (Clay_PointerOver(CLAY_ID("SettingsBackdrop")) || Clay_PointerOver(CLAY_ID("SettingsCloseButton"))) {
app->settings_panel_open = false;
@@ -6970,7 +7082,8 @@ void gui_handle_interactions(gui_app_t *app) {
gui_ui_begin_text_edit(app, UI_TEXT_FIELD_FLAC_AFFINITY, CLAY_ID("FlacAffinityListField"), 8.0f, 8.0f);
gui_ui_set_click_consumed();
}
- if (Clay_PointerOver(CLAY_ID("ToggleResampleA"))) {
+ if (!settings_ddd_v1_mode &&
+ Clay_PointerOver(CLAY_ID("ToggleResampleA"))) {
bool enable = !app->settings.enable_resample_a;
app->settings.enable_resample_a = enable;
if (!enable) {
@@ -6979,8 +7092,33 @@ void gui_handle_interactions(gui_app_t *app) {
gui_settings_save(&app->settings);
}
if (Clay_PointerOver(CLAY_ID("ResampleRateABox"))) {
- app->settings.resample_rate_a = cycle_resample_khz(app->settings.resample_rate_a, settings_base_rate_a_khz);
- gui_settings_save(&app->settings);
+#ifdef ENABLE_DDD
+ if (settings_ddd_v1_mode) {
+ ddd_v1_rate_plan_t current_plan;
+ if (app->is_capturing) {
+ gui_app_set_status(app,
+ "Stop capture before changing the DdD RF rate");
+ } else if (!gui_ui_ddd_v1_rate_plan(
+ app, ¤t_plan)) {
+ gui_app_set_status(app,
+ "Invalid DdD RF rate settings");
+ } else {
+ float next_rate_khz = cycle_resample_khz(
+ (float)current_plan.output_rate_khz,
+ (float)ddd_sample_rate_khz(
+ DDD_DECIMATION_FULL_RATE));
+ if (!gui_ui_set_ddd_v1_output_rate(
+ app, (uint32_t)lroundf(next_rate_khz))) {
+ gui_app_set_status(app,
+ "Invalid DdD RF rate selection");
+ }
+ }
+ } else
+#endif
+ {
+ app->settings.resample_rate_a = cycle_resample_khz(app->settings.resample_rate_a, settings_base_rate_a_khz);
+ gui_settings_save(&app->settings);
+ }
}
if (Clay_PointerOver(CLAY_ID("ToggleResampleB"))) {
if (settings_b_controls_disabled) {
diff --git a/misrc_tools/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c
new file mode 100644
index 0000000..43a3a9e
--- /dev/null
+++ b/misrc_tools/test/ddd_protocol_test.c
@@ -0,0 +1,394 @@
+#include "../common/ddd_protocol.h"
+
+#include
+#include
+#include
+
+#define CHECK(condition) do { \
+ if (!(condition)) { \
+ fprintf(stderr, "CHECK failed at %s:%d: %s\n", \
+ __FILE__, __LINE__, #condition); \
+ return false; \
+ } \
+} while (0)
+
+typedef struct {
+ uint8_t registers[256];
+ uint8_t requests[16];
+ uint16_t values[16];
+ size_t request_count;
+ int fail_at;
+} mock_usb_t;
+
+static void mock_usb_init(mock_usb_t *mock)
+{
+ memset(mock, 0, sizeof(*mock));
+ mock->registers[DDD_REGISTER_IDENTITY] = DDD_IDENTITY_VALUE;
+ mock->registers[DDD_REGISTER_MAP_VERSION] =
+ DDD_SUPPORTED_REGISTER_MAP;
+ mock->registers[DDD_REGISTER_IMAGE_ROLE] =
+ DDD_APPLICATION_IMAGE_ROLE;
+ mock->registers[DDD_REGISTER_BUILD_FLAGS] = DDD_BUILD_COMMIT_FLAG;
+ memcpy(&mock->registers[DDD_REGISTER_COMMIT], "deadbeef", 8);
+ mock->fail_at = -1;
+}
+
+static int mock_transfer(void *context,
+ uint8_t request_type,
+ uint8_t request,
+ uint16_t value,
+ uint16_t index,
+ uint8_t *data,
+ uint16_t length)
+{
+ mock_usb_t *mock = (mock_usb_t *)context;
+ size_t call = mock->request_count;
+ (void)index;
+ if (call < sizeof(mock->requests)) {
+ mock->requests[call] = request;
+ mock->values[call] = value;
+ }
+ ++mock->request_count;
+ if ((int)call == mock->fail_at) return -1;
+ if (request_type == DDD_USB_REQUEST_VENDOR_IN &&
+ request == DDD_REQUEST_REGISTER_READ) {
+ memcpy(data, &mock->registers[value & 0xffu], length);
+ return length;
+ }
+ if (request_type == DDD_USB_REQUEST_VENDOR_OUT &&
+ request == DDD_REQUEST_REGISTER_WRITE) {
+ mock->registers[(value >> 8) & 0xffu] = value & 0xffu;
+ return 0;
+ }
+ if (request_type == DDD_USB_REQUEST_VENDOR_OUT &&
+ request == DDD_REQUEST_COLLECTION) {
+ return 0;
+ }
+ return -1;
+}
+
+static bool test_profiles_and_rates(void)
+{
+ ddd_profile_index_state_t indices;
+ CHECK(ddd_classify_device(DDD_LEGACY_VENDOR_ID,
+ DDD_LEGACY_PRODUCT_ID, 0) ==
+ DDD_DEVICE_LEGACY);
+ CHECK(ddd_classify_device(DDD_CURRENT_VENDOR_ID,
+ DDD_CURRENT_PRODUCT_ID, 0x0100) ==
+ DDD_DEVICE_PROTOCOL_V1);
+ CHECK(ddd_classify_device(DDD_CURRENT_VENDOR_ID,
+ DDD_CURRENT_PRODUCT_ID, 0x0200) ==
+ DDD_DEVICE_UNSUPPORTED);
+ CHECK(ddd_classify_device(0xffff, 0xffff, 0) == DDD_DEVICE_NOT_DDD);
+ CHECK(ddd_profile_supports_decimation(DDD_DEVICE_LEGACY, 1));
+ CHECK(!ddd_profile_supports_decimation(DDD_DEVICE_LEGACY, 2));
+ CHECK(ddd_profile_supports_decimation(DDD_DEVICE_PROTOCOL_V1, 1));
+ CHECK(ddd_profile_supports_decimation(DDD_DEVICE_PROTOCOL_V1, 2));
+ CHECK(!ddd_profile_requires_usb_path(DDD_DEVICE_LEGACY));
+ CHECK(ddd_profile_requires_usb_path(DDD_DEVICE_PROTOCOL_V1));
+ CHECK(!ddd_profile_requires_usb_path(DDD_DEVICE_UNSUPPORTED));
+ CHECK(ddd_sample_rate_hz(1) == 40000000u);
+ CHECK(ddd_sample_rate_hz(2) == 20000000u);
+ CHECK(ddd_sample_rate_hz(3) == 0);
+ CHECK(ddd_v1_link_speed_allowed(false, false));
+ CHECK(!ddd_v1_link_speed_allowed(true, false));
+ CHECK(ddd_v1_link_speed_allowed(true, true));
+
+ ddd_profile_index_state_init(&indices);
+ CHECK(ddd_profile_index_take(&indices, DDD_DEVICE_PROTOCOL_V1) == 0);
+ CHECK(ddd_profile_index_take(&indices, DDD_DEVICE_LEGACY) == 0);
+ CHECK(ddd_profile_index_take(&indices, DDD_DEVICE_UNSUPPORTED) == 0);
+ CHECK(ddd_profile_index_take(&indices, DDD_DEVICE_PROTOCOL_V1) == 1);
+ CHECK(ddd_profile_index_take(&indices, DDD_DEVICE_LEGACY) == 1);
+ CHECK(ddd_profile_index_take(&indices, DDD_DEVICE_NOT_DDD) == -1);
+ return true;
+}
+
+static bool test_v1_rate_plans(void)
+{
+ ddd_v1_rate_plan_t plan;
+ static const uint32_t software_rates_khz[] = {
+ 5000u, 10000u, 14300u, 17900u
+ };
+
+ CHECK(ddd_v1_plan_output_rate_khz(40000u, &plan));
+ CHECK(plan.decimation_factor == DDD_DECIMATION_FULL_RATE);
+ CHECK(plan.hardware_rate_khz == 40000u);
+ CHECK(plan.output_rate_khz == 40000u);
+ CHECK(!plan.software_resample);
+
+ CHECK(ddd_v1_plan_output_rate_khz(20000u, &plan));
+ CHECK(plan.decimation_factor == DDD_DECIMATION_HALF_RATE);
+ CHECK(plan.hardware_rate_khz == 20000u);
+ CHECK(plan.output_rate_khz == 20000u);
+ CHECK(!plan.software_resample);
+
+ for (size_t i = 0;
+ i < sizeof(software_rates_khz) / sizeof(software_rates_khz[0]);
+ ++i) {
+ CHECK(ddd_v1_plan_output_rate_khz(
+ software_rates_khz[i], &plan));
+ CHECK(plan.decimation_factor == DDD_DECIMATION_HALF_RATE);
+ CHECK(plan.hardware_rate_khz == 20000u);
+ CHECK(plan.output_rate_khz == software_rates_khz[i]);
+ CHECK(plan.software_resample);
+ }
+
+ /* Preserve hand-edited intermediate rates without ever upsampling. */
+ CHECK(ddd_v1_plan_output_rate_khz(30000u, &plan));
+ CHECK(plan.decimation_factor == DDD_DECIMATION_FULL_RATE);
+ CHECK(plan.hardware_rate_khz == 40000u);
+ CHECK(plan.output_rate_khz == 30000u);
+ CHECK(plan.software_resample);
+
+ CHECK(!ddd_v1_plan_output_rate_khz(0, &plan));
+ CHECK(!ddd_v1_plan_output_rate_khz(40001u, &plan));
+ CHECK(!ddd_v1_plan_output_rate_khz(20000u, NULL));
+
+ CHECK(ddd_v1_effective_output_rate_khz(
+ false, 5000u, DDD_DECIMATION_FULL_RATE) == 40000u);
+ CHECK(ddd_v1_effective_output_rate_khz(
+ false, 5000u, DDD_DECIMATION_HALF_RATE) == 20000u);
+ CHECK(ddd_v1_effective_output_rate_khz(
+ true, 10000u, DDD_DECIMATION_FULL_RATE) == 10000u);
+ CHECK(ddd_v1_effective_output_rate_khz(
+ true, 30000u, DDD_DECIMATION_HALF_RATE) == 20000u);
+ CHECK(ddd_v1_effective_output_rate_khz(
+ true, 20000u, DDD_DECIMATION_HALF_RATE) == 20000u);
+ CHECK(ddd_v1_effective_output_rate_khz(
+ true, 10000u, 3) == 0);
+ return true;
+}
+
+static int select_clockgen_candidate(
+ const ddd_device_profile_t *profiles,
+ const bool *capture_supported,
+ size_t count)
+{
+ bool selection_present = false;
+ ddd_device_profile_t selected_profile = DDD_DEVICE_NOT_DDD;
+ int selected_index = -1;
+ for (size_t i = 0; i < count; ++i) {
+ if (ddd_clockgen_candidate_is_preferred(
+ selection_present, selected_profile, profiles[i],
+ capture_supported[i])) {
+ selection_present = true;
+ selected_profile = profiles[i];
+ selected_index = (int)i;
+ }
+ }
+ return selected_index;
+}
+
+static bool test_clockgen_profile_selection(void)
+{
+ static const ddd_device_profile_t legacy_then_v1[] = {
+ DDD_DEVICE_LEGACY, DDD_DEVICE_PROTOCOL_V1
+ };
+ static const ddd_device_profile_t v1_then_legacy[] = {
+ DDD_DEVICE_PROTOCOL_V1, DDD_DEVICE_LEGACY
+ };
+ static const ddd_device_profile_t only_v1[] = {
+ DDD_DEVICE_PROTOCOL_V1
+ };
+ static const ddd_device_profile_t unavailable_then_legacy[] = {
+ DDD_DEVICE_PROTOCOL_V1, DDD_DEVICE_LEGACY
+ };
+ static const ddd_device_profile_t unsupported[] = {
+ DDD_DEVICE_UNSUPPORTED
+ };
+ static const bool both_supported[] = {true, true};
+ static const bool one_supported[] = {true};
+ static const bool second_supported[] = {false, true};
+ static const bool none_supported[] = {false};
+
+ CHECK(select_clockgen_candidate(legacy_then_v1, both_supported, 2) == 0);
+ CHECK(select_clockgen_candidate(v1_then_legacy, both_supported, 2) == 1);
+ CHECK(select_clockgen_candidate(only_v1, one_supported, 1) == 0);
+ CHECK(select_clockgen_candidate(
+ unavailable_then_legacy, second_supported, 2) == 1);
+ CHECK(select_clockgen_candidate(unsupported, none_supported, 1) == -1);
+ return true;
+}
+
+static int select_reconnect_candidate(
+ ddd_device_profile_t target_profile,
+ const char *target_usb_path,
+ const ddd_device_profile_t *candidate_profiles,
+ const char *const *candidate_usb_paths,
+ size_t count)
+{
+ for (size_t i = 0; i < count; ++i) {
+ if (ddd_reconnect_path_matches(
+ target_profile, target_usb_path,
+ candidate_profiles[i], candidate_usb_paths[i])) {
+ return (int)i;
+ }
+ }
+ return -1;
+}
+
+static bool test_reconnect_path_selection(void)
+{
+ static const ddd_device_profile_t two_v1[] = {
+ DDD_DEVICE_PROTOCOL_V1, DDD_DEVICE_PROTOCOL_V1
+ };
+ static const char *const path_b_then_a[] = {
+ "usb:1-4", "usb:1-3"
+ };
+ static const char *const path_a_then_b[] = {
+ "usb:1-3", "usb:1-4"
+ };
+ static const char *const other_paths[] = {
+ "usb:1-4", "usb:1-5"
+ };
+
+ CHECK(select_reconnect_candidate(
+ DDD_DEVICE_PROTOCOL_V1, "usb:1-4", two_v1,
+ path_b_then_a, 2) == 0);
+ CHECK(select_reconnect_candidate(
+ DDD_DEVICE_PROTOCOL_V1, "usb:1-4", two_v1,
+ path_a_then_b, 2) == 1);
+ CHECK(select_reconnect_candidate(
+ DDD_DEVICE_PROTOCOL_V1, "usb:1-3", two_v1,
+ other_paths, 2) == -1);
+ CHECK(!ddd_reconnect_path_matches(
+ DDD_DEVICE_PROTOCOL_V1, "usb:1-3",
+ DDD_DEVICE_LEGACY, "usb:1-3"));
+ CHECK(!ddd_reconnect_path_matches(
+ DDD_DEVICE_PROTOCOL_V1, "",
+ DDD_DEVICE_PROTOCOL_V1, "usb:1-3"));
+ CHECK(!ddd_reconnect_path_matches(
+ DDD_DEVICE_PROTOCOL_V1, NULL,
+ DDD_DEVICE_PROTOCOL_V1, "usb:1-3"));
+ CHECK(!ddd_reconnect_path_matches(
+ DDD_DEVICE_LEGACY, "usb:1-3",
+ DDD_DEVICE_LEGACY, "usb:1-3"));
+ return true;
+}
+
+static bool test_topology_and_endpoint(void)
+{
+ uint8_t ports[] = {3, 2, 7};
+ char path[32];
+ ddd_stream_selector_t selector;
+ ddd_stream_path_t selected;
+ ddd_stream_endpoint_candidate_t wrong = {
+ .interface_number = 0, .alternate_setting = 0,
+ .endpoint_address = 0x81, .max_packet_size = 512,
+ .is_bulk = true, .is_in = true
+ };
+ ddd_stream_endpoint_candidate_t exact = {
+ .interface_number = 0, .alternate_setting = 0,
+ .endpoint_address = 0x81, .max_packet_size = 1024,
+ .is_bulk = true, .is_in = true
+ };
+ CHECK(ddd_format_usb_topology_path(1, ports, 3, path, sizeof(path)));
+ CHECK(strcmp(path, "usb:1-3.2.7") == 0);
+ ddd_stream_selector_init(&selector, DDD_DEVICE_PROTOCOL_V1);
+ ddd_stream_selector_consider(&selector, &wrong);
+ CHECK(!ddd_stream_selector_get(&selector, &selected));
+ ddd_stream_selector_consider(&selector, &exact);
+ CHECK(ddd_stream_selector_get(&selector, &selected));
+ ddd_stream_selector_consider(&selector, &exact);
+ CHECK(!ddd_stream_selector_get(&selector, &selected));
+ return true;
+}
+
+static bool test_lifecycle(void)
+{
+ mock_usb_t mock;
+ ddd_collection_state_t state;
+ ddd_control_ops_t ops = {.transfer = mock_transfer, .context = &mock};
+
+ mock_usb_init(&mock);
+ CHECK(ddd_collection_start_v1(&ops, true, 2, &state) ==
+ DDD_PROTOCOL_OK);
+ CHECK(state.collection_active);
+ CHECK(state.sample_rate_hz == 20000000u);
+ CHECK(mock.request_count == 6);
+ CHECK(mock.requests[0] == DDD_REQUEST_REGISTER_READ);
+ CHECK(mock.requests[1] == DDD_REQUEST_REGISTER_WRITE);
+ CHECK(mock.values[1] == ddd_make_register_write(
+ DDD_REGISTER_TEST_MODE, 1));
+ CHECK(mock.requests[2] == DDD_REQUEST_REGISTER_WRITE);
+ CHECK(mock.values[2] == ddd_make_register_write(
+ DDD_REGISTER_DECIMATION, 2));
+ CHECK(mock.requests[5] == DDD_REQUEST_COLLECTION);
+ CHECK(mock.values[5] == 1);
+ CHECK(ddd_collection_stop_v1(&ops, &state) == DDD_PROTOCOL_OK);
+ CHECK(!state.collection_active);
+
+ mock_usb_init(&mock);
+ mock.fail_at = 2;
+ CHECK(ddd_collection_start_v1(&ops, true, 2, &state) ==
+ DDD_PROTOCOL_CONTROL_FAILURE);
+ CHECK(state.rollback_attempted);
+ CHECK(state.rollback_succeeded);
+ CHECK(mock.registers[DDD_REGISTER_TEST_MODE] == 0);
+ CHECK(mock.registers[DDD_REGISTER_DECIMATION] == 1);
+ return true;
+}
+
+static bool test_validators(void)
+{
+ ddd_sequence_validator_t sequence;
+ ddd_test_ramp_validator_t ramp;
+ uint16_t *words = calloc(DDD_SEQUENCE_SAMPLES_PER_MARKER,
+ sizeof(*words));
+ CHECK(words != NULL);
+ ddd_sequence_validator_init(&sequence);
+ words[0] = (uint16_t)(10u << 10);
+ words[1] = (uint16_t)(11u << 10);
+ CHECK(ddd_sequence_validator_feed(&sequence, words, 2) ==
+ DDD_VALIDATION_OK);
+ CHECK(sequence.phase == DDD_SEQUENCE_RUNNING);
+ for (size_t i = 0; i < DDD_SEQUENCE_SAMPLES_PER_MARKER; ++i) {
+ words[i] = (uint16_t)(11u << 10);
+ }
+ /* One sample for marker 11 was already consumed above. */
+ CHECK(ddd_sequence_validator_feed(
+ &sequence, words, DDD_SEQUENCE_SAMPLES_PER_MARKER - 1) ==
+ DDD_VALIDATION_OK);
+ words[0] = (uint16_t)(12u << 10);
+ CHECK(ddd_sequence_validator_feed(&sequence, words, 1) ==
+ DDD_VALIDATION_OK);
+ words[0] = (uint16_t)(14u << 10);
+ CHECK(ddd_sequence_validator_feed(&sequence, words, 1) ==
+ DDD_VALIDATION_MISMATCH);
+
+ ddd_test_ramp_validator_init(&ramp);
+ for (size_t i = 0; i < DDD_TEST_RAMP_NEW_WRAP; ++i) words[i] = (uint16_t)i;
+ words[DDD_TEST_RAMP_NEW_WRAP] = 0;
+ CHECK(ddd_test_ramp_validator_feed(
+ &ramp, words, DDD_TEST_RAMP_NEW_WRAP + 1u) == DDD_VALIDATION_OK);
+ words[0] = 2;
+ CHECK(ddd_test_ramp_validator_feed(&ramp, words, 1) ==
+ DDD_VALIDATION_MISMATCH);
+
+ ddd_test_ramp_validator_init(&ramp);
+ for (size_t i = 0; i < DDD_TEST_RAMP_LEGACY_WRAP; ++i) {
+ words[i] = (uint16_t)i;
+ }
+ words[DDD_TEST_RAMP_LEGACY_WRAP] = 0;
+ CHECK(ddd_test_ramp_validator_feed(
+ &ramp, words, DDD_TEST_RAMP_LEGACY_WRAP + 1u) ==
+ DDD_VALIDATION_OK);
+ free(words);
+ return true;
+}
+
+int main(void)
+{
+ if (!test_profiles_and_rates() ||
+ !test_v1_rate_plans() ||
+ !test_clockgen_profile_selection() ||
+ !test_reconnect_path_selection() ||
+ !test_topology_and_endpoint() ||
+ !test_lifecycle() ||
+ !test_validators()) {
+ return 1;
+ }
+ puts("DDD protocol tests passed");
+ return 0;
+}
diff --git a/misrc_tools/test/gui_ddd_async_fault_test.c b/misrc_tools/test/gui_ddd_async_fault_test.c
new file mode 100644
index 0000000..60f3078
--- /dev/null
+++ b/misrc_tools/test/gui_ddd_async_fault_test.c
@@ -0,0 +1,161 @@
+#include "../misrc_gui/input/gui_ddd_async.h"
+
+#include
+#include
+#include
+#include
+
+#define TEST_EVENT_ERROR (-99)
+
+typedef struct {
+ uint64_t now_ms;
+ size_t submit_calls;
+ size_t cancel_calls;
+ size_t event_pump_calls;
+} async_fault_state_t;
+
+static uint64_t advancing_now_ms(void *context)
+{
+ async_fault_state_t *state = (async_fault_state_t *)context;
+ state->now_ms += 400;
+ return state->now_ms;
+}
+
+static int permanent_event_error(void *context, long timeout_us)
+{
+ async_fault_state_t *state = (async_fault_state_t *)context;
+ (void)timeout_us;
+ state->event_pump_calls++;
+ return TEST_EVENT_ERROR;
+}
+
+static int accept_fake_submit(
+ void *context,
+ struct libusb_transfer *transfer)
+{
+ async_fault_state_t *state = (async_fault_state_t *)context;
+ assert(transfer != NULL);
+ state->submit_calls++;
+ return 0;
+}
+
+static int accept_fake_cancel(
+ void *context,
+ struct libusb_transfer *transfer)
+{
+ async_fault_state_t *state = (async_fault_state_t *)context;
+ assert(transfer != NULL);
+ state->cancel_calls++;
+ return 0;
+}
+
+static gui_ddd_async_consume_result_t reject_unexpected_consume(
+ void *context,
+ const uint8_t *data,
+ size_t size)
+{
+ (void)context;
+ (void)data;
+ (void)size;
+ assert(!"permanent event failure must not consume a block");
+ return GUI_DDD_ASYNC_CONSUME_FAILED;
+}
+
+static gui_ddd_async_config_t make_fault_config(
+ async_fault_state_t *state,
+ atomic_bool *capture_running,
+ atomic_bool *transfer_ready,
+ atomic_bool *startup_failed,
+ int *usb_context_marker,
+ int *device_handle_marker)
+{
+ gui_ddd_async_config_t config;
+
+ memset(&config, 0, sizeof(config));
+ config.usb_context =
+ (struct libusb_context *)(void *)usb_context_marker;
+ config.device_handle =
+ (struct libusb_device_handle *)(void *)device_handle_marker;
+ config.endpoint = 0x82;
+ config.capture_running = capture_running;
+ config.transfer_ready = transfer_ready;
+ config.startup_failed = startup_failed;
+ config.consume = reject_unexpected_consume;
+ config.consume_context = state;
+ config.event_pump_override = permanent_event_error;
+ config.event_pump_context = state;
+ config.now_ms_override = advancing_now_ms;
+ config.now_ms_context = state;
+ config.submit_override = accept_fake_submit;
+ config.submit_context = state;
+ config.cancel_override = accept_fake_cancel;
+ config.cancel_context = state;
+ return config;
+}
+
+static void test_result_is_required_for_orphan_ownership(void)
+{
+ async_fault_state_t state = {0};
+ atomic_bool capture_running = true;
+ atomic_bool transfer_ready = true;
+ 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);
+
+ assert(gui_ddd_async_run(&config, NULL) == -1);
+ assert(!atomic_load(&transfer_ready));
+ assert(atomic_load(&startup_failed));
+ assert(state.submit_calls == 0);
+}
+
+static void test_permanent_event_error_returns_bounded_orphan(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;
+
+ memset(&result, 0, sizeof(result));
+ assert(!gui_ddd_async_global_quarantine_active());
+ assert(gui_ddd_async_run(&config, &result) == -1);
+
+ assert(result.code == GUI_DDD_ASYNC_RESULT_EVENT_FAILURE);
+ 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.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.event_pump_calls > 0);
+ assert(state.event_pump_calls <= 4);
+ assert(state.now_ms <=
+ GUI_DDD_ASYNC_CANCEL_REAP_TIMEOUT_MS + 1200);
+ assert(!atomic_load(&transfer_ready));
+ assert(atomic_load(&startup_failed));
+
+ assert(gui_ddd_async_orphan_has_unreaped(result.orphan));
+ assert(!gui_ddd_async_orphan_try_reclaim(result.orphan));
+ assert(!gui_ddd_async_policy_sync_control_allowed(true));
+ assert(gui_ddd_async_orphan_abandon(result.orphan));
+ assert(gui_ddd_async_global_quarantine_active());
+ assert(gui_ddd_async_orphan_abandon(result.orphan));
+}
+
+int main(void)
+{
+ test_result_is_required_for_orphan_ownership();
+ 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
new file mode 100644
index 0000000..a6ebecd
--- /dev/null
+++ b/misrc_tools/test/gui_ddd_async_policy_test.c
@@ -0,0 +1,47 @@
+#include "../misrc_gui/input/gui_ddd_async.h"
+
+#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)
+{
+ gui_ddd_async_order_policy_t policy;
+ 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}
+ };
+ size_t index = 99;
+
+ gui_ddd_async_order_policy_init(&policy, 2);
+ CHECK(gui_ddd_async_order_policy_peek(&policy, slots, &index) ==
+ GUI_DDD_ASYNC_NEXT_READY);
+ CHECK(index == 0);
+ gui_ddd_async_order_policy_advance(&policy);
+ CHECK(gui_ddd_async_order_policy_peek(&policy, slots, &index) ==
+ GUI_DDD_ASYNC_NEXT_WAIT);
+ CHECK(gui_ddd_async_policy_initial_queue_ready(
+ GUI_DDD_ASYNC_TRANSFER_COUNT, false));
+ CHECK(!gui_ddd_async_policy_stream_ready(true, false, true, false));
+ CHECK(gui_ddd_async_policy_stream_ready(true, true, true, false));
+ CHECK(!gui_ddd_async_policy_should_resubmit(true, false, false));
+ CHECK(gui_ddd_async_policy_stop_drain_complete(false, 0, 10, 10));
+ CHECK(gui_ddd_async_policy_reap_action(true, 100, 100, 1) ==
+ GUI_DDD_ASYNC_REAP_ORPHAN);
+ CHECK(!gui_ddd_async_policy_sync_control_allowed(true));
+ CHECK(gui_ddd_async_policy_exact_length(
+ GUI_DDD_ASYNC_TRANSFER_BYTES));
+ CHECK(!gui_ddd_async_policy_exact_length(
+ GUI_DDD_ASYNC_TRANSFER_BYTES - 1));
+ CHECK(gui_ddd_async_policy_abandon_slot_available(0));
+ CHECK(!gui_ddd_async_policy_abandon_slot_available(
+ GUI_DDD_ASYNC_ABANDONED_CAPACITY));
+ puts("DDD async policy tests passed");
+ return 0;
+}