All of lore.kernel.org
 help / color / mirror / Atom feed
* [libgpiod v2][PATCH 0/4] tools: line name focussed rework
@ 2022-06-27 13:44 Kent Gibson
  2022-06-27 13:44 ` [libgpiod v2][PATCH 1/4] " Kent Gibson
                   ` (4 more replies)
  0 siblings, 5 replies; 15+ messages in thread
From: Kent Gibson @ 2022-06-27 13:44 UTC (permalink / raw)
  To: linux-gpio, brgl; +Cc: Kent Gibson

This patch series is an optimistic reimagining of the tools intended to
simplify usage for well configured systems, i.e. for systems where lines
can be uniquely identified by name.  In such systems the chip and offset
location of the line is no longer of relevance to the user, so the tools
should be able to operate without mentioning them.
e.g.
  gpioget GPIO17

  gpioset GPIO17=active

  gpiomon --localtime GPIO17 GPIO18 

It is accepted that the kernel does not guarantee line name uniqueness
within the system, or even within a chip, and not all systems are well
configured, so the tools retain the option to identify lines by chip
and offset.  The hope and expectation is that over time systems will
become more well configured, not less, and identification of GPIO lines
by name will become the norm.

The core of the series is patch 1 which is a reworking of the tools to
support identifying lines by name, and to operate across multiple GPIO
chips if named lines are located on different chips.
The gpioset tool is extended to support toggling lines and interactive
control of line values, so some common use cases can be trivially
implemented from the command line.
e.g.
  gpioset --toggle 500ms LED=on

will blink the LED line at 1Hz, indefinitely.
More complex outputs can be generated by adding more entries to the
toggle sequence:
  gpioset --toggle 1s,2s,1s,300ms LED=on

Even more complex outputs can be generated by driving gpioset in
interactive mode from another script.

Those are the major changes.  A more complete list of the changes can be
found in the patch description.

Patch 2 updates and extends the tool tests to cover the reworked tools,
including demonstrating gpioset being driven interactively via a script.

The final two patches add a gpiowatch tool that monitors changes to 
the state line information, similar to the gpio-watch tool in the kernel,
and extend the test suite to cover it.

Cheers,
Kent.

Kent Gibson (4):
  tools: line name focussed rework
  tools: tests for line name focussed rework
  tools: add gpiowatch
  tools: gpiowatch tests

 configure.ac               |    9 +-
 man/Makefile.am            |    2 +-
 tools/.gitignore           |    1 +
 tools/Makefile.am          |    4 +-
 tools/gpio-tools-test      |    3 -
 tools/gpio-tools-test.bats | 2189 ++++++++++++++++++++++++++++--------
 tools/gpiodetect.c         |  108 +-
 tools/gpiofind.c           |  126 ++-
 tools/gpioget.c            |  200 ++--
 tools/gpioinfo.c           |  356 +++---
 tools/gpiomon.c            |  493 ++++----
 tools/gpioset.c            |  861 ++++++++++----
 tools/gpiowatch.c          |  214 ++++
 tools/tools-common.c       |  640 ++++++++++-
 tools/tools-common.h       |   59 +-
 15 files changed, 3927 insertions(+), 1338 deletions(-)
 create mode 100644 tools/gpiowatch.c

