Bill Hass <billhass@umich.edu>
Peter van Dijk <peter@7bits.nl>
Braden Young <braden@somewearlabs.com>
+Wes Koerber <wkoerber@acsd4u.com>
+HiFiPhile <admin@hifiphile.com>
+Paul Ruizendaal <pnr@planet.nl>
Thanks to everyone who has contributed to this project.
-=== tio v2.6 ===
+=== tio v2.7 ===
+
+
+
+Changes since tio v2.6:
+
+Paul Ruizendaal:
+
+ * Add xmodem and ymodem file send support
+
+HiFiPhile:
+
+ * tty_stdin_input_thread(): write to pipe only if byte_count > 0.
+
+ * Ignore EINTR error.
+
+ * CYGWIN: Add support for "COM*" naming.
+
+Wes Koerber:
+
+ * chore: reorder log-strip and log-append
+
+ reorder to maintain consistency with documentation
+
+ * chore: update readme, bash completion, man page
+
+ * fix: support --log-append in cli options
[![](https://img.shields.io/circleci/build/github/tio/tio)](https://circleci.com/github/tio/tio/tree/master)
[![](https://img.shields.io/github/v/release/tio/tio?sort=semver)](https://github.com/tio/tio/releases)
[![](https://img.shields.io/repology/repositories/tio)](https://repology.org/project/tio/versions)
-[![](https://img.shields.io/tokei/lines/github/tio/tio)](https://github.com/tio/tio)
+<!-- [![](https://img.shields.io/tokei/lines/github/tio/tio)](https://github.com/tio/tio) -->
## 1. Introduction
* Sensible defaults (115200 8n1)
* Support for non-standard baud rates
* Support for RS-485 mode
+ * X-modem (1K) and Y-modem file upload
* Support for mark and space parity
* List available serial devices by ID
* Show RX/TX statistics
-L, --list-devices List available serial devices
-l, --log Enable log to file
--log-file <filename> Set log filename
+ --log-append Append to log file
--log-strip Strip control characters and escape sequences
-m, --map <flags> Map characters
-c, --color 0..255|bold|none|list Colorize tio text (default: bold)
[20:19:12.041] ctrl-t t Toggle line timestamp mode
[20:19:12.041] ctrl-t U Toggle conversion to uppercase
[20:19:12.041] ctrl-t v Show version
+[20:19:12.041] ctrl-t x Send file using the XMODEM protocol
+[20:19:12.041] ctrl-t y Send file using the YMODEM protocol
[20:19:12.041] ctrl-t ctrl-t Send ctrl-t character
```
- * Support for interaction using simple autoresponse strings
+* Line mode feature
+
+ Only send line when pressing enter. Maybe even add readline support so one
+ can use all the readline editing feature before sending the line.
+
+* Support for interaction using simple autoresponse strings
Add support for simple autoresponse strings in the configuration file. For
example:
Clear screen
.IP "\fBctrl-t L"
Show line states (DTR, RTS, CTS, DSR, DCD, RI)
+.IP "\fBctrl-t m"
+Toggle MSB to LSB bit order
.IP "\fBctrl-t p"
Pulse serial port line
.IP "\fBctrl-t q"
Toggle conversion to uppercase on output
.IP "\fBctrl-t v"
Show version
+.IP "\fBctrl-t x"
+Send a file using the XMODEM protocol (prompts for file name)
+.IP "\fBctrl-t y"
+Send a file using the YMODEM protocol (prompts for file name)
.IP "\fBctrl-t ctrl-t"
Send ctrl-t character
Enable log to file
.IP "\fBlog-file"
Set log filename
+.IP "\fBlog-append"
+Append to log file
.IP "\fBlog-strip"
Enable strip of control and escape sequences from log
.IP "\fBlocal-echo"
log-file Set log filename
+ log-append Append to log file
+
log-strip Enable strip of control and escape sequences from log
local-echo Enable local echo
project('tio', 'c',
- version : '2.6',
+ version : '2.7',
license : [ 'GPL-2'],
meson_version : '>= 0.53.2',
default_options : [ 'warning_level=2', 'buildtype=release', 'c_std=gnu99' ]
)
# The tag date of the project_version(), update when the version bumps.
-version_date = '2022-12-17'
+version_date = '2023-09-19'
# Test for dynamic baudrate configuration interface
compiler = meson.get_compiler('c')
-e --local-echo \
-l --log \
--log-file \
+ --log-append \
--log-strip \
-m --map \
-t --timestamp \
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
;;
+ --log-append)
+ COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+ return 0
+ ;;
--log-strip)
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
'setspeed.c',
'rs485.c',
'timestamp.c',
- 'alert.c'
+ 'alert.c',
+ 'xymodem.c'
]
tio_dep = [
int ctrl_key_code(unsigned char key);
void alert_connect(void);
void alert_disconnect(void);
+
+extern char key_hit;
+int xymodem_send(int sio, const char *filename, char mode);
OPT_TIMESTAMP_FORMAT,
OPT_LOG_FILE,
OPT_LOG_STRIP,
+ OPT_LOG_APPEND,
OPT_LINE_PULSE_DURATION,
OPT_RESPONSE_TIMEOUT,
OPT_RS485,
{"list-devices", no_argument, 0, 'L' },
{"log", no_argument, 0, 'l' },
{"log-file", required_argument, 0, OPT_LOG_FILE },
+ {"log-append", no_argument, 0, OPT_LOG_APPEND },
{"log-strip", no_argument, 0, OPT_LOG_STRIP },
{"socket", required_argument, 0, 'S' },
{"map", required_argument, 0, 'm' },
option.log_strip = true;
break;
+ case OPT_LOG_APPEND:
+ option.log_append = true;
+ break;
+
case 'S':
option.socket = optarg;
break;
optind = 1; // Reset option index to restart scanning of argv
options_parse(argc, argv);
+#ifdef __CYGWIN__
+ unsigned char portnum;
+ char *tty_win;
+ if ( ((strncmp("COM", tty_device, 3) == 0)
+ || (strncmp("com", tty_device, 3) == 0) )
+ && (sscanf(tty_device + 3, "%hhu", &portnum) == 1)
+ && (portnum > 0) )
+ {
+ asprintf(&tty_win, "/dev/ttyS%hhu", portnum - 1);
+ tty_device = tty_win;
+ }
+#endif
+
/* Restore tty device */
option.tty_device = tty_device;
}
#define CMSPAR 010000000000
#endif
+#define LINE_SIZE_MAX 1000
+
#define KEY_0 0x30
#define KEY_1 0x31
#define KEY_2 0x32
#define KEY_T 0x74
#define KEY_U 0x55
#define KEY_V 0x76
+#define KEY_X 0x78
+#define KEY_Y 0x79
#define KEY_Z 0x7a
enum line_mode_t
bool map_i_cr_nl = false;
bool map_ign_cr = false;
+char key_hit = 0xff;
+
static struct termios tio, tio_old, stdout_new, stdout_old, stdin_new, stdin_old;
static unsigned long rx_total = 0, tx_total = 0;
static bool connected = false;
static pthread_t thread;
static int pipefd[2];
static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER;
+static char line[LINE_SIZE_MAX];
static void optional_local_echo(char c)
{
byte_count = read(STDIN_FILENO, input_buffer, BUFSIZ);
if (byte_count < 0)
{
+ /* No error actually occurred */
+ if (errno == EINTR)
+ {
+ continue;
+ }
tio_warning_printf("Could not read from stdin (%s)", strerror(errno));
}
else if (byte_count == 0)
// Process quit and flush key command
for (int i = 0; i<byte_count; i++)
{
+ // first do key hit check for xmodem abort
+ if (!key_hit) {
+ key_hit = input_buffer[i];
+ byte_count--;
+ memcpy(input_buffer+i, input_buffer+i+1, byte_count-i);
+ continue;
+ }
+
input_char = input_buffer[i];
if (previous_char == option.prefix_code)
}
// Write all bytes read to pipe
- while (byte_count)
+ while (byte_count > 0)
{
bytes_written = write(pipefd[1], input_buffer, byte_count);
if (bytes_written < 0)
}
}
+static int tio_readln(void)
+{
+ char *p = line;
+
+ /* Read line, accept BS and DEL as rubout characters */
+ for (p = line ; p < &line[LINE_SIZE_MAX-1]; )
+ {
+ if (read(pipefd[0], p, 1) > 0)
+ {
+ if (*p == 0x08 || *p == 0x7f)
+ {
+ if (p > line )
+ {
+ write(STDOUT_FILENO, "\b \b", 3);
+ p--;
+ }
+ continue;
+ }
+ write(STDOUT_FILENO, p, 1);
+ if (*p == '\r') break;
+ p++;
+ }
+ }
+ *p = 0;
+ return (p - line);
+}
+
void handle_command_sequence(char input_char, char *output_char, bool *forward)
{
char unused_char;
tio_printf(" ctrl-%c t Toggle line timestamp mode", option.prefix_key);
tio_printf(" ctrl-%c U Toggle conversion to uppercase on output", option.prefix_key);
tio_printf(" ctrl-%c v Show version", option.prefix_key);
- tio_printf(" ctrl-%c ctrl-%c Send ctrl-%c character", option.prefix_key, option.prefix_key, option.prefix_key);
+ tio_printf(" ctrl-%c x Send file via Xmodem-1K", option.prefix_key);
+ tio_printf(" ctrl-%c y Send file via Ymodem", option.prefix_key);
+ tio_printf(" ctrl-%c ctrl-%c Send ctrl-%c character", option.prefix_key, option.prefix_key, option.prefix_key);
break;
case KEY_SHIFT_L:
tio_printf("tio v%s", VERSION);
break;
+ case KEY_X:
+ case KEY_Y:
+ tio_printf("Send file with %cMODEM", toupper(input_char));
+ tio_printf_raw("Enter file name: ");
+ if (tio_readln()) {
+ tio_printf("Sending file '%s' ", line);
+ tio_printf("Press any key to abort transfer");
+ tio_printf("%s", xymodem_send(fd, line, input_char) < 0 ? "Aborted" : "Done");
+ }
+ break;
+
case KEY_Z:
tio_printf_array(random_array);
break;
--- /dev/null
+/*
+ * Minimalistic implementation of the xmodem-1k and ymodem sender protocol.
+ * https://en.wikipedia.org/wiki/XMODEM
+ * https://en.wikipedia.org/wiki/YMODEM
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later OR MIT-0
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <termios.h>
+#include "misc.h"
+
+#define STX 0x02
+#define ACK 0x06
+#define NAK 0x15
+#define CAN 0x18
+#define EOT "\004"
+
+#define OK 0
+#define ERR (-1)
+
+#define min(a, b) ((a) < (b) ? (a) : (b))
+
+struct xpacket {
+ uint8_t type;
+ uint8_t seq;
+ uint8_t nseq;
+ uint8_t data[1024];
+ uint8_t crc_hi;
+ uint8_t crc_lo;
+} __attribute__((packed));
+
+/* See https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks */
+static uint16_t crc16(const uint8_t *data, uint16_t size)
+{
+ uint16_t crc, s;
+
+ for (crc = 0; size > 0; size--) {
+ s = *data++ ^ (crc >> 8);
+ s ^= (s >> 4);
+ crc = (crc << 8) ^ s ^ (s << 5) ^ (s << 12);
+ }
+ return crc;
+}
+
+static int xmodem(int sio, const void *data, size_t len, int seq)
+{
+ struct xpacket packet;
+ const uint8_t *buf = data;
+ char resp = 0;
+ int rc, crc;
+
+ /* Drain pending characters from serial line. Insist on the
+ * last drained character being 'C'.
+ */
+ while(1) {
+ if (key_hit)
+ return -1;
+ if (read(sio, &resp, 1) < 0) {
+ if (errno == EWOULDBLOCK) {
+ if (resp == 'C') break;
+ if (resp == CAN) return ERR;
+ usleep(50000);
+ continue;
+ }
+ perror("Read sync from serial failed");
+ return ERR;
+ }
+ }
+
+ /* Always work with 1K packets */
+ packet.seq = seq;
+ packet.type = STX;
+
+ while (len) {
+ size_t sz, z = 0;
+ char *from, status;
+
+ /* Build next packet, pad with 0 to full seq */
+ z = min(len, sizeof(packet.data));
+ memcpy(packet.data, buf, z);
+ memset(packet.data + z, 0, sizeof(packet.data) - z);
+ crc = crc16(packet.data, sizeof(packet.data));
+ packet.crc_hi = crc >> 8;
+ packet.crc_lo = crc;
+ packet.nseq = 0xff - packet.seq;
+
+ /* Send packet */
+ from = (char *) &packet;
+ sz = sizeof(packet);
+ while (sz) {
+ if (key_hit)
+ return ERR;
+ if ((rc = write(sio, from, sz)) < 0 ) {
+ if (errno == EWOULDBLOCK) {
+ usleep(1000);
+ continue;
+ }
+ perror("Write packet to serial failed");
+ return ERR;
+ }
+ from += rc;
+ sz -= rc;
+ }
+
+ /* 'lrzsz' does not ACK ymodem's fin packet */
+ if (seq == 0 && packet.data[0] == 0) resp = ACK;
+
+ /* Read receiver response, timeout 1 s */
+ for(int n=0; n < 20; n++) {
+ if (key_hit)
+ return ERR;
+ if (read(sio, &resp, 1) < 0) {
+ if (errno == EWOULDBLOCK) {
+ usleep(50000);
+ continue;
+ }
+ perror("Read ack/nak from serial failed");
+ return ERR;
+ }
+ break;
+ }
+
+ /* Update "progress bar" */
+ switch (resp) {
+ case NAK: status = 'N'; break;
+ case ACK: status = '.'; break;
+ case 'C': status = 'C'; break;
+ case CAN: status = '!'; return ERR;
+ default: status = '?';
+ }
+ write(STDOUT_FILENO, &status, 1);
+
+ /* Move to next block after ACK */
+ if (resp == ACK) {
+ packet.seq++;
+ len -= z;
+ buf += z;
+ }
+ }
+
+ /* Send EOT at 1 Hz until ACK or CAN received */
+ while (seq) {
+ if (key_hit)
+ return ERR;
+ if (write(sio, EOT, 1) < 0) {
+ perror("Write EOT to serial failed");
+ return ERR;
+ }
+ write(STDOUT_FILENO, "|", 1);
+ usleep(1000000); /* 1 s timeout*/
+ if (read(sio, &resp, 1) < 0) {
+ if (errno == EWOULDBLOCK) continue;
+ perror("Read from serial failed");
+ return ERR;
+ }
+ if (resp == ACK || resp == CAN) {
+ write(STDOUT_FILENO, "\r\n", 2);
+ return (resp == ACK) ? OK : ERR;
+ }
+ }
+ return 0; /* not reached */
+}
+
+int xymodem_send(int sio, const char *filename, char mode)
+{
+ size_t len;
+ int rc, fd;
+ struct stat stat;
+ const uint8_t *buf;
+
+ /* Open file, map into memory */
+ fd = open(filename, O_RDONLY);
+ if (fd < 0) {
+ perror("Could not open file");
+ return ERR;
+ }
+ fstat(fd, &stat);
+ len = stat.st_size;
+ buf = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (!buf) {
+ close(fd);
+ perror("Could not mmap file");
+ return ERR;
+ }
+
+ /* Do transfer */
+ key_hit = 0;
+ if (mode == 'x') {
+ rc = xmodem(sio, buf, len, 1);
+ }
+ else {
+ /* Ymodem: hdr + file + fin */
+ while(1) {
+ char hdr[1024], *p;
+
+ rc = -1;
+ if (strlen(filename) > 977) break; /* hdr block overrun */
+ p = stpcpy(hdr, filename) + 1;
+ p += sprintf(p, "%ld %lo %o", len, stat.st_mtime, stat.st_mode);
+
+ if (xmodem(sio, hdr, p - hdr, 0) < 0) break; /* hdr with metadata */
+ if (xmodem(sio, buf, len, 1) < 0) break; /* xmodem file */
+ if (xmodem(sio, "", 1, 0) < 0) break; /* empty hdr = fin */
+ rc = 0; break;
+ }
+ }
+ key_hit = 0xff;
+
+ /* Flush serial and release resources */
+ tcflush(sio, TCIOFLUSH);
+ munmap((void *)buf, len);
+ close(fd);
+ return rc;
+}