From db6f496654d59566e654c8706e1f536e79b74997 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:31:22 +0800 Subject: [PATCH 01/16] ddd: classify firmware protocol profiles --- misrc_tools/common/ddd_protocol.c | 538 ++++++++++++++++++++++++++++++ misrc_tools/common/ddd_protocol.h | 226 +++++++++++++ misrc_tools/common/device_enum.c | 58 +++- misrc_tools/common/device_enum.h | 15 + misrc_tools/meson.build | 2 + 5 files changed, 832 insertions(+), 7 deletions(-) create mode 100644 misrc_tools/common/ddd_protocol.c create mode 100644 misrc_tools/common/ddd_protocol.h diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c new file mode 100644 index 0000000..267a42c --- /dev/null +++ b/misrc_tools/common/ddd_protocol.c @@ -0,0 +1,538 @@ +/* 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_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); +} + +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..d09906b --- /dev/null +++ b/misrc_tools/common/ddd_protocol.h @@ -0,0 +1,226 @@ +/* + * 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_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); +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..7c2072a 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,20 @@ 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); + if (num_devices < 0) { + libusb_exit(ctx); + return (int)list->count; + } + int ddd_index = 0; + bool enumeration_complete = true; 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); @@ -296,8 +308,37 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh, dev->type = MISRC_DEVICE_TYPE_DDD; dev->index = ddd_index++; + 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 (firmware 3.1)"); + } 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'; + dev->ddd_capture_supported = false; + enumeration_complete = false; + } + } /* Try to get serial number */ dev->device_id[0] = '\0'; @@ -315,11 +356,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..0fde5cf 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,6 +433,7 @@ 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_clockgen.c' endif From 2b8c43d9b330c67f1cb27908d411d8ba65adb4aa Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:31:40 +0800 Subject: [PATCH 02/16] ddd: add bounded asynchronous USB queue --- misrc_tools/misrc_gui/input/gui_ddd_async.c | 575 ++++++++++++++++++++ misrc_tools/misrc_gui/input/gui_ddd_async.h | 316 +++++++++++ 2 files changed, 891 insertions(+) create mode 100644 misrc_tools/misrc_gui/input/gui_ddd_async.c create mode 100644 misrc_tools/misrc_gui/input/gui_ddd_async.h 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 */ From f0f18c325df8798e79bb7fac88784fd42055bf30 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:32:20 +0800 Subject: [PATCH 03/16] ddd: route firmware 3.1 through isolated backend --- misrc_tools/meson.build | 2 + misrc_tools/misrc_gui/core/gui_app.h | 15 + misrc_tools/misrc_gui/input/gui_capture.c | 48 +- misrc_tools/misrc_gui/input/gui_ddd.c | 10 +- misrc_tools/misrc_gui/input/gui_ddd_v1.c | 676 ++++++++++++++++++++++ misrc_tools/misrc_gui/input/gui_ddd_v1.h | 27 + 6 files changed, 768 insertions(+), 10 deletions(-) create mode 100644 misrc_tools/misrc_gui/input/gui_ddd_v1.c create mode 100644 misrc_tools/misrc_gui/input/gui_ddd_v1.h diff --git a/misrc_tools/meson.build b/misrc_tools/meson.build index 0fde5cf..d8db4ab 100644 --- a/misrc_tools/meson.build +++ b/misrc_tools/meson.build @@ -435,6 +435,8 @@ if raylib_dep.found() 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 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/input/gui_capture.c b/misrc_tools/misrc_gui/input/gui_capture.c index 9c69cd8..a2005b7 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" @@ -1249,6 +1250,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 +1281,20 @@ 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); + 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; // 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) { + if (!ddd_device_added && src->ddd_capture_supported) { ddd_device_added = true; first_ddd_src_index = src->index; + first_ddd_device = *dst; } } #endif @@ -1309,6 +1320,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 +1340,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 @@ -1891,14 +1913,25 @@ int gui_app_start_capture(gui_app_t *app) { // 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; + } + // 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"); 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, app->settings.ddd_decimation, + 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 +2396,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_v1.c b/misrc_tools/misrc_gui/input/gui_ddd_v1.c new file mode 100644 index 0000000..6dec97e --- /dev/null +++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.c @@ -0,0 +1,676 @@ +/* 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 || !stable_usb_path || !stable_usb_path[0]) 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) return -1; + device_count = libusb_get_device_list(s_context, &devices); + if (device_count < 0) { + 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); + 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 3.1 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 3.1 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 3.1 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 3.1 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 || !s_handle || !ddd_decimation_is_supported(decimation)) 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) { + 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; + 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 3.1 rollback failed; unplug and reconnect it" + : "DDD 3.1 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) { + atomic_store(&app->ddd_running, false); + app->is_capturing = false; + ddd_v1_stop_workers(app, display_started); + if (ddd_collection_rollback_v1(&ops, &s_collection) != DDD_PROTOCOL_OK) { + ddd_v1_lock_active_path("thread-start rollback failed"); + } + ddd_v1_close(); + 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 3.1 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 3.1 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); + if (!ddd_v1_sync_control_allowed()) { + ddd_v1_lock_active_path("startup callbacks remained unreaped"); + } else if (ddd_collection_rollback_v1(&ops, &s_collection) != + DDD_PROTOCOL_OK) { + ddd_v1_lock_active_path("readiness rollback failed"); + } + ddd_v1_close(); + gui_app_set_status(app, "DDD 3.1 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); + gui_app_set_status(app, unsafe + ? "DDD 3.1 stop unverified; unplug and reconnect it" + : "DDD 3.1 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 */ From a747efecdd8cec4d9160cb999ac69db94cb0fb01 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:32:42 +0800 Subject: [PATCH 04/16] ui: expose DDD 3.1 native sample rates --- README.md | 8 ++- misrc_tools/misrc_gui/core/gui_settings.c | 15 +++++ misrc_tools/misrc_gui/ui/gui_ui.c | 71 ++++++++++++++++++++++- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b2ef4e6..70f992e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A universal cross platform GUI tool for interfacing with and visualizing monitor - CXADC (single cards and Clockgen Mod with sound) - HSDAOH - FX3 (Generic tinkering firmware support) -- DdD (DomesDay Duplicator) +- DdD (DomesDay Duplicator; legacy firmware and firmware 3.1 profiles) - FX3ADC (100mhz MUSE capture device) ## Downloads @@ -29,6 +29,12 @@ x86 (AMD/Intel) and ARM64 (Apple M, Snapdragon, RockChip) are fully supported an Building from source? See [INSTALLATION.md](INSTALLATION.md). +DdD firmware 3.1 is shown as a separate device profile from legacy DdD +firmware. Its ADC rate control selects the native 40 or 20 MSPS hardware rate; +the existing RF resampler remains an independent optional output setting and +is limited to the selected native rate. Other capture devices keep their +existing rate controls and behavior. + ## Setup With Devices 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/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c index 585dd04..1ab7468 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) { @@ -1141,6 +1148,9 @@ static char settings_flac_level_display[64]; static char settings_flac_threads_display[64]; static char settings_resample_a_display[32]; static char settings_resample_b_display[32]; +#ifdef ENABLE_DDD +static char settings_ddd_rate_display[32]; +#endif static char status_sample_rate_display[32]; static char status_samples_display[32]; static char status_frames_display[32]; @@ -2460,8 +2470,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,6 +2892,29 @@ CLAY(CLAY_ID("SettingsOutputPath"), { } } + // Firmware 3.1 exposes its ADC decimation independently + // from the existing optional output resampler. No other + // device sees or reads this control. +#ifdef ENABLE_DDD + if (settings_ddd_v1_mode) { + snprintf(settings_ddd_rate_display, + sizeof(settings_ddd_rate_display), + "%u MSPS (native)", + app->settings.ddd_decimation == + DDD_DECIMATION_HALF_RATE ? 20u : 40u); + 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; + CLAY(CLAY_ID("DddHardwareRateRow"), { .layout = { .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(28) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .childGap = 10 } }) { + CLAY_TEXT(CLAY_STRING("DDD ADC rate:"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(ddd_rate_fg) })); + CLAY(CLAY_ID("DddHardwareRateBox"), { .layout = { .sizing = { CLAY_SIZING_FIXED(150), 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_ddd_rate_display), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(ddd_rate_fg) })); + } + } + } +#endif + // Resample section CLAY_TEXT(CLAY_STRING("Resample (RF):"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(COLOR_TEXT_DIM) })); @@ -6875,8 +6910,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 +6922,11 @@ 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_base_rate_a_khz = settings_cxadc_mode + ? gui_ui_cxadc_base_rate_khz(app, 0) + : (settings_ddd_v1_mode + ? (float)ddd_sample_rate_khz(app->settings.ddd_decimation) + : 40000.0f); 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; @@ -6978,6 +7019,34 @@ void gui_handle_interactions(gui_app_t *app) { } gui_settings_save(&app->settings); } +#ifdef ENABLE_DDD + if (settings_ddd_v1_mode && + Clay_PointerOver(CLAY_ID("DddHardwareRateBox"))) { + if (app->is_capturing) { + gui_app_set_status(app, + "Stop capture before changing the DDD ADC rate"); + } else { + app->settings.ddd_decimation = + app->settings.ddd_decimation == + DDD_DECIMATION_FULL_RATE + ? DDD_DECIMATION_HALF_RATE + : DDD_DECIMATION_FULL_RATE; + settings_base_rate_a_khz = (float)ddd_sample_rate_khz( + app->settings.ddd_decimation); + if (app->settings.resample_rate_a > + settings_base_rate_a_khz) { + app->settings.resample_rate_a = + settings_base_rate_a_khz; + } + gui_settings_save(&app->settings); + gui_app_set_status(app, + app->settings.ddd_decimation == + DDD_DECIMATION_HALF_RATE + ? "DDD 3.1 native ADC rate set to 20 MSPS" + : "DDD 3.1 native ADC rate set to 40 MSPS"); + } + } +#endif 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); From 3fb0d259f7e4661fe7cf07647dfd330089536492 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:33:37 +0800 Subject: [PATCH 05/16] test: cover DDD 3.1 protocol and async safety --- misrc_tools/meson.build | 26 +++ misrc_tools/test/ddd_protocol_test.c | 216 +++++++++++++++++++ misrc_tools/test/gui_ddd_async_fault_test.c | 161 ++++++++++++++ misrc_tools/test/gui_ddd_async_policy_test.c | 47 ++++ 4 files changed, 450 insertions(+) create mode 100644 misrc_tools/test/ddd_protocol_test.c create mode 100644 misrc_tools/test/gui_ddd_async_fault_test.c create mode 100644 misrc_tools/test/gui_ddd_async_policy_test.c diff --git a/misrc_tools/meson.build b/misrc_tools/meson.build index d8db4ab..bdefa96 100644 --- a/misrc_tools/meson.build +++ b/misrc_tools/meson.build @@ -559,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/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c new file mode 100644 index 0000000..00861da --- /dev/null +++ b/misrc_tools/test/ddd_protocol_test.c @@ -0,0 +1,216 @@ +#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) +{ + 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_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)); + 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_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; +} From f37e44f7eccb4468cca8ea848309c97d67302a4d Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:36:52 +0800 Subject: [PATCH 06/16] fix(ui): preserve builds without DDD support --- misrc_tools/misrc_gui/ui/gui_ui.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/misrc_tools/misrc_gui/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c index 1ab7468..72c9e6b 100644 --- a/misrc_tools/misrc_gui/ui/gui_ui.c +++ b/misrc_tools/misrc_gui/ui/gui_ui.c @@ -2473,7 +2473,6 @@ static void render_settings_panel(gui_app_t *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); @@ -6913,7 +6912,6 @@ void gui_handle_interactions(gui_app_t *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); @@ -6922,11 +6920,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_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_ddd_v1_mode - ? (float)ddd_sample_rate_khz(app->settings.ddd_decimation) - : 40000.0f); + : 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; From 32ac367fe497b4d3795c8d4f6281d9ebeeaced26 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:46:07 +0800 Subject: [PATCH 07/16] fix(ddd): keep device indices profile-local --- misrc_tools/common/ddd_protocol.c | 18 ++++++++++++++++++ misrc_tools/common/ddd_protocol.h | 11 +++++++++++ misrc_tools/common/device_enum.c | 8 ++++++-- misrc_tools/test/ddd_protocol_test.c | 9 +++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c index 267a42c..d00e790 100644 --- a/misrc_tools/common/ddd_protocol.c +++ b/misrc_tools/common/ddd_protocol.c @@ -84,6 +84,24 @@ bool ddd_profile_can_capture(ddd_device_profile_t profile) profile == DDD_DEVICE_PROTOCOL_V1; } +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; diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h index d09906b..0e8e215 100644 --- a/misrc_tools/common/ddd_protocol.h +++ b/misrc_tools/common/ddd_protocol.h @@ -70,6 +70,17 @@ 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); + +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, diff --git a/misrc_tools/common/device_enum.c b/misrc_tools/common/device_enum.c index 7c2072a..e074806 100644 --- a/misrc_tools/common/device_enum.c +++ b/misrc_tools/common/device_enum.c @@ -290,8 +290,9 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh, return (int)list->count; } - int ddd_index = 0; + 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; int descriptor_result = libusb_get_device_descriptor(devlist[i], &desc); @@ -307,7 +308,10 @@ 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; diff --git a/misrc_tools/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c index 00861da..46a4f7e 100644 --- a/misrc_tools/test/ddd_protocol_test.c +++ b/misrc_tools/test/ddd_protocol_test.c @@ -69,6 +69,7 @@ static int mock_transfer(void *context, 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); @@ -89,6 +90,14 @@ static bool test_profiles_and_rates(void) 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; } From de3837670ea5b6b2b39b76d997d0d86c7971aad4 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Mon, 31 Aug 2026 15:58:47 +0800 Subject: [PATCH 08/16] fix(ddd): keep legacy capture path-independent --- misrc_tools/common/ddd_protocol.c | 5 +++++ misrc_tools/common/ddd_protocol.h | 1 + misrc_tools/common/device_enum.c | 6 ++++-- misrc_tools/test/ddd_protocol_test.c | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c index d00e790..e338608 100644 --- a/misrc_tools/common/ddd_protocol.c +++ b/misrc_tools/common/ddd_protocol.c @@ -84,6 +84,11 @@ bool ddd_profile_can_capture(ddd_device_profile_t profile) profile == DDD_DEVICE_PROTOCOL_V1; } +bool ddd_profile_requires_usb_path(ddd_device_profile_t profile) +{ + return profile == DDD_DEVICE_PROTOCOL_V1; +} + void ddd_profile_index_state_init(ddd_profile_index_state_t *state) { if (state) memset(state, 0, sizeof(*state)); diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h index 0e8e215..a6c5bdd 100644 --- a/misrc_tools/common/ddd_protocol.h +++ b/misrc_tools/common/ddd_protocol.h @@ -70,6 +70,7 @@ 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); typedef struct ddd_profile_index_state { int legacy_count; diff --git a/misrc_tools/common/device_enum.c b/misrc_tools/common/device_enum.c index e074806..d067bfd 100644 --- a/misrc_tools/common/device_enum.c +++ b/misrc_tools/common/device_enum.c @@ -339,8 +339,10 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh, port_count, dev->ddd_usb_path, sizeof(dev->ddd_usb_path))) { dev->ddd_usb_path[0] = '\0'; - dev->ddd_capture_supported = false; - enumeration_complete = false; + if (ddd_profile_requires_usb_path(profile)) { + dev->ddd_capture_supported = false; + enumeration_complete = false; + } } } diff --git a/misrc_tools/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c index 46a4f7e..f5d66b6 100644 --- a/misrc_tools/test/ddd_protocol_test.c +++ b/misrc_tools/test/ddd_protocol_test.c @@ -84,6 +84,9 @@ static bool test_profiles_and_rates(void) 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); From a93c0b0e3555cc1988e3de536db0171234679db6 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 04:06:04 +0800 Subject: [PATCH 09/16] ui: use DdD casing in user-visible text --- misrc_tools/misrc_gui/input/gui_ddd_v1.c | 22 +++++++++++----------- misrc_tools/misrc_gui/ui/gui_ui.c | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/misrc_tools/misrc_gui/input/gui_ddd_v1.c b/misrc_tools/misrc_gui/input/gui_ddd_v1.c index 6dec97e..dbb0565 100644 --- a/misrc_tools/misrc_gui/input/gui_ddd_v1.c +++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.c @@ -321,14 +321,14 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) 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 3.1 requires USB 3 SuperSpeed"); + gui_app_set_status(app, "DdD 3.1 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 3.1 USB stream descriptor mismatch"); + gui_app_set_status(app, "DdD 3.1 USB stream descriptor mismatch"); libusb_free_device_list(devices, 1); ddd_v1_close(); return -1; @@ -336,7 +336,7 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) 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 3.1 device"); + gui_app_set_status(app, "Failed to open DdD 3.1 device"); ddd_v1_close(); return -1; } @@ -345,7 +345,7 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) #endif result = libusb_claim_interface(s_handle, DDD_STREAM_INTERFACE_NUMBER); if (result != 0) { - gui_app_set_status(app, "Failed to claim DDD 3.1 USB interface"); + gui_app_set_status(app, "Failed to claim DdD 3.1 USB interface"); ddd_v1_close(); return -1; } @@ -571,8 +571,8 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) ddd_v1_stop_workers(app, display_started); ddd_v1_close(); gui_app_set_status(app, unsafe - ? "DDD 3.1 rollback failed; unplug and reconnect it" - : "DDD 3.1 configuration/readback failed"); + ? "DdD 3.1 rollback failed; unplug and reconnect it" + : "DdD 3.1 configuration/readback failed"); return -1; } ddd_sequence_validator_init(&s_sequence); @@ -603,12 +603,12 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) s_collection.identity, sizeof(s_collection.identity), commit, sizeof(commit)); snprintf(message, sizeof(message), - "DDD 3.1 capture started (path=%s, %u MSPS, test=%s, gateware=%s)", + "DdD 3.1 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 3.1 capture running"); + gui_app_set_status(app, "DdD 3.1 capture running"); return 0; } if (atomic_load(&s_startup_failed) || @@ -627,7 +627,7 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) ddd_v1_lock_active_path("readiness rollback failed"); } ddd_v1_close(); - gui_app_set_status(app, "DDD 3.1 stream did not become ready"); + gui_app_set_status(app, "DdD 3.1 stream did not become ready"); return -1; } @@ -664,8 +664,8 @@ void gui_ddd_v1_stop(gui_app_t *app) ddd_v1_close(); atomic_store(&app->stream_synced, false); gui_app_set_status(app, unsafe - ? "DDD 3.1 stop unverified; unplug and reconnect it" - : "DDD 3.1 capture stopped"); + ? "DdD 3.1 stop unverified; unplug and reconnect it" + : "DdD 3.1 capture stopped"); } bool gui_ddd_v1_is_active(void) diff --git a/misrc_tools/misrc_gui/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c index 72c9e6b..a45de9b 100644 --- a/misrc_tools/misrc_gui/ui/gui_ui.c +++ b/misrc_tools/misrc_gui/ui/gui_ui.c @@ -2906,7 +2906,7 @@ CLAY(CLAY_ID("SettingsOutputPath"), { Color ddd_rate_fg = app->is_capturing ? ui_disabled_color(COLOR_TEXT) : COLOR_TEXT; CLAY(CLAY_ID("DddHardwareRateRow"), { .layout = { .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(28) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .childGap = 10 } }) { - CLAY_TEXT(CLAY_STRING("DDD ADC rate:"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(ddd_rate_fg) })); + CLAY_TEXT(CLAY_STRING("DdD ADC rate:"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(ddd_rate_fg) })); CLAY(CLAY_ID("DddHardwareRateBox"), { .layout = { .sizing = { CLAY_SIZING_FIXED(150), 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_ddd_rate_display), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(ddd_rate_fg) })); } @@ -7027,7 +7027,7 @@ void gui_handle_interactions(gui_app_t *app) { Clay_PointerOver(CLAY_ID("DddHardwareRateBox"))) { if (app->is_capturing) { gui_app_set_status(app, - "Stop capture before changing the DDD ADC rate"); + "Stop capture before changing the DdD ADC rate"); } else { app->settings.ddd_decimation = app->settings.ddd_decimation == @@ -7045,8 +7045,8 @@ void gui_handle_interactions(gui_app_t *app) { gui_app_set_status(app, app->settings.ddd_decimation == DDD_DECIMATION_HALF_RATE - ? "DDD 3.1 native ADC rate set to 20 MSPS" - : "DDD 3.1 native ADC rate set to 40 MSPS"); + ? "DdD 3.1 native ADC rate set to 20 MSPS" + : "DdD 3.1 native ADC rate set to 40 MSPS"); } } #endif From 32879703658a9e4948dd848e51b85cc7de52dd2e Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 04:56:27 +0800 Subject: [PATCH 10/16] fix(ddd): prefer legacy DdD for Clockgen mode --- misrc_tools/common/ddd_protocol.c | 15 +++++++ misrc_tools/common/ddd_protocol.h | 5 +++ misrc_tools/misrc_gui/input/gui_capture.c | 11 +++-- misrc_tools/test/ddd_protocol_test.c | 52 +++++++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c index e338608..c928b8a 100644 --- a/misrc_tools/common/ddd_protocol.c +++ b/misrc_tools/common/ddd_protocol.c @@ -89,6 +89,21 @@ 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; +} + void ddd_profile_index_state_init(ddd_profile_index_state_t *state) { if (state) memset(state, 0, sizeof(*state)); diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h index a6c5bdd..9c5dfd2 100644 --- a/misrc_tools/common/ddd_protocol.h +++ b/misrc_tools/common/ddd_protocol.h @@ -71,6 +71,11 @@ ddd_device_profile_t ddd_classify_device(uint16_t vendor_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); typedef struct ddd_profile_index_state { int legacy_count; diff --git a/misrc_tools/misrc_gui/input/gui_capture.c b/misrc_tools/misrc_gui/input/gui_capture.c index a2005b7..972f62a 100644 --- a/misrc_tools/misrc_gui/input/gui_capture.c +++ b/misrc_tools/misrc_gui/input/gui_capture.c @@ -1289,9 +1289,14 @@ void gui_app_enumerate_devices(gui_app_t *app) { src->ddd_usb_path); dst->ddd_capture_supported = src->ddd_capture_supported; dst->ddd_clockgen = false; - // 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 && src->ddd_capture_supported) { + // 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; diff --git a/misrc_tools/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c index f5d66b6..130eae7 100644 --- a/misrc_tools/test/ddd_protocol_test.c +++ b/misrc_tools/test/ddd_protocol_test.c @@ -104,6 +104,57 @@ static bool test_profiles_and_rates(void) 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 bool test_topology_and_endpoint(void) { uint8_t ports[] = {3, 2, 7}; @@ -218,6 +269,7 @@ static bool test_validators(void) int main(void) { if (!test_profiles_and_rates() || + !test_clockgen_profile_selection() || !test_topology_and_endpoint() || !test_lifecycle() || !test_validators()) { From 11ba292b86ab160a8d89bf74dcd69b78b01188d7 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 04:58:22 +0800 Subject: [PATCH 11/16] fix(ddd): preserve the reconnect USB path --- misrc_tools/common/ddd_protocol.c | 13 ++++++ misrc_tools/common/ddd_protocol.h | 5 +++ misrc_tools/misrc_gui/core/misrc_gui.c | 35 ++++++++++++++-- misrc_tools/test/ddd_protocol_test.c | 57 ++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c index c928b8a..623732f 100644 --- a/misrc_tools/common/ddd_protocol.c +++ b/misrc_tools/common/ddd_protocol.c @@ -104,6 +104,19 @@ bool ddd_clockgen_candidate_is_preferred( 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)); diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h index 9c5dfd2..9c41538 100644 --- a/misrc_tools/common/ddd_protocol.h +++ b/misrc_tools/common/ddd_protocol.h @@ -76,6 +76,11 @@ bool ddd_clockgen_candidate_is_preferred( 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; 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/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c index 130eae7..0c8ddc9 100644 --- a/misrc_tools/test/ddd_protocol_test.c +++ b/misrc_tools/test/ddd_protocol_test.c @@ -155,6 +155,62 @@ static bool test_clockgen_profile_selection(void) 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}; @@ -270,6 +326,7 @@ int main(void) { if (!test_profiles_and_rates() || !test_clockgen_profile_selection() || + !test_reconnect_path_selection() || !test_topology_and_endpoint() || !test_lifecycle() || !test_validators()) { From 7c94afb5e57419f2c2edf6e799f158e86395caf9 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 05:50:26 +0800 Subject: [PATCH 12/16] ddd: add protocol-v1 RF rate policy --- misrc_tools/common/ddd_protocol.c | 39 +++++++++++++++++++ misrc_tools/common/ddd_protocol.h | 20 ++++++++++ misrc_tools/test/ddd_protocol_test.c | 57 ++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/misrc_tools/common/ddd_protocol.c b/misrc_tools/common/ddd_protocol.c index 623732f..21930d1 100644 --- a/misrc_tools/common/ddd_protocol.c +++ b/misrc_tools/common/ddd_protocol.c @@ -173,6 +173,45 @@ 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 && diff --git a/misrc_tools/common/ddd_protocol.h b/misrc_tools/common/ddd_protocol.h index 9c41538..f06db1d 100644 --- a/misrc_tools/common/ddd_protocol.h +++ b/misrc_tools/common/ddd_protocol.h @@ -99,6 +99,26 @@ bool ddd_profile_supports_decimation(ddd_device_profile_t profile, 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, diff --git a/misrc_tools/test/ddd_protocol_test.c b/misrc_tools/test/ddd_protocol_test.c index 0c8ddc9..43a3a9e 100644 --- a/misrc_tools/test/ddd_protocol_test.c +++ b/misrc_tools/test/ddd_protocol_test.c @@ -104,6 +104,62 @@ static bool test_profiles_and_rates(void) 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, @@ -325,6 +381,7 @@ static bool test_validators(void) int main(void) { if (!test_profiles_and_rates() || + !test_v1_rate_plans() || !test_clockgen_profile_selection() || !test_reconnect_path_selection() || !test_topology_and_endpoint() || From 2376628a213e6361ed9ce7dcd529900ee1ccdef5 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 06:12:07 +0800 Subject: [PATCH 13/16] ui: fold DdD hardware rates into RF selector --- README.md | 8 +- misrc_tools/misrc_gui/input/gui_capture.c | 31 +++- misrc_tools/misrc_gui/ui/gui_ui.c | 192 +++++++++++++++------- 3 files changed, 163 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 70f992e..9637b2f 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ x86 (AMD/Intel) and ARM64 (Apple M, Snapdragon, RockChip) are fully supported an Building from source? See [INSTALLATION.md](INSTALLATION.md). DdD firmware 3.1 is shown as a separate device profile from legacy DdD -firmware. Its ADC rate control selects the native 40 or 20 MSPS hardware rate; -the existing RF resampler remains an independent optional output setting and -is limited to the selected native rate. Other capture devices keep their -existing rate controls and behavior. +firmware. Its RF rate selector uses the native 40 and 20 MSPS hardware paths; +lower output rates use the 20 MSPS hardware stream as the source for the +existing software resampler. This routing applies only to the protocol-v1 DdD +profile. Other capture devices keep their existing rate controls and behavior. ## Setup With Devices diff --git a/misrc_tools/misrc_gui/input/gui_capture.c b/misrc_tools/misrc_gui/input/gui_capture.c index 972f62a..4fedebf 100644 --- a/misrc_tools/misrc_gui/input/gui_capture.c +++ b/misrc_tools/misrc_gui/input/gui_capture.c @@ -365,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; @@ -1912,6 +1927,7 @@ 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 @@ -1923,6 +1939,19 @@ int gui_app_start_capture(gui_app_t *app) { 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 @@ -1934,7 +1963,7 @@ int gui_app_start_capture(gui_app_t *app) { return -1; } int ddd_rc = dev->ddd_profile == DDD_DEVICE_PROTOCOL_V1 - ? gui_ddd_v1_start(app, app->settings.ddd_decimation, + ? gui_ddd_v1_start(app, v1_rate_plan.decimation_factor, gui_ddd_get_test_mode()) : gui_ddd_start(app); if (ddd_rc == 0) { diff --git a/misrc_tools/misrc_gui/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c index a45de9b..91f79da 100644 --- a/misrc_tools/misrc_gui/ui/gui_ui.c +++ b/misrc_tools/misrc_gui/ui/gui_ui.c @@ -1148,9 +1148,6 @@ static char settings_flac_level_display[64]; static char settings_flac_threads_display[64]; static char settings_resample_a_display[32]; static char settings_resample_b_display[32]; -#ifdef ENABLE_DDD -static char settings_ddd_rate_display[32]; -#endif static char status_sample_rate_display[32]; static char status_samples_display[32]; static char status_frames_display[32]; @@ -1679,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; @@ -2473,6 +2528,7 @@ static void render_settings_panel(gui_app_t *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); @@ -2891,47 +2947,58 @@ CLAY(CLAY_ID("SettingsOutputPath"), { } } - // Firmware 3.1 exposes its ADC decimation independently - // from the existing optional output resampler. No other - // device sees or reads this control. -#ifdef ENABLE_DDD if (settings_ddd_v1_mode) { - snprintf(settings_ddd_rate_display, - sizeof(settings_ddd_rate_display), - "%u MSPS (native)", - app->settings.ddd_decimation == - DDD_DECIMATION_HALF_RATE ? 20u : 40u); + 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); + } + 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; - CLAY(CLAY_ID("DddHardwareRateRow"), { .layout = { .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(28) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, .childGap = 10 } }) { - CLAY_TEXT(CLAY_STRING("DdD ADC rate:"), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_NORMAL, .textColor = to_clay_color(ddd_rate_fg) })); - CLAY(CLAY_ID("DddHardwareRateBox"), { .layout = { .sizing = { CLAY_SIZING_FIXED(150), 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_ddd_rate_display), CLAY_TEXT_CONFIG({ .fontSize = FONT_SIZE_STATS, .textColor = to_clay_color(ddd_rate_fg) })); + 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("Output A"), 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) })); - // 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) })); - } - 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) })); + 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) })); + } } } @@ -6912,6 +6979,7 @@ void gui_handle_interactions(gui_app_t *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); @@ -7014,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) { @@ -7022,38 +7091,35 @@ void gui_handle_interactions(gui_app_t *app) { } gui_settings_save(&app->settings); } + if (Clay_PointerOver(CLAY_ID("ResampleRateABox"))) { #ifdef ENABLE_DDD - if (settings_ddd_v1_mode && - Clay_PointerOver(CLAY_ID("DddHardwareRateBox"))) { - if (app->is_capturing) { - gui_app_set_status(app, - "Stop capture before changing the DdD ADC rate"); - } else { - app->settings.ddd_decimation = - app->settings.ddd_decimation == - DDD_DECIMATION_FULL_RATE - ? DDD_DECIMATION_HALF_RATE - : DDD_DECIMATION_FULL_RATE; - settings_base_rate_a_khz = (float)ddd_sample_rate_khz( - app->settings.ddd_decimation); - if (app->settings.resample_rate_a > - settings_base_rate_a_khz) { - app->settings.resample_rate_a = - settings_base_rate_a_khz; + 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); - gui_app_set_status(app, - app->settings.ddd_decimation == - DDD_DECIMATION_HALF_RATE - ? "DdD 3.1 native ADC rate set to 20 MSPS" - : "DdD 3.1 native ADC rate set to 40 MSPS"); } } -#endif - 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); - } if (Clay_PointerOver(CLAY_ID("ToggleResampleB"))) { if (settings_b_controls_disabled) { if (settings_ddd_mode) { From ba065290c62f118cc7b6dd4fae78d3eb69194dff Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 06:33:36 +0800 Subject: [PATCH 14/16] fix(ddd): preserve v1 recovery guidance --- README.md | 13 ++-- misrc_tools/common/device_enum.c | 2 +- misrc_tools/misrc_gui/input/gui_capture.c | 4 +- misrc_tools/misrc_gui/input/gui_ddd_v1.c | 72 ++++++++++++++++++----- 4 files changed, 67 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9637b2f..221aa2c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A universal cross platform GUI tool for interfacing with and visualizing monitor - CXADC (single cards and Clockgen Mod with sound) - HSDAOH - FX3 (Generic tinkering firmware support) -- DdD (DomesDay Duplicator; legacy firmware and firmware 3.1 profiles) +- DdD (DomesDay Duplicator; legacy and protocol-v1 firmware profiles) - FX3ADC (100mhz MUSE capture device) ## Downloads @@ -29,11 +29,12 @@ x86 (AMD/Intel) and ARM64 (Apple M, Snapdragon, RockChip) are fully supported an Building from source? See [INSTALLATION.md](INSTALLATION.md). -DdD firmware 3.1 is shown as a separate device profile from legacy DdD -firmware. Its RF rate selector uses the native 40 and 20 MSPS hardware paths; -lower output rates use the 20 MSPS hardware stream as the source for the -existing software resampler. This routing applies only to the protocol-v1 DdD -profile. Other capture devices keep their existing rate controls and behavior. +DdD protocol-v1 firmware, introduced in firmware 3.1, is shown as a separate +device profile from legacy DdD firmware. Its RF rate selector uses the native +40 and 20 MSPS hardware paths; lower output rates use the 20 MSPS hardware +stream as the source for the existing software resampler. This routing applies +only to the protocol-v1 DdD profile. Other capture devices keep their existing +rate controls and behavior. ## Setup With Devices diff --git a/misrc_tools/common/device_enum.c b/misrc_tools/common/device_enum.c index d067bfd..9b5a844 100644 --- a/misrc_tools/common/device_enum.c +++ b/misrc_tools/common/device_enum.c @@ -323,7 +323,7 @@ int misrc_device_enumerate_ddd(misrc_device_list_t *list, bool include_hsdaoh, "Domesday Duplicator (legacy firmware)"); } else if (profile == DDD_DEVICE_PROTOCOL_V1) { snprintf(dev->name, sizeof(dev->name), - "Domesday Duplicator (firmware 3.1)"); + "Domesday Duplicator"); } else { snprintf(dev->name, sizeof(dev->name), "Domesday Duplicator (unsupported protocol %u)", diff --git a/misrc_tools/misrc_gui/input/gui_capture.c b/misrc_tools/misrc_gui/input/gui_capture.c index 4fedebf..87caaea 100644 --- a/misrc_tools/misrc_gui/input/gui_capture.c +++ b/misrc_tools/misrc_gui/input/gui_capture.c @@ -1958,7 +1958,9 @@ int gui_app_start_capture(gui_app_t *app) { ? 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; } diff --git a/misrc_tools/misrc_gui/input/gui_ddd_v1.c b/misrc_tools/misrc_gui/input/gui_ddd_v1.c index dbb0565..23b2b6e 100644 --- a/misrc_tools/misrc_gui/input/gui_ddd_v1.c +++ b/misrc_tools/misrc_gui/input/gui_ddd_v1.c @@ -275,7 +275,11 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) size_t match_count = 0; int result; - if (!app || !stable_usb_path || !stable_usb_path[0]) return -1; + 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; @@ -289,9 +293,13 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) #else result = libusb_init(&s_context); #endif - if (result != 0) return -1; + 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; } @@ -313,6 +321,8 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) 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; @@ -321,14 +331,14 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) 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 3.1 requires USB 3 SuperSpeed"); + 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 3.1 USB stream descriptor mismatch"); + gui_app_set_status(app, "DdD USB stream descriptor mismatch"); libusb_free_device_list(devices, 1); ddd_v1_close(); return -1; @@ -336,7 +346,7 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) 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 3.1 device"); + gui_app_set_status(app, "Failed to open DdD device"); ddd_v1_close(); return -1; } @@ -345,7 +355,7 @@ int gui_ddd_v1_open(gui_app_t *app, const char *stable_usb_path) #endif result = libusb_claim_interface(s_handle, DDD_STREAM_INTERFACE_NUMBER); if (result != 0) { - gui_app_set_status(app, "Failed to claim DdD 3.1 USB interface"); + gui_app_set_status(app, "Failed to claim DdD USB interface"); ddd_v1_close(); return -1; } @@ -513,7 +523,15 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) bool display_started = false; thrd_t thread; - if (!app || !s_handle || !ddd_decimation_is_supported(decimation)) return -1; + 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); @@ -544,6 +562,7 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) 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; } @@ -551,6 +570,7 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) 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; } @@ -571,8 +591,8 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) ddd_v1_stop_workers(app, display_started); ddd_v1_close(); gui_app_set_status(app, unsafe - ? "DdD 3.1 rollback failed; unplug and reconnect it" - : "DdD 3.1 configuration/readback failed"); + ? "DdD rollback failed; unplug and reconnect it" + : "DdD configuration/readback failed"); return -1; } ddd_sequence_validator_init(&s_sequence); @@ -581,13 +601,19 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) 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); - if (ddd_collection_rollback_v1(&ops, &s_collection) != DDD_PROTOCOL_OK) { + 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; @@ -603,12 +629,12 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) s_collection.identity, sizeof(s_collection.identity), commit, sizeof(commit)); snprintf(message, sizeof(message), - "DdD 3.1 capture started (path=%s, %u MSPS, test=%s, gateware=%s)", + "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 3.1 capture running"); + gui_app_set_status(app, "DdD capture running"); return 0; } if (atomic_load(&s_startup_failed) || @@ -620,14 +646,24 @@ int gui_ddd_v1_start(gui_app_t *app, uint8_t decimation, bool test_mode) 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(); - gui_app_set_status(app, "DdD 3.1 stream did not become ready"); + 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; } @@ -663,9 +699,13 @@ void gui_ddd_v1_stop(gui_app_t *app) ddd_v1_result_name(s_result)); ddd_v1_close(); atomic_store(&app->stream_synced, false); - gui_app_set_status(app, unsafe - ? "DdD 3.1 stop unverified; unplug and reconnect it" - : "DdD 3.1 capture stopped"); + 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) From 1771e3cfd59dc65dfc5c233f958e92f7d9255a88 Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 14:09:26 +0800 Subject: [PATCH 15/16] Rename DdD RF channel labels --- misrc_tools/misrc_gui/ui/gui_ui.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misrc_tools/misrc_gui/ui/gui_ui.c b/misrc_tools/misrc_gui/ui/gui_ui.c index 91f79da..579ab3d 100644 --- a/misrc_tools/misrc_gui/ui/gui_ui.c +++ b/misrc_tools/misrc_gui/ui/gui_ui.c @@ -2973,7 +2973,7 @@ CLAY(CLAY_ID("SettingsOutputPath"), { 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("Output A"), 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) })); } @@ -3010,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; From 9b33fe43ed038e1d5c306f6e4db1353756db085e Mon Sep 17 00:00:00 2001 From: Ninkun Date: Tue, 1 Sep 2026 14:35:30 +0800 Subject: [PATCH 16/16] Simplify DdD README setup note --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 221aa2c..d1eff21 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A universal cross platform GUI tool for interfacing with and visualizing monitor - CXADC (single cards and Clockgen Mod with sound) - HSDAOH - FX3 (Generic tinkering firmware support) -- DdD (DomesDay Duplicator; legacy and protocol-v1 firmware profiles) +- DdD (DomesDay Duplicator) - FX3ADC (100mhz MUSE capture device) ## Downloads @@ -29,17 +29,18 @@ x86 (AMD/Intel) and ARM64 (Apple M, Snapdragon, RockChip) are fully supported an Building from source? See [INSTALLATION.md](INSTALLATION.md). -DdD protocol-v1 firmware, introduced in firmware 3.1, is shown as a separate -device profile from legacy DdD firmware. Its RF rate selector uses the native -40 and 20 MSPS hardware paths; lower output rates use the 20 MSPS hardware -stream as the source for the existing software resampler. This routing applies -only to the protocol-v1 DdD profile. Other capture devices keep their existing -rate controls and behavior. - ## 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