-- 
2.36.1


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [libgpiod v2][PATCH 1/4] tools: line name focussed rework
  2022-06-27 13:44 [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
@ 2022-06-27 13:44 ` Kent Gibson
  2022-07-06 20:20   ` Bartosz Golaszewski
  2022-06-27 13:44 ` [libgpiod v2][PATCH 2/4] tools: tests for " Kent Gibson
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 15+ messages in thread
From: Kent Gibson @ 2022-06-27 13:44 UTC (permalink / raw)
  To: linux-gpio, brgl; +Cc: Kent Gibson

Rework the tool suite to support identifying lines by name and to
support operating on the GPIO lines available to the user at once, rather
than on one particular GPIO chip.

All tools, other than gpiodetect, now provide the name to (chip,offset)
mapping that was previously only performed by gpiofind. As names are not
guaranteed to be unique, a --strict option is provided for all tools to
either abort the operation or report all lines with the matching name, as
appropriate.
By default the tools operate on the first line found with a matching name.

Selection of line by (chip,offset) is still supported with a --chip
option, though it restricts the scope of the operation to an individual
chip.  When the --chip option is specified, the lines are assumed to be
identified by offset where they parse as an integer, else by name.
To cater for the unusual case where a line name parses as an integer,
but is different from the offset, the --by-name option forces the lines
to be identified by name.

The updated tools are intentionally NOT backwardly compatible with the
previous tools. Using old command lines with the updated tools will
almost certainly fail, though migrating old command lines is generally as
simple as adding a '-c' before the chip.

In addition the individual tools are modified as follows:

gpiodetect:

Add the option to select individual chips.

gpiofind:

Add the option to display the info for found lines.

gpioinfo:

Change the focus from chips to lines, so the scope can be
an individual line, a subset of lines, all lines on a particular chip,
or all the lines available to the user.  For line scope a single line
summary is output for each line.  For chip scope the existing format
displaying a summary of the chip and each of its lines is retained.

Line attributes are consolidated into a list format, and are extended
to cover all attributes supported by uAPI v2.

gpioget:

The default output format is becomes line=value, as per the
input for gpioset, and the value is reported as active or inactive,
rather than 0 or 1.
The previous format is available using the --numeric option.

Add an optional hold period between requesting a line and reading the
value to allow the line to settle once the requested configuration has
been applied (e.g. bias).

gpiomon:

Consolidate the edge options into a single option.

Add a debounce period option.

Add options to report event times as UTC or localtime.

Add format specifiers for GPIO chip path, line name, stringified event
type, and event time as a datetime.

Rearrange default output format to place fields with more predicable
widths to the left, and to separate major field groups with tabs.
Lines are identified consistent with the command line.

gpioset:

Add a hold period option that specifies the minimum period the line
value must be held for.  This applies to all set options.

Support line values specified as active/inactive, on/off and
true/false, as well as 1/0.

Add a toggle option that specifies a time sequence over which the
requested lines should be toggled.  If the sequence is 0 terminated then
gpioset exits when the sequence completes, else it repeats the sequence.
This allows for anything from simple blinkers to bit bashing from the
command line. e.g. gpioset -t 500ms LED=on

Add an interactive option to provide a shell-like interface to allow
manual or scripted manipulation of requested lines.  A basic command set
allows lines to be get, set, or toggled, and to insert sleeps between
operations.

Remove the --mode, --sec, and --usec options.
The combination of hold period and interactive mode provide functionality
equivalent to the old --mode options.

Signed-off-by: Kent Gibson <warthog618@gmail.com>
---
 configure.ac         |   9 +-
 tools/gpiodetect.c   | 108 ++++--
 tools/gpiofind.c     | 126 +++++--
 tools/gpioget.c      | 200 ++++++----
 tools/gpioinfo.c     | 356 ++++++++----------
 tools/gpiomon.c      | 493 +++++++++++++++----------
 tools/gpioset.c      | 861 +++++++++++++++++++++++++++++++------------
 tools/tools-common.c | 640 +++++++++++++++++++++++++++++---
 tools/tools-common.h |  59 ++-
 9 files changed, 2011 insertions(+), 841 deletions(-)

diff --git a/configure.ac b/configure.ac
index ab03673..c7e353c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -105,14 +105,15 @@ AC_DEFUN([FUNC_NOT_FOUND_TOOLS],
 AC_DEFUN([HEADER_NOT_FOUND_TOOLS],
 	[ERR_NOT_FOUND([$1 header], [tools])])
 
+AC_DEFUN([LIB_NOT_FOUND_TOOLS],
+	[ERR_NOT_FOUND([lib$1], [tools])])
+
 if test "x$with_tools" = xtrue
 then
 	# These are only needed to build tools
-	AC_CHECK_FUNC([basename], [], [FUNC_NOT_FOUND_TOOLS([basename])])
 	AC_CHECK_FUNC([daemon], [], [FUNC_NOT_FOUND_TOOLS([daemon])])
-	AC_CHECK_FUNC([signalfd], [], [FUNC_NOT_FOUND_TOOLS([signalfd])])
-	AC_CHECK_FUNC([setlinebuf], [], [FUNC_NOT_FOUND_TOOLS([setlinebuf])])
-	AC_CHECK_HEADERS([sys/signalfd.h], [], [HEADER_NOT_FOUND_TOOLS([sys/signalfd.h])])
+	AC_CHECK_HEADERS([readline/readline.h], [], [HEADER_NOT_FOUND_TOOLS([readline/readline.h])])
+	AC_CHECK_LIB([readline], readline, [], [LIB_NOT_FOUND_TOOLS([readline])])
 fi
 
 AC_ARG_ENABLE([tests],
diff --git a/tools/gpiodetect.c b/tools/gpiodetect.c
index 8f6e8b3..18b6e95 100644
--- a/tools/gpiodetect.c
+++ b/tools/gpiodetect.c
@@ -1,7 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
 
-#include <dirent.h>
 #include <errno.h>
 #include <getopt.h>
 #include <gpiod.h>
@@ -21,21 +20,23 @@ static const char *const shortopts = "+hv";
 
 static void print_help(void)
 {
-	printf("Usage: %s [OPTIONS]\n", get_progname());
+	printf("Usage: %s [OPTIONS] [chip]...\n", get_progname());
 	printf("\n");
-	printf("List all GPIO chips, print their labels and number of GPIO lines.\n");
+	printf("List GPIO chips, print their labels and number of GPIO lines.\n");
+	printf("\n");
+	printf("Chips may be identified by number, name, or path.\n");
+	printf("e.g. '0', 'gpiochip0', and '/dev/gpiochip0' all refer to the same chip.\n");
+	printf("\n");
+	printf("If no chips are specified then all chips are listed.\n");
 	printf("\n");
 	printf("Options:\n");
-	printf("  -h, --help:\t\tdisplay this message and exit\n");
-	printf("  -v, --version:\tdisplay the version and exit\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
 }
 
-int main(int argc, char **argv)
+int parse_config(int argc, char **argv)
 {
-	int optc, opti, num_chips, i;
-	struct gpiod_chip *chip;
-	struct gpiod_chip_info *info;
-	struct dirent **entries;
+	int optc, opti;
 
 	for (;;) {
 		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
@@ -45,48 +46,77 @@ int main(int argc, char **argv)
 		switch (optc) {
 		case 'h':
 			print_help();
-			return EXIT_SUCCESS;
+			exit(EXIT_SUCCESS);
 		case 'v':
 			print_version();
-			return EXIT_SUCCESS;
+			exit(EXIT_SUCCESS);
 		case '?':
 			die("try %s --help", get_progname());
 		default:
 			abort();
 		}
 	}
+	return optind;
+}
 
-	argc -= optind;
-	argv += optind;
-
-	if (argc > 0)
-		die("unrecognized argument: %s", argv[0]);
-
-	num_chips = scandir("/dev/", &entries, chip_dir_filter, alphasort);
-	if (num_chips < 0)
-		die_perror("unable to scan /dev");
+int print_chip_info(const char *path)
+{
+	struct gpiod_chip *chip;
+	struct gpiod_chip_info *info;
 
-	for (i = 0; i < num_chips; i++) {
-		chip = chip_open_by_name(entries[i]->d_name);
-		if (!chip)
-			die_perror("unable to open %s", entries[i]->d_name);
+	chip = gpiod_chip_open(path);
+	if (!chip) {
+		print_perror("unable to open chip %s", path);
+		return 1;
+	}
 
-		info = gpiod_chip_get_info(chip);
-		if (!info)
-			die_perror("unable to get info for %s", entries[i]->d_name);
+	info = gpiod_chip_get_info(chip);
+	if (!info)
+		die_perror("unable to get info for %s", path);
 
+	printf("%s [%s] (%zu lines)\n",
+	       gpiod_chip_info_get_name(info),
+	       gpiod_chip_info_get_label(info),
+	       gpiod_chip_info_get_num_lines(info));
 
-		printf("%s [%s] (%zu lines)\n",
-		       gpiod_chip_info_get_name(info),
-		       gpiod_chip_info_get_label(info),
-		       gpiod_chip_info_get_num_lines(info));
+	gpiod_chip_info_free(info);
+	gpiod_chip_close(chip);
+	return 0;
+}
 
-		gpiod_chip_info_free(info);
-		gpiod_chip_close(chip);
-		free(entries[i]);
+int main(int argc, char **argv)
+{
+	int num_chips, i;
+	char **paths;
+	char *path;
+	int ret = EXIT_SUCCESS;
+
+	i = parse_config(argc, argv);
+	argc -= i;
+	argv += i;
+
+	if (argc == 0) {
+		num_chips = all_chip_paths(&paths);
+		for (i = 0; i < num_chips; i++) {
+			if (print_chip_info(paths[i]))
+				ret = EXIT_FAILURE;
+			free(paths[i]);
+		}
+		free(paths);
+	}
+	for (i = 0; i < argc; i++) {
+		if (chip_path_lookup(argv[i], &path)) {
+			print_chip_info(path);
+			free(path);
+		} else if (errno == ENOENT) {
+			print_error("cannot find a GPIO chip character device corresponding to %s",
+				    argv[i]);
+			ret = EXIT_FAILURE;
+		} else {
+			print_perror("unable to open chip %s", path);
+			ret = EXIT_FAILURE;
+		}
 	}
 
-	free(entries);
-
-	return EXIT_SUCCESS;
+	return ret;
 }
diff --git a/tools/gpiofind.c b/tools/gpiofind.c
index 03b15c9..a048c06 100644
--- a/tools/gpiofind.c
+++ b/tools/gpiofind.c
@@ -1,7 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
 
-#include <dirent.h>
 #include <errno.h>
 #include <getopt.h>
 #include <gpiod.h>
@@ -12,30 +11,44 @@
 #include "tools-common.h"
 
 static const struct option longopts[] = {
-	{ "help",	no_argument,	NULL,	'h' },
-	{ "version",	no_argument,	NULL,	'v' },
+	{ "chip",	required_argument,	NULL,	'c' },
+	{ "help",	no_argument,		NULL,	'h' },
+	{ "info",	no_argument,		NULL,	'i' },
+	{ "strict",	no_argument,		NULL,	's' },
+	{ "version",	no_argument,		NULL,	'v' },
 	{ GETOPT_NULL_LONGOPT },
 };
 
-static const char *const shortopts = "+hv";
+static const char *const shortopts = "+c:hisv";
 
 static void print_help(void)
 {
 	printf("Usage: %s [OPTIONS] <name>\n", get_progname());
 	printf("\n");
-	printf("Find a GPIO line by name. The output of this command can be used as input for gpioget/set.\n");
+	printf("Find a GPIO line by name.\n");
+	printf("\n");
+	printf("The output of this command can be used as input for gpioget/set.\n");
 	printf("\n");
 	printf("Options:\n");
-	printf("  -h, --help:\t\tdisplay this message and exit\n");
-	printf("  -v, --version:\tdisplay the version and exit\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -i, --info\t\tdisplay info for found lines\n");
+	printf("  -s, --strict\t\tcheck all lines - don't assume line names are unique\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	print_chip_help();
 }
 
-int main(int argc, char **argv)
+struct config {
+	bool strict;
+	bool display_info;
+	const char *chip_id;
+};
+
+int parse_config(int argc, char **argv, struct config *cfg)
 {
-	int i, num_chips, optc, opti, offset;
-	struct gpiod_chip *chip;
-	struct gpiod_chip_info *info;
-	struct dirent **entries;
+	int opti, optc;
+
+	memset(cfg, 0, sizeof(*cfg));
 
 	for (;;) {
 		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
@@ -43,12 +56,21 @@ int main(int argc, char **argv)
 			break;
 
 		switch (optc) {
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
+		case 'i':
+			cfg->display_info = true;
+			break;
+		case 's':
+			cfg->strict = true;
+			break;
 		case 'h':
 			print_help();
-			return EXIT_SUCCESS;
+			exit(EXIT_SUCCESS);
 		case 'v':
 			print_version();
-			return EXIT_SUCCESS;
+			exit(EXIT_SUCCESS);
 		case '?':
 			die("try %s --help", get_progname());
 		default:
@@ -56,38 +78,74 @@ int main(int argc, char **argv)
 		}
 	}
 
+	return optind;
+}
+
+int main(int argc, char **argv)
+{
+	int i, num_chips, num_lines, offset, num_found = 0, ret = EXIT_FAILURE;
+	struct gpiod_chip *chip;
+	struct gpiod_chip_info *chip_info;
+	char **paths;
+	const char *name;
+	struct gpiod_line_info *line_info;
+	struct config cfg;
+
+	i = parse_config(argc, argv, &cfg);
 	argc -= optind;
 	argv += optind;
 
 	if (argc != 1)
 		die("exactly one GPIO line name must be specified");
 
-	num_chips = scandir("/dev/", &entries, chip_dir_filter, alphasort);
-	if (num_chips < 0)
-		die_perror("unable to scan /dev");
+	num_chips = chip_paths(cfg.chip_id, &paths);
+	if ((cfg.chip_id != NULL)  && (num_chips == 0))
+		die("cannot find a GPIO chip character device corresponding to %s", cfg.chip_id);
 
 	for (i = 0; i < num_chips; i++) {
-		chip = chip_open_by_name(entries[i]->d_name);
+		chip = gpiod_chip_open(paths[i]);
 		if (!chip) {
-			if (errno == EACCES)
+			if ((errno == EACCES) && (!cfg.chip_id))
 				continue;
 
-			die_perror("unable to open %s", entries[i]->d_name);
+			die_perror("unable to open %s", paths[i]);
 		}
 
-		offset = gpiod_chip_get_line_offset_from_name(chip, argv[0]);
-		if (offset >= 0) {
-			info = gpiod_chip_get_info(chip);
-			if (!info)
-				die_perror("unable to get info for %s", entries[i]->d_name);
-
-			printf("%s %u\n",
-			       gpiod_chip_info_get_name(info), offset);
-			gpiod_chip_info_free(info);
-			gpiod_chip_close(chip);
-			return EXIT_SUCCESS;
+		chip_info = gpiod_chip_get_info(chip);
+		if (!chip_info)
+			die_perror("unable to get info for %s", paths[i]);
+
+		num_lines = gpiod_chip_info_get_num_lines(chip_info);
+		for (offset = 0; offset < num_lines; offset++) {
+			line_info = gpiod_chip_get_line_info(chip, offset);
+			if (!line_info)
+				die_perror("unable to retrieve the line info from chip %s",
+					   gpiod_chip_get_path(chip));
+
+			name = gpiod_line_info_get_name(line_info);
+			if (name && strcmp(argv[0], gpiod_line_info_get_name(line_info)) == 0) {
+				num_found++;
+				printf("%s %u", gpiod_chip_info_get_name(chip_info), offset);
+				if (cfg.display_info)
+					print_line_info(line_info);
+				printf("\n");
+				if (!cfg.strict) {
+					gpiod_chip_info_free(chip_info);
+					gpiod_chip_close(chip);
+					goto exit_paths;
+				}
+			}
 		}
+		gpiod_chip_info_free(chip_info);
+		gpiod_chip_close(chip);
 	}
-
-	return EXIT_FAILURE;
+	if (!num_found)
+		print_error("cannot find line %s", argv[0]);
+exit_paths:
+	if (num_found == 1)
+		ret = EXIT_SUCCESS;
+	for (i = 0; i < num_chips; i++)
+		free(paths[i]);
+	free(paths);
+	return ret;
 }
diff --git a/tools/gpioget.c b/tools/gpioget.c
index ae80271..001049f 100644
--- a/tools/gpioget.c
+++ b/tools/gpioget.c
@@ -1,56 +1,73 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
 
 #include <getopt.h>
 #include <gpiod.h>
-#include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 #include "tools-common.h"
 
+static int by_name;
+static int numeric;
 static const struct option longopts[] = {
-	{ "help",	no_argument,		NULL,	'h' },
-	{ "version",	no_argument,		NULL,	'v' },
-	{ "active-low",	no_argument,		NULL,	'l' },
-	{ "dir-as-is",	no_argument,		NULL,	'n' },
-	{ "bias",	required_argument,	NULL,	'B' },
+	{ "active-low",		no_argument,		NULL,	'l' },
+	{ "as-is",		no_argument,		NULL,	'a' },
+	{ "bias",		required_argument,	NULL,	'b' },
+	{ "by-name",		no_argument,		&by_name,	1 },
+	{ "chip",		required_argument,	NULL,	'c' },
+	{ "help",		no_argument,		NULL,	'h' },
+	{ "hold-period",	required_argument,	NULL,	'p' },
+	{ "numeric",		no_argument,		&numeric,	1 },
+	{ "strict",		no_argument,		NULL,	's' },
+	{ "version",		no_argument,		NULL,	'v' },
 	{ GETOPT_NULL_LONGOPT },
 };
 
-static const char *const shortopts = "+hvlnB:";
+static const char *const shortopts = "+ab:c:hlp:sv";
 
 static void print_help(void)
 {
-	printf("Usage: %s [OPTIONS] <chip name/number> <offset 1> <offset 2> ...\n",
-	       get_progname());
+	printf("Usage: %s [OPTIONS] <line> ...\n", get_progname());
 	printf("\n");
-	printf("Read line value(s) from a GPIO chip\n");
+	printf("Read values of GPIO lines.\n");
 	printf("\n");
-	printf("Options:\n");
-	printf("  -h, --help:\t\tdisplay this message and exit\n");
-	printf("  -v, --version:\tdisplay the version and exit\n");
-	printf("  -l, --active-low:\tset the line active state to low\n");
-	printf("  -n, --dir-as-is:\tdon't force-reconfigure line direction\n");
-	printf("  -B, --bias=[as-is|disable|pull-down|pull-up] (defaults to 'as-is'):\n");
-	printf("		set the line bias\n");
+	printf("Lines are specified by name, or optionally by offset if the chip option\n");
+	printf("is provided.\n");
 	printf("\n");
+	printf("Options:\n");
+	printf("  -a, --as-is\t\tleave the line direction unchanged, not forced to input\n");
 	print_bias_help();
+	printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -l, --active-low\ttreat the line as active low\n");
+	printf("  -p, --hold-period <period>\n");
+	printf("\t\t\tapply a settling period between requesting the line(s)\n");
+	printf("\t\t\tand reading the value(s)\n");
+	printf("      --numeric\t\tdisplay line values as '0' (inactive) or '1' (active)\n");
+	printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	print_chip_help();
+	print_period_help();
 }
+struct config {
+	bool active_low;
+	bool strict;
+	int bias;
+	int direction;
+	unsigned int hold_period_us;
+	const char *chip_id;
+};
 
-int main(int argc, char **argv)
+int parse_config(int argc, char **argv, struct config *cfg)
 {
-	int direction = GPIOD_LINE_DIRECTION_INPUT;
-	int optc, opti, bias = 0, ret, *values;
-	struct gpiod_request_config *req_cfg;
-	struct gpiod_line_request *request;
-	struct gpiod_line_config *line_cfg;
-	struct gpiod_chip *chip;
-	bool active_low = false;
-	unsigned int *offsets;
-	size_t i, num_lines;
-	char *device, *end;
+	int opti, optc;
+
+	memset(cfg, 0, sizeof(*cfg));
+	cfg->direction = GPIOD_LINE_DIRECTION_INPUT;
 
 	for (;;) {
 		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
@@ -58,65 +75,79 @@ int main(int argc, char **argv)
 			break;
 
 		switch (optc) {
-		case 'h':
-			print_help();
-			return EXIT_SUCCESS;
-		case 'v':
-			print_version();
-			return EXIT_SUCCESS;
+		case 'a':
+			cfg->direction = GPIOD_LINE_DIRECTION_AS_IS;
+			break;
+		case 'b':
+			cfg->bias = parse_bias_or_die(optarg);
+			break;
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
 		case 'l':
-			active_low = true;
+			cfg->active_low = true;
 			break;
-		case 'n':
-			direction = GPIOD_LINE_DIRECTION_AS_IS;
+		case 'p':
+			cfg->hold_period_us = parse_period_or_die(optarg);
 			break;
-		case 'B':
-			bias = parse_bias(optarg);
+		case 's':
+			cfg->strict = true;
 			break;
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'v':
+			print_version();
+			exit(EXIT_SUCCESS);
 		case '?':
 			die("try %s --help", get_progname());
+		case 0:
+			break;
 		default:
 			abort();
 		}
 	}
 
-	argc -= optind;
-	argv += optind;
+	return optind;
+}
 
-	if (argc < 1)
-		die("gpiochip must be specified");
+int main(int argc, char **argv)
+{
+	int i, num_lines, ret, *values;
+	struct gpiod_request_config *req_cfg;
+	struct gpiod_line_request *request;
+	struct gpiod_line_config *line_cfg;
+	struct gpiod_chip *chip;
+	unsigned int *offsets;
+	struct line_resolver *resolver;
+	struct resolved_line *line;
+	const char *chip_path;
+	struct config cfg;
+
+	i = parse_config(argc, argv, &cfg);
+	argc -= i;
+	argv += i;
 
-	if (argc < 2)
-		die("at least one GPIO line offset must be specified");
+	if (argc < 1)
+		die("at least one GPIO line must be specified");
 
-	device = argv[0];
-	num_lines = argc - 1;
+	resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict, by_name);
 
-	offsets = calloc(num_lines, sizeof(*offsets));
-	values = calloc(num_lines, sizeof(*values));
+	offsets = calloc(resolver->num_lines, sizeof(*offsets));
+	values = calloc(resolver->num_lines, sizeof(*values));
 	if (!offsets || ! values)
 		die("out of memory");
 
-	for (i = 0; i < num_lines; i++) {
-		offsets[i] = strtoul(argv[i + 1], &end, 10);
-		if (*end != '\0' || offsets[i] > INT_MAX)
-			die("invalid GPIO offset: %s", argv[i + 1]);
-	}
-
-	chip = chip_open_lookup(device);
-	if (!chip)
-		die_perror("unable to open %s", device);
-
 	line_cfg = gpiod_line_config_new();
 	if (!line_cfg)
 		die_perror("unable to allocate the line config structure");
 
-	gpiod_line_config_set_direction_default(line_cfg, direction);
+	gpiod_line_config_set_direction_default(line_cfg, cfg.direction);
 
-	if (bias)
-		gpiod_line_config_set_bias_default(line_cfg, bias);
+	if (cfg.bias)
+		gpiod_line_config_set_bias_default(line_cfg, cfg.bias);
 
-	if (active_low)
+	if (cfg.active_low)
 		gpiod_line_config_set_active_low_default(line_cfg, true);
 
 	req_cfg = gpiod_request_config_new();
@@ -124,27 +155,44 @@ int main(int argc, char **argv)
 		die_perror("unable to allocate the request config structure");
 
 	gpiod_request_config_set_consumer(req_cfg, "gpioget");
-	gpiod_request_config_set_offsets(req_cfg, num_lines, offsets);
+	for (i = 0; i < resolver->num_chips; i++) {
+		chip_path = resolver->chip_paths[i];
+		chip = gpiod_chip_open(chip_path);
+		if (!chip)
+			die_perror("unable to open chip %s", chip_path);
+		num_lines = get_line_offsets_and_values(resolver, chip_path, offsets, NULL);
+		gpiod_request_config_set_offsets(req_cfg, num_lines, offsets);
+
+		request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+		if (!request)
+			die_perror("unable to request lines");
 
-	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
-	if (!request)
-		die_perror("unable to request lines");
+		if (cfg.hold_period_us)
+			usleep(cfg.hold_period_us);
 
-	ret = gpiod_line_request_get_values(request, values);
-	if (ret)
-		die_perror("unable to read GPIO line values");
+		ret = gpiod_line_request_get_values(request, values);
+		if (ret)
+			die_perror("unable to read GPIO line values");
 
-	for (i = 0; i < num_lines; i++) {
-		printf("%d", values[i]);
-		if (i != num_lines - 1)
+		set_line_values(resolver, chip_path, values);
+
+		gpiod_line_request_release(request);
+		gpiod_chip_close(chip);
+	}
+	for (i = 0; i < resolver->num_lines; i++) {
+		line = &resolver->lines[i];
+		if (numeric)
+			printf("%d", line->value);
+		else
+			printf("%s=%s", line->id, line->value ? "active" : "inactive");
+		if (i != resolver->num_lines - 1)
 			printf(" ");
 	}
 	printf("\n");
 
-	gpiod_line_request_release(request);
+	free_line_resolver(resolver);
 	gpiod_request_config_free(req_cfg);
 	gpiod_line_config_free(line_cfg);
-	gpiod_chip_close(chip);
 	free(offsets);
 	free(values);
 
diff --git a/tools/gpioinfo.c b/tools/gpioinfo.c
index fbe2a13..a7f43d1 100644
--- a/tools/gpioinfo.c
+++ b/tools/gpioinfo.c
@@ -1,7 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
 
-#include <dirent.h>
 #include <errno.h>
 #include <getopt.h>
 #include <gpiod.h>
@@ -12,181 +11,142 @@
 
 #include "tools-common.h"
 
-typedef bool (*is_set_func)(struct gpiod_line_info *);
-
-struct flag {
-	const char *name;
-	is_set_func is_set;
+static int by_name;
+static const struct option longopts[] = {
+	{ "by-name",	no_argument,		&by_name,	1 },
+	{ "chip",	required_argument,	NULL,	'c' },
+	{ "help",	no_argument,		NULL,	'h' },
+	{ "strict",	no_argument,		NULL,	's' },
+	{ "version",	no_argument,		NULL,	'v' },
+	{ GETOPT_NULL_LONGOPT },
 };
 
-static bool line_bias_is_pullup(struct gpiod_line_info *info)
-{
-	return gpiod_line_info_get_bias(info) == GPIOD_LINE_BIAS_PULL_UP;
-}
+static const char *const shortopts = "+c:hsv";
 
-static bool line_bias_is_pulldown(struct gpiod_line_info *info)
-{
-	return gpiod_line_info_get_bias(info) == GPIOD_LINE_BIAS_PULL_DOWN;
-}
-
-static bool line_bias_is_disabled(struct gpiod_line_info *info)
+static void print_help(void)
 {
-	return gpiod_line_info_get_bias(info) == GPIOD_LINE_BIAS_DISABLED;
+	printf("Usage: %s [OPTIONS] [line] ...\n", get_progname());
+	printf("\n");
+	printf("Print information about GPIO lines.\n");
+	printf("\n");
+	printf("Lines are specified by name, or optionally by offset if the chip option\n");
+	printf("is provided.\n");
+	printf("\n");
+	printf("If no lines are specified than all lines are displayed.\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -s, --strict\t\tcheck all lines - don't assume line names are unique\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	print_chip_help();
 }
 
-static bool line_drive_is_open_drain(struct gpiod_line_info *info)
-{
-	return gpiod_line_info_get_drive(info) == GPIOD_LINE_DRIVE_OPEN_DRAIN;
-}
+struct config {
+	bool strict;
+	const char *chip_id;
+};
 
-static bool line_drive_is_open_source(struct gpiod_line_info *info)
+int parse_config(int argc, char **argv, struct config *cfg)
 {
-	return gpiod_line_info_get_drive(info) == GPIOD_LINE_DRIVE_OPEN_SOURCE;
-}
+	int opti, optc;
 
-static const struct flag flags[] = {
-	{
-		.name = "used",
-		.is_set = gpiod_line_info_is_used,
-	},
-	{
-		.name = "open-drain",
-		.is_set = line_drive_is_open_drain,
-	},
-	{
-		.name = "open-source",
-		.is_set = line_drive_is_open_source,
-	},
-	{
-		.name = "pull-up",
-		.is_set = line_bias_is_pullup,
-	},
-	{
-		.name = "pull-down",
-		.is_set = line_bias_is_pulldown,
-	},
-	{
-		.name = "bias-disabled",
-		.is_set = line_bias_is_disabled,
-	},
-};
+	memset(cfg, 0, sizeof(*cfg));
 
-static const struct option longopts[] = {
-	{ "help",	no_argument,	NULL,	'h' },
-	{ "version",	no_argument,	NULL,	'v' },
-	{ GETOPT_NULL_LONGOPT },
-};
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
 
-static const char *const shortopts = "+hv";
+		switch (optc) {
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
+		case 's':
+			cfg->strict = true;
+			break;
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'v':
+			print_version();
+			exit(EXIT_SUCCESS);
+		case '?':
+			die("try %s --help", get_progname());
+		case 0:
+			break;
+		default:
+			abort();
+		}
+	}
 
-static void print_help(void)
-{
-	printf("Usage: %s [OPTIONS] <gpiochip1> ...\n", get_progname());
-	printf("\n");
-	printf("Print information about all lines of the specified GPIO chip(s) (or all gpiochips if none are specified).\n");
-	printf("\n");
-	printf("Options:\n");
-	printf("  -h, --help:\t\tdisplay this message and exit\n");
-	printf("  -v, --version:\tdisplay the version and exit\n");
+	return optind;
 }
 
-static PRINTF(3, 4) void prinfo(bool *of,
-				unsigned int prlen, const char *fmt, ...)
+static bool filter_line(struct gpiod_line_info *info, const char *chip_path,
+			struct line_resolver *resolver)
 {
-	char *buf, *buffmt = NULL;
-	size_t len;
-	va_list va;
-	int rv;
-
-	va_start(va, fmt);
-	rv = vasprintf(&buf, fmt, va);
-	va_end(va);
-	if (rv < 0)
-		die("vasprintf: %s\n", strerror(errno));
-
-	len = strlen(buf) - 1;
-
-	if (len >= prlen || *of) {
-		*of = true;
-		printf("%s", buf);
-	} else {
-		rv = asprintf(&buffmt, "%%%us", prlen);
-		if (rv < 0)
-			die("asprintf: %s\n", strerror(errno));
-
-		printf(buffmt, buf);
+	int i;
+	struct resolved_line *line;
+	bool skip = true;
+	const char *name;
+
+	if (!resolver)
+		return false;
+
+	for (i = 0; i < resolver->num_lines; i++) {
+		line = &resolver->lines[i];
+		name = gpiod_line_info_get_name(info);
+		if (((line->offset == (int)gpiod_line_info_get_offset(info)) ||
+		     (name && strcmp(line->id, name) == 0)) &&
+		    (resolver->strict || !line->chip_path)) {
+			skip = false;
+			line->chip_path = chip_path;
+			resolver->num_found++;
+		}
 	}
+	return skip;
+}
 
-	free(buf);
-	if (fmt)
-		free(buffmt);
+static bool filter_done(struct line_resolver *resolver)
+{
+	return (resolver && !resolver->strict && resolver->num_found >= resolver->num_lines);
 }
 
-static void list_lines(struct gpiod_chip *chip)
+static void list_lines(struct gpiod_chip *chip, const char *chip_path,
+		       struct line_resolver *resolver)
 {
-	bool flag_printed, of, active_low;
 	struct gpiod_chip_info *chip_info;
 	struct gpiod_line_info *info;
-	const char *name, *consumer;
-	size_t i, offset, num_lines;
-	int direction;
+	int offset, num_lines;
 
 	chip_info = gpiod_chip_get_info(chip);
 	if (!chip_info)
-		die_perror("unable to retrieve the chip info from chip");
+		die_perror("unable to retrieve the chip info from chip %s",
+			   gpiod_chip_get_path(chip));
 
 	num_lines = gpiod_chip_info_get_num_lines(chip_info);
-	printf("%s - %zu lines:\n",
-	       gpiod_chip_info_get_name(chip_info), num_lines);
 
-	for (offset = 0; offset < num_lines; offset++) {
+	for (offset = 0; ((offset < num_lines) && !filter_done(resolver)); offset++) {
 		info = gpiod_chip_get_line_info(chip, offset);
 		if (!info)
-			die_perror("unable to retrieve the line info from chip");
-		name = gpiod_line_info_get_name(info);
-		consumer = gpiod_line_info_get_consumer(info);
-		direction = gpiod_line_info_get_direction(info);
-		active_low = gpiod_line_info_is_active_low(info);
-
-		of = false;
-
-		printf("\tline ");
-		prinfo(&of, 3, "%zu", offset);
-		printf(": ");
-
-		name ? prinfo(&of, 12, "\"%s\"", name)
-		     : prinfo(&of, 12, "unnamed");
-		printf(" ");
-
-		if (!gpiod_line_info_is_used(info))
-			prinfo(&of, 12, "unused");
-		else
-			consumer ? prinfo(&of, 12, "\"%s\"", consumer)
-				 : prinfo(&of, 12, "kernel");
-
-		printf(" ");
-
-		prinfo(&of, 8, "%s ", direction == GPIOD_LINE_DIRECTION_INPUT
-							? "input" : "output");
-		prinfo(&of, 13, "%s ",
-		       active_low ? "active-low" : "active-high");
-
-		flag_printed = false;
-		for (i = 0; i < ARRAY_SIZE(flags); i++) {
-			if (flags[i].is_set(info)) {
-				if (flag_printed)
-					printf(" ");
-				else
-					printf("[");
-				printf("%s", flags[i].name);
-				flag_printed = true;
-			}
+			die_perror("unable to retrieve the line info from chip %s",
+				   gpiod_chip_get_path(chip));
+
+		if (filter_line(info, chip_path, resolver))
+			continue;
+
+		if (resolver && resolver->num_lines) {
+			printf("%s %u", gpiod_chip_info_get_name(chip_info), offset);
+		} else {
+			if (offset == 0)
+				printf("%s - %u lines:\n",
+				       gpiod_chip_info_get_name(chip_info), num_lines);
+			printf("\tline %3u:", offset);
 		}
-		if (flag_printed)
-			printf("]");
-
+		print_line_info(info);
 		printf("\n");
-
 		gpiod_line_info_free(info);
 	}
 	gpiod_chip_info_free(chip_info);
@@ -194,61 +154,69 @@ static void list_lines(struct gpiod_chip *chip)
 
 int main(int argc, char **argv)
 {
-	int num_chips, i, optc, opti;
+	int num_chips, i, ret = EXIT_SUCCESS;
 	struct gpiod_chip *chip;
-	struct dirent **entries;
-
-	for (;;) {
-		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
-		if (optc < 0)
-			break;
-
-		switch (optc) {
-		case 'h':
-			print_help();
-			return EXIT_SUCCESS;
-		case 'v':
-			print_version();
-			return EXIT_SUCCESS;
-		case '?':
-			die("try %s --help", get_progname());
-		default:
-			abort();
+	char **paths;
+	struct line_resolver *resolver = NULL;
+	struct resolved_line *line;
+	struct config cfg;
+
+	i = parse_config(argc, argv, &cfg);
+	argc -= i;
+	argv += i;
+
+	if (!cfg.chip_id)
+		by_name = true;
+
+	num_chips = chip_paths(cfg.chip_id, &paths);
+	if ((cfg.chip_id != NULL)  && (num_chips == 0))
+		die("cannot find a GPIO chip character device corresponding to %s", cfg.chip_id);
+
+	if (argc) {
+		resolver = malloc(sizeof(*resolver) + argc * sizeof(struct resolved_line));
+		if (resolver == NULL)
+			die("out of memory");
+		resolver->num_lines = argc;
+		resolver->num_found = 0;
+		resolver->strict = cfg.strict;
+		for (i = 0; i < argc; i++) {
+			line = &resolver->lines[i];
+			line->id = argv[i];
+			line->chip_path = NULL;  // doubles as found flag
+			line->offset = by_name ? -1 : parse_uint(argv[i]);
+			line->id_is_name = (line->offset == -1);
 		}
 	}
 
-	argc -= optind;
-	argv += optind;
-
-	if (argc == 0) {
-		num_chips = scandir("/dev/", &entries,
-				    chip_dir_filter, alphasort);
-		if (num_chips < 0)
-			die_perror("unable to scan /dev");
-
-		for (i = 0; i < num_chips; i++) {
-			chip = chip_open_by_name(entries[i]->d_name);
-			if (!chip)
-				die_perror("unable to open %s",
-					   entries[i]->d_name);
-
-			list_lines(chip);
-
+	for (i = 0; i < num_chips; i++) {
+		chip = gpiod_chip_open(paths[i]);
+		if (chip) {
+			list_lines(chip, paths[i], resolver);
 			gpiod_chip_close(chip);
-			free(entries[i]);
+		} else {
+			print_perror("unable to open chip %s", paths[i]);
+			ret = EXIT_FAILURE;
+			if (cfg.chip_id)
+				return EXIT_FAILURE;
 		}
-		free(entries);
-	} else {
-		for (i = 0; i < argc; i++) {
-			chip = chip_open_lookup(argv[i]);
-			if (!chip)
-				die_perror("looking up chip %s", argv[i]);
-
-			list_lines(chip);
-
-			gpiod_chip_close(chip);
+		free(paths[i]);
+	}
+	free(paths);
+
+	if (resolver) {
+		for (i = 0; i < resolver->num_lines; i++) {
+			if (resolver->lines[i].chip_path)
+				continue;
+			if (cfg.chip_id && !resolver->lines[i].id_is_name)
+				print_error("offset %s is out of range on chip %s",
+					resolver->lines[i].id, cfg.chip_id);
+			else
+				print_error("cannot find line %s", resolver->lines[i].id);
+			ret = EXIT_FAILURE;
 		}
+		if (resolver->num_lines != resolver->num_found)
+			ret = EXIT_FAILURE;
+		free(resolver);
 	}
-
-	return EXIT_SUCCESS;
+	return ret;
 }
diff --git a/tools/gpiomon.c b/tools/gpiomon.c
index f6a0dba..488eee1 100644
--- a/tools/gpiomon.c
+++ b/tools/gpiomon.c
@@ -1,79 +1,196 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
 
-#include <errno.h>
 #include <getopt.h>
 #include <gpiod.h>
 #include <inttypes.h>
-#include <limits.h>
 #include <poll.h>
-#include <signal.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <unistd.h>
 
 #include "tools-common.h"
 
 #define EVENT_BUF_SIZE 32
 
+static int by_name;
+static int event_clock_mode;
+static int banner;
 static const struct option longopts[] = {
-	{ "help",		no_argument,		NULL,	'h' },
-	{ "version",		no_argument,		NULL,	'v' },
 	{ "active-low",		no_argument,		NULL,	'l' },
-	{ "bias",		required_argument,	NULL,	'B' },
-	{ "num-events",		required_argument,	NULL,	'n' },
-	{ "silent",		no_argument,		NULL,	's' },
-	{ "rising-edge",	no_argument,		NULL,	'r' },
-	{ "falling-edge",	no_argument,		NULL,	'f' },
-	{ "line-buffered",	no_argument,		NULL,	'b' },
+	{ "banner",		no_argument,		&banner,	1 },
+	{ "bias",		required_argument,	NULL,	'b' },
+	{ "by-name",		no_argument,		&by_name,	1 },
+	{ "chip",		required_argument,	NULL,	'c' },
+	{ "debounce-period",	required_argument,	NULL,	'p' },
+	{ "edge",		required_argument,	NULL,	'e' },
 	{ "format",		required_argument,	NULL,	'F' },
+	{ "help",		no_argument,		NULL,	'h' },
+	{ "localtime",		no_argument,		&event_clock_mode,	2 },
+	{ "num-events",		required_argument,	NULL,	'n' },
+	{ "quiet",		no_argument,		NULL,	'q' },
+	{ "silent",		no_argument,		NULL,	'q' },
+	{ "strict",		no_argument,		NULL,	's' },
+	{ "utc",		no_argument,		&event_clock_mode,	1 },
+	{ "version",		no_argument,		NULL,	'v' },
 	{ GETOPT_NULL_LONGOPT },
 };
 
-static const char *const shortopts = "+hvlB:n:srfbF:";
+static const char *const shortopts = "+b:c:e:hF:ln:p:qshv";
 
 static void print_help(void)
 {
-	printf("Usage: %s [OPTIONS] <chip name/number> <offset 1> <offset 2> ...\n",
-	       get_progname());
+	printf("Usage: %s [OPTIONS] <line> ...\n", get_progname());
 	printf("\n");
-	printf("Wait for events on GPIO lines and print them to standard output\n");
+	printf("Wait for events on GPIO lines and print them to standard output.\n");
 	printf("\n");
-	printf("Options:\n");
-	printf("  -h, --help:\t\tdisplay this message and exit\n");
-	printf("  -v, --version:\tdisplay the version and exit\n");
-	printf("  -l, --active-low:\tset the line active state to low\n");
-	printf("  -B, --bias=[as-is|disable|pull-down|pull-up] (defaults to 'as-is'):\n");
-	printf("		set the line bias\n");
-	printf("  -n, --num-events=NUM:\texit after processing NUM events\n");
-	printf("  -s, --silent:\t\tdon't print event info\n");
-	printf("  -r, --rising-edge:\tonly process rising edge events\n");
-	printf("  -f, --falling-edge:\tonly process falling edge events\n");
-	printf("  -b, --line-buffered:\tset standard output as line buffered\n");
-	printf("  -F, --format=FMT\tspecify custom output format\n");
+	printf("Lines are specified by name, or optionally by offset if the chip option\n");
+	printf("is provided.\n");
 	printf("\n");
+	printf("Options:\n");
+	printf("      --banner\t\tdisplay a banner on successful startup\n");
 	print_bias_help();
+	printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -e, --edge <edge>\tspecify the edges to monitor.\n");
+	printf("\t\t\tPossible values: 'falling', 'rising', 'both'.\n");
+	printf("\t\t\t(default is 'both')\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -F, --format <fmt>\tspecify a custom output format\n");
+	printf("  -l, --active-low\ttreat the line as active low, flipping the sense of\n");
+	printf("\t\t\trising and falling edges\n");
+	printf("      --localtime\treport event time as a local time (default is monotonic)\n");
+	printf("  -n, --num-events <num>\n");
+	printf("\t\t\texit after processing num events\n");
+	printf("  -p, --debounce-period <period>\n");
+	printf("\t\t\tdebounce the line(s) with the specified period\n");
+	printf("  -q, --quiet\t\tdon't generate any output\n");
+	printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+	printf("      --utc\t\treport event time as UTC (default is monotonic)\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	print_chip_help();
+	print_period_help();
 	printf("\n");
 	printf("Format specifiers:\n");
-	printf("  %%o:  GPIO line offset\n");
-	printf("  %%e:  event type (0 - falling edge, 1 rising edge)\n");
-	printf("  %%s:  seconds part of the event timestamp\n");
-	printf("  %%n:  nanoseconds part of the event timestamp\n");
+	printf("  %%o   GPIO line offset\n");
+	printf("  %%l   GPIO line name\n");
+	printf("  %%c   GPIO chip path\n");
+	printf("  %%e   numeric event type ('0' - falling edge or '1' - rising edge)\n");
+	printf("  %%E   event type (falling or rising)\n");
+	printf("  %%s   seconds part of the event timestamp\n");
+	printf("  %%n   nanoseconds part of the event timestamp\n");
+	printf("  %%T   event timestamp as datetime (UTC if --utc or local time if --localtime)\n");
 }
 
-struct mon_ctx {
-	unsigned int offset;
-	bool silent;
-	char *fmt;
+static int parse_edge_or_die(const char *option)
+{
+	if (strcmp(option, "rising") == 0)
+		return GPIOD_LINE_EDGE_RISING;
+	if (strcmp(option, "falling") == 0)
+		return GPIOD_LINE_EDGE_FALLING;
+	if (strcmp(option, "both") != 0)
+		die("invalid edge: %s", option);
+	return GPIOD_LINE_EDGE_BOTH;
+}
+
+struct config {
+	bool active_low;
+	bool quiet;
+	bool strict;
+	int bias;
+	int edge;
+	int events_wanted;
+	unsigned int debounce_period_us;
+	const char *chip_id;
+	const char *fmt;
 };
 
-static void event_print_custom(unsigned int offset, uint64_t timeout,
-			       int event_type, struct mon_ctx *ctx)
+int parse_config(int argc, char **argv, struct config *cfg)
+{
+	int opti, optc;
+
+	memset(cfg, 0, sizeof(*cfg));
+	cfg->edge = GPIOD_LINE_EDGE_BOTH;
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'b':
+			cfg->bias = parse_bias_or_die(optarg);
+			break;
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
+		case 'e':
+			cfg->edge = parse_edge_or_die(optarg);
+			break;
+		case 'F':
+			cfg->fmt = optarg;
+			break;
+		case 'l':
+			cfg->active_low = true;
+			break;
+		case 'n':
+			cfg->events_wanted = parse_uint_or_die(optarg);
+			break;
+		case 'p':
+			cfg->debounce_period_us = parse_period_or_die(optarg);
+			break;
+		case 'q':
+			cfg->quiet = true;
+			break;
+		case 's':
+			cfg->strict = true;
+			break;
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'v':
+			print_version();
+			exit(EXIT_SUCCESS);
+		case '?':
+			die("try %s --help", get_progname());
+		case 0:
+			break;
+		default:
+			abort();
+		}
+	}
+
+	return optind;
+}
+
+static void print_banner(int num_lines, char **lines)
 {
-	char *prev, *curr, fmt;
+	int i;
+
+	if (num_lines > 1) {
+		printf("Monitoring lines ");
+		for (i = 0; i < num_lines - 1; i++)
+			printf("%s, ", lines[i]);
+		printf("and %s...\n", lines[i]);
+	} else {
+		printf("Monitoring line %s ...\n", lines[0]);
+	}
+}
+
+static void event_print_custom(struct gpiod_edge_event *event, const char *chip_path,
+			       struct line_resolver *resolver, const char *evt_fmt)
+{
+	const char *lname, *prev, *curr;
+	char  fmt;
+	uint64_t evtime;
+	int evtype;
+	unsigned int offset;
+
+	offset = gpiod_edge_event_get_line_offset(event);
+	evtime = gpiod_edge_event_get_timestamp_ns(event);
+	evtype = gpiod_edge_event_get_event_type(event);
 
-	for (prev = curr = ctx->fmt;;) {
+	for (prev = curr = evt_fmt;;) {
 		curr = strchr(curr, '%');
 		if (!curr) {
 			fputs(prev, stdout);
@@ -86,20 +203,38 @@ static void event_print_custom(unsigned int offset, uint64_t timeout,
 		fmt = *(curr + 1);
 
 		switch (fmt) {
-		case 'o':
-			printf("%u", offset);
+		case 'c':
+			printf("%s", chip_path);
 			break;
 		case 'e':
-			if (event_type == GPIOD_EDGE_EVENT_RISING_EDGE)
+			if (evtype == GPIOD_EDGE_EVENT_RISING_EDGE)
 				fputc('1', stdout);
 			else
 				fputc('0', stdout);
 			break;
-		case 's':
-			printf("%"PRIu64, timeout / 1000000000);
+		case 'E':
+			if (evtype == GPIOD_EDGE_EVENT_RISING_EDGE)
+				fputs("rising", stdout);
+			else
+				fputs("falling", stdout);
+			break;
+		case 'l':
+			lname = get_line_name(resolver, chip_path, offset);
+			if (!lname)
+				lname = "??";
+			printf("%s", lname);
+			break;
+		case 'o':
+			printf("%u", offset);
 			break;
 		case 'n':
-			printf("%"PRIu64, timeout % 1000000000);
+			printf("%"PRIu64, evtime % 1000000000);
+			break;
+		case 's':
+			printf("%"PRIu64, evtime / 1000000000);
+			break;
+		case 'T':
+			print_event_time(evtime, event_clock_mode);
 			break;
 		case '%':
 			fputc('%', stdout);
@@ -120,201 +255,161 @@ end:
 	fputc('\n', stdout);
 }
 
-static void event_print_human_readable(unsigned int offset,
-				       uint64_t timeout, int event_type)
+static void event_print_human_readable(struct gpiod_edge_event *event,
+				       const char *chip_id,
+				       const char *chip_path,
+				       struct line_resolver *resolver)
 {
+	unsigned int offset;
+	uint64_t evtime;
 	char *evname;
+	const char *lname;
 
-	if (event_type == GPIOD_EDGE_EVENT_RISING_EDGE)
-		evname = " RISING EDGE";
-	else
-		evname = "FALLING EDGE";
+	offset = gpiod_edge_event_get_line_offset(event);
+	evtime = gpiod_edge_event_get_timestamp_ns(event);
 
-	printf("event: %s offset: %u timestamp: [%8"PRIu64".%09"PRIu64"]\n",
-	       evname, offset, timeout / 1000000000, timeout % 1000000000);
-}
-
-static void handle_event(unsigned int line_offset, unsigned int event_type,
-			 uint64_t timestamp, struct mon_ctx *ctx)
-{
-	if (!ctx->silent) {
-		if (ctx->fmt)
-			event_print_custom(line_offset, timestamp,
-					   event_type, ctx);
+	if (gpiod_edge_event_get_event_type(event) == GPIOD_EDGE_EVENT_RISING_EDGE)
+		evname = "RISING ";
+	else
+		evname = "FALLING";
+	lname = get_line_name(resolver, chip_path, offset);
+	print_event_time(evtime, event_clock_mode);
+	if (lname)
+		if (chip_id)
+			printf(" %s chip: %s offset: %u name: %s\n",
+			       evname, chip_id, offset, lname);
 		else
-			event_print_human_readable(line_offset,
-						   timestamp, event_type);
-	}
+			printf(" %s %s\n", evname, lname);
+	else
+		printf(" %s chip: %s offset: %u\n", evname, chip_id, offset);
 }
 
-static void handle_signal(int signum UNUSED)
+static void event_print(struct gpiod_edge_event *event, const char *chip_id,
+			const char *chip_path,	struct line_resolver *resolver,
+			const char *fmt)
 {
-	exit(EXIT_SUCCESS);
+	if (fmt)
+		event_print_custom(event, chip_path, resolver, fmt);
+	else
+		event_print_human_readable(event, chip_id, chip_path, resolver);
 }
 
 int main(int argc, char **argv)
 {
-	bool watch_rising = false, watch_falling = false, active_low = false;
-	size_t num_lines = 0, events_wanted = 0, events_done = 0;
+	int num_lines, events_done = 0;
 	struct gpiod_edge_event_buffer *event_buffer;
-	int optc, opti, ret, i, edge, bias = 0;
-	uint64_t timeout = 10 * 1000000000LLU;
+	int ret, i, j;
 	struct gpiod_request_config *req_cfg;
-	struct gpiod_line_request *request;
+	struct gpiod_line_request **requests;
+	struct pollfd *pollfds;
 	struct gpiod_line_config *line_cfg;
-	unsigned int offsets[64], offset;
+	unsigned int *offsets;
 	struct gpiod_edge_event *event;
 	struct gpiod_chip *chip;
-	struct mon_ctx ctx;
-	char *end;
-
-	/*
-	 * FIXME: use signalfd once the API has been converted to using a single file
-	 * descriptor as provided by uAPI v2.
-	 */
-	signal(SIGINT, handle_signal);
-	signal(SIGTERM, handle_signal);
-
-	memset(&ctx, 0, sizeof(ctx));
+	struct line_resolver *resolver;
+	const char *chip_path;
+	struct config cfg;
 
-	for (;;) {
-		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
-		if (optc < 0)
-			break;
-
-		switch (optc) {
-		case 'h':
-			print_help();
-			return EXIT_SUCCESS;
-		case 'v':
-			print_version();
-			return EXIT_SUCCESS;
-		case 'l':
-			active_low = true;
-			break;
-		case 'B':
-			bias = parse_bias(optarg);
-			break;
-		case 'n':
-			events_wanted = strtoul(optarg, &end, 10);
-			if (*end != '\0')
-				die("invalid number: %s", optarg);
-			break;
-		case 's':
-			ctx.silent = true;
-			break;
-		case 'r':
-			watch_rising = true;
-			break;
-		case 'f':
-			watch_falling = true;
-			break;
-		case 'b':
-			setlinebuf(stdout);
-			break;
-		case 'F':
-			ctx.fmt = optarg;
-			break;
-		case '?':
-			die("try %s --help", get_progname());
-		default:
-			abort();
-		}
-	}
-
-	argc -= optind;
-	argv += optind;
-
-	if (watch_rising && !watch_falling)
-		edge = GPIOD_LINE_EDGE_RISING;
-	else if (watch_falling && !watch_rising)
-		edge = GPIOD_LINE_EDGE_FALLING;
-	else
-		edge = GPIOD_LINE_EDGE_BOTH;
+	i = parse_config(argc, argv, &cfg);
+	argc -= i;
+	argv += i;
 
 	if (argc < 1)
-		die("gpiochip must be specified");
-
-	if (argc < 2)
-		die("at least one GPIO line offset must be specified");
-
-	if (argc > 65)
-		die("too many offsets given");
-
-	for (i = 1; i < argc; i++) {
-		offset = strtoul(argv[i], &end, 10);
-		if (*end != '\0' || offset > INT_MAX)
-			die("invalid GPIO offset: %s", argv[i]);
-
-		offsets[i - 1] = offset;
-		num_lines++;
-	}
+		die("at least one GPIO line must be specified");
 
-	chip = chip_open_lookup(argv[0]);
-	if (!chip)
-		die_perror("unable to open %s", argv[0]);
+	if (argc > 64)
+		die("too many lines given");
 
 	line_cfg = gpiod_line_config_new();
 	if (!line_cfg)
 		die_perror("unable to allocate the line config structure");
 
-	if (bias)
-		gpiod_line_config_set_bias_default(line_cfg, bias);
-	if (active_low)
+	if (cfg.bias)
+		gpiod_line_config_set_bias_default(line_cfg, cfg.bias);
+	if (cfg.active_low)
 		gpiod_line_config_set_active_low_default(line_cfg, true);
-	gpiod_line_config_set_edge_detection_default(line_cfg, edge);
+	if (cfg.debounce_period_us)
+		gpiod_line_config_set_debounce_period_us_default(line_cfg, cfg.debounce_period_us);
+	if (event_clock_mode)
+		gpiod_line_config_set_event_clock_default(line_cfg,
+							  GPIOD_LINE_EVENT_CLOCK_REALTIME);
+	gpiod_line_config_set_edge_detection_default(line_cfg, cfg.edge);
 
 	req_cfg = gpiod_request_config_new();
 	if (!req_cfg)
 		die_perror("unable to allocate the request config structure");
 
 	gpiod_request_config_set_consumer(req_cfg, "gpiomon");
-	gpiod_request_config_set_offsets(req_cfg, num_lines, offsets);
-
-	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
-	if (!request)
-		die_perror("unable to request lines");
 
 	event_buffer = gpiod_edge_event_buffer_new(EVENT_BUF_SIZE);
 	if (!event_buffer)
 		die_perror("unable to allocate the line event buffer");
 
+	resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict, by_name);
+	requests = calloc(resolver->num_chips, sizeof(*requests));
+	pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
+	offsets = calloc(resolver->num_lines, sizeof(*offsets));
+	if (!requests || !pollfds || !offsets)
+		die("out of memory");
+	for (i = 0; i < resolver->num_chips; i++) {
+		chip_path = resolver->chip_paths[i];
+		num_lines = get_line_offsets_and_values(resolver, chip_path, offsets, NULL);
+		gpiod_request_config_set_offsets(req_cfg, num_lines, offsets);
+
+		chip = gpiod_chip_open(chip_path);
+		if (!chip)
+			die_perror("unable to open chip %s", chip_path);
+
+		requests[i] = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+		if (!requests[i])
+			die_perror("unable to request lines on chip %s", chip_path);
+
+		pollfds[i].fd = gpiod_line_request_get_fd(requests[i]);
+		pollfds[i].events = POLLIN;
+		gpiod_chip_close(chip);
+	}
+	gpiod_request_config_free(req_cfg);
+	gpiod_line_config_free(line_cfg);
+
+	if (banner)
+		print_banner(argc, argv);
+
 	for (;;) {
-		ret = gpiod_line_request_wait_edge_event(request, timeout);
-		if (ret < 0)
-			die_perror("error waiting for events");
-		if (ret == 0)
-			continue;
-
-		ret = gpiod_line_request_read_edge_event(request, event_buffer,
-							 EVENT_BUF_SIZE);
-		if (ret < 0)
-			die_perror("error reading line events");
-
-		for (i = 0; i < ret; i++) {
-			event = gpiod_edge_event_buffer_get_event(event_buffer,
-								  i);
-			if (!event)
-				die_perror("unable to retrieve the event from the buffer");
-
-			handle_event(gpiod_edge_event_get_line_offset(event),
-				     gpiod_edge_event_get_event_type(event),
-				     gpiod_edge_event_get_timestamp_ns(event),
-				     &ctx);
-
-			events_done++;
-
-			if (events_wanted && events_done >= events_wanted)
-				goto done;
+		if (poll(pollfds, resolver->num_chips, -1) < 0)
+			die_perror("error polling for events");
+
+		for (i = 0; i < resolver->num_chips; i++) {
+			if (pollfds[i].revents == 0)
+				continue;
+
+			ret = gpiod_line_request_read_edge_event(requests[i], event_buffer,
+								 EVENT_BUF_SIZE);
+			if (ret < 0)
+				die_perror("error reading line events");
+
+			for (j = 0; j < ret; j++) {
+				event = gpiod_edge_event_buffer_get_event(event_buffer, j);
+				if (!event)
+					die_perror("unable to retrieve event from buffer");
+
+				if (!cfg.quiet)
+					event_print(event, cfg.chip_id, resolver->chip_paths[i],
+						    resolver, cfg.fmt);
+
+				events_done++;
+
+				if (cfg.events_wanted && events_done >= cfg.events_wanted)
+					goto done;
+			}
 		}
 	}
-
 done:
+	for (i = 0; i < resolver->num_chips; i++)
+		gpiod_line_request_release(requests[i]);
+	free(requests);
+	free_line_resolver(resolver);
 	gpiod_edge_event_buffer_free(event_buffer);
-	gpiod_line_request_release(request);
-	gpiod_request_config_free(req_cfg);
-	gpiod_line_config_free(line_cfg);
-	gpiod_chip_close(chip);
+	free(offsets);
 
 	return EXIT_SUCCESS;
 }
diff --git a/tools/gpioset.c b/tools/gpioset.c
index 576b87d..70f9fb7 100644
--- a/tools/gpioset.c
+++ b/tools/gpioset.c
@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
 
-#include <errno.h>
+#include <ctype.h>
 #include <gpiod.h>
 #include <getopt.h>
 #include <limits.h>
@@ -9,322 +9,723 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/select.h>
 #include <unistd.h>
+#include <readline/history.h>
+#include <readline/readline.h>
 
 #include "tools-common.h"
 
+static int by_name;
 static const struct option longopts[] = {
+	{ "active-low",		no_argument,		NULL,	'l' },
+	{ "bias",		required_argument,	NULL,	'b' },
+	{ "by-name",		no_argument,		&by_name,	1 },
+	{ "chip",		required_argument,	NULL,	'c' },
+	{ "daemonize",		no_argument,		NULL,	'z' },
+	{ "drive",		required_argument,	NULL,	'd' },
 	{ "help",		no_argument,		NULL,	'h' },
+	{ "hold-period",	required_argument,	NULL,	'p' },
+	{ "interactive",	no_argument,		NULL,	'i' },
+	{ "strict",		no_argument,		NULL,	's' },
+	{ "toggle",		required_argument,	NULL,	't' },
 	{ "version",		no_argument,		NULL,	'v' },
-	{ "active-low",		no_argument,		NULL,	'l' },
-	{ "bias",		required_argument,	NULL,	'B' },
-	{ "drive",		required_argument,	NULL,	'D' },
-	{ "mode",		required_argument,	NULL,	'm' },
-	{ "sec",		required_argument,	NULL,	's' },
-	{ "usec",		required_argument,	NULL,	'u' },
-	{ "background",		no_argument,		NULL,	'b' },
 	{ GETOPT_NULL_LONGOPT },
 };
 
-static const char *const shortopts = "+hvlB:D:m:s:u:b";
+static const char *const shortopts = "+b:c:d:hilp:st:vz";
 
 static void print_help(void)
 {
-	printf("Usage: %s [OPTIONS] <chip name/number> <offset1>=<value1> <offset2>=<value2> ...\n",
-	       get_progname());
-	printf("\n");
-	printf("Set GPIO line values of a GPIO chip and maintain the state until the process exits\n");
+	printf("Usage: %s [OPTIONS] <line>=<value> ...\n", get_progname());
 	printf("\n");
-	printf("Options:\n");
-	printf("  -h, --help:\t\tdisplay this message and exit\n");
-	printf("  -v, --version:\tdisplay the version and exit\n");
-	printf("  -l, --active-low:\tset the line active state to low\n");
-	printf("  -B, --bias=[as-is|disable|pull-down|pull-up] (defaults to 'as-is'):\n");
-	printf("		set the line bias\n");
-	printf("  -D, --drive=[push-pull|open-drain|open-source] (defaults to 'push-pull'):\n");
-	printf("		set the line drive mode\n");
-	printf("  -m, --mode=[exit|wait|time|signal] (defaults to 'exit'):\n");
-	printf("		tell the program what to do after setting values\n");
-	printf("  -s, --sec=SEC:\tspecify the number of seconds to wait (only valid for --mode=time)\n");
-	printf("  -u, --usec=USEC:\tspecify the number of microseconds to wait (only valid for --mode=time)\n");
-	printf("  -b, --background:\tafter setting values: detach from the controlling terminal\n");
+	printf("Set values of GPIO lines.\n");
 	printf("\n");
-	print_bias_help();
+	printf("Lines are specified by name, or optionally by offset if the chip option\n");
+	printf("is provided.\n");
+	printf("Values may be '1' or '0', or equivalently 'active'/'inactive' or 'on'/'off'.\n");
 	printf("\n");
-	printf("Drives:\n");
-	printf("  push-pull:\tdrive the line both high and low\n");
-	printf("  open-drain:\tdrive the line low or go high impedance\n");
-	printf("  open-source:\tdrive the line high or go high impedance\n");
+	printf("The line output state is maintained until the process exits, but after that\n");
+	printf("is not guaranteed.\n");
 	printf("\n");
-	printf("Modes:\n");
-	printf("  exit:\t\tset values and exit immediately\n");
-	printf("  wait:\t\tset values and wait for user to press ENTER\n");
-	printf("  time:\t\tset values and sleep for a specified amount of time\n");
-	printf("  signal:\tset values and wait for SIGINT or SIGTERM\n");
+	printf("Options:\n");
+	print_bias_help();
+	printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -d, --drive <drive>\tspecify the line drive mode.\n");
+	printf("\t\t\tPossible values: 'push-pull', 'open-drain', 'open-source'.\n");
+	printf("\t\t\t(default is 'push-pull')\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -i, --interactive\tset the lines then wait for additional set commands.\n");
+	printf("\t\t\tUse the 'help' command at the interactive prompt to get help\n");
+	printf("\t\t\tfor the supported commands.\n");
+	printf("  -l, --active-low\ttreat the line as active low\n");
+	printf("  -p, --hold-period <period>\n");
+	printf("\t\t\tthe minimum time period to hold lines at the requested values\n");
+	printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+	printf("  -t, --toggle <period>[,period]...\n");
+	printf("\t\t\ttoggle the line(s) after the specified period(s).\n");
+	printf("\t\t\tIf the last period is non-zero then the sequence repeats.\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	printf("  -z, --daemonize\tset values then detach from the controlling terminal\n");
+	print_chip_help();
+	print_period_help();
 	printf("\n");
-	printf("Note: the state of a GPIO line controlled over the character device reverts to default\n");
-	printf("when the last process referencing the file descriptor representing the device file exits.\n");
-	printf("This means that it's wrong to run gpioset, have it exit and expect the line to continue\n");
-	printf("being driven high or low. It may happen if given pin is floating but it must be interpreted\n");
-	printf("as undefined behavior.\n");
+	printf("*Note*\n");
+	printf("    The state of a GPIO line controlled over the character device reverts to default\n");
+	printf("    when the last process referencing the file descriptor representing the device file exits.\n");
+	printf("    This means that it's wrong to run gpioset, have it exit and expect the line to continue\n");
+	printf("    being driven high or low. It may happen if given pin is floating but it must be interpreted\n");
+	printf("    as undefined behavior.\n");
 }
 
-struct callback_data {
-	/* Replace with a union once we have more modes using callback data. */
-	struct timeval tv;
+static int parse_drive_or_die(const char *option)
+{
+	if (strcmp(option, "open-drain") == 0)
+		return GPIOD_LINE_DRIVE_OPEN_DRAIN;
+	if (strcmp(option, "open-source") == 0)
+		return GPIOD_LINE_DRIVE_OPEN_SOURCE;
+	if (strcmp(option, "push-pull") != 0)
+		die("invalid drive: %s", option);
+	return 0;
+}
+
+struct config {
+	bool active_low;
+	bool interactive;
+	bool strict;
 	bool daemonize;
+	int bias;
+	int drive;
+	int toggles;
+	unsigned int *toggle_periods;
+	unsigned int hold_period_us;
+	const char *chip_id;
 };
 
-static void maybe_daemonize(bool daemonize)
+int parse_config(int argc, char **argv, struct config *cfg)
 {
-	int rv;
+	int opti, optc;
 
-	if (daemonize) {
-		rv = daemon(0, 0);
-		if (rv < 0)
-			die("unable to daemonize: %s", strerror(errno));
+	memset(cfg, 0, sizeof(*cfg));
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'b':
+			cfg->bias = parse_bias_or_die(optarg);
+			break;
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
+		case 'd':
+			cfg->drive = parse_drive_or_die(optarg);
+			break;
+		case 'i':
+			cfg->interactive = true;
+			break;
+		case 'l':
+			cfg->active_low = true;
+			break;
+		case 'p':
+			cfg->hold_period_us = parse_period_or_die(optarg);
+			break;
+		case 's':
+			cfg->strict = true;
+			break;
+		case 't':
+			cfg->toggles = parse_periods_or_die(optarg, &cfg->toggle_periods);
+			break;
+		case 'z':
+			cfg->daemonize = true;
+			break;
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'v':
+			print_version();
+			exit(EXIT_SUCCESS);
+		case '?':
+			die("try %s --help", get_progname());
+		case 0:
+			break;
+		default:
+			abort();
+		}
 	}
+	if (cfg->daemonize && cfg->interactive)
+		die("can't combine daemonize with interactive");
+
+	if (cfg->toggles && cfg->interactive)
+		die("can't combine interactive with toggle");
+
+	return optind;
 }
 
-static void wait_enter(void *data UNUSED)
+static int parse_value(const char *option)
 {
-	getchar();
+	if (strcmp(option, "0") == 0)
+		return 0;
+	if (strcmp(option, "1") == 0)
+		return 1;
+	if (strcmp(option, "inactive") == 0)
+		return 0;
+	if (strcmp(option, "active") == 0)
+		return 1;
+	if (strcmp(option, "off") == 0)
+		return 0;
+	if (strcmp(option, "on") == 0)
+		return 1;
+	if (strcmp(option, "false") == 0)
+		return 0;
+	if (strcmp(option, "true") == 0)
+		return 1;
+	return -1;
 }
 
-static void wait_time(void *data)
+// parse num_lines line id and values from lvs into lines and values
+static bool parse_line_values(int num_lines, char **lvs, char **lines, int *values,
+			      bool interactive)
 {
-	struct callback_data *cbdata = data;
+	int i;
+	char *value;
 
-	maybe_daemonize(cbdata->daemonize);
-	select(0, NULL, NULL, NULL, &cbdata->tv);
+	for (i = 0; i < num_lines; i++) {
+		value = strchr(lvs[i], '=');
+		if (!value) {
+			if (interactive)
+				printf("invalid line value: %s\n", lvs[i]);
+			else
+				print_error("invalid line value: %s", lvs[i]);
+			return false;
+		}
+		*value = '\0';
+		value++;
+		values[i] = parse_value(value);
+		if (values[i] < 0) {
+			if (interactive)
+				printf("invalid line value: %s\n", value);
+			else
+				print_error("invalid line value: %s", value);
+			return false;
+		}
+		lines[i] = lvs[i];
+	}
+	return true;
 }
 
-static void wait_signal(void *data)
+// parse num_lines line id and values from lvs into lines and values, or die trying.
+static void parse_line_values_or_die(int num_lines, char **lvs, char **lines, int *values)
 {
-	struct callback_data *cbdata = data;
-	struct pollfd pfd;
-	int sigfd, rv;
+	if (!parse_line_values(num_lines, lvs, lines, values, false))
+		exit(EXIT_FAILURE);
+}
 
-	sigfd = make_signalfd();
+static void wait_fd(int fd)
+{
+	struct pollfd pfd;
 
-	memset(&pfd, 0, sizeof(pfd));
-	pfd.fd = sigfd;
-	pfd.events = POLLIN | POLLPRI;
+	pfd.fd = fd;
+	pfd.events = POLLERR;
 
-	maybe_daemonize(cbdata->daemonize);
+	if (poll(&pfd, 1, -1) < 0)
+		die_perror("error waiting on request");
+}
 
-	for (;;) {
-		rv = poll(&pfd, 1, 1000 /* one second */);
-		if (rv < 0)
-			die("error polling for signals: %s", strerror(errno));
-		else if (rv > 0)
-			break;
+// Apply values from the resolver to the requests.
+// offset and values are scratch pads for working.
+static void apply_values(struct gpiod_line_request **requests, struct line_resolver *resolver,
+			  unsigned int *offsets, int *values)
+{
+	int i;
+	const char *chip_path;
+
+	for (i = 0; i < resolver->num_chips; i++) {
+		chip_path = resolver->chip_paths[i];
+		get_line_offsets_and_values(resolver, chip_path, offsets, values);
+		if (gpiod_line_request_set_values(requests[i], values))
+			print_perror("failed to set values on chip %s", chip_path);
 	}
+}
 
-	/*
-	 * Don't bother reading siginfo - it's enough to know that we
-	 * received any signal.
-	 */
-	close(sigfd);
+// set the values in the resolver for the line values specified by the remaining parameters.
+static void set_line_values_subset(struct line_resolver *resolver,
+				   int num_lines, char **lines, int *values)
+{
+	int l, i;
+
+	for (l = 0; l < num_lines; l++)
+		for (i = 0; i < resolver->num_lines; i++)
+			if (strcmp(lines[l], resolver->lines[i].id) == 0) {
+				resolver->lines[i].value = values[l];
+				break;
+			}
 }
 
-enum {
-	MODE_EXIT = 0,
-	MODE_WAIT,
-	MODE_TIME,
-	MODE_SIGNAL,
-};
+static void print_all_line_values(struct line_resolver *resolver)
+{
+	int i;
+	char *fmt = "%s=%s ";
+
+	for (i = 0; i < resolver->num_lines; i++) {
+		if (i == resolver->num_lines - 1)
+			fmt = "%s=%s\n";
+		printf(fmt, resolver->lines[i].id,
+		       resolver->lines[i].value ? "active" : "inactive");
+	}
+}
 
-struct mode_mapping {
-	int id;
-	const char *name;
-	void (*callback)(void *);
-};
+// print the resovler line values for a subset of lines, specified by num_lines and lines.
+static void print_line_values(struct line_resolver *resolver, int num_lines, char **lines)
+{
+	int i, j;
+	char *fmt = "%s=%s ";
 
-static const struct mode_mapping modes[] = {
-	[MODE_EXIT] = {
-		.id		= MODE_EXIT,
-		.name		= "exit",
-		.callback	= NULL,
-	},
-	[MODE_WAIT] = {
-		.id		= MODE_WAIT,
-		.name		= "wait",
-		.callback	= wait_enter,
-	},
-	[MODE_TIME] = {
-		.id		= MODE_TIME,
-		.name		= "time",
-		.callback	= wait_time,
-	},
-	[MODE_SIGNAL] = {
-		.id		= MODE_SIGNAL,
-		.name		= "signal",
-		.callback	= wait_signal,
-	},
-};
+	for (i = 0; i < num_lines; i++) {
+		if (i == num_lines - 1)
+			fmt = "%s=%s\n";
+		for (j = 0; j < resolver->num_lines; j++)
+			if (strcmp(lines[i], resolver->lines[j].id) == 0) {
+				printf(fmt, resolver->lines[j].id,
+				       resolver->lines[j].value ? "active" : "inactive");
+				break;
+			}
+	}
+}
 
-static const struct mode_mapping *parse_mode(const char *mode)
+// toggle the values of all lines in the resolver
+static void toggle_all_lines(struct line_resolver *resolver)
 {
-	size_t i;
-
-	for (i = 0; i < ARRAY_SIZE(modes); i++)
-		if (strcmp(mode, modes[i].name) == 0)
-			return &modes[i];
+	int i;
 
-	return NULL;
+	for (i = 0; i < resolver->num_lines; i++)
+		resolver->lines[i].value = !resolver->lines[i].value;
 }
 
-static int parse_drive(const char *option)
+// toggle a subset of lines, specified by num_lines and lines, in the resolver.
+static void toggle_lines(struct line_resolver *resolver, int num_lines, char **lines)
 {
-	if (strcmp(option, "open-drain") == 0)
-		return GPIOD_LINE_DRIVE_OPEN_DRAIN;
-	if (strcmp(option, "open-source") == 0)
-		return GPIOD_LINE_DRIVE_OPEN_SOURCE;
-	if (strcmp(option, "push-pull") != 0)
-		die("invalid drive: %s", option);
-	return 0;
+	int i, l;
+
+	for (l = 0; l < num_lines; l++)
+		for (i = 0; i < resolver->num_lines; i++)
+			if (strcmp(lines[l], resolver->lines[i].id) == 0) {
+				resolver->lines[i].value = !resolver->lines[i].value;
+				break;
+			}
 }
 
-int main(int argc, char **argv)
+// toggle the resolved lines as specified by the toggle_periods,
+// and apply the values to the requests.
+// offset and values are scratch pads for working.
+static void toggle_sequence(int toggles, unsigned int *toggle_periods,
+			 struct gpiod_line_request **requests,
+			 struct line_resolver *resolver,
+			 unsigned int *offsets, int *values)
 {
-	const struct mode_mapping *mode = &modes[MODE_EXIT];
-	int ret, optc, opti, bias = 0, drive = 0, *values;
-	struct gpiod_request_config *req_cfg;
-	struct gpiod_line_request *request;
-	struct gpiod_line_config *line_cfg;
-	struct callback_data cbdata;
-	struct gpiod_chip *chip;
-	bool active_low = false;
-	unsigned int *offsets;
-	size_t i, num_lines;
-	char *device, *end;
-
-	memset(&cbdata, 0, sizeof(cbdata));
+	int i = 0;
 
 	for (;;) {
-		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
-		if (optc < 0)
-			break;
+		usleep(toggle_periods[i]);
+		toggle_all_lines(resolver);
+		apply_values(requests, resolver, offsets, values);
 
-		switch (optc) {
-		case 'h':
-			print_help();
-			return EXIT_SUCCESS;
-		case 'v':
-			print_version();
-			return EXIT_SUCCESS;
-		case 'l':
-			active_low = true;
-			break;
-		case 'B':
-			bias = parse_bias(optarg);
-			break;
-		case 'D':
-			drive = parse_drive(optarg);
-			break;
-		case 'm':
-			mode = parse_mode(optarg);
-			if (!mode)
-				die("invalid mode: %s", optarg);
-			break;
-		case 's':
-			cbdata.tv.tv_sec = strtoul(optarg, &end, 10);
-			if (*end != '\0')
-				die("invalid time value in seconds: %s", optarg);
-			break;
-		case 'u':
-			cbdata.tv.tv_usec = strtoul(optarg, &end, 10);
-			if (*end != '\0')
-				die("invalid time value in microseconds: %s",
-				    optarg);
-			break;
-		case 'b':
-			cbdata.daemonize = true;
-			break;
-		case '?':
-			die("try %s --help", get_progname());
-		default:
-			abort();
+		i++;
+		if ((i == toggles - 1) && (toggle_periods[i] == 0))
+			return;
+
+		if (i == toggles)
+			i = 0;
+	}
+}
+
+// check that a set of lines, specified by num_lines and lines, are all resolved lines.
+static bool valid_lines(struct line_resolver *resolver, int num_lines, char **lines)
+{
+	bool ret = true;
+	int i, l;
+	bool found;
+
+	for (l = 0; l < num_lines; l++) {
+		found = false;
+		for (i = 0; i < resolver->num_lines; i++) {
+			if (strcmp(lines[l], resolver->lines[i].id) == 0) {
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			printf("unknown line: '%s'\n", lines[l]);
+			ret = false;
 		}
 	}
+	return  ret;
+}
 
-	argc -= optind;
-	argv += optind;
+static void print_interactive_help(void)
+{
+	printf("COMMANDS:\n\n");
+	printf("    exit\n");
+	printf("        Exit the program\n");
+	printf("    get [line] ...\n");
+	printf("        Display the output values of the given requested lines\n\n");
+	printf("        If no lines are specified then all requested lines are displayed\n\n");
+	printf("    help\n");
+	printf("        Print this help\n\n");
+	printf("    set <line=value> ...\n");
+	printf("        Update the output values of the given requested lines\n\n");
+	printf("    sleep <period>\n");
+	printf("        Sleep for the specified period\n\n");
+	printf("    toggle [line] ...\n");
+	printf("        Toggle the output values of the given requested lines\n\n");
+	printf("        If no lines are specified then all requested lines are toggled\n\n");
+}
 
-	if (mode->id != MODE_TIME && (cbdata.tv.tv_sec || cbdata.tv.tv_usec))
-		die("can't specify wait time in this mode");
+// split a line into words, returning the each of the words and the count.
+// max_words specifies the msax number of words that may be returned in words.
+static int split_words(char *line, int max_words, char **words)
+{
+	int num_words = 0;
+	bool in_word = false;
+
+	while (*line != '\0') {
+		if (!in_word && !isspace(*line)) {
+			in_word = true;
+			// count all words, but only store max_words
+			if (num_words < max_words)
+				words[num_words] = line;
+		} else if (isspace(*line)) {
+			if (in_word) {
+				num_words++;
+				in_word = false;
+			}
+			*line = '\0';
+		}
+		line++;
+	}
+	if (in_word)
+		num_words++;
+	return num_words;
+}
 
-	if (mode->id != MODE_SIGNAL &&
-	    mode->id != MODE_TIME &&
-	    cbdata.daemonize)
-		die("can't daemonize in this mode");
+// check if a line is specified somewhere in the rl_line_buffer
+static bool in_line_buffer(const char *id)
+{
+	int len = strlen(id);
+	char *match = rl_line_buffer;
+
+	while ((match = strstr(rl_line_buffer, id))) {
+		if ((match > rl_line_buffer && isspace(match[-1])) &&
+		    (isspace(match[len]) || (match[len] == '=')))
+			return true;
+		match += len;
+	}
 
-	if (argc < 1)
-		die("gpiochip must be specified");
+	return false;
+}
 
-	if (argc < 2)
-		die("at least one GPIO line offset to value mapping must be specified");
+// context for complete_line_id, so it can provide valid line ids.
+static struct line_resolver *completion_context;
 
-	device = argv[0];
+// tab completion helper for line ids.
+static char *complete_line_id(const char *text, int state)
+{
+	static int idx, len;
+	const char *id;
 
-	num_lines = argc - 1;
+	if (!state) {
+		idx = 0;
+		len = strlen(text);
+	}
+	while (idx < completion_context->num_lines) {
+		id = completion_context->lines[idx].id;
+		idx++;
+		if ((strncmp(id, text, len) == 0) &&
+		    (!in_line_buffer(id)))
+			return strdup(id);
+	}
+	return NULL;
+}
 
-	offsets = calloc(num_lines, sizeof(*offsets));
-	values = calloc(num_lines, sizeof(*values));
-	if (!offsets)
-		die("out of memory");
+// tab completion helper for line values (just the value component)
+static char *complete_value(const char *text, int state)
+{
+	static const char * const values[] = {
+		"1", "0", "active", "inactive", "on", "off", "true", "false", NULL
+	};
+	static int idx, len;
+	const char *value;
+
+	if (!state) {
+		idx = 0;
+		len = strlen(text);
+	}
+	while ((value = values[idx])) {
+		idx++;
+		if (strncmp(value, text, len) == 0)
+			return strdup(value);
+	}
+	return NULL;
+}
 
-	for (i = 0; i < num_lines; i++) {
-		ret = sscanf(argv[i + 1], "%u=%d", &offsets[i], &values[i]);
-		if (ret != 2)
-			die("invalid offset<->value mapping: %s", argv[i + 1]);
+// tab completion help for interactive commands
+static char *complete_command(const char *text, int state)
+{
+	static const char * const commands[] = {
+		"get", "set", "toggle", "sleep", "help", "exit", NULL
+	};
+	static int idx, len;
+	const char *cmd;
+
+	if (!state) {
+		idx = 0;
+		len = strlen(text);
+	}
+	while ((cmd = commands[idx])) {
+		idx++;
+		if (strncmp(cmd, text, len) == 0)
+			return strdup(cmd);
+	}
+	return NULL;
+}
 
-		if (values[i] != 0 && values[i] != 1)
-			die("value must be 0 or 1: %s", argv[i + 1]);
+// tab completion for interactive command lines
+static char **tab_completion(const char *text, int start, int end)
+{
+	char **matches = NULL;
+	int cmd_start, cmd_end, len;
+
+	rl_attempted_completion_over = true;
+	rl_completion_type = '@';
+	rl_sort_completion_matches = false;
+
+	for (cmd_start = 0;
+	     isspace(rl_line_buffer[cmd_start]) && cmd_start < end;
+	     cmd_start++)
+		;
+	if (cmd_start == start)
+		matches = rl_completion_matches(text, complete_command);
+	for (cmd_end = cmd_start + 1;
+	     !isspace(rl_line_buffer[cmd_end]) && cmd_end < end;
+	     cmd_end++)
+		;
+
+	len = cmd_end - cmd_start;
+	if (len == 3 && strncmp("set", &rl_line_buffer[cmd_start], 3) == 0) {
+		if (rl_line_buffer[start-1] == '=') {
+			matches = rl_completion_matches(text, complete_value);
+		} else {
+			rl_completion_append_character = '=';
+			matches = rl_completion_matches(text, complete_line_id);
+		}
+	}
+	if ((len == 3 && strncmp("get", &rl_line_buffer[cmd_start], 3) == 0) ||
+	    (len == 6 && strncmp("toggle", &rl_line_buffer[cmd_start], 6) == 0))
+		matches = rl_completion_matches(text, complete_line_id);
+	return matches;
+}
 
-		if (offsets[i] > INT_MAX)
-			die("invalid offset: %s", argv[i + 1]);
+static void interact(struct gpiod_line_request **requests,
+		    struct line_resolver *resolver,
+		    char **lines, unsigned int *offsets, int *values)
+{
+	char *line;
+	int num_words, num_lines, max_words;
+	char **words;
+	int period_us, i;
+	char *line_buf;
+	bool done;
+
+	stifle_history(20);
+	rl_attempted_completion_function = tab_completion;
+	completion_context = resolver;
+
+	max_words = resolver->num_lines + 1;
+	words = calloc(max_words, sizeof(*words));
+	if (!words)
+		die("out of memory");
+	for (done = false; !done;) {
+		line = readline("gpioset> ");
+		if (!line || line[0] == '\0')
+			continue;
+		for (i = strlen(line) - 1; (i > 0) && isspace(line[i]); i--)
+			line[i] = '\0';
+		line_buf = strdup(line);
+		num_words = split_words(line_buf, max_words, words);
+		if (num_words > max_words) {
+			printf("too many command parameters provided\n");
+			goto cmd_done;
+		}
+		num_lines = num_words - 1;
+		if (strcmp(words[0], "get") == 0) {
+			if (num_lines == 0)
+				print_all_line_values(resolver);
+			else if (valid_lines(resolver, num_lines, &words[1]))
+				print_line_values(resolver, num_lines, &words[1]);
+			goto cmd_ok;
+		}
+		if (strcmp(words[0], "set") == 0) {
+			if (num_lines == 0)
+				printf("at least one GPIO line value must be specified\n");
+			else if (parse_line_values(num_lines, &words[1], lines, values, true) &&
+				 valid_lines(resolver, num_lines, lines)) {
+				set_line_values_subset(resolver, num_lines, lines, values);
+				apply_values(requests, resolver, offsets, values);
+			}
+			goto cmd_ok;
+		}
+		if (strcmp(words[0], "toggle") == 0) {
+			if (num_lines == 0)
+				toggle_all_lines(resolver);
+			else if (valid_lines(resolver, num_lines, &words[1]))
+				toggle_lines(resolver, num_lines, &words[1]);
+			apply_values(requests, resolver, offsets, values);
+			goto cmd_ok;
+		}
+		if (strcmp(words[0], "sleep") == 0) {
+			if (num_lines == 0) {
+				printf("a period must be specified\n");
+				goto cmd_ok;
+			}
+			if (num_lines > 1) {
+				printf("only one period can be specified\n");
+				goto cmd_ok;
+			}
+			period_us = parse_period(words[1]);
+			if (period_us < 0) {
+				printf("invalid period: %s\n", words[1]);
+				goto cmd_ok;
+			}
+			usleep(period_us);
+			goto cmd_ok;
+		}
+		if (strcmp(words[0], "exit") == 0) {
+			done = true;
+			goto cmd_done;
+		}
+		if (strcmp(words[0], "help") == 0) {
+			print_interactive_help();
+			goto cmd_done;
+		}
+		printf("unknown command: %s\n", words[0]);
+		printf("Try the 'help' command\n")
+			;
+cmd_ok:
+		for (i = 0; isspace(line[i]); i++)
+			;
+		if ((history_length) == 0 ||
+		    (strcmp(history_list()[history_length - 1]->line, &line[i]) != 0))
+			add_history(&line[i]);
+cmd_done:
+		free(line);
+		free(line_buf);
 	}
+	free(words);
+}
 
-	chip = chip_open_lookup(device);
-	if (!chip)
-		die_perror("unable to open %s", device);
+int main(int argc, char **argv)
+{
+	int i, num_lines, *values;
+	struct gpiod_request_config *req_cfg;
+	struct gpiod_line_request **requests;
+	struct gpiod_line_config *line_cfg;
+	struct gpiod_chip *chip;
+	unsigned int *offsets;
+	struct line_resolver *resolver;
+	char **lines;
+	const char *chip_path;
+	struct config cfg;
+
+	i = parse_config(argc, argv, &cfg);
+	argc -= i;
+	argv += i;
+
+	if (argc < 1)
+		die("at least one GPIO line value must be specified");
+
+	num_lines = argc;
+
+	lines = calloc(num_lines, sizeof(*lines));
+	values = calloc(num_lines, sizeof(*values));
+	if (!lines || !values)
+		die("out of memory");
+
+	parse_line_values_or_die(argc, argv, lines, values);
 
 	line_cfg = gpiod_line_config_new();
 	if (!line_cfg)
 		die_perror("unable to allocate the line config structure");
 
-	if (bias)
-		gpiod_line_config_set_bias_default(line_cfg, bias);
-	if (drive)
-		gpiod_line_config_set_drive_default(line_cfg, drive);
-	if (active_low)
+	if (cfg.bias)
+		gpiod_line_config_set_bias_default(line_cfg, cfg.bias);
+	if (cfg.drive)
+		gpiod_line_config_set_drive_default(line_cfg, cfg.drive);
+	if (cfg.active_low)
 		gpiod_line_config_set_active_low_default(line_cfg, true);
-	gpiod_line_config_set_direction_default(line_cfg,
-						GPIOD_LINE_DIRECTION_OUTPUT);
-	gpiod_line_config_set_output_values(line_cfg, num_lines,
-					    offsets, values);
+	gpiod_line_config_set_direction_default(line_cfg, GPIOD_LINE_DIRECTION_OUTPUT);
 
 	req_cfg = gpiod_request_config_new();
 	if (!req_cfg)
 		die_perror("unable to allocate the request config structure");
 
 	gpiod_request_config_set_consumer(req_cfg, "gpioset");
-	gpiod_request_config_set_offsets(req_cfg, num_lines, offsets);
+	resolver = resolve_lines(num_lines, lines, cfg.chip_id, cfg.strict, by_name);
+	for (i = 0; i < num_lines; i++)
+		resolver->lines[i].value = values[i];
+	requests = calloc(resolver->num_chips, sizeof(*requests));
+	offsets = calloc(num_lines, sizeof(*offsets));
+	if (!requests || !offsets)
+		die("out of memory");
+	for (i = 0; i < resolver->num_chips; i++) {
+		chip_path = resolver->chip_paths[i];
+		num_lines = get_line_offsets_and_values(resolver, chip_path, offsets, values);
+		gpiod_request_config_set_offsets(req_cfg, num_lines, offsets);
+		gpiod_line_config_set_output_values(line_cfg, num_lines,
+						    offsets, values);
+
+		chip = gpiod_chip_open(resolver->chip_paths[i]);
+		if (!chip)
+			die_perror("unable to open chip %s", chip_path);
+
+		requests[i] = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+		if (!requests[i])
+			die_perror("unable to request lines on chip %s", chip_path);
+
+		gpiod_chip_close(chip);
+	}
+	gpiod_request_config_free(req_cfg);
+	gpiod_line_config_free(line_cfg);
 
-	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
-	if (!request)
-		die_perror("unable to request lines");
+	if (cfg.daemonize)
+		if (daemon(0, 0) < 0)
+			die_perror("unable to daemonize");
+
+	if (cfg.toggles) {
+		for (i = 0; i < cfg.toggles; i++)
+			if ((cfg.hold_period_us > cfg.toggle_periods[i]) &&
+			    ((i != cfg.toggles - 1) || cfg.toggle_periods[i] != 0))
+				cfg.toggle_periods[i] = cfg.hold_period_us;
+		toggle_sequence(cfg.toggles, cfg.toggle_periods, requests, resolver,
+				offsets, values);
+		free(cfg.toggle_periods);
+	}
 
-	if (mode->callback)
-		mode->callback(&cbdata);
+	if (cfg.hold_period_us)
+		usleep(cfg.hold_period_us);
 
-	gpiod_line_request_release(request);
-	gpiod_request_config_free(req_cfg);
-	gpiod_line_config_free(line_cfg);
-	gpiod_chip_close(chip);
+	if (cfg.interactive)
+		interact(requests, resolver, lines, offsets, values);
+
+	if (cfg.daemonize)
+		wait_fd(gpiod_line_request_get_fd(requests[0]));
+
+	for (i = 0; i < resolver->num_chips; i++)
+		gpiod_line_request_release(requests[i]);
+	free(requests);
+	free_line_resolver(resolver);
+	free(lines);
+	free(values);
 	free(offsets);
 
 	return EXIT_SUCCESS;
diff --git a/tools/tools-common.c b/tools/tools-common.c
index f5fd50c..b567226 100644
--- a/tools/tools-common.c
+++ b/tools/tools-common.c
@@ -4,15 +4,18 @@
 /* Common code for GPIO tools. */
 
 #include <ctype.h>
+#include <dirent.h>
 #include <errno.h>
 #include <gpiod.h>
+#include <inttypes.h>
 #include <libgen.h>
-#include <signal.h>
+#include <limits.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/signalfd.h>
+#include <sys/stat.h>
+#include <time.h>
 
 #include "tools-common.h"
 
@@ -21,6 +24,28 @@ const char *get_progname(void)
 	return program_invocation_name;
 }
 
+void print_error(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	fprintf(stderr, "%s: ", program_invocation_name);
+	vfprintf(stderr, fmt, va);
+	fprintf(stderr, "\n");
+	va_end(va);
+}
+
+void print_perror(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	fprintf(stderr, "%s: ", program_invocation_name);
+	vfprintf(stderr, fmt, va);
+	fprintf(stderr, ": %s\n", strerror(errno));
+	va_end(va);
+}
+
 void die(const char *fmt, ...)
 {
 	va_list va;
@@ -57,93 +82,338 @@ void print_version(void)
 	printf("There is NO WARRANTY, to the extent permitted by law.\n");
 }
 
-int parse_bias(const char *option)
+int parse_bias_or_die(const char *option)
 {
 	if (strcmp(option, "pull-down") == 0)
 		return GPIOD_LINE_BIAS_PULL_DOWN;
 	if (strcmp(option, "pull-up") == 0)
 		return GPIOD_LINE_BIAS_PULL_UP;
-	if (strcmp(option, "disable") == 0)
+	if (strcmp(option, "disabled") == 0)
 		return GPIOD_LINE_BIAS_DISABLED;
 	if (strcmp(option, "as-is") != 0)
 		die("invalid bias: %s", option);
 	return 0;
 }
 
+int parse_period(const char *option)
+{
+	unsigned long p, m = 0;
+	char *end;
+
+	p = strtoul(option, &end, 10);
+	switch (*end) {
+	case 'u':
+		m = 1;
+		end++;
+		break;
+	case 'm':
+		m = 1000;
+		end++;
+		break;
+	case 's':
+		m = 1000000;
+		break;
+	case '\0':
+		break;
+	default:
+		return -1;
+	}
+	if (m) {
+		if (*end != 's')
+			return -1;
+		end++;
+	} else {
+		m = 1000;
+	}
+	p *= m;
+	if (*end != '\0' || p > INT_MAX)
+		return -1;
+
+	return p;
+}
+
+unsigned int parse_period_or_die(const char *option)
+{
+	int period = parse_period(option);
+
+	if (period < 0)
+		die("invalid period: %s", option);
+	return period;
+}
+
+int parse_periods_or_die(char *option, unsigned int **periods)
+{
+	int i, num_periods = 1;
+	unsigned int *pp;
+	char *end;
+
+	for (i = 0; option[i] != '\0'; i++)
+		if (option[i] == ',')
+			num_periods++;
+
+	pp = calloc(num_periods, sizeof(*pp));
+	for (i = 0; i < num_periods - 1; i++) {
+		for (end = option; *end != ','; end++)
+			;
+		*end = '\0';
+		pp[i] = parse_period_or_die(option);
+		option = end + 1;
+	}
+	pp[i] = parse_period_or_die(option);
+	*periods = pp;
+	return num_periods;
+}
+
+int parse_uint(const char *option)
+{
+	unsigned long o;
+	char *end;
+
+	o = strtoul(option, &end, 10);
+	if (*end == '\0' && o <= INT_MAX)
+		return o;
+
+	return -1;
+}
+
+unsigned int parse_uint_or_die(const char *option)
+{
+	int i = parse_uint(option);
+
+	if (i < 0)
+		die("invalid number: %s", option);
+
+	return i;
+}
+
 void print_bias_help(void)
 {
-	printf("Biases:\n");
-	printf("  as-is:\tleave bias unchanged\n");
-	printf("  disable:\tdisable bias\n");
-	printf("  pull-up:\tenable pull-up\n");
-	printf("  pull-down:\tenable pull-down\n");
+	printf("  -b, --bias <bias>\tspecify the line bias.\n");
+	printf("\t\t\tPossible values: 'as-is', 'pull-down', 'pull-up', 'disabled'.\n");
+	printf("\t\t\t(default is 'as-is')\n");
 }
 
-int make_signalfd(void)
+void print_chip_help(void)
 {
-	sigset_t sigmask;
-	int sigfd, rv;
+	printf("\nChips:\n");
+	printf("    A GPIO chip may be identified by number, name, or path.\n");
+	printf("    e.g. '0', 'gpiochip0', and '/dev/gpiochip0' all refer to the same chip.\n");
+}
 
-	sigemptyset(&sigmask);
-	sigaddset(&sigmask, SIGTERM);
-	sigaddset(&sigmask, SIGINT);
+void print_period_help(void)
+{
+	printf("\nPeriods:\n");
+	printf("    Periods are taken as milliseconds unless units are specified. e.g. 10us.\n");
+	printf("    Supported units are 's', 'ms', and 'us'.\n");
+}
+
+#define TIME_BUFFER_SIZE 20
+
+// mode:
+// 0: monotonic time
+// 1: utc time
+// 2: local time
+void print_event_time(uint64_t evtime, int mode)
+{
+	time_t evtsec;
+	struct tm t;
+	char tbuf[TIME_BUFFER_SIZE];
+	char *tz;
+
+	if (mode) {
+		evtsec = evtime / 1000000000;
+		if (mode == 2) {
+			localtime_r(&evtsec, &t);
+			tz = "";
+		} else {
+			gmtime_r(&evtsec, &t);
+			tz = "Z";
+		}
+		strftime(tbuf, TIME_BUFFER_SIZE, "%FT%T", &t);
+		printf("%s.%09"PRIu64"%s", tbuf, evtime % 1000000000, tz);
+	} else {
+		printf("%8"PRIu64".%09"PRIu64,
+		       evtime / 1000000000, evtime % 1000000000);
+	}
+}
 
-	rv = sigprocmask(SIG_BLOCK, &sigmask, NULL);
-	if (rv < 0)
-		die("error masking signals: %s", strerror(errno));
 
-	sigfd = signalfd(-1, &sigmask, 0);
-	if (sigfd < 0)
-		die("error creating signalfd: %s", strerror(errno));
+typedef bool (*is_set_func)(struct gpiod_line_info *);
 
-	return sigfd;
+struct flag {
+	const char *name;
+	is_set_func is_set;
+};
+
+static bool line_bias_is_pullup(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_bias(info) == GPIOD_LINE_BIAS_PULL_UP;
 }
 
-int chip_dir_filter(const struct dirent *entry)
+static bool line_bias_is_pulldown(struct gpiod_line_info *info)
 {
-	bool is_chip;
-	char *path;
-	int ret;
+	return gpiod_line_info_get_bias(info) == GPIOD_LINE_BIAS_PULL_DOWN;
+}
 
-	ret = asprintf(&path, "/dev/%s", entry->d_name);
-	if (ret < 0)
-		return 0;
+static bool line_bias_is_disabled(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_bias(info) == GPIOD_LINE_BIAS_DISABLED;
+}
 
-	is_chip = gpiod_is_gpiochip_device(path);
-	free(path);
-	return !!is_chip;
+static bool line_drive_is_open_drain(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_drive(info) == GPIOD_LINE_DRIVE_OPEN_DRAIN;
 }
 
-struct gpiod_chip *chip_open_by_name(const char *name)
+static bool line_drive_is_open_source(struct gpiod_line_info *info)
 {
-	struct gpiod_chip *chip;
-	char *path;
-	int ret;
+	return gpiod_line_info_get_drive(info) == GPIOD_LINE_DRIVE_OPEN_SOURCE;
+}
 
-	ret = asprintf(&path, "/dev/%s", name);
-	if (ret < 0)
-		return NULL;
+static bool line_edge_detection_is_both(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_edge_detection(info) == GPIOD_LINE_BIAS_PULL_UP;
+}
 
-	chip = gpiod_chip_open(path);
-	free(path);
+static bool line_edge_detection_is_rising(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_edge_detection(info) == GPIOD_LINE_EDGE_RISING;
+}
 
-	return chip;
+static bool line_edge_detection_is_falling(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_edge_detection(info) == GPIOD_LINE_EDGE_FALLING;
 }
 
-static struct gpiod_chip *chip_open_by_number(unsigned int num)
+static bool line_event_clock_realtime(struct gpiod_line_info *info)
+{
+	return gpiod_line_info_get_event_clock(info) == GPIOD_LINE_EVENT_CLOCK_REALTIME;
+}
+
+static const struct flag flags[] = {
+	{
+		.name = "used",
+		.is_set = gpiod_line_info_is_used,
+	},
+	{
+		.name = "open-drain",
+		.is_set = line_drive_is_open_drain,
+	},
+	{
+		.name = "open-source",
+		.is_set = line_drive_is_open_source,
+	},
+	{
+		.name = "pull-up",
+		.is_set = line_bias_is_pullup,
+	},
+	{
+		.name = "pull-down",
+		.is_set = line_bias_is_pulldown,
+	},
+	{
+		.name = "bias-disabled",
+		.is_set = line_bias_is_disabled,
+	},
+	{
+		.name = "both-edges",
+		.is_set = line_edge_detection_is_both,
+	},
+	{
+		.name = "rising-edges",
+		.is_set = line_edge_detection_is_rising,
+	},
+	{
+		.name = "falling-edges",
+		.is_set = line_edge_detection_is_falling,
+	},
+	{
+		.name = "event-clock-realtime",
+		.is_set = line_event_clock_realtime,
+	},
+};
+
+static PRINTF(3, 4) void prinfo(bool *of, unsigned int prlen, const char *fmt, ...)
+{
+	char *buf, *buffmt = NULL;
+	size_t len;
+	va_list va;
+	int rv;
+
+	va_start(va, fmt);
+	rv = vasprintf(&buf, fmt, va);
+	va_end(va);
+	if (rv < 0)
+		die("vasprintf: %s\n", strerror(errno));
+
+	len = strlen(buf) - 1;
+
+	if (len >= prlen || *of) {
+		*of = true;
+		printf("%s", buf);
+	} else {
+		rv = asprintf(&buffmt, "%%%us", prlen);
+		if (rv < 0)
+			die("asprintf: %s\n", strerror(errno));
+
+		printf(buffmt, buf);
+	}
+
+	free(buf);
+	if (fmt)
+		free(buffmt);
+}
+
+void print_line_info(struct gpiod_line_info *info)
+{
+	bool of = false;
+	const char *name, *consumer;
+	size_t i;
+	int direction;
+	unsigned long debounce;
+
+	name = gpiod_line_info_get_name(info);
+	consumer = gpiod_line_info_get_consumer(info);
+	direction = gpiod_line_info_get_direction(info);
+	debounce = gpiod_line_info_get_debounce_period_us(info);
+
+	name ? prinfo(&of, 16, "\t%s", name) : prinfo(&of, 16, "\tunnamed");
+
+	if (!gpiod_line_info_is_used(info))
+		prinfo(&of, 12, "\tunused");
+	else
+		consumer ? prinfo(&of, 12, "\t%s", consumer)
+			 : prinfo(&of, 12, "\tkernel");
+
+	printf("\t[%s", direction == GPIOD_LINE_DIRECTION_INPUT ? "input" : "output");
+
+	if (gpiod_line_info_is_active_low(info))
+		printf(" active-low");
+
+	for (i = 0; i < ARRAY_SIZE(flags); i++) {
+		if (flags[i].is_set(info))
+			printf(" %s", flags[i].name);
+	}
+
+	if (debounce)
+		printf(" debounce_period=%luus", debounce);
+
+	printf("]");
+}
+
+static int chip_dir_filter(const struct dirent *entry)
 {
-	struct gpiod_chip *chip;
 	char *path;
-	int ret;
+	int ret = 0;
+	struct stat sb;
 
-	ret = asprintf(&path, "/dev/gpiochip%u", num);
-	if (!ret)
-		return NULL;
+	if (asprintf(&path, "/dev/%s", entry->d_name) < 0)
+		return 0;
 
-	chip = gpiod_chip_open(path);
+	if ((lstat(path, &sb) == 0) && (!S_ISLNK(sb.st_mode)) && gpiod_is_gpiochip_device(path))
+		ret = 1;
 	free(path);
-
-	return chip;
+	return ret;
 }
 
 static bool isuint(const char *str)
@@ -154,18 +424,270 @@ static bool isuint(const char *str)
 	return *str == '\0';
 }
 
-struct gpiod_chip *chip_open_lookup(const char *device)
+bool chip_path_lookup(const char *id, char **path_ptr)
 {
-	struct gpiod_chip *chip;
+	char *path;
 
-	if (isuint(device)) {
-		chip = chip_open_by_number(strtoul(device, NULL, 10));
+	if (isuint(id)) {
+		// by number
+		if (asprintf(&path, "/dev/gpiochip%s", id) < 0)
+			return false;
+	} else if (strchr(id, '/')) {
+		// by path
+		if (asprintf(&path, "%s", id) < 0)
+			return false;
 	} else {
-		if (strncmp(device, "/dev/", 5))
-			chip = chip_open_by_name(device);
+		// by device name
+		if (asprintf(&path, "/dev/%s", id) < 0)
+			return false;
+	}
+	if (!gpiod_is_gpiochip_device(path)) {
+		free(path);
+		return false;
+	}
+	*path_ptr = path;
+	return true;
+}
+
+int chip_paths(const char *id, char ***paths_ptr)
+{
+	char *path;
+	char **paths;
+
+	if (id == NULL)
+		return all_chip_paths(paths_ptr);
+
+	if (!chip_path_lookup(id, &path))
+		return 0;
+	paths = malloc(sizeof(*paths));
+	if (paths == NULL) {
+		free(path);
+		return 0;
+	}
+	paths[0] = path;
+	*paths_ptr = paths;
+	return 1;
+}
+
+int all_chip_paths(char ***paths_ptr)
+{
+	int i, j, num_chips, ret = 0;
+	struct dirent **entries;
+	char **paths;
+
+	num_chips = scandir("/dev/", &entries, chip_dir_filter, alphasort);
+	if (num_chips < 0)
+		die_perror("unable to scan /dev");
+
+	paths = calloc(num_chips, sizeof(*paths));
+	if (paths == NULL)
+		goto free_entries;
+
+	for (i = 0; i < num_chips; i++) {
+		if (asprintf(&paths[i], "/dev/%s", entries[i]->d_name) < 0) {
+			for (j = 0; j < i; j++)
+				free(paths[j]);
+			free(paths);
+			return 0;
+		}
+	}
+	*paths_ptr = paths;
+	ret = num_chips;
+free_entries:
+	for (i = 0; i < num_chips; i++)
+		free(entries[i]);
+	free(entries);
+	return ret;
+}
+
+static bool resolve_line(struct gpiod_line_info *info, const char *chip_path,
+			 struct line_resolver *resolver)
+{
+	int i, found_idx = -1;
+	unsigned int offset;
+	struct resolved_line *line;
+	const char *name;
+
+	offset = gpiod_line_info_get_offset(info);
+	for (i = 0; i < resolver->num_lines; i++) {
+		line = &resolver->lines[i];
+		name = gpiod_line_info_get_name(info);
+		if ((!line->id_is_name && (line->offset == (int)offset)) ||
+		    (name && strcmp(line->id, name) == 0)) {
+			if (resolver->strict && line->chip_path)
+				die("line %s is not unique", line->id);
+			if (found_idx != -1)
+				die("lines %s and %s are the same",
+				    resolver->lines[found_idx].id, line->id);
+			found_idx = i;
+			line->chip_path = chip_path;
+			line->offset = offset;
+			resolver->num_found++;
+		}
+	}
+	return (found_idx != -1);
+}
+
+static bool resolve_done(struct line_resolver *resolver)
+{
+	return (!resolver->strict && resolver->num_found >= resolver->num_lines);
+}
+
+
+struct line_resolver *resolve_lines(int num_lines, char **lines, const char *chip_id,
+				    bool strict, bool by_name)
+{
+	char **paths;
+	int num_chips, i, j, offset;
+	size_t resolver_size;
+	struct line_resolver *resolver;
+	struct resolved_line *line;
+	struct gpiod_chip *chip;
+	struct gpiod_chip_info *chip_info;
+	struct gpiod_line_info *line_info;
+	bool chip_used;
+
+	if (chip_id == NULL)
+		by_name = true;
+
+	num_chips = chip_paths(chip_id, &paths);
+	if ((chip_id != NULL)  && (num_chips == 0))
+		die("cannot find a GPIO chip character device corresponding to %s", chip_id);
+
+	resolver_size = sizeof(*resolver) + num_lines * sizeof(*line);
+	resolver = malloc(resolver_size);
+	if (resolver == NULL)
+		die("out of memory");
+
+	memset(resolver, 0, resolver_size);
+
+	resolver->num_lines = num_lines;
+	resolver->strict = strict;
+	for (i = 0; i < num_lines; i++) {
+		line = &resolver->lines[i];
+		line->id = lines[i];
+		line->offset = by_name ? -1 : parse_uint(lines[i]);
+		line->id_is_name = (line->offset == -1);
+	}
+
+	for (i = 0; (i < num_chips) && !resolve_done(resolver); i++) {
+		chip_used = false;
+		chip = gpiod_chip_open(paths[i]);
+		if (!chip) {
+			if ((errno == EACCES) && (chip_id == NULL))
+				goto finalize_path;
+
+			die_perror("unable to open chip %s", paths[i]);
+		}
+
+		chip_info = gpiod_chip_get_info(chip);
+		if (!chip_info)
+			die_perror("unable to get info for %s", paths[i]);
+
+		num_lines = gpiod_chip_info_get_num_lines(chip_info);
+		gpiod_chip_info_free(chip_info);
+		for (offset = 0; (offset < num_lines) && !resolve_done(resolver); offset++) {
+			line_info = gpiod_chip_get_line_info(chip, offset);
+			if (!line_info)
+				die_perror("unable to retrieve the line info from chip %s",
+					   paths[i]);
+
+			if (resolve_line(line_info, paths[i], resolver))
+				chip_used = true;
+
+			gpiod_line_info_free(line_info);
+		}
+		gpiod_chip_close(chip);
+finalize_path:
+		if (chip_used) {
+			resolver->num_chips++;
+		} else {
+			free(paths[i]);
+			paths[i] = NULL;
+		}
+	}
+	for (i = 0 ; i < resolver->num_lines; i++) {
+		if (resolver->lines[i].chip_path)
+			continue;
+		if (chip_id && !resolver->lines[i].id_is_name)
+			print_error("offset %s is out of range on chip %s",
+				    resolver->lines[i].id, chip_id);
 		else
-			chip = gpiod_chip_open(device);
+			print_error("cannot find line %s", resolver->lines[i].id);
+	}
+
+	if (resolver->num_found != resolver->num_lines)
+		exit(EXIT_FAILURE);
+
+	// condense paths to remove freed paths
+	for (i = 0; i < resolver->num_chips; i++) {
+		if (paths[i] == NULL)
+			// note the limit is the uncondensed size
+			for (j = i + 1; j < num_chips; j++) {
+				if (paths[j] != NULL) {
+					paths[i] = paths[j];
+					paths[j] = NULL;
+					break;
+				}
+			}
 	}
+	resolver->chip_paths = paths;
 
-	return chip;
+	return resolver;
+}
+
+void free_line_resolver(struct line_resolver *resolver)
+{
+	int i;
+
+	if (!resolver)
+		return;
+
+	for (i = 0; i < resolver->num_chips; i++)
+		free(resolver->chip_paths[i]);
+	free(resolver->chip_paths);
+	free(resolver);
+}
+
+int get_line_offsets_and_values(struct line_resolver *resolver, const char *chip_path,
+				unsigned int *offsets, int *values)
+{
+	int i, num_lines = 0;
+	struct resolved_line *line;
+
+	for (i = 0; i < resolver->num_lines; i++) {
+		line = &resolver->lines[i];
+		if (line->chip_path == chip_path) {
+			offsets[num_lines] = line->offset;
+			if (values)
+				values[num_lines] = line->value;
+			num_lines++;
+		}
+	}
+	return num_lines;
+}
+
+const char *get_line_name(struct line_resolver *resolver, const char *chip_path,
+			  unsigned int offset)
+{
+	int i;
+
+	for (i = 0; i < resolver->num_lines; i++)
+		if ((resolver->lines[i].offset == (int)offset) &&
+		    (resolver->lines[i].chip_path == chip_path) &&
+		    (resolver->lines[i].id_is_name))
+			return resolver->lines[i].id;
+	return 0;
+}
+
+void set_line_values(struct line_resolver *resolver, const char *chip_path, int *values)
+{
+	int i, j;
+
+	for (i = 0, j = 0; i < resolver->num_lines; i++) {
+		if (resolver->lines[i].chip_path == chip_path) {
+			resolver->lines[i].value = values[j];
+			j++;
+		}
+	}
 }
diff --git a/tools/tools-common.h b/tools/tools-common.h
index f059440..885b1aa 100644
--- a/tools/tools-common.h
+++ b/tools/tools-common.h
@@ -4,7 +4,6 @@
 #ifndef __GPIOD_TOOLS_COMMON_H__
 #define __GPIOD_TOOLS_COMMON_H__
 
-#include <dirent.h>
 #include <gpiod.h>
 
 /*
@@ -21,15 +20,63 @@
 
 #define GETOPT_NULL_LONGOPT	NULL, 0, NULL, 0
 
+struct resolved_line {
+	// from the command line
+	const char *id;
+	// id is a name, not an offset
+	bool id_is_name;
+	// path of chip - if line found
+	const char *chip_path;
+	// if found then offset on chip
+	// if not found then atoi(id) or -1
+	int offset;
+	// place holder for line value in gpioget/set.
+	int value;
+};
+
+// a resolver from requested line names/offsets to lines on the system
+struct line_resolver {
+	// number of chips the lines span, and number of entries is in chip_paths
+	int num_chips;
+	// paths to the relevant chips
+	char **chip_paths;
+	// number of lines in lines
+	int num_lines;
+	// number of lines found
+	int num_found;
+	// perform exhaustive search to check line names are unique
+	bool strict;
+	// descriptors for the requested lines
+	struct resolved_line lines[];
+};
+
 const char *get_progname(void);
+void print_error(const char *fmt, ...) PRINTF(1, 2);
+void print_perror(const char *fmt, ...) PRINTF(1, 2);
 void die(const char *fmt, ...) NORETURN PRINTF(1, 2);
 void die_perror(const char *fmt, ...) NORETURN PRINTF(1, 2);
 void print_version(void);
-int parse_bias(const char *option);
+int parse_bias_or_die(const char *option);
+int parse_period(const char *option);
+unsigned int parse_period_or_die(const char *option);
+int parse_periods_or_die(char *option, unsigned int **periods);
+int parse_uint(const char *option);
+unsigned int parse_uint_or_die(const char *option);
 void print_bias_help(void);
-int make_signalfd(void);
-int chip_dir_filter(const struct dirent *entry);
-struct gpiod_chip *chip_open_by_name(const char *name);
-struct gpiod_chip *chip_open_lookup(const char *device);
+void print_chip_help(void);
+void print_period_help(void);
+void print_event_time(uint64_t evtime, int mode);
+void print_line_info(struct gpiod_line_info *info);
+bool chip_path_lookup(const char *id, char **path_ptr);
+int chip_paths(const char *id, char ***paths_ptr);
+int all_chip_paths(char ***paths_ptr);
+struct line_resolver *resolve_lines(int num_lines, char **lines, const char *chip_id,
+				    bool strict, bool by_name);
+void free_line_resolver(struct line_resolver *resolver);
+int get_line_offsets_and_values(struct line_resolver *resolver, const char *chip_path,
+				unsigned int *offsets, int *values);
+const char *get_line_name(struct line_resolver *resolver, const char *chip_path,
+			  unsigned int offset);
+void set_line_values(struct line_resolver *resolver, const char *chip_path, int *values);
 
 #endif /* __GPIOD_TOOLS_COMMON_H__ */
-- 
2.36.1


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [libgpiod v2][PATCH 2/4] tools: tests for line name focussed rework
  2022-06-27 13:44 [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
  2022-06-27 13:44 ` [libgpiod v2][PATCH 1/4] " Kent Gibson
@ 2022-06-27 13:44 ` Kent Gibson
  2022-07-01  0:42   ` Kent Gibson
  2022-06-27 13:44 ` [libgpiod v2][PATCH 3/4] tools: add gpiowatch Kent Gibson
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 15+ messages in thread
From: Kent Gibson @ 2022-06-27 13:44 UTC (permalink / raw)
  To: linux-gpio, brgl; +Cc: Kent Gibson

Rework the tools tests and expand to cover new functionality.

Signed-off-by: Kent Gibson <warthog618@gmail.com>
---
 tools/gpio-tools-test      |    3 -
 tools/gpio-tools-test.bats | 2052 ++++++++++++++++++++++++++----------
 2 files changed, 1504 insertions(+), 551 deletions(-)

diff --git a/tools/gpio-tools-test b/tools/gpio-tools-test
index 234f9bd..1a012dc 100755
--- a/tools/gpio-tools-test
+++ b/tools/gpio-tools-test
@@ -37,9 +37,6 @@ check_prog() {
 # Check all required non-coreutils tools
 check_prog bats
 check_prog modprobe
-check_prog rmmod
-check_prog udevadm
-check_prog timeout
 
 # Check if we're running a kernel at the required version or later
 check_kernel $MIN_KERNEL_VERSION
diff --git a/tools/gpio-tools-test.bats b/tools/gpio-tools-test.bats
index 69ad786..eaa814f 100755
--- a/tools/gpio-tools-test.bats
+++ b/tools/gpio-tools-test.bats
@@ -4,15 +4,16 @@
 
 # Simple test harness for the gpio-tools.
 
-# Where output from coprocesses is stored
-COPROC_OUTPUT=$BATS_TMPDIR/gpio-tools-test-output
+# Where output from the dut is stored
+DUT_OUTPUT=$BATS_TMPDIR/gpio-tools-test-output
 # Save the PID of coprocess - otherwise we won't be able to wait for it
 # once it exits as the COPROC_PID will be cleared.
-COPROC_SAVED_PID=""
+DUT_PID=""
 
 GPIOSIM_CHIPS=""
-GPIOSIM_CONFIGFS="/sys/kernel/config/gpio-sim/"
+GPIOSIM_CONFIGFS="/sys/kernel/config/gpio-sim"
 GPIOSIM_SYSFS="/sys/devices/platform/"
+GPIOSIM_APP_NAME="gpio-tools-test"
 
 # Run the command in $* and return 0 if the command failed. The way we do it
 # here is a workaround for the way bats handles failing processes.
@@ -23,10 +24,7 @@ assert_fail() {
 
 # Check if the string in $2 matches against the pattern in $1.
 regex_matches() {
-	local PATTERN=$1
-	local STRING=$2
-
-	[[ $STRING =~ $PATTERN ]]
+	[[ $2 =~ $1 ]] || (echo "Mismatched: \"$2\"" && false)
 }
 
 # Iterate over all lines in the output of the last command invoked with bats'
@@ -38,31 +36,39 @@ output_contains_line() {
 	do
 		test "$line" = "$LINE" && return 0
 	done
-
+	echo "Mismatched:"
+	echo "$output"
 	return 1
 }
 
+output_is() {
+	test "$output" = "$1" || (echo "Mismatched: \"$output\"" && false)
+}
+
+num_lines_is() {
+	test ${#lines[@]} -eq $1 || (echo "Num lines is : ${#lines[@]}" && false)
+}
+
+status_is() {
+	test "$status" -eq "$1"
+}
+
 # Same as above but match against the regex pattern in $1.
 output_regex_match() {
-	local PATTERN=$1
-
 	for line in "${lines[@]}"
 	do
-		regex_matches "$PATTERN" "$line" && return 0
+		[[ "$line" =~ $1 ]] && return 0
 	done
-
+	echo "Mismatched:"
+	echo "$output"
 	return 1
 }
 
-random_name() {
-	cat /proc/sys/kernel/random/uuid
-}
-
 gpiosim_chip() {
 	local VAR=$1
-	local NAME=$(random_name)
+	local NAME=${GPIOSIM_APP_NAME}-$$-${VAR}
 	local DEVPATH=$GPIOSIM_CONFIGFS/$NAME
-	local BANKPATH=$DEVPATH/$NAME
+	local BANKPATH=$DEVPATH/bank0
 
 	mkdir -p $BANKPATH
 
@@ -87,36 +93,24 @@ gpiosim_chip() {
 
 	echo 1 > $DEVPATH/live
 
-	GPIOSIM_CHIPS="$VAR:$NAME $GPIOSIM_CHIPS"
+	GPIOSIM_CHIPS="$VAR $GPIOSIM_CHIPS"
 }
 
-gpiosim_chip_map_name() {
-	local VAR=$1
-
-	for CHIP in $GPIOSIM_CHIPS
-	do
-		KEY=$(echo $CHIP | cut -d":" -f1)
-		VAL=$(echo $CHIP | cut -d":" -f2)
-
-		if [ "$KEY" = "$VAR" ]
-		then
-			echo $VAL
-		fi
-	done
+gpiosim_chip_name() {
+	echo $(<$GPIOSIM_CONFIGFS/${GPIOSIM_APP_NAME}-$$-$1/bank0/chip_name)
 }
 
-gpiosim_chip_name() {
-	local VAR=$1
-	local NAME=$(gpiosim_chip_map_name $VAR)
+gpiosim_chip_path() {
+	echo "/dev/$(<$GPIOSIM_CONFIGFS/${GPIOSIM_APP_NAME}-$$-$1/bank0/chip_name)"
+}
 
-	cat $GPIOSIM_CONFIGFS/$NAME/$NAME/chip_name
+gpiosim_chip_number() {
+	local NAME=$(<$GPIOSIM_CONFIGFS/${GPIOSIM_APP_NAME}-$$-$1/bank0/chip_name)
+	echo ${NAME#"gpiochip"}
 }
 
 gpiosim_dev_name() {
-	local VAR=$1
-	local NAME=$(gpiosim_chip_map_name $VAR)
-
-	cat $GPIOSIM_CONFIGFS/$NAME/dev_name
+	echo $(<$GPIOSIM_CONFIGFS/${GPIOSIM_APP_NAME}-$$-$1/dev_name)
 }
 
 gpiosim_set_pull() {
@@ -136,26 +130,21 @@ gpiosim_check_value() {
 	local DEVNAME=$(gpiosim_dev_name $VAR)
 	local CHIPNAME=$(gpiosim_chip_name $VAR)
 
-	VAL=$(cat $GPIOSIM_SYSFS/$DEVNAME/$CHIPNAME/sim_gpio$OFFSET/value)
-	if [ "$VAL" = "$EXPECTED" ]
-	then
-		return 0
-	fi
-
-	return 1
+	VAL=$(<$GPIOSIM_SYSFS/$DEVNAME/$CHIPNAME/sim_gpio$OFFSET/value)
+	[ "$VAL" = "$EXPECTED" ]
 }
 
 gpiosim_cleanup() {
 	for CHIP in $GPIOSIM_CHIPS
 	do
-		local NAME=$(echo $CHIP | cut -d":" -f2)
+		local NAME=${GPIOSIM_APP_NAME}-$$-$CHIP
 
 		local DEVPATH=$GPIOSIM_CONFIGFS/$NAME
-		local BANKPATH=$DEVPATH/$NAME
+		local BANKPATH=$DEVPATH/bank0
 
 		echo 0 > $DEVPATH/live
 
-		ls $BANKPATH/line* 2> /dev/null
+		ls $BANKPATH/line* > /dev/null 2>&1
 		if [ "$?" = "0" ]
 		then
 			for LINE in $(find $BANKPATH/ | egrep "line[0-9]+$")
@@ -178,178 +167,626 @@ run_tool() {
 	run timeout 10s $BATS_TEST_DIRNAME/"$@"
 }
 
-coproc_run_tool() {
-	rm -f $BR_PROC_OUTPUT
-	coproc timeout 10s $BATS_TEST_DIRNAME/"$@" > $COPROC_OUTPUT 2> $COPROC_OUTPUT
-	COPROC_SAVED_PID=$COPROC_PID
-	# FIXME We're giving the background process some time to get up, but really this
-	# should be more reliable...
+dut_run() {
+	coproc timeout 10s stdbuf -oL $BATS_TEST_DIRNAME/"$@" 2>&1
+	DUT_PID=$COPROC_PID
+	read -t1 -n1 -u ${COPROC[0]} DUT_FIRST_CHAR
+}
+
+dut_run_redirect() {
+	coproc timeout 10s $BATS_TEST_DIRNAME/"$@" > $DUT_OUTPUT 2>&1
+	DUT_PID=$COPROC_PID
+	# give the process time to spin up
+	# FIXME - find a better solution
 	sleep 0.2
 }
 
-coproc_tool_stdin_write() {
+dut_read_redirect() {
+	output=$(<$DUT_OUTPUT)
+        local ORIG_IFS="$IFS"
+        IFS=$'\n' lines=($output)
+        IFS="$ORIG_IFS"
+}
+
+dut_read() {
+	local LINE
+	lines=()
+	while read -t 0.2 -u ${COPROC[0]} LINE;
+	do
+		if [ -n "$DUT_FIRST_CHAR" ]
+		then
+			LINE=${DUT_FIRST_CHAR}${LINE}
+			unset DUT_FIRST_CHAR
+		fi
+		lines+=("$LINE")
+	done
+	output="${lines[@]}"
+}
+
+dut_readable() {
+	read -t 0 -u ${COPROC[0]} LINE
+}
+
+dut_flush() {
+	local JUNK
+	lines=()
+	output=
+	unset DUT_FIRST_CHAR
+	while read -t 0 -u ${COPROC[0]} JUNK;
+	do
+		read -t 0.1 -u ${COPROC[0]} JUNK || true
+	done
+}
+
+# check the next line of output matches the regex
+dut_regex_match() {
+	PATTERN=$1
+
+	read -t 0.2 -u ${COPROC[0]} LINE || (echo Timeout && false)
+	if [ -n "$DUT_FIRST_CHAR" ]
+	then
+		LINE=${DUT_FIRST_CHAR}${LINE}
+		unset DUT_FIRST_CHAR
+	fi
+	[[ $LINE =~ $PATTERN ]] || (echo "Mismatched: \"$LINE\"" && false)
+}
+
+dut_write() {
 	echo $* >&${COPROC[1]}
 }
 
-coproc_tool_kill() {
+dut_kill() {
 	SIGNUM=$1
 
-	kill $SIGNUM $COPROC_SAVED_PID
+	kill $SIGNUM $DUT_PID
 }
 
-coproc_tool_wait() {
+dut_wait() {
 	status="0"
 	# A workaround for the way bats handles command failures.
-	wait $COPROC_SAVED_PID || export status=$?
+	wait $DUT_PID || export status=$?
 	test "$status" -ne 0 || export status="0"
-	output=$(cat $COPROC_OUTPUT)
-	local ORIG_IFS="$IFS"
-	IFS=$'\n' lines=($output)
-	IFS="$ORIG_IFS"
-	rm -f $COPROC_OUTPUT
+	unset DUT_PID
 }
 
-teardown() {
-	if [ -n "$BG_PROC_PID" ]
-	then
-		kill -9 $BG_PROC_PID
-		run wait $BG_PROC_PID
-		BG_PROC_PID=""
-	fi
+dut_cleanup() {
+        if [ -n "$DUT_PID" ]
+        then
+		kill -SIGTERM $DUT_PID
+		wait $DUT_PID || false
+        fi
+        rm -f $DUT_OUTPUT
+}
 
+teardown() {
+	dut_cleanup
 	gpiosim_cleanup
 }
 
+request_release_line() {
+	$BATS_TEST_DIRNAME/gpioget -c $* >/dev/null
+}
+
 #
 # gpiodetect test cases
 #
 
-@test "gpiodetect: list chips" {
+@test "gpiodetect: all chips" {
 	gpiosim_chip sim0 num_lines=4
 	gpiosim_chip sim1 num_lines=8
 	gpiosim_chip sim2 num_lines=16
 
 	run_tool gpiodetect
 
-	test "$status" -eq 0
+	status_is 0
 	output_contains_line "$(gpiosim_chip_name sim0) [$(gpiosim_dev_name sim0)-node0] (4 lines)"
 	output_contains_line "$(gpiosim_chip_name sim1) [$(gpiosim_dev_name sim1)-node0] (8 lines)"
 	output_contains_line "$(gpiosim_chip_name sim2) [$(gpiosim_dev_name sim2)-node0] (16 lines)"
 }
 
-@test "gpiodetect: invalid args" {
-	run_tool gpiodetect unimplemented-arg
-	test "$status" -eq 1
+@test "gpiodetect: a chip" {
+	gpiosim_chip sim0 num_lines=4
+	gpiosim_chip sim1 num_lines=8
+	gpiosim_chip sim2 num_lines=16
+
+	# by name
+	run_tool gpiodetect $(gpiosim_chip_name sim0)
+
+	status_is 0
+	num_lines_is 1
+	output_contains_line "$(gpiosim_chip_name sim0) [$(gpiosim_dev_name sim0)-node0] (4 lines)"
+
+	# by path
+	run_tool gpiodetect $(gpiosim_chip_path sim1)
+
+	status_is 0
+	num_lines_is 1
+	output_contains_line "$(gpiosim_chip_name sim1) [$(gpiosim_dev_name sim1)-node0] (8 lines)"
+
+	# by number
+	run_tool gpiodetect $(gpiosim_chip_number sim2)
+
+	status_is 0
+	num_lines_is 1
+	output_contains_line "$(gpiosim_chip_name sim2) [$(gpiosim_dev_name sim2)-node0] (16 lines)"
+}
+
+@test "gpiodetect: multiple chips" {
+	gpiosim_chip sim0 num_lines=4
+	gpiosim_chip sim1 num_lines=8
+	gpiosim_chip sim2 num_lines=16
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
+	local sim2=$(gpiosim_chip_name sim2)
+
+	run_tool gpiodetect $sim0 $sim1 $sim2
+
+	status_is 0
+	num_lines_is 3
+	output_contains_line "$sim0 [$(gpiosim_dev_name sim0)-node0] (4 lines)"
+	output_contains_line "$sim1 [$(gpiosim_dev_name sim1)-node0] (8 lines)"
+	output_contains_line "$sim2 [$(gpiosim_dev_name sim2)-node0] (16 lines)"
+}
+
+@test "gpiodetect: with nonexistent chip" {
+	run_tool gpiodetect nonexistent-chip
+
+	status_is 1
+	output_regex_match \
+".*cannot find a GPIO chip character device corresponding to nonexistent-chip"
 }
 
 #
 # gpioinfo test cases
 #
 
-@test "gpioinfo: dump all chips" {
+@test "gpioinfo: all chips" {
 	gpiosim_chip sim0 num_lines=4
 	gpiosim_chip sim1 num_lines=8
 
 	run_tool gpioinfo
 
-	test "$status" -eq 0
+	status_is 0
 	output_contains_line "$(gpiosim_chip_name sim0) - 4 lines:"
 	output_contains_line "$(gpiosim_chip_name sim1) - 8 lines:"
-
-	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high"
-	output_regex_match "\\s+line\\s+7:\\s+unnamed\\s+unused\\s+input\\s+active-high"
+	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+7:\\s+unnamed\\s+unused\\s+\\[input\\]"
 }
 
-@test "gpioinfo: dump all chips with one line exported" {
-	gpiosim_chip sim0 num_lines=4
-	gpiosim_chip sim1 num_lines=8
+@test "gpioinfo: all chips with some used lines" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+	gpiosim_chip sim1 num_lines=8 line_name=3:baz line_name=4:xyz
 
-	coproc_run_tool gpioset --mode=signal --active-low "$(gpiosim_chip_name sim1)" 7=1
+	dut_run gpioset -i --active-low foo=1 baz=0
 
 	run_tool gpioinfo
 
-	test "$status" -eq 0
+	status_is 0
 	output_contains_line "$(gpiosim_chip_name sim0) - 4 lines:"
 	output_contains_line "$(gpiosim_chip_name sim1) - 8 lines:"
-	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high"
-	output_regex_match "\\s+line\\s+7:\\s+unnamed\\s+\\\"gpioset\\\"\\s+output\\s+active-low"
-
-	coproc_tool_kill
-	coproc_tool_wait
+	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+1:\\s+foo\\s+gpioset\\s+\\[output\\s+active-low\\s+used\\]"
+	output_regex_match "\\s+line\\s+3:\\s+baz\\s+gpioset\\s+\\[output\\s+active-low\\s+used\\]"
 }
 
-@test "gpioinfo: dump one chip" {
+@test "gpioinfo: a chip" {
 	gpiosim_chip sim0 num_lines=8
 	gpiosim_chip sim1 num_lines=4
+	local sim1=$(gpiosim_chip_name sim1)
+
+	# by name
+	run_tool gpioinfo --chip $sim1
+
+	status_is 0
+	num_lines_is 5
+	output_contains_line "$sim1 - 4 lines:"
+	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+unused\\s+\\[input\\]"
+
+	# by path
+	run_tool gpioinfo --chip $sim1
+
+	status_is 0
+	num_lines_is 5
+	output_contains_line "$sim1 - 4 lines:"
+	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+unused\\s+\\[input\\]"
+
+	# by number
+	run_tool gpioinfo --chip $sim1
+
+	status_is 0
+	num_lines_is 5
+	output_contains_line "$sim1 - 4 lines:"
+	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+unused\\s+\\[input\\]"
+}
+
+@test "gpioinfo: a line" {
+	gpiosim_chip sim0 num_lines=8 line_name=5:bar
+	gpiosim_chip sim1 num_lines=4 line_name=2:bar
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
+
+	# by offset
+	run_tool gpioinfo --chip $sim1 2
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$sim1\\s+2\\s+bar\\s+unused\\s+\\[input\\]"
+
+	# by name
+	run_tool gpioinfo bar
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$sim0\\s+5\\s+bar\\s+unused\\s+\\[input\\]"
+
+	# by chip and name
+	run_tool gpioinfo --chip $sim1 2
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$sim1\\s+2\\s+bar\\s+unused\\s+\\[input\\]"
+}
+
+@test "gpioinfo: first matching named line" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
 
-	run_tool gpioinfo "$(gpiosim_chip_name sim1)"
+	run_tool gpioinfo foobar
 
-	test "$status" -eq 0
-	assert_fail output_contains_line "$(gpiosim_chip_name sim0) - 8 lines:"
-	output_contains_line "$(gpiosim_chip_name sim1) - 4 lines:"
-	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high"
-	assert_fail output_regex_match "\\s+line\\s+7:\\s+unnamed\\s+unused\\s+input\\s+active-high"
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$(gpiosim_chip_name sim0)\\s+3\\s+foobar\\s+unused\\s+\\[input\\]"
 }
 
-@test "gpioinfo: dump all but one chip" {
-	gpiosim_chip sim0 num_lines=4
-	gpiosim_chip sim1 num_lines=4
-	gpiosim_chip sim2 num_lines=8
-	gpiosim_chip sim3 num_lines=4
+@test "gpioinfo: multiple lines" {
+	gpiosim_chip sim0 num_lines=8 line_name=5:bar
+	gpiosim_chip sim1 num_lines=4 line_name=2:baz
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
 
-	run_tool gpioinfo "$(gpiosim_chip_name sim0)" \
-			"$(gpiosim_chip_name sim1)" "$(gpiosim_chip_name sim3)"
+	# by offset
+	run_tool gpioinfo --chip $sim1 1 2
 
-	test "$status" -eq 0
-	output_contains_line "$(gpiosim_chip_name sim0) - 4 lines:"
-	output_contains_line "$(gpiosim_chip_name sim1) - 4 lines:"
-	assert_fail output_contains_line "$(gpiosim_chip_name sim2) - 8 lines:"
-	output_contains_line "$(gpiosim_chip_name sim3) - 4 lines:"
-	output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high"
-	assert_fail output_regex_match "\\s+line\\s+7:\\s+unnamed\\s+unused\\s+input\\s+active-high"
+	status_is 0
+	num_lines_is 2
+	output_regex_match "$(gpiosim_chip_name sim1)\\s+1\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "$(gpiosim_chip_name sim1)\\s+2\\s+baz\\s+unused\\s+\\[input\\]"
+
+	# by name
+	run_tool gpioinfo bar baz
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match "$(gpiosim_chip_name sim0)\\s+5\\s+bar\\s+unused\\s+\\[input\\]"
+	output_regex_match "$(gpiosim_chip_name sim1)\\s+2\\s+baz\\s+unused\\s+\\[input\\]"
+
+	# by name and offset
+	run_tool gpioinfo --chip $(gpiosim_chip_name sim0) bar 3
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match "$(gpiosim_chip_name sim0)\\s+5\\s+bar\\s+unused\\s+\\[input\\]"
+	output_regex_match "$(gpiosim_chip_name sim0)\\s+3\\s+unnamed\\s+unused\\s+\\[input\\]"
+}
+
+@test "gpioinfo: line attribute menagerie" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo
+	gpiosim_chip sim1 num_lines=8 line_name=3:baz
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
+
+	dut_run gpioset -i --active-low --bias=pull-up --drive=open-source foo=1 baz=0
+
+	run_tool gpioinfo foo baz
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match \
+"$sim0\\s+1\\s+foo\\s+gpioset\\s+\\[output\\s+active-low\\s+used\\s+open-source\\s+pull-up\\]"
+	output_regex_match \
+"$sim1\\s+3\\s+baz\\s+gpioset\\s+\\[output\\s+active-low\\s+used\\s+open-source\\s+pull-up\\]"
+
+	dut_kill
+	dut_wait
+
+	dut_run gpioset -i --bias=pull-down --drive=open-drain foo=1 baz=0
+
+	run_tool gpioinfo foo baz
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match \
+"$sim0\\s+1\\s+foo\\s+gpioset\\s+\\[output\\s+used\\s+open-drain\\s+pull-down\\]"
+	output_regex_match \
+"$sim1\\s+3\\s+baz\\s+gpioset\\s+\\[output\\s+used\\s+open-drain\\s+pull-down\\]"
+
+	dut_kill
+	dut_wait
+
+	dut_run gpiomon --banner --bias=disabled --utc --debounce-period=10ms foo baz
+
+	run_tool gpioinfo foo baz
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match \
+"$sim0\\s+1\\s+foo\\s+gpiomon\\s+\\[input\\s+used\\s+bias-disabled\\s+both-edges\\s+event-clock-realtime\\s+debounce_period=10000us\\]"
+	output_regex_match \
+"$sim1\\s+3\\s+baz\\s+gpiomon\\s+\\[input\\s+used\\s+bias-disabled\\s+both-edges\\s+event-clock-realtime\\s+debounce_period=10000us\\]"
+
+	dut_kill
+	dut_wait
+
+	dut_run gpiomon --banner --edge=rising --localtime foo baz
+
+	run_tool gpioinfo foo baz
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match \
+"$sim0\\s+1\\s+foo\\s+gpiomon\\s+\\[input\\s+used\\s+rising-edges\\s+event-clock-realtime\\]"
+	output_regex_match \
+"$sim1\\s+3\\s+baz\\s+gpiomon\\s+\\[input\\s+used\\s+rising-edges\\s+event-clock-realtime\\]"
+
+	dut_kill
+	dut_wait
+
+	dut_run gpiomon --banner --edge=falling foo baz
+
+	run_tool gpioinfo foo baz
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match "$sim0\\s+1\\s+foo\\s+gpiomon\\s+\\[input\\s+used\\s+falling-edges\\]"
+	output_regex_match "$sim1\\s+3\\s+baz\\s+gpiomon\\s+\\[input\\s+used\\s+falling-edges\\]"
+}
+
+@test "gpioinfo: with same line twice" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	local sim0=$(gpiosim_chip_name sim0)
+
+	# by offset
+	run_tool gpioinfo --chip $sim0 1 1
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$sim0\\s+1\\s+foo\\s+unused\\s+\\[input\\]"
+
+	# by name
+	run_tool gpioinfo foo foo
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$sim0\\s+1\\s+foo\\s+unused\\s+\\[input\\]"
+
+	# by name and offset
+	run_tool gpioinfo --chip $sim0 foo 1
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$sim0\\s+1\\s+foo\\s+unused\\s+\\[input\\]"
+}
+
+@test "gpioinfo: all lines matching name" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
+
+	run_tool gpioinfo --strict foobar
+
+	status_is 1
+	num_lines_is 3
+	output_regex_match "$sim0\\s+3\\s+foobar\\s+unused\\s+\\[input\\]"
+	output_regex_match "$sim1\\s+2\\s+foobar\\s+unused\\s+\\[input\\]"
+	output_regex_match "$sim1\\s+7\\s+foobar\\s+unused\\s+\\[input\\]"
 }
 
-@test "gpioinfo: inexistent chip" {
-	run_tool gpioinfo "inexistent"
+@test "gpioinfo: with lines strictly by name" {
+	# not suggesting this setup makes sense - just test that we can deal with it
+	gpiosim_chip sim0 num_lines=8 line_name=1:42 line_name=6:13
+	local sim0=$(gpiosim_chip_name sim0)
 
-	test "$status" -eq 1
+	run_tool gpioinfo --by-name --chip $sim0 42 13
+
+	status_is 0
+	num_lines_is 2
+	output_regex_match "$sim0\\s+1\\s+42\\s+unused\\s+\\[input\\]"
+	output_regex_match "$sim0\\s+6\\s+13\\s+unused\\s+\\[input\\]"
+}
+
+@test "gpioinfo: with nonexistent chip" {
+	run_tool gpioinfo --chip nonexistent-chip
+
+	status_is 1
+	output_regex_match \
+".*cannot find a GPIO chip character device corresponding to nonexistent-chip"
+}
+
+@test "gpioinfo: with nonexistent line" {
+	gpiosim_chip sim0 num_lines=8
+
+	run_tool gpioinfo nonexistent-line
+
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
+
+	run_tool gpioinfo --chip $(gpiosim_chip_name sim0) nonexistent-line
+
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
+}
+
+@test "gpioinfo: with offset out of range" {
+	gpiosim_chip sim0 num_lines=4
+	local sim0=$(gpiosim_chip_name sim0)
+
+	run_tool gpioinfo --chip $sim0 0 1 2 3 4 5
+
+	status_is 1
+	num_lines_is 6
+	output_regex_match "$sim0\\s+0\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "$sim0\\s+1\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "$sim0\\s+2\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match "$sim0\\s+3\\s+unnamed\\s+unused\\s+\\[input\\]"
+	output_regex_match ".*offset 4 is out of range on chip $sim0"
+	output_regex_match ".*offset 5 is out of range on chip $sim0"
 }
 
 #
 # gpiofind test cases
 #
 
-@test "gpiofind: line found" {
+@test "gpiofind: by name" {
 	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=3:bar
 	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz line_name=7:foobar
 	gpiosim_chip sim2 num_lines=16
 
 	run_tool gpiofind foobar
 
-	test "$status" -eq "0"
-	test "$output" = "$(gpiosim_chip_name sim1) 7"
+	status_is 0
+	output_is "$(gpiosim_chip_name sim1) 7"
+}
+
+@test "gpiofind: by chip and name" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+	local sim1=$(gpiosim_chip_name sim1)
+
+	run_tool gpiofind --chip $sim1 foobar
+
+	status_is 0
+	output_is "$sim1 2"
+}
+
+@test "gpiofind: first matching named line" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	run_tool gpiofind foobar
+
+	status_is 0
+	output_is "$(gpiosim_chip_name sim0) 3"
+}
+
+@test "gpiofind: with info" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=3:bar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	run_tool gpiofind --info baz
+
+	status_is 0
+	num_lines_is 1
+	output_regex_match "$(gpiosim_chip_name sim1)\\s+0\\s+baz\\s+unused\\s+\\[input\\]"
 }
 
-@test "gpiofind: line not found" {
+@test "gpiofind: all lines matching name" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
+
+	run_tool gpiofind --strict foobar
+
+	status_is 1
+	num_lines_is 3
+	output_regex_match "$sim0\\s+3"
+	output_regex_match "$sim1\\s+2"
+	output_regex_match "$sim1\\s+7"
+}
+
+@test "gpiofind: with nonexistent chip" {
+	run_tool gpiofind --chip nonexistant-chip 0
+
+	status_is 1
+	output_regex_match \
+".*cannot find a GPIO chip character device corresponding to nonexistant-chip"
+}
+
+@test "gpiofind: with nonexistent line" {
 	gpiosim_chip sim0 num_lines=4
 	gpiosim_chip sim1 num_lines=8
 	gpiosim_chip sim2 num_lines=16
 
 	run_tool gpiofind nonexistent-line
 
-	test "$status" -eq "1"
-}
-
-@test "gpiofind: invalid args" {
-	run_tool gpiodetect unimplemented-arg
-	test "$status" -eq 1
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
 }
 
 #
 # gpioget test cases
 #
 
-@test "gpioget: read all lines" {
+@test "gpioget: by name" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+	gpiosim_set_pull sim0 1 pull-up
+
+	run_tool gpioget foo
+
+	status_is 0
+	output_is "foo=active"
+}
+
+@test "gpioget: by offset" {
+	gpiosim_chip sim0 num_lines=8
+
+	gpiosim_set_pull sim0 1 pull-up
+
+	run_tool gpioget --chip $(gpiosim_chip_name sim0) 1
+
+	status_is 0
+	output_is "1=active"
+}
+
+@test "gpioget: by chip and name" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	gpiosim_chip sim1 num_lines=8 line_name=3:foo
+
+	gpiosim_set_pull sim1 3 pull-up
+
+	run_tool gpioget --chip $(gpiosim_chip_name sim1) foo
+
+	status_is 0
+	output_is "foo=active"
+}
+
+@test "gpioget: first matching named line" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+				      line_name=3:foobar line_name=7:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar line_name=4:xyz
+	gpiosim_chip sim2 num_lines=16
+
+	gpiosim_set_pull sim0 3 pull-up
+
+	run_tool gpioget foobar
+
+	status_is 0
+	output_is "foobar=active"
+}
+
+@test "gpioget: multiple lines" {
 	gpiosim_chip sim0 num_lines=8
 
 	gpiosim_set_pull sim0 2 pull-up
@@ -357,13 +794,41 @@ teardown() {
 	gpiosim_set_pull sim0 5 pull-up
 	gpiosim_set_pull sim0 7 pull-up
 
-	run_tool gpioget "$(gpiosim_chip_name sim0)" 0 1 2 3 4 5 6 7
+	run_tool gpioget --chip $(gpiosim_chip_name sim0) 0 1 2 3 4 5 6 7
 
-	test "$status" -eq "0"
-	test "$output" = "0 0 1 1 0 1 0 1"
+	status_is 0
+	output_is "0=inactive 1=inactive 2=active 3=active 4=inactive 5=active 6=inactive 7=active"
 }
 
-@test "gpioget: read all lines (active-low)" {
+@test "gpioget: multiple lines by name and offset" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=6:bar
+	gpiosim_chip sim1 num_lines=8 line_name=1:baz line_name=3:bar
+	local sim0=$(gpiosim_chip_name sim0)
+
+	gpiosim_set_pull sim0 1 pull-up
+	gpiosim_set_pull sim0 4 pull-up
+	gpiosim_set_pull sim0 6 pull-up
+
+	run_tool gpioget --chip $sim0 0 foo 4 bar
+
+	status_is 0
+	output_is "0=inactive foo=active 4=active bar=active"
+}
+
+@test "gpioget: multiple lines across multiple chips" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+
+	gpiosim_set_pull sim0 1 pull-up
+	gpiosim_set_pull sim1 4 pull-up
+
+	run_tool gpioget baz bar foo xyz
+
+	status_is 0
+	output_is "baz=inactive bar=inactive foo=active xyz=active"
+}
+
+@test "gpioget: with numeric values" {
 	gpiosim_chip sim0 num_lines=8
 
 	gpiosim_set_pull sim0 2 pull-up
@@ -371,13 +836,13 @@ teardown() {
 	gpiosim_set_pull sim0 5 pull-up
 	gpiosim_set_pull sim0 7 pull-up
 
-	run_tool gpioget --active-low "$(gpiosim_chip_name sim0)" 0 1 2 3 4 5 6 7
+	run_tool gpioget --numeric --chip $(gpiosim_chip_name sim0) 0 1 2 3 4 5 6 7
 
-	test "$status" -eq "0"
-	test "$output" = "1 1 0 0 1 0 1 0"
+	status_is 0
+	output_is "0 0 1 1 0 1 0 1"
 }
 
-@test "gpioget: read all lines (pull-up)" {
+@test "gpioget: with active-low" {
 	gpiosim_chip sim0 num_lines=8
 
 	gpiosim_set_pull sim0 2 pull-up
@@ -385,13 +850,13 @@ teardown() {
 	gpiosim_set_pull sim0 5 pull-up
 	gpiosim_set_pull sim0 7 pull-up
 
-	run_tool gpioget --bias=pull-up "$(gpiosim_chip_name sim0)" 0 1 2 3 4 5 6 7
+	run_tool gpioget --active-low --chip $(gpiosim_chip_name sim0) 0 1 2 3 4 5 6 7
 
-	test "$status" -eq "0"
-	test "$output" = "1 1 1 1 1 1 1 1"
+	status_is 0
+	output_is "0=active 1=active 2=inactive 3=inactive 4=active 5=inactive 6=active 7=inactive"
 }
 
-@test "gpioget: read all lines (pull-down)" {
+@test "gpioget: with pull-up" {
 	gpiosim_chip sim0 num_lines=8
 
 	gpiosim_set_pull sim0 2 pull-up
@@ -399,717 +864,1208 @@ teardown() {
 	gpiosim_set_pull sim0 5 pull-up
 	gpiosim_set_pull sim0 7 pull-up
 
-	run_tool gpioget --bias=pull-down "$(gpiosim_chip_name sim0)" 0 1 2 3 4 5 6 7
+	run_tool gpioget --bias=pull-up --chip $(gpiosim_chip_name sim0) 0 1 2 3 4 5 6 7
 
-	test "$status" -eq "0"
-	test "$output" = "0 0 0 0 0 0 0 0"
+	status_is 0
+	output_is "0=active 1=active 2=active 3=active 4=active 5=active 6=active 7=active"
 }
 
-@test "gpioget: read some lines" {
+@test "gpioget: with pull-down" {
 	gpiosim_chip sim0 num_lines=8
 
+	gpiosim_set_pull sim0 2 pull-up
+	gpiosim_set_pull sim0 3 pull-up
+	gpiosim_set_pull sim0 5 pull-up
+	gpiosim_set_pull sim0 7 pull-up
+
+	run_tool gpioget --bias=pull-down --chip $(gpiosim_chip_name sim0) 0 1 2 3 4 5 6 7
+
+	status_is 0
+	output_is \
+"0=inactive 1=inactive 2=inactive 3=inactive 4=inactive 5=inactive 6=inactive 7=inactive"
+}
+
+@test "gpioget: with direction as-is" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	local sim0=$(gpiosim_chip_name sim0)
+
+	# flip to output
+	run_tool gpioset foo=1
+
+	status_is 0
+
+	run_tool gpioinfo foo
+	status_is 0
+	output_regex_match "$sim0\\s+1\\s+foo\\s+unused\\s+\\[output\\]"
+
+	run_tool gpioget --as-is foo
+	status_is 0
+	# note gpio-sim reverts line to its pull when released
+	output_is "foo=inactive"
+
+	run_tool gpioinfo foo
+	status_is 0
+	output_regex_match "$sim0\\s+1\\s+foo\\s+unused\\s+\\[output\\]"
+
+	# whereas the default behaviour forces to input
+	run_tool gpioget foo
+	status_is 0
+	# note gpio-sim reverts line to its pull when released (defaults to pull-down)
+	output_is "foo=inactive"
+
+	run_tool gpioinfo foo
+	status_is 0
+	output_regex_match "$sim0\\s+1\\s+foo\\s+unused\\s+\\[input\\]"
+}
+
+@test "gpioget: with hold-period" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+	# only test parsing - testing the hold-period itself is tricky
+	run_tool gpioget --hold-period=100ms foo
+	status_is 0
+	output_is "foo=inactive"
+}
+
+@test "gpioget: with strict named line check" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	run_tool gpioget --strict foobar
+
+	status_is 1
+	output_regex_match ".*line foobar is not unique"
+}
+
+@test "gpioget: with lines strictly by name" {
+	# not suggesting this setup makes sense - just test that we can deal with it
+	gpiosim_chip sim0 num_lines=8 line_name=1:42 line_name=6:13
+
 	gpiosim_set_pull sim0 1 pull-up
-	gpiosim_set_pull sim0 4 pull-up
-	gpiosim_set_pull sim0 6 pull-up
+	gpiosim_set_pull sim0 6 pull-down
 
-	run_tool gpioget "$(gpiosim_chip_name sim0)" 0 1 4 6
+	run_tool gpioget --by-name --chip $(gpiosim_chip_name sim0) 42 13
 
-	test "$status" -eq "0"
-	test "$output" = "0 1 1 1"
+	status_is 0
+	output_is "42=active 13=inactive"
 }
 
-@test "gpioget: no arguments" {
+@test "gpioget: with no arguments" {
 	run_tool gpioget
 
-	test "$status" -eq "1"
-	output_regex_match ".*gpiochip must be specified"
+	status_is 1
+	output_regex_match ".*at least one GPIO line must be specified"
+}
+
+@test "gpioget: with chip but no line specified" {
+	gpiosim_chip sim0 num_lines=8
+
+	run_tool gpioget --chip $(gpiosim_chip_name sim0)
+
+	status_is 1
+	output_regex_match ".*at least one GPIO line must be specified"
+}
+
+@test "gpioget: with offset out of range" {
+	gpiosim_chip sim0 num_lines=4
+	local sim0=$(gpiosim_chip_name sim0)
+
+	run_tool gpioget --chip $sim0 0 1 2 3 4 5
+
+	status_is 1
+	output_regex_match ".*offset 4 is out of range on chip $sim0"
+	output_regex_match ".*offset 5 is out of range on chip $sim0"
+}
+
+@test "gpioget: with nonexistent line" {
+	run_tool gpioget nonexistent-line
+
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
+}
+
+@test "gpioget: with same line twice" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	local sim0=$(gpiosim_chip_name sim0)
+
+	# by offset
+	run_tool gpioget --chip $sim0 0 0
+
+	status_is 1
+	output_regex_match ".*lines 0 and 0 are the same"
+
+	# by name
+	run_tool gpioget foo foo
+
+	status_is 1
+	output_regex_match ".*lines foo and foo are the same"
+
+	# by chip and name
+	run_tool gpioget --chip $sim0 foo foo
+
+	status_is 1
+	output_regex_match ".*lines foo and foo are the same"
+}
+
+@test "gpioget: with invalid bias" {
+	gpiosim_chip sim0 num_lines=8
+
+	run_tool gpioget --bias=bad --chip $(gpiosim_chip_name sim0) 0 1
+
+	status_is 1
+	output_regex_match ".*invalid bias.*"
+}
+
+@test "gpioget: with invalid hold-period" {
+	gpiosim_chip sim0 num_lines=8
+
+	run_tool gpioget --hold-period=bad --chip $(gpiosim_chip_name sim0) 0
+
+	status_is 1
+	output_regex_match ".*invalid period.*"
+}
+
+#
+# gpioset test cases
+#
+
+@test "gpioset: by name" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+	dut_run gpioset -i foo=1
+
+	gpiosim_check_value sim0 1 1
+}
+
+@test "gpioset: by offset" {
+	gpiosim_chip sim0 num_lines=8
+
+	dut_run gpioset -i --chip $(gpiosim_chip_name sim0) 1=1
+
+	gpiosim_check_value sim0 1 1
+}
+
+@test "gpioset: by chip and name" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	gpiosim_chip sim1 num_lines=8 line_name=3:foo
+
+	dut_run gpioset -i --chip $(gpiosim_chip_name sim1) foo=1
+
+	gpiosim_check_value sim1 3 1
+}
+
+@test "gpioset: first matching named line" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	dut_run gpioset -i foobar=1
+
+	gpiosim_check_value sim0 3 1
+}
+
+@test "gpioset: multiple lines" {
+	gpiosim_chip sim0 num_lines=8
+
+	dut_run gpioset -i --chip $(gpiosim_chip_name sim0) 0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 1
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+}
+
+@test "gpioset: multiple lines by name and offset" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+
+	dut_run gpioset -i --chip $(gpiosim_chip_name sim0) 0=1 foo=1 bar=1 3=1
+
+	gpiosim_check_value sim0 0 1
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 1
+}
+
+
+@test "gpioset: multiple lines across multiple chips" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+
+	dut_run gpioset -i foo=1 bar=1 baz=1 xyz=1
+
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim1 0 1
+	gpiosim_check_value sim1 4 1
+}
+
+@test "gpioset: with active-low" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpioset -i --active-low --chip $sim0 0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 0 1
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 2 0
+	gpiosim_check_value sim0 3 0
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 5 0
+	gpiosim_check_value sim0 6 1
+	gpiosim_check_value sim0 7 0
+}
+
+@test "gpioset: with push-pull" {
+	gpiosim_chip sim0 num_lines=8
+
+	dut_run gpioset -i --drive=push-pull --chip $(gpiosim_chip_name sim0) \
+					0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 1
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+}
+
+@test "gpioset: with open-drain" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	gpiosim_set_pull sim0 2 pull-up
+	gpiosim_set_pull sim0 3 pull-up
+	gpiosim_set_pull sim0 5 pull-up
+	gpiosim_set_pull sim0 7 pull-up
+
+	dut_run gpioset -i --drive=open-drain --chip $sim0 0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 1
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+}
+
+@test "gpioset: with open-source" {
+	gpiosim_chip sim0 num_lines=8
+
+	gpiosim_set_pull sim0 2 pull-up
+	gpiosim_set_pull sim0 3 pull-up
+	gpiosim_set_pull sim0 5 pull-up
+	gpiosim_set_pull sim0 7 pull-up
+
+	dut_run gpioset -i --drive=open-source --chip $(gpiosim_chip_name sim0) \
+					0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 1
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+}
+
+@test "gpioset: with pull-up" {
+	gpiosim_chip sim0 num_lines=8
+
+	dut_run gpioset -i --bias=pull-up --drive=open-drain \
+			--chip $(gpiosim_chip_name sim0) 0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 0
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
 }
 
-@test "gpioget: no lines specified" {
+@test "gpioset: with pull-down" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioget "$(gpiosim_chip_name sim0)"
+	dut_run gpioset -i --bias=pull-down --drive=open-source \
+			--chip $(gpiosim_chip_name sim0) 0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*at least one GPIO line offset must be specified"
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 0
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+}
+
+@test "gpioset: with value variants" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	gpiosim_set_pull sim0 0 pull-up
+	gpiosim_set_pull sim0 1 pull-down
+	gpiosim_set_pull sim0 2 pull-down
+	gpiosim_set_pull sim0 3 pull-up
+	gpiosim_set_pull sim0 4 pull-down
+	gpiosim_set_pull sim0 5 pull-up
+	gpiosim_set_pull sim0 6 pull-up
+	gpiosim_set_pull sim0 7 pull-down
+
+	dut_run gpioset -i --chip $sim0	0=0 1=1 2=active 3=inactive 4=on 5=off 6=false 7=true
+
+	gpiosim_check_value sim0 0 0
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 3 0
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 5 0
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+}
+
+@test "gpioset: with hold-period" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	gpiosim_set_pull sim0 5 pull-up
+
+	# redirect as no startup output for dut_run
+	dut_run_redirect gpioset --hold-period=1200ms --chip $sim0 0=1 5=0 7=1
+
+	gpiosim_check_value sim0 0 1
+	gpiosim_check_value sim0 5 0
+	gpiosim_check_value sim0 7 1
+
+	dut_wait
+
+	status_is 0
+}
+
+@test "gpioset: interactive exit" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpioset --interactive --chip $sim0 1=0 2=1 5=1 6=0 7=1
+
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 2 1
+	gpiosim_check_value sim0 5 1
+	gpiosim_check_value sim0 6 0
+	gpiosim_check_value sim0 7 1
+
+	dut_write "exit"
+	dut_wait
+
+	status_is 0
+}
+
+@test "gpioset: interactive help" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
+
+	dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 7 0
+
+	dut_write "help"
+
+	dut_read
+	output_regex_match "COMMANDS:.*"
+	output_regex_match ".*get \[line\] \.\.\..*"
+	output_regex_match ".*set <line=value> \.\.\..*"
+	output_regex_match ".*toggle \[line\] \.\.\..*"
+	output_regex_match ".*sleep <period>.*"
+}
+
+@test "gpioset: interactive get" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
+
+	dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 7 0
+
+	dut_write "get"
+
+	dut_read
+	output_regex_match "\\s*foo=active\\s+bar=inactive\\s+baz=inactive\\s*"
+
+	dut_write "get bar"
+
+	dut_read
+	output_regex_match "\\s*bar=inactive\\s*"
+}
+
+@test "gpioset: interactive set" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
+
+	dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 7 0
+
+	dut_write "set bar=active"
+
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 7 0
+	dut_write "get"
+	dut_read
+	output_regex_match "\\s*foo=active\\s+bar=active\\s+baz=inactive\\s*"
 }
 
-@test "gpioget: too many lines specified" {
-	gpiosim_chip sim0 num_lines=4
-
-	run_tool gpioget "$(gpiosim_chip_name sim0)" 0 1 2 3 4
+@test "gpioset: interactive toggle" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
 
-	test "$status" -eq "1"
-	output_regex_match ".*unable to request lines.*"
-}
+	gpiosim_set_pull sim0 4 pull-up
+	gpiosim_set_pull sim0 7 pull-up
 
-@test "gpioget: same line twice" {
-	gpiosim_chip sim0 num_lines=8
+	dut_run gpioset --interactive foo=1 bar=0 baz=0
 
-	run_tool gpioget "$(gpiosim_chip_name sim0)" 0 0
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 7 0
 
-	test "$status" -eq "1"
-	output_regex_match ".*unable to request lines"
-}
+	dut_write "toggle"
 
-@test "gpioget: invalid bias" {
-	gpiosim_chip sim0 num_lines=8
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 7 1
+	dut_write "get"
+	dut_read
+	output_regex_match "\\s*foo=inactive\\s+bar=active\\s+baz=active\\s*"
 
-	run_tool gpioget --bias=bad "$(gpiosim_chip_name sim0)" 0 1
+	dut_write "toggle baz"
 
-	test "$status" -eq "1"
-	output_regex_match ".*invalid bias.*"
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 7 0
+	dut_write "get"
+	dut_read
+	output_regex_match "\\s*foo=inactive\\s+bar=active\\s+baz=inactive\\s*"
 }
 
-#
-# gpioset test cases
-#
+@test "gpioset: interactive sleep" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
 
-@test "gpioset: set lines and wait for SIGTERM" {
-	gpiosim_chip sim0 num_lines=8
+	dut_run gpioset --interactive foo=1 bar=0 baz=0
 
-	coproc_run_tool gpioset --mode=signal "$(gpiosim_chip_name sim0)" \
-					0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+	dut_write "sleep 500ms"
+	dut_flush
 
-	gpiosim_check_value sim0 0 0
-	gpiosim_check_value sim0 1 0
-	gpiosim_check_value sim0 2 1
-	gpiosim_check_value sim0 3 1
-	gpiosim_check_value sim0 4 1
-	gpiosim_check_value sim0 5 1
-	gpiosim_check_value sim0 6 0
-	gpiosim_check_value sim0 7 1
+	assert_fail dut_readable
 
-	coproc_tool_kill
-	coproc_tool_wait
+	sleep 1
 
-	test "$status" -eq "0"
+	# prompt, but not a full line...
+	dut_readable
 }
 
-@test "gpioset: set lines and wait for SIGTERM (active-low)" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpioset: toggle (continuous)" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
 
-	coproc_run_tool gpioset --active-low --mode=signal "$(gpiosim_chip_name sim0)" \
-					0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+	gpiosim_set_pull sim0 4 pull-up
+	gpiosim_set_pull sim0 7 pull-up
+
+	# redirect as gpioset has no banner
+	dut_run_redirect gpioset --toggle 1s foo=1 bar=0 baz=0
 
-	gpiosim_check_value sim0 0 1
 	gpiosim_check_value sim0 1 1
-	gpiosim_check_value sim0 2 0
-	gpiosim_check_value sim0 3 0
 	gpiosim_check_value sim0 4 0
-	gpiosim_check_value sim0 5 0
-	gpiosim_check_value sim0 6 1
 	gpiosim_check_value sim0 7 0
 
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-}
-
-@test "gpioset: set lines and wait for SIGTERM (push-pull)" {
-	gpiosim_chip sim0 num_lines=8
-
-	coproc_run_tool gpioset --drive=push-pull --mode=signal "$(gpiosim_chip_name sim0)" \
-					0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+	sleep 1
 
-	gpiosim_check_value sim0 0 0
 	gpiosim_check_value sim0 1 0
-	gpiosim_check_value sim0 2 1
-	gpiosim_check_value sim0 3 1
 	gpiosim_check_value sim0 4 1
-	gpiosim_check_value sim0 5 1
-	gpiosim_check_value sim0 6 0
 	gpiosim_check_value sim0 7 1
 
-	coproc_tool_kill
-	coproc_tool_wait
+	sleep 1
 
-	test "$status" -eq "0"
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 4 0
+	gpiosim_check_value sim0 7 0
 }
 
-@test "gpioset: set lines and wait for SIGTERM (open-drain)" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpioset: toggle (terminated)" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
 
-	gpiosim_set_pull sim0 2 pull-up
-	gpiosim_set_pull sim0 3 pull-up
-	gpiosim_set_pull sim0 5 pull-up
-	gpiosim_set_pull sim0 7 pull-up
+	gpiosim_set_pull sim0 4 pull-up
 
-	coproc_run_tool gpioset --drive=open-drain --mode=signal "$(gpiosim_chip_name sim0)" \
-					0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+	# redirect as gpioset has no banner
+	# hold-period to allow test to sample before gpioset exits
+	dut_run_redirect gpioset --toggle 1s,0 -p 600ms foo=1 bar=0 baz=1
 
-	gpiosim_check_value sim0 0 0
-	gpiosim_check_value sim0 1 0
-	gpiosim_check_value sim0 2 1
-	gpiosim_check_value sim0 3 1
+	gpiosim_check_value sim0 1 1
 	gpiosim_check_value sim0 4 0
-	gpiosim_check_value sim0 5 1
-	gpiosim_check_value sim0 6 0
 	gpiosim_check_value sim0 7 1
 
-	coproc_tool_kill
-	coproc_tool_wait
+	sleep 1
+
+	gpiosim_check_value sim0 1 0
+	gpiosim_check_value sim0 4 1
+	gpiosim_check_value sim0 7 0
+
+	dut_wait
 
-	test "$status" -eq "0"
+	status_is 0
 }
 
-@test "gpioset: set lines and wait for SIGTERM (open-source)" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpioset: with invalid toggle period" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar line_name=7:baz
 
-	gpiosim_set_pull sim0 2 pull-up
-	gpiosim_set_pull sim0 3 pull-up
-	gpiosim_set_pull sim0 5 pull-up
-	gpiosim_set_pull sim0 7 pull-up
+	run_tool gpioset --toggle 1ns foo=1 bar=0 baz=0
 
-	coproc_run_tool gpioset --drive=open-source --mode=signal "$(gpiosim_chip_name sim0)" \
-					0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
+	status_is 1
+	output_regex_match ".*invalid period.*"
+}
 
-	gpiosim_check_value sim0 0 0
-	gpiosim_check_value sim0 1 0
-	gpiosim_check_value sim0 2 1
-	gpiosim_check_value sim0 3 1
-	gpiosim_check_value sim0 4 1
-	gpiosim_check_value sim0 5 1
-	gpiosim_check_value sim0 6 0
-	gpiosim_check_value sim0 7 1
+@test "gpioset: with strict named line check" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
 
-	coproc_tool_kill
-	coproc_tool_wait
+	run_tool gpioset --strict foobar=active
 
-	test "$status" -eq "0"
+	status_is 1
+	output_regex_match ".*line foobar is not unique"
 }
 
-@test "gpioset: set some lines and wait for ENTER" {
-	gpiosim_chip sim0 num_lines=8
-
-	coproc_run_tool gpioset --mode=wait "$(gpiosim_chip_name sim0)" \
-					1=0 2=1 5=1 6=0 7=1
+@test "gpioset: with lines strictly by name" {
+	# not suggesting this setup makes sense - just test that we can deal with it
+	gpiosim_chip sim0 num_lines=8 line_name=1:42 line_name=6:13
 
-	gpiosim_check_value sim0 1 0
-	gpiosim_check_value sim0 2 1
-	gpiosim_check_value sim0 5 1
-	gpiosim_check_value sim0 6 0
-	gpiosim_check_value sim0 7 1
+	gpiosim_set_pull sim0 6 pull-up
 
-	coproc_tool_stdin_write ""
-	coproc_tool_wait
+	dut_run gpioset -i --by-name --chip $(gpiosim_chip_name sim0) 42=1 13=0
 
-	test "$status" -eq "0"
+	gpiosim_check_value sim0 1 1
+	gpiosim_check_value sim0 6 0
 }
 
-@test "gpioset: set some lines and wait for SIGINT" {
-	gpiosim_chip sim0 num_lines=4
-
-	coproc_run_tool gpioset --mode=signal "$(gpiosim_chip_name sim0)" 0=1
+@test "gpioset: interactive after SIGINT" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
 
-	gpiosim_check_value sim0 0 1
+	dut_run gpioset -i foo=1
 
-	coproc_tool_kill -SIGINT
-	coproc_tool_wait
+	dut_kill -SIGINT
+	dut_wait
 
-	test "$status" -eq "0"
+	status_is 130
 }
 
-@test "gpioset: set some lines and wait with --mode=time" {
-	gpiosim_chip sim0 num_lines=8
-
-	coproc_run_tool gpioset --mode=time --sec=1 --usec=200000 \
-				"$(gpiosim_chip_name sim0)" 0=1 5=0 7=1
+@test "gpioset: interactive after SIGTERM" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
 
-	gpiosim_check_value sim0 0 1
-	gpiosim_check_value sim0 5 0
-	gpiosim_check_value sim0 7 1
+	dut_run gpioset -i foo=1
 
-	coproc_tool_wait
+	dut_kill -SIGTERM
+	dut_wait
 
-	test "$status" -eq "0"
+	status_is 143
 }
 
-@test "gpioset: no arguments" {
+@test "gpioset: with no arguments" {
 	run_tool gpioset
 
-	test "$status" -eq "1"
-	output_regex_match ".*gpiochip must be specified"
+	status_is 1
+	output_regex_match ".*at least one GPIO line value must be specified"
 }
 
-@test "gpioset: no lines specified" {
+@test "gpioset: with chip but no line specified" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset "$(gpiosim_chip_name sim1)"
+	run_tool gpioset --chip $(gpiosim_chip_name sim0)
 
-	test "$status" -eq "1"
-	output_regex_match ".*at least one GPIO line offset to value mapping must be specified"
+	status_is 1
+	output_regex_match ".*at least one GPIO line value must be specified"
 }
 
-@test "gpioset: too many lines specified" {
+@test "gpioset: with offset out of range" {
 	gpiosim_chip sim0 num_lines=4
+	local sim0=$(gpiosim_chip_name sim0)
 
-	run_tool gpioset "$(gpiosim_chip_name sim0)" 0=1 1=1 2=1 3=1 4=1 5=1
+	run_tool gpioset --chip $sim0 0=1 1=1 2=1 3=1 4=1 5=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*unable to request lines.*"
+	status_is 1
+	output_regex_match ".*offset 4 is out of range on chip $sim0"
+	output_regex_match ".*offset 5 is out of range on chip $sim0"
 }
 
-@test "gpioset: use --sec without --mode=time" {
+@test "gpioset: with invalid hold-period" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset --mode=exit --sec=1 "$(gpiosim_chip_name sim0)" 0=1
+	run_tool gpioset --hold-period=bad --chip $(gpiosim_chip_name sim0) 0=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*can't specify wait time in this mode"
+	status_is 1
+	output_regex_match ".*invalid period.*"
 }
 
-@test "gpioset: use --usec without --mode=time" {
+@test "gpioset: with invalid value" {
 	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
 
-	run_tool gpioset --mode=exit --usec=1 "$(gpiosim_chip_name sim1)" 0=1
+	# by name
+	run_tool gpioset --chip $sim0 0=c
 
-	test "$status" -eq "1"
-	output_regex_match ".*can't specify wait time in this mode"
+	status_is 1
+	output_regex_match ".*invalid line value.*"
+
+	# by value
+	run_tool gpioset --chip $sim0 0=3
+
+	status_is 1
+	output_regex_match ".*invalid line value.*"
 }
 
-@test "gpioset: default mode" {
+@test "gpioset: with invalid offset" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset "$(gpiosim_chip_name sim0)" 0=1
+	run_tool gpioset --chip $(gpiosim_chip_name sim0) 4000000000=0
 
-	test "$status" -eq "0"
+	status_is 1
+	output_regex_match ".*cannot find line 4000000000"
 }
 
-@test "gpioset: invalid mapping" {
+@test "gpioset: with invalid bias" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset "$(gpiosim_chip_name sim1)" 0=c
+	run_tool gpioset --bias=bad --chip $(gpiosim_chip_name sim0) 0=1 1=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*invalid offset<->value mapping"
+	status_is 1
+	output_regex_match ".*invalid bias.*"
 }
 
-@test "gpioset: invalid value" {
+@test "gpioset: with invalid drive" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset "$(gpiosim_chip_name sim1)" 0=3
+	run_tool gpioset --drive=bad --chip $(gpiosim_chip_name sim0) 0=1 1=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*value must be 0 or 1"
+	status_is 1
+	output_regex_match ".*invalid drive.*"
 }
 
-@test "gpioset: invalid offset" {
+@test "gpioset: with daemonize and interactive" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset "$(gpiosim_chip_name sim1)" 4000000000=0
+	run_tool gpioset --interactive --daemonize --chip $(gpiosim_chip_name sim1) 0=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*invalid offset"
+	status_is 1
+	output_regex_match ".*can't combine daemonize with interactive"
 }
 
-@test "gpioset: invalid bias" {
+@test "gpioset: with interactive and toggle" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpioset --bias=bad "$(gpiosim_chip_name sim1)" 0=1 1=1
+	run_tool gpioset --interactive --toggle 1s --chip $(gpiosim_chip_name sim1) 0=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*invalid bias.*"
+	status_is 1
+	output_regex_match ".*can't combine interactive with toggle"
 }
 
-@test "gpioset: invalid drive" {
-	gpiosim_chip sim0 num_lines=8
-
-	run_tool gpioset --drive=bad "$(gpiosim_chip_name sim1)" 0=1 1=1
+@test "gpioset: with nonexistent line" {
+	run_tool gpioset nonexistent-line=0
 
-	test "$status" -eq "1"
-	output_regex_match ".*invalid drive.*"
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
 }
 
-@test "gpioset: daemonize in invalid mode" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpioset: with same line twice" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	local sim0=$(gpiosim_chip_name sim0)
 
-	run_tool gpioset --background "$(gpiosim_chip_name sim1)" 0=1
+	# by offset
+	run_tool gpioset --chip $sim0 0=1 0=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*can't daemonize in this mode"
-}
+	status_is 1
+	output_regex_match ".*lines 0 and 0 are the same"
 
-@test "gpioset: same line twice" {
-	gpiosim_chip sim0 num_lines=8
+	# by name
+	run_tool gpioset --chip $sim0 foo=1 foo=1
+
+	status_is 1
+	output_regex_match ".*lines foo and foo are the same"
 
-	run_tool gpioset "$(gpiosim_chip_name sim0)" 0=1 0=1
+	# by name and offset
+	run_tool gpioset --chip $sim0 foo=1 1=1
 
-	test "$status" -eq "1"
-	output_regex_match ".*unable to request lines"
+	status_is 1
+	output_regex_match ".*lines foo and 1 are the same"
 }
 
 #
 # gpiomon test cases
 #
 
-@test "gpiomon: single rising edge event" {
+@test "gpiomon: by offset" {
 	gpiosim_chip sim0 num_lines=8
-
-	coproc_run_tool gpiomon --rising-edge "$(gpiosim_chip_name sim0)" 4
+	local sim0=$(gpiosim_chip_name sim0)
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
 
-	coproc_tool_kill
-	coproc_tool_wait
+	dut_run gpiomon --banner --edge=rising --chip $sim0 4
+	dut_regex_match "Monitoring line .*"
 
-	test "$status" -eq "0"
-	output_regex_match \
-"event:\\s+RISING\\s+EDGE\\s+offset:\\s+4\\s+timestamp:\\s+\[\s*[0-9]+\.[0-9]+\]"
+	gpiosim_set_pull sim0 4 pull-down
+	gpiosim_set_pull sim0 4 pull-up
+	gpiosim_set_pull sim0 4 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+4"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: single falling edge event" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpiomon: by name" {
+	gpiosim_chip sim0 num_lines=8 line_name=4:foo
+
+	gpiosim_set_pull sim0 4 pull-up
 
-	coproc_run_tool gpiomon --falling-edge "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner --edge=rising foo
+	dut_flush
 
+	gpiosim_set_pull sim0 4 pull-down
 	gpiosim_set_pull sim0 4 pull-up
 	gpiosim_set_pull sim0 4 pull-down
-	sleep 0.2
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+foo"
+	assert_fail dut_readable
+}
 
-	coproc_tool_kill
-	coproc_tool_wait
+@test "gpiomon: by chip and name" {
+	gpiosim_chip sim0 num_lines=8 line_name=0:foo
+	gpiosim_chip sim1 num_lines=8 line_name=2:foo
+	local sim1=$(gpiosim_chip_name sim1)
 
-	test "$status" -eq "0"
-	output_regex_match \
-"event:\\s+FALLING\\s+EDGE\\s+offset:\\s+4\\s+timestamp:\\s+\[\s*[0-9]+\.[0-9]+\]"
+	gpiosim_set_pull sim1 0 pull-up
+
+	dut_run gpiomon --banner --edge=rising --chip $sim1 foo
+	dut_regex_match "Monitoring line .*"
+
+	gpiosim_set_pull sim1 2 pull-down
+	gpiosim_set_pull sim1 2 pull-up
+	gpiosim_set_pull sim1 2 pull-down
+	dut_regex_match \
+"\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim1\\s+offset:\\s+2\\s+name:\\s+foo"
+	assert_fail dut_readable
+}
+
+@test "gpiomon: first matching named line" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	dut_run gpiomon --banner foobar
+	dut_regex_match "Monitoring line .*"
+
+	gpiosim_set_pull sim0 3 pull-up
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+foobar"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: single falling edge event (pull-up)" {
+@test "gpiomon: rising edge" {
 	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	gpiosim_set_pull sim0 4 pull-up
+
+	dut_run gpiomon --banner --edge=rising --chip $sim0 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-down
+	gpiosim_set_pull sim0 4 pull-up
+	gpiosim_set_pull sim0 4 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+4"
+	assert_fail dut_readable
+}
 
-	coproc_run_tool gpiomon --bias=pull-up "$(gpiosim_chip_name sim0)" 4
+@test "gpiomon: falling edge" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
 
 	gpiosim_set_pull sim0 4 pull-down
-	sleep 0.2
 
-	coproc_tool_kill
-	coproc_tool_wait
+	dut_run gpiomon --banner --edge=falling --chip $sim0 4
+	dut_flush
 
-	test "$status" -eq "0"
-	output_regex_match \
-"event:\\s+FALLING\\s+EDGE\\s+offset:\\s+4\\s+timestamp:\\s+\[\s*[0-9]+\.[0-9]+\]"
+	gpiosim_set_pull sim0 4 pull-up
+	gpiosim_set_pull sim0 4 pull-down
+	gpiosim_set_pull sim0 4 pull-up
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+FALLING\\s+chip: $sim0\\s+offset:\\s+4"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: single rising edge event (pull-down)" {
+@test "gpiomon: both edges" {
 	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpiomon --banner --edge=both --chip $sim0 4
+	dut_regex_match "Monitoring line .*"
 
 	gpiosim_set_pull sim0 4 pull-up
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+4"
+
+	gpiosim_set_pull sim0 4 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+FALLING\\s+chip: $sim0\\s+offset:\\s+4"
+}
 
-	coproc_run_tool gpiomon --bias=pull-down "$(gpiosim_chip_name sim0)" 4
+@test "gpiomon: with pull-up" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
 
-	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
+	gpiosim_set_pull sim0 4 pull-down
 
-	coproc_tool_kill
-	coproc_tool_wait
+	dut_run gpiomon --banner --bias=pull-up --chip $sim0 4
+	dut_flush
 
-	test "$status" -eq "0"
-	output_regex_match \
-"event:\\s+RISING\\s+EDGE\\s+offset:\\s+4\\s+timestamp:\\s+\[\s*[0-9]+\.[0-9]+\]"
+	gpiosim_set_pull sim0 4 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+FALLING\\s+chip: $sim0\\s+offset:\\s+4"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: single rising edge event (active-low)" {
+@test "gpiomon: with pull-down" {
 	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
 
 	gpiosim_set_pull sim0 4 pull-up
 
-	coproc_run_tool gpiomon --rising-edge --active-low "$(gpiosim_chip_name sim0)" 4
-
-	gpiosim_set_pull sim0 4 pull-down
-	sleep 0.2
+	dut_run gpiomon --banner --bias=pull-down --chip $sim0 4
+	dut_flush
 
-	coproc_tool_kill
-	coproc_tool_wait
+	gpiosim_set_pull sim0 4 pull-up
 
-	test "$status" -eq "0"
-	output_regex_match \
-"event:\\s+RISING\\s+EDGE\\s+offset:\\s+4\\s+timestamp:\\s+\[\s*[0-9]+\.[0-9]+\]"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+4"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: single rising edge event (silent mode)" {
+@test "gpiomon: with active-low" {
 	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
 
-	coproc_run_tool gpiomon --rising-edge --silent "$(gpiosim_chip_name sim0)" 4
+	gpiosim_set_pull sim0 4 pull-up
+
+	dut_run gpiomon --banner --active-low --chip $sim0 4
+	dut_flush
 
+	gpiosim_set_pull sim0 4 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+4"
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+FALLING\\s+chip: $sim0\\s+offset:\\s+4"
+	assert_fail dut_readable
+}
+
+@test "gpiomon: with quiet mode" {
+	gpiosim_chip sim0 num_lines=8
 
-	coproc_tool_kill
-	coproc_tool_wait
+	dut_run gpiomon --banner --edge=rising --quiet --chip $(gpiosim_chip_name sim0) 4
+	dut_flush
 
-	test "$status" -eq "0"
-	test -z "$output"
+	gpiosim_set_pull sim0 4 pull-up
+	assert_fail dut_readable
 }
 
-@test "gpiomon: four alternating events" {
+@test "gpiomon: with num-events" {
 	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
 
-	coproc_run_tool gpiomon --num-events=4 "$(gpiosim_chip_name sim0)" 4
+	# redirect, as gpiomon exits after 4 events
+	dut_run_redirect gpiomon --num-events=4 --chip $sim0 4
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
 	gpiosim_set_pull sim0 4 pull-down
-	sleep 0.2
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
 	gpiosim_set_pull sim0 4 pull-down
-	sleep 0.2
 
-	coproc_tool_wait
+	dut_wait
+	dut_read_redirect
 
-	test "$status" -eq "0"
-	output_regex_match \
-"event\\:\\s+FALLING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[\s*[0-9]+\\.[0-9]+\\]"
-	output_regex_match \
-"event\\:\\s+RISING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[\s*[0-9]+\\.[0-9]+\\]"
+	num_lines_is 4
+
+	regex_matches "[0-9]+\.[0-9]+\\s+RISING\\s+chip:\\s+$sim0\\s+offset:\\s+4" "${lines[0]}"
+	regex_matches "[0-9]+\.[0-9]+\\s+FALLING\\s+chip:\\s+$sim0\\s+offset:\\s+4" "${lines[1]}"
+	regex_matches "[0-9]+\.[0-9]+\\s+RISING\\s+chip:\\s+$sim0\\s+offset:\\s+4" "${lines[2]}"
+	regex_matches "[0-9]+\.[0-9]+\\s+FALLING\\s+chip:\\s+$sim0\\s+offset:\\s+4" "${lines[3]}"
 }
 
-@test "gpiomon: exit after SIGINT" {
+@test "gpiomon: multiple lines" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner --format=%o --chip $(gpiosim_chip_name sim0) 1 3 2 5 4
+	dut_regex_match "Monitoring lines .*"
 
-	coproc_tool_kill -SIGINT
-	coproc_tool_wait
+	gpiosim_set_pull sim0 2 pull-up
+	dut_regex_match "2"
+	gpiosim_set_pull sim0 3 pull-up
+	dut_regex_match "3"
+	gpiosim_set_pull sim0 4 pull-up
+	dut_regex_match "4"
 
-	test "$status" -eq "0"
-	test -z "$output"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: exit after SIGTERM" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpiomon: multiple lines by name and offset" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+
+	dut_run gpiomon --banner --format=%o --chip $(gpiosim_chip_name sim0) foo bar 3
+	dut_regex_match "Monitoring lines .*"
+
+	gpiosim_set_pull sim0 2 pull-up
+	dut_regex_match "2"
+	gpiosim_set_pull sim0 3 pull-up
+	dut_regex_match "3"
+	gpiosim_set_pull sim0 1 pull-up
+	dut_regex_match "1"
+
+	assert_fail dut_readable
+}
 
-	coproc_run_tool gpiomon "$(gpiosim_chip_name sim0)" 4
+@test "gpiomon: multiple lines across multiple chips" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
 
-	coproc_tool_kill -SIGTERM
-	coproc_tool_wait
+	dut_run gpiomon --banner --format=%l foo bar baz
+	dut_regex_match "Monitoring lines .*"
 
-	test "$status" -eq "0"
-	test -z "$output"
+	gpiosim_set_pull sim0 2 pull-up
+	dut_regex_match "bar"
+	gpiosim_set_pull sim1 0 pull-up
+	dut_regex_match "baz"
+	gpiosim_set_pull sim0 1 pull-up
+	dut_regex_match "foo"
+
+	assert_fail dut_readable
 }
 
-@test "gpiomon: both event flags" {
+@test "gpiomon: exit after SIGINT" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon --falling-edge --rising-edge "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner --chip $(gpiosim_chip_name sim0) 4
+	dut_regex_match "Monitoring line .*"
 
-	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-	gpiosim_set_pull sim0 4 pull-down
-	sleep 0.2
+	dut_kill -SIGINT
+	dut_wait
 
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	output_regex_match \
-"event\\:\\s+FALLING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[\s*[0-9]+\\.[0-9]+\\]"
-	output_regex_match \
-"event\\:\\s+RISING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[\s*[0-9]+\\.[0-9]+\\]"
+	status_is 130
 }
 
-@test "gpiomon: watch multiple lines" {
+@test "gpiomon: exit after SIGTERM" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon --format=%o "$(gpiosim_chip_name sim0)" 1 2 3 4 5
+	dut_run gpiomon --banner --chip $(gpiosim_chip_name sim0) 4
+	dut_regex_match "Monitoring line .*"
 
-	gpiosim_set_pull sim0 2 pull-up
-	gpiosim_set_pull sim0 3 pull-up
-	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
+	dut_kill -SIGTERM
+	dut_wait
 
-	coproc_tool_kill
-	coproc_tool_wait
+	status_is 143
+}
+
+@test "gpiomon: with nonexistent line" {
+	run_tool gpiomon nonexistent-line
 
-	test "$status" -eq "0"
-	test "${lines[0]}" = "2"
-	test "${lines[1]}" = "3"
-	test "${lines[2]}" = "4"
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
 }
 
-@test "gpiomon: watch multiple lines (lines in mixed-up order)" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpiomon: with same line twice" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	local sim0=$(gpiosim_chip_name sim0)
 
-	coproc_run_tool gpiomon --format=%o "$(gpiosim_chip_name sim0)" 5 2 7 1 6
+	# by offset
+	run_tool gpiomon --chip $sim0 0 0
 
-	gpiosim_set_pull sim0 2 pull-up
-	gpiosim_set_pull sim0 1 pull-up
-	gpiosim_set_pull sim0 6 pull-up
-	sleep 0.2
+	status_is 1
+	output_regex_match ".*lines 0 and 0 are the same"
+
+	# by name
+	run_tool gpiomon foo foo
 
-	coproc_tool_kill
-	coproc_tool_wait
+	status_is 1
+	output_regex_match ".*lines foo and foo are the same"
 
-	test "$status" -eq "0"
-	test "${lines[0]}" = "2"
-	test "${lines[1]}" = "1"
-	test "${lines[2]}" = "6"
+	# by name and offset
+	run_tool gpiomon --chip $sim0 1 foo
+
+	status_is 1
+	output_regex_match ".*lines 1 and foo are the same"
 }
 
-@test "gpiomon: same line twice" {
-	gpiosim_chip sim0 num_lines=8
+@test "gpiomon: with strict named line check" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	run_tool gpiomon --strict foobar
 
-	run_tool gpiomon "$(gpiosim_chip_name sim0)" 0 0
+	status_is 1
+	output_regex_match ".*line foobar is not unique"
+}
+
+@test "gpiomon: with lines strictly by name" {
+	# not suggesting this setup makes sense - just test that we can deal with it
+	gpiosim_chip sim0 num_lines=8 line_name=1:42 line_name=6:13
+	local sim0=$(gpiosim_chip_name sim0)
+
+	gpiosim_set_pull sim0 1 pull-up
 
-	test "$status" -eq "1"
-	output_regex_match ".*unable to request lines"
+	dut_run gpiomon --banner --by-name --chip $sim0 42 13
+	dut_flush
+
+	gpiosim_set_pull sim0 1 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+FALLING\\s+chip: $sim0\\s+offset:\\s+1"
+	gpiosim_set_pull sim0 1 pull-up
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+1"
+	gpiosim_set_pull sim0 6 pull-up
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RISING\\s+chip: $sim0\\s+offset:\\s+6"
+	gpiosim_set_pull sim0 6 pull-down
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+FALLING\\s+chip: $sim0\\s+offset:\\s+6"
+	assert_fail dut_readable
 }
 
-@test "gpiomon: no arguments" {
+@test "gpiomon: with no arguments" {
 	run_tool gpiomon
 
-	test "$status" -eq "1"
-	output_regex_match ".*gpiochip must be specified"
+	status_is 1
+	output_regex_match ".*at least one GPIO line must be specified"
 }
 
-@test "gpiomon: line not specified" {
+@test "gpiomon: with no line specified" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpiomon "$(gpiosim_chip_name sim0)"
+	run_tool gpiomon --chip $(gpiosim_chip_name sim0)
 
-	test "$status" -eq "1"
-	output_regex_match ".*GPIO line offset must be specified"
+	status_is 1
+	output_regex_match ".*at least one GPIO line must be specified"
 }
 
-@test "gpiomon: line out of range" {
+@test "gpiomon: with offset out of range" {
 	gpiosim_chip sim0 num_lines=4
 
-	run_tool gpiomon "$(gpiosim_chip_name sim0)" 5
+	run_tool gpiomon --chip $(gpiosim_chip_name sim0) 5
 
-	test "$status" -eq "1"
-	output_regex_match ".*unable to request lines"
+	status_is 1
+	output_regex_match ".*offset 5 is out of range on chip $(gpiosim_chip_name sim0)"
 }
 
-@test "gpiomon: invalid bias" {
+@test "gpiomon: with invalid bias" {
 	gpiosim_chip sim0 num_lines=8
 
-	run_tool gpiomon --bias=bad "$(gpiosim_chip_name sim0)" 0 1
+	run_tool gpiomon --bias=bad -c $(gpiosim_chip_name sim0) 0 1
 
-	test "$status" -eq "1"
+	status_is 1
 	output_regex_match ".*invalid bias.*"
 }
 
-@test "gpiomon: custom format (event type + offset)" {
+@test "gpiomon: with custom format (event type + offset)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%e %o" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=%e %o" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	test "$output" = "1 4"
+	dut_read
+	output_is "1 4"
 }
 
-@test "gpiomon: custom format (event type + offset joined)" {
+@test "gpiomon: with custom format (event type + offset joined)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%e%o" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=%e%o" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
+	dut_read
+	output_is "14"
+}
 
-	coproc_tool_kill
-	coproc_tool_wait
+@test "gpiomon: with custom format (format menagerie)" {
+	gpiosim_chip sim0 num_lines=8 line_name=4:baz
 
-	test "$status" -eq "0"
-	test "$output" = "14"
+	dut_run gpiomon --banner "--format=%e %o %E %l %c %T" --utc -c $(gpiosim_chip_name sim0) baz
+	dut_flush
+
+	gpiosim_set_pull sim0 4 pull-up
+	dut_regex_match \
+"1 4 rising baz /dev/gpiochip[0-9]+ [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-1][0-9]:[0-5][0-9]:[0-5][0-9]\\.[0-9]+Z"
 }
 
-@test "gpiomon: custom format (timestamp)" {
+@test "gpiomon: with custom format (timestamp)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%e %o %s.%n" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=%e %o %s.%n" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	output_regex_match "1 4 [0-9]+\\.[0-9]+"
+	dut_regex_match "1 4 [0-9]+\\.[0-9]+"
 }
 
-@test "gpiomon: custom format (double percent sign)" {
+@test "gpiomon: with custom format (double percent sign)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%%" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=start%%end" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	test "$output" = "%"
+	dut_read
+	output_is "start%end"
 }
 
-@test "gpiomon: custom format (double percent sign + event type specifier)" {
+@test "gpiomon: with custom format (double percent sign + event type specifier)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%%e" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=%%e" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	test "$output" = "%e"
+	dut_read
+	output_is "%e"
 }
 
-@test "gpiomon: custom format (single percent sign)" {
+@test "gpiomon: with custom format (single percent sign)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=%" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	test "$output" = "%"
+	dut_read
+	output_is "%"
 }
 
-@test "gpiomon: custom format (single percent sign between other characters)" {
+@test "gpiomon: with custom format (single percent sign between other characters)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=foo % bar" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=foo % bar" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	test "$output" = "foo % bar"
+	dut_read
+	output_is "foo % bar"
 }
 
-@test "gpiomon: custom format (unknown specifier)" {
+@test "gpiomon: with custom format (unknown specifier)" {
 	gpiosim_chip sim0 num_lines=8
 
-	coproc_run_tool gpiomon "--format=%x" "$(gpiosim_chip_name sim0)" 4
+	dut_run gpiomon --banner "--format=%x" -c $(gpiosim_chip_name sim0) 4
+	dut_flush
 
 	gpiosim_set_pull sim0 4 pull-up
-	sleep 0.2
-
-	coproc_tool_kill
-	coproc_tool_wait
-
-	test "$status" -eq "0"
-	test "$output" = "%x"
+	dut_read
+	output_is "%x"
 }
-- 
2.36.1


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [libgpiod v2][PATCH 3/4] tools: add gpiowatch
  2022-06-27 13:44 [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
  2022-06-27 13:44 ` [libgpiod v2][PATCH 1/4] " Kent Gibson
  2022-06-27 13:44 ` [libgpiod v2][PATCH 2/4] tools: tests for " Kent Gibson
@ 2022-06-27 13:44 ` Kent Gibson
  2022-07-06 20:46   ` Bartosz Golaszewski
  2022-06-27 13:44 ` [libgpiod v2][PATCH 4/4] tools: gpiowatch tests Kent Gibson
  2022-06-28  5:26 ` [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
  4 siblings, 1 reply; 15+ messages in thread
From: Kent Gibson @ 2022-06-27 13:44 UTC (permalink / raw)
  To: linux-gpio, brgl; +Cc: Kent Gibson

Add a gpiowatch tool, based on gpiomon, but reporting line info change
events similar to the gpio-watch tool in the linux kernel.

Signed-off-by: Kent Gibson <warthog618@gmail.com>
---
 man/Makefile.am   |   2 +-
 tools/.gitignore  |   1 +
 tools/Makefile.am |   4 +-
 tools/gpiowatch.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 219 insertions(+), 2 deletions(-)
 create mode 100644 tools/gpiowatch.c

diff --git a/man/Makefile.am b/man/Makefile.am
index 4d2c29b..3badd3b 100644
--- a/man/Makefile.am
+++ b/man/Makefile.am
@@ -3,7 +3,7 @@
 
 if WITH_MANPAGES
 
-dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man
+dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man gpiowatch.man
 
 %.man: $(top_builddir)/tools/$(*F)
 	help2man $(top_builddir)/tools/$(*F) --include=$(srcdir)/template --output=$(builddir)/$@ --no-info
diff --git a/tools/.gitignore b/tools/.gitignore
index 0d53de9..6175e26 100644
--- a/tools/.gitignore
+++ b/tools/.gitignore
@@ -7,3 +7,4 @@ gpioget
 gpioset
 gpiomon
 gpiofind
+gpiowatch
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 4a13266..8bb2cac 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -9,7 +9,7 @@ libtools_common_la_SOURCES = tools-common.c tools-common.h
 
 LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la
 
-bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind
+bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind gpiowatch
 
 gpiodetect_SOURCES = gpiodetect.c
 
@@ -23,6 +23,8 @@ gpiomon_SOURCES = gpiomon.c
 
 gpiofind_SOURCES = gpiofind.c
 
+gpiowatch_SOURCES = gpiowatch.c
+
 EXTRA_DIST = gpio-tools-test gpio-tools-test.bats
 
 if WITH_TESTS
diff --git a/tools/gpiowatch.c b/tools/gpiowatch.c
new file mode 100644
index 0000000..e6bfeb6
--- /dev/null
+++ b/tools/gpiowatch.c
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <inttypes.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "tools-common.h"
+
+static int by_name;
+static int event_clock_mode;
+static int banner;
+
+static const struct option longopts[] = {
+	{ "banner",		no_argument,		&banner,	1 },
+	{ "by-name",		no_argument,		&by_name,	1 },
+	{ "chip",		required_argument,	NULL,	'c' },
+	{ "help",		no_argument,		NULL,	'h' },
+	{ "localtime",		no_argument,		&event_clock_mode,	2 },
+	{ "strict",		no_argument,		NULL,	's' },
+	{ "utc",		no_argument,		&event_clock_mode,	1 },
+	{ "version",		no_argument,		NULL,	'v' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+c:shv";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <line> ...\n", get_progname());
+	printf("\n");
+	printf("Wait for changes to info on GPIO lines and print them to standard output.\n");
+	printf("\n");
+	printf("Lines are specified by name, or optionally by offset if the chip option\n");
+	printf("is provided.\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("      --banner\t\tdisplay a banner on successful startup\n");
+	printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("      --localtime\treport event time as a local time (default is monotonic)\n");
+	printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+	printf("      --utc\t\treport event time as UTC (default is monotonic)\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	print_chip_help();
+}
+
+struct config {
+	bool strict;
+	const char *chip_id;
+};
+
+int parse_config(int argc, char **argv, struct config *cfg)
+{
+	int opti, optc;
+
+	memset(cfg, 0, sizeof(*cfg));
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
+		case 's':
+			cfg->strict = true;
+			break;
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'v':
+			print_version();
+			exit(EXIT_SUCCESS);
+		case '?':
+			die("try %s --help", get_progname());
+		case 0:
+			break;
+		default:
+			abort();
+		}
+	}
+
+	return optind;
+}
+
+static void print_banner(int num_lines, char **lines)
+{
+	int i;
+
+	if (num_lines > 1) {
+		printf("Watching lines ");
+		for (i = 0; i < num_lines - 1; i++)
+			printf("%s, ", lines[i]);
+		printf("and %s...\n", lines[i]);
+	} else {
+		printf("Watching line %s ...\n", lines[0]);
+	}
+}
+
+static void event_print(struct gpiod_info_event *event, const char *chip_id)
+{
+	struct gpiod_line_info *info;
+	uint64_t evtime, before, after, mono;
+	char *evname;
+	int evtype;
+	struct timespec ts;
+
+	info = gpiod_info_event_get_line_info(event);
+	evtime = gpiod_info_event_get_timestamp_ns(event);
+	evtype = gpiod_info_event_get_event_type(event);
+
+	switch (evtype) {
+	case GPIOD_INFO_EVENT_LINE_REQUESTED:
+		evname = "REQUESTED";
+		break;
+	case GPIOD_INFO_EVENT_LINE_RELEASED:
+		evname = "RELEASED ";
+		break;
+	case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
+		evname = "RECONFIG ";
+		break;
+	default:
+		evname = "UNKNOWN  ";
+	}
+	if (event_clock_mode) {
+		// map clock monotonic to realtime, as uAPI only supports CLOCK_MONOTONIC
+		clock_gettime(CLOCK_REALTIME, &ts);
+		before = ts.tv_nsec + ts.tv_sec * 1000000000;
+		clock_gettime(CLOCK_MONOTONIC, &ts);
+		mono = ts.tv_nsec + ts.tv_sec * 1000000000;
+		clock_gettime(CLOCK_REALTIME, &ts);
+		after = ts.tv_nsec + ts.tv_sec * 1000000000;
+		evtime += (after/2 - mono + before/2);
+	}
+	print_event_time(evtime, event_clock_mode);
+	printf(" %s", evname);
+	if (chip_id)
+		printf(" %s %d", chip_id, gpiod_line_info_get_offset(info));
+	print_line_info(info);
+	printf("\n");
+}
+
+int main(int argc, char **argv)
+{
+	int i, j;
+	struct gpiod_chip **chips;
+	struct pollfd *pollfds;
+	struct gpiod_chip *chip;
+	struct line_resolver *resolver;
+	struct gpiod_info_event *event;
+	struct config cfg;
+
+	i = parse_config(argc, argv, &cfg);
+	argc -= optind;
+	argv += optind;
+
+	if (argc < 1)
+		die("at least one GPIO line must be specified");
+
+	if (argc > 64)
+		die("too many lines given");
+
+	resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict, by_name);
+	chips = calloc(resolver->num_chips, sizeof(*chips));
+	pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
+	if (!pollfds)
+		die("out of memory");
+	for (i = 0; i < resolver->num_chips; i++) {
+		chip = gpiod_chip_open(resolver->chip_paths[i]);
+		if (!chip)
+			die_perror("unable to open chip %s", resolver->chip_paths[i]);
+
+		for (j = 0; j < resolver->num_lines; j++)
+			if (resolver->lines[j].chip_path == resolver->chip_paths[i])
+				if (!gpiod_chip_watch_line_info(chip, resolver->lines[j].offset))
+					die_perror("unable to watch line on chip %s",
+						   resolver->chip_paths[i]);
+
+		chips[i] = chip;
+		pollfds[i].fd = gpiod_chip_get_fd(chip);
+		pollfds[i].events = POLLIN;
+	}
+
+	if (banner)
+		print_banner(argc, argv);
+
+	for (;;) {
+		if (poll(pollfds, resolver->num_chips, -1) < 0)
+			die_perror("error polling for events");
+
+		for (i = 0; i < resolver->num_chips; i++) {
+			if (pollfds[i].revents == 0)
+				continue;
+
+			event = gpiod_chip_read_info_event(chips[i]);
+			event_print(event, cfg.chip_id);
+		}
+	}
+	for (i = 0; i < resolver->num_chips; i++)
+		gpiod_chip_close(chips[i]);
+	free(chips);
+	free_line_resolver(resolver);
+
+	return EXIT_SUCCESS;
+}
-- 
2.36.1


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [libgpiod v2][PATCH 4/4] tools: gpiowatch tests
  2022-06-27 13:44 [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
                   ` (2 preceding siblings ...)
  2022-06-27 13:44 ` [libgpiod v2][PATCH 3/4] tools: add gpiowatch Kent Gibson
@ 2022-06-27 13:44 ` Kent Gibson
  2022-06-28  5:26 ` [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
  4 siblings, 0 replies; 15+ messages in thread
From: Kent Gibson @ 2022-06-27 13:44 UTC (permalink / raw)
  To: linux-gpio, brgl; +Cc: Kent Gibson

Extend the tool test suite to cover the gpiowatch tool.

Signed-off-by: Kent Gibson <warthog618@gmail.com>
---
 tools/gpio-tools-test.bats | 249 +++++++++++++++++++++++++++++++++++++
 1 file changed, 249 insertions(+)

diff --git a/tools/gpio-tools-test.bats b/tools/gpio-tools-test.bats
index eaa814f..73fdd93 100755
--- a/tools/gpio-tools-test.bats
+++ b/tools/gpio-tools-test.bats
@@ -2069,3 +2069,252 @@ request_release_line() {
 	dut_read
 	output_is "%x"
 }
+
+#
+# gpiowatch test cases
+#
+
+@test "gpiowatch: by name" {
+	gpiosim_chip sim0 num_lines=8 line_name=4:foo
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpiowatch --banner foo
+	dut_regex_match "Watching line .*"
+
+	request_release_line $sim0 4
+
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+foo\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+foo\\s+.*"
+	# tools currently have no way to generate a RECONFIG event
+}
+
+@test "gpiowatch: by offset" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpiowatch --banner --chip $sim0 4
+	dut_regex_match "Watching line .*"
+
+	request_release_line $sim0 4
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+4\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+4\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: by chip and name" {
+	gpiosim_chip sim0 num_lines=8 line_name=4:foo
+	gpiosim_chip sim1 num_lines=8 line_name=2:foo
+	local sim1=$(gpiosim_chip_name sim1)
+
+	dut_run gpiowatch --banner --chip $sim1 foo
+	dut_regex_match "Watching line .*"
+
+	request_release_line $sim1 2
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim1\\s+2\\s+foo\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim1\\s+2\\s+foo\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: first matching named line" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	dut_run gpiowatch --banner foobar
+	dut_regex_match "Watching line .*"
+
+	request_release_line $(gpiosim_chip_name sim0) 3
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+foobar\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+foobar\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: multiple lines" {
+	gpiosim_chip sim0 num_lines=8
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpiowatch --banner --chip $sim0 1 2 3 4 5
+	dut_regex_match "Watching lines .*"
+
+	request_release_line $sim0 2
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+2\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+2\\s+.*"
+
+	request_release_line $sim0 3
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+3\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+3\\s+.*"
+
+	request_release_line $sim0 4
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+4\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+4\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: multiple lines by name and offset" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpiowatch --banner --chip $sim0 bar foo 3
+	dut_regex_match "Watching lines .*"
+
+	request_release_line $sim0 2
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+2\\s+bar\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+2\\s+bar\\s+.*"
+
+	request_release_line $sim0 1
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+1\\s+foo\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+1\\s+foo\\s+.*"
+
+	request_release_line $sim0 3
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+3\\s+unnamed\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+3\\s+unnamed\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: multiple lines across multiple chips" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+	local sim0=$(gpiosim_chip_name sim0)
+	local sim1=$(gpiosim_chip_name sim1)
+
+	dut_run gpiowatch --banner baz bar foo xyz
+	dut_regex_match "Watching lines .*"
+
+	request_release_line $sim0 2
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+bar\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+bar\\s+.*"
+
+	request_release_line $sim0 1
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+foo\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+foo\\s+.*"
+
+	request_release_line $sim1 4
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+xyz\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+xyz\\s+.*"
+
+	request_release_line $sim1 0
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+baz\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+baz\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: exit after SIGINT" {
+	gpiosim_chip sim0 num_lines=8
+
+	dut_run gpiowatch --banner --chip $(gpiosim_chip_name sim0) 4
+	dut_regex_match "Watching line .*"
+
+	dut_kill -SIGINT
+	dut_wait
+
+	status_is 130
+}
+
+@test "gpiowatch: exit after SIGTERM" {
+	gpiosim_chip sim0 num_lines=8
+
+	dut_run gpiowatch --banner --chip $(gpiosim_chip_name sim0) 4
+	dut_regex_match "Watching line .*"
+
+	dut_kill -SIGTERM
+	dut_wait
+
+	status_is 143
+}
+
+@test "gpiowatch: with nonexistent line" {
+	run_tool gpiowatch nonexistent-line
+
+	status_is 1
+	output_regex_match ".*cannot find line nonexistent-line"
+}
+
+@test "gpiowatch: with same line twice" {
+	gpiosim_chip sim0 num_lines=8 line_name=1:foo
+	local sim0=$(gpiosim_chip_name sim0)
+
+	# by offset
+	run_tool gpiowatch --chip $sim0 0 0
+
+	status_is 1
+	num_lines_is 1
+	output_regex_match ".*lines 0 and 0 are the same"
+
+	# by name
+	run_tool gpiowatch --chip $sim0 foo foo
+
+	status_is 1
+	num_lines_is 1
+	output_regex_match ".*lines foo and foo are the same"
+
+	# by name and offset
+	run_tool gpiowatch --chip $sim0 1 foo
+
+	status_is 1
+	num_lines_is 1
+	output_regex_match ".*lines 1 and foo are the same"
+}
+
+@test "gpiowatch: with strict named line check" {
+	gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar line_name=3:foobar
+	gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+				      line_name=4:xyz line_name=7:foobar
+	gpiosim_chip sim2 num_lines=16
+
+	run_tool gpiowatch --strict foobar
+
+	status_is 1
+	output_regex_match ".*line foobar is not unique"
+}
+
+@test "gpiowatch: with lines strictly by name" {
+	# not suggesting this setup makes sense - just test that we can deal with it
+	gpiosim_chip sim0 num_lines=8 line_name=1:42 line_name=6:13
+	local sim0=$(gpiosim_chip_name sim0)
+
+	dut_run gpiowatch --banner --by-name --chip $sim0 42 13
+	dut_flush
+
+	request_release_line $sim0 1
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+1\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+1\\s+.*"
+
+	request_release_line $sim0 6
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+REQUESTED\\s+$sim0\\s+6\\s+.*"
+	dut_regex_match "\\s*[0-9]+\.[0-9]+\\s+RELEASED\\s+$sim0\\s+6\\s+.*"
+
+	assert_fail dut_readable
+}
+
+@test "gpiowatch: with no arguments" {
+	run_tool gpiowatch
+
+	status_is 1
+	output_regex_match ".*at least one GPIO line must be specified"
+}
+
+@test "gpiowatch: with no line specified" {
+	gpiosim_chip sim0 num_lines=8
+
+	run_tool gpiowatch --chip $(gpiosim_chip_name sim0)
+
+	status_is 1
+	output_regex_match ".*at least one GPIO line must be specified"
+}
+
+@test "gpiowatch: with offset out of range" {
+	gpiosim_chip sim0 num_lines=4
+	local sim0=$(gpiosim_chip_name sim0)
+
+	run_tool gpiowatch --chip $sim0 5
+
+	status_is 1
+	output_regex_match ".*offset 5 is out of range on chip $sim0"
+}
-- 
2.36.1


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 0/4] tools: line name focussed rework
  2022-06-27 13:44 [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
                   ` (3 preceding siblings ...)
  2022-06-27 13:44 ` [libgpiod v2][PATCH 4/4] tools: gpiowatch tests Kent Gibson
@ 2022-06-28  5:26 ` Kent Gibson
  4 siblings, 0 replies; 15+ messages in thread
From: Kent Gibson @ 2022-06-28  5:26 UTC (permalink / raw)
  To: linux-gpio, brgl

On Mon, Jun 27, 2022 at 09:44:43PM +0800, Kent Gibson wrote:
> This patch series is an optimistic reimagining of the tools intended to
> simplify usage for well configured systems, i.e. for systems where lines
> can be uniquely identified by name.  In such systems the chip and offset
> location of the line is no longer of relevance to the user, so the tools
> should be able to operate without mentioning them.
> e.g.
>   gpioget GPIO17
> 
>   gpioset GPIO17=active
> 
>   gpiomon --localtime GPIO17 GPIO18 
> 
> It is accepted that the kernel does not guarantee line name uniqueness
> within the system, or even within a chip, and not all systems are well
> configured, so the tools retain the option to identify lines by chip
> and offset.  The hope and expectation is that over time systems will
> become more well configured, not less, and identification of GPIO lines
> by name will become the norm.
> 
> The core of the series is patch 1 which is a reworking of the tools to
> support identifying lines by name, and to operate across multiple GPIO
> chips if named lines are located on different chips.
> The gpioset tool is extended to support toggling lines and interactive
> control of line values, so some common use cases can be trivially
> implemented from the command line.
> e.g.
>   gpioset --toggle 500ms LED=on
> 
> will blink the LED line at 1Hz, indefinitely.
> More complex outputs can be generated by adding more entries to the
> toggle sequence:
>   gpioset --toggle 1s,2s,1s,300ms LED=on
> 
> Even more complex outputs can be generated by driving gpioset in
> interactive mode from another script.
> 
> Those are the major changes.  A more complete list of the changes can be
> found in the patch description.
> 
> Patch 2 updates and extends the tool tests to cover the reworked tools,
> including demonstrating gpioset being driven interactively via a script.
> 
> The final two patches add a gpiowatch tool that monitors changes to 
> the state line information, similar to the gpio-watch tool in the kernel,
> and extend the test suite to cover it.
> 

I forgot to mention that gpiofind is effectively redundant now its
functionality has been absorbed into the other tools.
I kept it in the patch for completeness but would be happy to remove
it.

I am also reconsidering gpioset's interactive get command.
The get currently returns the requested set values - it does not
query the kernel.  It may be useful to be able to do both for open-drain
outputs.
How about the get does a GET_VALUES, while a bare set displays the
requested set values?
Or would a flag on the get be clearer?

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 2/4] tools: tests for line name focussed rework
  2022-06-27 13:44 ` [libgpiod v2][PATCH 2/4] tools: tests for " Kent Gibson
@ 2022-07-01  0:42   ` Kent Gibson
  0 siblings, 0 replies; 15+ messages in thread
