]> sigrok.org Git - libsigrok.git/commitdiff
input/saleae: introduce input module for Saleae Logic exported files
authorGerhard Sittig <redacted>
Thu, 11 Jun 2020 15:25:20 +0000 (17:25 +0200)
committerGerhard Sittig <redacted>
Fri, 24 Jul 2020 15:21:13 +0000 (17:21 +0200)
Start the implementation of an input module which covers Saleae Logic's
export files. CSV and VCD are handled by other modules, this one accepts
binary exports for Logic1 digital data (every sample, and when changed),
Logic1 analog data, Logic2 digital data, and Logic2 analog data.

The newer file format versions contain header information and can get
auto-detected, the older formats require a user spec. Some of the file
formats lack essential information in the file content, thus require
another user spec (samplerate for digital data is an example).

The .logicdata file format is unknown, and is not supported. The .sal
format could get added later, but requires local file I/O in the input
module, which current common infrastructure does not provide.

Makefile.am
src/input/input.c
src/input/saleae.c [new file with mode: 0644]

index 93b5ed714bc66e3743ad28d6f516a55a74c78e6a..6a7fa899b1d76a5b1351ac79c3a016efb7ca58b4 100644 (file)
@@ -79,6 +79,7 @@ libsigrok_la_SOURCES += \
        src/input/csv.c \
        src/input/logicport.c \
        src/input/raw_analog.c \
+       src/input/saleae.c \
        src/input/trace32_ad.c \
        src/input/vcd.c \
        src/input/wav.c \
index 48dbb157dbbf224cd562b009da85a087326d9008..f7de6c7f36a821c16b54c88b4a573c8c427e6707 100644 (file)
@@ -71,6 +71,7 @@ extern SR_PRIV struct sr_input_module input_vcd;
 extern SR_PRIV struct sr_input_module input_wav;
 extern SR_PRIV struct sr_input_module input_raw_analog;
 extern SR_PRIV struct sr_input_module input_logicport;
+extern SR_PRIV struct sr_input_module input_saleae;
 extern SR_PRIV struct sr_input_module input_null;
 /** @endcond */
 
@@ -83,6 +84,7 @@ static const struct sr_input_module *input_module_list[] = {
        &input_wav,
        &input_raw_analog,
        &input_logicport,
+       &input_saleae,
        &input_null,
        NULL,
 };
