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;
+}