From: Kent Gibson @ 2022-07-01  0:42 UTC (permalink / raw)
  To: linux-gpio, brgl

On Mon, Jun 27, 2022 at 09:44:45PM +0800, Kent Gibson wrote:
> Rework the tools tests and expand to cover new functionality.
> 

Just reaslised the tests don't cover the symlink behaviour.

The expected behaviour is:

For bare gpiodetect and gpioinfo, symlinks in /dev are filtered out -
only actual devices are listed.

For all commands the --chip option will accept a path which is a symlink
that resolves to an actual device.  The symlink itself need not be in
/dev.

I'll extend the tests to cover that behaviour for the next patch
version.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 1/4] tools: line name focussed rework
  2022-06-27 13:44 ` [libgpiod v2][PATCH 1/4] " Kent Gibson
@ 2022-07-06 20:20   ` Bartosz Golaszewski
  2022-07-07  2:24     ` Kent Gibson
  0 siblings, 1 reply; 15+ messages in thread
From: Bartosz Golaszewski @ 2022-07-06 20:20 UTC (permalink / raw)
  To: Kent Gibson; +Cc: open list:GPIO SUBSYSTEM

On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
>
> Rework the tool suite to support identifying lines by name and to
> support operating on the GPIO lines available to the user at once, rather
> than on one particular GPIO chip.
>
> All tools, other than gpiodetect, now provide the name to (chip,offset)
> mapping that was previously only performed by gpiofind. As names are not
> guaranteed to be unique, a --strict option is provided for all tools to
> either abort the operation or report all lines with the matching name, as
> appropriate.
> By default the tools operate on the first line found with a matching name.
>
> Selection of line by (chip,offset) is still supported with a --chip
> option, though it restricts the scope of the operation to an individual
> chip.  When the --chip option is specified, the lines are assumed to be
> identified by offset where they parse as an integer, else by name.
> To cater for the unusual case where a line name parses as an integer,
> but is different from the offset, the --by-name option forces the lines
> to be identified by name.
>

We could potentially extend it by allowing multiple --chip arguments
for multiple chips but let's not go there unless requested.

> The updated tools are intentionally NOT backwardly compatible with the
> previous tools. Using old command lines with the updated tools will
> almost certainly fail, though migrating old command lines is generally as
> simple as adding a '-c' before the chip.
>
> In addition the individual tools are modified as follows:
>
> gpiodetect:
>
> Add the option to select individual chips.
>

We discussed at some point that gpiodetect should also check if any of
the files it iterates over is a symbolic link to a GPIO device and
print some info accordingly (e.g. foobar [link to /dev/gpiochip2])-
are you thinking about adding this too?

> gpiofind:
>
> Add the option to display the info for found lines.
>
> gpioinfo:
>
> Change the focus from chips to lines, so the scope can be
> an individual line, a subset of lines, all lines on a particular chip,
> or all the lines available to the user.  For line scope a single line
> summary is output for each line.  For chip scope the existing format
> displaying a summary of the chip and each of its lines is retained.
>
> Line attributes are consolidated into a list format, and are extended
> to cover all attributes supported by uAPI v2.
>
> gpioget:
>
> The default output format is becomes line=value, as per the
> input for gpioset, and the value is reported as active or inactive,
> rather than 0 or 1.
> The previous format is available using the --numeric option.
>
> Add an optional hold period between requesting a line and reading the
> value to allow the line to settle once the requested configuration has
> been applied (e.g. bias).
>
> gpiomon:
>
> Consolidate the edge options into a single option.
>
> Add a debounce period option.
>
> Add options to report event times as UTC or localtime.
>
> Add format specifiers for GPIO chip path, line name, stringified event
> type, and event time as a datetime.
>
> Rearrange default output format to place fields with more predicable
> widths to the left, and to separate major field groups with tabs.
> Lines are identified consistent with the command line.
>
> gpioset:
>
> Add a hold period option that specifies the minimum period the line
> value must be held for.  This applies to all set options.
>
> Support line values specified as active/inactive, on/off and
> true/false, as well as 1/0.
>
> Add a toggle option that specifies a time sequence over which the
> requested lines should be toggled.  If the sequence is 0 terminated then
> gpioset exits when the sequence completes, else it repeats the sequence.
> This allows for anything from simple blinkers to bit bashing from the
> command line. e.g. gpioset -t 500ms LED=on
>
> Add an interactive option to provide a shell-like interface to allow
> manual or scripted manipulation of requested lines.  A basic command set
> allows lines to be get, set, or toggled, and to insert sleeps between
> operations.
>
> Remove the --mode, --sec, and --usec options.
> The combination of hold period and interactive mode provide functionality
> equivalent to the old --mode options.
>
> Signed-off-by: Kent Gibson <warthog618@gmail.com>
> ---
>  configure.ac         |   9 +-
>  tools/gpiodetect.c   | 108 ++++--
>  tools/gpiofind.c     | 126 +++++--
>  tools/gpioget.c      | 200 ++++++----
>  tools/gpioinfo.c     | 356 ++++++++----------
>  tools/gpiomon.c      | 493 +++++++++++++++----------
>  tools/gpioset.c      | 861 +++++++++++++++++++++++++++++++------------
>  tools/tools-common.c | 640 +++++++++++++++++++++++++++++---
>  tools/tools-common.h |  59 ++-
>  9 files changed, 2011 insertions(+), 841 deletions(-)
>
> diff --git a/configure.ac b/configure.ac
> index ab03673..c7e353c 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -105,14 +105,15 @@ AC_DEFUN([FUNC_NOT_FOUND_TOOLS],
>  AC_DEFUN([HEADER_NOT_FOUND_TOOLS],
>         [ERR_NOT_FOUND([$1 header], [tools])])
>
> +AC_DEFUN([LIB_NOT_FOUND_TOOLS],
> +       [ERR_NOT_FOUND([lib$1], [tools])])
> +
>  if test "x$with_tools" = xtrue
>  then
>         # These are only needed to build tools
> -       AC_CHECK_FUNC([basename], [], [FUNC_NOT_FOUND_TOOLS([basename])])
>         AC_CHECK_FUNC([daemon], [], [FUNC_NOT_FOUND_TOOLS([daemon])])
> -       AC_CHECK_FUNC([signalfd], [], [FUNC_NOT_FOUND_TOOLS([signalfd])])
> -       AC_CHECK_FUNC([setlinebuf], [], [FUNC_NOT_FOUND_TOOLS([setlinebuf])])
> -       AC_CHECK_HEADERS([sys/signalfd.h], [], [HEADER_NOT_FOUND_TOOLS([sys/signalfd.h])])
> +       AC_CHECK_HEADERS([readline/readline.h], [], [HEADER_NOT_FOUND_TOOLS([readline/readline.h])])
> +       AC_CHECK_LIB([readline], readline, [], [LIB_NOT_FOUND_TOOLS([readline])])

readline is licensed under GPLv3 - this bleeds into gpioset and will
make a lot of companies bounce off of it. Any chance you could use
libedit instead? It's supposed to be a drop-in replacement for
readline but I have never used it first hand.

>  fi
>
>  AC_ARG_ENABLE([tests],
> diff --git a/tools/gpiodetect.c b/tools/gpiodetect.c
> index 8f6e8b3..18b6e95 100644
> --- a/tools/gpiodetect.c
> +++ b/tools/gpiodetect.c
> @@ -1,7 +1,6 @@
>  // SPDX-License-Identifier: GPL-2.0-or-later
> -// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
> +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>

This should have your copyright, not mine. Same elsewhere.

[snip]

I will try to get back to this one later but it's so big I just kept
losing focus. Do you think you could submit it the way I did for the
bindings - remove the existing code and then apply the new
implementations?

Just a general note on coding style: I would prefer that you use /* */
style comments for the sake of consistency with the rest of the code
and if you tried to keep the lines limited to 80 characters wherever
it doesn't significantly impact readability? A sprinkle of newlines
here and there separating logical blocks of code would be nice too.

We may not agree on the above but after all I will maintain that code
and would prefer to keep it so that it stays easy for my brain to
parse it.

Bart

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 3/4] tools: add gpiowatch
  2022-06-27 13:44 ` [libgpiod v2][PATCH 3/4] tools: add gpiowatch Kent Gibson
