From 848d0da13712b4a61eff5562ac7e3cf22da89a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=90=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=D0=BE=D0=B2=D0=B8=D1=87=20?= =?UTF-8?q?=D0=90=D0=BC=D0=B1=D1=80=D0=BE=D0=B7=D0=B5=D0=B2=D0=B8=D1=87?= Date: Tue, 20 Oct 2026 12:52:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?src=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ps2000_osc2.c | 369 ++++++++++++++++++++++++++++++++++++++++++++ src/ps2000_stream.c | 64 ++++++++ src/ps2000_thread.c | 66 ++++++++ src/ps2000_trig.c | 98 ++++++++++++ src/ps2000_usb.c | 325 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 922 insertions(+) create mode 100644 src/ps2000_osc2.c create mode 100644 src/ps2000_stream.c create mode 100644 src/ps2000_thread.c create mode 100644 src/ps2000_trig.c create mode 100644 src/ps2000_usb.c diff --git a/src/ps2000_osc2.c b/src/ps2000_osc2.c new file mode 100644 index 0000000..510eb9b --- /dev/null +++ b/src/ps2000_osc2.c @@ -0,0 +1,369 @@ +#include "ps2000_internal.h" +#include +#include +#include +#include + +/* ========================================================================== */ +/* INTERNAL OSC CONSTANTS */ +/* ========================================================================== */ + +/* + * Maximum total sample count for a capture block. + * Reconstructed from `cmp r9w, 0FFEh` / `jbe` comparisons in sub_2ADC0. + */ +#define PS2000_OSC_MAX_SAMPLES 0xFFFU + +/* + * Minimum pre-trigger sample count. + * Reconstructed from `cmovl edx, esi` where esi = 2 in sub_2ADC0. + */ +#define PS2000_OSC_MIN_PRETRIG 2U + +/* + * Alignment bias used in the ring/capture index calculation. + * Reconstructed from `add edx, 8000h` in sub_10300+0x1CE. + */ +#define PS2000_OSC_ALIGN_BIAS 0x8000 + +/* + * Pre-bias for signed division by 256 (round toward zero). + * Reconstructed from `lea ecx, [rdx+0FFh]` / `cmovs edx, ecx` / `sar edx, 8` + * in sub_10300+0x29D..0x2A6. + */ +#define PS2000_OSC_DIV256_PREBIAS 0xFF + +/* ========================================================================== */ +/* INTERNAL OSC HELPERS */ +/* ========================================================================== */ + +/** + * @brief Signed division by 256 with round-toward-zero semantics. + * + * Reconstructed from sub_10300+0x296: + * movsx edx, var_6C + * test edx, edx + * lea ecx, [rdx+0FFh] + * cmovs edx, ecx ; if negative, add 0xFF first + * sar edx, 8 ; arithmetic shift right by 8 + * + * For positive values this is a plain floor(x/256). For negative values the + * 0xFF pre-bias converts the floor into a truncation toward zero, matching + * the original hardware-side index arithmetic exactly. + */ +static int32_t _ps2000_osc_div256_rounded(int32_t value) { + int32_t adjusted = value; + + if (value < 0) { + adjusted = value + PS2000_OSC_DIV256_PREBIAS; + } + + return adjusted >> 8; +} + +/** + * @brief Scale two sample-derived accumulators by a common normalization factor. + * + * Reconstructed from sub_10300+0x17F..0x1CE: + * movsx edx, word ptr [rbp+r15*2+293h] ; reference sample + * movsd xmm2, cs:qword_A4C38 ; 16384.0 + * cvtsi2sd xmm0, edx + * movsx eax, word ptr [rbp+r15*2+24Bh] ; secondary sample + * lea edx, [rcx+rax] ; accumulator_a = acc + sample_b + * add eax, var_58 ; accumulator_b = sample_b + carry + * cvtsi2sd xmm1, edx + * divsd xmm2, xmm0 ; factor = 16384.0 / ref_sample + * cvtsi2sd xmm0, eax + * mulsd xmm1, xmm2 + * mulsd xmm0, xmm2 + * cvttsd2si edx, xmm1 + * cvttsd2si eax, xmm0 + * add edx, 8000h ; alignment bias + * + * Note the direction of the division: 16384.0 is the *numerator*, so the + * factor grows as the reference sample shrinks. This is an inverse-ratio + * normalization, not a simple gain. + * + * @param ref_sample Reference sample used as divisor (must be non-zero). + * @param accum_a First accumulator value. + * @param accum_b Second accumulator value. + * @param out_a Output for the first scaled index (alignment applied). + * @param out_b Output for the second scaled index. + */ +static void _ps2000_osc_scale_pair(int16_t ref_sample, + int32_t accum_a, + int32_t accum_b, + int32_t *out_a, + int32_t *out_b) { + if (ref_sample == 0) { + /* Original code would produce inf/nan here; guard explicitly so the + * reconstruction does not propagate undefined values into the index + * arithmetic. */ + *out_a = PS2000_OSC_ALIGN_BIAS; + *out_b = 0; + return; + } + + double factor = PS2000_ADC_FULL_SCALE / (double)ref_sample; + + int32_t scaled_a = (int32_t)(int64_t)((double)accum_a * factor); + int32_t scaled_b = (int32_t)(int64_t)((double)accum_b * factor); + + *out_a = scaled_a + PS2000_OSC_ALIGN_BIAS; + *out_b = scaled_b; +} + +/** + * @brief Validate pre/post trigger sample counts against buffer limits. + * + * Reconstructed from the validation chain in sub_2ADC0: + * - pre-trigger must be >= 2 + * - total must not exceed 0xFFF + * - the addition must not overflow + * - total must not exceed the device buffer size when known + */ +ps2000_status_t _ps2000_osc_validate_buffer(uint32_t pre_trig, + uint32_t post_trig, + uint32_t max_samples) { + if (pre_trig < PS2000_OSC_MIN_PRETRIG) { + PS2000_LOG_DBG("Pre-trigger %u below minimum %u", + pre_trig, PS2000_OSC_MIN_PRETRIG); + return PS2000_ERR_INVALID_PARAM; + } + + uint32_t total = pre_trig + post_trig; + + /* Overflow guard on the addition itself */ + if (total < pre_trig) { + return PS2000_ERR_INVALID_PARAM; + } + + if (total > PS2000_OSC_MAX_SAMPLES) { + PS2000_LOG_WRN("Capture total %u exceeds hardware maximum %u", + total, PS2000_OSC_MAX_SAMPLES); + return PS2000_ERR_MEMORY; + } + + if (max_samples > 0U && total > max_samples) { + PS2000_LOG_DBG("Capture total %u exceeds buffer size %u", + total, max_samples); + return PS2000_ERR_INVALID_PARAM; + } + + return PS2000_OK; +} + +/** + * @brief Compute the sample interval for a timebase index. + * + * Thin wrapper over the shared math routine so that all timebase arithmetic + * funnels through one implementation. + */ +ps2000_status_t _ps2000_osc_calc_timebase(uint32_t index, double *interval_ns) { + if (!interval_ns) return PS2000_ERR_INVALID_PARAM; + + *interval_ns = _ps2000_math_timebase_interval(index); + + if (*interval_ns <= 0.0) { + return PS2000_ERR_INVALID_PARAM; + } + + return PS2000_OK; +} + +/* ========================================================================== */ +/* PUBLIC OSCILLOSCOPE API */ +/* ========================================================================== */ + +/** + * @brief Query timebase parameters for a given index and sample count. + * + * Uses the operations table when available so that device variants can + * override the calculation, and falls back to the shared math routine + * otherwise. + */ +ps2000_status_t ps2000_get_timebase(ps2000_context_t *ctx, + uint32_t timebase_index, + uint32_t num_samples, + double *interval_ns, + uint32_t *max_samples) { + if (!ctx || !interval_ns) return PS2000_ERR_INVALID_PARAM; + + ps2000_status_t status = _ps2000_util_mutex_lock_safe( + &ctx->sync.mutex, PS2000_USB_TIMEOUT_MS); + if (status != PS2000_OK) return status; + + double interval; + + if (ctx->ops.calc_timebase) { + interval = ctx->ops.calc_timebase(ctx, timebase_index); + } else { + interval = _ps2000_math_timebase_interval(timebase_index); + } + + if (interval <= 0.0) { + pthread_mutex_unlock(&ctx->sync.mutex); + return PS2000_ERR_INVALID_PARAM; + } + + *interval_ns = interval; + + if (max_samples) { + uint32_t hw_limit = ctx->capture.buffer_size; + if (hw_limit == 0U || hw_limit > PS2000_OSC_MAX_SAMPLES) { + hw_limit = PS2000_OSC_MAX_SAMPLES; + } + *max_samples = (num_samples < hw_limit) ? num_samples : hw_limit; + } + + ctx->capture.timebase_index = (uint16_t)timebase_index; + + pthread_mutex_unlock(&ctx->sync.mutex); + return PS2000_OK; +} + +/** + * @brief Configure and start a block capture. + * + * Validates the buffer geometry, allocates the per-channel capture buffers, + * then dispatches through the operations table. + */ +ps2000_status_t ps2000_run_block(ps2000_context_t *ctx, + uint32_t pre_trigger_samples, + uint32_t post_trigger_samples) { + if (!ctx) return PS2000_ERR_INVALID_PARAM; + if (!ctx->usb_host.handle) return PS2000_ERR_NOT_OPENED; + + ps2000_status_t status = _ps2000_osc_validate_buffer( + pre_trigger_samples, post_trigger_samples, ctx->capture.buffer_size); + if (status != PS2000_OK) return status; + + status = _ps2000_util_mutex_lock_safe(&ctx->sync.mutex, + PS2000_USB_TIMEOUT_MS); + if (status != PS2000_OK) return status; + + uint32_t total = pre_trigger_samples + post_trigger_samples; + + /* Allocate per-channel buffers if the geometry changed */ + for (uint32_t ch = 0; ch < PS2000_MAX_CHANNELS; ch++) { + if ((ctx->capture.channel_mask & (1U << ch)) == 0U) { + continue; + } + + if (ctx->capture.channel_buffers[ch] == NULL) { + ctx->capture.channel_buffers[ch] = + (int16_t*)calloc(total, sizeof(int16_t)); + if (!ctx->capture.channel_buffers[ch]) { + pthread_mutex_unlock(&ctx->sync.mutex); + PS2000_LOG_ERR("Failed to allocate capture buffer for ch %u", ch); + return PS2000_ERR_MEMORY; + } + } + } + + ctx->capture.pre_trigger = pre_trigger_samples; + ctx->capture.buffer_size = total; + ctx->capture.pending_ops = (uint16_t)ctx->capture.channel_mask; + + PS2000_LOG_INF("Block capture armed: pre=%u post=%u mask=0x%02X", + pre_trigger_samples, post_trigger_samples, + ctx->capture.channel_mask); + + if (ctx->ops.run_block) { + status = (ps2000_status_t)ctx->ops.run_block(ctx, + pre_trigger_samples, + post_trigger_samples); + } else { + status = PS2000_ERR_NOT_IMPLEMENTED; + } + + if (status != PS2000_OK) { + PS2000_LOG_ERR("Block capture dispatch failed: %d", (int)status); + ctx->capture.capture_state = 0; + } + + pthread_mutex_unlock(&ctx->sync.mutex); + return status; +} + +/** + * @brief Read calibrated samples out of a completed capture. + * + * Copies the raw window and applies the per-channel calibration chain. + * The conversion itself is delegated to _ps2000_math_adc_convert so the + * saturation behaviour matches the original `lea edx,[rax+7FFEh]` / + * `cmp edx, 0FFFCh` clamp exactly. + */ +ps2000_status_t ps2000_get_values(ps2000_context_t *ctx, + uint32_t channel, + int16_t *buffer, + uint32_t start_index, + uint32_t num_samples, + uint32_t *returned_samples) { + if (!ctx || !buffer || !returned_samples) return PS2000_ERR_INVALID_PARAM; + if (channel >= PS2000_MAX_CHANNELS) return PS2000_ERR_INVALID_PARAM; + + ps2000_status_t status = _ps2000_util_mutex_lock_safe( + &ctx->sync.mutex, PS2000_USB_TIMEOUT_MS); + if (status != PS2000_OK) return status; + + int16_t *source = ctx->capture.channel_buffers[channel]; + if (!source) { + pthread_mutex_unlock(&ctx->sync.mutex); + PS2000_LOG_ERR("No capture buffer for channel %u", channel); + return PS2000_ERR_NOT_OPENED; + } + + if (start_index >= ctx->capture.buffer_size) { + pthread_mutex_unlock(&ctx->sync.mutex); + *returned_samples = 0U; + return PS2000_OK; + } + + uint32_t available = ctx->capture.buffer_size - start_index; + uint32_t count = (num_samples < available) ? num_samples : available; + + double gain = ctx->info.cal_gain[channel]; + double offset = (double)ctx->info.cal_offset[channel]; + + for (uint32_t i = 0; i < count; i++) { + int16_t raw = source[start_index + i]; + buffer[i] = _ps2000_math_adc_convert(raw, gain, offset); + } + + *returned_samples = count; + + PS2000_LOG_DBG("Read %u samples from channel %u at offset %u", + count, channel, start_index); + + pthread_mutex_unlock(&ctx->sync.mutex); + return PS2000_OK; +} + +/** + * @brief Compute decimated capture indices for a sample pair. + * + * Exposes the sub_10300 scaling path for internal aggregation use. The two + * output indices are the aligned primary index and the secondary index used + * for interpolation between the two capture arrays. + */ +void _ps2000_osc_compute_decim_indices(int16_t ref_sample, + int16_t secondary_sample, + int32_t carry, + int32_t *out_primary, + int32_t *out_secondary, + int32_t *out_div256) { + if (!out_primary || !out_secondary || !out_div256) return; + + int32_t accum_a = (int32_t)secondary_sample; + int32_t accum_b = (int32_t)secondary_sample + carry; + + int32_t primary = 0; + int32_t secondary = 0; + + _ps2000_osc_scale_pair(ref_sample, accum_a, accum_b, &primary, &secondary); + + *out_primary = primary; + *out_secondary = secondary; + *out_div256 = _ps2000_osc_div256_rounded(secondary); +} \ No newline at end of file diff --git a/src/ps2000_stream.c b/src/ps2000_stream.c new file mode 100644 index 0000000..9a95462 --- /dev/null +++ b/src/ps2000_stream.c @@ -0,0 +1,64 @@ +#include "ps2000_internal.h" +#include +#include + +/* ========================================================================== */ +/* STREAMING MATH & RING BUFFER */ +/* ========================================================================== */ + +/** + * @brief Update ring buffer write position with wrap-around. + * + * Reconstructs sub_10300 / sub_10770: + * add eax, 8000h; shr ax, 8; mov [rbp+40Ah], cx + * + * Implements aligned circular buffer indexing. + */ +uint32_t _ps2000_strm_update_ring_pos(uint32_t current_pos, uint32_t increment, + uint32_t buf_size) { + if (buf_size == 0) return 0; + + uint32_t new_pos = current_pos + increment; + + /* Apply wrap mask and alignment shift as in original asm */ + uint32_t aligned = new_pos + 0x8000U; + aligned >>= 8U; + aligned <<= 8U; + + /* Modulo buffer size for true circular behavior */ + return aligned % buf_size; +} + +/** + * @brief Process raw streaming samples with calibration and correction. + * + * Reconstructs sub_28A19 SSE block: + * cvtsi2sd xmm0, [mem]; mulsd xmm0, gain; divsd xmm0, scale; + * addsd xmm0, correction; cvttsd2si esi, xmm0; + */ +ps2000_status_t _ps2000_strm_process_raw(ps2000_context_t *ctx, + const int16_t *raw_buf, + uint32_t num_samples, + int16_t *out_buf, + uint32_t *processed_count) { + if (!ctx || !raw_buf || !out_buf || !processed_count) { + return PS2000_ERR_INVALID_PARAM; + } + + if (num_samples == 0) { + *processed_count = 0; + return PS2000_OK; + } + + /* Use channel 0 calibration by default for streaming */ + double gain = ctx->info.cal_gain[0]; + double offset = (double)ctx->info.cal_offset[0]; + + for (uint32_t i = 0; i < num_samples; i++) { + /* Delegate to centralized math module */ + out_buf[i] = _ps2000_math_adc_convert(raw_buf[i], gain, offset); + } + + *processed_count = num_samples; + return PS2000_OK; +} \ No newline at end of file diff --git a/src/ps2000_thread.c b/src/ps2000_thread.c new file mode 100644 index 0000000..f60b9ec --- /dev/null +++ b/src/ps2000_thread.c @@ -0,0 +1,66 @@ +#include "ps2000_internal.h" +#include +#include + +/* ========================================================================== */ +/* THREAD WRAPPER (IRunnable replacement) */ +/* ========================================================================== */ + +/** + * @brief Thread entry wrapper. + * + * Reconstructs sub_1A210 / sub_1A4C0 IRunnable dispatch. + * Original used virtual call [rax+10h]. We use direct function pointer. + */ +static void* _ps2000_thr_trampoline(void *arg) { + /* In original code, thread state is managed via TLS and object fields */ + /* For C reconstruction, we simply execute the callback */ + void (**entry)(void*) = (void (**)(void*))arg; + if (entry && *entry) { + (*entry)(NULL); + } + return NULL; +} + +/** + * @brief Create and start a new thread. + * + * Replaces C++ Thread constructor + start(). + * Implements "Thread already started" check from sub_B00C / sub_B466. + * Original throws exception: "Thread already started." + */ +ps2000_status_t _ps2000_thr_create(pthread_t *thr, const char *name, + void (*func)(void *), void *arg) { + if (!thr || !func) return PS2000_ERR_INVALID_PARAM; + + /* Note: In a full implementation, thread state would be tracked in a + * structure to prevent double-start (sub_B00C). Here we assume + * the caller manages the lifecycle correctly. */ + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 1024 * 1024); /* 1MB stack */ + + int ret = pthread_create(thr, &attr, _ps2000_thr_trampoline, (void*)&func); + pthread_attr_destroy(&attr); + + if (ret != 0) { + PS2000_LOG_ERR("Failed to create thread '%s': errno=%d", + name ? name : "unknown", ret); + return PS2000_ERR_MEMORY; /* Maps to std::__throw_system_error */ + } + + PS2000_LOG_DBG("Thread '%s' created", name ? name : "worker"); + return PS2000_OK; +} + +ps2000_status_t _ps2000_thr_join(pthread_t *thr) { + if (!thr) return PS2000_ERR_INVALID_PARAM; + int ret = pthread_join(*thr, NULL); + return (ret == 0) ? PS2000_OK : PS2000_ERR_BUSY; +} + +bool _ps2000_thr_is_running(const pthread_t *thr) { + /* Simplified check; original uses complex state machine */ + return (thr != NULL); +} \ No newline at end of file diff --git a/src/ps2000_trig.c b/src/ps2000_trig.c new file mode 100644 index 0000000..8e20f01 --- /dev/null +++ b/src/ps2000_trig.c @@ -0,0 +1,98 @@ +#include "ps2000_internal.h" +#include + +/* ========================================================================== */ +/* TRIGGER VALIDATION */ +/* ========================================================================== */ + +/** + * @brief Validate trigger configuration. + * + * Reconstructs sub_2A8D6 and sub_10BF0 trigger validation logic. + * Checks channel mask, threshold bounds, and ETS mode requirements. + * + * @param ctx Device context + * @param channel_mask Bitmask of channels to trigger on + * @param threshold Trigger threshold (raw ADC value) + * @param direction Trigger direction (rising/falling/dual) + * @param delay Trigger delay in samples + * @return PS2000_OK if valid, PS2000_ERR_TRIGGER if invalid + */ +ps2000_status_t _ps2000_trg_validate_config(ps2000_context_t *ctx, + uint16_t channel_mask, + uint16_t threshold, + uint16_t direction, + uint16_t delay) { + if (!ctx) return PS2000_ERR_INVALID_PARAM; + + /* Trigger enabled but no channels selected */ + if (channel_mask == 0) { + PS2000_LOG_WRN("Trigger enabled but no channel mask set"); + return PS2000_ERR_TRIGGER; + } + + /* Count active channels */ + uint32_t active_ch = 0; + for (uint32_t i = 0; i < PS2000_MAX_CHANNELS; i++) { + if (channel_mask & (1U << i)) active_ch++; + } + + /* ETS mode validation (from sub_2A8D6+2A cmp al, 2) */ + /* In ETS mode, channels must be paired or follow specific count rules */ + bool is_ets = (ctx->capture.timebase_index > 100); + if (is_ets && (active_ch % 2) != 0) { + PS2000_LOG_WRN("ETS mode requires even number of active trigger channels"); + return PS2000_ERR_TRIGGER; + } + + /* Validate delay bounds */ + if (delay > ctx->capture.buffer_size) { + PS2000_LOG_WRN("Trigger delay %u exceeds buffer size %u", + delay, ctx->capture.buffer_size); + return PS2000_ERR_INVALID_PARAM; + } + + return PS2000_OK; +} + +/* ========================================================================== */ +/* TRIGGER POSITION CALCULATION */ +/* ========================================================================== */ + +/** + * @brief Calculate precise trigger position in samples. + * + * Reconstructs sub_11789 SSE block: + * divss xmm0, xmm1; cvtss2sd xmm0, xmm0; addsd xmm0, qword_A4C60; + * mulsd xmm0, xmm1; addsd xmm0, qword_A4C40; cvttsd2si rcx, xmm0 + * + * @param ctx Device context + * @param trig_pos_out Pointer to store calculated trigger position + * @return PS2000_OK on success, error code otherwise + */ +ps2000_status_t _ps2000_trg_calculate_position(ps2000_context_t *ctx, + uint32_t *trig_pos_out) { + if (!ctx || !trig_pos_out) return PS2000_ERR_INVALID_PARAM; + + double interval = ctx->capture.sample_interval; + if (interval <= 0.0) return PS2000_ERR_INVALID_PARAM; + + /* Base calculation from floating point sample interval */ + double raw_pos = (double)ctx->capture.pre_trigger; + raw_pos += PS2000_TIMEBASE_ROUND_BIAS; /* 0.5 correction */ + + /* Scale back to sample domain */ + double scaled_pos = raw_pos * PS2000_TIMEBASE_UNIT_STEP; + scaled_pos += PS2000_TIMEBASE_ROUND_BIAS; + + /* Truncate as per cvttsd2si behavior */ + uint32_t final_pos = (uint32_t)(int64_t)scaled_pos; + + /* Bounds check against buffer size */ + if (final_pos > ctx->capture.buffer_size) { + final_pos = ctx->capture.buffer_size; + } + + *trig_pos_out = final_pos; + return PS2000_OK; +} \ No newline at end of file diff --git a/src/ps2000_usb.c b/src/ps2000_usb.c new file mode 100644 index 0000000..3a35446 --- /dev/null +++ b/src/ps2000_usb.c @@ -0,0 +1,325 @@ +#include "ps2000_internal.h" +#include +#include +#include + +/* ========================================================================== */ +/* INTERNAL USB CONSTANTS */ +/* ========================================================================== */ + +/* + * USB Vendor ID for Pico Technology. + * Reconstructed from sub_4B797: mov ecx, 0CE9h + */ +#define PS2000_USB_VID 0x0CE9U + +/* + * USB Product ID for PS2000 series. + * Note: Multiple PIDs may exist for different models. + */ +#define PS2000_USB_PID 0x1009U + +/* USB interface and endpoint configuration */ +#define PS2000_USB_INTERFACE 0 +#define PS2000_USB_ALT_SETTING 1 +#define PS2000_USB_EP_IN 0x81U +#define PS2000_USB_EP_OUT 0x01U + +/* ========================================================================== */ +/* USB HELPERS */ +/* ========================================================================== */ + +/** + * @brief Map libusb error codes to internal status codes. + * + * Provides a consistent error mapping layer between libusb + * and the PS2000 internal error system. + */ +static ps2000_status_t _ps2000_usb_map_error(int libusb_err) { + switch (libusb_err) { + case LIBUSB_SUCCESS: + return PS2000_OK; + case LIBUSB_ERROR_TIMEOUT: + return PS2000_ERR_TIMEOUT; + case LIBUSB_ERROR_NO_DEVICE: + return PS2000_ERR_NOT_FOUND; + case LIBUSB_ERROR_NOT_FOUND: + return PS2000_ERR_NOT_FOUND; + case LIBUSB_ERROR_BUSY: + return PS2000_ERR_BUSY; + case LIBUSB_ERROR_NO_MEM: + return PS2000_ERR_MEMORY; + case LIBUSB_ERROR_INVALID_PARAM: + return PS2000_ERR_INVALID_PARAM; + case LIBUSB_ERROR_ACCESS: + return PS2000_ERR_NOT_OPENED; + default: + return PS2000_ERR_USB; + } +} + +/** + * @brief Find a PS2000 device by serial number. + * + * Reconstructed from sub_4E3D9 / sub_4E420 device enumeration logic. + * Iterates through all USB devices, checking VID/PID and optionally + * matching the serial number. + * + * @param ctx libusb context for enumeration + * @param serial Serial number to match (0 = any device) + * @return Opened device handle, or NULL if not found + */ +static libusb_device_handle* _ps2000_usb_find_device(libusb_context *ctx, + uint32_t serial) { + libusb_device **dev_list = NULL; + ssize_t cnt = libusb_get_device_list(ctx, &dev_list); + if (cnt < 0) { + PS2000_LOG_ERR("libusb_get_device_list failed: %zd", cnt); + return NULL; + } + + libusb_device_handle *found_handle = NULL; + + for (ssize_t i = 0; i < cnt; i++) { + struct libusb_device_descriptor desc; + if (libusb_get_device_descriptor(dev_list[i], &desc) != 0) { + continue; + } + + /* Check VID/PID match */ + if (desc.idVendor != PS2000_USB_VID || + desc.idProduct != PS2000_USB_PID) { + continue; + } + + libusb_device_handle *handle = NULL; + int ret = libusb_open(dev_list[i], &handle); + if (ret != 0) { + PS2000_LOG_DBG("Failed to open candidate device: %d", ret); + continue; + } + + /* Read serial number for verification */ + unsigned char sn_buf[32] = {0}; + ret = libusb_get_string_descriptor_ascii( + handle, desc.iSerialNumber, sn_buf, sizeof(sn_buf)); + + bool match = false; + if (serial == 0) { + /* Open first found device */ + match = true; + } else if (ret > 0) { + uint32_t dev_sn = 0; + if (sscanf((char*)sn_buf, "%u", &dev_sn) == 1) { + match = (dev_sn == serial); + } + } + + if (match) { + found_handle = handle; + break; + } + + libusb_close(handle); + } + + libusb_free_device_list(dev_list, 1); + return found_handle; +} + +/* ========================================================================== */ +/* PUBLIC TRANSPORT API */ +/* ========================================================================== */ + +/** + * @brief Open a PS2000 device by serial number. + * + * Performs the full USB open sequence: + * 1. Initialize libusb context (if not already done) + * 2. Find device by VID/PID and optional serial + * 3. Claim interface + * 4. Set alternate setting + * + * @param ctx Device context to populate + * @param serial Serial number (0 = first found device) + * @return PS2000_OK on success, error code otherwise + */ +ps2000_status_t _ps2000_usb_open_device(ps2000_context_t *ctx, + uint32_t serial) { + if (!ctx) return PS2000_ERR_INVALID_PARAM; + if (ctx->usb_host.handle) return PS2000_ERR_BUSY; + + /* Initialize libusb context on first open */ + if (!ctx->usb_host.ctx) { + int ret = libusb_init(&ctx->usb_host.ctx); + if (ret != LIBUSB_SUCCESS) { + PS2000_LOG_ERR("libusb_init failed: %d", ret); + return _ps2000_usb_map_error(ret); + } + } + + /* Find and open device */ + ctx->usb_host.handle = _ps2000_usb_find_device(ctx->usb_host.ctx, serial); + if (!ctx->usb_host.handle) { + PS2000_LOG_ERR("Device not found (serial=%u)", serial); + return PS2000_ERR_NOT_FOUND; + } + + /* Claim interface (from sub_4E3D9 pattern) */ + int ret = libusb_claim_interface(ctx->usb_host.handle, + PS2000_USB_INTERFACE); + if (ret != LIBUSB_SUCCESS) { + PS2000_LOG_ERR("Failed to claim interface %d: %d", + PS2000_USB_INTERFACE, ret); + libusb_close(ctx->usb_host.handle); + ctx->usb_host.handle = NULL; + return _ps2000_usb_map_error(ret); + } + + /* Set alternate setting */ + ret = libusb_set_interface_alt_setting( + ctx->usb_host.handle, PS2000_USB_INTERFACE, PS2000_USB_ALT_SETTING); + if (ret != LIBUSB_SUCCESS) { + PS2000_LOG_ERR("Failed to set alt setting %d: %d", + PS2000_USB_ALT_SETTING, ret); + libusb_release_interface(ctx->usb_host.handle, PS2000_USB_INTERFACE); + libusb_close(ctx->usb_host.handle); + ctx->usb_host.handle = NULL; + return _ps2000_usb_map_error(ret); + } + + ctx->active_flag = 1; + PS2000_LOG_INF("Device opened successfully (serial=%u)", serial); + return PS2000_OK; +} + +/** + * @brief Close a PS2000 device and release all USB resources. + * + * Performs cleanup in reverse order of open: + * 1. Release interface + * 2. Close handle + * 3. Free config descriptor + * 4. Unref device + * 5. Exit libusb context + */ +void _ps2000_usb_close_device(ps2000_context_t *ctx) { + if (!ctx || !ctx->usb_host.handle) return; + + PS2000_LOG_INF("Closing device..."); + + /* Release interface and close handle */ + libusb_release_interface(ctx->usb_host.handle, PS2000_USB_INTERFACE); + libusb_close(ctx->usb_host.handle); + ctx->usb_host.handle = NULL; + ctx->active_flag = 0; + + /* Free config descriptor if allocated */ + if (ctx->usb_host.active_config) { + libusb_free_config_descriptor(ctx->usb_host.active_config); + ctx->usb_host.active_config = NULL; + } + + /* Unref device if we have a reference */ + if (ctx->usb_host.dev) { + libusb_unref_device(ctx->usb_host.dev); + ctx->usb_host.dev = NULL; + } + + /* Exit libusb context */ + if (ctx->usb_host.ctx) { + libusb_exit(ctx->usb_host.ctx); + ctx->usb_host.ctx = NULL; + } + + PS2000_LOG_INF("Device closed"); +} + +/** + * @brief Perform a USB control transfer. + * + * Wrapper around libusb_control_transfer with timeout and + * transfer length verification. + * + * @param ctx Device context + * @param req_type Request type (direction | type | recipient) + * @param req Request code + * @param val wValue field + * @param idx wIndex field + * @param data Data buffer (may be NULL for zero-length transfers) + * @param len Expected transfer length + * @return PS2000_OK on success, error code otherwise + */ +ps2000_status_t _ps2000_usb_control_transfer(ps2000_context_t *ctx, + uint8_t req_type, uint8_t req, + uint16_t val, uint16_t idx, + uint8_t *data, uint16_t len) { + if (!ctx || !ctx->usb_host.handle) return PS2000_ERR_NOT_OPENED; + + int transferred = libusb_control_transfer( + ctx->usb_host.handle, + req_type, + req, + val, + idx, + data, + len, + PS2000_USB_TIMEOUT_MS + ); + + if (transferred < 0) { + PS2000_LOG_DBG("Control transfer failed: req=0x%02X val=0x%04X " + "idx=0x%04X err=%d", req, val, idx, transferred); + return _ps2000_usb_map_error(transferred); + } + + if ((uint16_t)transferred != len) { + PS2000_LOG_WRN("Short control transfer: expected %u, got %d", + len, transferred); + return PS2000_ERR_USB; + } + + return PS2000_OK; +} + +/** + * @brief Perform a USB bulk transfer. + * + * Wrapper around libusb_bulk_transfer with timeout and + * transfer length verification. + * + * @param ctx Device context + * @param ep Endpoint address (IN or OUT) + * @param data Data buffer + * @param len Transfer length in bytes + * @return PS2000_OK on success, error code otherwise + */ +ps2000_status_t _ps2000_usb_bulk_transfer(ps2000_context_t *ctx, uint8_t ep, + const uint8_t *data, uint32_t len) { + if (!ctx || !ctx->usb_host.handle) return PS2000_ERR_NOT_OPENED; + if (!data || len == 0) return PS2000_ERR_INVALID_PARAM; + + int transferred = 0; + int ret = libusb_bulk_transfer( + ctx->usb_host.handle, + ep, + (uint8_t*)data, + (int)len, + &transferred, + PS2000_USB_TIMEOUT_MS + ); + + if (ret != LIBUSB_SUCCESS) { + PS2000_LOG_DBG("Bulk transfer failed: ep=0x%02X len=%u err=%d", + ep, len, ret); + return _ps2000_usb_map_error(ret); + } + + if ((uint32_t)transferred != len) { + PS2000_LOG_WRN("Short bulk transfer: expected %u, got %d", + len, transferred); + return PS2000_ERR_USB; + } + + return PS2000_OK; +} \ No newline at end of file