]> sigrok.org Git - libsigrok.git/commitdiff
input/csv: introduce generic "column processing" support
authorGerhard Sittig <redacted>
Mon, 14 Oct 2019 21:43:11 +0000 (23:43 +0200)
committerGerhard Sittig <redacted>
Sat, 21 Dec 2019 17:20:04 +0000 (18:20 +0100)
Rephrase the CSV input module's implementation such that generic support
to "process a column" becomes available. All columns of an input file's
text line get inspected, a column can either get ignored, or converted
to logic data. A future version can then remove the current limitations
of single- and multi-column modes (either one single multi-bit cell, or
multiple single-bit cells which must be adjacent).

Combine the bin/oct/hex parse routines into one routine which handles up
to four bits per input number digit with common logic. Availability of
more data than channels (according to user specs) is not fatal.

Drop the counter intuitive "first-channel" option, use "first-column"
instead. Warn when comment leader and column separator are identical
(was silent before, may be unexpected). Extend diagnostics and address
minor readability nits, update comments. Rephrase logic channel name
assignment.

Use simple scalar options to derive generic processing details: Either
'single-column' and 'numchannels' are required, with an optional
'format' spec (resulting in single-column mode). Or 'first-column' with
an optional 'numchannels' (multi-column mode with fixed format, using
all available columns by default). The default is multi-column mode with
one logic channel per column and spanning all columns on a text line.

src/input/csv.c

index 546d0f3c0080f6bc65018420b92c28272f4fad25..0833f3182dad07658380c0a9923ecae667e198c5 100644 (file)
@@ -2,6 +2,7 @@
  * This file is part of the libsigrok project.
  *
  * Copyright (C) 2013 Marc Schink <sigrok-dev@marcschink.de>