@ 2022-07-06 20:46   ` Bartosz Golaszewski
  2022-07-07  2:27     ` Kent Gibson
  0 siblings, 1 reply; 15+ messages in thread
From: Bartosz Golaszewski @ 2022-07-06 20:46 UTC (permalink / raw)
  To: Kent Gibson; +Cc: open list:GPIO SUBSYSTEM

On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
>
> Add a gpiowatch tool, based on gpiomon, but reporting line info change
> events similar to the gpio-watch tool in the linux kernel.
>
> Signed-off-by: Kent Gibson <warthog618@gmail.com>
> ---
>  man/Makefile.am   |   2 +-
>  tools/.gitignore  |   1 +
>  tools/Makefile.am |   4 +-
>  tools/gpiowatch.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++
>  4 files changed, 219 insertions(+), 2 deletions(-)
>  create mode 100644 tools/gpiowatch.c
>
> diff --git a/man/Makefile.am b/man/Makefile.am
> index 4d2c29b..3badd3b 100644
> --- a/man/Makefile.am
> +++ b/man/Makefile.am
> @@ -3,7 +3,7 @@
>
>  if WITH_MANPAGES
>
> -dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man
> +dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man gpiowatch.man
>
>  %.man: $(top_builddir)/tools/$(*F)
>         help2man $(top_builddir)/tools/$(*F) --include=$(srcdir)/template --output=$(builddir)/$@ --no-info
> diff --git a/tools/.gitignore b/tools/.gitignore
> index 0d53de9..6175e26 100644
> --- a/tools/.gitignore
> +++ b/tools/.gitignore
> @@ -7,3 +7,4 @@ gpioget
>  gpioset
>  gpiomon
>  gpiofind
> +gpiowatch
> diff --git a/tools/Makefile.am b/tools/Makefile.am
> index 4a13266..8bb2cac 100644
> --- a/tools/Makefile.am
> +++ b/tools/Makefile.am
> @@ -9,7 +9,7 @@ libtools_common_la_SOURCES = tools-common.c tools-common.h
>
>  LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la
>
> -bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind
> +bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind gpiowatch
>
>  gpiodetect_SOURCES = gpiodetect.c
>
> @@ -23,6 +23,8 @@ gpiomon_SOURCES = gpiomon.c
>
>  gpiofind_SOURCES = gpiofind.c
>
> +gpiowatch_SOURCES = gpiowatch.c
> +
>  EXTRA_DIST = gpio-tools-test gpio-tools-test.bats
>
>  if WITH_TESTS
> diff --git a/tools/gpiowatch.c b/tools/gpiowatch.c
> new file mode 100644
> index 0000000..e6bfeb6
> --- /dev/null
> +++ b/tools/gpiowatch.c
> @@ -0,0 +1,214 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
> +
> +#include <getopt.h>
> +#include <gpiod.h>
> +#include <inttypes.h>
> +#include <poll.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <time.h>
> +
> +#include "tools-common.h"
> +
> +static int by_name;
> +static int event_clock_mode;
> +static int banner;
> +
> +static const struct option longopts[] = {
> +       { "banner",             no_argument,            &banner,        1 },
> +       { "by-name",            no_argument,            &by_name,       1 },
> +       { "chip",               required_argument,      NULL,   'c' },
> +       { "help",               no_argument,            NULL,   'h' },
> +       { "localtime",          no_argument,            &event_clock_mode,      2 },
> +       { "strict",             no_argument,            NULL,   's' },
> +       { "utc",                no_argument,            &event_clock_mode,      1 },
> +       { "version",            no_argument,            NULL,   'v' },
> +       { GETOPT_NULL_LONGOPT },
> +};
> +
> +static const char *const shortopts = "+c:shv";
> +
> +static void print_help(void)
> +{
> +       printf("Usage: %s [OPTIONS] <line> ...\n", get_progname());
> +       printf("\n");
> +       printf("Wait for changes to info on GPIO lines and print them to standard output.\n");
> +       printf("\n");
> +       printf("Lines are specified by name, or optionally by offset if the chip option\n");
> +       printf("is provided.\n");
> +       printf("\n");
> +       printf("Options:\n");
> +       printf("      --banner\t\tdisplay a banner on successful startup\n");
> +       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
> +       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
> +       printf("  -h, --help\t\tdisplay this help and exit\n");
> +       printf("      --localtime\treport event time as a local time (default is monotonic)\n");
> +       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
> +       printf("      --utc\t\treport event time as UTC (default is monotonic)\n");
> +       printf("  -v, --version\t\toutput version information and exit\n");
> +       print_chip_help();
> +}
> +
> +struct config {
> +       bool strict;
> +       const char *chip_id;
> +};

