diff --git a/TODO b/TODO index c0496e37..14d80254 100644 --- a/TODO +++ b/TODO @@ -29,13 +29,3 @@ $ tio --socket ws:1234 Use libwesockets to implement feature. - - * RS-485 support - - Many modern RS-485 devices such as the ones from FTDI already operate in - RS-485 mode by default and will work with tio out of the box. However, there - are still some RS-232/485 devices which need to be switched from e.g. RS-232 to - RS-485 mode to operate accordingly on the physical level. - - To enable RS-485 mode on such serial devices the idea is to add a --rs-485 - option. diff --git a/example/tiorc b/example/tiorc index a1340e30..e7f0f188 100644 --- a/example/tiorc +++ b/example/tiorc @@ -44,3 +44,9 @@ color = 11 pattern = usb([0-9]*) tty = /dev/ttyUSB%s color = 12 + +[rs-485-device] +tty = /dev/ttyUSB0 +rs-485 = enable +rs-485-config = RTS_ON_SEND=1,RTS_AFTER_SEND=1,RTS_DELAY_BEFORE_SEND=60,RTS_DELAY_AFTER_SEND=80,RX_DURING_TX +color = 13 diff --git a/man/tio.1.in b/man/tio.1.in index 9d81c2dc..36b0b1c4 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -229,6 +229,33 @@ response mode to make it easy to parse the response. Set timeout [ms] of line response (default: 100). +.TP +.BR " \-\-rs\-485" + +Enable RS-485 mode. + +.TP +.BR " \-\-rs\-485\-config " \fI + +Set the RS-485 configuration using the following key or key value pair format in +the configuration field: + +.RS +.TP 30n +.IP \fBRTS_ON_SEND=value +Set logical level (0 or 1) for RTS pin when sending +.IP \fBRTS_AFTER_SEND=value +Set logical level (0 or 1) for RTS pin after sending +.IP \fBRTS_DELAY_BEFORE_SEND=value +Set RTS delay (ms) before sending +.IP \fBRTS_DELAY_AFTER_SEND=value +Set RTS delay (ms) after sending +.IP \fBRX_DURING_TX +Receive data even while sending data +.P +If defining more than one key or key value pair, they must be comma separated. +.RE + .TP .BR \-v ", " \-\-version @@ -358,6 +385,10 @@ Set prefix ctrl key (a..z, default: t) Enable wait for line response .IP "\fBresponse-timeout" Set line response timeout +.IP "\fBrs-485" +Enable RS-485 mode +.IP "\fBrs-485-config" +Set RS-485 configuration .SH "CONFIGURATION FILE EXAMPLES" @@ -507,6 +538,11 @@ Likewise, to pipe data from file to the serial device: $ cat data.bin | tio /dev/serial/by\-id/usb\-FTDI_TTL232R-3V3_FTGQVXBL\-if00\-port0 +.TP +Enable RS-485 mode: + +$ tio --rs-485 --rs-485-config=RTS_ON_SEND=1,RX_DURING_TX /dev/ttyUSB0 + .SH "WEBSITE" .PP Visit https://tio.github.io diff --git a/meson.build b/meson.build index eba9fdc7..d1eedcc6 100644 --- a/meson.build +++ b/meson.build @@ -71,5 +71,12 @@ foreach rate : test_baudrates endif endforeach +# Test for RS-485 support on Linux +if host_machine.system() == 'linux' + if compiler.check_header('linux/serial.h') + enable_rs485 = compiler.has_header_symbol('sys/ioctl.h', 'TIOCSRS485') + endif +endif + subdir('src') subdir('man') diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index 67b936cf..cea90f15 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -31,6 +31,8 @@ _tio() -S --socket \ -r --response-wait \ --response-timeout \ + --rs-485 \ + --rs-485-config \ -x --hexadecimal \ -v --version \ -h --help" @@ -122,6 +124,14 @@ _tio() COMPREPLY=( $(compgen -W "1 10 100" -- ${cur}) ) return 0 ;; + --rs-485) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + --rs-485-config) + COMPREPLY=( $(compgen -W "RTS_ON_SEND RTS_AFTER_SEND RTS_DELAY_BEFORE_SEND RTS_DELAY_AFTER_SEND RX_DURING_TX" -- ${cur}) ) + return 0 + ;; -x | --hexadecimal) COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 diff --git a/src/configfile.c b/src/configfile.c index 1a31ae37..e081a702 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -42,6 +42,7 @@ #include "options.h" #include "error.h" #include "print.h" +#include "rs485.h" static struct config_t *c; @@ -264,6 +265,22 @@ static int data_handler(void *user, const char *section, const char *name, { option.response_timeout = atoi(value); } + else if (!strcmp(name, "rs-485")) + { + if (!strcmp(value, "enable")) + { + option.rs485 = true; + } + else if (!strcmp(value, "disable")) + { + option.rs485 = false; + } + } + else if (!strcmp(name, "rs-485-config")) + { + rs485_parse_config(value); + } + } return 0; } diff --git a/src/meson.build b/src/meson.build index 464f4f3e..061d0706 100644 --- a/src/meson.build +++ b/src/meson.build @@ -14,7 +14,8 @@ tio_sources = [ 'configfile.c', 'signals.c', 'socket.c', - 'setspeed.c' + 'setspeed.c', + 'rs485.c' ] tio_dep = dependency('inih', required: true, @@ -31,6 +32,10 @@ if enable_iossiospeed tio_c_args += '-DHAVE_IOSSIOSPEED' endif +if enable_rs485 + tio_c_args += '-DHAVE_RS485' +endif + executable('tio', tio_sources, c_args: tio_c_args, diff --git a/src/options.c b/src/options.c index e11af127..b6099ee6 100644 --- a/src/options.c +++ b/src/options.c @@ -35,6 +35,7 @@ #include "misc.h" #include "print.h" #include "tty.h" +#include "rs485.h" enum opt_t { @@ -44,6 +45,8 @@ enum opt_t OPT_LOG_STRIP, OPT_LINE_PULSE_DURATION, OPT_RESPONSE_TIMEOUT, + OPT_RS485, + OPT_RS485_CONFIG, }; /* Default options */ @@ -78,6 +81,10 @@ struct option_t option = .response_wait = false, .response_timeout = 100, .mute = false, + .rs485 = false, + .rs485_config_flags = 0, + .rs485_delay_rts_before_send = -1, + .rs485_delay_rts_after_send = -1, }; void print_help(char *argv[]) @@ -109,6 +116,8 @@ void print_help(char *argv[]) printf(" -x, --hexadecimal Enable hexadecimal mode\n"); printf(" -r, --response-wait Wait for line response then quit\n"); printf(" --response-timeout Response timeout (default: 100)\n"); + printf(" --rs-485 Enable RS-485 mode\n"); + printf(" --rs-485-config Set RS-485 configuration\n"); printf(" -v, --version Display version\n"); printf(" -h, --help Display help\n"); printf("\n"); @@ -295,6 +304,8 @@ void options_parse(int argc, char *argv[]) {"hexadecimal", no_argument, 0, 'x' }, {"response-wait", no_argument, 0, 'r' }, {"response-timeout", required_argument, 0, OPT_RESPONSE_TIMEOUT }, + {"rs-485", no_argument, 0, OPT_RS485 }, + {"rs-485-config", required_argument, 0, OPT_RS485_CONFIG }, {"version", no_argument, 0, 'v' }, {"help", no_argument, 0, 'h' }, {0, 0, 0, 0 } @@ -437,6 +448,14 @@ void options_parse(int argc, char *argv[]) option.response_timeout = string_to_long(optarg); break; + case OPT_RS485: + option.rs485 = true; + break; + + case OPT_RS485_CONFIG: + rs485_parse_config(optarg); + break; + case 'v': printf("tio v%s\n", VERSION); exit(EXIT_SUCCESS); diff --git a/src/options.h b/src/options.h index 76500476..b5e1bca8 100644 --- a/src/options.h +++ b/src/options.h @@ -21,6 +21,7 @@ #pragma once +#include #include #include #include @@ -70,6 +71,10 @@ struct option_t bool response_wait; int response_timeout; bool mute; + bool rs485; + uint32_t rs485_config_flags; + int32_t rs485_delay_rts_before_send; + int32_t rs485_delay_rts_after_send; }; extern struct option_t option; diff --git a/src/print.h b/src/print.h index c2aba7cd..84eef2b2 100644 --- a/src/print.h +++ b/src/print.h @@ -21,6 +21,7 @@ #pragma once +#include #include #include "misc.h" #include "error.h" diff --git a/src/rs485.c b/src/rs485.c new file mode 100644 index 00000000..e364b132 --- /dev/null +++ b/src/rs485.c @@ -0,0 +1,200 @@ +/* + * tio - a simple serial device I/O tool + * + * 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. + */ + +#include +#include +#include +#include +#include +#include "options.h" +#include "print.h" +#include "error.h" + +#ifdef HAVE_RS485 + +#include + +static struct serial_rs485 rs485_config_saved; +static struct serial_rs485 rs485_config; +static bool rs485_config_written = false; + +void rs485_parse_config(const char *arg) +{ + bool token_found = true; + char *token = NULL; + char *buffer = strdup(arg); + + while (token_found == true) + { + if (token == NULL) + { + token = strtok(buffer,","); + } + else + { + token = strtok(NULL, ","); + } + + if (token != NULL) + { + char keyname[31]; + unsigned int value; + sscanf(token, "%30[^=]=%d", keyname, &value); + + if (!strcmp(keyname, "RTS_ON_SEND")) + { + if (value) + { + + /* Set logical level for RTS pin equal to 1 when sending */ + option.rs485_config_flags |= SER_RS485_RTS_ON_SEND; + } + else + { + /* Set logical level for RTS pin equal to 0 when sending */ + option.rs485_config_flags &= ~(SER_RS485_RTS_ON_SEND); + } + } + else if (!strcmp(keyname, "RTS_AFTER_SEND")) + { + if (value) + { + /* Set logical level for RTS pin equal to 1 after sending */ + option.rs485_config_flags |= SER_RS485_RTS_AFTER_SEND; + } + else + { + /* Set logical level for RTS pin equal to 0 after sending */ + option.rs485_config_flags &= ~(SER_RS485_RTS_AFTER_SEND); + } + } + else if (!strcmp(keyname, "RTS_DELAY_BEFORE_SEND")) + { + /* Set RTS delay before send */ + option.rs485_delay_rts_before_send = value; + } + else if (!strcmp(keyname, "RTS_DELAY_AFTER_SEND")) + { + /* Set RTS delay after send */ + option.rs485_delay_rts_after_send = value; + } + else if (!strcmp(keyname, "RX_DURING_TX")) + { + /* Receive data even while sending data */ + option.rs485_config_flags |= SER_RS485_RX_DURING_TX; + } + } + else + { + token_found = false; + } + } + free(buffer); +} + +void rs485_print_config(void) +{ + tio_printf(" RS-485 Configuration:"); + tio_printf(" RTS_ON_SEND: %s", (rs485_config.flags & SER_RS485_RTS_ON_SEND) ? "high" : "low"); + tio_printf(" RTS_AFTER_SEND: %s", (rs485_config.flags & SER_RS485_RTS_AFTER_SEND) ? "high" : "low"); + tio_printf(" RTS_DELAY_BEFORE_SEND = %d", rs485_config.delay_rts_before_send); + tio_printf(" RTS_DELAY_AFTER_SEND = %d", rs485_config.delay_rts_after_send); + tio_printf(" RX_DURING_TX: %s", (rs485_config.flags & SER_RS485_RX_DURING_TX) ? "enabled" : "disabled"); +} + +int rs485_mode_enable(int fd) +{ + /* Save existing RS-485 configuration */ + ioctl (fd, TIOCGRS485, &rs485_config_saved); + + /* Prepare new RS-485 configuration */ + rs485_config.flags = SER_RS485_ENABLED; + rs485_config.flags |= option.rs485_config_flags; + + if (option.rs485_delay_rts_before_send > 0) + { + rs485_config.delay_rts_before_send = option.rs485_delay_rts_before_send; + } + else + { + rs485_config.delay_rts_before_send = rs485_config_saved.delay_rts_before_send; + } + + if (option.rs485_delay_rts_after_send > 0) + { + rs485_config.delay_rts_after_send = option.rs485_delay_rts_after_send; + } + else + { + rs485_config.delay_rts_after_send = rs485_config_saved.delay_rts_after_send; + } + + /* Write new RS-485 configuration */ + if (ioctl(fd, TIOCSRS485, &rs485_config) < 0) + { + tio_warning_printf("RS-485 mode is not supported by your device (%s)", strerror(errno)); + return -1; + } + + rs485_config_written = true; + + return 0; +} + +void rs485_mode_restore(int fd) +{ + if (rs485_config_written) + { + /* Write saved RS-485 configuration */ + if (ioctl(fd, TIOCSRS485, &rs485_config_saved) < 0) + { + tio_warning_printf("TIOCGRS485 ioctl failed (%s)", strerror(errno)); + } + } +} + +#else + +void rs485_parse_config(const char *arg) +{ + UNUSED(arg); + return; +} + +void rs485_print_config(void) +{ + return; +} + +int rs485_mode_enable(int fd) +{ + UNUSED(fd); + tio_error_printf("RS485 mode is not supported on your system"); + exit(EXIT_FAILURE); +} + +void rs485_mode_restore(int fd) +{ + UNUSED(fd); + return; +} + +#endif diff --git a/src/rs485.h b/src/rs485.h new file mode 100644 index 00000000..b7a48828 --- /dev/null +++ b/src/rs485.h @@ -0,0 +1,27 @@ +/* + * tio - a simple serial device I/O tool + * + * 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. + */ + +#pragma once + +void rs485_parse_config(const char *arg); +int rs485_mode_enable(int fd); +void rs485_mode_restore(int fd); +void rs485_print_config(void); diff --git a/src/tty.c b/src/tty.c index a1e42338..8064ac19 100644 --- a/src/tty.c +++ b/src/tty.c @@ -50,6 +50,7 @@ #include "error.h" #include "socket.h" #include "setspeed.h" +#include "rs485.h" #ifdef __APPLE__ #define PATH_SERIAL_DEVICES "/dev/" @@ -456,6 +457,10 @@ void handle_command_sequence(char input_char, char previous_char, char *output_c tio_printf("Configuration:"); config_file_print(); options_print(); + if (option.rs485) + { + rs485_print_config(); + } break; case KEY_E: @@ -947,6 +952,12 @@ void tty_restore(void) { tcsetattr(fd, TCSANOW, &tio_old); + if (option.rs485) + { + /* Restore original RS-485 mode */ + rs485_mode_restore(fd); + } + if (connected) { tty_disconnect(); @@ -1081,6 +1092,12 @@ int tty_connect(void) } #endif + /* Manage RS-485 mode */ + if (option.rs485) + { + rs485_mode_enable(fd); + } + /* Make sure we restore tty settings on exit */ if (first) {