diff --git a/README.md b/README.md index 7f57ab7..dc70429 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ ctrl-t ? to list the available key commands. [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 ``` diff --git a/man/tio.1.in b/man/tio.1.in index 6b75422..b9cdf1a 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -323,6 +323,10 @@ Toggle line timestamp mode 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 diff --git a/src/meson.build b/src/meson.build index cec9429..73af4c7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -18,6 +18,7 @@ tio_sources = [ 'rs485.c', 'timestamp.c', 'alert.c' + 'xymodem.c' ] tio_dep = [ diff --git a/src/misc.h b/src/misc.h index 6fe5368..d9a8e2b 100644 --- a/src/misc.h +++ b/src/misc.h @@ -29,3 +29,6 @@ long string_to_long(char *string); 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); diff --git a/src/tty.c b/src/tty.c index d6a38ed..0f57d75 100644 --- a/src/tty.c +++ b/src/tty.c @@ -105,6 +105,8 @@ #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 @@ -133,6 +135,8 @@ bool map_i_nl_cr = false; 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; @@ -321,6 +325,14 @@ void *tty_stdin_input_thread(void *arg) // Process quit and flush key command for (int i = 0; i 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; @@ -562,7 +600,9 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) 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: @@ -721,6 +761,17 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) 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; diff --git a/src/xymodem.c b/src/xymodem.c new file mode 100644 index 0000000..1dbeddc --- /dev/null +++ b/src/xymodem.c @@ -0,0 +1,223 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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[128], *p; + p = stpcpy(hdr, filename) + 1; + p += sprintf(p, "%ld %lo %o", len, stat.st_mtime, stat.st_mode); + + rc = -1; + 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; +}