Let's either have all options in a local config struct (preferred) or
all of them as global variables, otherwise it's confusing. If you want
to use flags in long opts you can always define that structure within
the function calling getopt().

> +
> +int parse_config(int argc, char **argv, struct config *cfg)
> +{
> +       int opti, optc;
> +
> +       memset(cfg, 0, sizeof(*cfg));
> +
> +       for (;;) {
> +               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
> +               if (optc < 0)
> +                       break;
> +
> +               switch (optc) {
> +               case 'c':
> +                       cfg->chip_id = optarg;
> +                       break;
> +               case 's':
> +                       cfg->strict = true;
> +                       break;
> +               case 'h':
> +                       print_help();
> +                       exit(EXIT_SUCCESS);
> +               case 'v':
> +                       print_version();
> +                       exit(EXIT_SUCCESS);
> +               case '?':
> +                       die("try %s --help", get_progname());
> +               case 0:
> +                       break;
> +               default:
> +                       abort();
> +               }
> +       }
> +
> +       return optind;
> +}
> +
> +static void print_banner(int num_lines, char **lines)
> +{
> +       int i;
> +
> +       if (num_lines > 1) {
> +               printf("Watching lines ");
> +               for (i = 0; i < num_lines - 1; i++)
> +                       printf("%s, ", lines[i]);
> +               printf("and %s...\n", lines[i]);
> +       } else {
> +               printf("Watching line %s ...\n", lines[0]);
> +       }
> +}
> +
> +static void event_print(struct gpiod_info_event *event, const char *chip_id)
> +{
> +       struct gpiod_line_info *info;
> +       uint64_t evtime, before, after, mono;
> +       char *evname;
> +       int evtype;
> +       struct timespec ts;
> +
> +       info = gpiod_info_event_get_line_info(event);
> +       evtime = gpiod_info_event_get_timestamp_ns(event);
> +       evtype = gpiod_info_event_get_event_type(event);
> +
> +       switch (evtype) {
> +       case GPIOD_INFO_EVENT_LINE_REQUESTED:
> +               evname = "REQUESTED";
> +               break;
> +       case GPIOD_INFO_EVENT_LINE_RELEASED:
> +               evname = "RELEASED ";
> +               break;
> +       case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
> +               evname = "RECONFIG ";
> +               break;
> +       default:
> +               evname = "UNKNOWN  ";
> +       }

Newline for readability, please.

> +       if (event_clock_mode) {

C-style comments only except for SPDX headers please.

> +               // map clock monotonic to realtime, as uAPI only supports CLOCK_MONOTONIC
> +               clock_gettime(CLOCK_REALTIME, &ts);
> +               before = ts.tv_nsec + ts.tv_sec * 1000000000;
> +               clock_gettime(CLOCK_MONOTONIC, &ts);
> +               mono = ts.tv_nsec + ts.tv_sec * 1000000000;
> +               clock_gettime(CLOCK_REALTIME, &ts);
> +               after = ts.tv_nsec + ts.tv_sec * 1000000000;
> +               evtime += (after/2 - mono + before/2);
> +       }

Moar newlines, I really like between blocks of code, it really helps me, thanks.

> +       print_event_time(evtime, event_clock_mode);
> +       printf(" %s", evname);
> +       if (chip_id)
> +               printf(" %s %d", chip_id, gpiod_line_info_get_offset(info));
> +       print_line_info(info);
> +       printf("\n");
> +}
> +
> +int main(int argc, char **argv)
> +{
> +       int i, j;
> +       struct gpiod_chip **chips;
> +       struct pollfd *pollfds;
> +       struct gpiod_chip *chip;
> +       struct line_resolver *resolver;
> +       struct gpiod_info_event *event;
> +       struct config cfg;
> +
> +       i = parse_config(argc, argv, &cfg);
> +       argc -= optind;
> +       argv += optind;
> +
> +       if (argc < 1)
> +               die("at least one GPIO line must be specified");
> +
> +       if (argc > 64)
> +               die("too many lines given");
> +
> +       resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict, by_name);
> +       chips = calloc(resolver->num_chips, sizeof(*chips));
> +       pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
> +       if (!pollfds)
> +               die("out of memory");
> +       for (i = 0; i < resolver->num_chips; i++) {
> +               chip = gpiod_chip_open(resolver->chip_paths[i]);
> +               if (!chip)
> +                       die_perror("unable to open chip %s", resolver->chip_paths[i]);
> +

Don't other tools do the same thing basically (resolving and opening
chips)? Can't we fold it into tools-common.c so that we get a list of
open chips?

