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..cf342c8 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; @@ -299,11 +303,6 @@ void *tty_stdin_input_thread(void *arg) 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) @@ -321,6 +320,14 @@ void *tty_stdin_input_thread(void *arg) // Process quit and flush key command for (int i = 0; i 0) + while (byte_count) { bytes_written = write(pipefd[1], input_buffer, byte_count); if (bytes_written < 0) @@ -472,6 +479,32 @@ static void toggle_line(const char *line_name, int mask, enum line_mode_t line_m } } +#define MAX_LINE 100 +static char line[MAX_LINE]; + +static int tio_readln(void) +{ + char *p = line; + + /* Read line, accept BS and DEL as rubout characters */ + for (p = line ; p < &line[MAX_LINE-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; @@ -562,7 +595,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 +756,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)); + fprintf(stdout, "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..ae8cbb7 --- /dev/null +++ b/src/xymodem.c @@ -0,0 +1,219 @@ +/* + * Minimalistic implementation of the xmodem-1k and ymodem sender protocol. + * https://en.wikipedia.org/wiki/XMODEM + * https://en.wikipedia.org/wiki/YMODEM + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "misc.h" + +#define X_STX 0x02 +#define X_ACK 0x06 +#define X_NAK 0x15 +#define X_CAN 0x18 + +#define EOT "\004" + +#define min(a, b) ((a) < (b) ? (a) : (b)) + +struct xpacket { + uint8_t start; + uint8_t seq; + uint8_t nseq; + uint8_t data[1024]; + uint16_t crc; +} __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 uint16_t swap16(uint16_t in) +{ + return (in >> 8) | ((in & 0xff) << 8); +} + +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; + + /* 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 == X_CAN) return -1; + usleep(50000); + continue; + } + perror("Read sync from serial failed"); + return -1; + } + } + + /* Always work with 1K packets */ + packet.seq = seq; + packet.start = X_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); + packet.crc = swap16(crc16(packet.data, sizeof(packet.data))); + packet.nseq = 0xff - packet.seq; + + /* Send packet */ + from = (char *) &packet; + sz = sizeof(packet); + while (sz) { + if (key_hit) + return -1; + if ((rc = write(sio, from, sz)) < 0 ) { + if (errno == EWOULDBLOCK) { + usleep(1000); + continue; + } + perror("Write packet to serial failed"); + return -1; + } + from += rc; + sz -= rc; + } + + /* Read receiver response, timeout 1 s */ + resp = X_ACK; + for(int n=0; n < 20; n++) { + if (key_hit) + return -1; + if (read(sio, &resp, 1) < 0) { + if (errno == EWOULDBLOCK) { + usleep(50000); + continue; + } + perror("Read ack/nak from serial failed"); + return -1; + } + break; + } + + /* Update "progress bar" */ + switch (resp) { + case X_NAK: status = 'N'; break; + case X_ACK: status = '.'; break; + case 'C': status = 'C'; break; + case X_CAN: status = '!'; return -1; + default: status = '?'; + } + write(STDOUT_FILENO, &status, 1); + + /* Move to next block after ACK */ + if (resp == X_ACK) { + packet.seq++; + len -= z; + buf += z; + } + } + + /* Send EOT at 1 Hz until ACK or CAN received */ + while (seq) { + if (key_hit) + return -1; + if (write(sio, EOT, 1) < 0) { + perror("Write EOT to serial failed"); + return -1; + } + 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 -1; + } + if (resp == X_ACK || resp == X_CAN) { + write(STDOUT_FILENO, "\r\n", 2); + return (resp == X_ACK) ? 0 : -1; + } + } + 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 -1; + } + 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 -1; + } + + /* 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; +}