From d54bec26adc085afeaed1e297659c87a7ec11fc7 Mon Sep 17 00:00:00 2001 From: Luke Fulcomer Date: Thu, 1 Oct 2026 10:10:10 -0700 Subject: [PATCH] Add dslcap, a headless capture command-line tool dslcap drives a DSLogic through libsigrok4DSL without the GUI, for scripted and automated captures: dslcap --list dslcap --channels 0,1 --samplerate 10000000 --samples 1000000 \ [--vth 1.6] [--mode buffer|stream] [--trigger CH[:R|F|C|1|0]] \ [--trigpos PERCENT] [--timeout SEC] [--res DIR] --out /path/base It writes .bin (each enabled channel's samples packed LSB-first) and prints one JSON object describing the capture on stdout. Every failure prints a JSON object with an "error" key and exits non-zero (1 runtime/I/O error, 2 invalid argument or unavailable setting, 3 failed capture). - Validates all options before the library is initialised: whole-string numeric parses with range checks, finite --vth (0-5 V) and --timeout, positive rate and sample count, channel lists of plain indexes without duplicates, --mode exactly buffer or stream, --trigpos 0-100 (and small enough that its share of the sample limit fits the driver's 32-bit trigger position), and a trigger type of exactly one of R F C 1 0. - Accepts only the samplerates the device reports, and picks a channel mode that supports the requested rate and channel count; triggers must be on an enabled channel. - Checks every configuration call (operation mode, threshold voltage, samplerate, sample limit, trigger) and reads back the samplerate, sample limit and threshold, reporting the values the device holds. The threshold voltage is set only on devices that list SR_CONF_VTH; on older models with only the 3.3 V / 5 V threshold setting, "vth" is reported as null and an explicit --vth is rejected. - A failure to list devices is reported as an error, not as an empty device list. - Library callbacks update shared state under a mutex. The capture waits, against a monotonic deadline, for the collect task's final end event rather than SR_DF_END or the device-stopped event. - A capture that timed out, ended by error or detach, reported a packet or logic data error, overflowed, or returned fewer samples than requested is reported as failed and no .bin is created. - Spools the logic data to disk while capturing, so long or streaming captures do not have to fit in memory, then de-interleaves it in bounded chunks; the target is built with 64-bit file offsets and output offsets are range-checked. - Checks every write, including the JSON on stdout, and publishes the output file atomically without replacing an existing one; the output name is allocated to full length. - Finds the firmware next to the executable (build tree, app bundle or installed ../share/DSView/res), in the configured install prefix, or in --res / $DSLCAP_RES. - Built and installed (to the configured bindir) on non-Windows platforms, with its own link list including libm; Windows builds are unchanged. tools/dslcap/test_spool.c is a standalone check, needing no hardware, of argument validation, packet/event status handling, the de-interleave path and large-file offsets; its header gives macOS and Linux build commands. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S6AB69CNtvrecQVn1nyvL1 --- CMakeLists.txt | 30 ++ tools/dslcap/dslcap.c | 839 ++++++++++++++++++++++++++++++++++++++ tools/dslcap/test_spool.c | 212 ++++++++++ 3 files changed, 1081 insertions(+) create mode 100644 tools/dslcap/dslcap.c create mode 100644 tools/dslcap/test_spool.c diff --git a/CMakeLists.txt b/CMakeLists.txt index cc82669f..4c5be4ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -717,4 +717,34 @@ if(ENABLE_TESTS) add_test(test ${CMAKE_CURRENT_BINARY_DIR}/DSView/test/DSView-test) endif(ENABLE_TESTS) +#=============================================================================== +#= dslcap: headless capture command-line tool +#------------------------------------------------------------------------------- +# The tool uses POSIX file and executable-path APIs, so it is not built on +# Windows; the GUI build there is unchanged. +if(NOT WIN32) + add_executable(dslcap tools/dslcap/dslcap.c ${common_SOURCES} ${libsigrok4DSL_SOURCES}) + + # Only what the C sources need; unlike the C++ link of DSView, the C link + # does not pull in libm implicitly. + set(DSLCAP_LINK_LIBS + -lz + -lglib-2.0 + ${CMAKE_THREAD_LIBS_INIT} + ${LIBUSB_1_LIBRARIES} + ) + find_library(DSLCAP_MATH_LIBRARY m) + if(DSLCAP_MATH_LIBRARY) + list(APPEND DSLCAP_LINK_LIBS ${DSLCAP_MATH_LIBRARY}) + endif() + target_link_libraries(dslcap ${DSLCAP_LINK_LIBS}) + # 64-bit file offsets for long captures on 32-bit systems; the installed + # firmware directory matches the install(DIRECTORY DSView/res ...) rule above. + target_compile_definitions(dslcap PRIVATE + _FILE_OFFSET_BITS=64 + DSLCAP_INSTALL_RES_DIR="${CMAKE_INSTALL_PREFIX}/share/DSView/res" + ) + + install(TARGETS dslcap DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() diff --git a/tools/dslcap/dslcap.c b/tools/dslcap/dslcap.c new file mode 100644 index 00000000..61b7e37a --- /dev/null +++ b/tools/dslcap/dslcap.c @@ -0,0 +1,839 @@ +/* + * dslcap: headless DSLogic capture on libsigrok4DSL (GPL-3.0, as DSView). + * + * dslcap --list + * dslcap --channels 0,1 --samplerate 10000000 --samples 1000000 + * [--vth 1.6] [--mode buffer|stream] [--trigger CH[:R|F|C|1|0]] + * [--trigpos PERCENT] [--timeout SEC] [--res DIR] --out /path/base + * + * Writes .bin: for each enabled channel in ascending order, the + * channel's samples packed LSB-first, ceil(samples/64)*8 bytes per channel. + * Prints one JSON object describing the capture on stdout; every failure + * prints an object with an "error" key instead, and .bin is only + * created for a complete capture. + * + * Exit status: 0 success, 1 runtime or I/O error, 2 invalid arguments or + * unavailable settings, 3 the capture itself failed. + */ +#ifndef _FILE_OFFSET_BITS +#define _FILE_OFFSET_BITS 64 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#endif +#include "libsigrok.h" + +/* The spool and the output may exceed 2 GiB. */ +G_STATIC_ASSERT(sizeof(off_t) >= 8); + +#define MAX_CHANNELS 64 +#define STOP_GRACE_US (5 * G_TIME_SPAN_SECOND) +#define POLL_US 2000 + +/* + * Capture state. The library delivers data and events on its own threads, + * so everything below is guarded by g_lock (a pthread mutex, as the library + * itself uses). + */ +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; +static int g_done = 0; /* main thread should stop waiting */ +static int g_task_end = 0; /* terminal collect-task event seen */ +static int g_err = 0; /* DS_EV_COLLECT_TASK_END_BY_ERROR/_DETACHED */ +static int g_data_end = 0; /* SR_DF_END seen */ +static int g_pkt_error = 0; /* packet status or logic data error */ +static int g_overflow = 0; /* SR_DF_OVERFLOW seen */ +static FILE *g_raw = NULL; /* disk spool of logic payload */ +static uint64_t g_raw_bytes = 0; +static int g_io_error = 0; +static int g_format = -1; +static long long g_trig_pos = -1; +static int g_split_seen = 0; + +static void on_event(int ev) +{ + pthread_mutex_lock(&g_lock); + /* SR_DF_END and DS_EV_DEVICE_STOPPED come before the final status; + * only the collect-task end events say how the capture finished. */ + if (ev == DS_EV_COLLECT_TASK_END || + ev == DS_EV_COLLECT_TASK_END_BY_ERROR || + ev == DS_EV_COLLECT_TASK_END_BY_DETACHED) { + if (ev != DS_EV_COLLECT_TASK_END) + g_err = ev; + g_task_end = 1; + g_done = 1; + } + pthread_mutex_unlock(&g_lock); +} + +static void on_data(const struct sr_dev_inst *sdi, const struct sr_datafeed_packet *p) +{ + (void)sdi; + pthread_mutex_lock(&g_lock); + if (p->status != SR_PKT_OK) { + g_pkt_error = 1; + g_done = 1; + if (p->type == SR_DF_END) + g_data_end = 1; + } else if (p->type == SR_DF_LOGIC) { + const struct sr_datafeed_logic *l = p->payload; + if (l->data_error) { + g_pkt_error = 1; + g_done = 1; + } else { + g_format = l->format; + if (l->format == LA_SPLIT_DATA) + g_split_seen = 1; + if (g_raw && !g_io_error) { + if (fwrite(l->data, 1, l->length, g_raw) != l->length) { + g_io_error = 1; + g_done = 1; + } else { + g_raw_bytes += l->length; + } + } + } + } else if (p->type == SR_DF_TRIGGER && p->payload) { + const struct ds_trigger_pos *t = p->payload; + g_trig_pos = t->real_pos; + } else if (p->type == SR_DF_OVERFLOW) { + g_overflow = 1; + g_done = 1; + } else if (p->type == SR_DF_END) { + g_data_end = 1; + } + pthread_mutex_unlock(&g_lock); +} + +/* Wait until a callback sets *flag or the monotonic deadline passes. */ +static int wait_flag(const int *flag, gint64 deadline) +{ + for (;;) { + pthread_mutex_lock(&g_lock); + int set = *flag; + pthread_mutex_unlock(&g_lock); + gint64 now = g_get_monotonic_time(); + if (set || now >= deadline) + return set; + g_usleep(MIN(POLL_US, deadline - now)); + } +} + +static void json_str(const char *s) +{ + putchar('"'); + for (; *s; s++) { + if ((unsigned char)*s < 0x20) + printf("\\u%04x", (unsigned char)*s); + else { + if (*s == '"' || *s == '\\') putchar('\\'); + putchar(*s); + } + } + putchar('"'); +} + +/* The JSON result is the tool's output; failing to deliver it is an error. */ +static int finish_stdout(int rc) +{ + if (fflush(stdout) || ferror(stdout)) { + fprintf(stderr, "dslcap: cannot write the result to stdout\n"); + return rc ? rc : 1; + } + return rc; +} + +static void arg_error(const char *what, const char *option, const char *value) +{ + printf("{\"error\":"); + json_str(what); + if (option) { + printf(",\"option\":"); + json_str(option); + } + if (value) { + printf(",\"value\":"); + json_str(value); + } + printf("}\n"); +} + +/* Whole-string unsigned decimal; no sign, whitespace or trailing text. */ +static int parse_u64(const char *s, uint64_t *out) +{ + char *end; + if (!g_ascii_isdigit(*s)) return -1; + errno = 0; + unsigned long long v = strtoull(s, &end, 10); + if (errno || *end) return -1; + *out = v; + return 0; +} + +/* Whole-string finite number, independent of the locale. */ +static int parse_double(const char *s, double *out) +{ + char *end; + if (!*s || g_ascii_isspace(*s)) return -1; + errno = 0; + double v = g_ascii_strtod(s, &end); + if (errno || end == s || *end || !isfinite(v)) return -1; + *out = v; + return 0; +} + +struct options { + const char *res, *out, *chans, *mode, *trig; + uint64_t rate, samples; + double vth, timeout; + int trigpos, list_only, stream, vth_given; + int enabled[MAX_CHANNELS], nch; + int trig_ch; + char trig_type; +}; + +/* Validates every option before the library or the device is touched. + * Returns 0, or 2 after printing a JSON error. */ +static int parse_args(int argc, char **argv, struct options *o) +{ + uint64_t u; + memset(o, 0, sizeof *o); + o->res = getenv("DSLCAP_RES"); + o->chans = "0"; + o->mode = "buffer"; + o->rate = 10000000; + o->samples = 1000000; + o->vth = 1.6; + o->timeout = 30; + o->trigpos = 10; + o->trig_ch = -1; + + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (!strcmp(a, "--list")) { + o->list_only = 1; + continue; + } + static const char *const valued[] = { + "--res", "--out", "--channels", "--samplerate", "--samples", "--vth", + "--mode", "--trigger", "--trigpos", "--timeout", + }; + int known = 0; + for (size_t k = 0; k < G_N_ELEMENTS(valued); k++) + if (!strcmp(a, valued[k])) known = 1; + if (!known) { + arg_error("unknown argument", a, NULL); + return 2; + } + if (i + 1 >= argc) { + arg_error("missing option value", a, NULL); + return 2; + } + const char *v = argv[++i]; + int bad = 0; + if (!strcmp(a, "--res")) o->res = v; + else if (!strcmp(a, "--out")) o->out = v; + else if (!strcmp(a, "--channels")) o->chans = v; + else if (!strcmp(a, "--mode")) o->mode = v; + else if (!strcmp(a, "--trigger")) o->trig = v; + else if (!strcmp(a, "--samplerate")) bad = parse_u64(v, &o->rate) || o->rate == 0; + else if (!strcmp(a, "--samples")) + /* The driver rounds the limit up to SAMPLES_ALIGN + 1. */ + bad = parse_u64(v, &o->samples) || o->samples == 0 || + o->samples > UINT64_MAX - SAMPLES_ALIGN; + else if (!strcmp(a, "--vth")) { + /* The range DSView offers for the threshold voltage. */ + bad = parse_double(v, &o->vth) || o->vth < 0.0 || o->vth > 5.0; + o->vth_given = 1; + } + else if (!strcmp(a, "--trigpos")) { + bad = parse_u64(v, &u) || u > 100; + if (!bad) o->trigpos = (int)u; + } else if (!strcmp(a, "--timeout")) + bad = parse_double(v, &o->timeout) || o->timeout <= 0 || o->timeout > 1e9; + if (bad) { + arg_error("invalid option value", a, v); + return 2; + } + } + + if (strcmp(o->mode, "buffer") && strcmp(o->mode, "stream")) { + arg_error("invalid option value", "--mode", o->mode); + return 2; + } + o->stream = !strcmp(o->mode, "stream"); + if (o->list_only) + return 0; + /* The driver converts trigpos% of the (aligned) sample limit to a 32-bit + * trigger position before applying its depth limit, so keep it in range. */ + if (o->trigpos > 0 && + o->samples + SAMPLES_ALIGN > (uint64_t)UINT32_MAX * 100 / (uint64_t)o->trigpos) { + printf("{\"error\":\"--trigpos too large for --samples\",\"samples\":%llu,\"trigpos\":%d}\n", + (unsigned long long)o->samples, o->trigpos); + return 2; + } + if (!o->out) { + arg_error("--out is required", NULL, NULL); + return 2; + } + + /* Comma-separated channel indexes, each a plain decimal number. */ + const char *s = o->chans; + for (;;) { + const char *comma = strchr(s, ','); + size_t len = comma ? (size_t)(comma - s) : strlen(s); + char token[8]; + int bad = len == 0 || len >= sizeof token; + if (!bad) { + memcpy(token, s, len); + token[len] = '\0'; + bad = parse_u64(token, &u) || u >= MAX_CHANNELS; + } + if (bad) { + arg_error("invalid option value", "--channels", o->chans); + return 2; + } + for (int i = 0; i < o->nch; i++) { + if (o->enabled[i] == (int)u) { + printf("{\"error\":\"duplicate channel\",\"channel\":%d}\n", (int)u); + return 2; + } + } + if (o->nch == MAX_CHANNELS) { + printf("{\"error\":\"too many channels\"}\n"); + return 2; + } + o->enabled[o->nch++] = (int)u; + if (!comma) break; + s = comma + 1; + } + + /* CH, or CH:T with T exactly one of R F C 1 0. */ + if (o->trig) { + const char *colon = strchr(o->trig, ':'); + size_t len = colon ? (size_t)(colon - o->trig) : strlen(o->trig); + char token[8]; + int bad = len == 0 || len >= sizeof token; + if (!bad) { + memcpy(token, o->trig, len); + token[len] = '\0'; + bad = parse_u64(token, &u) || u >= MaxTriggerProbes; + } + o->trig_type = 'R'; + if (!bad && colon) { + o->trig_type = colon[1]; + bad = !o->trig_type || !strchr("RFC10", o->trig_type) || colon[2]; + } + if (bad) { + arg_error("invalid option value", "--trigger", o->trig); + return 2; + } + o->trig_ch = (int)u; + } + return 0; +} + +static int pick_device(int list_only) +{ + struct ds_device_base_info *list = NULL; + int count = 0, pick = -1; + if (ds_get_device_list(&list, &count) != SR_OK) { + g_free(list); + return -3; + } + if (list_only) printf("{\"devices\":["); + for (int i = 0; i < count; i++) { + if (list_only) { + printf("%s", i ? "," : ""); + json_str(list[i].name); + } + if (pick < 0 && strstr(list[i].name, "DSLogic")) + pick = i; + } + if (list_only) printf("]}\n"); + if (pick >= 0 && !list_only) { + if (ds_active_device(list[pick].handle) != SR_OK) + pick = -2; + } + g_free(list); + return pick; +} + +static int set_u64(int key, uint64_t v) { return ds_set_actived_device_config(NULL, NULL, key, g_variant_new_uint64(v)); } + +/* Read back a configuration value of the expected type. */ +static GVariant *get_config(int key, const GVariantType *type) +{ + GVariant *gv = NULL; + if (ds_get_actived_device_config(NULL, NULL, key, &gv) != SR_OK || !gv) + return NULL; + if (!g_variant_is_of_type(gv, type)) { + g_variant_unref(gv); + return NULL; + } + return gv; +} + +/* Whether the active device lists key among its SR_CONF_DEVICE_OPTIONS. */ +static int device_has_option(int key) +{ + GVariant *gv = NULL; + int found = 0; + if (ds_get_actived_device_config_list(NULL, SR_CONF_DEVICE_OPTIONS, &gv) != SR_OK || !gv) + return 0; + if (g_variant_is_of_type(gv, G_VARIANT_TYPE("ai"))) { + gsize n = 0; + const gint32 *keys = g_variant_get_fixed_array(gv, &n, sizeof(gint32)); + for (gsize i = 0; i < n; i++) + if (keys[i] == key) found = 1; + } + g_variant_unref(gv); + return found; +} + +static void config_error(const char *setting, int code) +{ + printf("{\"error\":\"device configuration failed\",\"setting\":\"%s\",\"code\":%d}\n", + setting, code); +} + +/* Keep the channel-major file layout, using fixed-size conversion buffers. */ +static int write_output(const char *path, int nch, uint64_t per_ch, uint64_t got) +{ + uint64_t raw[4096], channel[4096]; + if (nch <= 0 || nch > (int)G_N_ELEMENTS(raw)) return -1; + /* Every channel's last byte must be addressable as an off_t. */ + if (per_ch > (uint64_t)G_MAXINT64 / 8 / (uint64_t)nch) return -1; + if (fflush(g_raw) || fseeko(g_raw, 0, SEEK_SET)) return -1; + /* Write to a temporary file and publish it only on success. */ + char *tmp = g_strdup_printf("%s.XXXXXX", path); + int fd = g_mkstemp(tmp); + if (fd < 0) { g_free(tmp); return -1; } + FILE *f = fdopen(fd, "wb"); + if (!f) { close(fd); unlink(tmp); g_free(tmp); return -1; } + int failed = 0; + for (uint64_t k = 0; k < per_ch && !failed;) { + size_t count = MIN(per_ch - k, G_N_ELEMENTS(raw) / nch); + if (fread(raw, sizeof(uint64_t) * nch, count, g_raw) != count) { + failed = 1; + break; + } + for (int c = 0; c < nch; c++) { + for (size_t i = 0; i < count; i++) channel[i] = raw[i * nch + c]; + /* Clear samples past the requested count in the last word. */ + if (k + count == per_ch && got % 64) + channel[count - 1] &= (1ULL << (got % 64)) - 1; + off_t offset = (off_t)(((uint64_t)c * per_ch + k) * 8); + if (fseeko(f, offset, SEEK_SET) || fwrite(channel, 8, count, f) != count) { + failed = 1; + break; + } + } + k += count; + } + if (fclose(f)) failed = 1; + /* link() publishes atomically and, unlike rename(), refuses to replace an existing capture. */ + if (!failed && link(tmp, path)) failed = 1; + unlink(tmp); /* after link() the published name keeps the data; drop the temporary one either way */ + g_free(tmp); + return failed ? -1 : 0; +} + +/* Why a finished acquisition must not be published, or NULL if it may. */ +static const char *capture_failure(int timed_out, int task_end, int err, int pkt_error, + int overflow, uint64_t got, uint64_t samples) +{ + if (timed_out) return "capture timed out"; + if (!task_end) return "capture did not finish"; + if (err == DS_EV_COLLECT_TASK_END_BY_DETACHED) return "device detached during capture"; + if (err) return "capture ended with a device error"; + if (pkt_error) return "device reported a data error"; + if (overflow) return "device buffer overflow"; + if (got == 0) return "no samples captured"; + if (got < samples) return "capture ended before the requested sample count"; + return NULL; +} + +static int set_samplerate(uint64_t rate) +{ + GVariant *dict = NULL; + if (ds_get_actived_device_config_list(NULL, SR_CONF_SAMPLERATE, &dict) != SR_OK || !dict) + return SR_ERR; + GVariant *rates = g_variant_lookup_value(dict, "samplerates", G_VARIANT_TYPE("at")); + g_variant_unref(dict); + if (!rates) + return SR_ERR; + + gsize count; + const uint64_t *values = g_variant_get_fixed_array(rates, &count, sizeof(uint64_t)); + int supported = 0; + for (gsize i = 0; i < count; i++) + if (values[i] == rate) { supported = 1; break; } + g_variant_unref(rates); + + /* The driver stores arbitrary rates but rounds the hardware divider. */ + return supported ? set_u64(SR_CONF_SAMPLERATE, rate) : SR_ERR_ARG; +} + +static int select_channel_mode(uint64_t rate) +{ + GVariant *gv = NULL; + if (ds_get_actived_device_config_list(NULL, SR_CONF_CHANNEL_MODE, &gv) != SR_OK || !gv) + return SR_ERR; + const struct sr_list_item *modes = (const struct sr_list_item *)g_variant_get_uint64(gv); + g_variant_unref(gv); + + /* The driver lists modes from most channels to fewest. */ + for (; modes && modes->id >= 0; modes++) { + if (ds_set_actived_device_config(NULL, NULL, SR_CONF_CHANNEL_MODE, + g_variant_new_int16(modes->id)) != SR_OK) + return SR_ERR; + gv = NULL; + if (ds_get_actived_device_config_list(NULL, SR_CONF_SAMPLERATE, &gv) != SR_OK || !gv) + return SR_ERR; + GVariant *rates = g_variant_lookup_value(gv, "samplerates", G_VARIANT_TYPE("at")); + g_variant_unref(gv); + if (!rates) return SR_ERR; + gsize count = 0; + const uint64_t *values = g_variant_get_fixed_array(rates, &count, sizeof(uint64_t)); + int found = 0; + for (gsize i = 0; i < count; i++) + if (values[i] == rate) found = 1; + g_variant_unref(rates); + if (found) return SR_OK; + } + return SR_ERR; +} + +static int configure_trigger(const struct options *o) +{ + int rc; + if ((rc = ds_trigger_reset()) != SR_OK) { config_error("trigger", rc); return rc; } + if ((rc = ds_trigger_set_mode(SIMPLE_TRIGGER)) != SR_OK) { config_error("trigger_mode", rc); return rc; } + if ((rc = ds_trigger_set_pos((uint16_t)o->trigpos)) != SR_OK) { config_error("trigpos", rc); return rc; } + if (o->trig && (rc = ds_trigger_probe_set((uint16_t)o->trig_ch, + (unsigned char)o->trig_type, 'X')) != SR_OK) { + config_error("trigger", rc); + return rc; + } + if ((rc = ds_trigger_set_en(o->trig ? TRUE : FALSE)) != SR_OK) { config_error("trigger_enable", rc); return rc; } + return SR_OK; +} + +/* Firmware directory: next to this executable (app bundle Contents/Resources/res, + * build tree DSView/res, installed ../share/DSView/res), then the configured + * install location. */ +static char *default_res_dir(void) +{ + char exe[4096] = {0}; + char *dir = NULL; +#ifdef __APPLE__ + uint32_t size = sizeof exe; + if (_NSGetExecutablePath(exe, &size) != 0) exe[0] = '\0'; +#else + ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1); + if (n < 0 || n >= (ssize_t)sizeof exe - 1) exe[0] = '\0'; +#endif + char *real = exe[0] ? realpath(exe, NULL) : NULL; + if (real) { + dir = g_path_get_dirname(real); + free(real); + } + const char *candidates[] = { "../Resources/res", "../DSView/res", "res", "../share/DSView/res" }; + char *found = NULL; + for (size_t i = 0; dir && i < G_N_ELEMENTS(candidates) && !found; i++) { + char *path = g_build_filename(dir, candidates[i], NULL); + if (g_file_test(path, G_FILE_TEST_IS_DIR)) found = path; + else g_free(path); + } + g_free(dir); +#ifdef DSLCAP_INSTALL_RES_DIR + if (!found && g_file_test(DSLCAP_INSTALL_RES_DIR, G_FILE_TEST_IS_DIR)) + found = g_strdup(DSLCAP_INSTALL_RES_DIR); +#endif + return found; +} + +struct report { + const char *device; + uint64_t rate, samples, got, per_ch, limit; + const int *channels; + int nch; + double vth; + const char *mode, *trig; + int format; + long long trig_pos; + int timed_out, err, pkt_error, overflow; + double secs; + const char *bin; +}; + +/* One JSON object; error, when set, makes it a failure report without a file. */ +static void print_report(const char *error, const struct report *r) +{ + printf("{"); + if (error) { + printf("\"error\":"); + json_str(error); + printf(","); + } + printf("\"device\":"); + json_str(r->device); + printf(",\"samplerate\":%llu,\"samples_requested\":%llu,\"samples\":%llu,\"words_per_channel\":%llu," + "\"channels\":[", (unsigned long long)r->rate, (unsigned long long)r->samples, + (unsigned long long)r->got, (unsigned long long)r->per_ch); + for (int i = 0; i < r->nch; i++) printf("%s%d", i ? "," : "", r->channels[i]); + /* vth is null on devices without a threshold voltage. */ + if (isfinite(r->vth)) printf("],\"vth\":%.3f,\"mode\":", r->vth); + else printf("],\"vth\":null,\"mode\":"); + json_str(r->mode); + printf(",\"format\":\"%s\",\"trigger\":", r->format == LA_SPLIT_DATA ? "split" : "cross"); + if (r->trig) json_str(r->trig); else printf("null"); + printf(",\"trigger_pos\":%lld,\"timed_out\":%s,\"lib_error_event\":%d,\"elapsed_s\":%.3f,\"bin\":", + r->trig_pos, r->timed_out ? "true" : "false", r->err, r->secs); + if (r->bin) json_str(r->bin); else printf("null"); + printf(",\"limit_samples\":%llu,\"packet_error\":%s,\"overflow\":%s}\n", + (unsigned long long)r->limit, r->pkt_error ? "true" : "false", + r->overflow ? "true" : "false"); +} + +int main(int argc, char **argv) +{ + struct options o; + int rc = parse_args(argc, argv, &o); + if (rc) return finish_stdout(rc); + + char *res_found = NULL; + const char *res = o.res; + if (!res) res = res_found = default_res_dir(); + if (!res) { + printf("{\"error\":\"firmware directory not found; set --res or DSLCAP_RES\"}\n"); + return finish_stdout(2); + } + if (strlen(res) >= 500) { + printf("{\"error\":\"firmware directory path is too long (limit 499 bytes)\"}\n"); + g_free(res_found); + return finish_stdout(2); + } + + uint64_t hw_samples = o.samples; + if (!o.stream) { + /* Buffer delivery is aligned to 1024 samples in the driver. */ + hw_samples = (o.samples + SAMPLES_ALIGN) & ~SAMPLES_ALIGN; + } + + ds_log_level(1); + ds_set_firmware_resource_dir(res); + g_free(res_found); + ds_set_event_callback(on_event); + ds_set_datafeed_callback(on_data); + if (ds_lib_init() != SR_OK) { printf("{\"error\":\"lib init failed\"}\n"); return finish_stdout(1); } + + int dev = pick_device(o.list_only); + if (dev == -3) { + printf("{\"error\":\"cannot list devices\"}\n"); + ds_lib_exit(); + return finish_stdout(1); + } + if (o.list_only) { ds_lib_exit(); return finish_stdout(0); } + if (dev < 0) { + printf("{\"error\":\"%s\"}\n", dev == -1 ? "no DSLogic found" : "device activation failed"); + ds_lib_exit(); + return finish_stdout(1); + } + + struct ds_device_full_info info; + memset(&info, 0, sizeof info); + ds_get_actived_device_info(&info); + info.name[sizeof info.name - 1] = '\0'; + + /* Mode first: it changes the channel mode and the allowed rates. */ + if (ds_set_actived_device_config(NULL, NULL, SR_CONF_OPERATION_MODE, + g_variant_new_int16(o.stream ? LO_OP_STREAM : LO_OP_BUFFER)) != SR_OK || + select_channel_mode(o.rate) != SR_OK) { + printf("{\"error\":\"samplerate unavailable in operation mode\",\"samplerate\":%llu}\n", + (unsigned long long)o.rate); + ds_lib_exit(); + return finish_stdout(2); + } + GVariant *gv = get_config(SR_CONF_VLD_CH_NUM, G_VARIANT_TYPE_INT16); + if (!gv) { + printf("{\"error\":\"cannot read channel limit\"}\n"); + ds_lib_exit(); + return finish_stdout(1); + } + int max_channels = g_variant_get_int16(gv); + g_variant_unref(gv); + + for (GSList *l = ds_get_actived_device_channels(); l; l = l->next) + ds_enable_device_channel(l->data, FALSE); + for (int i = 0; i < o.nch; i++) { + /* The selected channel mode limits which channels exist at this rate. */ + if (i >= max_channels || ds_enable_device_channel_index(o.enabled[i], TRUE) != SR_OK) { + printf("{\"error\":\"channel/rate combination unavailable\",\"channel\":%d,\"samplerate\":%llu,\"max_channels\":%d}\n", + o.enabled[i], (unsigned long long)o.rate, max_channels); + ds_lib_exit(); + return finish_stdout(2); + } + } + + /* Devices with a threshold voltage list SR_CONF_VTH; older ones only have a + * 3.3 V / 5 V threshold setting, which is left as it is. */ + int has_vth = device_has_option(SR_CONF_VTH); + if (!has_vth && o.vth_given) { + printf("{\"error\":\"this device has no threshold voltage setting\",\"device\":"); + json_str(info.name); + printf("}\n"); + ds_lib_exit(); + return finish_stdout(2); + } + if (has_vth && (rc = ds_set_actived_device_config(NULL, NULL, SR_CONF_VTH, g_variant_new_double(o.vth))) != SR_OK) { + config_error("vth", rc); + ds_lib_exit(); + return finish_stdout(1); + } + int rate_rc = set_samplerate(o.rate); + if (rate_rc != SR_OK) { + printf("{\"error\":\"%s\",\"samplerate\":%llu}\n", + rate_rc == SR_ERR_ARG ? "unsupported samplerate" : "samplerate configuration failed", + (unsigned long long)o.rate); + ds_lib_exit(); + return finish_stdout(rate_rc == SR_ERR_ARG ? 2 : 1); + } + if ((rc = set_u64(SR_CONF_LIMIT_SAMPLES, hw_samples)) != SR_OK) { + config_error("samples", rc); + ds_lib_exit(); + return finish_stdout(1); + } + + int enabled[MAX_CHANNELS], nch = 0; + for (GSList *l = ds_get_actived_device_channels(); l; l = l->next) { + const struct sr_channel *ch = l->data; + if (!ch->enabled) continue; + if (nch == MAX_CHANNELS) { + printf("{\"error\":\"too many enabled channels\"}\n"); + ds_lib_exit(); + return finish_stdout(1); + } + enabled[nch++] = ch->index; + } + /* ascending order, as the device sends them */ + for (int i = 0; i < nch; i++) for (int j = i + 1; j < nch; j++) + if (enabled[j] < enabled[i]) { int t = enabled[i]; enabled[i] = enabled[j]; enabled[j] = t; } + + if (o.trig) { + int trigger_enabled = 0; + for (int i = 0; i < nch; i++) + if (enabled[i] == o.trig_ch) trigger_enabled = 1; + if (!trigger_enabled) { + printf("{\"error\":\"trigger channel must be an enabled device channel\"}\n"); + ds_lib_exit(); + return finish_stdout(2); + } + } + if (configure_trigger(&o) != SR_OK) { + ds_lib_exit(); + return finish_stdout(1); + } + + /* Report what the device holds, not what was requested. */ + GVariant *rate_gv = get_config(SR_CONF_SAMPLERATE, G_VARIANT_TYPE_UINT64); + GVariant *limit_gv = get_config(SR_CONF_LIMIT_SAMPLES, G_VARIANT_TYPE_UINT64); + GVariant *vth_gv = has_vth ? get_config(SR_CONF_VTH, G_VARIANT_TYPE_DOUBLE) : NULL; + uint64_t act_rate = rate_gv ? g_variant_get_uint64(rate_gv) : 0; + uint64_t act_limit = limit_gv ? g_variant_get_uint64(limit_gv) : 0; + double act_vth = vth_gv ? g_variant_get_double(vth_gv) : NAN; + const char *unread = !rate_gv ? "samplerate" : !limit_gv ? "samples" : has_vth && !vth_gv ? "vth" : NULL; + if (rate_gv) g_variant_unref(rate_gv); + if (limit_gv) g_variant_unref(limit_gv); + if (vth_gv) g_variant_unref(vth_gv); + if (unread || act_limit != hw_samples || (has_vth && !isfinite(act_vth))) { + printf("{\"error\":\"cannot verify device configuration\",\"setting\":\"%s\"}\n", + unread ? unread : act_limit != hw_samples ? "samples" : "vth"); + ds_lib_exit(); + return finish_stdout(1); + } + + char *spool = g_strdup_printf("%s.raw-XXXXXX", o.out); + int fd = g_mkstemp(spool); + if (fd >= 0) { + g_raw = fdopen(fd, "w+b"); + if (!g_raw) close(fd); + unlink(spool); + } + g_free(spool); + if (!g_raw) { printf("{\"error\":\"cannot create capture spool\"}\n"); ds_lib_exit(); return finish_stdout(1); } + + /* One monotonic clock for both the deadline and the reported time. */ + gint64 t0 = g_get_monotonic_time(); + gint64 deadline = t0 + (gint64)(o.timeout * G_TIME_SPAN_SECOND); + rc = ds_start_collect(); + if (rc != SR_OK) { + printf("{\"error\":\"start failed\",\"code\":%d}\n", rc); + fclose(g_raw); + ds_lib_exit(); + return finish_stdout(1); + } + int timed_out = !wait_flag(&g_done, deadline); + + /* Stop early on timeout or a reported error; either way wait for the + * collect task's own end event before trusting the data and status. */ + if (ds_is_collecting()) ds_stop_collect(); + gint64 stop_deadline = g_get_monotonic_time() + STOP_GRACE_US; + wait_flag(&g_task_end, stop_deadline); + pthread_mutex_lock(&g_lock); + int task_end = g_task_end, err = g_err, pkt_error = g_pkt_error, overflow = g_overflow; + int io_error = g_io_error, format = g_split_seen ? LA_SPLIT_DATA : g_format; + uint64_t raw_bytes = g_raw_bytes; + long long trig_pos = g_trig_pos; + pthread_mutex_unlock(&g_lock); + gint64 t1 = g_get_monotonic_time(); + + /* De-interleave LA_CROSS_DATA: 64-sample words rotate through channels. */ + uint64_t words = raw_bytes / 8; + uint64_t per_ch = nch ? words / nch : 0; + uint64_t got = per_ch > o.samples / 64 ? o.samples : per_ch * 64; + per_ch = got / 64 + (got % 64 != 0); + + struct report r = { + .device = info.name, .rate = act_rate, .samples = o.samples, .got = got, + .per_ch = per_ch, .limit = act_limit, .channels = enabled, .nch = nch, + .vth = act_vth, .mode = o.mode, .trig = o.trig, .format = format, + .trig_pos = trig_pos, .timed_out = timed_out, .err = err, + .pkt_error = pkt_error, .overflow = overflow, + .secs = (t1 - t0) / (double)G_TIME_SPAN_SECOND, .bin = NULL, + }; + const char *failure = io_error ? NULL : capture_failure(timed_out, task_end, err, + pkt_error, overflow, got, o.samples); + char *path = g_strdup_printf("%s.bin", o.out); + int exit_code = 0; + if (io_error || (!failure && write_output(path, nch, per_ch, got))) { + printf("{\"error\":\"cannot write capture data\"}\n"); + exit_code = 1; + } else if (failure) { + print_report(failure, &r); + exit_code = 3; + } else { + r.bin = path; + print_report(NULL, &r); + } + g_free(path); + + /* Late callbacks, if the task never reported its end, must not see a closed spool. */ + pthread_mutex_lock(&g_lock); + FILE *raw = g_raw; + g_raw = NULL; + pthread_mutex_unlock(&g_lock); + fclose(raw); + ds_release_actived_device(); + ds_lib_exit(); + return finish_stdout(exit_code); +} diff --git a/tools/dslcap/test_spool.c b/tools/dslcap/test_spool.c new file mode 100644 index 00000000..252704c5 --- /dev/null +++ b/tools/dslcap/test_spool.c @@ -0,0 +1,212 @@ +/* Standalone regression harness; no analyzer is accessed. Dead-code + * stripping drops the CLI's main and with it every hardware reference. + * + * macOS: + * cc -Ilibsigrok4DSL -Icommon $(pkg-config --cflags glib-2.0) tools/dslcap/test_spool.c \ + * $(pkg-config --libs glib-2.0) -Wl,-dead_strip -o test_spool + * Linux: + * cc -D_DEFAULT_SOURCE -ffunction-sections -fdata-sections \ + * -Ilibsigrok4DSL -Icommon $(pkg-config --cflags glib-2.0) tools/dslcap/test_spool.c \ + * $(pkg-config --libs glib-2.0) -lm -Wl,--gc-sections -o test_spool + * + * ./test_spool /tmp/raw /tmp/output.bin + */ +#define main dslcap_main +#include "dslcap.c" +#undef main +#undef NDEBUG +#include +#include + +static void reset_state(void) +{ + g_done = g_task_end = g_err = g_data_end = g_pkt_error = g_overflow = 0; + g_raw_bytes = 0; + g_io_error = 0; +} + +/* parse_args with its JSON error output discarded. */ +static int parse(struct options *o, ...) +{ + char *argv[32] = { "dslcap" }; + int argc = 1; + va_list ap; + va_start(ap, o); + for (char *a; (a = va_arg(ap, char *)) && argc < 31;) argv[argc++] = a; + va_end(ap); + fflush(stdout); + int saved = dup(STDOUT_FILENO), null = open("/dev/null", O_WRONLY); + dup2(null, STDOUT_FILENO); + int rc = parse_args(argc, argv, o); + fflush(stdout); + dup2(saved, STDOUT_FILENO); + close(null); + close(saved); + return rc; +} + +static void test_arguments(void) +{ + struct options o; + assert(parse(&o, "--out", "x", "--channels", "3,0,15", "--samplerate", "100000000", + "--samples", "64", "--vth", "2.5", "--mode", "stream", "--trigger", "15:F", + "--trigpos", "100", "--timeout", "0.5", NULL) == 0); + assert(o.nch == 3 && o.enabled[0] == 3 && o.enabled[2] == 15 && o.stream); + assert(o.rate == 100000000 && o.samples == 64 && o.vth == 2.5 && o.timeout == 0.5); + assert(o.trig_ch == 15 && o.trig_type == 'F' && o.trigpos == 100); + assert(parse(&o, "--out", "x", "--trigger", "0", NULL) == 0 && o.trig_type == 'R'); + assert(parse(&o, "--list", NULL) == 0 && o.list_only); + /* trigpos% of the aligned sample limit must fit the driver's 32-bit position. */ + assert(parse(&o, "--out", "x", "--samples", "8589934592", "--trigpos", "100", NULL) == 2); + assert(parse(&o, "--out", "x", "--samples", "4294000000", "--trigpos", "100", NULL) == 0); + assert(parse(&o, "--out", "x", "--samples", "8589934592", "--trigpos", "10", NULL) == 0); + assert(parse(&o, "--out", "x", "--samples", "8589934592", "--trigpos", "0", NULL) == 0); + + const char *bad[][2] = { + {"--channels", "garbage"}, {"--channels", "1,,2"}, {"--channels", "1,"}, + {"--channels", "-1"}, {"--channels", "64"}, {"--channels", "1,1"}, + {"--channels", " 1"}, {"--mode", "garbage"}, {"--mode", "Stream"}, + {"--trigger", "0:garbage"}, {"--trigger", "0:Rx"}, {"--trigger", "0:"}, + {"--trigger", "0:r"}, {"--trigger", "32"}, {"--trigger", ":R"}, {"--trigger", "x:R"}, + {"--trigpos", "101"}, {"--trigpos", "-1"}, {"--trigpos", "10%"}, + {"--vth", "nan"}, {"--vth", "inf"}, {"--vth", "5.1"}, {"--vth", "-0.1"}, + {"--vth", "1.6V"}, {"--vth", ""}, {"--samples", "0"}, {"--samples", "-5"}, + {"--samples", "1e6"}, {"--samples", "18446744073709551616"}, + {"--samples", "18446744073709551615"}, {"--samplerate", "0"}, + {"--samplerate", "10M"}, {"--timeout", "0"}, {"--timeout", "nan"}, + {"--timeout", "-1"}, + }; + for (size_t i = 0; i < G_N_ELEMENTS(bad); i++) + assert(parse(&o, "--out", "x", bad[i][0], bad[i][1], NULL) == 2); + assert(parse(&o, "--channels", "0", NULL) == 2); /* no --out */ + assert(parse(&o, "--out", NULL) == 2); /* missing value */ + assert(parse(&o, "--out", "x", "--bogus", NULL) == 2); +} + +static void test_status(void) +{ + struct ds_trigger_pos t = {.real_pos = 7}; + struct sr_datafeed_packet packet = {.type = SR_DF_TRIGGER, .payload = &t}; + + /* A bad trigger header arrives as a packet with an error status. */ + reset_state(); + packet.status = SR_PKT_DATA_ERROR; + on_data(NULL, &packet); + assert(g_pkt_error && g_done); + + reset_state(); + uint64_t word = 0; + struct sr_datafeed_logic logic = {.length = 8, .format = LA_CROSS_DATA, .data = &word, + .data_error = 1}; + packet = (struct sr_datafeed_packet){.type = SR_DF_LOGIC, .payload = &logic}; + on_data(NULL, &packet); + assert(g_pkt_error && g_done && g_raw_bytes == 0); + + reset_state(); + packet = (struct sr_datafeed_packet){.type = SR_DF_OVERFLOW}; + on_data(NULL, &packet); + assert(g_overflow && g_done); + + /* Data end and device stop precede the task's final status. */ + reset_state(); + packet = (struct sr_datafeed_packet){.type = SR_DF_END}; + on_data(NULL, &packet); + on_event(DS_EV_DEVICE_STOPPED); + assert(g_data_end && !g_done && !g_task_end); + on_event(DS_EV_COLLECT_TASK_END_BY_ERROR); + assert(g_done && g_task_end && g_err == DS_EV_COLLECT_TASK_END_BY_ERROR); + + assert(capture_failure(0, 1, 0, 0, 0, 1024, 1000) == NULL); + assert(capture_failure(1, 1, 0, 0, 0, 1024, 1000)); + assert(capture_failure(0, 0, 0, 0, 0, 1024, 1000)); + assert(capture_failure(0, 1, DS_EV_COLLECT_TASK_END_BY_DETACHED, 0, 0, 1024, 1000)); + assert(capture_failure(0, 1, DS_EV_COLLECT_TASK_END_BY_ERROR, 0, 0, 1024, 1000)); + assert(capture_failure(0, 1, 0, 1, 0, 1024, 1000)); + assert(capture_failure(0, 1, 0, 0, 1, 1024, 1000)); + assert(capture_failure(0, 1, 0, 0, 0, 0, 1000)); + assert(capture_failure(0, 1, 0, 0, 0, 960, 1000)); + reset_state(); +} + +int main(int argc, char **argv) +{ + assert(argc == 3); + test_arguments(); + test_status(); + + const int channels[] = {1, 2, 16, 32}; + const uint64_t frames = 5003; + for (size_t test = 0; test < G_N_ELEMENTS(channels); test++) { + int nch = channels[test]; + size_t bytes = frames * nch * 8; + uint64_t *raw = g_malloc(bytes + 7); + for (uint64_t k = 0; k < frames; k++) + for (int c = 0; c < nch; c++) raw[k * nch + c] = (k << 16) | c; + memset((char *)raw + bytes, 0xFF, 7); /* Incomplete last word. */ + g_raw = fopen(argv[1], "w+b"); + assert(g_raw); + reset_state(); + /* Packet boundaries may split words and channel groups. */ + for (size_t pos = 0; pos < bytes + 7;) { + size_t length = MIN((pos % 4096) + 3, bytes + 7 - pos); + struct sr_datafeed_logic logic = {.length = length, .format = LA_CROSS_DATA, + .data = (char *)raw + pos}; + struct sr_datafeed_packet packet = {.type = SR_DF_LOGIC, .payload = &logic}; + on_data(NULL, &packet); + assert(!g_io_error && !g_done); + pos += length; + } + assert(g_raw_bytes == bytes + 7); + /* Publication never replaces a file, so clear the previous case's output. */ + unlink(argv[2]); + uint64_t per_ch = g_raw_bytes / 8 / nch; + assert(write_output(argv[2], nch, per_ch, per_ch * 64) == 0); + assert(write_output(argv[2], nch, per_ch, per_ch * 64) != 0); + /* Offsets past what an off_t can address are refused up front. */ + assert(write_output(argv[2], nch, (uint64_t)G_MAXINT64 / 8, 64) != 0); + FILE *out = fopen(argv[2], "rb"); + assert(out); + for (int c = 0; c < nch; c++) + for (uint64_t k = 0; k < frames; k++) { + uint64_t word; + assert(fread(&word, 8, 1, out) == 1); + assert(word == ((k << 16) | c)); + } + assert(fgetc(out) == EOF); + fclose(out); + + /* Payload accounting crosses the old GByteArray 32-bit limit. */ + g_raw_bytes = UINT32_MAX - 3ULL; + struct sr_datafeed_logic logic = {.length = 8, .format = LA_CROSS_DATA, .data = raw}; + struct sr_datafeed_packet packet = {.type = SR_DF_LOGIC, .payload = &logic}; + on_data(NULL, &packet); + assert(g_raw_bytes == (1ULL << 32) + 4); + fclose(g_raw); + + /* A spool write failure must stop collection and report failure. */ + g_raw = fopen(argv[1], "rb"); + assert(g_raw); + g_done = 0; + on_data(NULL, &packet); + assert(g_io_error && g_done); + fclose(g_raw); + g_raw = NULL; + g_free(raw); + } + + /* Large-file interface: seek and write past 4 GiB in a sparse spool. */ + FILE *big = fopen(argv[1], "w+b"); + assert(big); + const off_t far = ((off_t)1 << 32) + 8; + uint64_t word = 0x0123456789abcdefULL, back = 0; + assert(fseeko(big, far, SEEK_SET) == 0 && fwrite(&word, 8, 1, big) == 1); + assert(fflush(big) == 0 && fseeko(big, far, SEEK_SET) == 0 && ftello(big) == far); + assert(fread(&back, 8, 1, big) == 1 && back == word); + fclose(big); + + unlink(argv[1]); + unlink(argv[2]); + puts("spool tests passed: arguments, packet status, packet boundaries, channel layout, " + "64-bit count and offsets, I/O failure"); + return 0; +}