> +               for (j = 0; j < resolver->num_lines; j++)
> +                       if (resolver->lines[j].chip_path == resolver->chip_paths[i])
> +                               if (!gpiod_chip_watch_line_info(chip, resolver->lines[j].offset))
> +                                       die_perror("unable to watch line on chip %s",
> +                                                  resolver->chip_paths[i]);
> +
> +               chips[i] = chip;
> +               pollfds[i].fd = gpiod_chip_get_fd(chip);
> +               pollfds[i].events = POLLIN;
> +       }
> +
> +       if (banner)
> +               print_banner(argc, argv);
> +
> +       for (;;) {
> +               if (poll(pollfds, resolver->num_chips, -1) < 0)
> +                       die_perror("error polling for events");
> +
> +               for (i = 0; i < resolver->num_chips; i++) {
> +                       if (pollfds[i].revents == 0)
> +                               continue;
> +
> +                       event = gpiod_chip_read_info_event(chips[i]);
> +                       event_print(event, cfg.chip_id);
> +               }
> +       }
> +       for (i = 0; i < resolver->num_chips; i++)
> +               gpiod_chip_close(chips[i]);
> +       free(chips);
> +       free_line_resolver(resolver);
> +
> +       return EXIT_SUCCESS;
> +}
> --
> 2.36.1
>

