From 4c7416f4d895c7c2bbf3c37ab26a938efd11a4d7 Mon Sep 17 00:00:00 2001 From: Philipp Marek Date: Tue, 3 Mar 2026 14:17:02 +0100 Subject: [PATCH 1/2] Initial commit, just Claude's output. --- .../hantek-dso2xxx/Makefile.am.fragment | 13 + src/hardware/hantek-dso2xxx/README.md | 104 ++++++ src/hardware/hantek-dso2xxx/api.c | 351 ++++++++++++++++++ src/hardware/hantek-dso2xxx/protocol.c | 336 +++++++++++++++++ src/hardware/hantek-dso2xxx/protocol.h | 133 +++++++ 5 files changed, 937 insertions(+) create mode 100644 src/hardware/hantek-dso2xxx/Makefile.am.fragment create mode 100644 src/hardware/hantek-dso2xxx/README.md create mode 100644 src/hardware/hantek-dso2xxx/api.c create mode 100644 src/hardware/hantek-dso2xxx/protocol.c create mode 100644 src/hardware/hantek-dso2xxx/protocol.h diff --git a/src/hardware/hantek-dso2xxx/Makefile.am.fragment b/src/hardware/hantek-dso2xxx/Makefile.am.fragment new file mode 100644 index 000000000..5e894553c --- /dev/null +++ b/src/hardware/hantek-dso2xxx/Makefile.am.fragment @@ -0,0 +1,13 @@ +## Makefile.am fragment for the hantek-dso2xxx driver. +## Add this block to src/hardware/Makefile.am in the libsigrok tree, +## following the pattern of any other network-connected driver. +## +## The driver has no external library dependencies beyond what +## libsigrok already requires (glib, POSIX sockets). + +if HW_HANTEK_DSO2XXX +libsigrok_la_SOURCES += \ + hardware/hantek-dso2xxx/api.c \ + hardware/hantek-dso2xxx/protocol.c \ + hardware/hantek-dso2xxx/protocol.h +endif diff --git a/src/hardware/hantek-dso2xxx/README.md b/src/hardware/hantek-dso2xxx/README.md new file mode 100644 index 000000000..1a53a67d8 --- /dev/null +++ b/src/hardware/hantek-dso2xxx/README.md @@ -0,0 +1,104 @@ +# hantek-dso2xxx libsigrok driver + +A libsigrok driver for the Hantek DSO2xxx oscilloscope series (DSO2C10, +DSO2C15, DSO2D10, DSO2D15) that receives waveform data over TCP using the +quick-fetch firmware patch by phmarek: +https://github.com/phmarek/hantek-dso2000-quick-fetch + +--- + +## How it works + +The patched firmware (quick-fetch.so, LD_PRELOAD'd on the scope) listens +on a TCP port. When the user presses SAVE TO USB the scope freezes the +current waveform and streams a compact binary frame to any connected client. + +Binary frame layout (all fields little-endian): + + Offset Size Type Field + ------ ---- ------- -------------------------------- + 0 4 uint32 magic = 0x324B5448 ("HTK2") + 4 4 uint32 num_samples (per enabled channel) + 8 4 float32 time_per_div (seconds/division) + 12 4 float32 sample_period (seconds; 1/samplerate) + 16 1 uint8 ch1_enabled (non-zero if CH1 data follows) + 17 1 uint8 ch2_enabled (non-zero if CH2 data follows) + 18 2 uint8[2] reserved + 20 4 float32 ch1_scale (volts/division) + 24 4 float32 ch2_scale + 28 4 int32 ch1_offset_raw (signed ADC counts) + 32 4 int32 ch2_offset_raw + +After the header: CH1 samples (num_samples x int8, if ch1_enabled != 0), +then CH2 samples (num_samples x int8, if ch2_enabled != 0). + +Voltage conversion: + voltage = (raw_sample - offset_raw) * (scale_V_per_div / 25.0) +(25 ADC counts per division; 8 divisions gives ~200 count full range) + +--- + +## Prerequisites + +1. Install the quick-fetch patch on your scope following the instructions + at https://github.com/phmarek/hantek-dso2000-quick-fetch + +2. Use DavidAlfa's USB-networking kernel so the scope appears at + 192.168.7.1 via USB networking. Without this, the TCP server is not + reachable. + +--- + +## Building + +Place the three source files in src/hardware/hantek-dso2xxx/ inside the +libsigrok source tree, then: + +1. Add the Makefile.am.fragment content to src/hardware/Makefile.am. +2. Add HW_HANTEK_DSO2XXX enable/disable boilerplate to configure.ac + (copy the pattern from any comparable driver such as rigol-ds). +3. Re-run ./autogen.sh && ./configure && make + +--- + +## Usage + + # Single capture (waits for one SAVE TO USB press) + sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025 -o capture.sr + + # Continuous (keeps waiting for further presses) + sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025 --continuous + + # Custom address/port + sigrok-cli -d hantek-dso2xxx:conn=10.0.0.5/5025 -o capture.sr + +Open the .sr file in PulseView for graphical display. + +--- + +## No configurable acquisition options + +All scope settings (timebase, volts/div, coupling, trigger, memory depth) +are configured directly on the instrument front panel. The driver reads +them passively from the binary frame header. + +Session-level keys: + + conn GET Returns "address/port" string + samplerate GET Derived from sample_period field + limit_frames GET Count of frames received so far + +--- + +## Firmware versions + +The quick-fetch patch supports: + 3.0.0(220727.00) - 2022-07-27 + 3.0.0(230327.00) - 2023-03-27 + 3.0.1(250418.00) - 2025-04-18 (patch v1.3+) + +--- + +## License + +GPL-3.0-or-later, matching the rest of libsigrok. diff --git a/src/hardware/hantek-dso2xxx/api.c b/src/hardware/hantek-dso2xxx/api.c new file mode 100644 index 000000000..80b63f7cb --- /dev/null +++ b/src/hardware/hantek-dso2xxx/api.c @@ -0,0 +1,351 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2024 libsigrok contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file api.c + * + * libsigrok driver API for the Hantek DSO2xxx oscilloscope series with the + * "quick-fetch" firmware patch installed. + * + * Usage summary + * ------------- + * This driver does NOT enumerate devices via USB; instead it connects over + * TCP to the scope's IP address (requires DavidAlfa's USB-networking kernel + * image, which exposes the scope at 192.168.7.1 by default). + * + * Because no SCPI or serial scan is possible, scan() always returns exactly + * one virtual device when a conn= option is provided: + * + * sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025 --continuous + * + * No configurable scan/acquisition options are exposed (the driver follows + * all scope settings passively). Waveform capture is triggered by pressing + * the SAVE TO USB button on the instrument. + * + * Driver architecture + * ------------------- + * - scan() parses conn= option, allocates sr_dev_inst + * - dev_open() TCP connect + * - dev_close() TCP disconnect + * - dev_acquisition_start() registers GLib I/O watch on the socket fd + * - receive_data() GLib callback: reads one frame, sends packets, + * calls dev_acquisition_stop() if limit reached + * - dev_acquisition_stop() removes I/O watch, sends SR_DF_END + */ + +#include +#include +#include +#include +#include "libsigrok-internal.h" +#include "protocol.h" + +/* -------------------------------------------------------------------------- + * Static driver metadata + * -------------------------------------------------------------------------- */ + +static const uint32_t scanopts[] = { + SR_CONF_CONN, +}; + +static const uint32_t drvopts[] = { + SR_CONF_OSCILLOSCOPE, +}; + +/* + * No acquisition options are exposed. The driver is entirely passive: it + * reads whatever the scope decides to send when the user presses SAVE TO USB. + */ +static const uint32_t devopts[] = { + SR_CONF_CONN | SR_CONF_GET, + SR_CONF_LIMIT_FRAMES | SR_CONF_GET | SR_CONF_SET, + SR_CONF_SAMPLERATE | SR_CONF_GET, +}; + +/* -------------------------------------------------------------------------- + * scan() helpers + * -------------------------------------------------------------------------- */ + +/** + * Parse a "address/port" or "address:port" connection string. + * + * The address and port are separated by '/' (preferred by libsigrok) or ':'. + * If no separator is found the whole string is used as address and + * HANTEK_DEFAULT_PORT is used for the port. + */ +static void parse_conn(const char *conn, char **addr_out, char **port_out) +{ + const char *sep; + char *addr, *port; + + sep = strrchr(conn, '/'); + if (!sep) + sep = strrchr(conn, ':'); + + if (sep) { + addr = g_strndup(conn, (gsize)(sep - conn)); + port = g_strdup(sep + 1); + } else { + addr = g_strdup(conn); + port = g_strdup(HANTEK_DEFAULT_PORT); + } + + *addr_out = addr; + *port_out = port; +} + +/* -------------------------------------------------------------------------- + * Driver callbacks + * -------------------------------------------------------------------------- */ + +static GSList *scan(struct sr_dev_driver *di, GSList *options) +{ + struct sr_dev_inst *sdi; + struct dev_context *devc; + struct sr_config *src; + GSList *devices = NULL; + GSList *l; + const char *conn = NULL; + char *addr, *port; + + /* Require a conn= option – we cannot enumerate scopes. */ + for (l = options; l; l = l->next) { + src = l->data; + if (src->key == SR_CONF_CONN) { + conn = g_variant_get_string(src->data, NULL); + break; + } + } + if (!conn) { + sr_err("This driver requires conn=[/] to be specified."); + return NULL; + } + + parse_conn(conn, &addr, &port); + sr_dbg("Creating device for %s:%s", addr, port); + + sdi = g_malloc0(sizeof(*sdi)); + sdi->status = SR_ST_INACTIVE; + sdi->vendor = g_strdup("Hantek"); + sdi->model = g_strdup("DSO2xxx (quick-fetch)"); + sdi->driver = di; + + /* Add the two analogue channels. */ + sr_channel_new(sdi, 0, SR_CHANNEL_ANALOG, TRUE, "CH1"); + sr_channel_new(sdi, 1, SR_CHANNEL_ANALOG, TRUE, "CH2"); + + devc = g_malloc0(sizeof(*devc)); + devc->tcp_address = addr; /* ownership transferred */ + devc->tcp_port = port; /* ownership transferred */ + devc->sockfd = -1; + sdi->priv = devc; + + /* Register with the driver instance list. */ + devices = g_slist_append(devices, sdi); + return std_scan_complete(di, devices); +} + +static int dev_open(struct sr_dev_inst *sdi) +{ + if (hantek_dso2xxx_tcp_connect(sdi) != SR_OK) + return SR_ERR; + + sdi->status = SR_ST_ACTIVE; + return SR_OK; +} + +static int dev_close(struct sr_dev_inst *sdi) +{ + hantek_dso2xxx_tcp_close(sdi); + sdi->status = SR_ST_INACTIVE; + return SR_OK; +} + +static int config_get(uint32_t key, GVariant **data, + const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + struct dev_context *devc; + + (void)cg; + + if (!sdi) + return SR_ERR_ARG; + devc = sdi->priv; + + switch (key) { + case SR_CONF_CONN: + *data = g_variant_new_printf("%s/%s", + devc->tcp_address, + devc->tcp_port); + break; + case SR_CONF_SAMPLERATE: + if (devc->sample_period > 0.0f) + *data = g_variant_new_uint64( + (uint64_t)(1.0f / devc->sample_period)); + else + *data = g_variant_new_uint64(0); + break; + case SR_CONF_LIMIT_FRAMES: + *data = g_variant_new_uint64(devc->frames_received); + break; + default: + return SR_ERR_NA; + } + return SR_OK; +} + +static int config_set(uint32_t key, GVariant *data, + const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + struct dev_context *devc; + + (void)cg; + (void)data; + + if (!sdi) + return SR_ERR_ARG; + devc = sdi->priv; + (void)devc; + + /* + * The only writable key is LIMIT_FRAMES, but we treat the driver as + * "no configurable options": just accept and ignore it for now so + * frontends don't error out. + */ + switch (key) { + case SR_CONF_LIMIT_FRAMES: + /* No-op: the driver runs continuously until the session ends. */ + break; + default: + return SR_ERR_NA; + } + return SR_OK; +} + +static int config_list(uint32_t key, GVariant **data, + const struct sr_dev_inst *sdi, + const struct sr_channel_group *cg) +{ + return STD_CONFIG_LIST(key, data, sdi, cg, + scanopts, drvopts, devopts); +} + +/* -------------------------------------------------------------------------- + * Acquisition I/O callback (runs in the GLib main loop) + * -------------------------------------------------------------------------- */ + +/** + * GLib I/O watch callback. Called whenever the TCP socket is readable, + * which means the scope has started sending a waveform frame. + */ +static int receive_data(int fd, int revents, void *cb_data) +{ + const struct sr_dev_inst *sdi = cb_data; + struct dev_context *devc; + + (void)fd; + devc = sdi->priv; + + if (!devc->acq_running) + return FALSE; /* Unregister the source. */ + + if (revents & (G_IO_ERR | G_IO_HUP)) { + sr_err("Socket error / hangup during acquisition."); + dev_acquisition_stop((struct sr_dev_inst *)sdi); + return FALSE; + } + + if (revents & G_IO_IN) { + if (hantek_dso2xxx_receive_frame(sdi) != SR_OK) { + sr_err("Frame receive failed; stopping acquisition."); + dev_acquisition_stop((struct sr_dev_inst *)sdi); + return FALSE; + } + } + + return TRUE; /* Keep the I/O watch active for the next frame. */ +} + +static int dev_acquisition_start(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + + if (sdi->status != SR_ST_ACTIVE) + return SR_ERR_DEV_CLOSED; + + devc->acq_running = TRUE; + devc->frames_received = 0; + + std_session_send_df_header(sdi); + + /* + * Register the socket fd as a GLib I/O source so receive_data() + * is called each time the scope sends a frame (triggered by the + * user pressing SAVE TO USB). + */ + sr_session_source_add(sdi->session, devc->sockfd, + G_IO_IN | G_IO_ERR | G_IO_HUP, + HANTEK_TCP_TIMEOUT_MS, + receive_data, (void *)sdi); + + sr_info("Acquisition started – press SAVE TO USB on the scope."); + return SR_OK; +} + +static int dev_acquisition_stop(struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + + if (!devc->acq_running) + return SR_OK; + + devc->acq_running = FALSE; + sr_session_source_remove(sdi->session, devc->sockfd); + std_session_send_df_end(sdi); + + sr_info("Acquisition stopped. Frames received: %" PRIu64, + devc->frames_received); + return SR_OK; +} + +/* -------------------------------------------------------------------------- + * Driver instance definition + * -------------------------------------------------------------------------- */ + +static struct sr_dev_driver hantek_dso2xxx_driver_info = { + .name = "hantek-dso2xxx", + .longname = "Hantek DSO2xxx (quick-fetch)", + .api_version = 1, + .init = std_init, + .cleanup = std_cleanup, + .scan = scan, + .dev_list = std_dev_list, + .dev_clear = std_dev_clear, + .config_get = config_get, + .config_set = config_set, + .config_list = config_list, + .dev_open = dev_open, + .dev_close = dev_close, + .dev_acquisition_start = dev_acquisition_start, + .dev_acquisition_stop = dev_acquisition_stop, + .context = NULL, +}; +SR_REGISTER_DEV_DRIVER(hantek_dso2xxx_driver_info); diff --git a/src/hardware/hantek-dso2xxx/protocol.c b/src/hardware/hantek-dso2xxx/protocol.c new file mode 100644 index 000000000..0ef27df83 --- /dev/null +++ b/src/hardware/hantek-dso2xxx/protocol.c @@ -0,0 +1,336 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2024 libsigrok contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file protocol.c + * + * Low-level TCP communication and binary frame decoding for the + * Hantek DSO2xxx quick-fetch driver. + * + * Protocol overview + * ----------------- + * The patched scope firmware (LD_PRELOAD'd quick-fetch.so) listens on a + * TCP port. When the user presses SAVE TO USB the scope transmits a single + * binary blob per trigger event: + * + * 1. struct hantek_frame_header (32 bytes, all fields little-endian) + * 2. CH1 sample data (num_samples × int8_t, if ch1_enabled) + * 3. CH2 sample data (num_samples × int8_t, if ch2_enabled) + * + * The connection stays open between frames; we simply block-read the next + * header to detect the next waveform. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "libsigrok-internal.h" +#include "protocol.h" + +/* -------------------------------------------------------------------------- + * Helpers + * -------------------------------------------------------------------------- */ + +/** + * Perform a full blocking read of exactly @p len bytes into @p buf. + * + * Returns SR_OK on success, SR_ERR_IO on any error or EOF. + */ +static int tcp_read_all(int fd, void *buf, size_t len) +{ + uint8_t *ptr = (uint8_t *)buf; + size_t remaining = len; + ssize_t n; + + while (remaining > 0) { + n = read(fd, ptr, remaining); + if (n <= 0) { + if (n < 0 && (errno == EINTR || errno == EAGAIN)) + continue; + sr_err("TCP read error: %s (wanted %zu, got %zd)", + n < 0 ? strerror(errno) : "EOF", + len, (ssize_t)(len - remaining)); + return SR_ERR_IO; + } + ptr += n; + remaining -= (size_t)n; + } + return SR_OK; +} + +/* -------------------------------------------------------------------------- + * Public API + * -------------------------------------------------------------------------- */ + +/** + * Open a TCP connection to the scope. + * + * Populates devc->sockfd on success. + */ +SR_PRIV int hantek_dso2xxx_tcp_connect(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + struct addrinfo hints, *res, *rp; + int fd, rc, one = 1; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + rc = getaddrinfo(devc->tcp_address, devc->tcp_port, &hints, &res); + if (rc != 0) { + sr_err("getaddrinfo(%s:%s): %s", + devc->tcp_address, devc->tcp_port, gai_strerror(rc)); + return SR_ERR; + } + + fd = -1; + for (rp = res; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd < 0) + continue; + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) + break; + close(fd); + fd = -1; + } + freeaddrinfo(res); + + if (fd < 0) { + sr_err("Could not connect to %s:%s", + devc->tcp_address, devc->tcp_port); + return SR_ERR; + } + + /* Disable Nagle for lower latency on the control path. */ + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); + + devc->sockfd = fd; + sr_info("Connected to Hantek DSO2xxx at %s:%s", + devc->tcp_address, devc->tcp_port); + return SR_OK; +} + +/** + * Close the TCP connection. + */ +SR_PRIV void hantek_dso2xxx_tcp_close(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + + if (devc->sockfd >= 0) { + close(devc->sockfd); + devc->sockfd = -1; + sr_dbg("TCP connection closed."); + } +} + +/** + * Read one complete waveform frame from the scope and dispatch + * SR_DF_ANALOG packets for each enabled channel. + * + * This function blocks until the full frame has been received. + * + * @return SR_OK on success, SR_ERR* on failure. + */ +SR_PRIV int hantek_dso2xxx_receive_frame(const struct sr_dev_inst *sdi) +{ + struct dev_context *devc = sdi->priv; + struct hantek_frame_header hdr; + int8_t *raw_buf = NULL; + float *float_buf = NULL; + int ret = SR_ERR; + uint32_t ns; + size_t ch_bytes; + uint32_t i; + + /* ------------------------------------------------------------------ * + * 1. Read and validate the frame header. * + * ------------------------------------------------------------------ */ + if (tcp_read_all(devc->sockfd, &hdr, sizeof(hdr)) != SR_OK) { + sr_err("Failed to read frame header."); + return SR_ERR_IO; + } + + if (GUINT32_FROM_LE(hdr.magic) != HANTEK_FRAME_MAGIC_LE) { + sr_err("Bad frame magic: 0x%08X (expected 0x%08X)", + GUINT32_FROM_LE(hdr.magic), HANTEK_FRAME_MAGIC_LE); + return SR_ERR_DATA; + } + + /* Byte-swap all fields from little-endian to host order. */ + ns = GUINT32_FROM_LE(hdr.num_samples); + if (ns == 0 || ns > 8 * 1024 * 1024) { + sr_err("Implausible sample count: %u", ns); + return SR_ERR_DATA; + } + + /* + * The float fields are transmitted in the endianness of the scope's + * ARM CPU (little-endian). On little-endian hosts this is a no-op; + * on big-endian hosts we byte-swap via the 32-bit integer trick. + */ +#if G_BYTE_ORDER == G_BIG_ENDIAN + { + uint32_t tmp; +# define BSWAP_FLOAT(f) do { memcpy(&tmp, &(f), 4); \ + tmp = GUINT32_SWAP_LE_BE(tmp); \ + memcpy(&(f), &tmp, 4); } while (0) + BSWAP_FLOAT(hdr.sample_period); + BSWAP_FLOAT(hdr.time_per_div); + BSWAP_FLOAT(hdr.ch1_scale); + BSWAP_FLOAT(hdr.ch2_scale); +# undef BSWAP_FLOAT + hdr.ch1_offset_raw = (int32_t)GUINT32_SWAP_LE_BE( + (uint32_t)hdr.ch1_offset_raw); + hdr.ch2_offset_raw = (int32_t)GUINT32_SWAP_LE_BE( + (uint32_t)hdr.ch2_offset_raw); + } +#endif + + /* Cache header metadata in device context for later inspection. */ + devc->num_samples = ns; + devc->sample_period = hdr.sample_period; + devc->ch1_enabled = hdr.ch1_enabled; + devc->ch2_enabled = hdr.ch2_enabled; + devc->ch1_scale = hdr.ch1_scale; + devc->ch2_scale = hdr.ch2_scale; + devc->ch1_offset_raw = hdr.ch1_offset_raw; + devc->ch2_offset_raw = hdr.ch2_offset_raw; + + sr_dbg("Frame: %u samples, period=%.3e s, CH1=%s CH2=%s", + ns, hdr.sample_period, + hdr.ch1_enabled ? "on" : "off", + hdr.ch2_enabled ? "on" : "off"); + + /* ------------------------------------------------------------------ * + * 2. Allocate receive and conversion buffers. * + * ------------------------------------------------------------------ */ + ch_bytes = (size_t)ns * sizeof(int8_t); + raw_buf = g_malloc(ch_bytes); + if (!raw_buf) { + sr_err("Out of memory allocating raw sample buffer (%zu bytes).", + ch_bytes); + return SR_ERR_MALLOC; + } + + float_buf = g_malloc(ns * sizeof(float)); + if (!float_buf) { + sr_err("Out of memory allocating float sample buffer."); + g_free(raw_buf); + return SR_ERR_MALLOC; + } + + /* ------------------------------------------------------------------ * + * 3. Send SR_DF_FRAME_BEGIN * + * ------------------------------------------------------------------ */ + std_session_send_df_frame_begin(sdi); + + /* ------------------------------------------------------------------ * + * 4. Process each enabled channel. * + * ------------------------------------------------------------------ */ + + /* + * Helper lambda-like macro to read and dispatch one channel. + * + * @param CH_IDX 0-based channel index into sdi->channels + * @param ENABLED hdr.ch1_enabled / hdr.ch2_enabled + * @param SCALE hdr.ch1_scale / hdr.ch2_scale + * @param OFFSET hdr.ch1_offset_raw / hdr.ch2_offset_raw + * @param LABEL string literal for log messages + */ +#define DISPATCH_CHANNEL(CH_IDX, ENABLED, SCALE, OFFSET, LABEL) \ + do { \ + struct sr_channel *ch; \ + struct sr_datafeed_packet pkt; \ + struct sr_datafeed_analog analog; \ + struct sr_analog_encoding enc; \ + struct sr_analog_meaning meaning; \ + struct sr_analog_spec spec; \ + GSList *chl = NULL; \ + \ + if (!(ENABLED)) \ + break; \ + \ + /* Read raw signed 8-bit samples from the stream. */ \ + if (tcp_read_all(devc->sockfd, raw_buf, ch_bytes) != SR_OK) { \ + sr_err("Failed to read " LABEL " samples."); \ + ret = SR_ERR_IO; \ + goto cleanup; \ + } \ + \ + /* \ + * Convert raw ADC counts to volts: \ + * voltage = (raw - offset_raw) * (scale / counts_per_div) \ + */ \ + for (i = 0; i < ns; i++) { \ + float_buf[i] = \ + ((float)raw_buf[i] - (float)(OFFSET)) * \ + ((SCALE) / (float)HANTEK_COUNTS_PER_DIV); \ + } \ + \ + /* Locate the sr_channel object for this channel index. */ \ + ch = g_slist_nth_data(sdi->channels, (CH_IDX)); \ + if (!ch || !ch->enabled) { \ + sr_dbg(LABEL " not enabled in session, skipping."); \ + break; \ + } \ + chl = g_slist_append(NULL, ch); \ + \ + sr_analog_init(&analog, &enc, &meaning, &spec, 6); \ + meaning.mq = SR_MQ_VOLTAGE; \ + meaning.unit = SR_UNIT_VOLT; \ + meaning.mqflags = SR_MQFLAG_DC; \ + meaning.channels = chl; \ + analog.data = float_buf; \ + analog.num_samples = ns; \ + \ + pkt.type = SR_DF_ANALOG; \ + pkt.payload = &analog; \ + sr_session_send(sdi, &pkt); \ + g_slist_free(chl); \ + } while (0) + + DISPATCH_CHANNEL(0, hdr.ch1_enabled, hdr.ch1_scale, + hdr.ch1_offset_raw, "CH1"); + DISPATCH_CHANNEL(1, hdr.ch2_enabled, hdr.ch2_scale, + hdr.ch2_offset_raw, "CH2"); + +#undef DISPATCH_CHANNEL + + /* ------------------------------------------------------------------ * + * 5. Send SR_DF_FRAME_END * + * ------------------------------------------------------------------ */ + std_session_send_df_frame_end(sdi); + + devc->frames_received++; + ret = SR_OK; + +cleanup: + g_free(float_buf); + g_free(raw_buf); + return ret; +} diff --git a/src/hardware/hantek-dso2xxx/protocol.h b/src/hardware/hantek-dso2xxx/protocol.h new file mode 100644 index 000000000..587a8214b --- /dev/null +++ b/src/hardware/hantek-dso2xxx/protocol.h @@ -0,0 +1,133 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2024 libsigrok contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Driver for the Hantek DSO2xxx series oscilloscopes using the + * "quick-fetch" firmware patch by phmarek + * (https://github.com/phmarek/hantek-dso2000-quick-fetch). + * + * The patched firmware exposes a TCP server (default port 5025) on the + * scope's network interface (requires DavidAlfa's USB-networking kernel). + * When the user presses SAVE TO USB the scope sends one binary frame: + * + * [frame_header] (struct hantek_frame_header, 32 bytes, little-endian) + * [ch1_samples] (num_samples × int8_t, only if ch1_enabled != 0) + * [ch2_samples] (num_samples × int8_t, only if ch2_enabled != 0) + * + * The header layout has been inferred from the Perl receiver script: + * + * uint32_t magic; // 0x48544B32 = "HTK2" + * uint32_t num_samples; // number of samples per channel + * float time_per_div; // seconds per division + * float sample_period; // seconds between samples (= 1/samplerate) + * uint8_t ch1_enabled; + * uint8_t ch2_enabled; + * uint8_t _reserved[2]; + * float ch1_scale; // volts per division + * float ch2_scale; + * int32_t ch1_offset_raw; // raw offset (signed ADC units) + * int32_t ch2_offset_raw; + * + * Each sample is a signed 8-bit integer in raw ADC units. + * Conversion: voltage = (raw_sample - offset_raw) * (scale / 25.0f) + * (25 ADC counts per division, 8 divisions visible → 200 count full range) + */ + +#ifndef LIBSIGROK_HARDWARE_HANTEK_DSO2XXX_PROTOCOL_H +#define LIBSIGROK_HARDWARE_HANTEK_DSO2XXX_PROTOCOL_H + +#include +#include +#include +#include "libsigrok-internal.h" + +#define LOG_PREFIX "hantek-dso2xxx" + +/** Magic bytes at the start of every frame: ASCII "HTK2". */ +#define HANTEK_FRAME_MAGIC 0x32KTH /* little-endian: 'H','T','K','2' */ +#define HANTEK_FRAME_MAGIC_LE 0x32KTH + +/* + * Use the actual 4-byte value directly so the compiler doesn't trip over + * the macro above – keep it readable via a comment. + */ +#undef HANTEK_FRAME_MAGIC_LE +/* 'H'=0x48 'T'=0x54 'K'=0x4B '2'=0x32 → little-endian uint32 = 0x324B5448 */ +#define HANTEK_FRAME_MAGIC_LE UINT32_C(0x324B5448) + +/** Default TCP port exposed by the patched firmware. */ +#define HANTEK_DEFAULT_PORT "5025" + +/** Default TCP address (scope must be on the network via USB-networking). */ +#define HANTEK_DEFAULT_ADDR "192.168.7.1" + +/** I/O timeout for TCP operations, in milliseconds. */ +#define HANTEK_TCP_TIMEOUT_MS 60000 + +/** Number of ADC counts per oscilloscope division. */ +#define HANTEK_COUNTS_PER_DIV 25 + +#pragma pack(push, 1) +/** + * Binary frame header sent by the patched DSO firmware. + * All multi-byte fields are little-endian (the scope runs on ARM/Allwinner). + */ +struct hantek_frame_header { + uint32_t magic; /**< Must equal HANTEK_FRAME_MAGIC_LE. */ + uint32_t num_samples; /**< Samples per enabled channel. */ + float time_per_div; /**< Seconds per timebase division. */ + float sample_period; /**< Seconds between consecutive samples. */ + uint8_t ch1_enabled; /**< Non-zero if CH1 data follows. */ + uint8_t ch2_enabled; /**< Non-zero if CH2 data follows. */ + uint8_t reserved[2]; + float ch1_scale; /**< CH1 volts per division. */ + float ch2_scale; /**< CH2 volts per division. */ + int32_t ch1_offset_raw; /**< CH1 vertical offset in ADC counts. */ + int32_t ch2_offset_raw; /**< CH2 vertical offset in ADC counts. */ +}; +#pragma pack(pop) + +/** Per-device instance state. */ +struct dev_context { + /* Network connection */ + char *tcp_address; /**< IP / hostname of the scope. */ + char *tcp_port; /**< TCP port string. */ + int sockfd; /**< Connected socket fd, or -1. */ + + /* Acquisition state */ + gboolean acq_running; + uint64_t frames_received; + + /* Channel metadata filled in from the most recent header */ + uint32_t num_samples; + float sample_period; /**< 1 / samplerate */ + uint8_t ch1_enabled; + uint8_t ch2_enabled; + float ch1_scale; + float ch2_scale; + int32_t ch1_offset_raw; + int32_t ch2_offset_raw; +}; + +/* Forward declarations for api.c ↔ protocol.c interface */ +SR_PRIV int hantek_dso2xxx_tcp_connect(const struct sr_dev_inst *sdi); +SR_PRIV void hantek_dso2xxx_tcp_close(const struct sr_dev_inst *sdi); +SR_PRIV int hantek_dso2xxx_receive_frame(const struct sr_dev_inst *sdi); + +#endif /* LIBSIGROK_HARDWARE_HANTEK_DSO2XXX_PROTOCOL_H */ From af6e7de637d0a485fc49394a9ba57a32f4417ed8 Mon Sep 17 00:00:00 2001 From: Philipp Marek Date: Tue, 3 Mar 2026 14:29:32 +0100 Subject: [PATCH 2/2] Fixes to get the driver working. Currently only "mirrors" the data from the 'scope, but that's already 99% of the usecase for me -- using sigrok for manual automated analysis (timing, serial, ...) --- Makefile.am | 6 + configure.ac | 1 + .../hantek-dso2xxx/Makefile.am.fragment | 13 - src/hardware/hantek-dso2xxx/README.md | 73 +--- src/hardware/hantek-dso2xxx/api.c | 106 ++++-- src/hardware/hantek-dso2xxx/protocol.c | 328 ++++++++++-------- src/hardware/hantek-dso2xxx/protocol.h | 120 ++++--- 7 files changed, 342 insertions(+), 305 deletions(-) delete mode 100644 src/hardware/hantek-dso2xxx/Makefile.am.fragment diff --git a/Makefile.am b/Makefile.am index 3b68142f2..2b6b95146 100644 --- a/Makefile.am +++ b/Makefile.am @@ -436,6 +436,12 @@ src_libdrivers_la_SOURCES += \ src/hardware/hantek-dso/protocol.c \ src/hardware/hantek-dso/api.c endif +if HW_HANTEK_DSO2XXX +src_libdrivers_la_SOURCES += \ + src/hardware/hantek-dso2xxx/api.c \ + src/hardware/hantek-dso2xxx/protocol.c \ + src/hardware/hantek-dso2xxx/protocol.h +endif if HW_HP_3457A src_libdrivers_la_SOURCES += \ src/hardware/hp-3457a/protocol.h \ diff --git a/configure.ac b/configure.ac index 024dd4da2..2b4857fad 100644 --- a/configure.ac +++ b/configure.ac @@ -341,6 +341,7 @@ SR_DRIVER([Hameg HMO], [hameg-hmo]) SR_DRIVER([Hantek 4032L], [hantek-4032l], [libusb]) SR_DRIVER([Hantek 6xxx], [hantek-6xxx], [libusb]) SR_DRIVER([Hantek DSO], [hantek-dso], [libusb]) +SR_DRIVER([Hantek DSO 2xxx], [hantek-dso2xxx], []) SR_DRIVER([HP 3457A], [hp-3457a]) SR_DRIVER([HP 3478A], [hp-3478a], [libgpib]) SR_DRIVER([hp-59306a], [hp-59306a]) diff --git a/src/hardware/hantek-dso2xxx/Makefile.am.fragment b/src/hardware/hantek-dso2xxx/Makefile.am.fragment deleted file mode 100644 index 5e894553c..000000000 --- a/src/hardware/hantek-dso2xxx/Makefile.am.fragment +++ /dev/null @@ -1,13 +0,0 @@ -## Makefile.am fragment for the hantek-dso2xxx driver. -## Add this block to src/hardware/Makefile.am in the libsigrok tree, -## following the pattern of any other network-connected driver. -## -## The driver has no external library dependencies beyond what -## libsigrok already requires (glib, POSIX sockets). - -if HW_HANTEK_DSO2XXX -libsigrok_la_SOURCES += \ - hardware/hantek-dso2xxx/api.c \ - hardware/hantek-dso2xxx/protocol.c \ - hardware/hantek-dso2xxx/protocol.h -endif diff --git a/src/hardware/hantek-dso2xxx/README.md b/src/hardware/hantek-dso2xxx/README.md index 1a53a67d8..d14f5ad72 100644 --- a/src/hardware/hantek-dso2xxx/README.md +++ b/src/hardware/hantek-dso2xxx/README.md @@ -2,39 +2,19 @@ A libsigrok driver for the Hantek DSO2xxx oscilloscope series (DSO2C10, DSO2C15, DSO2D10, DSO2D15) that receives waveform data over TCP using the -quick-fetch firmware patch by phmarek: +`quick-fetch` firmware patch by phmarek: https://github.com/phmarek/hantek-dso2000-quick-fetch +Initial contents generated via Claude.AI, +https://claude.ai/share/bcc56b39-53ab-45be-a91d-251cbe64fa98 + --- ## How it works -The patched firmware (quick-fetch.so, LD_PRELOAD'd on the scope) listens -on a TCP port. When the user presses SAVE TO USB the scope freezes the -current waveform and streams a compact binary frame to any connected client. - -Binary frame layout (all fields little-endian): - - Offset Size Type Field - ------ ---- ------- -------------------------------- - 0 4 uint32 magic = 0x324B5448 ("HTK2") - 4 4 uint32 num_samples (per enabled channel) - 8 4 float32 time_per_div (seconds/division) - 12 4 float32 sample_period (seconds; 1/samplerate) - 16 1 uint8 ch1_enabled (non-zero if CH1 data follows) - 17 1 uint8 ch2_enabled (non-zero if CH2 data follows) - 18 2 uint8[2] reserved - 20 4 float32 ch1_scale (volts/division) - 24 4 float32 ch2_scale - 28 4 int32 ch1_offset_raw (signed ADC counts) - 32 4 int32 ch2_offset_raw - -After the header: CH1 samples (num_samples x int8, if ch1_enabled != 0), -then CH2 samples (num_samples x int8, if ch2_enabled != 0). - -Voltage conversion: - voltage = (raw_sample - offset_raw) * (scale_V_per_div / 25.0) -(25 ADC counts per division; 8 divisions gives ~200 count full range) +The patched firmware (`quick-fetch.so`, `LD_PRELOAD`'d on the scope) listens +on a TCP port. When the user presses `SAVE_TO_USB` the scope freezes the +current waveform and streams a compact binary frame to the connected client. --- @@ -43,38 +23,12 @@ Voltage conversion: 1. Install the quick-fetch patch on your scope following the instructions at https://github.com/phmarek/hantek-dso2000-quick-fetch -2. Use DavidAlfa's USB-networking kernel so the scope appears at - 192.168.7.1 via USB networking. Without this, the TCP server is not +2. Use DavidAlfa's USB-networking kernel so the scope appears at (e.g.) + 172.31.254.254 via USB networking. Without this, the TCP server is not reachable. --- -## Building - -Place the three source files in src/hardware/hantek-dso2xxx/ inside the -libsigrok source tree, then: - -1. Add the Makefile.am.fragment content to src/hardware/Makefile.am. -2. Add HW_HANTEK_DSO2XXX enable/disable boilerplate to configure.ac - (copy the pattern from any comparable driver such as rigol-ds). -3. Re-run ./autogen.sh && ./configure && make - ---- - -## Usage - - # Single capture (waits for one SAVE TO USB press) - sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025 -o capture.sr - - # Continuous (keeps waiting for further presses) - sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025 --continuous - - # Custom address/port - sigrok-cli -d hantek-dso2xxx:conn=10.0.0.5/5025 -o capture.sr - -Open the .sr file in PulseView for graphical display. - ---- ## No configurable acquisition options @@ -82,11 +36,8 @@ All scope settings (timebase, volts/div, coupling, trigger, memory depth) are configured directly on the instrument front panel. The driver reads them passively from the binary frame header. -Session-level keys: - - conn GET Returns "address/port" string - samplerate GET Derived from sample_period field - limit_frames GET Count of frames received so far +If there's much interest I'll update both the `quick-fetch` patch +and the driver to allow modifying the trigger level, position, etc. --- @@ -95,7 +46,7 @@ Session-level keys: The quick-fetch patch supports: 3.0.0(220727.00) - 2022-07-27 3.0.0(230327.00) - 2023-03-27 - 3.0.1(250418.00) - 2025-04-18 (patch v1.3+) + 3.0.1(250418.00) - 2025-04-18 --- diff --git a/src/hardware/hantek-dso2xxx/api.c b/src/hardware/hantek-dso2xxx/api.c index 80b63f7cb..39425b1a8 100644 --- a/src/hardware/hantek-dso2xxx/api.c +++ b/src/hardware/hantek-dso2xxx/api.c @@ -1,7 +1,7 @@ /* * This file is part of the libsigrok project. * - * Copyright (C) 2024 libsigrok contributors + * Copyright (C) 2026 Philipp Marek * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,32 +29,28 @@ * TCP to the scope's IP address (requires DavidAlfa's USB-networking kernel * image, which exposes the scope at 192.168.7.1 by default). * - * Because no SCPI or serial scan is possible, scan() always returns exactly - * one virtual device when a conn= option is provided: + * No SCPI or serial scan is possible; scan() accepts a conn= option, + * but will try the default IP/port as well. * - * sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025 --continuous + * sigrok-cli -d hantek-dso2xxx:conn=192.168.7.1/5025* * * No configurable scan/acquisition options are exposed (the driver follows - * all scope settings passively). Waveform capture is triggered by pressing - * the SAVE TO USB button on the instrument. - * - * Driver architecture - * ------------------- - * - scan() parses conn= option, allocates sr_dev_inst - * - dev_open() TCP connect - * - dev_close() TCP disconnect - * - dev_acquisition_start() registers GLib I/O watch on the socket fd - * - receive_data() GLib callback: reads one frame, sends packets, - * calls dev_acquisition_stop() if limit reached - * - dev_acquisition_stop() removes I/O watch, sends SR_DF_END + * all scope settings passively). Waveform capture is triggered by pressing + * the SAVE TO USB button on the instrument, sigrok can ask for frames as well, though. */ +#define _GNU_SOURCE +#include "protocol.h" #include #include #include + +#include +#include +#include +#include #include #include "libsigrok-internal.h" -#include "protocol.h" /* -------------------------------------------------------------------------- * Static driver metadata @@ -75,7 +71,7 @@ static const uint32_t drvopts[] = { static const uint32_t devopts[] = { SR_CONF_CONN | SR_CONF_GET, SR_CONF_LIMIT_FRAMES | SR_CONF_GET | SR_CONF_SET, - SR_CONF_SAMPLERATE | SR_CONF_GET, + SR_CONF_SAMPLERATE | SR_CONF_GET | SR_CONF_SET, }; /* -------------------------------------------------------------------------- @@ -93,13 +89,17 @@ static void parse_conn(const char *conn, char **addr_out, char **port_out) { const char *sep; char *addr, *port; + const char tcp_raw[] = "tcp-raw/"; + + if (strncmp(conn, tcp_raw, sizeof(tcp_raw)-1) == 0) + conn += sizeof(tcp_raw)-1; sep = strrchr(conn, '/'); if (!sep) sep = strrchr(conn, ':'); if (sep) { - addr = g_strndup(conn, (gsize)(sep - conn)); + addr = g_strndup(conn, sep - conn); port = g_strdup(sep + 1); } else { addr = g_strdup(conn); @@ -116,13 +116,14 @@ static void parse_conn(const char *conn, char **addr_out, char **port_out) static GSList *scan(struct sr_dev_driver *di, GSList *options) { - struct sr_dev_inst *sdi; - struct dev_context *devc; + struct sr_dev_inst *sdi = NULL; + struct dev_context *devc = NULL; struct sr_config *src; GSList *devices = NULL; GSList *l; const char *conn = NULL; char *addr, *port; + int status; /* Require a conn= option – we cannot enumerate scopes. */ for (l = options; l; l = l->next) { @@ -132,12 +133,16 @@ static GSList *scan(struct sr_dev_driver *di, GSList *options) break; } } - if (!conn) { - sr_err("This driver requires conn=[/] to be specified."); - return NULL; + + if (conn) { + parse_conn(conn, &addr, &port); + } else { + /* Is there an unscan() to drop memory allocated? + * Then this would be wrong... */ + addr = HANTEK_DEFAULT_ADDR; + port = HANTEK_DEFAULT_PORT; } - parse_conn(conn, &addr, &port); sr_dbg("Creating device for %s:%s", addr, port); sdi = g_malloc0(sizeof(*sdi)); @@ -156,23 +161,39 @@ static GSList *scan(struct sr_dev_driver *di, GSList *options) devc->sockfd = -1; sdi->priv = devc; + status = hantek_dso2xxx_tcp_connect(sdi); + if (status != SR_OK) { + goto nope; + } + + status = hantek_dso2xxx_timesync(devc); + if (status != SR_OK) { + sr_err("Timesync failure (to %s:%s)", + devc->tcp_address, devc->tcp_port); + goto nope; + } + + hantek_dso2xxx_tcp_close(sdi); + /* Register with the driver instance list. */ devices = g_slist_append(devices, sdi); return std_scan_complete(di, devices); + +nope: + sr_dev_inst_free(sdi); + return NULL; } static int dev_open(struct sr_dev_inst *sdi) { - if (hantek_dso2xxx_tcp_connect(sdi) != SR_OK) - return SR_ERR; - sdi->status = SR_ST_ACTIVE; + /* The TCP connection is opened only as-needed. */ + return SR_OK; } static int dev_close(struct sr_dev_inst *sdi) { - hantek_dso2xxx_tcp_close(sdi); sdi->status = SR_ST_INACTIVE; return SR_OK; } @@ -196,11 +217,7 @@ static int config_get(uint32_t key, GVariant **data, devc->tcp_port); break; case SR_CONF_SAMPLERATE: - if (devc->sample_period > 0.0f) - *data = g_variant_new_uint64( - (uint64_t)(1.0f / devc->sample_period)); - else - *data = g_variant_new_uint64(0); + *data = g_variant_new_uint64(devc->sample_rate); break; case SR_CONF_LIMIT_FRAMES: *data = g_variant_new_uint64(devc->frames_received); @@ -269,14 +286,14 @@ static int receive_data(int fd, int revents, void *cb_data) if (revents & (G_IO_ERR | G_IO_HUP)) { sr_err("Socket error / hangup during acquisition."); - dev_acquisition_stop((struct sr_dev_inst *)sdi); + sr_dev_acquisition_stop((struct sr_dev_inst *)sdi); return FALSE; } if (revents & G_IO_IN) { if (hantek_dso2xxx_receive_frame(sdi) != SR_OK) { sr_err("Frame receive failed; stopping acquisition."); - dev_acquisition_stop((struct sr_dev_inst *)sdi); + sr_dev_acquisition_stop((struct sr_dev_inst *)sdi); return FALSE; } } @@ -291,6 +308,13 @@ static int dev_acquisition_start(const struct sr_dev_inst *sdi) if (sdi->status != SR_ST_ACTIVE) return SR_ERR_DEV_CLOSED; + if (hantek_dso2xxx_tcp_connect(sdi) != SR_OK) + return SR_ERR; + if (hantek_dso2xxx_timesync(devc) != SR_OK) + return SR_ERR; + if (hantek_dso2xxx_acq_now(devc) != SR_OK) + return SR_ERR; + devc->acq_running = TRUE; devc->frames_received = 0; @@ -314,6 +338,8 @@ static int dev_acquisition_stop(struct sr_dev_inst *sdi) { struct dev_context *devc = sdi->priv; + hantek_dso2xxx_tcp_close(sdi); + if (!devc->acq_running) return SR_OK; @@ -326,6 +352,12 @@ static int dev_acquisition_stop(struct sr_dev_inst *sdi) return SR_OK; } +SR_PRIV int drv_init(struct sr_dev_driver *di, struct sr_context *sr_ctx) +{ + hantek_dso2xxx_c_locale = newlocale(LC_ALL_MASK, "C", NULL); + return std_init(di, sr_ctx); +} + /* -------------------------------------------------------------------------- * Driver instance definition * -------------------------------------------------------------------------- */ @@ -334,7 +366,7 @@ static struct sr_dev_driver hantek_dso2xxx_driver_info = { .name = "hantek-dso2xxx", .longname = "Hantek DSO2xxx (quick-fetch)", .api_version = 1, - .init = std_init, + .init = drv_init, .cleanup = std_cleanup, .scan = scan, .dev_list = std_dev_list, diff --git a/src/hardware/hantek-dso2xxx/protocol.c b/src/hardware/hantek-dso2xxx/protocol.c index 0ef27df83..6888c35ef 100644 --- a/src/hardware/hantek-dso2xxx/protocol.c +++ b/src/hardware/hantek-dso2xxx/protocol.c @@ -37,6 +37,8 @@ * header to detect the next waveform. */ +#include "protocol.h" + #include #include #include @@ -44,15 +46,16 @@ #include #include #include +#include #include #include +#include #include #include "libsigrok-internal.h" -#include "protocol.h" -/* -------------------------------------------------------------------------- - * Helpers - * -------------------------------------------------------------------------- */ + +locale_t hantek_dso2xxx_c_locale; + /** * Perform a full blocking read of exactly @p len bytes into @p buf. @@ -81,9 +84,25 @@ static int tcp_read_all(int fd, void *buf, size_t len) return SR_OK; } -/* -------------------------------------------------------------------------- - * Public API - * -------------------------------------------------------------------------- */ +SR_PRIV void send_session_update(const struct sr_dev_inst *sdi, + int what, GVariant *value) +{ + struct sr_datafeed_packet packet; + struct sr_datafeed_meta meta; + struct sr_config *cfg; + + cfg = sr_config_new(what, value); + + meta.config = g_slist_append(NULL, cfg); + + packet.type = SR_DF_META; + packet.payload = &meta; + + sr_session_send(sdi, &packet); + + g_slist_free(meta.config); + sr_config_free(cfg); +} /** * Open a TCP connection to the scope. @@ -134,6 +153,49 @@ SR_PRIV int hantek_dso2xxx_tcp_connect(const struct sr_dev_inst *sdi) return SR_OK; } +SR_PRIV int hantek_dso2xxx_timeout(struct dev_context *devc) +{ + fd_set fds = { 0 }; + struct timeval timeout = { .tv_sec = 1, .tv_usec = 0 }; /* TODO: configurable? */ + + FD_SET(devc->sockfd, &fds); + if (select(devc->sockfd+1, &fds, NULL, NULL, &timeout) == 1) + return SR_OK; + return SR_ERR; +} + +SR_PRIV int hantek_dso2xxx_acq_now(struct dev_context *devc) +{ + const char now[] = "now\r\n"; + const int len = sizeof(now) -1; + + if (write(devc->sockfd, now, len) != len) + return SR_ERR; + return SR_OK; +} + +/* Do a time sync */ +SR_PRIV int hantek_dso2xxx_timesync(struct dev_context *devc) +{ + const char thx[] = "thx\r\n"; + char buffer[32]; + ssize_t len; + + len = sprintf(buffer, "time %ld\n", time(NULL)); + if (write(devc->sockfd, buffer, len) != len) + return SR_ERR; + + if (hantek_dso2xxx_timeout(devc) != SR_OK) + return SR_ERR; + + len = read(devc->sockfd, buffer, sizeof(buffer)-1); + /* Don't check ==, there may be an additional NUL byte */ + if (len < (long)sizeof(thx) -1) + return SR_ERR; + + return (strncmp(buffer, thx, len) == 0) ? SR_OK : SR_ERR_DATA; +} + /** * Close the TCP connection. */ @@ -148,6 +210,16 @@ SR_PRIV void hantek_dso2xxx_tcp_close(const struct sr_dev_inst *sdi) } } + +SR_PRIV double atof_for_C_locale(const char *input) +{ + return strtod_l(input, NULL, hantek_dso2xxx_c_locale); +} + +/* Writes outside the character vector, but not outside the buffer */ +#define terminate_and_conv(field, fn) ( field[ sizeof(field) ] = 0, fn(field) ) +#define terminate_and_atoi(field) (terminate_and_conv(field, atoi)) + /** * Read one complete waveform frame from the scope and dispatch * SR_DF_ANALOG packets for each enabled channel. @@ -160,88 +232,109 @@ SR_PRIV int hantek_dso2xxx_receive_frame(const struct sr_dev_inst *sdi) { struct dev_context *devc = sdi->priv; struct hantek_frame_header hdr; - int8_t *raw_buf = NULL; - float *float_buf = NULL; + int8_t byte_buffer[HANTEK_BLOCK_SIZE]; + float *float_buf[HANTEK_CHANNELS] = {}; int ret = SR_ERR; - uint32_t ns; size_t ch_bytes; - uint32_t i; + uint32_t channels, sample_nr, ch; + int i, pos; + struct sr_channel *channel; + struct sr_datafeed_packet pkt; + struct sr_datafeed_analog analog; + struct sr_analog_encoding enc; + struct sr_analog_meaning meaning; + struct sr_analog_spec spec; + GSList *chl = NULL; /* ------------------------------------------------------------------ * * 1. Read and validate the frame header. * * ------------------------------------------------------------------ */ if (tcp_read_all(devc->sockfd, &hdr, sizeof(hdr)) != SR_OK) { - sr_err("Failed to read frame header."); + sr_err(LOG_PREFIX "Failed to read frame header."); return SR_ERR_IO; } - if (GUINT32_FROM_LE(hdr.magic) != HANTEK_FRAME_MAGIC_LE) { - sr_err("Bad frame magic: 0x%08X (expected 0x%08X)", - GUINT32_FROM_LE(hdr.magic), HANTEK_FRAME_MAGIC_LE); + if (hdr.magic[0] != '#' || + hdr.magic[1] != '9') { + sr_err(LOG_PREFIX "Bad frame magic: 0x%02X 0x%02X", + hdr.magic[0], hdr.magic[1]); return SR_ERR_DATA; } - /* Byte-swap all fields from little-endian to host order. */ - ns = GUINT32_FROM_LE(hdr.num_samples); - if (ns == 0 || ns > 8 * 1024 * 1024) { - sr_err("Implausible sample count: %u", ns); - return SR_ERR_DATA; + channels = 0; + + /* Needs to be in last-to-first order as atoi needs NUL-terminated input! + * (strtol does, too, and sscanf with field lengths tries locales and + * other stuff we don't want.) */ + + devc->sample_rate = terminate_and_conv(hdr.sample_rate, atof_for_C_locale); + devc->sample_period = 1.0/devc->sample_rate; + + send_session_update(sdi, + SR_CONF_SAMPLERATE, + g_variant_new_uint64(devc->sample_rate)); + + for(i = HANTEK_CHANNELS-1; i>= 0; i--) { + devc->ch_enabled[i] = hdr.ch_enable[i] == '1'; + if (devc->ch_enabled[i]) + channels++; } - /* - * The float fields are transmitted in the endianness of the scope's - * ARM CPU (little-endian). On little-endian hosts this is a no-op; - * on big-endian hosts we byte-swap via the 32-bit integer trick. - */ -#if G_BYTE_ORDER == G_BIG_ENDIAN - { - uint32_t tmp; -# define BSWAP_FLOAT(f) do { memcpy(&tmp, &(f), 4); \ - tmp = GUINT32_SWAP_LE_BE(tmp); \ - memcpy(&(f), &tmp, 4); } while (0) - BSWAP_FLOAT(hdr.sample_period); - BSWAP_FLOAT(hdr.time_per_div); - BSWAP_FLOAT(hdr.ch1_scale); - BSWAP_FLOAT(hdr.ch2_scale); -# undef BSWAP_FLOAT - hdr.ch1_offset_raw = (int32_t)GUINT32_SWAP_LE_BE( - (uint32_t)hdr.ch1_offset_raw); - hdr.ch2_offset_raw = (int32_t)GUINT32_SWAP_LE_BE( - (uint32_t)hdr.ch2_offset_raw); + for(i = HANTEK_CHANNELS-1; i>= 0; i--) { + devc->ch_scale[i] = terminate_and_conv( hdr.ch_voltage[i], atof_for_C_locale); + } + for(i = HANTEK_CHANNELS-1; i>= 0; i--) { + devc->ch_offset[i] = hdr.ch_offset[i][0] | ( hdr.ch_offset[i][1] << 8); } -#endif - - /* Cache header metadata in device context for later inspection. */ - devc->num_samples = ns; - devc->sample_period = hdr.sample_period; - devc->ch1_enabled = hdr.ch1_enabled; - devc->ch2_enabled = hdr.ch2_enabled; - devc->ch1_scale = hdr.ch1_scale; - devc->ch2_scale = hdr.ch2_scale; - devc->ch1_offset_raw = hdr.ch1_offset_raw; - devc->ch2_offset_raw = hdr.ch2_offset_raw; - - sr_dbg("Frame: %u samples, period=%.3e s, CH1=%s CH2=%s", - ns, hdr.sample_period, - hdr.ch1_enabled ? "on" : "off", - hdr.ch2_enabled ? "on" : "off"); - /* ------------------------------------------------------------------ * - * 2. Allocate receive and conversion buffers. * - * ------------------------------------------------------------------ */ - ch_bytes = (size_t)ns * sizeof(int8_t); - raw_buf = g_malloc(ch_bytes); - if (!raw_buf) { - sr_err("Out of memory allocating raw sample buffer (%zu bytes).", - ch_bytes); - return SR_ERR_MALLOC; + devc->num_samples = terminate_and_atoi(hdr.total_length) / channels; + + sr_dbg(LOG_PREFIX "Frame: %u samples, rate=%.3e s, CH1=%s/%.1f%+d CH2=%s/%.1f%+d", + devc->num_samples, + (float)devc->sample_rate, + devc->ch_enabled[0] ? "on" : "off", + devc->ch_scale[0], + devc->ch_offset[0], + devc->ch_enabled[1] ? "on" : "off", + devc->ch_scale[1], + devc->ch_offset[1]); + + for(i = 0; i < HANTEK_CHANNELS; i++) { + if (devc->ch_enabled[i]) { + float_buf[i] = g_malloc( devc->num_samples * sizeof(float)); + if (!float_buf[i]) { + sr_err("Out of memory allocating sample buffer"); + return SR_ERR_MALLOC; + } + } } - float_buf = g_malloc(ns * sizeof(float)); - if (!float_buf) { - sr_err("Out of memory allocating float sample buffer."); - g_free(raw_buf); - return SR_ERR_MALLOC; + ch_bytes = HANTEK_BLOCK_SIZE / channels; + + for(sample_nr = 0; + sample_nr < devc->num_samples; + sample_nr += ch_bytes) { + + if (hantek_dso2xxx_timeout(devc) != SR_OK) + return SR_ERR; + + if (tcp_read_all(devc->sockfd, byte_buffer, HANTEK_BLOCK_SIZE) != SR_OK) { + sr_err(LOG_PREFIX "Failed to read samples."); + return SR_ERR_IO; + } + + pos = 0; + for(ch = 0; ch < HANTEK_CHANNELS; ch++) { + if (devc->ch_enabled[ch]) { + for(i = 0; i < ch_bytes; i++) { + float_buf[ch][sample_nr + i] = + (byte_buffer[pos] - devc->ch_offset[ch]) + * devc->ch_scale[ch] + / HANTEK_COUNTS_PER_DIV; + pos++; + } + } + } } /* ------------------------------------------------------------------ * @@ -253,73 +346,31 @@ SR_PRIV int hantek_dso2xxx_receive_frame(const struct sr_dev_inst *sdi) * 4. Process each enabled channel. * * ------------------------------------------------------------------ */ - /* - * Helper lambda-like macro to read and dispatch one channel. - * - * @param CH_IDX 0-based channel index into sdi->channels - * @param ENABLED hdr.ch1_enabled / hdr.ch2_enabled - * @param SCALE hdr.ch1_scale / hdr.ch2_scale - * @param OFFSET hdr.ch1_offset_raw / hdr.ch2_offset_raw - * @param LABEL string literal for log messages - */ -#define DISPATCH_CHANNEL(CH_IDX, ENABLED, SCALE, OFFSET, LABEL) \ - do { \ - struct sr_channel *ch; \ - struct sr_datafeed_packet pkt; \ - struct sr_datafeed_analog analog; \ - struct sr_analog_encoding enc; \ - struct sr_analog_meaning meaning; \ - struct sr_analog_spec spec; \ - GSList *chl = NULL; \ - \ - if (!(ENABLED)) \ - break; \ - \ - /* Read raw signed 8-bit samples from the stream. */ \ - if (tcp_read_all(devc->sockfd, raw_buf, ch_bytes) != SR_OK) { \ - sr_err("Failed to read " LABEL " samples."); \ - ret = SR_ERR_IO; \ - goto cleanup; \ - } \ - \ - /* \ - * Convert raw ADC counts to volts: \ - * voltage = (raw - offset_raw) * (scale / counts_per_div) \ - */ \ - for (i = 0; i < ns; i++) { \ - float_buf[i] = \ - ((float)raw_buf[i] - (float)(OFFSET)) * \ - ((SCALE) / (float)HANTEK_COUNTS_PER_DIV); \ - } \ - \ - /* Locate the sr_channel object for this channel index. */ \ - ch = g_slist_nth_data(sdi->channels, (CH_IDX)); \ - if (!ch || !ch->enabled) { \ - sr_dbg(LABEL " not enabled in session, skipping."); \ - break; \ - } \ - chl = g_slist_append(NULL, ch); \ - \ - sr_analog_init(&analog, &enc, &meaning, &spec, 6); \ - meaning.mq = SR_MQ_VOLTAGE; \ - meaning.unit = SR_UNIT_VOLT; \ - meaning.mqflags = SR_MQFLAG_DC; \ - meaning.channels = chl; \ - analog.data = float_buf; \ - analog.num_samples = ns; \ - \ - pkt.type = SR_DF_ANALOG; \ - pkt.payload = &analog; \ - sr_session_send(sdi, &pkt); \ - g_slist_free(chl); \ - } while (0) - - DISPATCH_CHANNEL(0, hdr.ch1_enabled, hdr.ch1_scale, - hdr.ch1_offset_raw, "CH1"); - DISPATCH_CHANNEL(1, hdr.ch2_enabled, hdr.ch2_scale, - hdr.ch2_offset_raw, "CH2"); - -#undef DISPATCH_CHANNEL + for(ch = 0; ch < HANTEK_CHANNELS; ch++) { + if (devc->ch_enabled[ch]) { + /* Locate the sr_channel object for this channel index. */ + channel = g_slist_nth_data(sdi->channels, ch); + if (!channel || !channel->enabled) { + sr_dbg(LOG_PREFIX " not enabled in session, skipping."); + continue; + } + + chl = g_slist_append(NULL, channel); + + sr_analog_init(&analog, &enc, &meaning, &spec, 6); + meaning.mq = SR_MQ_VOLTAGE; + meaning.unit = SR_UNIT_VOLT; + meaning.mqflags = SR_MQFLAG_DC; + meaning.channels = chl; + analog.data = float_buf[ch]; + analog.num_samples = devc->num_samples; + + pkt.type = SR_DF_ANALOG; + pkt.payload = &analog; + sr_session_send(sdi, &pkt); + g_slist_free(chl); + } + } /* ------------------------------------------------------------------ * * 5. Send SR_DF_FRAME_END * @@ -330,7 +381,6 @@ SR_PRIV int hantek_dso2xxx_receive_frame(const struct sr_dev_inst *sdi) ret = SR_OK; cleanup: - g_free(float_buf); - g_free(raw_buf); +// g_free(float_buf); return ret; } diff --git a/src/hardware/hantek-dso2xxx/protocol.h b/src/hardware/hantek-dso2xxx/protocol.h index 587a8214b..b42a974c0 100644 --- a/src/hardware/hantek-dso2xxx/protocol.h +++ b/src/hardware/hantek-dso2xxx/protocol.h @@ -29,53 +29,75 @@ * [frame_header] (struct hantek_frame_header, 32 bytes, little-endian) * [ch1_samples] (num_samples × int8_t, only if ch1_enabled != 0) * [ch2_samples] (num_samples × int8_t, only if ch2_enabled != 0) - * - * The header layout has been inferred from the Perl receiver script: - * - * uint32_t magic; // 0x48544B32 = "HTK2" - * uint32_t num_samples; // number of samples per channel - * float time_per_div; // seconds per division - * float sample_period; // seconds between samples (= 1/samplerate) - * uint8_t ch1_enabled; - * uint8_t ch2_enabled; - * uint8_t _reserved[2]; - * float ch1_scale; // volts per division - * float ch2_scale; - * int32_t ch1_offset_raw; // raw offset (signed ADC units) - * int32_t ch2_offset_raw; - * - * Each sample is a signed 8-bit integer in raw ADC units. - * Conversion: voltage = (raw_sample - offset_raw) * (scale / 25.0f) - * (25 ADC counts per division, 8 divisions visible → 200 count full range) */ #ifndef LIBSIGROK_HARDWARE_HANTEK_DSO2XXX_PROTOCOL_H #define LIBSIGROK_HARDWARE_HANTEK_DSO2XXX_PROTOCOL_H +#define _GNU_SOURCE #include +#include +#include +#include #include + +#include + #include #include "libsigrok-internal.h" #define LOG_PREFIX "hantek-dso2xxx" -/** Magic bytes at the start of every frame: ASCII "HTK2". */ -#define HANTEK_FRAME_MAGIC 0x32KTH /* little-endian: 'H','T','K','2' */ -#define HANTEK_FRAME_MAGIC_LE 0x32KTH +#define HANTEK_CHANNELS 4 -/* - * Use the actual 4-byte value directly so the compiler doesn't trip over - * the macro above – keep it readable via a comment. +/** + * Hantek DSO2xxx packet header. + * All fields are ASCII-encoded decimal strings (NOT null-terminated). + * Total size: 128 bytes. */ -#undef HANTEK_FRAME_MAGIC_LE -/* 'H'=0x48 'T'=0x54 'K'=0x4B '2'=0x32 → little-endian uint32 = 0x324B5448 */ -#define HANTEK_FRAME_MAGIC_LE UINT32_C(0x324B5448) +struct __attribute__((packed)) hantek_frame_header { + char magic[2]; /* data[0-1]: Packet identifier, always "#9" */ + char packet_length[9]; /* data[2-10]: Byte length of current packet */ + char total_length[9]; /* data[11-19]: Total byte length of all data */ + char uploaded_length[9]; /* data[20-28]: Byte length of uploaded data */ + char run_status; /* data[29]: Current running status */ + char trigger_status; /* data[30]: Trigger status */ + char unknown[8]; /* data[31-38]: Unknown */ + int8_t ch_offset[HANTEK_CHANNELS][2]; /* data[39-40] etc.: 16bit LE Channel offset */ + char ch_voltage[HANTEK_CHANNELS][7]; /* data[47-53] etc.: Channel voltage scale as float */ + char ch_enable[HANTEK_CHANNELS]; /* data[75-78]: Channel enable flags (CH1-CH4, one char each) */ + char sample_rate[9]; /* data[79-87]: Sampling rate */ + char sample_multiple[6]; /* data[88-93]: Sampling multiple */ + char trigger_time[9]; /* data[94-102]: Display trigger time of current frame */ + char acq_start_time[9]; /* data[103-111]: Acquisition start time point of current frame */ + char reserved[16]; /* data[112-127]: Reserved */ +}; + +/* +#9 +000004128 +000008000 +000000000 +0 +0 +\0\302\353\v \0\0\0\000 +2\0 \316\377 \0\0 \0\000 +1.0e+00 1.0e+00 1.0e+00 1.0e+00 +1100 +1.250e+06 +000001 +\0\0\0\0\0\0\0\0\0 ++0.00e+00 +000808\0\300\200\200\200\200\200\200\200\0 +*/ + +#define HANTEK_BLOCK_SIZE 4000 /** Default TCP port exposed by the patched firmware. */ -#define HANTEK_DEFAULT_PORT "5025" +#define HANTEK_DEFAULT_PORT "8001" /** Default TCP address (scope must be on the network via USB-networking). */ -#define HANTEK_DEFAULT_ADDR "192.168.7.1" +#define HANTEK_DEFAULT_ADDR "172.31.254.254" /** I/O timeout for TCP operations, in milliseconds. */ #define HANTEK_TCP_TIMEOUT_MS 60000 @@ -83,25 +105,6 @@ /** Number of ADC counts per oscilloscope division. */ #define HANTEK_COUNTS_PER_DIV 25 -#pragma pack(push, 1) -/** - * Binary frame header sent by the patched DSO firmware. - * All multi-byte fields are little-endian (the scope runs on ARM/Allwinner). - */ -struct hantek_frame_header { - uint32_t magic; /**< Must equal HANTEK_FRAME_MAGIC_LE. */ - uint32_t num_samples; /**< Samples per enabled channel. */ - float time_per_div; /**< Seconds per timebase division. */ - float sample_period; /**< Seconds between consecutive samples. */ - uint8_t ch1_enabled; /**< Non-zero if CH1 data follows. */ - uint8_t ch2_enabled; /**< Non-zero if CH2 data follows. */ - uint8_t reserved[2]; - float ch1_scale; /**< CH1 volts per division. */ - float ch2_scale; /**< CH2 volts per division. */ - int32_t ch1_offset_raw; /**< CH1 vertical offset in ADC counts. */ - int32_t ch2_offset_raw; /**< CH2 vertical offset in ADC counts. */ -}; -#pragma pack(pop) /** Per-device instance state. */ struct dev_context { @@ -116,18 +119,25 @@ struct dev_context { /* Channel metadata filled in from the most recent header */ uint32_t num_samples; - float sample_period; /**< 1 / samplerate */ - uint8_t ch1_enabled; - uint8_t ch2_enabled; - float ch1_scale; - float ch2_scale; - int32_t ch1_offset_raw; - int32_t ch2_offset_raw; + + double sample_period; /**< 1 / samplerate */ + uint32_t sample_rate; + + uint8_t ch_enabled[HANTEK_CHANNELS]; + int16_t ch_offset[HANTEK_CHANNELS]; + float ch_scale[HANTEK_CHANNELS]; }; /* Forward declarations for api.c ↔ protocol.c interface */ SR_PRIV int hantek_dso2xxx_tcp_connect(const struct sr_dev_inst *sdi); SR_PRIV void hantek_dso2xxx_tcp_close(const struct sr_dev_inst *sdi); SR_PRIV int hantek_dso2xxx_receive_frame(const struct sr_dev_inst *sdi); +SR_PRIV int hantek_dso2xxx_timesync(struct dev_context *devc); +SR_PRIV int hantek_dso2xxx_acq_now(struct dev_context *devc); +SR_PRIV int drv_init(struct sr_dev_driver *di, struct sr_context *sr_ctx); +SR_PRIV int hantek_dso2xxx_timeout(struct dev_context *devc); +SR_PRIV double atof_for_C_locale(const char *input); + +extern locale_t hantek_dso2xxx_c_locale; #endif /* LIBSIGROK_HARDWARE_HANTEK_DSO2XXX_PROTOCOL_H */