diff --git a/src/input/saleae.c b/src/input/saleae.c
new file mode 100644 (file)
index 0000000..05de3e3
--- /dev/null
@@ -0,0 +1,1202 @@
+/*
+ * This file is part of the libsigrok project.
+ *
+ * Copyright (C) 2020 Gerhard Sittig <gerhard.sittig@gmx.net>
+ *
+ * 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 2 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 <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * See the vendor's FAQ on file format details for exported files and
+ * different software versions:
+ *
+ * https://support.saleae.com/faq/technical-faq/binary-data-export-format
+ * https://support.saleae.com/faq/technical-faq/data-export-format-analog-binary
+ * https://support.saleae.com/faq/technical-faq/binary-export-format-logic-2
+ *
+ * All data is in little endian representation, floating point values
+ * in IEEE754 format. Recent versions add header information, while
+ * previous versions tend to "raw" formats. This input module is about
+ * digital and analog data in their "binary presentation". CSV and VCD
+ * exports are handled by other input modules.
+ *
+ * Saleae Logic applications typically export one file per channel. The
+ * sigrok input modules exclusively handle an individual file, existing
+ * applications may not be prepared to handle a set of files, or handle
+ * "special" file types like directories. Some of them will even actively
+ * reject such input specs. Merging multiple exported channels into either
+ * another input file or a sigrok session is supposed to be done outside
+ * of this input module. Support for ZIP archives is currently missing.
+ *
+ * TODO
+ * - Need to create a channel group in addition to channels?
+ * - Check file re-load and channel references. See bug #1241.
+ * - Fixup 'digits' use for analog data. The current implementation made
+ *   an educated guess, assuming some 12bit resolution and logic levels
+ *   which roughly results in the single digit mV range.
+ * - Add support for "local I/O" in the input module when the common
+ *   support code becomes available. The .sal save files of the Logic
+ *   application appears to be a ZIP archive with *.bin files in it
+ *   plus some meta.json dictionary. This will also introduce a new
+ *   JSON reader dependency.
+ * - When ZIP support gets added and .sal files become accepted, this
+ *   import module needs to merge the content of several per-channel
+ *   files, which may be of different types (mixed signal), and/or may
+ *   even differ in their samplerate (which becomes complex, similar to
+ *   VCD or CSV input). Given the .sal archive's layout this format may
+ *   even only become attractive when common sigrok infrastructure has
+ *   support for per-channel compression and rate(?).
+ */
+
+#include <config.h>
+#include <glib.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <libsigrok/libsigrok.h>
+#include "libsigrok-internal.h"
+
+#define LOG_PREFIX "input/saleae"
+
+/*
+ * Saleae Logic "save files" (ZIP archives with .sal file extension)
+ * could get detected, but are not yet supported. Usability would be
+ * rather limited when the current development support gets enabled.
+ * This compile time switch is strictly for internal developer use.
+ */
+#define SALEAE_WITH_SAL_SUPPORT 0
+
+#define CHUNK_SIZE  (4 * 1024 * 1024)
+
+#define LOGIC2_MAGIC "<SALEAE>"
+#define LOGIC2_VERSION 0
+#define LOGIC2_TYPE_DIGITAL 0
+#define LOGIC2_TYPE_ANALOG 1
+
+/* Simple header check approach. Assume minimum file size for all formats. */
+#define LOGIC2_MIN_SIZE 0x30
+
+enum logic_format {
+       FMT_UNKNOWN,
+       FMT_AUTO_DETECT,
+       FMT_LOGIC1_DIGITAL,
+       FMT_LOGIC1_ANALOG,
+       FMT_LOGIC2_DIGITAL,
+       FMT_LOGIC2_ANALOG,
+       FMT_LOGIC2_ARCHIVE,
+};
+
+enum input_stage {
+       STAGE_ALL_WAIT_HEADER,
+       STAGE_ALL_DETECT_TYPE,
+       STAGE_ALL_READ_HEADER,
+       STAGE_L1D_EVERY_VALUE,
+       STAGE_L1D_CHANGE_INIT,
+       STAGE_L1D_CHANGE_VALUE,
+       STAGE_L1A_NEW_CHANNEL,
+       STAGE_L1A_SAMPLE,
+       STAGE_L2D_CHANGE_VALUE,
+       STAGE_L2A_FIRST_VALUE,
+       STAGE_L2A_EVERY_VALUE,
+};
+
+struct context {
+       struct context_options {
+               enum logic_format format;
+               gboolean when_changed;
+               size_t word_size;
+               size_t channel_count;
+               uint64_t sample_rate;
+       } options;
+       struct {
+               gboolean got_header;
+               gboolean header_sent;
+               gboolean rate_sent;
+               GSList *prev_channels;
+       } module_state;
+       struct {
+               enum logic_format format;
+               gboolean when_changed;
+               size_t word_size;
+               size_t channel_count;
+               uint64_t sample_rate;
+               enum input_stage stage;
+               struct {
+                       uint64_t samples_per_channel;
+                       uint64_t current_channel_idx;
+                       uint64_t current_per_channel;
+               } l1a;
+               struct {
+                       uint32_t init_state;
+                       double begin_time;
+                       double end_time;
+                       uint64_t transition_count;
+                       double sample_period;
+                       double min_time_step;
+               } l2d;
+               struct {
+                       double begin_time;
+                       uint64_t sample_rate;
+                       uint64_t down_sample;
+                       uint64_t sample_count;
+               } l2a;
+       } logic_state;
+       struct {
+               GSList *channels;
+               gboolean is_analog;
+               size_t unit_size;
+               size_t samples_per_chunk;
+               size_t samples_in_buffer;
+               uint8_t *buffer_digital;
+               float *buffer_analog;
+               uint8_t *write_pos;
+               struct {
+                       uint64_t stamp;
+                       double time;
+                       uint32_t digital;
+                       float analog;
+               } last;
+       } feed;
+};
+
+static const char *format_texts[] = {
+       [FMT_UNKNOWN] = "unknown",
+       [FMT_AUTO_DETECT] = "auto-detect",
+       [FMT_LOGIC1_DIGITAL] = "logic1-digital",
+       [FMT_LOGIC1_ANALOG] = "logic1-analog",
+       [FMT_LOGIC2_DIGITAL] = "logic2-digital",
+       [FMT_LOGIC2_ANALOG] = "logic2-analog",
+#if SALEAE_WITH_SAL_SUPPORT
+       [FMT_LOGIC2_ARCHIVE] = "logic2-archive",
+#endif
+};
+
+static const char *get_format_text(enum logic_format fmt)
+{
+       const char *text;
+
+       if (fmt >= ARRAY_SIZE(format_texts))
+               return NULL;
+       text = format_texts[fmt];
+       if (!text || !*text)
+               return NULL;
+       return text;
+}
+
+static int create_channels(struct sr_input *in)
+{
+       struct context *inc;
+       int type;
+       size_t count, idx;
+       char name[4];
+       struct sr_channel *ch;
+
+       inc = in->priv;
+
+       if (in->sdi->channels)
+               return SR_OK;
+
+       count = inc->logic_state.channel_count;
+       switch (inc->logic_state.format) {
+       case FMT_LOGIC1_DIGITAL:
+       case FMT_LOGIC2_DIGITAL:
+               type = SR_CHANNEL_LOGIC;
+               break;
+       case FMT_LOGIC1_ANALOG:
+       case FMT_LOGIC2_ANALOG:
+               type = SR_CHANNEL_ANALOG;
+               break;
+       default:
+               return SR_ERR_NA;
+       }
+
+       /* TODO Need to create a channel group? */
+       for (idx = 0; idx < count; idx++) {
+               snprintf(name, sizeof(name), "%zu", idx);
+               ch = sr_channel_new(in->sdi, idx, type, TRUE, name);
+               if (!ch)
+                       return SR_ERR_MALLOC;
+       }
+
+       return SR_OK;
+}
+
+static int alloc_feed_buffer(struct sr_input *in)
+{
+       struct context *inc;
+       size_t alloc_size;
+
+       inc = in->priv;
+
+       inc->feed.is_analog = FALSE;
+       alloc_size = CHUNK_SIZE;
+       switch (inc->logic_state.format) {
+       case FMT_LOGIC1_DIGITAL:
+       case FMT_LOGIC2_DIGITAL:
+               inc->feed.unit_size = sizeof(inc->feed.last.digital);
+               alloc_size /= inc->feed.unit_size;
+               inc->feed.samples_per_chunk = alloc_size;
+               alloc_size *= inc->feed.unit_size;
+               inc->feed.buffer_digital = g_try_malloc(alloc_size);
+               if (!inc->feed.buffer_digital)
+                       return SR_ERR_MALLOC;
+               inc->feed.write_pos = inc->feed.buffer_digital;
+               break;
+       case FMT_LOGIC1_ANALOG:
+       case FMT_LOGIC2_ANALOG:
+               inc->feed.is_analog = TRUE;
+               alloc_size /= sizeof(inc->feed.last.analog);
+               inc->feed.samples_per_chunk = alloc_size;
+               alloc_size *= sizeof(inc->feed.last.analog);
+               inc->feed.buffer_analog = g_try_malloc(alloc_size);
+               if (!inc->feed.buffer_analog)
+                       return SR_ERR_MALLOC;
+               inc->feed.write_pos = (void *)inc->feed.buffer_analog;
+               break;
+       default:
+               return SR_ERR_NA;
+       }
+       inc->feed.samples_in_buffer = 0;
+
+       return SR_OK;
+}
+
+static int relse_feed_buffer(struct sr_input *in)
+{
+       struct context *inc;
+
+       inc = in->priv;
+
+       inc->feed.is_analog = FALSE;
+       inc->feed.unit_size = 0;
+       inc->feed.samples_per_chunk = 0;
+       inc->feed.samples_in_buffer = 0;
+       g_free(inc->feed.buffer_digital);
+       inc->feed.buffer_digital = NULL;
+       g_free(inc->feed.buffer_analog);
+       inc->feed.buffer_analog = NULL;
+       inc->feed.write_pos = NULL;
+
+       return SR_OK;
+}
+
+static int setup_feed_buffer_channel(struct sr_input *in, size_t ch_idx)
+{
+       struct context *inc;
+       struct sr_channel *ch;
+
+       inc = in->priv;
+
+       g_slist_free(inc->feed.channels);
+       inc->feed.channels = NULL;
+       if (ch_idx >= inc->logic_state.channel_count)
+               return SR_OK;
+
+       ch = g_slist_nth_data(in->sdi->channels, ch_idx);
+       if (!ch)
+               return SR_ERR_ARG;
+       inc->feed.channels = g_slist_append(NULL, ch);
+       return SR_OK;
+}
+
+static int flush_feed_buffer(struct sr_input *in)
+{
+       struct context *inc;
+       struct sr_datafeed_packet packet;
+       struct sr_datafeed_logic logic;
+       struct sr_datafeed_analog analog;
+       struct sr_analog_encoding encoding;
+       struct sr_analog_meaning meaning;
+       struct sr_analog_spec spec;
+       int rc;
+
+       inc = in->priv;
+
+       if (!inc->feed.samples_in_buffer)
+               return SR_OK;
+
+       /* Automatically send a datafeed header before meta and samples. */
+       if (!inc->module_state.header_sent) {
+               rc = std_session_send_df_header(in->sdi);
+               if (rc)
+                       return rc;
+               inc->module_state.header_sent = TRUE;
+       }
+
+       /* Automatically send the samplerate (when available). */
+       if (inc->logic_state.sample_rate && !inc->module_state.rate_sent) {
+               rc = sr_session_send_meta(in->sdi, SR_CONF_SAMPLERATE,
+                       g_variant_new_uint64(inc->logic_state.sample_rate));
+               inc->module_state.rate_sent = TRUE;
+       }
+
+       /*
+        * Create a packet with either logic or analog payload. Rewind
+        * the caller's write position.
+        */
+       memset(&packet, 0, sizeof(packet));
+       if (inc->feed.is_analog) {
+               /* TODO: Use proper 'digits' value for this input module. */
+               sr_analog_init(&analog, &encoding, &meaning, &spec, 3);
+               analog.data = inc->feed.buffer_analog;
+               analog.num_samples = inc->feed.samples_in_buffer;
+               analog.meaning->channels = inc->feed.channels;
+               analog.meaning->mq = SR_MQ_VOLTAGE;
+               analog.meaning->mqflags |= SR_MQFLAG_DC;
+               analog.meaning->unit = SR_UNIT_VOLT;
+               packet.type = SR_DF_ANALOG;
+               packet.payload = &analog;
+               inc->feed.write_pos = (void *)inc->feed.buffer_analog;
+       } else {
+               memset(&logic, 0, sizeof(logic));
+               logic.length = inc->feed.samples_in_buffer;
+               logic.length *= inc->feed.unit_size;
+               logic.unitsize = inc->feed.unit_size;
+               logic.data = inc->feed.buffer_digital;
+               packet.type = SR_DF_LOGIC;
+               packet.payload = &logic;
+               inc->feed.write_pos = inc->feed.buffer_digital;
+       }
+       inc->feed.samples_in_buffer = 0;
+
+       /* Send the packet to the session feed. */
+       return sr_session_send(in->sdi, &packet);
+}
+
+static int addto_feed_buffer_logic(struct sr_input *in,
+       uint64_t data, size_t count)
+{
+       struct context *inc;
+
+       inc = in->priv;
+
+       if (inc->feed.is_analog)
+               return SR_ERR_ARG;
+
+       while (count--) {
+               if (inc->feed.unit_size == sizeof(uint64_t))
+                       write_u64le_inc(&inc->feed.write_pos, data);
+               else if (inc->feed.unit_size == sizeof(uint32_t))
+                       write_u32le_inc(&inc->feed.write_pos, data);
+               else if (inc->feed.unit_size == sizeof(uint16_t))
+                       write_u16le_inc(&inc->feed.write_pos, data);
+               else if (inc->feed.unit_size == sizeof(uint8_t))
+                       write_u8_inc(&inc->feed.write_pos, data);
+               else
+                       return SR_ERR_BUG;
+               inc->feed.samples_in_buffer++;
+               if (inc->feed.samples_in_buffer == inc->feed.samples_per_chunk)
+                       flush_feed_buffer(in);
+       }
+
+       return SR_OK;
+}
+
+static int addto_feed_buffer_analog(struct sr_input *in,
+       float data, size_t count)
+{
+       struct context *inc;
+
+       inc = in->priv;
+
+       if (!inc->feed.is_analog)
+               return SR_ERR_ARG;
+
+       while (count--) {
+               if (sizeof(inc->feed.buffer_analog[0]) == sizeof(float))
+                       write_fltle_inc(&inc->feed.write_pos, data);
+               else if (sizeof(inc->feed.buffer_analog[0]) == sizeof(double))
+                       write_dblle_inc(&inc->feed.write_pos, data);
+               else
+                       return SR_ERR_BUG;
+               inc->feed.samples_in_buffer++;
+               if (inc->feed.samples_in_buffer == inc->feed.samples_per_chunk)
+                       flush_feed_buffer(in);
+       }
+
+       return SR_OK;
+}
+
+static enum logic_format check_format(const uint8_t *data, size_t dlen)
+{
+       const char *s;
+       uint32_t v, t;
+
+       /* TODO
+        * Can we check ZIP content here in useful ways? Probably only
+        * when the input module got extended to optionally handle local
+        * file I/O, and passes some archive handle to this routine.
+        */
+
+       /* Check for the magic literal. */
+       s = (void *)data;
+       if (dlen < strlen(LOGIC2_MAGIC))
+               return FMT_UNKNOWN;
+       if (strncmp(s, LOGIC2_MAGIC, strlen(LOGIC2_MAGIC)) != 0)
+               return FMT_UNKNOWN;
+       data += strlen(LOGIC2_MAGIC);
+       dlen -= strlen(LOGIC2_MAGIC);
+
+       /* Get the version and type fields. */
+       if (dlen < 2 * sizeof(uint32_t))
+               return FMT_UNKNOWN;
+       v = read_u32le_inc(&data);
+       t = read_u32le_inc(&data);
+       if (v != LOGIC2_VERSION)
+               return FMT_UNKNOWN;
+       switch (t) {
+       case LOGIC2_TYPE_DIGITAL:
+               return FMT_LOGIC2_DIGITAL;
+       case LOGIC2_TYPE_ANALOG:
+               return FMT_LOGIC2_ANALOG;
+       default:
+               return FMT_UNKNOWN;
+       }
+
+       return FMT_UNKNOWN;
+}
+
+/* Check for availability of required header data. */
+static gboolean have_header(struct context *inc, GString *buf)
+{
+
+       /*
+        * The amount of required data depends on the file format. Which
+        * either was specified before, or is yet to get determined. The
+        * input module ideally would apply a sequence of checks for the
+        * currently available (partial) data, access a few first header
+        * fields, before checking for a little more receive data, before
+        * accessing more fields, until the input file's type was found,
+        * and its header length is known, and can get checked.
+        *
+        * This simple implementation just assumes that any input file
+        * has at least a given number of bytes, which should not be an
+        * issue for typical use cases. Only extremely short yet valid
+        * input files with just a few individual samples may fail this
+        * check. It's assumed that these files are very rare, and may
+        * be of types which are covered by other input modules (raw
+        * binary).
+        */
+       (void)inc;
+       return buf->len >= LOGIC2_MIN_SIZE;
+}
+
+/* Process/inspect previously received input data. Get header parameters. */
+static int parse_header(struct sr_input *in)
+{
+       struct context *inc;
+       const uint8_t *read_pos, *start_pos;
+       size_t read_len, want_len;
+       uint64_t samples_per_channel;
+       size_t channel_count;
+       double sample_period;
+       uint64_t sample_rate;
+
+       inc = in->priv;
+       read_pos = (const uint8_t *)in->buf->str;
+       read_len = in->buf->len;
+
+       /*
+        * Clear internal state. Normalize user specified option values
+        * before amending them from the input file's header information.
+        */
+       memset(&inc->logic_state, 0, sizeof(inc->logic_state));
+       inc->logic_state.format = inc->options.format;
+       inc->logic_state.when_changed = inc->options.when_changed;
+       inc->logic_state.word_size = inc->options.word_size;
+       if (!inc->logic_state.word_size) {
+               sr_err("Need a word size.");
+               return SR_ERR_ARG;
+       }
+       inc->logic_state.word_size += 8 - 1;
+       inc->logic_state.word_size /= 8; /* Sample width in bytes. */
+       if (inc->logic_state.word_size > sizeof(inc->feed.last.digital)) {
+               sr_err("Excessive word size %zu.", inc->logic_state.word_size);
+               return SR_ERR_ARG;
+       }
+       inc->logic_state.channel_count = inc->options.channel_count;
+       inc->logic_state.sample_rate = inc->options.sample_rate;
+       if (inc->logic_state.format == FMT_AUTO_DETECT)
+               inc->logic_state.stage = STAGE_ALL_DETECT_TYPE;
+       else
+               inc->logic_state.stage = STAGE_ALL_READ_HEADER;
+
+       /*
+        * Optionally auto-detect the format if none was specified yet.
+        * This only works for some of the supported formats. ZIP support
+        * requires local I/O in the input module (won't work on memory
+        * buffers).
+        */
+       if (inc->logic_state.stage == STAGE_ALL_DETECT_TYPE) {
+               inc->logic_state.format = check_format(read_pos, read_len);
+               if (inc->logic_state.format == FMT_UNKNOWN) {
+                       sr_err("Unknown or unsupported file format.");
+                       return SR_ERR_DATA;
+               }
+               sr_info("Detected file format: '%s'.",
+                       get_format_text(inc->logic_state.format));
+               inc->logic_state.stage = STAGE_ALL_READ_HEADER;
+       }
+
+       /*
+        * Read the header fields, depending on the specific file format.
+        * Arrange for the subsequent inspection of sample data items.
+        */
+       start_pos = read_pos;
+       switch (inc->logic_state.format) {
+       case FMT_LOGIC1_DIGITAL:
+               channel_count = inc->logic_state.channel_count;
+               if (!channel_count) {
+                       channel_count = inc->logic_state.word_size;
+                       channel_count *= 8;
+                       inc->logic_state.channel_count = channel_count;
+               }
+               /* EMPTY */ /* No header fields to read here. */
+               sr_dbg("L1D, empty header, changed %d.",
+                       inc->logic_state.when_changed ? 1 : 0);
+               if (inc->logic_state.when_changed)
+                       inc->logic_state.stage = STAGE_L1D_CHANGE_INIT;
+               else
+                       inc->logic_state.stage = STAGE_L1D_EVERY_VALUE;
+               break;
+       case FMT_LOGIC1_ANALOG:
+               want_len = sizeof(uint64_t) + sizeof(uint32_t) + sizeof(double);
+               if (read_len < want_len)
+                       return SR_ERR_DATA;
+               samples_per_channel = read_u64le_inc(&read_pos);
+               channel_count = read_u32le_inc(&read_pos);
+               sample_period = read_dblle_inc(&read_pos);
+               inc->logic_state.l1a.samples_per_channel = samples_per_channel;
+               inc->logic_state.channel_count = channel_count;
+               sample_rate = 0;
+               if (sample_period) {
+                       sample_period = 1.0 / sample_period;
+                       sample_period += 0.5;
+                       sample_rate = (uint64_t)sample_period;
+                       inc->logic_state.sample_rate = sample_rate;
+               }
+               sr_dbg("L1A header, smpls %zu, chans %zu, per %lf, rate %zu.",
+                       (size_t)samples_per_channel, (size_t)channel_count,
+                       sample_period, (size_t)sample_rate);
+               inc->logic_state.stage = STAGE_L1A_NEW_CHANNEL;
+               inc->logic_state.l1a.current_channel_idx = 0;
+               inc->logic_state.l1a.current_per_channel = 0;
+               break;
+       case FMT_LOGIC2_DIGITAL:
+               inc->logic_state.channel_count = 1;
+               want_len = sizeof(uint64_t); /* magic */
+               want_len += 2 * sizeof(uint32_t); /* version, type */
+               want_len += sizeof(uint32_t); /* initial state */
+               want_len += 2 * sizeof(double); /* begin time, end time */
+               want_len += sizeof(uint64_t); /* transition count */
+               if (read_len < want_len)
+                       return SR_ERR_DATA;
+               if (check_format(read_pos, read_len) != FMT_LOGIC2_DIGITAL)
+                       return SR_ERR_DATA;
+               (void)read_u64le_inc(&read_pos);
+               (void)read_u32le_inc(&read_pos);
+               (void)read_u32le_inc(&read_pos);
+               inc->logic_state.l2d.init_state = read_u32le_inc(&read_pos);
+               inc->logic_state.l2d.begin_time = read_dblle_inc(&read_pos);
+               inc->logic_state.l2d.end_time = read_dblle_inc(&read_pos);
+               inc->logic_state.l2d.transition_count = read_u64le_inc(&read_pos);
+               sr_dbg("L2D header, init %u, begin %lf, end %lf, transitions %" PRIu64 ".",
+                       (unsigned)inc->logic_state.l2d.init_state,
+                       inc->logic_state.l2d.begin_time,
+                       inc->logic_state.l2d.end_time,
+                       inc->logic_state.l2d.transition_count);
+               if (!inc->logic_state.sample_rate) {
+                       sr_err("Need a samplerate.");
+                       return SR_ERR_ARG;
+               }
+               inc->feed.last.time = inc->logic_state.l2d.begin_time;
+               inc->feed.last.digital = inc->logic_state.l2d.init_state ? 1 : 0;
+               inc->logic_state.l2d.sample_period = inc->logic_state.sample_rate;
+               inc->logic_state.l2d.sample_period = 1.0 / inc->logic_state.l2d.sample_period;
+               inc->logic_state.l2d.min_time_step = inc->logic_state.l2d.end_time;
+               inc->logic_state.l2d.min_time_step -= inc->logic_state.l2d.begin_time;
+               inc->logic_state.stage = STAGE_L2D_CHANGE_VALUE;
+               break;
+       case FMT_LOGIC2_ANALOG:
+               inc->logic_state.channel_count = 1;
+               want_len = sizeof(uint64_t); /* magic */
+               want_len += 2 * sizeof(uint32_t); /* version, type */
+               want_len += sizeof(double); /* begin time */
+               want_len += 2 * sizeof(uint64_t); /* sample rate, down sample */
+               want_len += sizeof(uint64_t); /* sample count */
+               if (read_len < want_len)
+                       return SR_ERR_DATA;
+               if (check_format(read_pos, read_len) != FMT_LOGIC2_ANALOG)
+                       return SR_ERR_DATA;
+               (void)read_u64le_inc(&read_pos);
+               (void)read_u32le_inc(&read_pos);
+               (void)read_u32le_inc(&read_pos);
+               inc->logic_state.l2a.begin_time = read_dblle_inc(&read_pos);
+               inc->logic_state.l2a.sample_rate = read_u64le_inc(&read_pos);
+               inc->logic_state.l2a.down_sample = read_u64le_inc(&read_pos);
+               inc->logic_state.l2a.sample_count = read_u64le_inc(&read_pos);
+               if (!inc->logic_state.sample_rate)
+                       inc->logic_state.sample_rate = inc->logic_state.l2a.sample_rate;
+               sr_dbg("L2A header, begin %lf, rate %" PRIu64 ", down %" PRIu64 ", samples %" PRIu64 ".",
+                       inc->logic_state.l2a.begin_time,
+                       inc->logic_state.l2a.sample_rate,
+                       inc->logic_state.l2a.down_sample,
+                       inc->logic_state.l2a.sample_count);
+               inc->feed.last.time = inc->logic_state.l2a.begin_time;
+               inc->logic_state.stage = STAGE_L2A_FIRST_VALUE;
+               break;
+       case FMT_LOGIC2_ARCHIVE:
+               sr_err("Support for .sal archives not implemented yet.");
+               return SR_ERR_NA;
+       default:
+               sr_err("Unknown or unsupported file format.");
+               return SR_ERR_NA;
+       }
+
+       /* Remove the consumed header fields from the receive buffer. */
+       read_len = read_pos - start_pos;
+       g_string_erase(in->buf, 0, read_len);
+
+       return SR_OK;
+}
+
+/* Check availablity of the next sample data item. */
+static gboolean have_next_item(struct sr_input *in,
+       const uint8_t *buff, size_t blen,
+       const uint8_t **curr, const uint8_t **next)
+{
+       struct context *inc;
+       size_t want_len;
+       const uint8_t *pos;
+
+       inc = in->priv;
+       if (curr)
+               *curr = NULL;
+       if (next)
+               *next = NULL;
+
+       /*
+        * The amount of required data depends on the file format and
+        * the current state. Wait for the availabilty of the desired
+        * data before processing it (to simplify data inspection
+        * code paths).
+        */
+       switch (inc->logic_state.stage) {
+       case STAGE_L1D_EVERY_VALUE:
+               want_len = inc->logic_state.word_size;
+               break;
+       case STAGE_L1D_CHANGE_INIT:
+       case STAGE_L1D_CHANGE_VALUE:
+               want_len = sizeof(uint64_t);
+               want_len += inc->logic_state.word_size;
+               break;
+       case STAGE_L1A_NEW_CHANNEL:
+               want_len = 0;
+               break;
+       case STAGE_L1A_SAMPLE:
+               want_len = sizeof(float);
+               break;
+       case STAGE_L2D_CHANGE_VALUE:
+               want_len = sizeof(double);
+               break;
+       case STAGE_L2A_FIRST_VALUE:
+       case STAGE_L2A_EVERY_VALUE:
+               want_len = sizeof(float);
+               break;
+       default:
+               return FALSE;
+       }
+       if (blen < want_len)
+               return FALSE;
+
+       /* Provide references to the next item, and the position after it. */
+       pos = buff;
+       if (curr)
+               *curr = pos;
+       pos += want_len;
+       if (next)
+               *next = pos;
+       return TRUE;
+}
+
+/* Process the next sample data item after it became available. */
+static int parse_next_item(struct sr_input *in,
+       const uint8_t *curr, size_t len)
+{
+       struct context *inc;
+       uint64_t next_stamp, count;
+       uint64_t digital;
+       float analog;
+       double next_time, diff_time;
+       int rc;
+
+       inc = in->priv;
+       (void)len;
+
+       /*
+        * The specific item to get processed next depends on the file
+        * format and current state.
+        */
+       switch (inc->logic_state.stage) {
+       case STAGE_L1D_CHANGE_INIT:
+       case STAGE_L1D_CHANGE_VALUE:
+               next_stamp = read_u64le_inc(&curr);
+               if (inc->logic_state.stage == STAGE_L1D_CHANGE_INIT) {
+                       inc->feed.last.stamp = next_stamp;
+                       inc->logic_state.stage = STAGE_L1D_CHANGE_VALUE;
+               }
+               count = next_stamp - inc->feed.last.stamp;
+               digital = inc->feed.last.digital;
+               rc = addto_feed_buffer_logic(in, digital, count);
+               if (rc)
+                       return rc;
+               inc->feed.last.stamp = next_stamp - 1;
+               /* FALLTHROUGH */
+       case STAGE_L1D_EVERY_VALUE:
+               if (inc->logic_state.word_size == sizeof(uint8_t)) {
+                       digital = read_u8_inc(&curr);
+               } else if (inc->logic_state.word_size == sizeof(uint16_t)) {
+                       digital = read_u16le_inc(&curr);
+               } else if (inc->logic_state.word_size == sizeof(uint32_t)) {
+                       digital = read_u32le_inc(&curr);
+               } else if (inc->logic_state.word_size == sizeof(uint64_t)) {
+                       digital = read_u64le_inc(&curr);
+               } else {
+                       /*
+                        * In theory the sigrok input module could support
+                        * arbitrary word sizes, but the Saleae exporter
+                        * only provides the 8/16/32/64 choices anyway.
+                        */
+                       sr_err("Unsupported word size %zu.", inc->logic_state.word_size);
+                       return SR_ERR_ARG;
+               }
+               rc = addto_feed_buffer_logic(in, digital, 1);
+               if (rc)
+                       return rc;
+               inc->feed.last.digital = digital;
+               inc->feed.last.stamp++;
+               return SR_OK;
+       case STAGE_L1A_NEW_CHANNEL:
+               /* Just select the channel. Don't consume any data. */
+               rc = setup_feed_buffer_channel(in, inc->logic_state.l1a.current_channel_idx);
+               if (rc)
+                       return rc;
+               inc->logic_state.l1a.current_channel_idx++;
+               inc->logic_state.l1a.current_per_channel = 0;
+               inc->logic_state.stage = STAGE_L1A_SAMPLE;
+               return SR_OK;
+       case STAGE_L1A_SAMPLE:
+               analog = read_fltle_inc(&curr);
+               rc = addto_feed_buffer_analog(in, analog, 1);
+               if (rc)
+                       return rc;
+               inc->logic_state.l1a.current_per_channel++;
+               if (inc->logic_state.l1a.current_channel_idx == inc->logic_state.l1a.samples_per_channel)
+                       inc->logic_state.stage = STAGE_L1A_NEW_CHANNEL;
+               return SR_OK;
+       case STAGE_L2D_CHANGE_VALUE:
+               next_time = read_dblle_inc(&curr);
+               diff_time = next_time - inc->feed.last.time;
+               if (inc->logic_state.l2d.min_time_step > diff_time)
+                       inc->logic_state.l2d.min_time_step = diff_time;
+               diff_time /= inc->logic_state.l2d.sample_period;
+               diff_time += 0.5;
+               count = (uint64_t)diff_time;
+               digital = inc->feed.last.digital;
+               rc = addto_feed_buffer_logic(in, digital, count);
+               if (rc)
+                       return rc;
+               inc->feed.last.time = next_time;
+               inc->feed.last.digital = 1 - inc->feed.last.digital;
+               return SR_OK;
+       case STAGE_L2A_FIRST_VALUE:
+       case STAGE_L2A_EVERY_VALUE:
+               analog = read_fltle_inc(&curr);
+               if (inc->logic_state.stage == STAGE_L2A_FIRST_VALUE) {
+                       rc = setup_feed_buffer_channel(in, 0);
+                       if (rc)
+                               return rc;
+                       count = 1;
+               } else {
+                       count = inc->logic_state.l2a.down_sample;
+               }
+               rc = addto_feed_buffer_analog(in, analog, 1);
+               if (rc)
+                       return rc;
+               return SR_OK;
+
+       default:
+               (void)analog;
+               return SR_ERR_NA;
+       }
+       /* UNREACH */
+}
+
+static int parse_samples(struct sr_input *in)
+{
+       const uint8_t *buff, *start;
+       size_t blen;
+
+       const uint8_t *curr, *next;
+       size_t len;
+       int rc;
+
+       start = (const uint8_t *)in->buf->str;
+       buff = start;
+       blen = in->buf->len;
+       while (have_next_item(in, buff, blen, &curr, &next)) {
+               len = next - curr;
+               rc = parse_next_item(in, curr, len);
+               if (rc)
+                       return rc;
+               buff += len;
+               blen -= len;
+       }
+       len = buff - start;
+       g_string_erase(in->buf, 0, len);
+
+       return SR_OK;
+}
+
+/*
+ * Try to auto detect an input's file format. Mismatch is non-fatal.
+ * Silent operation by design. Not all details need to be available.
+ * Get the strongest possible match in a best-effort manner.
+ *
+ * TODO Extend the .sal check when local file I/O becomes available.
+ * File extensions can lie, and need not be available. Check for a
+ * ZIP archive and the meta.json member in it.
+ */
+static int format_match(GHashTable *metadata, unsigned int *confidence)
+{
+       static const char *zip_ext = ".sal";
+       static const char *bin_ext = ".bin";
+
+       const char *fn;
+       size_t fn_len, ext_len;
+       const char *ext_pos;
+       GString *buf;
+
+       /* Weak match on the filename (when available). */
+       fn = g_hash_table_lookup(metadata, GINT_TO_POINTER(SR_INPUT_META_FILENAME));
+       if (fn && *fn) {
+               fn_len = strlen(fn);
+               ext_len = strlen(zip_ext);
+               ext_pos = &fn[fn_len - ext_len];
+               if (fn_len >= ext_len && g_ascii_strcasecmp(ext_pos, zip_ext) == 0) {
+                       if (SALEAE_WITH_SAL_SUPPORT)
+                               *confidence = 10;
+               }
+               ext_len = strlen(bin_ext);
+               ext_pos = &fn[fn_len - ext_len];
+               if (fn_len >= ext_len && g_ascii_strcasecmp(ext_pos, bin_ext) == 0) {
+                       *confidence = 50;
+               }
+       }
+
+       /* Stronger match when magic literals are found in file content. */
+       buf = g_hash_table_lookup(metadata, GINT_TO_POINTER(SR_INPUT_META_HEADER));
+       if (!buf || !buf->len || !buf->str)
+               return SR_ERR_ARG;
+       switch (check_format((const uint8_t *)buf->str, buf->len)) {
+       case FMT_LOGIC2_DIGITAL:
+       case FMT_LOGIC2_ANALOG:
+               *confidence = 1;
+               break;
+       default:
+               /* EMPTY */
+               break;
+       }
+
+       return SR_OK;
+}
+
+static int init(struct sr_input *in, GHashTable *options)
+{
+       struct context *inc;
+       const char *type, *fmt_text;
+       enum logic_format format, fmt_idx;
+       gboolean changed;
+       size_t size, count;
+       uint64_t rate;
+
+       /* Allocate resources. */
+       in->sdi = g_malloc0(sizeof(*in->sdi));
+       inc = g_malloc0(sizeof(*inc));
+       in->priv = inc;
+
+       /* Get caller provided specs, dump before check. */
+       type = g_variant_get_string(g_hash_table_lookup(options, "format"), NULL);
+       changed = g_variant_get_boolean(g_hash_table_lookup(options, "changed"));
+       size = g_variant_get_uint32(g_hash_table_lookup(options, "wordsize"));
+       count = g_variant_get_uint32(g_hash_table_lookup(options, "logic_channels"));
+       rate = g_variant_get_uint64(g_hash_table_lookup(options, "samplerate"));
+       sr_dbg("Caller options: type '%s', changed %d, wordsize %zu, channels %zu, rate %" PRIu64 ".",
+               type, changed ? 1 : 0, size, count, rate);
+
+       /* Run a few simple checks. Normalization is done in .init(). */
+       format = FMT_UNKNOWN;
+       for (fmt_idx = FMT_AUTO_DETECT; fmt_idx < ARRAY_SIZE(format_texts); fmt_idx++) {
+               fmt_text = format_texts[fmt_idx];
+               if (!fmt_text || !*fmt_text)
+                       continue;
+               if (g_ascii_strcasecmp(type, fmt_text) != 0)
+                       continue;
+               format = fmt_idx;
+               break;
+       }
+       if (format == FMT_UNKNOWN) {
+               sr_err("Unknown file type name: '%s'.", type);
+               return SR_ERR_ARG;
+       }
+       if (!size) {
+               sr_err("Need a word size.");
+               return SR_ERR_ARG;
+       }
+
+       /*
+        * Keep input specs around. We never get back to .init() even
+        * when input files are re-read later.
+        */
+       inc->options.format = format;
+       inc->options.when_changed = !!changed;
+       inc->options.word_size = size;
+       inc->options.channel_count = count;
+       inc->options.sample_rate = rate;
+       sr_dbg("Resulting options: type '%s', changed %d",
+               get_format_text(format), changed ? 1 : 0);
+
+       return SR_OK;
+}
+
+static int receive(struct sr_input *in, GString *buf)
+{
+       struct context *inc;
+       int rc;
+       const char *text;
+
+       inc = in->priv;
+
+       /* Accumulate another chunk of input data. */
+       g_string_append_len(in->buf, buf->str, buf->len);
+
+       /*
+        * Wait for the full header's availability, then process it in
+        * a single call, and set the "ready" flag. Make sure sample data
+        * and the header get processed in disjoint receive() calls, the
+        * backend requires those separate phases.
+        */
+       if (!inc->module_state.got_header) {
+               if (!have_header(inc, in->buf))
+                       return SR_OK;
+               rc = parse_header(in);
+               if (rc)
+                       return rc;
+               inc->module_state.got_header = TRUE;
+               text = get_format_text(inc->logic_state.format) ? : "<unknown>";
+               sr_info("Using file format: '%s'.", text);
+               rc = create_channels(in);
+               if (rc)
+                       return rc;
+               rc = alloc_feed_buffer(in);
+               if (rc)
+                       return rc;
+               in->sdi_ready = TRUE;
+               return SR_OK;
+       }
+
+       /* Process sample data, after the header got processed. */
+       return parse_samples(in);
+}
+
+static int end(struct sr_input *in)
+{
+       struct context *inc;
+       int rc;
+
+       /* Nothing to do here if we never started feeding the session. */
+       if (!in->sdi_ready)
+               return SR_OK;
+
+       /*
+        * Process input data which may not have been inspected before.
+        * Flush any potentially queued samples.
+        */
+       rc = parse_samples(in);
+       if (rc)
+               return rc;
+       rc = flush_feed_buffer(in);
+       if (rc)
+               return rc;
+
+       /* End the session feed if one was started. */
+       inc = in->priv;
+       if (inc->module_state.header_sent) {
+               rc = std_session_send_df_end(in->sdi);
+               if (rc)
+                       return rc;
+               inc->module_state.header_sent = FALSE;
+       }
+
+       /* Input data shall be exhausted by now. Non-fatal condition. */
+       if (in->buf->len)
+               sr_warn("Unprocessed remaining input: %zu bytes.", in->buf->len);
+
+       return SR_OK;
+}
+
+static void cleanup(struct sr_input *in)
+{
+       struct context *inc;
+       struct context_options save_opts;
+
+       if (!in)
+               return;
+       inc = in->priv;
+       if (!inc)
+               return;
+
+       /* Keep references to previously created channels. */
+       g_slist_free_full(inc->module_state.prev_channels, sr_channel_free_cb);
+       inc->module_state.prev_channels = in->sdi->channels;
+       in->sdi->channels = NULL;
+
+       /* Release dynamically allocated resources. */
+       relse_feed_buffer(in);
+
+       /* Clear internal state, but keep what .init() has provided. */
+       save_opts = inc->options;
+       memset(inc, 0, sizeof(*inc));
+       inc->options = save_opts;
+}
+
+static int reset(struct sr_input *in)
+{
+       struct context *inc;
+
+       inc = in->priv;
+
+       /*
+        * The input module's .reset() routine clears the 'inc' context.
+        * But 'in' is kept which contains channel groups which reference
+        * channels. We cannot re-create the channels, since applications
+        * still reference them and expect us to keep them. The .cleanup()
+        * routine also keeps the user specified option values, the module
+        * will derive internal state again when the input gets re-read.
+        */
+       cleanup(in);
+       in->sdi->channels = inc->module_state.prev_channels;
+
+       inc->module_state.got_header = FALSE;
+       inc->module_state.header_sent = FALSE;
+       inc->module_state.rate_sent = FALSE;
+       g_string_truncate(in->buf, 0);
+
+       return SR_OK;
+}
+
+enum option_index {
+       OPT_FMT_TYPE,
+       OPT_CHANGE,
+       OPT_WORD_SIZE,
+       OPT_NUM_LOGIC,
+       OPT_SAMPLERATE,
+       OPT_MAX,
+};
+
+static struct sr_option options[] = {
+       [OPT_FMT_TYPE] = {
+               "format", "File format.",
+               "Type of input file format. Not all types can get auto-detected.",
+               NULL, NULL,
+       },
+       [OPT_CHANGE] = {
+               "changed", "Save when changed.",
+               "Sample value was saved when changed (in contrast to: every sample).",
+               NULL, NULL,
+       },
+       [OPT_WORD_SIZE] = {
+               "wordsize", "Word size.",
+               "The number of bits per set of samples for digital data.",
+               NULL, NULL,
+       },
+       [OPT_NUM_LOGIC] = {
+               "logic_channels", "Channel count.",
+               "The number of digital channels. Word size is used when not specified.",
+               NULL, NULL,
+       },
+       [OPT_SAMPLERATE] = {
+               "samplerate", "Samplerate.",
+               "The samplerate. Needed when the file content lacks this information.",
+               NULL, NULL,
+       },
+       [OPT_MAX] = ALL_ZERO,
+};
+
+static const struct sr_option *get_options(void)
+{
+       enum logic_format fmt_idx;
+       const char *fmt_text;
+       size_t word_size;
+       GSList *l;
+
+       /* Been here before? Already assigned default values? */
+       if (options[0].def)
+               return options;
+
+       /* Assign default values, and list choices to select from. */
+       fmt_text = format_texts[FMT_AUTO_DETECT];
+       options[OPT_FMT_TYPE].def = g_variant_ref_sink(g_variant_new_string(fmt_text));
+       l = NULL;
+       for (fmt_idx = FMT_AUTO_DETECT; fmt_idx < ARRAY_SIZE(format_texts); fmt_idx++) {
+               fmt_text = format_texts[fmt_idx];
+               if (!fmt_text || !*fmt_text)
+                       continue;
+               l = g_slist_append(l, g_variant_ref_sink(g_variant_new_string(fmt_text)));
+       }
+       options[OPT_FMT_TYPE].values = l;
+       options[OPT_CHANGE].def = g_variant_ref_sink(g_variant_new_boolean(FALSE));
+       options[OPT_WORD_SIZE].def = g_variant_ref_sink(g_variant_new_uint32(8));
+       l = NULL;
+       for (word_size = sizeof(uint8_t); word_size <= sizeof(uint64_t); word_size *= 2)
+               l = g_slist_append(l, g_variant_ref_sink(g_variant_new_uint32(8 * word_size)));
+       options[OPT_WORD_SIZE].values = l;
+       options[OPT_NUM_LOGIC].def = g_variant_ref_sink(g_variant_new_uint32(0));
+       options[OPT_SAMPLERATE].def = g_variant_ref_sink(g_variant_new_uint64(0));
+
+       return options;
+}
+
+SR_PRIV struct sr_input_module input_saleae = {
+       .id = "saleae",
+       .name = "Saleae",
+#if SALEAE_WITH_SAL_SUPPORT
+       .desc = "Saleae Logic software export/save files",
+       .exts = (const char *[]){"bin", "sal", NULL},
+#else
+       .desc = "Saleae Logic software export files",
+       .exts = (const char *[]){"bin", NULL},
+#endif
+       .metadata = {
+               SR_INPUT_META_FILENAME,
+               SR_INPUT_META_HEADER | SR_INPUT_META_REQUIRED
+       },
+       .options = get_options,
+       .format_match = format_match,
+       .init = init,
+       .receive = receive,
+       .end = end,
+       .cleanup = cleanup,
+       .reset = reset,
+};