Looks good to me, I would have probably added a machine-readable
output formatting like gpiomon but we can always extend it later.

Bart

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 1/4] tools: line name focussed rework
  2022-07-06 20:20   ` Bartosz Golaszewski
@ 2022-07-07  2:24     ` Kent Gibson
  2022-07-07  9:01       ` Bartosz Golaszewski
  0 siblings, 1 reply; 15+ messages in thread
From: Kent Gibson @ 2022-07-07  2:24 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: open list:GPIO SUBSYSTEM

On Wed, Jul 06, 2022 at 10:20:00PM +0200, Bartosz Golaszewski wrote:
> On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > Rework the tool suite to support identifying lines by name and to
> > support operating on the GPIO lines available to the user at once, rather
> > than on one particular GPIO chip.
> >
> > All tools, other than gpiodetect, now provide the name to (chip,offset)
> > mapping that was previously only performed by gpiofind. As names are not
> > guaranteed to be unique, a --strict option is provided for all tools to
> > either abort the operation or report all lines with the matching name, as
> > appropriate.
> > By default the tools operate on the first line found with a matching name.
> >
> > Selection of line by (chip,offset) is still supported with a --chip
> > option, though it restricts the scope of the operation to an individual
> > chip.  When the --chip option is specified, the lines are assumed to be
> > identified by offset where they parse as an integer, else by name.
> > To cater for the unusual case where a line name parses as an integer,
> > but is different from the offset, the --by-name option forces the lines
> > to be identified by name.
> >
> 
> We could potentially extend it by allowing multiple --chip arguments
> for multiple chips but let's not go there unless requested.
> 

We could but then we'd have to custom parse the command line.
Or repeatedly apply getopt()?
So yeah, keep it simple for now.

> > The updated tools are intentionally NOT backwardly compatible with the
> > previous tools. Using old command lines with the updated tools will
> > almost certainly fail, though migrating old command lines is generally as
> > simple as adding a '-c' before the chip.
> >
> > In addition the individual tools are modified as follows:
> >
> > gpiodetect:
> >
> > Add the option to select individual chips.
> >
> 
> We discussed at some point that gpiodetect should also check if any of
> the files it iterates over is a symbolic link to a GPIO device and
> print some info accordingly (e.g. foobar [link to /dev/gpiochip2])-
> are you thinking about adding this too?
> 

Did we?  My bad then - I clearly forgot and instead filtered the symlinks
out so it only reports actual chips (btw the v1 tools report the symlinks
by repeating the actual, which is the worst of both worlds).

> > gpiofind:
> >
> > Add the option to display the info for found lines.
> >
> > gpioinfo:
> >
> > Change the focus from chips to lines, so the scope can be
> > an individual line, a subset of lines, all lines on a particular chip,
> > or all the lines available to the user.  For line scope a single line
> > summary is output for each line.  For chip scope the existing format
> > displaying a summary of the chip and each of its lines is retained.
> >
> > Line attributes are consolidated into a list format, and are extended
> > to cover all attributes supported by uAPI v2.
> >
> > gpioget:
> >
> > The default output format is becomes line=value, as per the
> > input for gpioset, and the value is reported as active or inactive,
> > rather than 0 or 1.
> > The previous format is available using the --numeric option.
> >
> > Add an optional hold period between requesting a line and reading the
> > value to allow the line to settle once the requested configuration has
> > been applied (e.g. bias).
> >
> > gpiomon:
> >
> > Consolidate the edge options into a single option.
> >
> > Add a debounce period option.
> >
> > Add options to report event times as UTC or localtime.
> >
> > Add format specifiers for GPIO chip path, line name, stringified event
> > type, and event time as a datetime.
> >
> > Rearrange default output format to place fields with more predicable
> > widths to the left, and to separate major field groups with tabs.
> > Lines are identified consistent with the command line.
> >
> > gpioset:
> >
> > Add a hold period option that specifies the minimum period the line
> > value must be held for.  This applies to all set options.
> >
> > Support line values specified as active/inactive, on/off and
> > true/false, as well as 1/0.
> >
> > Add a toggle option that specifies a time sequence over which the
> > requested lines should be toggled.  If the sequence is 0 terminated then
> > gpioset exits when the sequence completes, else it repeats the sequence.
> > This allows for anything from simple blinkers to bit bashing from the
> > command line. e.g. gpioset -t 500ms LED=on
> >
> > Add an interactive option to provide a shell-like interface to allow
> > manual or scripted manipulation of requested lines.  A basic command set
> > allows lines to be get, set, or toggled, and to insert sleeps between
> > operations.
> >
> > Remove the --mode, --sec, and --usec options.
> > The combination of hold period and interactive mode provide functionality
> > equivalent to the old --mode options.
> >
> > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > ---
> >  configure.ac         |   9 +-
> >  tools/gpiodetect.c   | 108 ++++--
> >  tools/gpiofind.c     | 126 +++++--
> >  tools/gpioget.c      | 200 ++++++----
> >  tools/gpioinfo.c     | 356 ++++++++----------
> >  tools/gpiomon.c      | 493 +++++++++++++++----------
> >  tools/gpioset.c      | 861 +++++++++++++++++++++++++++++++------------
> >  tools/tools-common.c | 640 +++++++++++++++++++++++++++++---
> >  tools/tools-common.h |  59 ++-
> >  9 files changed, 2011 insertions(+), 841 deletions(-)
> >
> > diff --git a/configure.ac b/configure.ac
> > index ab03673..c7e353c 100644
> > --- a/configure.ac
> > +++ b/configure.ac
> > @@ -105,14 +105,15 @@ AC_DEFUN([FUNC_NOT_FOUND_TOOLS],
> >  AC_DEFUN([HEADER_NOT_FOUND_TOOLS],
> >         [ERR_NOT_FOUND([$1 header], [tools])])
> >
> > +AC_DEFUN([LIB_NOT_FOUND_TOOLS],
> > +       [ERR_NOT_FOUND([lib$1], [tools])])
> > +
> >  if test "x$with_tools" = xtrue
> >  then
> >         # These are only needed to build tools
> > -       AC_CHECK_FUNC([basename], [], [FUNC_NOT_FOUND_TOOLS([basename])])
> >         AC_CHECK_FUNC([daemon], [], [FUNC_NOT_FOUND_TOOLS([daemon])])
> > -       AC_CHECK_FUNC([signalfd], [], [FUNC_NOT_FOUND_TOOLS([signalfd])])
> > -       AC_CHECK_FUNC([setlinebuf], [], [FUNC_NOT_FOUND_TOOLS([setlinebuf])])
> > -       AC_CHECK_HEADERS([sys/signalfd.h], [], [HEADER_NOT_FOUND_TOOLS([sys/signalfd.h])])
> > +       AC_CHECK_HEADERS([readline/readline.h], [], [HEADER_NOT_FOUND_TOOLS([readline/readline.h])])
> > +       AC_CHECK_LIB([readline], readline, [], [LIB_NOT_FOUND_TOOLS([readline])])
> 
> readline is licensed under GPLv3 - this bleeds into gpioset and will
> make a lot of companies bounce off of it. Any chance you could use
> libedit instead? It's supposed to be a drop-in replacement for
> readline but I have never used it first hand.
> 

Hey, you mentioned readline before I implemented it.
Though I don't recall "avoid" being mentioned :-(.

Ok, I'll take a look.  Hopefully it is just a drop-in.

Out of curiosity which aspect of GPLv3 is the problem, for a tool
which is publicly available and they aren't going to modify?
Just having a GPLv3 library on their system?

> >  fi
> >
> >  AC_ARG_ENABLE([tests],
> > diff --git a/tools/gpiodetect.c b/tools/gpiodetect.c
> > index 8f6e8b3..18b6e95 100644
> > --- a/tools/gpiodetect.c
> > +++ b/tools/gpiodetect.c
> > @@ -1,7 +1,6 @@
> >  // SPDX-License-Identifier: GPL-2.0-or-later
> > -// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
> > +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
> 
> This should have your copyright, not mine. Same elsewhere.
> 

Your argument would be stronger for gpiowatch, though even that was
based on your gpiomon.

These files are originally yours, I just modified them, so it wouldn't
be right to replace your copyright.

And as you mention below, you will be maintaining these and to keep it
simple I grant you the copyrights to the content of this series.

> [snip]
> 
> I will try to get back to this one later but it's so big I just kept
> losing focus. Do you think you could submit it the way I did for the
> bindings - remove the existing code and then apply the new
> implementations?
> 

Hey, this is nothing compared to your bindings ;-).

I'm not a huge fan of the remove and replace approach as it loses history,
and while the changes here are substantial they aren't a complete
replacement like the bindings changes were.

If the diff is too confusing I usually refer to the final code for
clarity.

I had originally split it per-tool plus common code, but that didn't
help much.  But I'll take another look at making the patches more
easily digestible.

> Just a general note on coding style: I would prefer that you use /* */
> style comments for the sake of consistency with the rest of the code
> and if you tried to keep the lines limited to 80 characters wherever
> it doesn't significantly impact readability? A sprinkle of newlines
> here and there separating logical blocks of code would be nice too.
> 

Keeping to 80 characters might be tough, I find 100 limiting, but I'll
try.  The kernel has switched to 100, right?

I guess I can break out a green screen terminal.

> We may not agree on the above but after all I will maintain that code
> and would prefer to keep it so that it stays easy for my brain to
> parse it.
> 

Indeed.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 3/4] tools: add gpiowatch
  2022-07-06 20:46   ` Bartosz Golaszewski
@ 2022-07-07  2:27     ` Kent Gibson
  2022-07-07  8:41       ` Bartosz Golaszewski
  0 siblings, 1 reply; 15+ messages in thread
From: Kent Gibson @ 2022-07-07  2:27 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: open list:GPIO SUBSYSTEM

On Wed, Jul 06, 2022 at 10:46:28PM +0200, Bartosz Golaszewski wrote:
> On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > Add a gpiowatch tool, based on gpiomon, but reporting line info change
> > events similar to the gpio-watch tool in the linux kernel.
> >
> > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > ---
> >  man/Makefile.am   |   2 +-
> >  tools/.gitignore  |   1 +
> >  tools/Makefile.am |   4 +-
> >  tools/gpiowatch.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++
> >  4 files changed, 219 insertions(+), 2 deletions(-)
> >  create mode 100644 tools/gpiowatch.c
> >
> > diff --git a/man/Makefile.am b/man/Makefile.am
> > index 4d2c29b..3badd3b 100644
> > --- a/man/Makefile.am
> > +++ b/man/Makefile.am
> > @@ -3,7 +3,7 @@
> >
> >  if WITH_MANPAGES
> >
> > -dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man
> > +dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man gpiowatch.man
> >
> >  %.man: $(top_builddir)/tools/$(*F)
> >         help2man $(top_builddir)/tools/$(*F) --include=$(srcdir)/template --output=$(builddir)/$@ --no-info
> > diff --git a/tools/.gitignore b/tools/.gitignore
> > index 0d53de9..6175e26 100644
> > --- a/tools/.gitignore
> > +++ b/tools/.gitignore
> > @@ -7,3 +7,4 @@ gpioget
> >  gpioset
> >  gpiomon
> >  gpiofind
> > +gpiowatch
> > diff --git a/tools/Makefile.am b/tools/Makefile.am
> > index 4a13266..8bb2cac 100644
> > --- a/tools/Makefile.am
> > +++ b/tools/Makefile.am
> > @@ -9,7 +9,7 @@ libtools_common_la_SOURCES = tools-common.c tools-common.h
> >
> >  LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la
> >
> > -bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind
> > +bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind gpiowatch
> >
> >  gpiodetect_SOURCES = gpiodetect.c
> >
> > @@ -23,6 +23,8 @@ gpiomon_SOURCES = gpiomon.c
> >
> >  gpiofind_SOURCES = gpiofind.c
> >
> > +gpiowatch_SOURCES = gpiowatch.c
> > +
> >  EXTRA_DIST = gpio-tools-test gpio-tools-test.bats
> >
> >  if WITH_TESTS
> > diff --git a/tools/gpiowatch.c b/tools/gpiowatch.c
> > new file mode 100644
> > index 0000000..e6bfeb6
> > --- /dev/null
> > +++ b/tools/gpiowatch.c
> > @@ -0,0 +1,214 @@
> > +// SPDX-License-Identifier: GPL-2.0-or-later
> > +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
> > +
> > +#include <getopt.h>
> > +#include <gpiod.h>
> > +#include <inttypes.h>
> > +#include <poll.h>
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +#include <string.h>
> > +#include <time.h>
> > +
> > +#include "tools-common.h"
> > +
> > +static int by_name;
> > +static int event_clock_mode;
> > +static int banner;
> > +
> > +static const struct option longopts[] = {
> > +       { "banner",             no_argument,            &banner,        1 },
> > +       { "by-name",            no_argument,            &by_name,       1 },
> > +       { "chip",               required_argument,      NULL,   'c' },
> > +       { "help",               no_argument,            NULL,   'h' },
> > +       { "localtime",          no_argument,            &event_clock_mode,      2 },
> > +       { "strict",             no_argument,            NULL,   's' },
> > +       { "utc",                no_argument,            &event_clock_mode,      1 },
> > +       { "version",            no_argument,            NULL,   'v' },
> > +       { GETOPT_NULL_LONGOPT },
> > +};
> > +
> > +static const char *const shortopts = "+c:shv";
> > +
> > +static void print_help(void)
> > +{
> > +       printf("Usage: %s [OPTIONS] <line> ...\n", get_progname());
> > +       printf("\n");
> > +       printf("Wait for changes to info on GPIO lines and print them to standard output.\n");
> > +       printf("\n");
> > +       printf("Lines are specified by name, or optionally by offset if the chip option\n");
> > +       printf("is provided.\n");
> > +       printf("\n");
> > +       printf("Options:\n");
> > +       printf("      --banner\t\tdisplay a banner on successful startup\n");
> > +       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
> > +       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
> > +       printf("  -h, --help\t\tdisplay this help and exit\n");
> > +       printf("      --localtime\treport event time as a local time (default is monotonic)\n");
> > +       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
> > +       printf("      --utc\t\treport event time as UTC (default is monotonic)\n");
> > +       printf("  -v, --version\t\toutput version information and exit\n");
> > +       print_chip_help();
> > +}
> > +
> > +struct config {
> > +       bool strict;
> > +       const char *chip_id;
> > +};
> 
> Let's either have all options in a local config struct (preferred) or
> all of them as global variables, otherwise it's confusing. If you want
> to use flags in long opts you can always define that structure within
> the function calling getopt().
> 

Agreed.  I wasn't keen on moving the struct into the function, but will
do.

Btw I prefer the long only options for the corner case options to avoid
poluting the short space.  The common options get a short form as well.

