mirror of
https://github.com/tio/tio.git
synced 2026-05-01 14:57:59 +02:00
Add a simple line response feature to make it possible to send e.g. a command string to your serial device and easily receive and parse a line response. This is a convenience feature for simple request/response interaction based on lines. For more advanced interaction the socket feature should be used instead. The line response feature is detailed via the following options: -r, --response-wait Wait for line response then quit. A line is considered any string ending with either CR or NL character. If no line is received tio will quit after response timeout. Any tio text is automatically muted when piping a string to tio while in response mode to make it easy to parse the response. --response-timeout <ms> Set timeout [ms] of line response (default: 100). Example: Sending a string (SCPI command) to a test instrument (Korad PSU) and print line response: $ echo "*IDN?" | tio /dev/ttyACM0 --response-wait KORAD KD3305P V4.2 SN:32477045
439 lines
11 KiB
C
439 lines
11 KiB
C
/*
|
|
* tio - a simple serial terminal I/O tool
|
|
*
|
|
* Copyright (c) 2020-2022 Liam Beguin
|
|
* Copyright (c) 2022 Martin Lund
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "config.h"
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <stdbool.h>
|
|
#include <errno.h>
|
|
#include <getopt.h>
|
|
#include <termios.h>
|
|
#include <limits.h>
|
|
#include <unistd.h>
|
|
#include <regex.h>
|
|
#include <ini.h>
|
|
#include "options.h"
|
|
#include "configfile.h"
|
|
#include "misc.h"
|
|
#include "options.h"
|
|
#include "error.h"
|
|
#include "print.h"
|
|
|
|
static struct config_t *c;
|
|
|
|
static int get_match(const char *input, const char *pattern, char **match)
|
|
{
|
|
int ret;
|
|
int len = 0;
|
|
regex_t re;
|
|
regmatch_t m[2];
|
|
char err[128];
|
|
|
|
/* compile a regex with the pattern */
|
|
ret = regcomp(&re, pattern, REG_EXTENDED);
|
|
if (ret)
|
|
{
|
|
regerror(ret, &re, err, sizeof(err));
|
|
tio_error_printf("Regex failure: %s", err);
|
|
return ret;
|
|
}
|
|
|
|
/* try to match on input */
|
|
ret = regexec(&re, input, 2, m, 0);
|
|
if (!ret)
|
|
{
|
|
len = m[1].rm_eo - m[1].rm_so;
|
|
}
|
|
|
|
regfree(&re);
|
|
|
|
if (len)
|
|
{
|
|
asprintf(match, "%s", &input[m[1].rm_so]);
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
/**
|
|
* data_handler() - walk config file to load parameters matching user input
|
|
*
|
|
* INIH handler used to get all parameters from a given section
|
|
*/
|
|
static int data_handler(void *user, const char *section, const char *name,
|
|
const char *value)
|
|
{
|
|
UNUSED(user);
|
|
|
|
// If section matches current section being parsed
|
|
if (!strcmp(section, c->section_name))
|
|
{
|
|
// Set configuration parameter if found
|
|
if (!strcmp(name, "tty"))
|
|
{
|
|
asprintf(&c->tty, value, c->match);
|
|
option.tty_device = c->tty;
|
|
}
|
|
else if (!strcmp(name, "baudrate"))
|
|
{
|
|
option.baudrate = string_to_long((char *)value);
|
|
}
|
|
else if (!strcmp(name, "databits"))
|
|
{
|
|
option.databits = atoi(value);
|
|
}
|
|
else if (!strcmp(name, "flow"))
|
|
{
|
|
asprintf(&c->flow, "%s", value);
|
|
option.flow = c->flow;
|
|
}
|
|
else if (!strcmp(name, "stopbits"))
|
|
{
|
|
option.stopbits = atoi(value);
|
|
}
|
|
else if (!strcmp(name, "parity"))
|
|
{
|
|
asprintf(&c->parity, "%s", value);
|
|
option.parity = c->parity;
|
|
}
|
|
else if (!strcmp(name, "output-delay"))
|
|
{
|
|
option.output_delay = atoi(value);
|
|
}
|
|
else if (!strcmp(name, "output-line-delay"))
|
|
{
|
|
option.output_line_delay = atoi(value);
|
|
}
|
|
else if (!strcmp(name, "line-pulse-duration"))
|
|
{
|
|
line_pulse_duration_option_parse(value);
|
|
}
|
|
else if (!strcmp(name, "no-autoconnect"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.no_autoconnect = true;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.no_autoconnect = false;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "log"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.log = true;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.log = false;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "log-file"))
|
|
{
|
|
asprintf(&c->log_filename, "%s", value);
|
|
option.log_filename = c->log_filename;
|
|
}
|
|
else if (!strcmp(name, "log-strip"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.log_strip = true;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.log_strip = false;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "local-echo"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.local_echo = true;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.local_echo = false;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "hexadecimal"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.hex_mode = true;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.hex_mode = false;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "timestamp"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.timestamp = TIMESTAMP_24HOUR;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.timestamp = TIMESTAMP_NONE;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "timestamp-format"))
|
|
{
|
|
option.timestamp = timestamp_option_parse(value);
|
|
}
|
|
else if (!strcmp(name, "map"))
|
|
{
|
|
asprintf(&c->map, "%s", value);
|
|
option.map = c->map;
|
|
}
|
|
else if (!strcmp(name, "color"))
|
|
{
|
|
if (!strcmp(value, "list"))
|
|
{
|
|
// Ignore
|
|
return 0;
|
|
}
|
|
else if (!strcmp(value, "none"))
|
|
{
|
|
option.color = -1; // No color
|
|
return 0;
|
|
}
|
|
else if (!strcmp(value, "bold"))
|
|
{
|
|
option.color = 256; // Bold
|
|
return 0;
|
|
}
|
|
|
|
option.color = atoi(value);
|
|
if ((option.color < 0) || (option.color > 255))
|
|
{
|
|
option.color = -1; // No color
|
|
}
|
|
}
|
|
else if (!strcmp(name, "socket"))
|
|
{
|
|
asprintf(&c->socket, "%s", value);
|
|
option.socket = c->socket;
|
|
}
|
|
else if (!strcmp(name, "prefix-ctrl-key"))
|
|
{
|
|
if (ctrl_key_code(value[0]) > 0)
|
|
{
|
|
option.prefix_code = ctrl_key_code(value[0]);
|
|
option.prefix_key = value[0];
|
|
}
|
|
}
|
|
else if (!strcmp(name, "response-wait"))
|
|
{
|
|
if (!strcmp(value, "enable"))
|
|
{
|
|
option.response_wait = true;
|
|
}
|
|
else if (!strcmp(value, "disable"))
|
|
{
|
|
option.response_wait = false;
|
|
}
|
|
}
|
|
else if (!strcmp(name, "response-timeout"))
|
|
{
|
|
option.response_timeout = atoi(value);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* section_pattern_search_handler() - walk config file to find section matching user input
|
|
*
|
|
* INIH handler used to resolve the section matching the user's input.
|
|
* This will look for the pattern element of each section and try to match it
|
|
* with the user input.
|
|
*/
|
|
static int section_pattern_search_handler(void *user, const char *section, const char *varname,
|
|
const char *varval)
|
|
{
|
|
UNUSED(user);
|
|
|
|
if (strcmp(varname, "pattern"))
|
|
return 0;
|
|
|
|
if (!strcmp(varval, c->user))
|
|
{
|
|
/* pattern matches as plain text */
|
|
asprintf(&c->section_name, "%s", section);
|
|
}
|
|
else if (get_match(c->user, varval, &c->match) > 0)
|
|
{
|
|
/* pattern matches as regex */
|
|
asprintf(&c->section_name, "%s", section);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* section_pattern_search_handler() - walk config file to find section matching user input
|
|
*
|
|
* INIH handler used to resolve the section matching the user's input.
|
|
* This will try to match the user input against a section with the name of the user input.
|
|
*/
|
|
static int section_name_search_handler(void *user, const char *section, const char *varname,
|
|
const char *varval)
|
|
{
|
|
UNUSED(user);
|
|
UNUSED(varname);
|
|
UNUSED(varval);
|
|
|
|
if (!strcmp(section, c->user))
|
|
{
|
|
/* section name matches as plain text */
|
|
asprintf(&c->section_name, "%s", section);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_config_file(void)
|
|
{
|
|
asprintf(&c->path, "%s/tio/tiorc", getenv("XDG_CONFIG_HOME"));
|
|
if (!access(c->path, F_OK))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
free(c->path);
|
|
|
|
asprintf(&c->path, "%s/.config/tio/tiorc", getenv("HOME"));
|
|
if (!access(c->path, F_OK))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
free(c->path);
|
|
|
|
asprintf(&c->path, "%s/.tiorc", getenv("HOME"));
|
|
if (!access(c->path, F_OK))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
free(c->path);
|
|
|
|
c->path = NULL;
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
void config_file_parse(void)
|
|
{
|
|
int ret;
|
|
|
|
c = malloc(sizeof(struct config_t));
|
|
if (!c)
|
|
{
|
|
tio_error_printf("Insufficient memory allocation");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
memset(c, 0, sizeof(struct config_t));
|
|
|
|
// Find config file
|
|
if (resolve_config_file() != 0)
|
|
{
|
|
// None found - stop parsing
|
|
return;
|
|
}
|
|
|
|
// Set user input which may be tty device or sub config
|
|
c->user = option.tty_device;
|
|
|
|
if (!c->user)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Parse default (unnamed) settings
|
|
asprintf(&c->section_name, "%s", "");
|
|
ret = ini_parse(c->path, data_handler, NULL);
|
|
if (ret < 0)
|
|
{
|
|
tio_error_printf("Unable to parse configuration file (%d)", ret);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
free(c->section_name);
|
|
c->section_name = NULL;
|
|
|
|
// Find matching section
|
|
ret = ini_parse(c->path, section_pattern_search_handler, NULL);
|
|
if (!c->section_name)
|
|
{
|
|
ret = ini_parse(c->path, section_name_search_handler, NULL);
|
|
if (!c->section_name)
|
|
{
|
|
tio_debug_printf("Unable to match user input to configuration section (%d)", ret);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Parse settings of found section (sub config)
|
|
ret = ini_parse(c->path, data_handler, NULL);
|
|
if (ret < 0)
|
|
{
|
|
tio_error_printf("Unable to parse configuration file (%d)", ret);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
atexit(&config_exit);
|
|
}
|
|
|
|
void config_exit(void)
|
|
{
|
|
free(c->tty);
|
|
free(c->flow);
|
|
free(c->parity);
|
|
free(c->log_filename);
|
|
free(c->map);
|
|
|
|
free(c->match);
|
|
free(c->section_name);
|
|
free(c->path);
|
|
|
|
free(c);
|
|
}
|
|
|
|
void config_file_print(void)
|
|
{
|
|
if (c->path != NULL)
|
|
{
|
|
tio_printf(" Path: %s", c->path);
|
|
if (c->section_name != NULL)
|
|
{
|
|
tio_printf(" Active sub-configuration: %s", c->section_name);
|
|
}
|
|
}
|
|
}
|