mirror of
https://github.com/tio/tio.git
synced 2026-05-01 14:57:59 +02:00
Add xmodem and ymodem file send support (#208)
* Add xmodem and ymodem file send support --------- Co-authored-by: pnr <pnr@home25.nl>
This commit is contained in:
parent
812dee8e54
commit
e6ffbd9058
6 changed files with 285 additions and 1 deletions
|
|
@ -18,6 +18,7 @@ tio_sources = [
|
|||
'rs485.c',
|
||||
'timestamp.c',
|
||||
'alert.c'
|
||||
'xymodem.c'
|
||||
]
|
||||
|
||||
tio_dep = [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
53
src/tty.c
53
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<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)
|
||||
|
|
@ -472,6 +484,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 +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;
|
||||
|
|
|
|||
223
src/xymodem.c
Normal file
223
src/xymodem.c
Normal file
|
|
@ -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 <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[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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue