X-Git-Url: http://sigrok.org/gitweb/?a=blobdiff_plain;f=src%2Finput%2Fsaleae.c;fp=src%2Finput%2Fsaleae.c;h=05de3e3ea5eddae271e6f86213392f5f79db9de9;hb=d891892dc05df8ce33fcadf4a123a83e17b5d620;hp=0000000000000000000000000000000000000000;hpb=daa895cba3836f3bd5e02b845f85e23854dff820;p=libsigrok.git diff --git a/src/input/saleae.c b/src/input/saleae.c new file mode 100644 index 00000000..05de3e3e --- /dev/null +++ b/src/input/saleae.c @@ -0,0 +1,1202 @@ +/* + * This file is part of the libsigrok project. + * + * Copyright (C) 2020 Gerhard Sittig + * + * 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 . + */ + +/* + * 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 +#include +#include +#include +#include +#include +#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 "" +#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) ? : ""; + 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, +};