> > +
> > +int parse_config(int argc, char **argv, struct config *cfg)
> > +{
> > +       int opti, optc;
> > +
> > +       memset(cfg, 0, sizeof(*cfg));
> > +
> > +       for (;;) {
> > +               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
> > +               if (optc < 0)
> > +                       break;
> > +
> > +               switch (optc) {
> > +               case 'c':
> > +                       cfg->chip_id = optarg;
> > +                       break;
> > +               case 's':
> > +                       cfg->strict = true;
> > +                       break;
> > +               case 'h':
> > +                       print_help();
> > +                       exit(EXIT_SUCCESS);
> > +               case 'v':
> > +                       print_version();
> > +                       exit(EXIT_SUCCESS);
> > +               case '?':
> > +                       die("try %s --help", get_progname());
> > +               case 0:
> > +                       break;
> > +               default:
> > +                       abort();
> > +               }
> > +       }
> > +
> > +       return optind;
> > +}
> > +
> > +static void print_banner(int num_lines, char **lines)
> > +{
> > +       int i;
> > +
> > +       if (num_lines > 1) {
> > +               printf("Watching lines ");
> > +               for (i = 0; i < num_lines - 1; i++)
> > +                       printf("%s, ", lines[i]);
> > +               printf("and %s...\n", lines[i]);
> > +       } else {
> > +               printf("Watching line %s ...\n", lines[0]);
> > +       }
> > +}
> > +
> > +static void event_print(struct gpiod_info_event *event, const char *chip_id)
> > +{
> > +       struct gpiod_line_info *info;
> > +       uint64_t evtime, before, after, mono;
> > +       char *evname;
> > +       int evtype;
> > +       struct timespec ts;
> > +
> > +       info = gpiod_info_event_get_line_info(event);
> > +       evtime = gpiod_info_event_get_timestamp_ns(event);
> > +       evtype = gpiod_info_event_get_event_type(event);
> > +
> > +       switch (evtype) {
> > +       case GPIOD_INFO_EVENT_LINE_REQUESTED:
> > +               evname = "REQUESTED";
> > +               break;
> > +       case GPIOD_INFO_EVENT_LINE_RELEASED:
> > +               evname = "RELEASED ";
> > +               break;
> > +       case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
> > +               evname = "RECONFIG ";
> > +               break;
> > +       default:
> > +               evname = "UNKNOWN  ";
> > +       }
> 
> Newline for readability, please.
> 
> > +       if (event_clock_mode) {
> 
> C-style comments only except for SPDX headers please.
> 

Yeah, sorry - lazy habit.  I don't even notice I'm doing it.

> > +               // map clock monotonic to realtime, as uAPI only supports CLOCK_MONOTONIC
> > +               clock_gettime(CLOCK_REALTIME, &ts);
> > +               before = ts.tv_nsec + ts.tv_sec * 1000000000;
> > +               clock_gettime(CLOCK_MONOTONIC, &ts);
> > +               mono = ts.tv_nsec + ts.tv_sec * 1000000000;
> > +               clock_gettime(CLOCK_REALTIME, &ts);
> > +               after = ts.tv_nsec + ts.tv_sec * 1000000000;
> > +               evtime += (after/2 - mono + before/2);
> > +       }
> 
> Moar newlines, I really like between blocks of code, it really helps me, thanks.
> 

Oh, ok, I see this as being one block.  Where would you like the splits?

> > +       print_event_time(evtime, event_clock_mode);
> > +       printf(" %s", evname);
> > +       if (chip_id)
> > +               printf(" %s %d", chip_id, gpiod_line_info_get_offset(info));
> > +       print_line_info(info);
> > +       printf("\n");
> > +}
> > +
> > +int main(int argc, char **argv)
> > +{
> > +       int i, j;
> > +       struct gpiod_chip **chips;
> > +       struct pollfd *pollfds;
> > +       struct gpiod_chip *chip;
> > +       struct line_resolver *resolver;
> > +       struct gpiod_info_event *event;
> > +       struct config cfg;
> > +
> > +       i = parse_config(argc, argv, &cfg);
> > +       argc -= optind;
> > +       argv += optind;
> > +
> > +       if (argc < 1)
> > +               die("at least one GPIO line must be specified");
> > +
> > +       if (argc > 64)
> > +               die("too many lines given");
> > +
> > +       resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict, by_name);
> > +       chips = calloc(resolver->num_chips, sizeof(*chips));
> > +       pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
> > +       if (!pollfds)
> > +               die("out of memory");
> > +       for (i = 0; i < resolver->num_chips; i++) {
> > +               chip = gpiod_chip_open(resolver->chip_paths[i]);
> > +               if (!chip)
> > +                       die_perror("unable to open chip %s", resolver->chip_paths[i]);
> > +
> 
> Don't other tools do the same thing basically (resolving and opening
> chips)? Can't we fold it into tools-common.c so that we get a list of
> open chips?
> 

Yes and no.  I wasn't keen on opening all the chips at once in the
common code as in general the chips are immediately closed once the lines
are requested. And the request is tool specific.
Watch is a bit of an aberation in that regard - it holds the chips open
indefinitely.

> > +               for (j = 0; j < resolver->num_lines; j++)
> > +                       if (resolver->lines[j].chip_path == resolver->chip_paths[i])
> > +                               if (!gpiod_chip_watch_line_info(chip, resolver->lines[j].offset))
> > +                                       die_perror("unable to watch line on chip %s",
> > +                                                  resolver->chip_paths[i]);
> > +
> > +               chips[i] = chip;
> > +               pollfds[i].fd = gpiod_chip_get_fd(chip);
> > +               pollfds[i].events = POLLIN;
> > +       }
> > +
> > +       if (banner)
> > +               print_banner(argc, argv);
> > +
> > +       for (;;) {
> > +               if (poll(pollfds, resolver->num_chips, -1) < 0)
> > +                       die_perror("error polling for events");
> > +
> > +               for (i = 0; i < resolver->num_chips; i++) {
> > +                       if (pollfds[i].revents == 0)
> > +                               continue;
> > +
> > +                       event = gpiod_chip_read_info_event(chips[i]);
> > +                       event_print(event, cfg.chip_id);
> > +               }
> > +       }
> > +       for (i = 0; i < resolver->num_chips; i++)
> > +               gpiod_chip_close(chips[i]);
> > +       free(chips);
> > +       free_line_resolver(resolver);
> > +
> > +       return EXIT_SUCCESS;
> > +}
> > --
> > 2.36.1
> >
> 
> Looks good to me, I would have probably added a machine-readable
> output formatting like gpiomon but we can always extend it later.
> 

You mean the format option?

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 3/4] tools: add gpiowatch
  2022-07-07  2:27     ` Kent Gibson
@ 2022-07-07  8:41       ` Bartosz Golaszewski
  2022-07-07  8:55         ` Kent Gibson
  0 siblings, 1 reply; 15+ messages in thread
From: Bartosz Golaszewski @ 2022-07-07  8:41 UTC (permalink / raw)
  To: Kent Gibson; +Cc: open list:GPIO SUBSYSTEM

On Thu, Jul 7, 2022 at 4:27 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Wed, Jul 06, 2022 at 10:46:28PM +0200, Bartosz Golaszewski wrote:
> > On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
> > >
> > > Add a gpiowatch tool, based on gpiomon, but reporting line info change
> > > events similar to the gpio-watch tool in the linux kernel.
> > >
> > > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > > ---
> > >  man/Makefile.am   |   2 +-
> > >  tools/.gitignore  |   1 +
> > >  tools/Makefile.am |   4 +-
> > >  tools/gpiowatch.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++
> > >  4 files changed, 219 insertions(+), 2 deletions(-)
> > >  create mode 100644 tools/gpiowatch.c
> > >
> > > diff --git a/man/Makefile.am b/man/Makefile.am
> > > index 4d2c29b..3badd3b 100644
> > > --- a/man/Makefile.am
> > > +++ b/man/Makefile.am
> > > @@ -3,7 +3,7 @@
> > >
> > >  if WITH_MANPAGES
> > >
> > > -dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man
> > > +dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man gpiowatch.man
> > >
> > >  %.man: $(top_builddir)/tools/$(*F)
> > >         help2man $(top_builddir)/tools/$(*F) --include=$(srcdir)/template --output=$(builddir)/$@ --no-info
> > > diff --git a/tools/.gitignore b/tools/.gitignore
> > > index 0d53de9..6175e26 100644
> > > --- a/tools/.gitignore
> > > +++ b/tools/.gitignore
> > > @@ -7,3 +7,4 @@ gpioget
> > >  gpioset
> > >  gpiomon
> > >  gpiofind
> > > +gpiowatch
> > > diff --git a/tools/Makefile.am b/tools/Makefile.am
> > > index 4a13266..8bb2cac 100644
> > > --- a/tools/Makefile.am
> > > +++ b/tools/Makefile.am
> > > @@ -9,7 +9,7 @@ libtools_common_la_SOURCES = tools-common.c tools-common.h
> > >
> > >  LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la
> > >
> > > -bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind
> > > +bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind gpiowatch
> > >
> > >  gpiodetect_SOURCES = gpiodetect.c
> > >
> > > @@ -23,6 +23,8 @@ gpiomon_SOURCES = gpiomon.c
> > >
> > >  gpiofind_SOURCES = gpiofind.c
> > >
> > > +gpiowatch_SOURCES = gpiowatch.c
> > > +
> > >  EXTRA_DIST = gpio-tools-test gpio-tools-test.bats
> > >
> > >  if WITH_TESTS
> > > diff --git a/tools/gpiowatch.c b/tools/gpiowatch.c
> > > new file mode 100644
> > > index 0000000..e6bfeb6
> > > --- /dev/null
> > > +++ b/tools/gpiowatch.c
> > > @@ -0,0 +1,214 @@
> > > +// SPDX-License-Identifier: GPL-2.0-or-later
> > > +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
> > > +
> > > +#include <getopt.h>
> > > +#include <gpiod.h>
> > > +#include <inttypes.h>
> > > +#include <poll.h>
> > > +#include <stdio.h>
> > > +#include <stdlib.h>
> > > +#include <string.h>
> > > +#include <time.h>
> > > +
> > > +#include "tools-common.h"
> > > +
> > > +static int by_name;
> > > +static int event_clock_mode;
> > > +static int banner;
> > > +
> > > +static const struct option longopts[] = {
> > > +       { "banner",             no_argument,            &banner,        1 },
> > > +       { "by-name",            no_argument,            &by_name,       1 },
> > > +       { "chip",               required_argument,      NULL,   'c' },
> > > +       { "help",               no_argument,            NULL,   'h' },
> > > +       { "localtime",          no_argument,            &event_clock_mode,      2 },
> > > +       { "strict",             no_argument,            NULL,   's' },
> > > +       { "utc",                no_argument,            &event_clock_mode,      1 },
> > > +       { "version",            no_argument,            NULL,   'v' },
> > > +       { GETOPT_NULL_LONGOPT },
> > > +};
> > > +
> > > +static const char *const shortopts = "+c:shv";
> > > +
> > > +static void print_help(void)
> > > +{
> > > +       printf("Usage: %s [OPTIONS] <line> ...\n", get_progname());
> > > +       printf("\n");
> > > +       printf("Wait for changes to info on GPIO lines and print them to standard output.\n");
> > > +       printf("\n");
> > > +       printf("Lines are specified by name, or optionally by offset if the chip option\n");
> > > +       printf("is provided.\n");
> > > +       printf("\n");
> > > +       printf("Options:\n");
> > > +       printf("      --banner\t\tdisplay a banner on successful startup\n");
> > > +       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
> > > +       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
> > > +       printf("  -h, --help\t\tdisplay this help and exit\n");
> > > +       printf("      --localtime\treport event time as a local time (default is monotonic)\n");
> > > +       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
> > > +       printf("      --utc\t\treport event time as UTC (default is monotonic)\n");
> > > +       printf("  -v, --version\t\toutput version information and exit\n");
> > > +       print_chip_help();
> > > +}
> > > +
> > > +struct config {
> > > +       bool strict;
> > > +       const char *chip_id;
> > > +};
> >
> > Let's either have all options in a local config struct (preferred) or
> > all of them as global variables, otherwise it's confusing. If you want
> > to use flags in long opts you can always define that structure within
> > the function calling getopt().
> >
>
> Agreed.  I wasn't keen on moving the struct into the function, but will
> do.
>
> Btw I prefer the long only options for the corner case options to avoid
> poluting the short space.  The common options get a short form as well.
>
> > > +
> > > +int parse_config(int argc, char **argv, struct config *cfg)
> > > +{
> > > +       int opti, optc;
> > > +
> > > +       memset(cfg, 0, sizeof(*cfg));
> > > +
> > > +       for (;;) {
> > > +               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
> > > +               if (optc < 0)
> > > +                       break;
> > > +
> > > +               switch (optc) {
> > > +               case 'c':
> > > +                       cfg->chip_id = optarg;
> > > +                       break;
> > > +               case 's':
> > > +                       cfg->strict = true;
> > > +                       break;
> > > +               case 'h':
> > > +                       print_help();
> > > +                       exit(EXIT_SUCCESS);
> > > +               case 'v':
> > > +                       print_version();
> > > +                       exit(EXIT_SUCCESS);
> > > +               case '?':
> > > +                       die("try %s --help", get_progname());
> > > +               case 0:
> > > +                       break;
> > > +               default:
> > > +                       abort();
> > > +               }
> > > +       }
> > > +
> > > +       return optind;
> > > +}
> > > +
> > > +static void print_banner(int num_lines, char **lines)
> > > +{
> > > +       int i;
> > > +
> > > +       if (num_lines > 1) {
> > > +               printf("Watching lines ");
> > > +               for (i = 0; i < num_lines - 1; i++)
> > > +                       printf("%s, ", lines[i]);
> > > +               printf("and %s...\n", lines[i]);
> > > +       } else {
> > > +               printf("Watching line %s ...\n", lines[0]);
> > > +       }
> > > +}
> > > +
> > > +static void event_print(struct gpiod_info_event *event, const char *chip_id)
> > > +{
> > > +       struct gpiod_line_info *info;
> > > +       uint64_t evtime, before, after, mono;
> > > +       char *evname;
> > > +       int evtype;
> > > +       struct timespec ts;
> > > +
> > > +       info = gpiod_info_event_get_line_info(event);
> > > +       evtime = gpiod_info_event_get_timestamp_ns(event);
> > > +       evtype = gpiod_info_event_get_event_type(event);
> > > +
> > > +       switch (evtype) {
> > > +       case GPIOD_INFO_EVENT_LINE_REQUESTED:
> > > +               evname = "REQUESTED";
> > > +               break;
> > > +       case GPIOD_INFO_EVENT_LINE_RELEASED:
> > > +               evname = "RELEASED ";
> > > +               break;
> > > +       case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
> > > +               evname = "RECONFIG ";
> > > +               break;
> > > +       default:
> > > +               evname = "UNKNOWN  ";
> > > +       }
> >
> > Newline for readability, please.
> >
> > > +       if (event_clock_mode) {
> >
> > C-style comments only except for SPDX headers please.
> >
>
> Yeah, sorry - lazy habit.  I don't even notice I'm doing it.
>
> > > +               // map clock monotonic to realtime, as uAPI only supports CLOCK_MONOTONIC
> > > +               clock_gettime(CLOCK_REALTIME, &ts);
> > > +               before = ts.tv_nsec + ts.tv_sec * 1000000000;
> > > +               clock_gettime(CLOCK_MONOTONIC, &ts);
> > > +               mono = ts.tv_nsec + ts.tv_sec * 1000000000;
> > > +               clock_gettime(CLOCK_REALTIME, &ts);
> > > +               after = ts.tv_nsec + ts.tv_sec * 1000000000;
> > > +               evtime += (after/2 - mono + before/2);
> > > +       }
> >
> > Moar newlines, I really like between blocks of code, it really helps me, thanks.
> >
>
> Oh, ok, I see this as being one block.  Where would you like the splits?
>

I will add them myself later, don't worry about it.

> > > +       print_event_time(evtime, event_clock_mode);
> > > +       printf(" %s", evname);
> > > +       if (chip_id)
> > > +               printf(" %s %d", chip_id, gpiod_line_info_get_offset(info));
> > > +       print_line_info(info);
> > > +       printf("\n");
> > > +}
> > > +
> > > +int main(int argc, char **argv)
> > > +{
> > > +       int i, j;
> > > +       struct gpiod_chip **chips;
> > > +       struct pollfd *pollfds;
> > > +       struct gpiod_chip *chip;
> > > +       struct line_resolver *resolver;
> > > +       struct gpiod_info_event *event;
> > > +       struct config cfg;
> > > +
> > > +       i = parse_config(argc, argv, &cfg);
> > > +       argc -= optind;
> > > +       argv += optind;
> > > +
> > > +       if (argc < 1)
> > > +               die("at least one GPIO line must be specified");
> > > +
> > > +       if (argc > 64)
> > > +               die("too many lines given");
> > > +
> > > +       resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict, by_name);
> > > +       chips = calloc(resolver->num_chips, sizeof(*chips));
> > > +       pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
> > > +       if (!pollfds)
> > > +               die("out of memory");
> > > +       for (i = 0; i < resolver->num_chips; i++) {
> > > +               chip = gpiod_chip_open(resolver->chip_paths[i]);
> > > +               if (!chip)
> > > +                       die_perror("unable to open chip %s", resolver->chip_paths[i]);
> > > +
> >
> > Don't other tools do the same thing basically (resolving and opening
> > chips)? Can't we fold it into tools-common.c so that we get a list of
> > open chips?
> >
>
> Yes and no.  I wasn't keen on opening all the chips at once in the
> common code as in general the chips are immediately closed once the lines
> are requested. And the request is tool specific.
> Watch is a bit of an aberation in that regard - it holds the chips open
> indefinitely.
>

Right, makes sense.

> > > +               for (j = 0; j < resolver->num_lines; j++)
> > > +                       if (resolver->lines[j].chip_path == resolver->chip_paths[i])
> > > +                               if (!gpiod_chip_watch_line_info(chip, resolver->lines[j].offset))
> > > +                                       die_perror("unable to watch line on chip %s",
> > > +                                                  resolver->chip_paths[i]);
> > > +
> > > +               chips[i] = chip;
> > > +               pollfds[i].fd = gpiod_chip_get_fd(chip);
> > > +               pollfds[i].events = POLLIN;
> > > +       }
> > > +
> > > +       if (banner)
> > > +               print_banner(argc, argv);
> > > +
> > > +       for (;;) {
> > > +               if (poll(pollfds, resolver->num_chips, -1) < 0)
> > > +                       die_perror("error polling for events");
> > > +
> > > +               for (i = 0; i < resolver->num_chips; i++) {
> > > +                       if (pollfds[i].revents == 0)
> > > +                               continue;
> > > +
> > > +                       event = gpiod_chip_read_info_event(chips[i]);
> > > +                       event_print(event, cfg.chip_id);
> > > +               }
> > > +       }
> > > +       for (i = 0; i < resolver->num_chips; i++)
> > > +               gpiod_chip_close(chips[i]);
> > > +       free(chips);
> > > +       free_line_resolver(resolver);
> > > +
> > > +       return EXIT_SUCCESS;
> > > +}
> > > --
> > > 2.36.1
> > >
> >
> > Looks good to me, I would have probably added a machine-readable
> > output formatting like gpiomon but we can always extend it later.
> >
>
> You mean the format option?
>

Yes. But that can be easily added later.

Bart

> Cheers,
> Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 3/4] tools: add gpiowatch
  2022-07-07  8:41       ` Bartosz Golaszewski
@ 2022-07-07  8:55         ` Kent Gibson
  0 siblings, 0 replies; 15+ messages in thread
From: Kent Gibson @ 2022-07-07  8:55 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: open list:GPIO SUBSYSTEM

On Thu, Jul 07, 2022 at 10:41:21AM +0200, Bartosz Golaszewski wrote:
> On Thu, Jul 7, 2022 at 4:27 AM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Wed, Jul 06, 2022 at 10:46:28PM +0200, Bartosz Golaszewski wrote:
> > > On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
> > > >
> > > > Add a gpiowatch tool, based on gpiomon, but reporting line info change
> > > > events similar to the gpio-watch tool in the linux kernel.
> > > >
> > > > +       }

...

> > >
> > > Moar newlines, I really like between blocks of code, it really helps me, thanks.
> > >
> >
> > Oh, ok, I see this as being one block.  Where would you like the splits?
> >
> 
> I will add them myself later, don't worry about it.
> 

I think I know what you mean.
Anyway, I'll add a tonne of whitespace...

> > > > +       print_event_time(evtime, event_clock_mode);
> > > > +       printf(" %s", evname);

Like here

> > > > +       if (chip_id)
> > > > +               printf(" %s %d", chip_id, gpiod_line_info_get_offset(info));

and here.

> > > > +       print_line_info(info);
> > > > +       printf("\n");
> > > > +}
> > > > +

...

> > > >
> > >
> > > Looks good to me, I would have probably added a machine-readable
> > > output formatting like gpiomon but we can always extend it later.
> > >
> >
> > You mean the format option?
> >
> 
> Yes. But that can be easily added later.
> 

I was thinking the Python bindings would be the way to go if you want
to extract specific bits.

Turns out when I created a new libgpiod workspace in my editor it
defaulted tabs to 4 spaces, which might've been the cause of some
unfortunate formatting.  Sorted for the next version.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 1/4] tools: line name focussed rework
  2022-07-07  2:24     ` Kent Gibson
@ 2022-07-07  9:01       ` Bartosz Golaszewski
  2022-07-07  9:18         ` Kent Gibson
  0 siblings, 1 reply; 15+ messages in thread
From: Bartosz Golaszewski @ 2022-07-07  9:01 UTC (permalink / raw)
  To: Kent Gibson; +Cc: open list:GPIO SUBSYSTEM

On Thu, Jul 7, 2022 at 4:24 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Wed, Jul 06, 2022 at 10:20:00PM +0200, Bartosz Golaszewski wrote:
> > On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
> > >
> > > Rework the tool suite to support identifying lines by name and to
> > > support operating on the GPIO lines available to the user at once, rather
> > > than on one particular GPIO chip.
> > >
> > > All tools, other than gpiodetect, now provide the name to (chip,offset)
> > > mapping that was previously only performed by gpiofind. As names are not
> > > guaranteed to be unique, a --strict option is provided for all tools to
> > > either abort the operation or report all lines with the matching name, as
> > > appropriate.
> > > By default the tools operate on the first line found with a matching name.
> > >
> > > Selection of line by (chip,offset) is still supported with a --chip
> > > option, though it restricts the scope of the operation to an individual
> > > chip.  When the --chip option is specified, the lines are assumed to be
> > > identified by offset where they parse as an integer, else by name.
> > > To cater for the unusual case where a line name parses as an integer,
> > > but is different from the offset, the --by-name option forces the lines
> > > to be identified by name.
> > >
> >
> > We could potentially extend it by allowing multiple --chip arguments
> > for multiple chips but let's not go there unless requested.
> >
>
> We could but then we'd have to custom parse the command line.
> Or repeatedly apply getopt()?
> So yeah, keep it simple for now.

getopt() will go to the relevant switch case everytime it encounters
the flag, then you can process it and add it to the list.

>
> > > The updated tools are intentionally NOT backwardly compatible with the
> > > previous tools. Using old command lines with the updated tools will
> > > almost certainly fail, though migrating old command lines is generally as
> > > simple as adding a '-c' before the chip.
> > >
> > > In addition the individual tools are modified as follows:
> > >
> > > gpiodetect:
> > >
> > > Add the option to select individual chips.
> > >
> >
> > We discussed at some point that gpiodetect should also check if any of
> > the files it iterates over is a symbolic link to a GPIO device and
> > print some info accordingly (e.g. foobar [link to /dev/gpiochip2])-
> > are you thinking about adding this too?
> >
>
> Did we?  My bad then - I clearly forgot and instead filtered the symlinks
> out so it only reports actual chips (btw the v1 tools report the symlinks
> by repeating the actual, which is the worst of both worlds).
>

I think so. In any case I think this would be useful.

> > > gpiofind:
> > >
> > > Add the option to display the info for found lines.
> > >
> > > gpioinfo:
> > >
> > > Change the focus from chips to lines, so the scope can be
> > > an individual line, a subset of lines, all lines on a particular chip,
> > > or all the lines available to the user.  For line scope a single line
> > > summary is output for each line.  For chip scope the existing format
> > > displaying a summary of the chip and each of its lines is retained.
> > >
> > > Line attributes are consolidated into a list format, and are extended
> > > to cover all attributes supported by uAPI v2.
> > >
> > > gpioget:
> > >
> > > The default output format is becomes line=value, as per the
> > > input for gpioset, and the value is reported as active or inactive,
> > > rather than 0 or 1.
> > > The previous format is available using the --numeric option.
> > >
> > > Add an optional hold period between requesting a line and reading the
> > > value to allow the line to settle once the requested configuration has
> > > been applied (e.g. bias).
> > >
> > > gpiomon:
> > >
> > > Consolidate the edge options into a single option.
> > >
> > > Add a debounce period option.
> > >
> > > Add options to report event times as UTC or localtime.
> > >
> > > Add format specifiers for GPIO chip path, line name, stringified event
> > > type, and event time as a datetime.
> > >
> > > Rearrange default output format to place fields with more predicable
> > > widths to the left, and to separate major field groups with tabs.
> > > Lines are identified consistent with the command line.
> > >
> > > gpioset:
> > >
> > > Add a hold period option that specifies the minimum period the line
> > > value must be held for.  This applies to all set options.
> > >
> > > Support line values specified as active/inactive, on/off and
> > > true/false, as well as 1/0.
> > >
> > > Add a toggle option that specifies a time sequence over which the
> > > requested lines should be toggled.  If the sequence is 0 terminated then
> > > gpioset exits when the sequence completes, else it repeats the sequence.
> > > This allows for anything from simple blinkers to bit bashing from the
> > > command line. e.g. gpioset -t 500ms LED=on
> > >
> > > Add an interactive option to provide a shell-like interface to allow
> > > manual or scripted manipulation of requested lines.  A basic command set
> > > allows lines to be get, set, or toggled, and to insert sleeps between
> > > operations.
> > >
> > > Remove the --mode, --sec, and --usec options.
> > > The combination of hold period and interactive mode provide functionality
> > > equivalent to the old --mode options.
> > >
> > > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > > ---
> > >  configure.ac         |   9 +-
> > >  tools/gpiodetect.c   | 108 ++++--
> > >  tools/gpiofind.c     | 126 +++++--
> > >  tools/gpioget.c      | 200 ++++++----
> > >  tools/gpioinfo.c     | 356 ++++++++----------
> > >  tools/gpiomon.c      | 493 +++++++++++++++----------
> > >  tools/gpioset.c      | 861 +++++++++++++++++++++++++++++++------------
> > >  tools/tools-common.c | 640 +++++++++++++++++++++++++++++---
> > >  tools/tools-common.h |  59 ++-
> > >  9 files changed, 2011 insertions(+), 841 deletions(-)
> > >
> > > diff --git a/configure.ac b/configure.ac
> > > index ab03673..c7e353c 100644
> > > --- a/configure.ac
> > > +++ b/configure.ac
> > > @@ -105,14 +105,15 @@ AC_DEFUN([FUNC_NOT_FOUND_TOOLS],
> > >  AC_DEFUN([HEADER_NOT_FOUND_TOOLS],
> > >         [ERR_NOT_FOUND([$1 header], [tools])])
> > >
> > > +AC_DEFUN([LIB_NOT_FOUND_TOOLS],
> > > +       [ERR_NOT_FOUND([lib$1], [tools])])
> > > +
> > >  if test "x$with_tools" = xtrue
> > >  then
> > >         # These are only needed to build tools
> > > -       AC_CHECK_FUNC([basename], [], [FUNC_NOT_FOUND_TOOLS([basename])])
> > >         AC_CHECK_FUNC([daemon], [], [FUNC_NOT_FOUND_TOOLS([daemon])])
> > > -       AC_CHECK_FUNC([signalfd], [], [FUNC_NOT_FOUND_TOOLS([signalfd])])
> > > -       AC_CHECK_FUNC([setlinebuf], [], [FUNC_NOT_FOUND_TOOLS([setlinebuf])])
> > > -       AC_CHECK_HEADERS([sys/signalfd.h], [], [HEADER_NOT_FOUND_TOOLS([sys/signalfd.h])])
> > > +       AC_CHECK_HEADERS([readline/readline.h], [], [HEADER_NOT_FOUND_TOOLS([readline/readline.h])])
> > > +       AC_CHECK_LIB([readline], readline, [], [LIB_NOT_FOUND_TOOLS([readline])])
> >
> > readline is licensed under GPLv3 - this bleeds into gpioset and will
> > make a lot of companies bounce off of it. Any chance you could use
> > libedit instead? It's supposed to be a drop-in replacement for
> > readline but I have never used it first hand.
> >
>
> Hey, you mentioned readline before I implemented it.
> Though I don't recall "avoid" being mentioned :-(.
>

No, you're right, I mentioned it but then realized it's GPLv3.

> Ok, I'll take a look.  Hopefully it is just a drop-in.
>
> Out of curiosity which aspect of GPLv3 is the problem, for a tool
> which is publicly available and they aren't going to modify?
> Just having a GPLv3 library on their system?
>

You'd be surprised how allergic companies are to GPLv3. Anything
"tainted" with GPLv3 is often banned.

> > >  fi
> > >
> > >  AC_ARG_ENABLE([tests],
> > > diff --git a/tools/gpiodetect.c b/tools/gpiodetect.c
> > > index 8f6e8b3..18b6e95 100644
> > > --- a/tools/gpiodetect.c
> > > +++ b/tools/gpiodetect.c
> > > @@ -1,7 +1,6 @@
> > >  // SPDX-License-Identifier: GPL-2.0-or-later
> > > -// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
> > > +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
> >
> > This should have your copyright, not mine. Same elsewhere.
> >
>
> Your argument would be stronger for gpiowatch, though even that was
> based on your gpiomon.
>
> These files are originally yours, I just modified them, so it wouldn't
> be right to replace your copyright.
>
> And as you mention below, you will be maintaining these and to keep it
> simple I grant you the copyrights to the content of this series.
>

You're introducing very significant changes to the code, I think it's
only fair to include your name here together with git history. It
should be:

// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>

All open-source projects do it this way. And yeah - especially for
gpiowatch as it's your file entirely.

> > [snip]
> >
> > I will try to get back to this one later but it's so big I just kept
> > losing focus. Do you think you could submit it the way I did for the
> > bindings - remove the existing code and then apply the new
> > implementations?
> >
>
> Hey, this is nothing compared to your bindings ;-).
>

I know, it was just late :)

> I'm not a huge fan of the remove and replace approach as it loses history,
> and while the changes here are substantial they aren't a complete
> replacement like the bindings changes were.
>

Oh, sure, we'd squash it back for the master branch, it's just for an
easier review.

> If the diff is too confusing I usually refer to the final code for
> clarity.
>
> I had originally split it per-tool plus common code, but that didn't
> help much.  But I'll take another look at making the patches more
> easily digestible.
>
> > Just a general note on coding style: I would prefer that you use /* */
> > style comments for the sake of consistency with the rest of the code
> > and if you tried to keep the lines limited to 80 characters wherever
> > it doesn't significantly impact readability? A sprinkle of newlines
> > here and there separating logical blocks of code would be nice too.
> >
>
> Keeping to 80 characters might be tough, I find 100 limiting, but I'll
> try.  The kernel has switched to 100, right?
>

The limit was technically lifted but let's see what the docs say:

---
The preferred limit on the length of a single line is 80 columns.

Statements longer than 80 columns should be broken into sensible chunks,
unless exceeding 80 columns significantly increases readability and does
not hide information
---

So 80 columns are still preferred.

> I guess I can break out a green screen terminal.
>

I feel personally attacked. :)

I personally use one big screen for work (and my laptop below) divided
into multiple columns. Keeping the line length limited to 80
characters allows for more columns. :)

Bart

> > We may not agree on the above but after all I will maintain that code
> > and would prefer to keep it so that it stays easy for my brain to
> > parse it.
> >
>
> Indeed.
>
> Cheers,
> Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [libgpiod v2][PATCH 1/4] tools: line name focussed rework
  2022-07-07  9:01       ` Bartosz Golaszewski
@ 2022-07-07  9:18         ` Kent Gibson
  0 siblings, 0 replies; 15+ messages in thread
From: Kent Gibson @ 2022-07-07  9:18 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: open list:GPIO SUBSYSTEM

On Thu, Jul 07, 2022 at 11:01:49AM +0200, Bartosz Golaszewski wrote:
> On Thu, Jul 7, 2022 at 4:24 AM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Wed, Jul 06, 2022 at 10:20:00PM +0200, Bartosz Golaszewski wrote:
> > > On Mon, Jun 27, 2022 at 3:46 PM Kent Gibson <warthog618@gmail.com> wrote:
> > > >
> > > > Rework the tool suite to support identifying lines by name and to
> > > > support operating on the GPIO lines available to the user at once, rather
> > > > than on one particular GPIO chip.
> > > >
> > > > All tools, other than gpiodetect, now provide the name to (chip,offset)
> > > > mapping that was previously only performed by gpiofind. As names are not
> > > > guaranteed to be unique, a --strict option is provided for all tools to
> > > > either abort the operation or report all lines with the matching name, as
> > > > appropriate.
> > > > By default the tools operate on the first line found with a matching name.
> > > >
> > > > Selection of line by (chip,offset) is still supported with a --chip
> > > > option, though it restricts the scope of the operation to an individual
> > > > chip.  When the --chip option is specified, the lines are assumed to be
> > > > identified by offset where they parse as an integer, else by name.
> > > > To cater for the unusual case where a line name parses as an integer,
> > > > but is different from the offset, the --by-name option forces the lines
> > > > to be identified by name.
> > > >
> > >
> > > We could potentially extend it by allowing multiple --chip arguments
> > > for multiple chips but let's not go there unless requested.
> > >
> >
> > We could but then we'd have to custom parse the command line.
> > Or repeatedly apply getopt()?
> > So yeah, keep it simple for now.
> 
> getopt() will go to the relevant switch case everytime it encounters
> the flag, then you can process it and add it to the list.
> 

Yeah, but the lines are positional parameters between the --chip
options.
i.e.
 
 --chip gpiochip1 1 2 4  --chip gpiochip3 6 7 8

I thought getopt() bails when it hits positional args?
Or am I missing something?

> >
> > > > The updated tools are intentionally NOT backwardly compatible with the
> > > > previous tools. Using old command lines with the updated tools will
> > > > almost certainly fail, though migrating old command lines is generally as
> > > > simple as adding a '-c' before the chip.
> > > >
> > > > In addition the individual tools are modified as follows:
> > > >
> > > > gpiodetect:
> > > >
> > > > Add the option to select individual chips.
> > > >
> > >
> > > We discussed at some point that gpiodetect should also check if any of
> > > the files it iterates over is a symbolic link to a GPIO device and
> > > print some info accordingly (e.g. foobar [link to /dev/gpiochip2])-
> > > are you thinking about adding this too?
> > >
> >
> > Did we?  My bad then - I clearly forgot and instead filtered the symlinks
> > out so it only reports actual chips (btw the v1 tools report the symlinks
> > by repeating the actual, which is the worst of both worlds).
> >
> 
> I think so. In any case I think this would be useful.
> 

gpiodetect on a symlink will report for the chip that link points to.
Similarly gpioinfo.
Isn't that sufficient?

i.e. the bare form of gpiodetect and gpioinfo report all the actual
chipts, while the specific form reports whatever the provided path,
resolves to, including following symlinks.

...
> > >
> > > readline is licensed under GPLv3 - this bleeds into gpioset and will
> > > make a lot of companies bounce off of it. Any chance you could use
> > > libedit instead? It's supposed to be a drop-in replacement for
> > > readline but I have never used it first hand.
> > >
> >
> > Hey, you mentioned readline before I implemented it.
> > Though I don't recall "avoid" being mentioned :-(.
> >
> 
> No, you're right, I mentioned it but then realized it's GPLv3.
> 
> > Ok, I'll take a look.  Hopefully it is just a drop-in.
> >
> > Out of curiosity which aspect of GPLv3 is the problem, for a tool
> > which is publicly available and they aren't going to modify?
> > Just having a GPLv3 library on their system?
> >
> 
> You'd be surprised how allergic companies are to GPLv3. Anything
> "tainted" with GPLv3 is often banned.
> 

Ok, news to me.  I might quarantine them to prevent building against
them, but banning them outright is a bit extreme.
OTOH I've worked at places that banned Linux (or tried to anyway).

Anyway, I'll look into the alternatives.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2022-07-07  9:18 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-06-27 13:44 [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson
2022-06-27 13:44 ` [libgpiod v2][PATCH 1/4] " Kent Gibson
2022-07-06 20:20   ` Bartosz Golaszewski
2022-07-07  2:24     ` Kent Gibson
2022-07-07  9:01       ` Bartosz Golaszewski
2022-07-07  9:18         ` Kent Gibson
2022-06-27 13:44 ` [libgpiod v2][PATCH 2/4] tools: tests for " Kent Gibson
2022-07-01  0:42   ` Kent Gibson
2022-06-27 13:44 ` [libgpiod v2][PATCH 3/4] tools: add gpiowatch Kent Gibson
2022-07-06 20:46   ` Bartosz Golaszewski
2022-07-07  2:27     ` Kent Gibson
2022-07-07  8:41       ` Bartosz Golaszewski
2022-07-07  8:55         ` Kent Gibson
2022-06-27 13:44 ` [libgpiod v2][PATCH 4/4] tools: gpiowatch tests Kent Gibson
2022-06-28  5:26 ` [libgpiod v2][PATCH 0/4] tools: line name focussed rework Kent Gibson

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.