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@xxxxxxxxx> --- man/Makefile.am | 2 +- tools/.gitignore | 1 + tools/Makefile.am | 4 +- tools/gpiowatch.c | 231 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 236 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 b5f5c2d..31b7cc9 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 $(LIBEDIT_LIBS) -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..43f6a92 --- /dev/null +++ b/tools/gpiowatch.c @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@xxxxxxxxx> + +#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 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 by_name; + int event_clock_mode; + int banner; +}; + +int parse_config(int argc, char **argv, struct config *cfg) +{ + int opti, optc; + static const char *const shortopts = "+c:shv"; + const struct option longopts[] = { + { "banner", no_argument, &cfg->banner, 1 }, + { "by-name", no_argument, &cfg->by_name, 1 }, + { "chip", required_argument, NULL, 'c' }, + { "help", no_argument, NULL, 'h' }, + { "localtime", no_argument, &cfg->event_clock_mode, 2 }, + { "strict", no_argument, NULL, 's' }, + { "utc", no_argument, &cfg->event_clock_mode, 1 }, + { "version", no_argument, NULL, 'v' }, + { GETOPT_NULL_LONGOPT }, + }; + + 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, struct config *cfg) +{ + 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 (cfg->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, cfg->event_clock_mode); + printf(" %s", evname); + + if (cfg->chip_id) + printf(" %s %d", cfg->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, + cfg.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 (cfg.banner) + print_banner(argc, argv); + + for (;;) { + fflush(stdout); + + 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); + } + } + + for (i = 0; i < resolver->num_chips; i++) + gpiod_chip_close(chips[i]); + + free(chips); + free_line_resolver(resolver); + + return EXIT_SUCCESS; +} -- 2.37.0