+ * Copyright (C) 2019 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
 
 /* Single column formats. */
 enum single_col_format {
-       FORMAT_BIN,
-       FORMAT_HEX,
-       FORMAT_OCT,
+       FORMAT_NONE,    /* Ignore this column. */
+       FORMAT_BIN,     /* Bin digits for a set of bits (or just one bit). */
+       FORMAT_HEX,     /* Hex digits for a set of bits. */
+       FORMAT_OCT,     /* Oct digits for a set of bits. */
+};
+
+static const char *col_format_text[] = {
+       [FORMAT_NONE] = "unknown",
+       [FORMAT_BIN] = "binary",
+       [FORMAT_HEX] = "hexadecimal",
+       [FORMAT_OCT] = "octal",
+};
+
+struct column_details {
+       size_t col_nr;
+       enum single_col_format text_format;
+       size_t channel_offset;
+       size_t channel_count;
 };
 
 struct context {
@@ -133,22 +149,16 @@ struct context {
        /* Determines if sample data is stored in multiple columns. */
        gboolean multi_column_mode;
 
+       /* Parameters how to process the columns. */
+       size_t column_want_count;
+       struct column_details *column_details;
+
        /* Column number of the sample data in single column mode. */
        size_t single_column;
 
-       /*
-        * Number of the first column to parse. Equivalent to the number of the
-        * first channel in multi column mode and the single column number in
-        * single column mode.
-        */
+       /* Column number of the first channel. */
        size_t first_column;
 
-       /*
-        * Column number of the first channel in multi column mode and position of
-        * the bit for the first channel in single column mode.
-        */
-       size_t first_channel;
-
        /* Line number to start processing. */
        size_t start_line;
 
@@ -269,6 +279,77 @@ static int queue_logic_samples(const struct sr_input *in)
        return SR_OK;
 }
 
+static int make_column_details_single(struct context *inc,
+       size_t col_nr, size_t bit_count, enum single_col_format format)
+{
+       struct column_details *details;
+
+       /*
+        * Need at least as many columns to also include the one with
+        * the single-column input data.
+        */
+       inc->column_want_count = col_nr;
+
+       /*
+        * Allocate the columns' processing details. Columns are counted
+        * from 1 (user's perspective), array items from 0 (programmer's
+        * perspective).
+        */
+       inc->column_details = g_malloc0_n(col_nr, sizeof(inc->column_details[0]));
+       details = &inc->column_details[col_nr - 1];
+       details->col_nr = col_nr;
+
+       /*
+        * In single-column mode this single column will hold all bits
+        * of all logic channels, in the user specified number format.
+        */
+       details->text_format = format;
+       details->channel_offset = 0;
+       details->channel_count = bit_count;
+
+       return SR_OK;
+}
+
+static int make_column_details_multi(struct context *inc,
+       size_t first_col, size_t last_col)
+{
+       struct column_details *details;
+       size_t col_nr;
+
+       /*
+        * Need at least as many columns to also include the one with
+        * the last channel's data.
+        */
+       inc->column_want_count = last_col;
+
+       /*
+        * Allocate the columns' processing details. Columns are counted
+        * from 1, array items from 0.
+        * In multi-column mode each column will hold a single bit for
+        * the respective channel.
+        */
+       inc->column_details = g_malloc0_n(last_col, sizeof(inc->column_details[0]));
+       for (col_nr = first_col; col_nr <= last_col; col_nr++) {
+               details = &inc->column_details[col_nr - 1];
+               details->col_nr = col_nr;
+               details->text_format = FORMAT_BIN;
+               details->channel_offset = col_nr - first_col;
+               details->channel_count = 1;
+       }
+
+
+       return SR_OK;
+}
+
+static const struct column_details *lookup_column_details(struct context *inc, size_t nr)
+{
+       if (!inc || !inc->column_details)
+               return NULL;
+       if (!nr || nr > inc->column_want_count)
+               return NULL;
+       return &inc->column_details[nr - 1];
+}
+
 /*
  * Primitive operations for text input: Strip comments off text lines.
  * Split text lines into columns. Process input text for individual
@@ -288,244 +369,137 @@ static void strip_comment(char *buf, const GString *prefix)
        }
 }
 
-/* TODO Move parse_line() here. */
-
 /**
- * @brief Parse a text field into multiple bits, binary presentation.
+ * @brief Splits a text line into a set of columns.
  *
- * @param[in] str      The input text, a run of 0/1 digits.
+ * @param[in] buf      The input text line to split.
  * @param[in] inc      The input module's context.
  *
- * @retval SR_OK       Success.
- * @retval SR_ERR      Invalid input data (empty, or format error).
+ * @returns An array of strings, representing the columns' text.
  *
- * This routine modifies the logic levels in the current sample set,
- * based on the text input which consists of binary digits.
+ * This routine splits a text line on previously determined separators.
  */
-static int parse_binstr(const char *str, struct context *inc)
+static char **split_line(char *buf, struct context *inc)
 {
-       gsize i, j, length;
-       char c;
-
-       length = strlen(str);
-       if (!length) {
-               sr_err("Column %zu in line %zu is empty.", inc->single_column,
-                       inc->line_number);
-               return SR_ERR;
-       }
-
-       i = inc->first_channel;
-       for (j = 0; i < length && j < inc->num_channels; i++, j++) {
-               c = str[length - i - 1];
-               if (c == '1') {
-                       set_logic_level(inc, j, 1);
-               } else if (c != '0') {
-                       sr_err("Invalid text '%s' in binary column %zu in line %zu.",
-                               str, inc->single_column, inc->line_number);
-                       return SR_ERR;
-               }
-       }
-
-       return SR_OK;
+       return g_strsplit(buf, inc->delimiter->str, 0);
 }
 
 /**
- * @brief Parse a text field into multiple bits, hexadecimal presentation.
+ * @brief Parse a multi-bit field into several logic channels.
  *
- * @param[in] str      The input text, a run of hex digits.
+ * @param[in] column   The input text, a run of bin/hex/oct digits.
  * @param[in] inc      The input module's context.
+ * @param[in] col_nr   The involved column number (1-based).
  *
  * @retval SR_OK       Success.
  * @retval SR_ERR      Invalid input data (empty, or format error).
  *
  * This routine modifies the logic levels in the current sample set,
- * based on the text input which consists of hexadecimal digits.
+ * based on the text input and a user provided format spec.
  */
-static int parse_hexstr(const char *str, struct context *inc)
+static int parse_column(const char *column, struct context *inc, size_t col_nr)
 {
-       gsize i, j, k, length;
-       uint8_t value;
+       const struct column_details *col_det;
+       size_t length, ch_rem, ch_idx, ch_inc;
+       const char *rdptr;
        char c;
-
-       length = strlen(str);
-       if (!length) {
-               sr_err("Column %zu in line %zu is empty.", inc->single_column,
-                       inc->line_number);
+       gboolean valid;
+       const char *type_text;
+       uint8_t bits;
+
+       /* See whether and how the columns needs to get processed. */
+       col_det = lookup_column_details(inc, col_nr);
+       if (!col_det) {
+               sr_dbg("Column %zu in line %zu without processing details?",
+                       col_nr, inc->line_number);
                return SR_ERR;
        }
+       if (col_det->text_format == FORMAT_NONE)
+               return SR_OK;
 
-       /* Calculate the position of the first hexadecimal digit. */
-       i = inc->first_channel / 4;
-       for (j = 0; i < length && j < inc->num_channels; i++) {
-               c = str[length - i - 1];
-               if (!g_ascii_isxdigit(c)) {
-                       sr_err("Invalid text '%s' in hex column %zu in line %zu.",
-                               str, inc->single_column, inc->line_number);
-                       return SR_ERR;
-               }
-
-               value = g_ascii_xdigit_value(c);
-               k = (inc->first_channel + j) % 4;
-               for (; j < inc->num_channels && k < 4; j++, k++) {
-                       set_logic_level(inc, j, value & (1 << k));
-               }
-       }
-
-       return SR_OK;
-}
-
-/**
- * @brief Parse a text field into multiple bits, octal presentation.
- *
- * @param[in] str      The input text, a run of oct digits.
- * @param[in] inc      The input module's context.
- *
- * @retval SR_OK       Success.
- * @retval SR_ERR      Invalid input data (empty, or format error).
- *
- * This routine modifies the logic levels in the current sample set,
- * based on the text input which consists of octal digits.
- */
-static int parse_octstr(const char *str, struct context *inc)
-{
-       gsize i, j, k, length;
-       uint8_t value;
-       char c;
-
-       length = strlen(str);
+       /*
+        * Prepare to read the digits from the text end towards the start.
+        * A digit corresponds to a variable number of channels (depending
+        * on the value's radix). Prepare the mapping of text digits to
+        * (a number of) logic channels.
+        */
+       length = strlen(column);
        if (!length) {
-               sr_err("Column %zu in line %zu is empty.", inc->single_column,
+               sr_err("Column %zu in line %zu is empty.", col_nr,
                        inc->line_number);
                return SR_ERR;
        }
+       rdptr = &column[length];
+       ch_idx = col_det->channel_offset;
+       ch_rem = col_det->channel_count;
 
-       /* Calculate the position of the first octal digit. */
-       i = inc->first_channel / 3;
-       for (j = 0; i < length && j < inc->num_channels; i++) {
-               c = str[length - i - 1];
-               if (c < '0' || c > '7') {
-                       sr_err("Invalid text '%s' in oct column %zu in line %zu.",
-                               str, inc->single_column, inc->line_number);
-                       return SR_ERR;
-               }
-
-               value = g_ascii_xdigit_value(c);
-               k = (inc->first_channel + j) % 3;
-               for (; j < inc->num_channels && k < 3; j++, k++) {
-                       set_logic_level(inc, j, value & (1 << k));
-               }
-       }
-
-       return SR_OK;
-}
-
-static int parse_single_column(const char *column, struct context *inc)
-{
-       switch (inc->format) {
-       case FORMAT_BIN:
-               return parse_binstr(column, inc);
-       case FORMAT_HEX:
-               return parse_hexstr(column, inc);
-       case FORMAT_OCT:
-               return parse_octstr(column, inc);
-       }
-
-       return SR_ERR;
-}
-
-/**
- * @brief Splits a text line into a set of columns.
- *
- * @param[in] buf      The input text line to split.
- * @param[in] inc      The input module's context.
- * @param[in] max_cols The maximum column count, negative to get all of them.
- *
- * @returns An array of strings, representing the columns' text.
- *
- * This routine splits a text line on previously determined separators.
- * A previously determined set of columns gets isolated (starting at a
- * first position and spanning a given number of columns). A negative
- * value for the maximum number of columns results in no restriction on
- * the result set's length (the first columns still get trimmed off).
- */
-static char **parse_line(char *buf, struct context *inc, ssize_t max_cols)
-{
-       const char *str, *remainder;
-       GSList *list, *l;
-       char **columns;
-       char *column;
-       gsize seen, taken;
-
-       seen = 0;
-       taken = 0;
-       list = NULL;
-
-       remainder = buf;
-       str = strstr(remainder, inc->delimiter->str);
-       while (str && max_cols) {
-               if (seen >= inc->first_column) {
-                       column = g_strndup(remainder, str - remainder);
-                       list = g_slist_prepend(list, g_strstrip(column));
-
-                       max_cols--;
-                       taken++;
+       /*
+        * Get another digit and derive up to four logic channels' state from
+        * it. Make sure to not process more bits than the column has channels
+        * associated with it.
+        */
+       while (rdptr > column && ch_rem) {
+               /* Check for valid digits according to the input radix. */
+               c = *(--rdptr);
+               switch (inc->format) {
+               case FORMAT_BIN:
+                       valid = g_ascii_isxdigit(c) && c < '2';
+                       ch_inc = 1;
+                       break;
+               case FORMAT_OCT:
+                       valid = g_ascii_isxdigit(c) && c < '8';
+                       ch_inc = 3;
+                       break;
+               case FORMAT_HEX:
+                       valid = g_ascii_isxdigit(c);
+                       ch_inc = 4;
+                       break;
+               default:
+                       valid = FALSE;
+                       break;
                }
-
-               remainder = str + inc->delimiter->len;
-               str = strstr(remainder, inc->delimiter->str);
-               seen++;
-       }
-
-       if (buf[0] && max_cols && seen >= inc->first_column) {
-               column = g_strdup(remainder);
-               list = g_slist_prepend(list, g_strstrip(column));
-               taken++;
-       }
-
-       if (!(columns = g_try_new(char *, taken + 1)))
-               return NULL;
-       columns[taken--] = NULL;
-       for (l = list; l; l = l->next)
-               columns[taken--] = l->data;
-
-       g_slist_free(list);
-
-       return columns;
-}
-
-/**
- * @brief Picks logic levels from multiple binary colomns, one channel per column.
- *
- * @param[in] columns  The text fields which are kept in the columns.
- * @param[in] inc      The input module's context.
- *
- * @retval SR_OK       Success.
- * @retval SR_ERR      Insufficient input, or syntax errors.
- *
- * This routine exclusively handles binary input where one logic channel
- * occupies one column each. All channels are expected to reside in one
- * consequtive run of columns.
- */
-static int parse_multi_columns(char **columns, struct context *inc)
-{
-       gsize i;
-       char *column;
-
-       for (i = 0; i < inc->num_channels; i++) {
-               column = columns[i];
-               if (strcmp(column, "1") == 0) {
-                       set_logic_level(inc, i, 1);
-               } else if (!strlen(column)) {
-                       sr_err("Column %zu in line %zu is empty.",
-                               inc->first_channel + i, inc->line_number);
+               if (!valid) {
+                       type_text = col_format_text[inc->format];
+                       sr_err("Invalid text '%s' in %s type column %zu in line %zu.",
+                               column, type_text, col_nr, inc->line_number);
                        return SR_ERR;
-               } else if (strcmp(column, "0") != 0) {
-                       sr_err("Invalid text '%s' in bit column %zu in line %zu.",
-                               column, inc->first_channel + i,
-                               inc->line_number);
+               }
+               /* Use the digit's bits for logic channels' data. */
+               bits = g_ascii_xdigit_value(c);
+               switch (inc->format) {
+               case FORMAT_NONE:
+                       /* ShouldNotHappen(TM), but silences compiler warning. */
                        return SR_ERR;
+               case FORMAT_HEX:
+                       if (ch_rem >= 4) {
+                               ch_rem--;
+                               set_logic_level(inc, ch_idx + 3, bits & (1 << 3));
+                       }
+                       /* FALLTHROUGH */
+               case FORMAT_OCT:
+                       if (ch_rem >= 3) {
+                               ch_rem--;
+                               set_logic_level(inc, ch_idx + 2, bits & (1 << 2));
+                       }
+                       if (ch_rem >= 2) {
+                               ch_rem--;
+                               set_logic_level(inc, ch_idx + 1, bits & (1 << 1));
+                       }
+                       /* FALLTHROUGH */
+               case FORMAT_BIN:
+                       ch_rem--;
+                       set_logic_level(inc, ch_idx + 0, bits & (1 << 0));
+                       break;
                }
+               ch_idx += ch_inc;
        }
+       /*
+        * TODO Determine whether the availability of extra input data
+        * for unhandled logic channels is worth warning here. In this
+        * implementation users are in control, and can have the more
+        * significant bits ignored (which can be considered a feature
+        * and not really a limitation).
+        */
 
        return SR_OK;
 }
@@ -534,6 +508,7 @@ static int init(struct sr_input *in, GHashTable *options)
 {
        struct context *inc;
        const char *s;
+       int ret;
 
        in->sdi = g_malloc0(sizeof(struct sr_dev_inst));
        in->priv = inc = g_malloc0(sizeof(struct context));
@@ -546,7 +521,7 @@ static int init(struct sr_input *in, GHashTable *options)
        inc->delimiter = g_string_new(g_variant_get_string(
                        g_hash_table_lookup(options, "delimiter"), NULL));
        if (inc->delimiter->len == 0) {
-               sr_err("Delimiter must be at least one character.");
+               sr_err("Column delimiter cannot be empty.");
                return SR_ERR_ARG;
        }
 
@@ -565,15 +540,19 @@ static int init(struct sr_input *in, GHashTable *options)
        inc->comment = g_string_new(g_variant_get_string(
                        g_hash_table_lookup(options, "comment"), NULL));
        if (g_string_equal(inc->comment, inc->delimiter)) {
-               /* That's never going to work. Likely the result of the user
-                * setting the delimiter to ; -- the default comment. Clearing
-                * the comment setting will work in that case. */
+               /*
+                * Using the same sequence as comment leader and column
+                * delimiter won't work. The user probably specified ';'
+                * as the column delimiter but did not adjust the comment
+                * leader. Try DWIM, drop comment strippin support here.
+                */
+               sr_warn("Comment leader and column delimiter conflict, disabling comment support.");
                g_string_truncate(inc->comment, 0);
        }
 
        inc->samplerate = g_variant_get_uint64(g_hash_table_lookup(options, "samplerate"));
 
-       inc->first_channel = g_variant_get_uint32(g_hash_table_lookup(options, "first-channel"));
+       inc->first_column = g_variant_get_uint32(g_hash_table_lookup(options, "first-column"));
 
        inc->use_header = g_variant_get_boolean(g_hash_table_lookup(options, "header"));
 
@@ -583,13 +562,41 @@ static int init(struct sr_input *in, GHashTable *options)
                return SR_ERR_ARG;
        }
 
-       if (inc->multi_column_mode)
-               inc->first_column = inc->first_channel;
-       else
-               inc->first_column = inc->single_column;
-
-       if (!inc->multi_column_mode && !inc->num_channels) {
-               sr_err("Number of channels needs to be specified in single column mode.");
+       /*
+        * Derive the set of columns to inspect and their respective
+        * formats from simple input specs. Remain close to the previous
+        * set of option keywords and their meaning. Exclusively support
+        * a single column with multiple bits in it, or an adjacent set
+        * of colums with one bit each. The latter may not know the total
+        * column count here (when the user omitted the spec), and will
+        * derive it from the first text line of the input file.
+        */
+       if (inc->single_column && inc->num_channels) {
+               sr_dbg("DIAG Got single column (%zu) and channels (%zu).",
+                       inc->single_column, inc->num_channels);
+               sr_dbg("DIAG -> column %zu, %zu bits in %s format.",
+                       inc->single_column, inc->num_channels,
+                       col_format_text[inc->format]);
+               ret = make_column_details_single(inc,
+                       inc->single_column, inc->num_channels, inc->format);
+               if (ret != SR_OK)
+                       return ret;
+       } else if (inc->multi_column_mode) {
+               sr_dbg("DIAG Got multi-column, first column %zu, count %zu.",
+                       inc->first_column, inc->num_channels);
+               if (inc->num_channels) {
+                       sr_dbg("DIAG -> columns %zu-%zu, 1 bit each.",
+                               inc->first_column,
+                               inc->first_column + inc->num_channels - 1);
+                       ret = make_column_details_multi(inc, inc->first_column,
+                               inc->first_column + inc->num_channels - 1);
+                       if (ret != SR_OK)
+                               return ret;
+               } else {
+                       sr_dbg("DIAG -> incomplete spec, have to update later.");
+               }
+       } else {
+               sr_err("Unknown or unsupported combination of option values.");
                return SR_ERR_ARG;
        }
 
@@ -655,10 +662,11 @@ static int initial_parse(const struct sr_input *in, GString *buf)
 {
        struct context *inc;
        GString *channel_name;
-       size_t num_columns, i;
-       size_t line_number, l;
+       size_t num_columns, ch_idx, ch_name_idx, col_idx, col_nr;
+       size_t line_number, line_idx;
        int ret;
        char **lines, *line, **columns, *column;
+       const struct column_details *detail;
 
        ret = SR_OK;
        inc = in->priv;
@@ -666,11 +674,10 @@ static int initial_parse(const struct sr_input *in, GString *buf)
 
        line_number = 0;
        lines = g_strsplit_set(buf->str, delim_set, 0);
-       for (l = 0; lines[l]; l++) {
+       for (line_idx = 0; (line = lines[line_idx]); line_idx++) {
                line_number++;
-               line = lines[l];
                if (inc->start_line > line_number) {
-                       sr_spew("Line %zu skipped.", line_number);
+                       sr_spew("Line %zu skipped (before start).", line_number);
                        continue;
                }
                if (line[0] == '\0') {
@@ -686,63 +693,77 @@ static int initial_parse(const struct sr_input *in, GString *buf)
                /* Reached first proper line. */
                break;
        }
-       if (!lines[l]) {
+       if (!line) {
                /* Not enough data for a proper line yet. */
                ret = SR_ERR_NA;
                goto out;
        }
 
-       /*
-        * In order to determine the number of columns parse the current line
-        * without limiting the number of columns.
-        */
-       columns = parse_line(line, inc, -1);
+       /* See how many columns the current line has. */
+       columns = split_line(line, inc);
        if (!columns) {
                sr_err("Error while parsing line %zu.", line_number);
                ret = SR_ERR;
                goto out;
        }
        num_columns = g_strv_length(columns);
-
-       /* Ensure that the first column is not out of bounds. */
        if (!num_columns) {
-               sr_err("Column %zu in line %zu is out of bounds.",
-                       inc->first_column, line_number);
+               sr_err("Error while parsing line %zu.", line_number);
                ret = SR_ERR;
                goto out;
        }
-
-       if (inc->multi_column_mode) {
-               /*
-                * Detect the number of channels in multi column mode
-                * automatically if not specified.
-                */
-               if (!inc->num_channels) {
-                       inc->num_channels = num_columns;
-                       sr_dbg("Number of auto-detected channels: %zu.",
-                               inc->num_channels);
-               }
-
-               /*
-                * Ensure that the number of channels does not exceed the number
-                * of columns in multi column mode.
-                */
-               if (num_columns < inc->num_channels) {
-                       sr_err("Not enough columns for desired number of channels in line %zu.",
-                               line_number);
-                       ret = SR_ERR;
+       sr_dbg("DIAG Got %zu columns in text line: %s.", num_columns, line);
+
+       /* Optionally update incomplete multi-column specs. */
+       if (inc->multi_column_mode && !inc->num_channels) {
+               inc->num_channels = num_columns - inc->first_column + 1;
+               sr_dbg("DIAG -> multi-column update: columns %zu-%zu, 1 bit each.",
+                       inc->first_column,
+                       inc->first_column + inc->num_channels - 1);
+               ret = make_column_details_multi(inc, inc->first_column,
+                       inc->first_column + inc->num_channels - 1);
+               if (ret != SR_OK)
                        goto out;
-               }
        }
 
+       /*
+        * Assume all lines have equal length (column count). Bail out
+        * early on suspicious or insufficient input data (check input
+        * which became available here against previous user specs or
+        * auto-determined properties, regardless of layout variant).
+        */
+       if (num_columns < inc->column_want_count) {
+               sr_err("Insufficient input text width for desired data amount, got %zu but want %zu columns.",
+                       num_columns, inc->column_want_count);
+               ret = SR_ERR;
+               goto out;
+       }
+
+       /*
+        * Determine channel names. Optionally use text from a header
+        * line (when requested by the user, and only works in multi
+        * column mode). In the absence of header text, or in single
+        * column mode, channels are assigned rather generic names.
+        */
        channel_name = g_string_sized_new(64);
-       for (i = 0; i < inc->num_channels; i++) {
-               column = columns[i];
-               if (inc->use_header && inc->multi_column_mode && column[0] != '\0')
-                       g_string_assign(channel_name, column);
-               else
-                       g_string_printf(channel_name, "%zu", i);
-               sr_channel_new(in->sdi, i, SR_CHANNEL_LOGIC, TRUE, channel_name->str);
+       for (col_idx = 0; col_idx < inc->column_want_count; col_idx++) {
+               col_nr = col_idx + 1;
+               detail = lookup_column_details(inc, col_nr);
+               if (detail->text_format == FORMAT_NONE)
+                       continue;
+               column = columns[col_idx];
+               sr_dbg("DIAG col %zu, ch count %zu, text %s.",
+                       col_nr, detail->channel_count, column);
+               for (ch_idx = 0; ch_idx < detail->channel_count; ch_idx++) {
+                       ch_name_idx = detail->channel_offset + ch_idx;
+                       if (inc->use_header && *column && inc->multi_column_mode)
+                               g_string_assign(channel_name, column);
+                       else
+                               g_string_printf(channel_name, "%zu", ch_name_idx);
+                       sr_dbg("DIAG ch idx %zu, name %s.", ch_name_idx, channel_name->str);
+                       sr_channel_new(in->sdi, ch_name_idx, SR_CHANNEL_LOGIC, TRUE,
+                               channel_name->str);
+               }
        }
        g_string_free(channel_name, TRUE);
        if (!check_header_in_reread(in)) {
@@ -833,9 +854,9 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
 {
        struct context *inc;
        gsize num_columns;
-       size_t max_columns, l;
+       size_t line_idx, col_idx, col_nr;
        int ret;
-       char *p, **lines, *line, **columns;
+       char *p, **lines, *line, **columns, *column;
 
        inc = in->priv;
        if (!inc->started) {
@@ -843,12 +864,6 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
                inc->started = TRUE;
        }
 
-       /* Limit the number of columns to parse. */
-       if (inc->multi_column_mode)
-               max_columns = inc->num_channels;
-       else
-               max_columns = 1;
-
        /*
         * Consider empty input non-fatal. Keep accumulating input until
         * at least one full text line has become available. Grab the
@@ -877,9 +892,8 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
 
        ret = SR_OK;
        lines = g_strsplit_set(in->buf->str, delim_set, 0);
-       for (l = 0; lines[l]; l++) {
+       for (line_idx = 0; (line = lines[line_idx]); line_idx++) {
                inc->line_number++;
-               line = lines[l];
                if (line[0] == '\0') {
                        sr_spew("Blank line %zu skipped.", inc->line_number);
                        continue;
@@ -899,27 +913,17 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
                        continue;
                }
 
-               columns = parse_line(line, inc, max_columns);
+               /* Split the line into columns, check for minimum length. */
+               columns = split_line(line, inc);
                if (!columns) {
                        sr_err("Error while parsing line %zu.", inc->line_number);
                        g_strfreev(lines);
                        return SR_ERR;
                }
                num_columns = g_strv_length(columns);
-               if (!num_columns) {
-                       sr_err("Column %zu in line %zu is out of bounds.",
-                               inc->first_column, inc->line_number);
-                       g_strfreev(columns);
-                       g_strfreev(lines);
-                       return SR_ERR;
-               }
-               /*
-                * Ensure that the number of channels does not exceed the number
-                * of columns in multi column mode.
-                */
-               if (inc->multi_column_mode && num_columns < inc->num_channels) {
-                       sr_err("Not enough columns for desired number of channels in line %zu.",
-                               inc->line_number);
+               if (num_columns < inc->column_want_count) {
+                       sr_err("Insufficient column count %zu in line %zu.",
+                               num_columns, inc->line_number);
                        g_strfreev(columns);
                        g_strfreev(lines);
                        return SR_ERR;
@@ -927,14 +931,15 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
 
                clear_logic_samples(inc);
 
-               if (inc->multi_column_mode)
-                       ret = parse_multi_columns(columns, inc);
-               else
-                       ret = parse_single_column(columns[0], inc);
-               if (ret != SR_OK) {
-                       g_strfreev(columns);
-                       g_strfreev(lines);
-                       return SR_ERR;
+               for (col_idx = 0; col_idx < inc->column_want_count; col_idx++) {
+                       column = columns[col_idx];
+                       col_nr = col_idx + 1;
+                       ret = parse_column(column, inc, col_nr);
+                       if (ret != SR_OK) {
+                               g_strfreev(columns);
+                               g_strfreev(lines);
+                               return SR_ERR;
+                       }
                }
 
                /* Send sample data to the session bus (buffered). */
@@ -1035,7 +1040,7 @@ enum option_index {
        OPT_FORMAT,
        OPT_COMMENT,
        OPT_RATE,
-       OPT_FIRST_LOGIC,
+       OPT_FIRST_COL,
        OPT_HEADER,
        OPT_START,
        OPT_MAX,
@@ -1048,7 +1053,7 @@ static struct sr_option options[] = {
        [OPT_FORMAT] = { "format", "Data format (single-col. mode)", "The numeric format of the data (single-col. mode): bin, hex, oct", NULL, NULL },
        [OPT_COMMENT] = { "comment", "Comment character(s)", "The comment prefix character(s)", NULL, NULL },
        [OPT_RATE] = { "samplerate", "Samplerate (Hz)", "The sample rate (used during capture) in Hz", NULL, NULL },
-       [OPT_FIRST_LOGIC] = { "first-channel", "First channel", "The column number of the first channel (multi-col. mode); bit position for the first channel (single-col. mode)", NULL, NULL },
+       [OPT_FIRST_COL] = { "first-column", "First column", "The column number of the first channel (multi-col. mode)", NULL, NULL },
        [OPT_HEADER] = { "header", "Interpret first line as header (multi-col. mode)", "Treat the first line as header with channel names (multi-col. mode)", NULL, NULL },
        [OPT_START] = { "startline", "Start line", "The line number at which to start processing samples (>= 1)", NULL, NULL },
        [OPT_MAX] = ALL_ZERO,
@@ -1059,8 +1064,8 @@ static const struct sr_option *get_options(void)
        GSList *l;
 
        if (!options[0].def) {
-               options[OPT_SINGLE_COL].def = g_variant_ref_sink(g_variant_new_int32(0));
-               options[OPT_NUM_LOGIC].def = g_variant_ref_sink(g_variant_new_int32(0));
+               options[OPT_SINGLE_COL].def = g_variant_ref_sink(g_variant_new_uint32(0));
+               options[OPT_NUM_LOGIC].def = g_variant_ref_sink(g_variant_new_uint32(0));
                options[OPT_DELIM].def = g_variant_ref_sink(g_variant_new_string(","));
                options[OPT_FORMAT].def = g_variant_ref_sink(g_variant_new_string("bin"));
                l = NULL;
@@ -1070,9 +1075,9 @@ static const struct sr_option *get_options(void)
                options[OPT_FORMAT].values = l;
                options[OPT_COMMENT].def = g_variant_ref_sink(g_variant_new_string(";"));
                options[OPT_RATE].def = g_variant_ref_sink(g_variant_new_uint64(0));
-               options[OPT_FIRST_LOGIC].def = g_variant_ref_sink(g_variant_new_int32(0));
+               options[OPT_FIRST_COL].def = g_variant_ref_sink(g_variant_new_uint32(1));
                options[OPT_HEADER].def = g_variant_ref_sink(g_variant_new_boolean(FALSE));
-               options[OPT_START].def = g_variant_ref_sink(g_variant_new_int32(1));
+               options[OPT_START].def = g_variant_ref_sink(g_variant_new_uint32(1));
        }
 
        return options;