From 102657af582a069e18cce136e55d1782a639f980 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Thu, 14 Aug 2025 09:03:58 +0900 Subject: [PATCH 01/53] Fix XMODEM reception start failure. --- src/tty.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tty.c b/src/tty.c index efda859..01c2b1d 100644 --- a/src/tty.c +++ b/src/tty.c @@ -756,7 +756,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf("Ready to receiving file '%s' ", line); tio_printf("Press any key to abort transfer"); - ret = xymodem_send(device_fd, line, XMODEM_CRC); + ret = xymodem_receive(device_fd, line, XMODEM_CRC); tio_printf("%s", ret < 0 ? "Aborted" : "Done"); } break; From 4aa36e6e9171b191e0c4316e2f22d98c3f174763 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Thu, 14 Aug 2025 13:50:58 +0900 Subject: [PATCH 02/53] Adjust code style to comply with clang-format rules --- .clang-format | 4 + src/main.c | 5 +- src/misc.c | 2 + src/options.c | 12 +- src/readline.c | 2 +- src/script.c | 17 ++- src/tty.c | 36 +++-- src/xymodem.c | 362 +++++++++++++++++++++++++++++++------------------ src/xymodem.h | 4 +- 9 files changed, 285 insertions(+), 159 deletions(-) diff --git a/.clang-format b/.clang-format index 682ae09..d896a25 100644 --- a/.clang-format +++ b/.clang-format @@ -3,3 +3,7 @@ IndentWidth: 4 AllowShortFunctionsOnASingleLine: None KeepEmptyLinesAtTheStartOfBlocks: false BreakBeforeBraces: Allman +IndentCaseLabels: true +ColumnLimit: 144 +SortIncludes: false +SkipMacroDefinitionBody: true diff --git a/src/main.c b/src/main.c index 6676bb3..8434caa 100644 --- a/src/main.c +++ b/src/main.c @@ -68,7 +68,7 @@ int main(int argc, char *argv[]) /* Configure input terminal */ if (isatty(fileno(stdin))) { - stdin_configure(); + stdin_configure(); } else { @@ -110,7 +110,8 @@ int main(int argc, char *argv[]) if (interactive_mode) { tio_printf("Press ctrl-%c q to quit", option.prefix_key); - } else + } + else { tio_printf("Non-interactive mode enabled"); tio_printf("Press ctrl-c to quit"); diff --git a/src/misc.c b/src/misc.c index bd0a429..13d601e 100644 --- a/src/misc.c +++ b/src/misc.c @@ -170,6 +170,7 @@ bool match_patterns(const char *string, const char *patterns) pattern = strtok(patterns_copy, ","); while (pattern != NULL) { + // clang-format off // Check if the string matches the current pattern #ifdef FNM_EXTMATCH if (fnmatch(pattern, string, FNM_EXTMATCH) == 0) @@ -180,6 +181,7 @@ bool match_patterns(const char *string, const char *patterns) free(patterns_copy); return true; } + // clang-format on // Move to the next pattern pattern = strtok(NULL, ","); diff --git a/src/options.c b/src/options.c index acff7ce..beddd90 100644 --- a/src/options.c +++ b/src/options.c @@ -61,6 +61,7 @@ enum opt_t OPT_EXEC, }; +// clang-format off /* Default options */ struct option_t option = { @@ -125,6 +126,7 @@ struct option_t option = .map_i_msb2lsb = false, .map_o_ign_cr = false, }; +// clang-format on void option_print_help(char *argv[]) { @@ -833,12 +835,14 @@ void options_print() tio_printf(" Output line delay: %d", option.output_line_delay); tio_printf(" Automatic connect strategy: %s", option_auto_connect_state_to_string(option.auto_connect)); tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "true" : "false"); + // clang-format off tio_printf(" Pulse duration: DTR=%d RTS=%d CTS=%d DSR=%d DCD=%d RI=%d", option.dtr_pulse_duration, option.rts_pulse_duration, option.cts_pulse_duration, option.dsr_pulse_duration, option.dcd_pulse_duration, option.ri_pulse_duration); + // clang-format on tio_printf(" Input mode: %s", option_input_mode_to_string(option.input_mode)); tio_printf(" Output mode: %s", option_output_mode_to_string(option.output_mode)); tio_printf(" Alert: %s", option_alert_state_to_string(option.alert)); @@ -887,8 +891,9 @@ void options_parse(int argc, char *argv[]) option.vt100 = true; } - while (1) + while (true) { + // clang-format off static struct option long_options[] = { {"baudrate", required_argument, 0, 'b' }, @@ -932,6 +937,7 @@ void options_parse(int argc, char *argv[]) {"complete-profiles", no_argument, 0, OPT_COMPLETE_PROFILES }, {0, 0, 0, 0 } }; + // clang-format on /* getopt_long stores the option index here */ int option_index = 0; @@ -1131,7 +1137,7 @@ void options_parse(int argc, char *argv[]) /* Assume first non-option is the target (tty device, profile, tid) */ if (strcmp(option.target, "")) { - optind++; + optind++; } else if (optind < argc) { @@ -1176,6 +1182,7 @@ void options_parse_final(int argc, char *argv[]) #ifdef __CYGWIN__ unsigned char portnum; char *tty_win; + // clang-format off if ( ((strncmp("COM", option.target, 3) == 0) || (strncmp("com", option.target, 3) == 0) ) && (sscanf(option.target + 3, "%hhu", &portnum) == 1) @@ -1184,5 +1191,6 @@ void options_parse_final(int argc, char *argv[]) asprintf(&tty_win, "/dev/ttyS%hhu", portnum - 1); option.target = tty_win; } + // clang-format on #endif } diff --git a/src/readline.c b/src/readline.c index 8176a0b..6cfae00 100644 --- a/src/readline.c +++ b/src/readline.c @@ -90,7 +90,7 @@ static void readline_input_cr(void) rl_history_count++; } else - { + { free(rl_history[0]); memmove(&rl_history[0], &rl_history[1], (RL_HISTORY_MAX - 1) * sizeof(char*)); rl_history[RL_HISTORY_MAX - 1] = strndup(rl_line, rl_line_length); diff --git a/src/script.c b/src/script.c index b69d55b..e050af6 100644 --- a/src/script.c +++ b/src/script.c @@ -45,6 +45,7 @@ static int device_fd; +// clang-format off static char script_init[] = "tio.set = function(arg)\n" " local dtr = arg.DTR or -1\n" @@ -71,6 +72,7 @@ static char script_init[] = "end\n" "tio.alwaysecho = true\n" "setmetatable(tio, tio)\n"; +// clang-format on static bool alwaysecho(lua_State *L) { @@ -100,7 +102,9 @@ static int api_echo(lua_State *L) log_printf("\n[%s] %s", pTimeStampNow, str); } } - } else { + } + else + { for (size_t i=0; i= '0' && c <= '9') + if (c >= '0' && c <= '9') { return c - '0'; } @@ -331,7 +333,7 @@ void *tty_stdin_input_thread(void *arg) pthread_mutex_unlock(&mutex_input_ready); // Input loop for stdin - while (1) + while (true) { /* Input from stdin ready */ byte_count = read(STDIN_FILENO, input_buffer, BUFSIZ); @@ -360,7 +362,8 @@ void *tty_stdin_input_thread(void *arg) for (int i = 0; i 0; size--) { + for (crc = 0; size > 0; size--) + { s = *data++ ^ (crc >> 8); s ^= (s >> 4); crc = (crc << 8) ^ s ^ (s << 5) ^ (s << 12); @@ -81,16 +84,19 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) /* Drain pending characters from serial line. Insist on the * last drained character being 'C'. */ - while(1) { + while (true) + { if (key_hit) return -1; rc = read_poll(sio, &resp, 1, 50); - if (rc == 0) { + if (rc == 0) + { if (resp == 'C') break; if (resp == CAN) return ERR; continue; } - else if (rc < 0) { + else if (rc < 0) + { tio_error_print("Read sync from serial failed"); return ERR; } @@ -100,7 +106,8 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) packet.seq = seq; packet.type = STX; - while (len) { + while (len) + { size_t sz, z = 0; char *from, status; @@ -116,11 +123,14 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) /* Send packet */ from = (char *) &packet; sz = sizeof(packet); - while (sz) { + while (sz) + { if (key_hit) return ERR; - if ((rc = write(sio, from, sz)) < 0 ) { - if (errno == EWOULDBLOCK) { + if ((rc = write(sio, from, sz)) < 0 ) + { + if (errno == EWOULDBLOCK) + { usleep(1000); continue; } @@ -138,30 +148,36 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) if (seq == 0 && packet.data[0] == 0) resp = ACK; /* Read receiver response, timeout 1 s */ - for(int n=0; n < 20; n++) { + for (int n = 0; n < 20; n++) + { if (key_hit) return ERR; rc = read_poll(sio, &resp, 1, 50); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Read ack/nak from serial failed"); return ERR; - } else if(rc > 0) { + } + else if (rc > 0) + { 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 = '?'; + 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) { + if (resp == ACK) + { packet.seq++; len -= z; buf += z; @@ -169,23 +185,29 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) } /* Send EOT at 1 Hz until ACK or CAN received */ - while (seq) { + while (seq) + { if (key_hit) return ERR; - if (write(sio, EOT_STR, 1) < 0) { + if (write(sio, EOT_STR, 1) < 0) + { tio_error_print("Write EOT to serial failed"); return ERR; } write(STDOUT_FILENO, "|", 1); /* 1s timeout */ rc = read_poll(sio, &resp, 1, 1000); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Read from serial failed"); return ERR; - } else if(rc == 0) { + } + else if (rc == 0) + { continue; } - if (resp == ACK || resp == CAN) { + if (resp == ACK || resp == CAN) + { write(STDOUT_FILENO, "\r\n", 2); return (resp == ACK) ? OK : ERR; } @@ -203,16 +225,19 @@ static int xmodem(int sio, const void *data, size_t len) /* Drain pending characters from serial line. Insist on the * last drained character being 'C'. */ - while(1) { + while (true) + { if (key_hit) return -1; rc = read_poll(sio, &resp, 1, 50); - if (rc == 0) { + if (rc == 0) + { if (resp == 'C') break; if (resp == CAN) return ERR; continue; } - else if (rc < 0) { + else if (rc < 0) + { tio_error_print("Read sync from serial failed"); return ERR; } @@ -222,7 +247,8 @@ static int xmodem(int sio, const void *data, size_t len) packet.seq = 1; packet.type = SOH; - while (len) { + while (len) + { size_t sz, z = 0; char *from, status; @@ -238,11 +264,14 @@ static int xmodem(int sio, const void *data, size_t len) /* Send packet */ from = (char *) &packet; sz = sizeof(packet); - while (sz) { + while (sz) + { if (key_hit) return ERR; - if ((rc = write(sio, from, sz)) < 0 ) { - if (errno == EWOULDBLOCK) { + if ((rc = write(sio, from, sz)) < 0 ) + { + if (errno == EWOULDBLOCK) + { usleep(1000); continue; } @@ -257,30 +286,36 @@ static int xmodem(int sio, const void *data, size_t len) resp = 0; /* Read receiver response, timeout 1 s */ - for(int n=0; n < 20; n++) { + for (int n = 0; n < 20; n++) + { if (key_hit) return ERR; rc = read_poll(sio, &resp, 1, 50); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Read ack/nak from serial failed"); return ERR; - } else if(rc > 0) { + } + else if (rc > 0) + { 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 = '?'; + 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) { + if (resp == ACK) + { packet.seq++; len -= z; buf += z; @@ -288,23 +323,29 @@ static int xmodem(int sio, const void *data, size_t len) } /* Send EOT at 1 Hz until ACK or CAN received */ - while (1) { + while (true) + { if (key_hit) return ERR; - if (write(sio, EOT_STR, 1) < 0) { + if (write(sio, EOT_STR, 1) < 0) + { tio_error_print("Write EOT to serial failed"); return ERR; } write(STDOUT_FILENO, "|", 1); /* 1s timeout */ rc = read_poll(sio, &resp, 1, 1000); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Read from serial failed"); return ERR; - } else if(rc == 0) { + } + else if (rc == 0) + { continue; } - if (resp == ACK || resp == CAN) { + if (resp == ACK || resp == CAN) + { write(STDOUT_FILENO, "\r\n", 2); return (resp == ACK) ? OK : ERR; } @@ -325,8 +366,10 @@ int start_receive(int sio) seconds. If nothing is received in that time then return false to indicate that the transfer did not start. */ rc = write(sio, "C", 1); - if (rc < 0) { - if (errno == EWOULDBLOCK) { + if (rc < 0) + { + if (errno == EWOULDBLOCK) + { usleep(1000); continue; } @@ -353,7 +396,7 @@ int start_receive(int sio) return rc; } -uint16_t update_CRC(uint16_t crc, char data_char) +uint16_t update_CRC(uint16_t crc, char data_char) { uint8_t data = data_char; crc = crc ^ ((uint16_t)data << 8); @@ -385,19 +428,25 @@ int receive_packet(int sio, struct xpacket packet, int fd) /* Read seq bytes*/ rc = read_poll(sio, &rxSeq1, 1, 3000); - if (rc == 0) { + if (rc == 0) + { tio_error_print("Timeout waiting for first seq byte"); return ERR; - } else if (rc < 0) { - tio_error_print("Error reading first seq byte") + } + else if (rc < 0) + { + tio_error_print("Error reading first seq byte"); return ERR_FATAL; } rc = read_poll(sio, &rxSeq2, 1, 3000); - if (rc == 0) { + if (rc == 0) + { tio_error_print("Timeout waiting for second seq byte"); return ERR; - } else if (rc < 0) { - tio_error_print("Error reading second seq byte") + } + else if (rc < 0) + { + tio_error_print("Error reading second seq byte"); return ERR_FATAL; } if (key_hit) @@ -407,20 +456,24 @@ int receive_packet(int sio, struct xpacket packet, int fd) for (unsigned ix = 0; (ix < sizeof(packet.data)); ix++) { rc = read_poll(sio, &resp, 1, 3000); - /* If the read times out or fails then fail this packet. */ + /* If the read times out or fails then fail this packet. */ if (rc == 0) { tio_error_print("Timeout waiting for next packet char"); rc = write(sio, CAN_STR, 1); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Write cancel packet to serial failed"); return ERR_FATAL; } return ERR; - } else if (rc < 0) { - tio_error_print("Error reading next packet char") + } + else if (rc < 0) + { + tio_error_print("Error reading next packet char"); rc = write(sio, CAN_STR, 1); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Write cancel packet to serial failed"); } return ERR_FATAL; @@ -433,11 +486,14 @@ int receive_packet(int sio, struct xpacket packet, int fd) /* Read CRC */ rc = read_poll(sio, &resp, 1, 3000); - if (rc == 0) { + if (rc == 0) + { tio_error_print("Timeout waiting for first CRC byte"); return ERR; - } else if (rc < 0) { - tio_error_print("Error reading first CRC byte") + } + else if (rc < 0) + { + tio_error_print("Error reading first CRC byte"); return ERR_FATAL; } @@ -446,20 +502,23 @@ int receive_packet(int sio, struct xpacket packet, int fd) rxCrc = uresp16 << 8; rc = read_poll(sio, &resp, 1, 3000); - if (rc == 0) { + if (rc == 0) + { tio_error_print("Timeout waiting for second CRC byte"); return ERR; - } else if (rc < 0) { - tio_error_print("Error reading second CRC byte") + } + else if (rc < 0) + { + tio_error_print("Error reading second CRC byte"); return ERR_FATAL; } - + uresp = resp; uresp16 = uresp; rxCrc |= uresp16; if (key_hit) - return USER_CAN; + return USER_CAN; /* At this point in the code, there should not be anything in the receive buffer because the sender has just sent a complete packet and is waiting on a response. */ @@ -469,7 +528,8 @@ int receive_packet(int sio, struct xpacket packet, int fd) tio_error_print("%s", strerror(errno)); tio_error_print("Poll check error after packet finish"); rc = write(sio, CAN_STR, 1); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Write cancel packet to serial failed"); } return ERR_FATAL; @@ -494,7 +554,8 @@ int receive_packet(int sio, struct xpacket packet, int fd) { /* Resend of previously processed packet. */ rc = write(sio, ACK_STR, 1); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Write acknowlegdement packet to serial failed"); return ERR_FATAL; } @@ -520,7 +581,8 @@ int receive_packet(int sio, struct xpacket packet, int fd) { tio_error_print("Problem writing to file"); rc = write(sio, CAN_STR, 1); - if (rc < 0) { + if (rc < 0) + { tio_error_print("Write cancel packet to serial failed"); } return ERR_FATAL; @@ -545,16 +607,20 @@ int xmodem_receive(int sio, int fd) char status; /* Drain pending characters from serial line.*/ - while(1) { + while (true) + { if (key_hit) return -1; rc = read_poll(sio, &resp, 1, 50); - if (rc == 0) { + if (rc == 0) + { if (resp == CAN) return ERR; break; } - else if (rc < 0) { - if (rc != USER_CAN) { + else if (rc < 0) + { + if (rc != USER_CAN) + { tio_error_print("Read sync from serial failed"); } return ERR; @@ -571,79 +637,95 @@ int xmodem_receive(int sio, int fd) { tio_error_print("Timeout waiting for transfer to start"); return ERR; - } else if (rc < 0) { + } + else if (rc < 0) + { tio_error_print("Error starting XMODEM receive"); return ERR; } - while (!complete) { + while (!complete) + { /* Poll for 1 new byte for 3 seconds */ rc = read_poll(sio, &resp, 1, 3000); - if (rc == 0) { + if (rc == 0) + { tio_error_print("Timeout waiting for start of next packet"); return ERR; - } else if (rc < 0) { - tio_error_print("Error reading start of next packet") + } + else if (rc < 0) + { + tio_error_print("Error reading start of next packet"); return ERR; } if (key_hit) return USER_CAN; - switch(resp) + switch (resp) { case SOH: - /* Start of a packet */ - rc = receive_packet(sio, packet, fd); - if (rc == OK) { - packet.seq++; - status = '.'; - } else if (rc == ERR) { - rc = write(sio, NAK_STR, 1); - if (rc < 0) { - tio_error_print("Writing not acknowledge packet to serial failed"); + /* Start of a packet */ + rc = receive_packet(sio, packet, fd); + if (rc == OK) + { + packet.seq++; + status = '.'; + } + else if (rc == ERR) + { + rc = write(sio, NAK_STR, 1); + if (rc < 0) + { + tio_error_print("Writing not acknowledge packet to serial failed"); + return ERR; + } + status = 'N'; + } + else if (rc == ERR_FATAL) + { + tio_error_print("Receive cancelled due to fatal error"); return ERR; } - status = 'N'; - } else if (rc == ERR_FATAL) { - tio_error_print("Receive cancelled due to fatal error"); - return ERR; - } else if (rc == USER_CAN) { - rc = write(sio, CAN_STR, 1); - if (rc < 0) { - tio_error_print("Writing cancel to serial failed"); - return ERR; + else if (rc == USER_CAN) + { + rc = write(sio, CAN_STR, 1); + if (rc < 0) + { + tio_error_print("Writing cancel to serial failed"); + return ERR; + } + return USER_CAN; } - return USER_CAN; - } else if (rc == RX_IGNORE) { - status = ':'; - } - break; + else if (rc == RX_IGNORE) + { + status = ':'; + } + break; case EOT: - /* End of Transfer */ - rc = write(sio, ACK_STR, 1); - if (rc < 0) - { - tio_error_print("Write acknowlegdement packet to serial failed"); - return ERR; - } - complete = true; - status = '\0'; - write(STDOUT_FILENO, "|\r\n", 3); - break; + /* End of Transfer */ + rc = write(sio, ACK_STR, 1); + if (rc < 0) + { + tio_error_print("Write acknowlegdement packet to serial failed"); + return ERR; + } + complete = true; + status = '\0'; + write(STDOUT_FILENO, "|\r\n", 3); + break; case CAN: - /* Cancel from sender */ - tio_error_print("Transmission cancelled from sender"); - return ERR; - break; + /* Cancel from sender */ + tio_error_print("Transmission cancelled from sender"); + return ERR; + break; default: - tio_error_print("Unexpected character received waiting for next packet"); - return ERR; - break; + tio_error_print("Unexpected character received waiting for next packet"); + return ERR; + break; } - /* Update "progress bar" */ write(STDOUT_FILENO, &status, 1); @@ -660,14 +742,16 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) /* Open file, map into memory */ fd = open(filename, O_RDONLY); - if (fd < 0) { + if (fd < 0) + { tio_error_print("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) { + if (!buf) + { close(fd); tio_error_print("Could not mmap file"); return ERR; @@ -675,15 +759,19 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) /* Do transfer */ key_hit = 0; - if (mode == XMODEM_1K) { + if (mode == XMODEM_1K) + { rc = xmodem_1k(sio, buf, len, 1); } - else if (mode == XMODEM_CRC) { + else if (mode == XMODEM_CRC) + { rc = xmodem(sio, buf, len); } - else { + else + { /* Ymodem: hdr + file + fin */ - while(1) { + while (true) + { char hdr[1024], *p; rc = -1; @@ -694,7 +782,7 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) if (xmodem_1k(sio, hdr, p - hdr, 0) < 0) break; /* hdr with metadata */ if (xmodem_1k(sio, buf, len, 1) < 0) break; /* xmodem file */ if (xmodem_1k(sio, "", 1, 0) < 0) break; /* empty hdr = fin */ - rc = 0; break; + rc = 0; break; } } key_hit = 0xff; @@ -712,21 +800,25 @@ int xymodem_receive(int sio, const char *filename, modem_mode_t mode) /* Create new file */ fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0664); - if (fd < 0) { + if (fd < 0) + { tio_error_print("Could not open file"); return ERR; } /* Do transfer */ key_hit = 0; - if (mode == XMODEM_1K) { + if (mode == XMODEM_1K) + { tio_error_print("Not supported"); rc = -1; } - else if (mode == XMODEM_CRC) { + else if (mode == XMODEM_CRC) + { rc = xmodem_receive(sio, fd); } - else { + else + { tio_error_print("Not supported"); rc = -1; } diff --git a/src/xymodem.h b/src/xymodem.h index 1b46cd7..b5d2b8d 100644 --- a/src/xymodem.h +++ b/src/xymodem.h @@ -21,7 +21,8 @@ #pragma once -typedef enum { +typedef enum +{ XMODEM_1K, XMODEM_CRC, YMODEM, @@ -30,5 +31,4 @@ typedef enum { extern char key_hit; int xymodem_send(int sio, const char *filename, modem_mode_t mode); - int xymodem_receive(int sio, const char *filename, modem_mode_t mode); From b2a8c02d1da4bd5aa146489f6f7ed2f7a7ae1a81 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Thu, 14 Aug 2025 22:18:41 +0900 Subject: [PATCH 03/53] Fix incorrect handling of the return value of the poll system call --- src/misc.c | 7 ++++++- src/xymodem.c | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/misc.c b/src/misc.c index 13d601e..271d266 100644 --- a/src/misc.c +++ b/src/misc.c @@ -98,12 +98,17 @@ int read_poll(int fd, void *data, size_t len, int timeout) if (fds.revents & POLLIN) { // Read ready data + // return value should not be 0 return read(fd, data, len); } + else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ + { + return -1; + } } /* Timeout */ - return ret; + return 0; } // Function to calculate djb2 hash of string diff --git a/src/xymodem.c b/src/xymodem.c index 3b645cc..3b46ef8 100644 --- a/src/xymodem.c +++ b/src/xymodem.c @@ -91,6 +91,9 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) rc = read_poll(sio, &resp, 1, 50); if (rc == 0) { + /* timeout 50 ms + resp has last received character beacuse read_poll() doesn't + destroy resp value in this case. */ if (resp == 'C') break; if (resp == CAN) return ERR; continue; @@ -232,6 +235,9 @@ static int xmodem(int sio, const void *data, size_t len) rc = read_poll(sio, &resp, 1, 50); if (rc == 0) { + /* timeout 50 ms + resp has last received character beacuse read_poll() doesn't + destroy resp value in this case. */ if (resp == 'C') break; if (resp == CAN) return ERR; continue; @@ -389,6 +395,10 @@ int start_receive(int sio) { return rc; } + else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ + { + return -1; + } } if (key_hit) return USER_CAN; @@ -544,6 +554,10 @@ int receive_packet(int sio, struct xpacket packet, int fd) while (read_poll(sio, &dummy, 1, 100) > 0) {} return ERR; } + else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ + { + return ERR; + } } uint8_t tester = 0xff; From 6cc0a58ed87172caeca2366bfb37a78e1e188cb7 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Fri, 15 Aug 2025 23:10:20 +0900 Subject: [PATCH 04/53] Refactoring XYMODEM codes and Fix error code handling Refactoring. - separete routines from xmodem_send_xxx, - xmsend_inirial_handshake - xmsend_wait_response, - xmsend_repeat_eot_and_wait_response - separete routines from xmodem_receive, - xmrecv_drain_pending_chars - separete routines from xymodem_send, - ymodem_send_1k - rename routines. - crc16 --> calculate_crc16 - update_CRC --> update_crc16 - xmodem_1k --> xmodem_send_1k - xmodem_send --> xmodem_send_128b - receive_packet --> xmrecv_receive_packet - rename struct types. - xpacket --> xpacket_128b - separete xpacket's header / footer - xpacket_hdr, xpacket_ftr_crc Fix error code handling. - use 'rc' for system call's return code, and add 'err' for xymodem function's return code (OK, ERR, ...) - add ERR_TMO to return codes - change USER_CAN to ERR_USER_CAN, because it is negative value Fix xmrecv_receive_packet()'s argument packet to pointer type. --- src/xymodem.c | 647 +++++++++++++++++++++++++++----------------------- 1 file changed, 349 insertions(+), 298 deletions(-) diff --git a/src/xymodem.c b/src/xymodem.c index 3b46ef8..cbc7834 100644 --- a/src/xymodem.c +++ b/src/xymodem.c @@ -34,34 +34,42 @@ #define OK 0 #define ERR (-1) #define ERR_FATAL (-2) -#define USER_CAN (-5) +#define ERR_TMO (-3) +#define ERR_USER_CAN (-5) #define RX_IGNORE 5 #define min(a, b) ((a) < (b) ? (a) : (b)) -struct xpacket_1k +struct xpkt_hdr { uint8_t type; uint8_t seq; uint8_t nseq; - uint8_t data[1024]; +} __attribute__((packed)); + +struct xpkt_ftr_crc +{ uint8_t crc_hi; uint8_t crc_lo; } __attribute__((packed)); -struct xpacket +struct xpacket_1k { - uint8_t type; - uint8_t seq; - uint8_t nseq; + struct xpkt_hdr hdr; + uint8_t data[1024]; + struct xpkt_ftr_crc ftr; +} __attribute__((packed)); + +struct xpacket_128b +{ + struct xpkt_hdr hdr; uint8_t data[128]; - uint8_t crc_hi; - uint8_t crc_lo; + struct xpkt_ftr_crc ftr; } __attribute__((packed)); /* See https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks */ -static uint16_t crc16(const uint8_t *data, uint16_t size) +static uint16_t calculate_crc16(const uint8_t *data, uint16_t size) { uint16_t crc, s; @@ -74,27 +82,47 @@ static uint16_t crc16(const uint8_t *data, uint16_t size) return crc; } -static int xmodem_1k(int sio, const void *data, size_t len, int seq) +static uint16_t update_crc16(uint16_t crc, char data_char) { - struct xpacket_1k packet; - const uint8_t *buf = data; - char resp = 0; - int rc, crc; + uint8_t data = data_char; - /* Drain pending characters from serial line. Insist on the - * last drained character being 'C'. - */ + crc = crc ^ ((uint16_t)data << 8); + for (int ix = 0; (ix < 8); ix++) + { + if (crc & 0x8000) + { + crc = (crc << 1) ^ 0x1021; + } + else + { + crc <<= 1; + } + } + return crc; +} + +/* + * Drain pending characters from serial line. Insist on the + * last drained character being initial character 'C':CRC,1K + */ +static int xmsend_initial_handshake(int sio, char init_ch) +{ + int rc; + char resp = 0; + + /* Wait for initial character */ while (true) { if (key_hit) - return -1; + return ERR_USER_CAN; + rc = read_poll(sio, &resp, 1, 50); if (rc == 0) { /* timeout 50 ms resp has last received character beacuse read_poll() doesn't destroy resp value in this case. */ - if (resp == 'C') break; + if (resp == init_ch) break; if (resp == CAN) return ERR; continue; } @@ -104,10 +132,95 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) return ERR; } } + return OK; +} + +/* + * Read receiver response, timeout 1 s + */ +static int xmsend_wait_response(int sio, char *resp, char tmo_resp) +{ + int rc; + + for (int n = 0; n < 20; n++) + { + if (key_hit) + return ERR_USER_CAN; + + rc = read_poll(sio, resp, 1, 50); + if (rc < 0) + { + tio_error_print("Read ack/nak from serial failed"); + return ERR; + } + else if (rc > 0) + { + /* response received */ + return OK; + } + } + /* no response time-out */ + *resp = tmo_resp; + return OK; +} + +/* + * Send EOT at 1 Hz until ACK or CAN received + */ +static int xmsend_repeat_eot_and_wait_response(int sio) +{ + int rc; + char resp; + + while (true) + { + if (key_hit) + return ERR_USER_CAN; + + if (write(sio, EOT_STR, 1) < 0) + { + tio_error_print("Write EOT to serial failed"); + return ERR; + } + write(STDOUT_FILENO, "|", 1); + /* 1s timeout */ + rc = read_poll(sio, &resp, 1, 1000); + if (rc < 0) + { + tio_error_print("Read from serial failed"); + return ERR; + } + else if (rc == 0) + { + continue; + } + if (resp == ACK || resp == CAN) + { + write(STDOUT_FILENO, "\r\n", 2); + return (resp == ACK) ? OK : ERR; + } + } + return OK; /* not reached */ +} + +static int xmodem_send_1k(int sio, const void *data, size_t len, int seq) +{ + struct xpacket_1k packet; + const uint8_t *buf = data; + char resp = 0, tmo_resp; + int rc, crc, err; + + /* Drain pending characters from serial line. + Insist on the last drained character being 'C' */ + err = xmsend_initial_handshake(sio, 'C'); + if (err != OK) + { + return err; + } /* Always work with 1K packets */ - packet.seq = seq; - packet.type = STX; + packet.hdr.seq = seq; + packet.hdr.type = STX; while (len) { @@ -118,10 +231,10 @@ static int xmodem_1k(int sio, const void *data, size_t len, int 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; + crc = calculate_crc16(packet.data, sizeof(packet.data)); + packet.ftr.crc_hi = crc >> 8; + packet.ftr.crc_lo = crc; + packet.hdr.nseq = 0xff - packet.hdr.seq; /* Send packet */ from = (char *) &packet; @@ -129,7 +242,8 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) while (sz) { if (key_hit) - return ERR; + return ERR_USER_CAN; + if ((rc = write(sio, from, sz)) < 0 ) { if (errno == EWOULDBLOCK) @@ -145,26 +259,16 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) } /* Clear response */ - resp = 0; + tmo_resp = 0; /* 'lrzsz' does not ACK ymodem's fin packet */ - if (seq == 0 && packet.data[0] == 0) resp = ACK; + if (seq == 0 && packet.data[0] == 0) tmo_resp = ACK; /* Read receiver response, timeout 1 s */ - for (int n = 0; n < 20; n++) + err = xmsend_wait_response(sio, &resp, tmo_resp); + if (err != OK) { - if (key_hit) - return ERR; - rc = read_poll(sio, &resp, 1, 50); - if (rc < 0) - { - tio_error_print("Read ack/nak from serial failed"); - return ERR; - } - else if (rc > 0) - { - break; - } + return err; } /* Update "progress bar" */ @@ -181,190 +285,178 @@ static int xmodem_1k(int sio, const void *data, size_t len, int seq) /* Move to next block after ACK */ if (resp == ACK) { - packet.seq++; + packet.hdr.seq++; + len -= z; + buf += z; + } + } + + if (seq != 0) + { + /* Send EOT at 1 Hz until ACK or CAN received */ + err = xmsend_repeat_eot_and_wait_response(sio); + if (err != OK) { + return err; + } + } + + return OK; +} + +static int xmodem_send_128b(int sio, const void *data, size_t len) +{ + struct xpacket_128b packet; + const uint8_t *buf = data; + char resp = 0, tmo_resp; + int rc, crc, err; + + /* Drain pending characters from serial line. + Insist on the last drained character being 'C' */ + err = xmsend_initial_handshake(sio, 'C'); + if (err != OK) + return err; + + /* Always work with 128b packets */ + packet.hdr.seq = 1; + packet.hdr.type = SOH; + + 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 = calculate_crc16(packet.data, sizeof(packet.data)); + packet.ftr.crc_hi = crc >> 8; + packet.ftr.crc_lo = crc; + packet.hdr.nseq = 0xff - packet.hdr.seq; + + /* Send packet */ + from = (char *) &packet; + sz = sizeof(packet); + while (sz) + { + if (key_hit) + return ERR_USER_CAN; + + if ((rc = write(sio, from, sz)) < 0 ) + { + if (errno == EWOULDBLOCK) + { + usleep(1000); + continue; + } + tio_error_print("Write packet to serial failed"); + return ERR; + } + from += rc; + sz -= rc; + } + + /* Clear response */ + tmo_resp = 0; + + /* Read receiver response, timeout 1 s */ + err = xmsend_wait_response(sio, &resp, tmo_resp); + if (err != OK) + { + return err; + } + + /* 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.hdr.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_STR, 1) < 0) - { - tio_error_print("Write EOT to serial failed"); - return ERR; - } - write(STDOUT_FILENO, "|", 1); - /* 1s timeout */ - rc = read_poll(sio, &resp, 1, 1000); - if (rc < 0) - { - tio_error_print("Read from serial failed"); - return ERR; - } - else if (rc == 0) - { - continue; - } - if (resp == ACK || resp == CAN) - { - write(STDOUT_FILENO, "\r\n", 2); - return (resp == ACK) ? OK : ERR; - } - } - return 0; /* not reached */ + err = xmsend_repeat_eot_and_wait_response(sio); + + return err; } -static int xmodem(int sio, const void *data, size_t len) +/* + * Ymodem: hdr + file + fin + */ +static int ymodem_send_1k(int sio, const void *data, size_t len, const char *filename, struct stat *stat) { - struct xpacket packet; - const uint8_t *buf = data; - char resp = 0; - int rc, crc; + int err; + + while (1) + { + char hdr[1024], *p; + + err = ERR; + if (strlen(filename) > 977) break; /* hdr block overrun */ + p = stpncpy(hdr, filename, 1024) + 1; + p += sprintf(p, "%ld %lo %o", len, stat->st_mtime, stat->st_mode); + + if (xmodem_send_1k(sio, hdr, p - hdr, 0) < 0) break; /* hdr with metadata */ + if (xmodem_send_1k(sio, data, len, 1) < 0) break; /* xmodem file */ + if (xmodem_send_1k(sio, "", 1, 0) < 0) break; /* empty hdr = fin */ + err = OK; break; + } + return err; +} + +/* + * Drain pending characters from serial line. + */ +static int xmrecv_drain_pending_chars(int sio) +{ + int rc; + char resp; - /* Drain pending characters from serial line. Insist on the - * last drained character being 'C'. - */ while (true) { if (key_hit) - return -1; + return ERR_USER_CAN; + rc = read_poll(sio, &resp, 1, 50); if (rc == 0) { - /* timeout 50 ms - resp has last received character beacuse read_poll() doesn't - destroy resp value in this case. */ - if (resp == 'C') break; - if (resp == CAN) return ERR; - continue; + break; } else if (rc < 0) { tio_error_print("Read sync from serial failed"); return ERR; } - } - - /* Always work with 128b packets */ - packet.seq = 1; - packet.type = SOH; - - 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 (resp == CAN) { - if (key_hit) - return ERR; - if ((rc = write(sio, from, sz)) < 0 ) - { - if (errno == EWOULDBLOCK) - { - usleep(1000); - continue; - } - tio_error_print("Write packet to serial failed"); - return ERR; - } - from += rc; - sz -= rc; - } - - /* Clear response */ - resp = 0; - - /* Read receiver response, timeout 1 s */ - for (int n = 0; n < 20; n++) - { - if (key_hit) - return ERR; - rc = read_poll(sio, &resp, 1, 50); - if (rc < 0) - { - tio_error_print("Read ack/nak from serial failed"); - return ERR; - } - else if (rc > 0) - { - 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 (true) - { - if (key_hit) - return ERR; - if (write(sio, EOT_STR, 1) < 0) - { - tio_error_print("Write EOT to serial failed"); return ERR; } - write(STDOUT_FILENO, "|", 1); - /* 1s timeout */ - rc = read_poll(sio, &resp, 1, 1000); - if (rc < 0) - { - tio_error_print("Read from serial failed"); - return ERR; - } - else if (rc == 0) - { - continue; - } - if (resp == ACK || resp == CAN) - { - write(STDOUT_FILENO, "\r\n", 2); - return (resp == ACK) ? OK : ERR; - } } - return 0; /* not reached */ + return OK; } -int start_receive(int sio) +/* + * Start Receive + */ +static int xmrecv_start_receive(int sio) { int rc; struct pollfd fds; fds.events = POLLIN; fds.fd = sio; + for (int n = 0; n < 20; n++) { /* Send the 'C' byte until the sender of the file responds with @@ -387,44 +479,29 @@ int start_receive(int sio) if (rc < 0) { tio_error_print("%s", strerror(errno)); - return rc; + return ERR; } else if (rc > 0) { if (fds.revents & POLLIN) { - return rc; + return OK; } else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ { - return -1; + return ERR; } } if (key_hit) - return USER_CAN; + return ERR_USER_CAN; } - return rc; + return ERR_TMO; } -uint16_t update_CRC(uint16_t crc, char data_char) -{ - uint8_t data = data_char; - crc = crc ^ ((uint16_t)data << 8); - for (int ix = 0; (ix < 8); ix++) - { - if (crc & 0x8000) - { - crc = (crc << 1) ^ 0x1021; - } - else - { - crc <<= 1; - } - } - return crc; -} - -int receive_packet(int sio, struct xpacket packet, int fd) +/* + * Receive a packet + */ +static int xmrecv_receive_packet(int sio, struct xpacket_128b *packet, int fd) { char rxSeq1, rxSeq2 = 0; char resp = 0; @@ -441,7 +518,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) if (rc == 0) { tio_error_print("Timeout waiting for first seq byte"); - return ERR; + return ERR_TMO; } else if (rc < 0) { @@ -452,7 +529,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) if (rc == 0) { tio_error_print("Timeout waiting for second seq byte"); - return ERR; + return ERR_TMO; } else if (rc < 0) { @@ -460,10 +537,10 @@ int receive_packet(int sio, struct xpacket packet, int fd) return ERR_FATAL; } if (key_hit) - return USER_CAN; + return ERR_USER_CAN; /* Read packet Data */ - for (unsigned ix = 0; (ix < sizeof(packet.data)); ix++) + for (unsigned ix = 0; (ix < sizeof(packet->data)); ix++) { rc = read_poll(sio, &resp, 1, 3000); /* If the read times out or fails then fail this packet. */ @@ -476,7 +553,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) tio_error_print("Write cancel packet to serial failed"); return ERR_FATAL; } - return ERR; + return ERR_TMO; } else if (rc < 0) { @@ -488,10 +565,10 @@ int receive_packet(int sio, struct xpacket packet, int fd) } return ERR_FATAL; } - packet.data[ix] = (uint8_t) resp; - calcCrc = update_CRC(calcCrc, resp); + packet->data[ix] = (uint8_t) resp; + calcCrc = update_crc16(calcCrc, resp); if (key_hit) - return USER_CAN; + return ERR_USER_CAN; } /* Read CRC */ @@ -499,7 +576,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) if (rc == 0) { tio_error_print("Timeout waiting for first CRC byte"); - return ERR; + return ERR_TMO; } else if (rc < 0) { @@ -515,7 +592,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) if (rc == 0) { tio_error_print("Timeout waiting for second CRC byte"); - return ERR; + return ERR_TMO; } else if (rc < 0) { @@ -528,10 +605,10 @@ int receive_packet(int sio, struct xpacket packet, int fd) rxCrc |= uresp16; if (key_hit) - return USER_CAN; + return ERR_USER_CAN; /* At this point in the code, there should not be anything in the receive buffer - because the sender has just sent a complete packet and is waiting on a response. */ + because the sender has just sent a complete packet and is waiting on a response. */ rc = poll(&fds, 1, 10); if (rc < 0) { @@ -564,7 +641,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) uint8_t seq1 = rxSeq1; uint8_t seq2 = rxSeq2; - if ((calcCrc == rxCrc) && (seq1 == packet.seq - 1) && ((seq1 ^ seq2) == tester)) + if ((calcCrc == rxCrc) && (seq1 == packet->hdr.seq - 1) && ((seq1 ^ seq2) == tester)) { /* Resend of previously processed packet. */ rc = write(sio, ACK_STR, 1); @@ -575,7 +652,7 @@ int receive_packet(int sio, struct xpacket packet, int fd) } return RX_IGNORE; } - else if ((calcCrc != rxCrc) || (seq1 != packet.seq) || ((seq1 ^ seq2) != tester)) + else if ((calcCrc != rxCrc) || (seq1 != packet->hdr.seq) || ((seq1 ^ seq2) != tester)) { /* Fail if the CRC or sequence number is not correct or if the two received sequence numbers are not the complement of one another. */ @@ -583,14 +660,14 @@ int receive_packet(int sio, struct xpacket packet, int fd) tio_debug_printf("CRC read: %u", rxCrc); tio_debug_printf("CRC calculated: %u", calcCrc); tio_debug_printf("Seq read: %hhu", rxSeq1); - tio_debug_printf("Seq should be: %hhu", packet.seq); + tio_debug_printf("Seq should be: %hhu", packet->hdr.seq); tio_debug_printf("inv seq: %hhu", rxSeq2); return ERR; } else { /* The data is good. Process the packet then ACK it to the sender. */ - rc = write(fd, packet.data, sizeof(packet.data)); + rc = write(fd, packet->data, sizeof(packet->data)); if (rc < 0) { tio_error_print("Problem writing to file"); @@ -614,48 +691,36 @@ int receive_packet(int sio, struct xpacket packet, int fd) int xmodem_receive(int sio, int fd) { - struct xpacket packet; + struct xpacket_128b packet; char resp = 0; - int rc; + int rc, err; bool complete = false; char status; /* Drain pending characters from serial line.*/ - while (true) + err = xmrecv_drain_pending_chars(sio); + if (err != OK) { - if (key_hit) - return -1; - rc = read_poll(sio, &resp, 1, 50); - if (rc == 0) - { - if (resp == CAN) return ERR; - break; - } - else if (rc < 0) - { - if (rc != USER_CAN) - { - tio_error_print("Read sync from serial failed"); - } - return ERR; - } + return err; } /* Always work with 128b packets */ - packet.seq = 1; - packet.type = SOH; + packet.hdr.seq = 1; + packet.hdr.type = SOH; /* Start Receive*/ - rc = start_receive(sio); - if (rc == 0) + err = xmrecv_start_receive(sio); + if (err != OK) { - tio_error_print("Timeout waiting for transfer to start"); - return ERR; - } - else if (rc < 0) - { - tio_error_print("Error starting XMODEM receive"); - return ERR; + if (err == ERR_TMO) + { + tio_error_print("Timeout waiting for transfer to start"); + } + else + { + tio_error_print("Error starting XMODEM receive"); + } + return err; } while (!complete) @@ -665,7 +730,7 @@ int xmodem_receive(int sio, int fd) if (rc == 0) { tio_error_print("Timeout waiting for start of next packet"); - return ERR; + return ERR_TMO; } else if (rc < 0) { @@ -673,19 +738,19 @@ int xmodem_receive(int sio, int fd) return ERR; } if (key_hit) - return USER_CAN; + return ERR_USER_CAN; switch (resp) { case SOH: /* Start of a packet */ - rc = receive_packet(sio, packet, fd); - if (rc == OK) + err = xmrecv_receive_packet(sio, &packet, fd); + if (err == OK) { - packet.seq++; + packet.hdr.seq++; status = '.'; } - else if (rc == ERR) + else if (err == ERR || err == ERR_TMO) { rc = write(sio, NAK_STR, 1); if (rc < 0) @@ -695,12 +760,12 @@ int xmodem_receive(int sio, int fd) } status = 'N'; } - else if (rc == ERR_FATAL) + else if (err == ERR_FATAL) { tio_error_print("Receive cancelled due to fatal error"); return ERR; } - else if (rc == USER_CAN) + else if (err == ERR_USER_CAN) { rc = write(sio, CAN_STR, 1); if (rc < 0) @@ -708,9 +773,9 @@ int xmodem_receive(int sio, int fd) tio_error_print("Writing cancel to serial failed"); return ERR; } - return USER_CAN; + return ERR_USER_CAN; } - else if (rc == RX_IGNORE) + else if (err == RX_IGNORE) { status = ':'; } @@ -750,7 +815,7 @@ int xmodem_receive(int sio, int fd) int xymodem_send(int sio, const char *filename, modem_mode_t mode) { size_t len; - int rc, fd; + int err, fd; struct stat stat; const uint8_t *buf; @@ -775,29 +840,15 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) key_hit = 0; if (mode == XMODEM_1K) { - rc = xmodem_1k(sio, buf, len, 1); + err = xmodem_send_1k(sio, buf, len, 1); } else if (mode == XMODEM_CRC) { - rc = xmodem(sio, buf, len); + err = xmodem_send_128b(sio, buf, len); } - else + else /* if (mode == YMODEM) */ { - /* Ymodem: hdr + file + fin */ - while (true) - { - char hdr[1024], *p; - - rc = -1; - if (strlen(filename) > 977) break; /* hdr block overrun */ - p = stpncpy(hdr, filename, 1024) + 1; - p += sprintf(p, "%ld %lo %o", len, stat.st_mtime, stat.st_mode); - - if (xmodem_1k(sio, hdr, p - hdr, 0) < 0) break; /* hdr with metadata */ - if (xmodem_1k(sio, buf, len, 1) < 0) break; /* xmodem file */ - if (xmodem_1k(sio, "", 1, 0) < 0) break; /* empty hdr = fin */ - rc = 0; break; - } + err = ymodem_send_1k(sio, buf, len, filename, &stat); } key_hit = 0xff; @@ -805,12 +856,12 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) tcflush(sio, TCIOFLUSH); munmap((void *)buf, len); close(fd); - return rc; + return err; } int xymodem_receive(int sio, const char *filename, modem_mode_t mode) { - int rc, fd; + int err, fd; /* Create new file */ fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0664); @@ -825,21 +876,21 @@ int xymodem_receive(int sio, const char *filename, modem_mode_t mode) if (mode == XMODEM_1K) { tio_error_print("Not supported"); - rc = -1; + err = ERR; } else if (mode == XMODEM_CRC) { - rc = xmodem_receive(sio, fd); + err = xmodem_receive(sio, fd); } else { tio_error_print("Not supported"); - rc = -1; + err = ERR; } key_hit = 0xff; /* Flush serial and release resources */ tcflush(sio, TCIOFLUSH); close(fd); - return rc; + return err; } From e458b02771b02fe9768c3cd3fda618bd802d047b Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 16 Aug 2025 10:52:31 +0900 Subject: [PATCH 05/53] Add Xmodem-SUM support. --- src/script.c | 7 ++ src/tty.c | 30 ++++++++ src/xymodem.c | 206 ++++++++++++++++++++++++++++++++++++-------------- src/xymodem.h | 1 + 4 files changed, 189 insertions(+), 55 deletions(-) diff --git a/src/script.c b/src/script.c index e050af6..cb104bd 100644 --- a/src/script.c +++ b/src/script.c @@ -241,6 +241,12 @@ static int api_send(lua_State *L) tio_printf("%s", ret < 0 ? "Aborted" : "Done"); break; + case XMODEM_SUM: + tio_printf("Sending file '%s' using XMODEM-SUM", file); + ret = xymodem_send(device_fd, file, XMODEM_SUM); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + break; + case YMODEM: tio_printf("Sending file '%s' using YMODEM", file); ret = xymodem_send(device_fd, file, YMODEM); @@ -486,6 +492,7 @@ static void script_set_globals(lua_State *L) script_set_global(L, "toggle", 2); script_set_global(L, "high", 1); script_set_global(L, "low", 0); + script_set_global(L, "XMODEM_SUM", XMODEM_SUM); script_set_global(L, "XMODEM_CRC", XMODEM_CRC); script_set_global(L, "XMODEM_1K", XMODEM_1K); script_set_global(L, "YMODEM", YMODEM); diff --git a/src/tty.c b/src/tty.c index 70c5277..d59c7c1 100644 --- a/src/tty.c +++ b/src/tty.c @@ -767,6 +767,34 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) } break; + case KEY_3: + tio_printf("Send file with XMODEM-SUM"); + tio_printf_raw("Enter file name: "); + if (tio_readln()) + { + int ret; + + tio_printf("Sending file '%s' ", line); + tio_printf("Press any key to abort transfer"); + ret = xymodem_send(device_fd, line, XMODEM_SUM); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + + case KEY_4: + tio_printf("Receive file with XMODEM-SUM"); + tio_printf_raw("Enter file name: "); + if (tio_readln()) + { + int ret; + + tio_printf("Ready to receiving file '%s' ", line); + tio_printf("Press any key to abort transfer"); + ret = xymodem_receive(device_fd, line, XMODEM_SUM); + tio_printf("%s", ret < 0 ? "Aborted" : "Done"); + } + break; + default: tio_error_print("Invalid protocol option"); break; @@ -1127,6 +1155,8 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" (0) XMODEM-1K send"); tio_printf(" (1) XMODEM-CRC send"); tio_printf(" (2) XMODEM-CRC receive"); + tio_printf(" (3) XMODEM-SUM send"); + tio_printf(" (4) XMODEM-SUM receive"); // Process next input character as sub command sub_command = SUBCOMMAND_XMODEM; break; diff --git a/src/xymodem.c b/src/xymodem.c index cbc7834..5190e40 100644 --- a/src/xymodem.c +++ b/src/xymodem.c @@ -54,6 +54,11 @@ struct xpkt_ftr_crc uint8_t crc_lo; } __attribute__((packed)); +struct xpkt_ftr_sum +{ + uint8_t cksum; +} __attribute__((packed)); + struct xpacket_1k { struct xpkt_hdr hdr; @@ -61,13 +66,20 @@ struct xpacket_1k struct xpkt_ftr_crc ftr; } __attribute__((packed)); -struct xpacket_128b +struct xpacket_128b_crc { struct xpkt_hdr hdr; uint8_t data[128]; struct xpkt_ftr_crc ftr; } __attribute__((packed)); +struct xpacket_128b_sum +{ + struct xpkt_hdr hdr; + uint8_t data[128]; + struct xpkt_ftr_sum ftr; +} __attribute__((packed)); + /* See https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks */ static uint16_t calculate_crc16(const uint8_t *data, uint16_t size) { @@ -101,6 +113,15 @@ static uint16_t update_crc16(uint16_t crc, char data_char) return crc; } +static uint8_t calculate_cksum8(const uint8_t *data, uint16_t size) +{ + uint8_t sum; + for (sum = 0; size > 0; size--) { + sum += *data++; + } + return sum; +} + /* * Drain pending characters from serial line. Insist on the * last drained character being initial character 'C':CRC,1K @@ -303,40 +324,75 @@ static int xmodem_send_1k(int sio, const void *data, size_t len, int seq) return OK; } -static int xmodem_send_128b(int sio, const void *data, size_t len) +static int xmodem_send_128b(int sio, const void *data, size_t len, bool use_crc) { - struct xpacket_128b packet; + union + { + struct xpacket_128b_crc c; + struct xpacket_128b_sum s; + } packet; const uint8_t *buf = data; char resp = 0, tmo_resp; int rc, crc, err; /* Drain pending characters from serial line. Insist on the last drained character being 'C' */ - err = xmsend_initial_handshake(sio, 'C'); + if (use_crc) + { + err = xmsend_initial_handshake(sio, 'C'); + } + else + { + err = xmsend_initial_handshake(sio, NAK); + } if (err != OK) return err; /* Always work with 128b packets */ - packet.hdr.seq = 1; - packet.hdr.type = SOH; + if (use_crc) + { + packet.c.hdr.seq = 1; + packet.c.hdr.type = SOH; + } + else + { + packet.s.hdr.seq = 1; + packet.s.hdr.type = SOH; + } 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 = calculate_crc16(packet.data, sizeof(packet.data)); - packet.ftr.crc_hi = crc >> 8; - packet.ftr.crc_lo = crc; - packet.hdr.nseq = 0xff - packet.hdr.seq; + if (use_crc) + { + /* Build next packet, pad with 0 to full seq */ + z = min(len, sizeof(packet.c.data)); + memcpy(packet.c.data, buf, z); + memset(packet.c.data + z, 0, sizeof(packet.c.data) - z); + crc = calculate_crc16(packet.c.data, sizeof(packet.c.data)); + packet.c.ftr.crc_hi = crc >> 8; + packet.c.ftr.crc_lo = crc; + packet.c.hdr.nseq = 0xff - packet.c.hdr.seq; + + from = (char *) &packet.c; + sz = sizeof(packet.c); + } + else + { + /* Build next packet, pad with 0 to full seq */ + z = min(len, sizeof(packet.s.data)); + memcpy(packet.s.data, buf, z); + memset(packet.s.data + z, 0, sizeof(packet.s.data) - z); + packet.s.ftr.cksum = calculate_cksum8(packet.s.data, sizeof(packet.s.data)); + packet.s.hdr.nseq = 0xff - packet.s.hdr.seq; + + from = (char *) &packet.s; + sz = sizeof(packet.s); + } /* Send packet */ - from = (char *) &packet; - sz = sizeof(packet); while (sz) { if (key_hit) @@ -380,7 +436,11 @@ static int xmodem_send_128b(int sio, const void *data, size_t len) /* Move to next block after ACK */ if (resp == ACK) { - packet.hdr.seq++; + if (use_crc) + packet.c.hdr.seq++; + else + packet.s.hdr.seq++; + len -= z; buf += z; } @@ -450,7 +510,7 @@ static int xmrecv_drain_pending_chars(int sio) /* * Start Receive */ -static int xmrecv_start_receive(int sio) +static int xmrecv_start_receive(int sio, bool use_crc) { int rc; struct pollfd fds; @@ -463,7 +523,11 @@ static int xmrecv_start_receive(int sio) something. The start character will be sent once a second for a number of seconds. If nothing is received in that time then return false to indicate that the transfer did not start. */ - rc = write(sio, "C", 1); + if (use_crc) + rc = write(sio, "C", 1); + else + rc = write(sio, NAK_STR, 1); + if (rc < 0) { if (errno == EWOULDBLOCK) @@ -501,7 +565,7 @@ static int xmrecv_start_receive(int sio) /* * Receive a packet */ -static int xmrecv_receive_packet(int sio, struct xpacket_128b *packet, int fd) +static int xmrecv_receive_packet(int sio, struct xpacket_128b_crc *packet, int fd, bool use_crc) { char rxSeq1, rxSeq2 = 0; char resp = 0; @@ -566,43 +630,67 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b *packet, int fd) return ERR_FATAL; } packet->data[ix] = (uint8_t) resp; - calcCrc = update_crc16(calcCrc, resp); + if (use_crc) + calcCrc = update_crc16(calcCrc, resp); + else + calcCrc = (calcCrc + (uint8_t)resp) & 0xff; + if (key_hit) return ERR_USER_CAN; } - /* Read CRC */ - rc = read_poll(sio, &resp, 1, 3000); - if (rc == 0) + if (use_crc) { - tio_error_print("Timeout waiting for first CRC byte"); - return ERR_TMO; - } - else if (rc < 0) - { - tio_error_print("Error reading first CRC byte"); - return ERR_FATAL; - } + /* Read CRC */ + rc = read_poll(sio, &resp, 1, 3000); + if (rc == 0) + { + tio_error_print("Timeout waiting for first CRC byte"); + return ERR_TMO; + } + else if (rc < 0) + { + tio_error_print("Error reading first CRC byte"); + return ERR_FATAL; + } - uint8_t uresp = resp; - uint16_t uresp16 = uresp; - rxCrc = uresp16 << 8; + uint8_t uresp = resp; + uint16_t uresp16 = uresp; + rxCrc = uresp16 << 8; - rc = read_poll(sio, &resp, 1, 3000); - if (rc == 0) - { - tio_error_print("Timeout waiting for second CRC byte"); - return ERR_TMO; - } - else if (rc < 0) - { - tio_error_print("Error reading second CRC byte"); - return ERR_FATAL; - } + rc = read_poll(sio, &resp, 1, 3000); + if (rc == 0) + { + tio_error_print("Timeout waiting for second CRC byte"); + return ERR_TMO; + } + else if (rc < 0) + { + tio_error_print("Error reading second CRC byte"); + return ERR_FATAL; + } - uresp = resp; - uresp16 = uresp; - rxCrc |= uresp16; + uresp = resp; + uresp16 = uresp; + rxCrc |= uresp16; + } + else + { + /* Read checksum */ + rc = read_poll(sio, &resp, 1, 3000); + if (rc == 0) + { + tio_error_print("Timeout waiting for checksum byte"); + return ERR_TMO; + } + else if (rc < 0) + { + tio_error_print("Error reading checksum byte"); + return ERR_FATAL; + } + + rxCrc = (uint8_t)resp; + } if (key_hit) return ERR_USER_CAN; @@ -689,9 +777,9 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b *packet, int fd) return OK; } -int xmodem_receive(int sio, int fd) +int xmodem_receive(int sio, int fd, bool use_crc) { - struct xpacket_128b packet; + struct xpacket_128b_crc packet; char resp = 0; int rc, err; bool complete = false; @@ -709,7 +797,7 @@ int xmodem_receive(int sio, int fd) packet.hdr.type = SOH; /* Start Receive*/ - err = xmrecv_start_receive(sio); + err = xmrecv_start_receive(sio, use_crc); if (err != OK) { if (err == ERR_TMO) @@ -744,7 +832,7 @@ int xmodem_receive(int sio, int fd) { case SOH: /* Start of a packet */ - err = xmrecv_receive_packet(sio, &packet, fd); + err = xmrecv_receive_packet(sio, &packet, fd, use_crc); if (err == OK) { packet.hdr.seq++; @@ -844,7 +932,11 @@ int xymodem_send(int sio, const char *filename, modem_mode_t mode) } else if (mode == XMODEM_CRC) { - err = xmodem_send_128b(sio, buf, len); + err = xmodem_send_128b(sio, buf, len, /* use_crc= */ true); + } + else if (mode == XMODEM_SUM) + { + err = xmodem_send_128b(sio, buf, len, /* use_crc= */ false); } else /* if (mode == YMODEM) */ { @@ -880,7 +972,11 @@ int xymodem_receive(int sio, const char *filename, modem_mode_t mode) } else if (mode == XMODEM_CRC) { - err = xmodem_receive(sio, fd); + err = xmodem_receive(sio, fd, /* use_crc= */ true); + } + else if (mode == XMODEM_SUM) + { + err = xmodem_receive(sio, fd, /* use_crc= */ false); } else { diff --git a/src/xymodem.h b/src/xymodem.h index b5d2b8d..ed239be 100644 --- a/src/xymodem.h +++ b/src/xymodem.h @@ -25,6 +25,7 @@ typedef enum { XMODEM_1K, XMODEM_CRC, + XMODEM_SUM, YMODEM, } modem_mode_t; From 9784d3c277fb0906f9b94e0819ecf58379767a98 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 16 Aug 2025 18:05:56 +0900 Subject: [PATCH 06/53] Fix compile error in MSYS2/Cygwin --- src/tty.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tty.c b/src/tty.c index d59c7c1..6cc0ab0 100644 --- a/src/tty.c +++ b/src/tty.c @@ -2401,7 +2401,7 @@ void tty_wait_for_device(void) // Happens when port unpluged if (errno == EACCES) { - goto error; + break; } #elif defined(__APPLE__) if (errno == EBADF) From 3922ce3447f2885592da596fae8c652655178811 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 20 Sep 2025 10:19:53 +0900 Subject: [PATCH 07/53] Fix output-line-delay behavior If output-line-delay is non-zero and output-delay is any, line delay time set output-line-delay. If output-line-delay is zero and output-delay is non-zero, line delay time set output-delay. It was previously (output-line-delay + output-delay) in any case. Also, since the buffer was flushed after the line delay, there was a possibility that the delay would be reduced or not applied at all. --- src/tty.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tty.c b/src/tty.c index 6cc0ab0..b5e8c10 100644 --- a/src/tty.c +++ b/src/tty.c @@ -283,15 +283,14 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) } bytes_written += retval; + fsync(fd); + tcdrain(fd); + if (option.output_line_delay && *(unsigned char*)buffer == '\n') { delay(option.output_line_delay); } - - fsync(fd); - tcdrain(fd); - - if (option.output_delay) + else if (option.output_delay) { delay(option.output_delay); } From 6a590bd598379192d47021d5378c4967f78a2e0a Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 20 Sep 2025 10:56:11 +0900 Subject: [PATCH 08/53] Fix output-delay and input-mode line combination bug --- src/tty.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tty.c b/src/tty.c index b5e8c10..3dd99ae 100644 --- a/src/tty.c +++ b/src/tty.c @@ -259,22 +259,23 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) { ssize_t retval = 0, bytes_written = 0; size_t i; + unsigned char *cbuf = (unsigned char *)buffer; if (option.map_o_ltu) { // Convert lower case to upper case - for (i = 0; i Date: Tue, 23 Sep 2025 19:15:30 +0900 Subject: [PATCH 09/53] Add Lua API twrite The twrite Lua API is a variation of the write API, with the output character mapping and output-delay / output-line-delay features enabled. Since it processes each character internally, writing speed is slower than the write API. ~~~~~~~~~~~~~~~~~~~~~~~ twrite(string) Translate string with output character mapping and write the translated string to serial device with output-delay / output-line-delay. --- src/script.c | 18 ++++++++++++++++++ src/tty.h | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/script.c b/src/script.c index cb104bd..998c085 100644 --- a/src/script.c +++ b/src/script.c @@ -286,6 +286,23 @@ static int api_write(lua_State *L) return 1; } +// lua: tio.twrite(string) +static int api_twrite(lua_State *L) +{ + size_t len = 0; + const char *string = luaL_checklstring(L, 1, &len); + + for (; len > 0; --len, string++) + { + forward_to_tty(device_fd, *string); + } + tty_sync(device_fd); + + lua_getglobal(L, "tio"); + + return 1; +} + // lua: tio.read(size, timeout) static int api_read(lua_State *L) { @@ -462,6 +479,7 @@ static const struct luaL_Reg tio_lib[] = { "line_set", line_set}, { "send", api_send}, { "write", api_write}, + { "twrite", api_twrite}, { "read", api_read}, { "readline", api_readline}, { "ttysearch", api_ttysearch}, diff --git a/src/tty.h b/src/tty.h index 39c64af..eb3dfd4 100644 --- a/src/tty.h +++ b/src/tty.h @@ -84,3 +84,5 @@ void tty_input_thread_wait_ready(void); void tty_line_set(int fd, tty_line_config_t line_config[]); void tty_search(void); GList *tty_search_for_serial_devices(void); +void forward_to_tty(int fd, char output_char); +void tty_sync(int fd); From e62a23f32008248f3353657a8872379d7c38fd54 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 5 Oct 2025 15:25:47 +0900 Subject: [PATCH 10/53] Add option '--output-line-delay-char cr|lf' --- src/configfile.c | 7 +++++++ src/options.c | 28 ++++++++++++++++++++++++++++ src/options.h | 2 ++ src/tty.c | 2 +- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/configfile.c b/src/configfile.c index ca116bf..0fc230c 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -171,6 +171,13 @@ static void config_parse_keys(GKeyFile *key_file, char *group) config_get_integer(key_file, group, "output-delay", &option.output_delay, 0, INT_MAX); config_get_integer(key_file, group, "output-line-delay", &option.output_line_delay, 0, INT_MAX); + config_get_string(key_file, group, "output-line-delay-char", &string, NULL); + if (string != NULL) + { + option_parse_output_line_delay_char(string); + g_free((void *)string); + string = NULL; + } config_get_string(key_file, group, "line-pulse-duration", &string, NULL); if (string != NULL) { diff --git a/src/options.c b/src/options.c index beddd90..09f35e8 100644 --- a/src/options.c +++ b/src/options.c @@ -44,6 +44,7 @@ enum opt_t OPT_LOG_DIRECTORY, OPT_LOG_STRIP, OPT_LOG_APPEND, + OPT_OUTPUT_LINE_DELAY_CHAR, OPT_LINE_PULSE_DURATION, OPT_RS485, OPT_RS485_CONFIG, @@ -73,6 +74,7 @@ struct option_t option = .parity = PARITY_NONE, .output_delay = 0, .output_line_delay = 0, + .output_line_delay_char = '\n', .dtr_pulse_duration = 100, .rts_pulse_duration = 100, .cts_pulse_duration = 100, @@ -144,6 +146,7 @@ void option_print_help(char *argv[]) printf(" -p, --parity odd|even|none|mark|space Parity (default: none)\n"); printf(" -o, --output-delay Output character delay (default: 0)\n"); printf(" -O, --output-line-delay Output line delay (default: 0)\n"); + printf(" --output-line-delay-char cr|lf Output line delay character(default: lf)\n"); printf(" --line-pulse-duration Set line pulse duration\n"); printf(" -a, --auto-connect new|latest|direct Automatic connect strategy (default: direct)\n"); printf(" --exclude-devices Exclude devices by pattern\n"); @@ -504,6 +507,25 @@ void option_parse_auto_connect(const char *arg, auto_connect_t *auto_connect) } } +void option_parse_output_line_delay_char(const char *arg) +{ + assert(arg != NULL); + + if (strcmp("cr", arg) == 0) + { + option.output_line_delay_char = '\r'; + } + else if (strcmp("lf", arg) == 0) + { + option.output_line_delay_char = '\n'; + } + else + { + tio_error_print("Invalid char '%s'", arg); + exit(EXIT_FAILURE); + } +} + void option_parse_line_pulse_duration(const char *arg) { bool token_found = true; @@ -833,6 +855,7 @@ void options_print() tio_printf(" Timestamp timeout: %u", option.timestamp_timeout); tio_printf(" Output delay: %d", option.output_delay); tio_printf(" Output line delay: %d", option.output_line_delay); + tio_printf(" Output line delay char: %s", option.output_line_delay_char == '\r' ? "cr" : "lf"); tio_printf(" Automatic connect strategy: %s", option_auto_connect_state_to_string(option.auto_connect)); tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "true" : "false"); // clang-format off @@ -903,6 +926,7 @@ void options_parse(int argc, char *argv[]) {"parity", required_argument, 0, 'p' }, {"output-delay", required_argument, 0, 'o' }, {"output-line-delay" , required_argument, 0, 'O' }, + {"output-line-delay-char", required_argument, 0, OPT_OUTPUT_LINE_DELAY_CHAR}, {"line-pulse-duration", required_argument, 0, OPT_LINE_PULSE_DURATION }, {"auto-connect", required_argument, 0, 'a' }, {"exclude-devices", required_argument, 0, OPT_EXCLUDE_DEVICES }, @@ -989,6 +1013,10 @@ void options_parse(int argc, char *argv[]) option_string_to_integer(optarg, &option.output_line_delay, "output line delay", 0, INT_MAX); break; + case OPT_OUTPUT_LINE_DELAY_CHAR: + option_parse_output_line_delay_char(optarg); + break; + case OPT_LINE_PULSE_DURATION: option_parse_line_pulse_duration(optarg); break; diff --git a/src/options.h b/src/options.h index c552217..5388de2 100644 --- a/src/options.h +++ b/src/options.h @@ -54,6 +54,7 @@ struct option_t parity_t parity; int output_delay; int output_line_delay; + char output_line_delay_char; int dtr_pulse_duration; int rts_pulse_duration; int cts_pulse_duration; @@ -122,6 +123,7 @@ void option_parse_parity(const char *arg, parity_t *parity); void option_parse_output_mode(const char *arg, output_mode_t *mode); void option_parse_input_mode(const char *arg, input_mode_t *mode); +void option_parse_output_line_delay_char(const char *arg); void option_parse_line_pulse_duration(const char *arg); void option_parse_script_run(const char *arg, script_run_t *script_run); void option_parse_alert(const char *arg, alert_t *alert); diff --git a/src/tty.c b/src/tty.c index 3dd99ae..08abf41 100644 --- a/src/tty.c +++ b/src/tty.c @@ -287,7 +287,7 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) fsync(fd); tcdrain(fd); - if (option.output_line_delay && cbuf[i] == '\n') + if (option.output_line_delay && cbuf[i] == option.output_line_delay_char) { delay(option.output_line_delay); } From 58aae5511f77f90eee5372d82bb5c097ee63ad53 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 5 Oct 2025 18:52:49 +0900 Subject: [PATCH 11/53] Add --script function to key command ctrl-t r "Run script" If filename starts with '!', do filename's remain parts as lua commands. --- src/script.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/script.c b/src/script.c index 998c085..76b5933 100644 --- a/src/script.c +++ b/src/script.c @@ -549,7 +549,15 @@ void script_run(int fd, const char *script_filename) if (script_filename != NULL) { tio_printf("Running script %s", script_filename); - script_file_run(L, script_filename); + // if filename starts with '!', do filename's remain parts as lua commands. + if (strlen(script_filename) >= 1 && script_filename[0] == '!') + { + script_buffer_run(L, &script_filename[1]); + } + else + { + script_file_run(L, script_filename); + } } else if (option.script_filename != NULL) { From d1ff5f71424e1901809c0256b635201e56c0aea8 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 5 Oct 2025 20:43:06 +0900 Subject: [PATCH 12/53] Add multipul patterns function to Lua API tio.expect() Lua regular expressions do not have an OR specification, so the current tio.expect() cannot wait until any of multiple patterns match. This will cause OR waiting behavior when a table of multiple regular expressions is passed as a pattern. If you pass a string as a pattern, it will treats a table has single regular expression. and this will change return values. 1st: table of captures if one of the patterns matched nil if any pattern unmatched and timeout. 2nd: whole input strings 3rd: index of the matched pattern. 0 if unmatched. For example, captures, whole, idx = tio.expect( {"\nOK", "\nERROR"} ) captures, whole, idx = tio.expect( "\nOK" ) --- src/script.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/script.c b/src/script.c index 76b5933..9cb5baa 100644 --- a/src/script.c +++ b/src/script.c @@ -58,15 +58,22 @@ static char script_init[] = "end\n" "tio.expect = function(pattern, timeout)\n" " local str = ''\n" +" if type(pattern) ~= 'table' then\n" +" pattern = { pattern }\n" +" end\n" " while true do\n" +" local idx, pat\n" " local c = tio.read(1, timeout)\n" " if c then\n" " str = str .. c\n" -" if string.match(str, pattern) then\n" -" return string.match(str, pattern)\n" +" for idx, pat in ipairs(pattern) do\n" +" local matched = { string.match(str, pat) }\n" +" if matched[1] then\n" +" return matched, str, idx\n" +" end\n" " end\n" " else\n" -" return nil, str\n" +" return nil, str, 0\n" " end\n" " end\n" "end\n" From 67bd6b1a8c7329c500c11173c34049cd70d87318 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 26 Oct 2025 18:50:38 +0900 Subject: [PATCH 13/53] Fix map option's input/output description of tio man page. --- man/tio.1.in | 4 ++-- man/tio.1.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/man/tio.1.in b/man/tio.1.in index eb0338d..aaaf8a5 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -205,8 +205,8 @@ Strip control characters and escape sequences from log. .TP .BR \-m ", " "\-\-map " \fI -Map (replace, translate) characters on input to the serial device or output -from the serial device. The following mapping flags are supported: +Map (replace, translate) characters on input from the serial device or output +to the serial device. The following mapping flags are supported: .RS .TP 12n diff --git a/man/tio.1.txt b/man/tio.1.txt index eeac82c..46afb5e 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -158,7 +158,7 @@ OPTIONS -m, --map - Map (replace, translate) characters on input to the serial device or output from the serial device. The following mapping flags are supported: + Map (replace, translate) characters on input from the serial device or output to the serial device. The following mapping flags are supported: ICRNL Map CR to NL on input (unless IGNCR is set) From b5944275d31172f1bb59b7f768ad8385a8c3b914 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 2 Nov 2025 09:50:44 +0900 Subject: [PATCH 14/53] Revert "Add multipul patterns function to Lua API tio.expect()" This reverts commit d1ff5f71424e1901809c0256b635201e56c0aea8. --- src/script.c | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/script.c b/src/script.c index 9cb5baa..76b5933 100644 --- a/src/script.c +++ b/src/script.c @@ -58,22 +58,15 @@ static char script_init[] = "end\n" "tio.expect = function(pattern, timeout)\n" " local str = ''\n" -" if type(pattern) ~= 'table' then\n" -" pattern = { pattern }\n" -" end\n" " while true do\n" -" local idx, pat\n" " local c = tio.read(1, timeout)\n" " if c then\n" " str = str .. c\n" -" for idx, pat in ipairs(pattern) do\n" -" local matched = { string.match(str, pat) }\n" -" if matched[1] then\n" -" return matched, str, idx\n" -" end\n" +" if string.match(str, pattern) then\n" +" return string.match(str, pattern)\n" " end\n" " else\n" -" return nil, str, 0\n" +" return nil, str\n" " end\n" " end\n" "end\n" From 56c378f5f9a3389ed3e7184f1d7d2b92758bbbb8 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 2 Nov 2025 10:15:16 +0900 Subject: [PATCH 15/53] Add multipul patterns function tio.expects() to Lua API Add tio.expects() which has a table of patterns in the arguments to support "OR" pattern waiting. The return values of tio.expects() are: 1st: index of the matched pattern in the table. nil if unmatched. 2nd: a table of captures if one of the patterns are matched. nil if any of the patterns are unmatched and timeout. 3rd: all received strings in tio.expects() to treat the strings after match. Add all received string to tio.expect()'s return values too. For example, idx, captures, all_received = tio.expects( {"\nOK", "\nERROR"}, 1000 ) captures, all_received = tio.expect( "\nPROMPT>", 1000 ) --- src/script.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/script.c b/src/script.c index 76b5933..13d20da 100644 --- a/src/script.c +++ b/src/script.c @@ -60,14 +60,32 @@ static char script_init[] = " local str = ''\n" " while true do\n" " local c = tio.read(1, timeout)\n" -" if c then\n" -" str = str .. c\n" -" if string.match(str, pattern) then\n" -" return string.match(str, pattern)\n" -" end\n" -" else\n" +" if c == nil then\n" " return nil, str\n" " end\n" +" str = str .. c\n" +" if string.match(str, pattern) then\n" +" return string.match(str, pattern), str\n" +" end\n" +" end\n" +"end\n" +"tio.expects = function(patterns, timeout)\n" +" local str = ''\n" +" if type(patterns) ~= 'table' then\n" +" patterns = { patterns }\n" +" end\n" +" while true do\n" +" local c = tio.read(1, timeout)\n" +" if c == nil then\n" +" return nil, nil, str\n" +" end\n" +" str = str .. c\n" +" for idx, pat in ipairs(patterns) do\n" +" local captured = { string.match(str, pat) }\n" +" if #captured > 0 then\n" +" return idx, captured, str\n" +" end\n" +" end\n" " end\n" "end\n" "tio.alwaysecho = true\n" From 8e02cded171d8de766810d297ea2c87a2f694da5 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 15 Nov 2025 22:46:54 +0900 Subject: [PATCH 16/53] Change the Lua API timeout specification to allow for non-blocking reads. Change timeout argument of tio.read / readline / expect[s] to be similar to the timeout specification for poll(2) and LuaSocket. If timeout is negative value or not provided or nil, it will wait indefinitely until data is ready to read. If timeout is 0, it will read data that has arrived with non-blocking. Prior to this fix, non-blocking reads could not be specified. --- src/script.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/script.c b/src/script.c index 13d20da..e5b2f4f 100644 --- a/src/script.c +++ b/src/script.c @@ -325,12 +325,7 @@ static int api_twrite(lua_State *L) static int api_read(lua_State *L) { int size = luaL_checkinteger(L, 1); - int timeout = lua_tointeger(L, 2); - - if (timeout == 0) - { - timeout = -1; // Wait forever - } + int timeout = luaL_optinteger(L, 2, -1); // ms, negative value means forever. luaL_Buffer buffer; luaL_buffinit(L, &buffer); @@ -367,15 +362,10 @@ static int api_read(lua_State *L) // lua: string = tio.readline(timeout) static int api_readline(lua_State *L) { - int timeout = lua_tointeger(L, 1); //ms + int timeout = luaL_optinteger(L, 1, -1); // ms, negative value means forever. luaL_Buffer b; char ch; - if (timeout == 0) - { - timeout = -1; // Wait forever - } - luaL_buffinit(L, &b); luaL_prepbuffer(&b); while (true) From 8a1b739ae60cdba41b7d553efd4aef6ca5c01a3e Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 15 Nov 2025 23:26:43 +0900 Subject: [PATCH 17/53] Add cleanup read to Lua API tio.expect[s] tio.expect[s]() performs pattern matching for each character received, but there are cases where the character reception speed exceeds the pattern matching speed, resulting in characters being missed. Add non-blocking reads to read all characters that have been received at that time. --- src/script.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/script.c b/src/script.c index e5b2f4f..74419ef 100644 --- a/src/script.c +++ b/src/script.c @@ -56,16 +56,22 @@ static char script_init[] = " local ri = arg.RI or -1\n" " tio.line_set(dtr, rts, cts, dsr, cd, ri)\n" "end\n" +"tio.EXPECT_CLEANUP_READ_SIZE = 4096\n" "tio.expect = function(pattern, timeout)\n" " local str = ''\n" " while true do\n" -" local c = tio.read(1, timeout)\n" -" if c == nil then\n" -" return nil, str\n" +" local astr = tio.read(tio.EXPECT_CLEANUP_READ_SIZE, 0)\n" +" local c = nil\n" +" if astr == nil then\n" +" c = tio.read(1, timeout)\n" +" if c == nil then\n" +" return nil, str\n" +" end\n" " end\n" -" str = str .. c\n" -" if string.match(str, pattern) then\n" -" return string.match(str, pattern), str\n" +" str = table.concat{str, astr or '', c or ''}\n" +" local captured = { string.match(str, pattern) }\n" +" if #captured > 0 then\n" +" return table.unpack(captured), str\n" " end\n" " end\n" "end\n" @@ -75,11 +81,15 @@ static char script_init[] = " patterns = { patterns }\n" " end\n" " while true do\n" -" local c = tio.read(1, timeout)\n" -" if c == nil then\n" -" return nil, nil, str\n" +" local astr = tio.read(tio.EXPECT_CLEANUP_READ_SIZE, 0)\n" +" local c = nil\n" +" if astr == nil then\n" +" c = tio.read(1, timeout)\n" +" if c == nil then\n" +" return nil, nil, str\n" +" end\n" " end\n" -" str = str .. c\n" +" str = table.concat{str, astr or '', c or ''}\n" " for idx, pat in ipairs(patterns) do\n" " local captured = { string.match(str, pat) }\n" " if #captured > 0 then\n" From 0983ce6eebc386fd282e3c32a56ecdac0258ac92 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 29 Nov 2025 16:41:04 +0900 Subject: [PATCH 18/53] Add history/line-edit function to Ctrl-t command string input Ctrl-t r/R/x/y commands require string input. To reduce the hassle of repeatedly entering this information, I apply readline functions (line-editing and history) to the string input. Now tio have two histories: one for the line input-mode and one for the ctrl-t command's string input. And the following changes are made to the line input: - Add prompts. "> ": line input-mode, ">> " Ctrl-t string input. - Omits duplicate lines from the history. To implement the above functionality, we will modify the readline functions to use the multi-instance style. --- src/readline.c | 267 +++++++++++++++++++++++++++++-------------------- src/readline.h | 14 ++- src/tty.c | 74 +++++++------- 3 files changed, 204 insertions(+), 151 deletions(-) diff --git a/src/readline.c b/src/readline.c index 6cfae00..27125c6 100644 --- a/src/readline.c +++ b/src/readline.c @@ -19,85 +19,133 @@ * 02110-1301, USA. */ +#include "readline.h" #include "print.h" #include "misc.h" +#include #define RL_LINE_LENGTH_MAX PATH_MAX -#define RL_HISTORY_MAX 1000 -static char rl_line[RL_LINE_LENGTH_MAX] = {}; -static char *rl_history[RL_HISTORY_MAX]; -static int rl_history_count = 0; -static int rl_history_index = 0; -static int rl_line_length = 0; -static int rl_cursor_pos = 0; -static int rl_escape = 0; +typedef struct readline_s +{ + char line[RL_LINE_LENGTH_MAX]; + char *history[RL_HISTORY_MAX]; + char prompt[RL_PROMPT_LENGTH_MAX]; + int prompt_length; + int history_count; + int history_index; + int line_length; + int cursor_pos; + int escape; +} readline_t; -static void print_line(const char *string, int cursor_pos) +void print_line(readline_t *rl) { clear_line(); - print("%s", string); + print("%s%s", rl->prompt, rl->line); print("\r"); // Move the cursor back to the beginning - for (int i = 0; i < cursor_pos; ++i) + for (int i = 0; i < rl->prompt_length + rl->cursor_pos; ++i) { print("\x1b[C"); // Move the cursor right } } -void readline_init(void) +readline_t *readline_create(void) { - rl_history_count = 0; - rl_history_index = 0; + readline_t *rl = malloc(sizeof(readline_t)); + if (rl == NULL) + return NULL; + + readline_reinit(rl); + return rl; +} + +void readline_reinit(readline_t *rl) +{ + assert(rl != NULL); + + rl->prompt[0] = '\0'; + rl->prompt_length = 0; + + rl->history_count = 0; + rl->history_index = 0; for (int i = 0; i < RL_HISTORY_MAX; ++i) { - rl_history[i] = NULL; + rl->history[i] = NULL; } - rl_line[0] = 0; - rl_line_length = 0; - rl_cursor_pos = 0; - rl_escape = 0; + rl->line[0] = 0; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->escape = 0; } -char * readline_get(void) +void readline_set_prompt(readline_t *rl, const char *prompt) { - return rl_line; + strncpy(rl->prompt, prompt, RL_PROMPT_LENGTH_MAX - 1); + rl->prompt[RL_PROMPT_LENGTH_MAX - 1] = '\0'; + rl->prompt_length = strlen(rl->prompt); } -static void readline_input_char(char input_char) +char * readline_get(readline_t *rl) { - if (rl_line_length < RL_LINE_LENGTH_MAX - 1) + assert(rl != NULL); + return rl->line; +} + +void readline_prompt_for_input(readline_t *rl) +{ + assert(rl != NULL); + + rl->line[0] = 0; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->escape = 0; + print_line(rl); +} + +static void readline_input_char(readline_t *rl, char input_char) +{ + assert(rl != NULL); + + if (rl->line_length < RL_LINE_LENGTH_MAX - 1) { - memmove(&rl_line[rl_cursor_pos + 1], &rl_line[rl_cursor_pos], rl_line_length - rl_cursor_pos); - rl_line[rl_cursor_pos] = input_char; - rl_line_length++; - rl_cursor_pos++; - rl_line[rl_line_length] = '\0'; - print_line(rl_line, rl_cursor_pos); + memmove(&rl->line[rl->cursor_pos + 1], &rl->line[rl->cursor_pos], rl->line_length - rl->cursor_pos); + rl->line[rl->cursor_pos] = input_char; + rl->line_length++; + rl->cursor_pos++; + rl->line[rl->line_length] = '\0'; + print_line(rl); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_cr(void) +static void readline_input_cr(readline_t *rl) { - if (rl_line_length > 0) + rl->line[rl->line_length] = '\0'; + + if (rl->line_length > 0) { - // Save to history - if (rl_history_count < RL_HISTORY_MAX) + // Different line only + if (rl->history_count == 0 || + (rl->history_count > 0 && strncmp(rl->history[rl->history_count - 1], rl->line, rl->line_length) != 0)) { - rl_history[rl_history_count] = strndup(rl_line, rl_line_length); - rl_history_count++; - } - else - { - free(rl_history[0]); - memmove(&rl_history[0], &rl_history[1], (RL_HISTORY_MAX - 1) * sizeof(char*)); - rl_history[RL_HISTORY_MAX - 1] = strndup(rl_line, rl_line_length); + // Save to history + if (rl->history_count < RL_HISTORY_MAX) + { + rl->history[rl->history_count] = strndup(rl->line, rl->line_length); + rl->history_count++; + } + else + { + free(rl->history[0]); + memmove(&rl->history[0], &rl->history[1], (RL_HISTORY_MAX - 1) * sizeof(char*)); + rl->history[RL_HISTORY_MAX - 1] = strndup(rl->line, rl->line_length); + } } } - rl_line[rl_line_length] = '\0'; if (option.local_echo == false) { clear_line(); @@ -107,170 +155,171 @@ static void readline_input_cr(void) print("\r\n"); } - rl_line_length = 0; - rl_cursor_pos = 0; - rl_history_index = rl_history_count; - rl_escape = 0; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->history_index = rl->history_count; + rl->escape = 0; } -static void readline_input_bs(void) +static void readline_input_bs(readline_t *rl) { - if (rl_cursor_pos > 0) + if (rl->cursor_pos > 0) { - memmove(&rl_line[rl_cursor_pos - 1], &rl_line[rl_cursor_pos], rl_line_length - rl_cursor_pos); - rl_line_length--; - rl_cursor_pos--; - rl_line[rl_line_length] = '\0'; - print_line(rl_line, rl_cursor_pos); + memmove(&rl->line[rl->cursor_pos - 1], &rl->line[rl->cursor_pos], rl->line_length - rl->cursor_pos); + rl->line_length--; + rl->cursor_pos--; + rl->line[rl->line_length] = '\0'; + print_line(rl); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_escape(void) +static void readline_input_escape(readline_t *rl) { - rl_escape = 1; + rl->escape = 1; } -static void readline_input_left_bracket(void) +static void readline_input_left_bracket(readline_t *rl) { - if (rl_escape == 1) + if (rl->escape == 1) { - rl_escape = 2; + rl->escape = 2; } else { - rl_escape = 0; + readline_input_char(rl, '['); + rl->escape = 0; } } -static void readline_input_A(void) +static void readline_input_A(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Up arrow - if (rl_history_index > 0) + if (rl->history_index > 0) { - rl_history_index--; - strncpy(rl_line, rl_history[rl_history_index], RL_LINE_LENGTH_MAX-1); - rl_line_length = strlen(rl_line); - rl_cursor_pos = rl_line_length; - print_line(rl_line, rl_cursor_pos); + rl->history_index--; + strncpy(rl->line, rl->history[rl->history_index], RL_LINE_LENGTH_MAX-1); + rl->line_length = strlen(rl->line); + rl->cursor_pos = rl->line_length; + print_line(rl); } } else { - readline_input_char('A'); + readline_input_char(rl, 'A'); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_B(void) +static void readline_input_B(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Down arrow - if (rl_history_index < rl_history_count - 1) + if (rl->history_index < rl->history_count - 1) { - rl_history_index++; - strncpy(rl_line, rl_history[rl_history_index], RL_LINE_LENGTH_MAX-1); - rl_line_length = strlen(rl_line); - rl_cursor_pos = rl_line_length; - print_line(rl_line, rl_cursor_pos); + rl->history_index++; + strncpy(rl->line, rl->history[rl->history_index], RL_LINE_LENGTH_MAX-1); + rl->line_length = strlen(rl->line); + rl->cursor_pos = rl->line_length; + print_line(rl); } - else if (rl_history_index == rl_history_count - 1) + else if (rl->history_index == rl->history_count - 1) { - rl_history_index++; - rl_line_length = 0; - rl_cursor_pos = 0; - rl_line[rl_line_length] = '\0'; - print_line(rl_line, rl_cursor_pos); + rl->history_index++; + rl->line_length = 0; + rl->cursor_pos = 0; + rl->line[rl->line_length] = '\0'; + print_line(rl); } } else { - readline_input_char('B'); + readline_input_char(rl, 'B'); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_C(void) +static void readline_input_C(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Right arrow - if (rl_cursor_pos < rl_line_length) + if (rl->cursor_pos < rl->line_length) { - rl_cursor_pos++; + rl->cursor_pos++; print("\x1b[C"); } } else { - readline_input_char('C'); + readline_input_char(rl, 'C'); } - rl_escape = 0; + rl->escape = 0; } -static void readline_input_D(void) +static void readline_input_D(readline_t *rl) { - if (rl_escape == 2) + if (rl->escape == 2) { // Left arrow - if (rl_cursor_pos > 0) + if (rl->cursor_pos > 0) { - rl_cursor_pos--; + rl->cursor_pos--; print("\b"); } } else { - readline_input_char('D'); + readline_input_char(rl, 'D'); } - rl_escape = 0; + rl->escape = 0; } -void readline_input(char input_char) +void readline_input(readline_t *rl, char input_char) { switch (input_char) { case '\r': // Carriage return - readline_input_cr(); + readline_input_cr(rl); break; case 127: // Backspace - readline_input_bs(); + readline_input_bs(rl); break; case 27: // Escape - readline_input_escape(); + readline_input_escape(rl); break; case '[': - readline_input_left_bracket(); + readline_input_left_bracket(rl); break; case 'A': - readline_input_A(); + readline_input_A(rl); break; case 'B': - readline_input_B(); + readline_input_B(rl); break; case 'C': - readline_input_C(); + readline_input_C(rl); break; case 'D': - readline_input_D(); + readline_input_D(rl); break; default: - readline_input_char(input_char); + readline_input_char(rl, input_char); break; } } diff --git a/src/readline.h b/src/readline.h index 46c3f10..0c043b7 100644 --- a/src/readline.h +++ b/src/readline.h @@ -21,6 +21,14 @@ #pragma once -void readline_init(void); -void readline_input(char input_char); -char * readline_get(void); +#define RL_HISTORY_MAX 1000 +#define RL_PROMPT_LENGTH_MAX 16 + +typedef struct readline_s readline_t; + +readline_t *readline_create(void); +void readline_reinit(readline_t *rl); +void readline_set_prompt(readline_t *rl, const char *prompt); +void readline_prompt_for_input(readline_t *rl); +void readline_input(readline_t *rl, char input_char); +char *readline_get(readline_t *rl); diff --git a/src/tty.c b/src/tty.c index 08abf41..63f7054 100644 --- a/src/tty.c +++ b/src/tty.c @@ -185,6 +185,8 @@ static int pipefd[2]; static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER; static char line[PATH_MAX]; static size_t listing_device_name_length_max = 0; +static readline_t *readline_ctx = NULL; +static readline_t *subcmd_readline_ctx = NULL; static void optional_local_echo(char c) { @@ -589,31 +591,25 @@ static void tty_line_poke(int fd, int mask, tty_line_mode_t mode, unsigned int d } } -static int tio_readln(void) +static int tio_subcmd_readln(const char *title_prompt) { - char *p = line; + tio_printf_raw("%s\r\n", title_prompt); + readline_prompt_for_input(subcmd_readline_ctx); - /* Read line, accept BS and DEL as rubout characters */ - for (p = line ; p < &line[PATH_MAX-1]; ) + /* Read line with line edit and history. */ + char c; + while (true) { - if (read(pipefd[0], p, 1) > 0) + if (read(pipefd[0], &c, 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++; + readline_input(subcmd_readline_ctx, c); + if (c == '\r') break; } } - *p = 0; - return (p - line); + strncpy(line, readline_get(subcmd_readline_ctx), PATH_MAX - 1); + line[PATH_MAX - 1] = 0; + + return strlen(line); } void tty_output_mode_set(output_mode_t mode) @@ -727,8 +723,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) { case KEY_0: tio_printf("Send file with XMODEM-1K"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { int ret; @@ -741,8 +736,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_1: tio_printf("Send file with XMODEM-CRC"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { int ret; @@ -755,8 +749,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_2: tio_printf("Receive file with XMODEM-CRC"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { int ret; @@ -769,8 +762,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_3: tio_printf("Send file with XMODEM-SUM"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { int ret; @@ -783,8 +775,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_4: tio_printf("Receive file with XMODEM-SUM"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { int ret; @@ -1087,8 +1078,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_R: /* Run script */ tio_printf("Run Lua script"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { clear_line(); script_run(device_fd, line); @@ -1103,8 +1093,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_SHIFT_R: /* Execute shell command */ tio_printf("Execute shell command with I/O redirected to device"); - tio_printf_raw("Enter command: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter command: ")) execute_shell_command(device_fd, line); break; @@ -1163,8 +1152,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_Y: tio_printf("Send file with YMODEM"); - tio_printf_raw("Enter file name: "); - if (tio_readln()) + if (tio_subcmd_readln("Enter file name: ")) { int ret; @@ -2718,7 +2706,15 @@ int tty_connect(void) } // Initialize readline like history - readline_init(); + readline_ctx = readline_create(); + subcmd_readline_ctx = readline_create(); + if (readline_ctx == NULL || subcmd_readline_ctx == NULL) + { + tio_error_printf("Could not allocate readline buffer."); + exit(EXIT_FAILURE); + } + readline_set_prompt(readline_ctx, "> "); + readline_set_prompt(subcmd_readline_ctx, ">> "); /* Input loop */ while (true) @@ -2972,15 +2968,15 @@ int tty_connect(void) if (input_char == '\r') { // Carriage return - readline_input(input_char); + readline_input(readline_ctx, input_char); // Write current line to tty device - char *rl_line = readline_get(); + char *rl_line = readline_get(readline_ctx); tty_write(device_fd, rl_line, strlen(rl_line)); } else { - readline_input(input_char); + readline_input(readline_ctx, input_char); forward = false; } break; From a9f05a66f737bb183c8d039424466c910ffc3bbb Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 29 Nov 2025 18:03:30 +0900 Subject: [PATCH 19/53] Change Ctrl-t r lua funtcion to start with the previous call state When calling the Lua interpreter with Ctrl-t r, change to start with the previous call state. This allows you to use it like REPL: (example) Ctrl-t r : my_library.lua Ctrl-t r : !result1 = my_func("test1"); result2 = my_func("test2") Ctrl-t r : !if (result1 == result2) then print("OK") else print("NG") Ctrl-t r : @new When you enter strings using Ctrl-t r, the string is interpreted as follows: - If the string does not begin with either "!" or "@", the string is assumed to be the file name of a script and is executed. - If the string begins with "!", the string excluding the "!" is interpreted as Lua commands. - If the string begins with "@", Strings beginning with "@" are considered instructions to the interpreter. Currently valid instructions are: @new: Clear the Lua state. (== reset the Lua interpreter.) @doopt: Execute the Lua script action specified by the option that start with clearing the Lua state. @nuldo=opt: do @doopt action when an empty string is entered (default). @nuldo=none: Do nothing when an empty string is entered. And now, lua interpreter start with GC. If you need to stop GC, do lua function collectgarbage("stop"). --- src/script.c | 103 ++++++++++++++++++++++++++++++++++++++++++++------- src/script.h | 1 + src/tty.c | 15 ++------ 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/script.c b/src/script.c index 74419ef..13c5048 100644 --- a/src/script.c +++ b/src/script.c @@ -29,6 +29,7 @@ #include #include #include +#include #include "misc.h" #include "print.h" #include "options.h" @@ -44,6 +45,7 @@ #define READ_LINE_SIZE 4096 // read_line buffer length static int device_fd; +static lua_State *script_interp = NULL; // clang-format off static char script_init[] = @@ -542,14 +544,26 @@ static int luaopen_tio(lua_State *L) } #endif -void script_run(int fd, const char *script_filename) +static lua_State *script_interp_new(void) { lua_State *L; - device_fd = fd; + if (script_interp != NULL) { + lua_close(script_interp); + } L = luaL_newstate(); + script_interp = L; + + if (L == NULL) { + tio_error_printf("Can't allocate script buffer"); + return NULL; + } + + lua_gc(L, LUA_GCSTOP); luaL_openlibs(L); + lua_gc(L, LUA_GCRESTART); + lua_gc(L, LUA_GCGEN, 0, 0); #if LUA_VERSION_NUM >= 502 luaL_requiref(L, "tio", luaopen_tio, 1); @@ -564,31 +578,94 @@ void script_run(int fd, const char *script_filename) // Initialize globals script_set_globals(L); - if (script_filename != NULL) + return L; +} + +void script_run(int fd, const char *script_filename) +{ + static bool doopt_by_nul = true; + device_fd = fd; + + assert(script_filename != NULL); + + if (script_interp == NULL) { - tio_printf("Running script %s", script_filename); - // if filename starts with '!', do filename's remain parts as lua commands. - if (strlen(script_filename) >= 1 && script_filename[0] == '!') + if (script_interp_new() == NULL) + return; + } + + if (script_filename[0] == '\0') + { + if (doopt_by_nul) { - script_buffer_run(L, &script_filename[1]); + script_run_as_specified_by_options(fd); + } + return; + } + else if (script_filename[0] == '@') + { + if (strcmp(script_filename, "@new") == 0) + { + tio_printf("Restart interpreter"); + script_interp_new(); + } + else if (strcmp(script_filename, "@doopt") == 0) + { + script_run_as_specified_by_options(fd); + } + else if (strcmp(script_filename, "@nuldo=opt") == 0) + { + doopt_by_nul = true; + } + else if (strcmp(script_filename, "@nuldo=none") == 0) + { + doopt_by_nul = false; } else { - script_file_run(L, script_filename); + tio_printf("Unknown command"); } + return; } - else if (option.script_filename != NULL) + else + { + // if filename starts with '!', do filename's remain parts as lua commands. + tio_printf("Running script %s", script_filename); + if (script_filename[0] == '!') + { + script_buffer_run(script_interp, &script_filename[1]); + } + else + { + script_file_run(script_interp, script_filename); + } + return; + } +} + +void script_run_as_specified_by_options(int fd) +{ + device_fd = fd; + + if (script_interp == NULL) + { + if (script_interp_new() == NULL) + return; + } + + if (option.script_filename != NULL) { tio_printf("Running script %s", option.script_filename); - script_file_run(L, option.script_filename); + if (script_interp_new()) + script_file_run(script_interp, option.script_filename); + } else if (option.script != NULL) { tio_printf("Running script"); - script_buffer_run(L, option.script); + if (script_interp_new()) + script_buffer_run(script_interp, option.script); } - - lua_close(L); } const char *script_run_state_to_string(script_run_t state) diff --git a/src/script.h b/src/script.h index 58ba1e1..7cf68c8 100644 --- a/src/script.h +++ b/src/script.h @@ -30,4 +30,5 @@ typedef enum } script_run_t; void script_run(int fd, const char *script_filename); +void script_run_as_specified_by_options(int fd); const char *script_run_state_to_string(script_run_t state); diff --git a/src/tty.c b/src/tty.c index 63f7054..5e39d47 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1078,16 +1078,9 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) case KEY_R: /* Run script */ tio_printf("Run Lua script"); - if (tio_subcmd_readln("Enter file name: ")) - { - clear_line(); - script_run(device_fd, line); - } - else - { - clear_line(); - script_run(device_fd, NULL); - } + tio_subcmd_readln("Enter file name or \"!\" lua commands or \"@\" direction to interpreter: "); + clear_line(); + script_run(device_fd, line); break; case KEY_SHIFT_R: @@ -2685,7 +2678,7 @@ int tty_connect(void) /* Manage script activation */ if (option.script_run != SCRIPT_RUN_NEVER) { - script_run(device_fd, NULL); + script_run_as_specified_by_options(device_fd); if (option.script_run == SCRIPT_RUN_ONCE) { From c25a379fbb347450e526d7f74e09bfbbb7b09574 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 7 Dec 2025 11:21:55 +0900 Subject: [PATCH 20/53] Add some socket examples of bidirectional command helpers and pexpect bidir_cmd_herlper.sh: Bidirectional command helper by socat. bidir_cmd_helper.py: Bidirectional command helper by python and netcat. pexpect-ping.py, pexpect-pong.py: Scripts for throwing Ping-Pong between cross-connected tios. --- examples/socket/bidir_cmd_helper.py | 94 +++++++++++++++++++++++++++++ examples/socket/bidir_cmd_helper.sh | 15 +++++ examples/socket/pexpect-ping.py | 22 +++++++ examples/socket/pexpect-pong.py | 22 +++++++ 4 files changed, 153 insertions(+) create mode 100755 examples/socket/bidir_cmd_helper.py create mode 100755 examples/socket/bidir_cmd_helper.sh create mode 100755 examples/socket/pexpect-ping.py create mode 100755 examples/socket/pexpect-pong.py diff --git a/examples/socket/bidir_cmd_helper.py b/examples/socket/bidir_cmd_helper.py new file mode 100755 index 0000000..6344058 --- /dev/null +++ b/examples/socket/bidir_cmd_helper.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 + +import sys +import subprocess +import shlex +import os + +# netcat (openbsd) +NC_CMD = "nc" +NC_OPT = "-UN" + +def connect_and_execute_bidirectional_command(socket_path, command): + TIMEOUT_SEC = 600 + + # Sanitize user input (command stays as a string, but avoid passing raw input directly) + safe_cmd = " ".join(shlex.split(command)) + + # Launch nc process (connect to the Unix domain socket) + p_nc = subprocess.Popen( + [NC_CMD, NC_OPT, socket_path], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + text=False, # binary transfer + ) + + # Launch the bidirectional command connected to nc + p_bidir_cmd = subprocess.Popen( + safe_cmd, # keep string command for shell=True + stdin=p_nc.stdout, + stdout=p_nc.stdin, + stderr=None, + shell=True, + text=False, + ) + + # Close unused pipe ends in the parent process to propagate SIGPIPE properly + p_nc.stdout.close() + p_nc.stdin.close() + + try: + p_bidir_cmd.communicate(timeout=TIMEOUT_SEC) + + except subprocess.TimeoutExpired: + # Ensure the command is killed on timeout + p_bidir_cmd.kill() + + except Exception as e: + # Print unexpected exceptions for debugging + print(type(e), file=sys.stderr) + + # Check bidirectional command exit code + if p_bidir_cmd.returncode != 0: + print(f"command exited with {p_bidir_cmd.returncode}", file=sys.stderr) + + # Ensure nc is terminated regardless of exceptions + try: + p_nc.terminate() + p_nc.wait(timeout=5) + except subprocess.TimeoutExpired: + # Force kill if graceful shutdown fails + p_nc.kill() + except Exception as e: + print(type(e), file=sys.stderr) + + # Read and report stderr of nc + nc_stderr = p_nc.stderr.read() + if nc_stderr: + print("nc stderr:", nc_stderr.decode(errors='ignore'), file=sys.stderr) + + return p_bidir_cmd.returncode + + +def main(): + script = os.path.basename(__file__) + usage = f"Usage: {script} " + example = f"Example: {script} /tmp/tio-socket0 \"sz -b -p sample.bin\"" + note = f"Note: Please run \"tio -S \" beforehand." + + if len(sys.argv) != 3: + print(usage, file=sys.stderr) + print(example, file=sys.stderr) + print(note, file=sys.stderr) + return 1 + + socket_path = sys.argv[1] + bidirectional_command = sys.argv[2] + return connect_and_execute_bidirectional_command(socket_path, bidirectional_command) + + +if __name__ == '__main__': + exit_code = main() + sys.exit(exit_code) diff --git a/examples/socket/bidir_cmd_helper.sh b/examples/socket/bidir_cmd_helper.sh new file mode 100755 index 0000000..5459721 --- /dev/null +++ b/examples/socket/bidir_cmd_helper.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +if [ $# -lt 2 ]; then + echo "Usage: $0 " + echo "Example: $0 /tmp/tio-socket0 \"sz -b -p sample.bin\"" + echo "Note: Please run \"tio -S \" beforehand." + exit 1 +fi + +SOCKET_PATH=$1 +BIDIR_CMD=$2 + +socat EXEC:"$BIDIR_CMD" $SOCKET_PATH + +exit $? diff --git a/examples/socket/pexpect-ping.py b/examples/socket/pexpect-ping.py new file mode 100755 index 0000000..4323849 --- /dev/null +++ b/examples/socket/pexpect-ping.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# In MSYS2, use /mingw64/bin/python3 +# +# Send a "Ping" and wait for a "Pong". +# Repeat this process. +# + +import pexpect +from pexpect import popen_spawn + +child = pexpect.popen_spawn.PopenSpawn("nc -UN /tmp/tio-socket0") + +cnt = 0 +while True: + try: + child.sendline(f"Ping {cnt:d}") + cnt += 1 + child.expect(r'Pong \d+[\r\n]+', timeout = 10) + except Exception as e: + print(type(e)) + break + diff --git a/examples/socket/pexpect-pong.py b/examples/socket/pexpect-pong.py new file mode 100755 index 0000000..8f93c96 --- /dev/null +++ b/examples/socket/pexpect-pong.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# In MSYS2, use /mingw64/bin/python3 +# +# wait for a "Ping" and Send a "Pong" +# Repeat this process. +# + +import pexpect +from pexpect import popen_spawn + +child = pexpect.popen_spawn.PopenSpawn("nc -UN /tmp/tio-socket1") + +cnt = 0 +while True: + try: + child.expect(r'Ping \d+[\r\n]+', timeout = 10) + child.sendline(f"Pong {cnt:d}") + cnt += 1 + except Exception as e: + print(type(e)) + break + From 266338a92610919bb140056b4cc883c2525329f1 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 30 Dec 2025 11:41:39 +0900 Subject: [PATCH 21/53] Add Lua REPL mode and multiline edit function in the mode. Typing "@repl" after Ctrl-t r, enter Lua REPL mode. Typing "@exit" in REPL mode will return you to normal mode. Note: - the determination of continuation lines is not done automatically, and if the end of a line is \, it is determined to be a continuation instruction. - In REPL mode, tio's main loop is blocked. (Ctrl-t q works.) Example: >> t = {1,2,3,4,5} >> for _,v in ipairs(t) do\ >> print(v,"\r")\ >> end Implementation improvements: - Add tty_init() and script_interp_init() in order to only once initialization. - Fix script function to work without device_fd. --- src/main.c | 6 ++++ src/script.c | 83 +++++++++++++++++++++++++++++++++++----------- src/script.h | 8 +++-- src/tty.c | 94 +++++++++++++++++++++++++++++++++++++++++++--------- src/tty.h | 1 + 5 files changed, 155 insertions(+), 37 deletions(-) diff --git a/src/main.c b/src/main.c index 8434caa..3c2a47a 100644 --- a/src/main.c +++ b/src/main.c @@ -123,6 +123,12 @@ int main(int argc, char *argv[]) socket_configure(); } + /* Script interpreter init */ + script_interp_init(); + + /* Initialize tty module once on program start */ + tty_init(); + /* Spawn input handling into separate thread */ tty_input_thread_create(); diff --git a/src/script.c b/src/script.c index 13c5048..4c85d4e 100644 --- a/src/script.c +++ b/src/script.c @@ -44,7 +44,7 @@ #define MAX_BUFFER_SIZE 2000 // Maximum size of circular buffer #define READ_LINE_SIZE 4096 // read_line buffer length -static int device_fd; +static int device_fd = 0; static lua_State *script_interp = NULL; // clang-format off @@ -203,6 +203,11 @@ static int line_set(lua_State *L) int cd = lua_tointeger(L, 5); int ri = lua_tointeger(L, 6); + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + if (dtr != -1) { line_config[0].mask = TIOCM_DTR; @@ -252,6 +257,11 @@ static int api_send(lua_State *L) int protocol = luaL_checkinteger(L, 2); int ret; + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + if (file == NULL) { return 0; @@ -295,6 +305,11 @@ static int api_write(lua_State *L) ssize_t ret; int attempts = 100; + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + do { ret = write(device_fd, string, len); @@ -322,6 +337,11 @@ static int api_twrite(lua_State *L) size_t len = 0; const char *string = luaL_checklstring(L, 1, &len); + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + for (; len > 0; --len, string++) { forward_to_tty(device_fd, *string); @@ -339,6 +359,11 @@ static int api_read(lua_State *L) int size = luaL_checkinteger(L, 1); int timeout = luaL_optinteger(L, 2, -1); // ms, negative value means forever. + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + luaL_Buffer buffer; luaL_buffinit(L, &buffer); @@ -378,6 +403,11 @@ static int api_readline(lua_State *L) luaL_Buffer b; char ch; + if (device_fd == 0) + { + return luaL_error(L, "tty device not ready"); + } + luaL_buffinit(L, &b); luaL_prepbuffer(&b); while (true) @@ -581,24 +611,36 @@ static lua_State *script_interp_new(void) return L; } -void script_run(int fd, const char *script_filename) +void script_device_bind(int fd) +{ + device_fd = fd; +} + +void script_device_unbind(void) +{ + device_fd = 0; +} + +void script_do_line(const char *script_line) +{ + assert(script_line != NULL); + assert(script_interp != NULL); + + script_buffer_run(script_interp, script_line); +} + +void script_run(const char *script_filename) { static bool doopt_by_nul = true; - device_fd = fd; assert(script_filename != NULL); - - if (script_interp == NULL) - { - if (script_interp_new() == NULL) - return; - } + assert(script_interp != NULL); if (script_filename[0] == '\0') { if (doopt_by_nul) { - script_run_as_specified_by_options(fd); + script_run_as_specified_by_options(); } return; } @@ -611,7 +653,7 @@ void script_run(int fd, const char *script_filename) } else if (strcmp(script_filename, "@doopt") == 0) { - script_run_as_specified_by_options(fd); + script_run_as_specified_by_options(); } else if (strcmp(script_filename, "@nuldo=opt") == 0) { @@ -643,15 +685,9 @@ void script_run(int fd, const char *script_filename) } } -void script_run_as_specified_by_options(int fd) +void script_run_as_specified_by_options(void) { - device_fd = fd; - - if (script_interp == NULL) - { - if (script_interp_new() == NULL) - return; - } + assert(script_interp != NULL); if (option.script_filename != NULL) { @@ -682,3 +718,12 @@ const char *script_run_state_to_string(script_run_t state) return "Unknown"; } } + +void script_interp_init(void) +{ + if (script_interp_new() == NULL) + { + tio_error_printf("Could not start script interpreter."); + exit(EXIT_FAILURE); + } +} diff --git a/src/script.h b/src/script.h index 7cf68c8..c0f14e6 100644 --- a/src/script.h +++ b/src/script.h @@ -29,6 +29,10 @@ typedef enum SCRIPT_RUN_END, } script_run_t; -void script_run(int fd, const char *script_filename); -void script_run_as_specified_by_options(int fd); +void script_interp_init(void); +void script_device_bind(int fd); +void script_device_unbind(void); +void script_run(const char *script_filename); +void script_run_as_specified_by_options(void); +void script_do_line(const char *script_line); const char *script_run_state_to_string(script_run_t state); diff --git a/src/tty.c b/src/tty.c index 5e39d47..b1fe6d7 100644 --- a/src/tty.c +++ b/src/tty.c @@ -147,6 +147,8 @@ typedef enum SUBCOMMAND_MAP, } sub_command_t; +#define MLINE_MAX 4096 + // clang-format off const char random_array[] = { @@ -183,7 +185,7 @@ static char *tty_buffer_write_ptr = tty_buffer; static pthread_t thread; static int pipefd[2]; static pthread_mutex_t mutex_input_ready = PTHREAD_MUTEX_INITIALIZER; -static char line[PATH_MAX]; +static char line[PATH_MAX], mline[MLINE_MAX]; static size_t listing_device_name_length_max = 0; static readline_t *readline_ctx = NULL; static readline_t *subcmd_readline_ctx = NULL; @@ -593,7 +595,9 @@ static void tty_line_poke(int fd, int mask, tty_line_mode_t mode, unsigned int d static int tio_subcmd_readln(const char *title_prompt) { - tio_printf_raw("%s\r\n", title_prompt); + if (title_prompt && (title_prompt[0] != '\0')) { + tio_printf_raw("%s\r\n", title_prompt); + } readline_prompt_for_input(subcmd_readline_ctx); /* Read line with line edit and history. */ @@ -660,6 +664,50 @@ static void mappings_print(void) // clang-format on } +static void handle_script_repl(void) +{ + bool local_echo_bkup = option.local_echo; + int line_len; + int mline_len = 0; + + option.local_echo = true; + tio_printf("Enter Lua REPL mode (@exit to exit)"); + + strcpy(mline, ""); + while (true) + { + tio_subcmd_readln(""); + if (strcmp(line, "@exit") == 0) + break; + line_len = strlen(line); + + if (line_len > 0) + { + if (mline_len + line_len + 1 > MLINE_MAX) + { + tio_printf("Too long lines. The size should be lesser then %d bytes", MLINE_MAX); + strcpy(mline, ""); + mline_len = 0; + continue; + } + + strcat(&mline[mline_len], line); + mline_len += line_len; + + if (mline_len > 0 && mline[mline_len - 1] == '\\') + { + mline[mline_len - 1] = '\n'; + continue; + } + } + script_do_line(mline); + strcpy(mline, ""); + mline_len = 0; + } + + option.local_echo = local_echo_bkup; +} + void handle_command_sequence(char input_char, char *output_char, bool *forward) { char unused_char; @@ -1079,8 +1127,15 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) /* Run script */ tio_printf("Run Lua script"); tio_subcmd_readln("Enter file name or \"!\" lua commands or \"@\" direction to interpreter: "); - clear_line(); - script_run(device_fd, line); + if (strcmp(line, "@repl") == 0) + { + handle_script_repl(); + } + else + { + clear_line(); + script_run(line); + } break; case KEY_SHIFT_R: @@ -2545,6 +2600,21 @@ void forward_to_tty(int fd, char output_char) } } +void tty_init(void) +{ + // Initialize readline like history + readline_ctx = readline_create(); + subcmd_readline_ctx = readline_create(); + if (readline_ctx == NULL || subcmd_readline_ctx == NULL) + { + tio_error_printf("Could not allocate readline buffer."); + exit(EXIT_FAILURE); + } + readline_set_prompt(readline_ctx, "> "); + readline_set_prompt(subcmd_readline_ctx, ">> "); + +} + int tty_connect(void) { fd_set rdfs; /* Read file descriptor set */ @@ -2676,9 +2746,11 @@ int tty_connect(void) } /* Manage script activation */ + script_device_bind(device_fd); + if (option.script_run != SCRIPT_RUN_NEVER) { - script_run_as_specified_by_options(device_fd); + script_run_as_specified_by_options(); if (option.script_run == SCRIPT_RUN_ONCE) { @@ -2698,17 +2770,6 @@ int tty_connect(void) exit(status); } - // Initialize readline like history - readline_ctx = readline_create(); - subcmd_readline_ctx = readline_create(); - if (readline_ctx == NULL || subcmd_readline_ctx == NULL) - { - tio_error_printf("Could not allocate readline buffer."); - exit(EXIT_FAILURE); - } - readline_set_prompt(readline_ctx, "> "); - readline_set_prompt(subcmd_readline_ctx, ">> "); - /* Input loop */ while (true) { @@ -3035,6 +3096,7 @@ error_setspeed: error_tcsetattr: error_tcgetattr: error_read: + script_device_unbind(); tty_disconnect(); error_open: return TIO_ERROR; diff --git a/src/tty.h b/src/tty.h index eb3dfd4..4ef210d 100644 --- a/src/tty.h +++ b/src/tty.h @@ -76,6 +76,7 @@ void stdout_configure(void); void stdin_configure(void); void tty_configure(void); void tty_reconfigure(void); +void tty_init(void); int tty_connect(void); void tty_wait_for_device(void); void list_serial_devices(void); From b5656112d724427d6e3bd550cb9b52a545c70837 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 30 Dec 2025 12:07:43 +0900 Subject: [PATCH 22/53] Fix clear_line and print_line to untaint line status --- src/misc.c | 2 +- src/readline.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/misc.c b/src/misc.c index 271d266..de17170 100644 --- a/src/misc.c +++ b/src/misc.c @@ -256,5 +256,5 @@ int execute_shell_command(int fd, const char *command) void clear_line() { - print("\r\033[K"); + printf("\r\033[K"); } diff --git a/src/readline.c b/src/readline.c index 27125c6..8742e4c 100644 --- a/src/readline.c +++ b/src/readline.c @@ -42,11 +42,11 @@ typedef struct readline_s void print_line(readline_t *rl) { clear_line(); - print("%s%s", rl->prompt, rl->line); - print("\r"); // Move the cursor back to the beginning + printf("%s%s", rl->prompt, rl->line); + printf("\r"); // Move the cursor back to the beginning for (int i = 0; i < rl->prompt_length + rl->cursor_pos; ++i) { - print("\x1b[C"); // Move the cursor right + printf("\x1b[C"); // Move the cursor right } } From eab0f6245baf0f38773cb6e2ce6eeb5793e7fdd2 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 30 Dec 2025 20:19:26 +0900 Subject: [PATCH 23/53] Add write_poll to improve tty_write and tty_sync Implement blockable write with poll + nonblock write. --- src/misc.c | 34 ++++++++++++++++++++++++++++++++++ src/misc.h | 2 ++ src/script.c | 2 +- src/tty.c | 34 +++++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/misc.c b/src/misc.c index de17170..6ca0f8b 100644 --- a/src/misc.c +++ b/src/misc.c @@ -111,6 +111,40 @@ int read_poll(int fd, void *data, size_t len, int timeout) return 0; } +ssize_t write_poll(int fd, void *data, size_t len, int timeout) +{ + struct pollfd fds; + ssize_t ret = 0; + + fds.events = POLLOUT; + fds.fd = fd; + + /* Wait data available */ + ret = poll(&fds, 1, timeout); + if (ret < 0) + { + tio_error_print("%s", strerror(errno)); + return ret; + } + else if (ret > 0) + { + if (fds.revents & POLLOUT) + { + // Ready to write + // return value should not be 0 + return write(fd, data, len); + } + else /* if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) */ + { + return -1; + } + } + + /* Timeout */ + return 0; +} + + // Function to calculate djb2 hash of string unsigned long djb2_hash(const unsigned char *str) { diff --git a/src/misc.h b/src/misc.h index 08ae5eb..9f26d82 100644 --- a/src/misc.h +++ b/src/misc.h @@ -24,6 +24,7 @@ #include #include +#define WRITE_POLL_FOREVER (-1) #define UNUSED(expr) do { (void)(expr); } while (0) void delay(long ms); @@ -32,6 +33,7 @@ bool regex_match(const char *string, const char *pattern); unsigned long djb2_hash(const unsigned char *str); void *base62_encode(unsigned long num, char *output); int read_poll(int fd, void *data, size_t len, int timeout); +ssize_t write_poll(int fd, const void *data, size_t len, int timeout); double get_current_time(void); bool match_patterns(const char *string, const char *patterns); int execute_shell_command(int fd, const char *command); diff --git a/src/script.c b/src/script.c index 4c85d4e..269bd69 100644 --- a/src/script.c +++ b/src/script.c @@ -312,7 +312,7 @@ static int api_write(lua_State *L) do { - ret = write(device_fd, string, len); + ret = write_poll(device_fd, string, len, WRITE_POLL_FOREVER); if (ret < 0) return luaL_error(L, "%s", strerror(errno)); diff --git a/src/tty.c b/src/tty.c index b1fe6d7..d026ac4 100644 --- a/src/tty.c +++ b/src/tty.c @@ -238,28 +238,43 @@ inline static unsigned char char_to_nibble(char c) void tty_sync(int fd) { + /* If output_delay is valid, tty_buffer should be already empty. + * So this function doesn't consider output_delay options. */ ssize_t count; + size_t remain = tty_buffer_count; + char *cp = tty_buffer; - while (tty_buffer_count > 0) + while (remain > 0) { - count = write(fd, tty_buffer, tty_buffer_count); + count = write_poll(fd, cp, remain, WRITE_POLL_FOREVER); if (count < 0) { // Error tio_debug_printf("Write error while flushing tty buffer (%s)", strerror(errno)); break; } - tty_buffer_count -= count; - fsync(fd); - tcdrain(fd); + cp += count; + remain -= count; + + /* Reduce the number of additional sends */ + if (remain > 0) + { + int estimated_sendtime_us = (int)((int64_t)count * 10 * 1000 * 1000 / option.baudrate); + if (estimated_sendtime_us > 300 * 1000) + usleep(300 * 1000); + else + usleep(estimated_sendtime_us); + } } + fsync(fd); + tcdrain(fd); // Reset tty_buffer_write_ptr = tty_buffer; tty_buffer_count = 0; } -ssize_t tty_write(int fd, const void *buffer, size_t count) +ssize_t tty_write(int fd, void *buffer, size_t count) { ssize_t retval = 0, bytes_written = 0; size_t i; @@ -279,7 +294,7 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) // Write byte by byte with output delay for (i = 0; i < count; i++) { - retval = write(fd, &cbuf[i], 1); + retval = write_poll(fd, &cbuf[i], 1, WRITE_POLL_FOREVER); if (retval < 0) { // Error @@ -2526,7 +2541,7 @@ void forward_to_tty(int fd, char output_char) /* Map newline character */ if ((output_char == '\n' || output_char == '\r') && (option.map_o_nl_crnl)) { - const char *crlf = "\r\n"; + char crlf[] = "\r\n"; optional_local_echo(crlf[0]); optional_local_echo(crlf[1]); @@ -2730,7 +2745,7 @@ int tty_connect(void) else if (ret > 0) { // Forward to tty device - ret = write(device_fd, &input_char, 1); + ret = write_poll(device_fd, &input_char, 1, WRITE_POLL_FOREVER); if (ret < 0) { tio_error_printf("Could not write to serial device (%s)", strerror(errno)); @@ -2743,6 +2758,7 @@ int tty_connect(void) break; } } + tty_sync(device_fd); } /* Manage script activation */ From 2b0e674ee48b399e501e19047df627e1a6872a3b Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 30 Dec 2025 20:30:41 +0900 Subject: [PATCH 24/53] Fix execute_shell_command to use tty_write --- src/misc.c | 31 ++++++++++++++++++++++++++++--- src/tty.h | 1 + 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/misc.c b/src/misc.c index 6ca0f8b..0df64fd 100644 --- a/src/misc.c +++ b/src/misc.c @@ -230,12 +230,22 @@ bool match_patterns(const char *string, const char *patterns) return false; } -// Function that forks subprocess, redirects its stdin and stdout to the +// Function that forks subprocess, redirects its stdout and stderr to the // specified filedescriptor, and runs command. int execute_shell_command(int fd, const char *command) { +#define READ_END 0 +#define WRITE_END 1 pid_t pid; int status; + int pipe_fd[2]; + + // Create Pipes + if (pipe(pipe_fd) == -1) + { + tio_error_print("pipe() failed (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } // Fork a child process pid = fork(); @@ -252,7 +262,8 @@ int execute_shell_command(int fd, const char *command) tio_printf("Executing shell command '%s'", command); // Redirect stdout and stderr to the file descriptor - if (dup2(fd, STDOUT_FILENO) == -1 || dup2(fd, STDERR_FILENO) == -1) + close(pipe_fd[READ_END]); + if (dup2(pipe_fd[WRITE_END], STDOUT_FILENO) == -1 || dup2(pipe_fd[WRITE_END], STDERR_FILENO) == -1) { tio_error_print("dup2() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); @@ -262,6 +273,7 @@ int execute_shell_command(int fd, const char *command) execl("/bin/sh", "sh", "-c", command, (char *)NULL); // If execlp() returns, it means an error occurred + close(pipe_fd[WRITE_END]); perror("execlp"); tio_error_print("execlp() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); @@ -269,6 +281,20 @@ int execute_shell_command(int fd, const char *command) else { // Parent process + char buf[BUFSIZ]; + int bytes; + + // Read pipe and transfer to tty device. + close(pipe_fd[WRITE_END]); + while ( (bytes = read(pipe_fd[READ_END], buf, sizeof(buf))) > 0) + { + if (tty_write(fd, buf, bytes) <= 0) + { + tio_warning_printf("Could not write to tty device"); + } + } + tty_sync(fd); + close(pipe_fd[READ_END]); // Wait for the child process to finish waitpid(pid, &status, 0); @@ -284,7 +310,6 @@ int execute_shell_command(int fd, const char *command) return -1; } } - return 0; } diff --git a/src/tty.h b/src/tty.h index 4ef210d..c9d10ec 100644 --- a/src/tty.h +++ b/src/tty.h @@ -86,4 +86,5 @@ void tty_line_set(int fd, tty_line_config_t line_config[]); void tty_search(void); GList *tty_search_for_serial_devices(void); void forward_to_tty(int fd, char output_char); +ssize_t tty_write(int fd, void *buffer, size_t count); void tty_sync(int fd); From 5045ca176820701dc1852034f6699db263354c41 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 30 Dec 2025 20:58:06 +0900 Subject: [PATCH 25/53] Fix issue that output-delay doen't work in non-interactive mode --- src/tty.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tty.c b/src/tty.c index d026ac4..8191e0c 100644 --- a/src/tty.c +++ b/src/tty.c @@ -2736,7 +2736,7 @@ int tty_connect(void) { while (true) { - int ret = read(pipefd[0], &input_char, 1); + int ret = read(pipefd[0], input_buffer, BUFSIZ); if (ret < 0) { tio_error_printf("Could not read from pipe (%s)", strerror(errno)); @@ -2745,7 +2745,7 @@ int tty_connect(void) else if (ret > 0) { // Forward to tty device - ret = write_poll(device_fd, &input_char, 1, WRITE_POLL_FOREVER); + ret = tty_write(device_fd, input_buffer, ret); if (ret < 0) { tio_error_printf("Could not write to serial device (%s)", strerror(errno)); From e73ff6d208c10f601605f58037e4e96f98023024 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 30 Dec 2025 21:04:29 +0900 Subject: [PATCH 26/53] Fix tty_write to return error code (negative value) in error case --- src/misc.c | 2 +- src/tty.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc.c b/src/misc.c index 0df64fd..ec39f51 100644 --- a/src/misc.c +++ b/src/misc.c @@ -288,7 +288,7 @@ int execute_shell_command(int fd, const char *command) close(pipe_fd[WRITE_END]); while ( (bytes = read(pipe_fd[READ_END], buf, sizeof(buf))) > 0) { - if (tty_write(fd, buf, bytes) <= 0) + if (tty_write(fd, buf, bytes) < 0) { tio_warning_printf("Could not write to tty device"); } diff --git a/src/tty.c b/src/tty.c index 8191e0c..2bfad11 100644 --- a/src/tty.c +++ b/src/tty.c @@ -299,7 +299,7 @@ ssize_t tty_write(int fd, void *buffer, size_t count) { // Error tio_debug_printf("Write error (%s)", strerror(errno)); - break; + return retval; } bytes_written += retval; From c74273ecaa132cf3076d8469a3a401fbced6e097 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Wed, 31 Dec 2025 15:14:18 +0900 Subject: [PATCH 27/53] Add option --no-tty-restore / -N that omits tty_restore() call #334 On exit, do not reset the tty device settings to the state they were in before the program started. This is effective when sending characters at a low baud rate and then immediately exiting, to ensure that the last character is output correctly. --- README.md | 1 + examples/config/config | 1 + man/tio.1.in | 8 ++++++++ man/tio.1.txt | 6 ++++++ src/bash-completion/tio.in | 1 + src/configfile.c | 1 + src/options.c | 11 ++++++++++- src/options.h | 1 + src/tty.c | 7 +++++-- 9 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3ab066..78d4ce7 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Options: --exclude-drivers Exclude drivers by pattern --exclude-tids Exclude topology IDs by pattern -n, --no-reconnect Do not reconnect + -N, --no-tty-restore Do not restore initial TTY device settings -e, --local-echo Enable local echo --input-mode normal|hex|line Select input mode (default: normal) --output-mode normal|hex|hexN Select output mode (default: normal) diff --git a/examples/config/config b/examples/config/config index 7bd141e..1aa038b 100644 --- a/examples/config/config +++ b/examples/config/config @@ -19,6 +19,7 @@ output-delay = 0 output-line-delay = 0 auto-connect = direct no-reconnect = false +no-tty-restore = false local-echo = false input-mode = normal output-mode = normal diff --git a/man/tio.1.in b/man/tio.1.in index aaaf8a5..dd8243e 100644 --- a/man/tio.1.in +++ b/man/tio.1.in @@ -123,6 +123,14 @@ Do not reconnect. This means that tio will exit if it fails to connect to device or an established connection is lost. +.TP +.BR \-n ", " \-\-no\-tty\-restore + +Do not restore initial TTY device settings. + +This means that tio will exit without trying to restore TTY device +settings that existed when tio was started. + .TP .BR \-e ", " "\-\-local\-echo diff --git a/man/tio.1.txt b/man/tio.1.txt index 46afb5e..f3344a0 100644 --- a/man/tio.1.txt +++ b/man/tio.1.txt @@ -94,6 +94,12 @@ OPTIONS This means that tio will exit if it fails to connect to device or an established connection is lost. + -N, --no-tty-restore + + Do not restore initial TTY device settings. + + This means that tio will exit without trying to restore TTY device settings that existed when tio was started. + -e, --local-echo Enable local echo. diff --git a/src/bash-completion/tio.in b/src/bash-completion/tio.in index b3b61cb..3d59d73 100644 --- a/src/bash-completion/tio.in +++ b/src/bash-completion/tio.in @@ -23,6 +23,7 @@ _tio() --exclude-drivers \ --exclude-tids \ -n --no-reconnect \ + -N --no-tty-restore \ -e --local-echo \ -l --log \ --log-file \ diff --git a/src/configfile.c b/src/configfile.c index 0fc230c..cc21dfb 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -196,6 +196,7 @@ static void config_parse_keys(GKeyFile *key_file, char *group) config_get_string(key_file, group, "exclude-drivers", &option.exclude_devices, NULL); config_get_string(key_file, group, "exclude-tids", &option.exclude_devices, NULL); config_get_bool(key_file, group, "no-reconnect", &option.no_reconnect); + config_get_bool(key_file, group, "no-tty-restore", &option.no_tty_restore); config_get_bool(key_file, group, "local-echo", &option.local_echo); config_get_string(key_file, group, "input-mode", &string, "normal", "hex", "line", NULL); if (string != NULL) diff --git a/src/options.c b/src/options.c index 09f35e8..5cdcbe4 100644 --- a/src/options.c +++ b/src/options.c @@ -82,6 +82,7 @@ struct option_t option = .dcd_pulse_duration = 100, .ri_pulse_duration = 100, .no_reconnect = false, + .no_tty_restore = false, .auto_connect = AUTO_CONNECT_DIRECT, .log = false, .log_append = false, @@ -153,6 +154,7 @@ void option_print_help(char *argv[]) printf(" --exclude-drivers Exclude drivers by pattern\n"); printf(" --exclude-tids Exclude topology IDs by pattern\n"); printf(" -n, --no-reconnect Do not reconnect\n"); + printf(" -N, --no-tty-restore Do not restore initial TTY device settings\n"); printf(" -e, --local-echo Enable local echo\n"); printf(" --input-mode normal|hex|line Select input mode (default: normal)\n"); printf(" --output-mode normal|hex|hexN Select output mode (default: normal)\n"); @@ -844,6 +846,7 @@ void option_parse_mappings(const char *map) void options_print() { + /* note: negative true/false settings are rephrased as affirmative no/yes. */ tio_printf(" Device: %s", device_name); tio_printf(" Baudrate: %u", option.baudrate); tio_printf(" Databits: %d", option.databits); @@ -858,6 +861,7 @@ void options_print() tio_printf(" Output line delay char: %s", option.output_line_delay_char == '\r' ? "cr" : "lf"); tio_printf(" Automatic connect strategy: %s", option_auto_connect_state_to_string(option.auto_connect)); tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "true" : "false"); + tio_printf(" TTY device settings restore: %s", option.no_tty_restore ? "true" : "false"); // clang-format off tio_printf(" Pulse duration: DTR=%d RTS=%d CTS=%d DSR=%d DCD=%d RI=%d", option.dtr_pulse_duration, option.rts_pulse_duration, @@ -933,6 +937,7 @@ void options_parse(int argc, char *argv[]) {"exclude-drivers", required_argument, 0, OPT_EXCLUDE_DRIVERS }, {"exclude-tids", required_argument, 0, OPT_EXCLUDE_TIDS }, {"no-reconnect", no_argument, 0, 'n' }, + {"no-tty-restore", no_argument, 0, 'N' }, {"local-echo", no_argument, 0, 'e' }, {"timestamp", no_argument, 0, 't' }, {"timestamp-format", required_argument, 0, OPT_TIMESTAMP_FORMAT }, @@ -967,7 +972,7 @@ void options_parse(int argc, char *argv[]) int option_index = 0; /* Parse argument using getopt_long */ - c = getopt_long(argc, argv, "b:d:f:s:p:o:O:a:netLlS:m:c:xrvh", long_options, &option_index); + c = getopt_long(argc, argv, "b:d:f:s:p:o:O:a:nNetLlS:m:c:xrvh", long_options, &option_index); /* Detect the end of the options */ if (c == -1) @@ -1041,6 +1046,10 @@ void options_parse(int argc, char *argv[]) option.no_reconnect = true; break; + case 'N': + option.no_tty_restore = true; + break; + case 'e': option.local_echo = true; break; diff --git a/src/options.h b/src/options.h index 5388de2..4f501ae 100644 --- a/src/options.h +++ b/src/options.h @@ -62,6 +62,7 @@ struct option_t int dcd_pulse_duration; int ri_pulse_duration; bool no_reconnect; + bool no_tty_restore; auto_connect_t auto_connect; bool log; bool log_append; diff --git a/src/tty.c b/src/tty.c index 2bfad11..2c4ace8 100644 --- a/src/tty.c +++ b/src/tty.c @@ -2706,10 +2706,13 @@ int tty_connect(void) rs485_mode_enable(device_fd); } - /* Make sure we restore tty settings on exit */ + /* Make sure we restore tty settings on exit unless ordered to keep current settings */ if (first) { - atexit(&tty_restore); + if (option.no_tty_restore == false) + { + atexit(&tty_restore); + } first = false; } From 6425ffe66a8589d3987d43ceefc8296acff7ebd3 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Wed, 31 Dec 2025 15:44:42 +0900 Subject: [PATCH 28/53] Fix Ctrl-t c to display the yes/no of the function instead of true/false of the setting When options.no_reconnect is true, it displays "Automatic reconnect: no" --- src/options.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/options.c b/src/options.c index 5cdcbe4..67c6ed6 100644 --- a/src/options.c +++ b/src/options.c @@ -853,15 +853,15 @@ void options_print() tio_printf(" Flow: %s", option_flow_to_string(option.flow)); tio_printf(" Stopbits: %d", option.stopbits); tio_printf(" Parity: %s", option_parity_to_string(option.parity)); - tio_printf(" Local echo: %s", option.local_echo ? "true" : "false"); + tio_printf(" Local echo: %s", option.local_echo ? "yes" : "no"); tio_printf(" Timestamp: %s", option_timestamp_format_to_string(option.timestamp)); tio_printf(" Timestamp timeout: %u", option.timestamp_timeout); tio_printf(" Output delay: %d", option.output_delay); tio_printf(" Output line delay: %d", option.output_line_delay); tio_printf(" Output line delay char: %s", option.output_line_delay_char == '\r' ? "cr" : "lf"); tio_printf(" Automatic connect strategy: %s", option_auto_connect_state_to_string(option.auto_connect)); - tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "true" : "false"); - tio_printf(" TTY device settings restore: %s", option.no_tty_restore ? "true" : "false"); + tio_printf(" Automatic reconnect: %s", option.no_reconnect ? "no" : "yes"); + tio_printf(" TTY device settings restore: %s", option.no_tty_restore ? "no" : "yes"); // clang-format off tio_printf(" Pulse duration: DTR=%d RTS=%d CTS=%d DSR=%d DCD=%d RI=%d", option.dtr_pulse_duration, option.rts_pulse_duration, From 3b7fe3d258fd2f96acf2cf0f54e92e4b29bc07ca Mon Sep 17 00:00:00 2001 From: yabu76 Date: Fri, 2 Jan 2026 00:35:52 +0900 Subject: [PATCH 29/53] Reorganize forward_to_tty, tty_write, tty_raw_write functions Add tty_raw_write function and reorganize roles of them. tty_raw_write: Performs any processing that must be done character-by-character on the fly. That is, output delays and ONULBRK processing. tty_write : Converts input data using output mapping and passes it to tty_raw_write. forward_to_tty : Converts input data according to the input/output mode and passes it to tty_write. (output-mapping function move to tty_write. tx_total statistics counting move to tty_raw_write and tty_sync.) --- src/tty.c | 184 ++++++++++++++++++++++++++++++++---------------------- src/tty.h | 2 +- 2 files changed, 112 insertions(+), 74 deletions(-) diff --git a/src/tty.c b/src/tty.c index 2c4ace8..36a4bac 100644 --- a/src/tty.c +++ b/src/tty.c @@ -256,7 +256,10 @@ void tty_sync(int fd) cp += count; remain -= count; - /* Reduce the number of additional sends */ + // Update transmit statistics + tx_total += count; + + // Reduce the number of additional writes if (remain > 0) { int estimated_sendtime_us = (int)((int64_t)count * 10 * 1000 * 1000 / option.baudrate); @@ -274,64 +277,127 @@ void tty_sync(int fd) tty_buffer_count = 0; } -ssize_t tty_write(int fd, void *buffer, size_t count) +static ssize_t tty_raw_write(int fd) { - ssize_t retval = 0, bytes_written = 0; - size_t i; - unsigned char *cbuf = (unsigned char *)buffer; - - if (option.map_o_ltu) + if ( ! (option.output_delay || option.output_line_delay || option.map_o_nulbrk) ) { - // Convert lower case to upper case - for (i = 0; i < count; i++) - { - cbuf[i] = toupper(cbuf[i]); - } + return 0; // No-op in this function, write in tty_sync() } - if (option.output_delay || option.output_line_delay) + // Write byte by byte with output delay and null break + ssize_t retval = 0; + size_t i; + + for (i = 0; i < tty_buffer_count; i++) { - // Write byte by byte with output delay - for (i = 0; i < count; i++) + if ((tty_buffer[i] == 0) && (option.map_o_nulbrk)) { - retval = write_poll(fd, &cbuf[i], 1, WRITE_POLL_FOREVER); + retval = tcsendbreak(fd, 0); if (retval < 0) { - // Error - tio_debug_printf("Write error (%s)", strerror(errno)); - return retval; + tio_warning_printf("Could not send break"); + goto error; } - bytes_written += retval; + continue; + } - fsync(fd); - tcdrain(fd); + retval = write_poll(fd, &tty_buffer[i], 1, WRITE_POLL_FOREVER); + if (retval < 0) + { + // Error + tio_debug_printf("Write error (%s)", strerror(errno)); + goto error; + } - if (option.output_line_delay && cbuf[i] == option.output_line_delay_char) - { - delay(option.output_line_delay); - } - else if (option.output_delay) - { - delay(option.output_delay); - } + fsync(fd); + tcdrain(fd); + + // Update transmit statistics + tx_total++; + + if (option.output_line_delay && tty_buffer[i] == option.output_line_delay_char) + { + delay(option.output_line_delay); + } + else if (option.output_delay) + { + delay(option.output_delay); } } - else + retval = tty_buffer_count; + // Path-through + + error: + // Reset + tty_buffer_write_ptr = tty_buffer; + tty_buffer_count = 0; + + return retval; +} + +ssize_t tty_write(int fd, const void *buffer, size_t count) +{ + int status; + const char *cp = (char *)buffer; + size_t i; + + for (i = 0; i < count; i++, cp++) { - // Force write of tty buffer if too full - if ((tty_buffer_count + count) > BUFSIZ) + char *tp; + int bytes_add; + + if (tty_buffer_count >= BUFSIZ) { + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } tty_sync(fd); } - // Copy bytes to tty write buffer - memcpy(tty_buffer_write_ptr, buffer, count); - tty_buffer_write_ptr += count; - tty_buffer_count += count; - bytes_written = count; + /* Map output character */ + bytes_add = -1; /* negative value means "not mapped yet" */ + tp = tty_buffer_write_ptr; + if ((*cp == 127) && (option.map_o_del_bs)) + { + *tp = '\b'; + bytes_add = 1; + } + if ((*cp == '\r') && (option.map_o_cr_nl)) + { + *tp = '\n'; + bytes_add = 1; + } + if ((*cp == '\r') && (option.map_o_ign_cr)) + { + bytes_add = 0; + } + if ((*cp == '\n' || *cp == '\r') && (option.map_o_nl_crnl)) + { + *tp = '\r'; + *(tp + 1) = '\n'; + bytes_add = 2; + } + if (bytes_add < 0) + { + *tp = (option.map_o_ltu) ? toupper(*cp) : *cp; + bytes_add = 1; + } + + if (bytes_add > 0) + { + tty_buffer_write_ptr += bytes_add; + tty_buffer_count += bytes_add; + } } - return bytes_written; + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } + return count; } void *tty_stdin_input_thread(void *arg) @@ -476,10 +542,6 @@ static void handle_hex_prompt(char c) { tio_warning_printf("Could not write to tty device"); } - else - { - tx_total++; - } } } @@ -2520,38 +2582,26 @@ void tty_restore(void) } } +/* output mode and local echo handling */ void forward_to_tty(int fd, char output_char) { int status; - /* Map output character */ - if ((output_char == 127) && (option.map_o_del_bs)) - { - output_char = '\b'; - } - if ((output_char == '\r') && (option.map_o_cr_nl)) - { - output_char = '\n'; - } if ((output_char == '\r') && (option.map_o_ign_cr)) { return; } - /* Map newline character */ + /* local echo special case in newline character */ if ((output_char == '\n' || output_char == '\r') && (option.map_o_nl_crnl)) { - char crlf[] = "\r\n"; - - optional_local_echo(crlf[0]); - optional_local_echo(crlf[1]); - status = tty_write(fd, crlf, 2); + optional_local_echo('\r'); + optional_local_echo('\n'); + status = tty_write(fd, &output_char, 1); if (status < 0) { tio_warning_printf("Could not write to tty device"); } - - tx_total += 2; } else { @@ -2570,22 +2620,11 @@ void forward_to_tty(int fd, char output_char) optional_local_echo(output_char); } - if ((output_char == 0) && (option.map_o_nulbrk)) - { - status = tcsendbreak(fd, 0); - } - else - { - status = tty_write(fd, &output_char, 1); - } - + status = tty_write(fd, &output_char, 1); if (status < 0) { tio_warning_printf("Could not write to tty device"); } - - /* Update transmit statistics */ - tx_total++; } break; @@ -2604,7 +2643,6 @@ void forward_to_tty(int fd, char output_char) else { optional_local_echo(output_char); - tx_total++; } } break; diff --git a/src/tty.h b/src/tty.h index c9d10ec..31ae00e 100644 --- a/src/tty.h +++ b/src/tty.h @@ -86,5 +86,5 @@ void tty_line_set(int fd, tty_line_config_t line_config[]); void tty_search(void); GList *tty_search_for_serial_devices(void); void forward_to_tty(int fd, char output_char); -ssize_t tty_write(int fd, void *buffer, size_t count); +ssize_t tty_write(int fd, const void *buffer, size_t count); void tty_sync(int fd); From 321494b4e6a333f344640f7d2ecd09b8197d2daf Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 17 Jan 2026 11:41:10 +0900 Subject: [PATCH 30/53] Add option --raw/--raw-interactive and Ctrl-t j/J subcommand Non-interactive operations (input from a pipe, running a shell command, XYMODEM transfers) often fail when mapping, soft flow control, or output delay are enabled. To reduce the hassle of switching settings, add the function shown in the title. The raw option can be set to one of the following: - off ... no effects - on ... soft-flow off, character mapping off, output delay enabled - on-nodelay ... soft-flow off, character mapping off, output delay disable raw option is for Piped-input / Shell command execution / XYMODEM transfering. default is on. raw-interactive option is for socket-mode and normal terminal use. default is off. You can type Ctrl-t j if you need to change raw setting for non-interactive case. it toggles the raw setting. You can type Ctrl-t J if you need to change raw setting of interactive case. it toggles the raw setting. It is useful when transferring files in socket mode. --- src/configfile.c | 14 +++ src/options.c | 60 ++++++++- src/options.h | 12 ++ src/script.c | 11 +- src/tty.c | 310 ++++++++++++++++++++++++++++++++++------------- src/tty.h | 11 ++ src/xymodem.c | 41 ++++--- 7 files changed, 357 insertions(+), 102 deletions(-) diff --git a/src/configfile.c b/src/configfile.c index cc21dfb..686cae9 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -212,6 +212,20 @@ static void config_parse_keys(GKeyFile *key_file, char *group) g_free((void *)string); string = NULL; } + config_get_string(key_file, group, "raw", &string, NULL); + if (string != NULL) + { + option_parse_raw(string, &option.raw); + g_free((void *)string); + string = NULL; + } + config_get_string(key_file, group, "raw-interactive", &string, NULL); + if (string != NULL) + { + option_parse_raw(string, &option.raw_interactive); + g_free((void *)string); + string = NULL; + } config_get_bool(key_file, group, "timestamp", (bool*) &option.timestamp); if (option.timestamp != TIMESTAMP_NONE) { diff --git a/src/options.c b/src/options.c index 67c6ed6..d85a5c6 100644 --- a/src/options.c +++ b/src/options.c @@ -60,6 +60,8 @@ enum opt_t OPT_EXCLUDE_DRIVERS, OPT_EXCLUDE_TIDS, OPT_EXEC, + OPT_RAW, + OPT_RAW_INTERACTIVE, }; // clang-format off @@ -128,6 +130,8 @@ struct option_t option = .map_o_nulbrk = false, .map_i_msb2lsb = false, .map_o_ign_cr = false, + .raw = RAW_ON_DELAY, + .raw_interactive = RAW_OFF, }; // clang-format on @@ -170,6 +174,8 @@ void option_print_help(char *argv[]) printf(" -m, --map Map characters\n"); printf(" -c, --color 0..255|bold|none|list Colorize tio text (default: bold)\n"); printf(" -S, --socket Redirect I/O to socket\n"); + printf(" --raw off|on|on-nodelay Select raw mode for non-interactive use (default: on)\n"); + printf(" --raw-interactive off|on|on-nodelay Select raw mode for interactive use (default: off)\n"); printf(" --rs-485 Enable RS-485 mode\n"); printf(" --rs-485-config Set RS-485 configuration\n"); printf(" --alert bell|blink|none Alert on connect/disconnect (default: none)\n"); @@ -453,9 +459,9 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp) } } -const char *option_alert_state_to_string(alert_t state) +const char *option_alert_state_to_string(alert_t alert_state) { - switch (state) + switch (alert_state) { case ALERT_NONE: return "none"; @@ -729,6 +735,44 @@ const char *option_output_mode_to_string(output_mode_t mode) return NULL; } +void option_parse_raw(const char *arg, raw_t *raw) +{ + assert(arg != NULL); + + if (strcmp("off", arg) == 0) + { + *raw = RAW_OFF; + } + else if (strcmp("on", arg) == 0) + { + *raw = RAW_ON_DELAY; + } + else if (strcmp("on-nodelay", arg) == 0) + { + *raw = RAW_ON_NODELAY; + } + else + { + tio_error_print("Invalid raw option '%s'", arg); + exit(EXIT_FAILURE); + } +} + +const char *option_raw_to_string(raw_t raw) +{ + switch (raw) + { + case RAW_OFF: + return "off"; + case RAW_ON_DELAY: + return "on"; + case RAW_ON_NODELAY: + return "on-nodelay"; + } + + return NULL; +} + void option_parse_script_run(const char *arg, script_run_t *script_run) { assert(arg != NULL); @@ -872,6 +916,8 @@ void options_print() // clang-format on tio_printf(" Input mode: %s", option_input_mode_to_string(option.input_mode)); tio_printf(" Output mode: %s", option_output_mode_to_string(option.output_mode)); + tio_printf(" Raw (non-interactive): %s", option_raw_to_string(option.raw)); + tio_printf(" Raw interactive: %s", option_raw_to_string(option.raw_interactive)); tio_printf(" Alert: %s", option_alert_state_to_string(option.alert)); if (option.log) { @@ -953,6 +999,8 @@ void options_parse(int argc, char *argv[]) {"color", required_argument, 0, 'c' }, {"input-mode", required_argument, 0, OPT_INPUT_MODE }, {"output-mode", required_argument, 0, OPT_OUTPUT_MODE }, + {"raw", required_argument, 0, OPT_RAW }, + {"raw-interactive", required_argument, 0, OPT_RAW_INTERACTIVE }, {"rs-485", no_argument, 0, OPT_RS485 }, {"rs-485-config", required_argument, 0, OPT_RS485_CONFIG }, {"alert", required_argument, 0, OPT_ALERT }, @@ -1147,6 +1195,14 @@ void options_parse(int argc, char *argv[]) option.exec = optarg; break; + case OPT_RAW: + option_parse_raw(optarg, &option.raw); + break; + + case OPT_RAW_INTERACTIVE: + option_parse_raw(optarg, &option.raw_interactive); + break; + case 'v': printf("tio %s\n", VERSION); exit(EXIT_SUCCESS); diff --git a/src/options.h b/src/options.h index 4f501ae..337768c 100644 --- a/src/options.h +++ b/src/options.h @@ -43,6 +43,13 @@ typedef enum OUTPUT_MODE_END, } output_mode_t; +typedef enum +{ + RAW_OFF, + RAW_ON_DELAY, + RAW_ON_NODELAY, +} raw_t; + /* Options */ struct option_t { @@ -75,6 +82,8 @@ struct option_t int color; input_mode_t input_mode; output_mode_t output_mode; + raw_t raw; + raw_t raw_interactive; char prefix_code; char prefix_key; bool prefix_enabled; @@ -123,6 +132,7 @@ void option_parse_parity(const char *arg, parity_t *parity); void option_parse_output_mode(const char *arg, output_mode_t *mode); void option_parse_input_mode(const char *arg, input_mode_t *mode); +void option_parse_raw(const char *arg, raw_t *raw); void option_parse_output_line_delay_char(const char *arg); void option_parse_line_pulse_duration(const char *arg); @@ -136,3 +146,5 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp); const char* option_timestamp_format_to_string(timestamp_t timestamp); void option_parse_mappings(const char *map); + +const char* option_raw_to_string(raw_t raw); diff --git a/src/script.c b/src/script.c index 269bd69..45e9fea 100644 --- a/src/script.c +++ b/src/script.c @@ -267,6 +267,10 @@ static int api_send(lua_State *L) return 0; } + state_t state_orig = state; + state = STATE_XYMODEM; + tty_tcsetattr(device_fd); + switch (protocol) { case XMODEM_1K: @@ -294,6 +298,9 @@ static int api_send(lua_State *L) break; } + state = state_orig; + tty_tcsetattr(device_fd); + return 0; } @@ -704,9 +711,9 @@ void script_run_as_specified_by_options(void) } } -const char *script_run_state_to_string(script_run_t state) +const char *script_run_state_to_string(script_run_t run_state) { - switch (state) + switch (run_state) { case SCRIPT_RUN_ONCE: return "once"; diff --git a/src/tty.c b/src/tty.c index 36a4bac..441ea3d 100644 --- a/src/tty.c +++ b/src/tty.c @@ -117,6 +117,8 @@ #define KEY_SHIFT_F 0x46 #define KEY_G 0x67 #define KEY_I 0x69 +#define KEY_J 0x6A +#define KEY_SHIFT_J 0x4A #define KEY_L 0x6C #define KEY_SHIFT_L 0x4C #define KEY_M 0x6D @@ -166,12 +168,13 @@ const char random_array[] = // clang-format on bool interactive_mode = true; +state_t state = STATE_STARTING; char key_hit = 0xff; const char* device_name = NULL; GList *device_list = NULL; -static struct termios tio, tio_old, stdout_new, stdout_old, stdin_new, stdin_old; +static struct termios tio, tio_raw, tio_old, stdout_new, stdout_old, stdin_new, stdin_old; static unsigned long rx_total = 0, tx_total = 0; static bool connected = false; static bool standard_baudrate = true; @@ -236,6 +239,59 @@ inline static unsigned char char_to_nibble(char c) } } +raw_t tty_get_raw_mode(void) +{ + switch (state) + { + case STATE_INTERACTIVE: + return option.raw_interactive; + + case STATE_STARTING: + case STATE_PIPED_INPUT: + case STATE_EXEC_SHELL_COMMAND: + case STATE_XYMODEM: + default: + return option.raw; + } +} + +int tty_tcsetattr(int fd) +{ + if ( ! connected ) + { + return -1; + } + + /* Activate or change port settings */ + int status; + raw_t raw = tty_get_raw_mode(); + if (raw == RAW_OFF) + { + status = tcsetattr(fd, TCSANOW, &tio); + } + else + { + status = tcsetattr(fd, TCSANOW, &tio_raw); + } + if (status == -1) + { + tio_error_printf_silent("Could not apply port settings (%s)", strerror(errno)); + return -1; + } + + /* Set arbitrary baudrate (only works on supported platforms) */ + if (!standard_baudrate) + { + if (setspeed(device_fd, option.baudrate) != 0) + { + tio_error_printf_silent("Could not set baudrate speed (%s)", strerror(errno)); + return -1; + } + } + + return 0; +} + void tty_sync(int fd) { /* If output_delay is valid, tty_buffer should be already empty. @@ -279,7 +335,10 @@ void tty_sync(int fd) static ssize_t tty_raw_write(int fd) { - if ( ! (option.output_delay || option.output_line_delay || option.map_o_nulbrk) ) + raw_t raw = tty_get_raw_mode(); + if ((raw == RAW_ON_NODELAY) || + ((raw == RAW_ON_DELAY) && ( ! option.output_delay )) || + ((raw == RAW_OFF) && ( ! (option.output_delay || option.output_line_delay || option.map_o_nulbrk) ))) { return 0; // No-op in this function, write in tty_sync() } @@ -290,7 +349,7 @@ static ssize_t tty_raw_write(int fd) for (i = 0; i < tty_buffer_count; i++) { - if ((tty_buffer[i] == 0) && (option.map_o_nulbrk)) + if ((raw == RAW_OFF) && (tty_buffer[i] == 0) && (option.map_o_nulbrk)) { retval = tcsendbreak(fd, 0); if (retval < 0) @@ -315,7 +374,7 @@ static ssize_t tty_raw_write(int fd) // Update transmit statistics tx_total++; - if (option.output_line_delay && tty_buffer[i] == option.output_line_delay_char) + if ((raw == RAW_OFF) && (option.output_line_delay) && (tty_buffer[i] == option.output_line_delay_char)) { delay(option.output_line_delay); } @@ -340,55 +399,79 @@ ssize_t tty_write(int fd, const void *buffer, size_t count) int status; const char *cp = (char *)buffer; size_t i; - - for (i = 0; i < count; i++, cp++) + raw_t raw = tty_get_raw_mode(); + if (raw != RAW_OFF) { - char *tp; - int bytes_add; - - if (tty_buffer_count >= BUFSIZ) + /* RAW mode */ + for (i = 0; i < count; i++, cp++) { - status = tty_raw_write(fd); - if (status < 0) + if (tty_buffer_count >= BUFSIZ) { - return status; + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } + tty_sync(fd); } - tty_sync(fd); - } - /* Map output character */ - bytes_add = -1; /* negative value means "not mapped yet" */ - tp = tty_buffer_write_ptr; - if ((*cp == 127) && (option.map_o_del_bs)) - { - *tp = '\b'; - bytes_add = 1; + *tty_buffer_write_ptr = *cp; + tty_buffer_write_ptr++; + tty_buffer_count++; } - if ((*cp == '\r') && (option.map_o_cr_nl)) + } + else + { + /* not RAW mode */ + for (i = 0; i < count; i++, cp++) { - *tp = '\n'; - bytes_add = 1; - } - if ((*cp == '\r') && (option.map_o_ign_cr)) - { - bytes_add = 0; - } - if ((*cp == '\n' || *cp == '\r') && (option.map_o_nl_crnl)) - { - *tp = '\r'; - *(tp + 1) = '\n'; - bytes_add = 2; - } - if (bytes_add < 0) - { - *tp = (option.map_o_ltu) ? toupper(*cp) : *cp; - bytes_add = 1; - } + if (tty_buffer_count >= BUFSIZ) + { + status = tty_raw_write(fd); + if (status < 0) + { + return status; + } + tty_sync(fd); + } - if (bytes_add > 0) - { - tty_buffer_write_ptr += bytes_add; - tty_buffer_count += bytes_add; + /* Map output character */ + char *tp; + int bytes_add; + + bytes_add = -1; /* negative value means "not mapped yet" */ + tp = tty_buffer_write_ptr; + if ((*cp == 127) && (option.map_o_del_bs)) + { + *tp = '\b'; + bytes_add = 1; + } + if ((*cp == '\r') && (option.map_o_cr_nl)) + { + *tp = '\n'; + bytes_add = 1; + } + if ((*cp == '\r') && (option.map_o_ign_cr)) + { + bytes_add = 0; + } + if ((*cp == '\n' || *cp == '\r') && (option.map_o_nl_crnl)) + { + *tp = '\r'; + *(tp + 1) = '\n'; + bytes_add = 2; + } + if (bytes_add < 0) + { + *tp = (option.map_o_ltu) ? toupper(*cp) : *cp; + bytes_add = 1; + } + + if (bytes_add > 0) + { + tty_buffer_write_ptr += bytes_add; + tty_buffer_count += bytes_add; + } } } @@ -568,10 +651,10 @@ static const char *tty_line_name(int mask) void tty_line_set(int fd, tty_line_config_t line_config[]) { - static int state; + static int line_state; int i = 0; - if (ioctl(fd, TIOCMGET, &state) < 0) + if (ioctl(fd, TIOCMGET, &line_state) < 0) { tio_warning_printf("Could not get line state (%s)", strerror(errno)); return; @@ -584,21 +667,21 @@ void tty_line_set(int fd, tty_line_config_t line_config[]) if (line_config[i].value == 0) { // Low - state |= line_config[i].mask; + line_state |= line_config[i].mask; tio_printf("Setting %s to LOW", tty_line_name(line_config[i].mask)); } else if (line_config[i].value == 1) { // High - state &= ~line_config[i].mask; + line_state &= ~line_config[i].mask; tio_printf("Setting %s to HIGH", tty_line_name(line_config[i].mask)); } else if (line_config[i].value == 2) { // Toggle - state ^= line_config[i].mask; + line_state ^= line_config[i].mask; - if (state & line_config[i].mask) + if (line_state & line_config[i].mask) { tio_printf("Setting %s to LOW", tty_line_name(line_config[i].mask)); } @@ -610,7 +693,7 @@ void tty_line_set(int fd, tty_line_config_t line_config[]) } } - if (ioctl(fd, TIOCMSET, &state) < 0) + if (ioctl(fd, TIOCMSET, &line_state) < 0) { tio_warning_printf("Could not set line state (%s)", strerror(errno)); } @@ -618,26 +701,26 @@ void tty_line_set(int fd, tty_line_config_t line_config[]) void tty_line_toggle(int fd, int mask) { - int state; + int line_state; - if (ioctl(fd, TIOCMGET, &state) < 0) + if (ioctl(fd, TIOCMGET, &line_state) < 0) { tio_warning_printf("Could not get line state (%s)", strerror(errno)); return; } - if (state & mask) + if (line_state & mask) { - state &= ~mask; + line_state &= ~mask; tio_printf("Setting %s to HIGH", tty_line_name(mask)); } else { - state |= mask; + line_state |= mask; tio_printf("Setting %s to LOW", tty_line_name(mask)); } - if (ioctl(fd, TIOCMSET, &state) < 0) + if (ioctl(fd, TIOCMSET, &line_state) < 0) { tio_warning_printf("Could not set line state (%s)", strerror(errno)); } @@ -789,7 +872,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) { char unused_char; bool unused_bool; - int state; + int line_state; static tty_line_mode_t line_mode; static sub_command_t sub_command = SUBCOMMAND_NONE; static char previous_char = 0; @@ -844,6 +927,9 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) break; case SUBCOMMAND_XMODEM: + state_t state_orig = state; + state = STATE_XYMODEM; + tty_tcsetattr(device_fd); switch (input_char) { case KEY_0: @@ -915,6 +1001,8 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_error_print("Invalid protocol option"); break; } + state = state_orig; + tty_tcsetattr(device_fd); break; case SUBCOMMAND_MAP: @@ -1015,6 +1103,8 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" ctrl-%c F Flush data I/O buffers", option.prefix_key); tio_printf(" ctrl-%c g Toggle serial port line", option.prefix_key); tio_printf(" ctrl-%c i Toggle input mode", option.prefix_key); + tio_printf(" ctrl-%c j Toggle raw mode for non-interactive use", option.prefix_key); + tio_printf(" ctrl-%c J Toggle raw mode for interactive use", option.prefix_key); tio_printf(" ctrl-%c l Clear screen", option.prefix_key); tio_printf(" ctrl-%c L Show line states", option.prefix_key); tio_printf(" ctrl-%c m Change mapping of characters on input or output", option.prefix_key); @@ -1032,18 +1122,18 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) break; case KEY_SHIFT_L: - if (ioctl(device_fd, TIOCMGET, &state) < 0) + if (ioctl(device_fd, TIOCMGET, &line_state) < 0) { tio_warning_printf("Could not get line state (%s)", strerror(errno)); break; } tio_printf("Line states:"); - tio_printf(" DTR: %s", (state & TIOCM_DTR) ? "LOW" : "HIGH"); - tio_printf(" RTS: %s", (state & TIOCM_RTS) ? "LOW" : "HIGH"); - tio_printf(" CTS: %s", (state & TIOCM_CTS) ? "LOW" : "HIGH"); - tio_printf(" DSR: %s", (state & TIOCM_DSR) ? "LOW" : "HIGH"); - tio_printf(" DCD: %s", (state & TIOCM_CD) ? "LOW" : "HIGH"); - tio_printf(" RI : %s", (state & TIOCM_RI) ? "LOW" : "HIGH"); + tio_printf(" DTR: %s", (line_state & TIOCM_DTR) ? "LOW" : "HIGH"); + tio_printf(" RTS: %s", (line_state & TIOCM_RTS) ? "LOW" : "HIGH"); + tio_printf(" CTS: %s", (line_state & TIOCM_CTS) ? "LOW" : "HIGH"); + tio_printf(" DSR: %s", (line_state & TIOCM_DSR) ? "LOW" : "HIGH"); + tio_printf(" DCD: %s", (line_state & TIOCM_CD) ? "LOW" : "HIGH"); + tio_printf(" RI : %s", (line_state & TIOCM_RI) ? "LOW" : "HIGH"); break; case KEY_F: @@ -1155,6 +1245,42 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) } break; + case KEY_J: + option.raw += 1; + switch (option.raw) + { + case RAW_ON_DELAY: + tio_printf("Turn on raw mode for non-interactive use"); + break; + case RAW_ON_NODELAY: + tio_printf("Turn on raw-nodelay mode for non-interactive use"); + break; + case RAW_OFF: + default: + option.raw = RAW_OFF; + tio_printf("Turn off raw mode for non-interactive use"); + break; + } + break; + + case KEY_SHIFT_J: + option.raw_interactive += 1; + switch (option.raw_interactive) + { + case RAW_ON_DELAY: + tio_printf("Turn on raw mode for interactive use"); + break; + case RAW_ON_NODELAY: + tio_printf("Turn on raw-nodelay mode for interactive use"); + break; + case RAW_OFF: + default: + option.raw_interactive = RAW_OFF; + tio_printf("Turn off raw mode for interactive use"); + break; + } + break; + case KEY_L: /* Clear screen using ANSI/VT100 escape code */ printf("\033c"); @@ -1219,7 +1345,14 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) /* Execute shell command */ tio_printf("Execute shell command with I/O redirected to device"); if (tio_subcmd_readln("Enter command: ")) + { + state_t state_orig = state; + state = STATE_EXEC_SHELL_COMMAND; + tty_tcsetattr(device_fd); execute_shell_command(device_fd, line); + state = state_orig; + tty_tcsetattr(device_fd); + } break; case KEY_S: @@ -1280,10 +1413,15 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) if (tio_subcmd_readln("Enter file name: ")) { int ret; + state_t state_orig = state; tio_printf("Sending file '%s' ", line); tio_printf("Press any key to abort transfer"); + state = STATE_XYMODEM; + tty_tcsetattr(device_fd); ret = xymodem_send(device_fd, line, YMODEM); + state = state_orig; + tty_tcsetattr(device_fd); tio_printf("%s", ret < 0 ? "Aborted" : "Done"); } break; @@ -1560,6 +1698,14 @@ void tty_configure(void) { tio.c_iflag |= ICRNL; } + + /* Create raw-mode configuration */ + memcpy(&tio_raw, &tio, sizeof(tio_raw)); + if (option.flow == FLOW_SOFT) + { + tio_raw.c_iflag &= ~(IXON | IXOFF | IXANY); + tio_raw.c_iflag &= ~(INLCR | IGNCR | ICRNL); + } } void tty_reconfigure(void) @@ -2680,6 +2826,8 @@ int tty_connect(void) char* now = NULL; struct timeval tval_before = {}, tval_now, tval_result; + state = STATE_STARTING; + /* Open tty device */ device_fd = open(device_name, O_RDWR | O_NOCTTY | O_NONBLOCK); if (device_fd < 0) @@ -2692,7 +2840,7 @@ int tty_connect(void) if (!isatty(device_fd)) { tio_error_printf("Not a tty device"); - exit(EXIT_FAILURE);; + exit(EXIT_FAILURE); } /* Lock device file */ @@ -2754,27 +2902,17 @@ int tty_connect(void) first = false; } - /* Activate new port settings */ - status = tcsetattr(device_fd, TCSANOW, &tio); + /* Activate new port settings and set speed for non-interactive phase */ + status = tty_tcsetattr(device_fd); if (status == -1) { - tio_error_printf_silent("Could not apply port settings (%s)", strerror(errno)); goto error_tcsetattr; } - /* Set arbitrary baudrate (only works on supported platforms) */ - if (!standard_baudrate) - { - if (setspeed(device_fd, option.baudrate) != 0) - { - tio_error_printf_silent("Could not set baudrate speed (%s)", strerror(errno)); - goto error_setspeed; - } - } - /* If stdin is a pipe forward all input to tty device */ if (interactive_mode == false) { + state = STATE_PIPED_INPUT; while (true) { int ret = read(pipefd[0], input_buffer, BUFSIZ); @@ -2815,7 +2953,7 @@ int tty_connect(void) } } - // Exit if piped input + // Exit if piped input or given execute option if (interactive_mode == false) { exit(EXIT_SUCCESS); @@ -2823,10 +2961,19 @@ int tty_connect(void) if (option.exec != NULL) { + state = STATE_EXEC_SHELL_COMMAND; status = execute_shell_command(device_fd, option.exec); exit(status); } + /* Activate new port settings for interactive phase */ + state = STATE_INTERACTIVE; + status = tty_tcsetattr(device_fd); + if (status == -1) + { + goto error_tcsetattr; + } + /* Input loop */ while (true) { @@ -3149,7 +3296,6 @@ int tty_connect(void) return TIO_SUCCESS; -error_setspeed: error_tcsetattr: error_tcgetattr: error_read: diff --git a/src/tty.h b/src/tty.h index 31ae00e..5135394 100644 --- a/src/tty.h +++ b/src/tty.h @@ -69,8 +69,18 @@ typedef struct bool reserved; } tty_line_config_t; +typedef enum +{ + STATE_INTERACTIVE, + STATE_STARTING, + STATE_PIPED_INPUT, + STATE_EXEC_SHELL_COMMAND, + STATE_XYMODEM, +} state_t; + extern const char *device_name; extern bool interactive_mode; +extern state_t state; void stdout_configure(void); void stdin_configure(void); @@ -88,3 +98,4 @@ GList *tty_search_for_serial_devices(void); void forward_to_tty(int fd, char output_char); ssize_t tty_write(int fd, const void *buffer, size_t count); void tty_sync(int fd); +int tty_tcsetattr(int fd); diff --git a/src/xymodem.c b/src/xymodem.c index 5190e40..33b6924 100644 --- a/src/xymodem.c +++ b/src/xymodem.c @@ -185,6 +185,15 @@ static int xmsend_wait_response(int sio, char *resp, char tmo_resp) return OK; } +static ssize_t xmodem_tty_write(int sio, const void *buf, size_t bufsiz) +{ + ssize_t ret = tty_write(sio, buf, bufsiz); + if (ret >= 0) { + tty_sync(sio); + } + return ret; +} + /* * Send EOT at 1 Hz until ACK or CAN received */ @@ -198,7 +207,7 @@ static int xmsend_repeat_eot_and_wait_response(int sio) if (key_hit) return ERR_USER_CAN; - if (write(sio, EOT_STR, 1) < 0) + if (xmodem_tty_write(sio, EOT_STR, 1) < 0) { tio_error_print("Write EOT to serial failed"); return ERR; @@ -265,7 +274,7 @@ static int xmodem_send_1k(int sio, const void *data, size_t len, int seq) if (key_hit) return ERR_USER_CAN; - if ((rc = write(sio, from, sz)) < 0 ) + if ((rc = xmodem_tty_write(sio, from, sz)) < 0 ) { if (errno == EWOULDBLOCK) { @@ -398,7 +407,7 @@ static int xmodem_send_128b(int sio, const void *data, size_t len, bool use_crc) if (key_hit) return ERR_USER_CAN; - if ((rc = write(sio, from, sz)) < 0 ) + if ((rc = xmodem_tty_write(sio, from, sz)) < 0 ) { if (errno == EWOULDBLOCK) { @@ -524,9 +533,9 @@ static int xmrecv_start_receive(int sio, bool use_crc) seconds. If nothing is received in that time then return false to indicate that the transfer did not start. */ if (use_crc) - rc = write(sio, "C", 1); + rc = xmodem_tty_write(sio, "C", 1); else - rc = write(sio, NAK_STR, 1); + rc = xmodem_tty_write(sio, NAK_STR, 1); if (rc < 0) { @@ -611,7 +620,7 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b_crc *packet, int f if (rc == 0) { tio_error_print("Timeout waiting for next packet char"); - rc = write(sio, CAN_STR, 1); + rc = xmodem_tty_write(sio, CAN_STR, 1); if (rc < 0) { tio_error_print("Write cancel packet to serial failed"); @@ -622,7 +631,7 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b_crc *packet, int f else if (rc < 0) { tio_error_print("Error reading next packet char"); - rc = write(sio, CAN_STR, 1); + rc = xmodem_tty_write(sio, CAN_STR, 1); if (rc < 0) { tio_error_print("Write cancel packet to serial failed"); @@ -702,7 +711,7 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b_crc *packet, int f { tio_error_print("%s", strerror(errno)); tio_error_print("Poll check error after packet finish"); - rc = write(sio, CAN_STR, 1); + rc = xmodem_tty_write(sio, CAN_STR, 1); if (rc < 0) { tio_error_print("Write cancel packet to serial failed"); @@ -732,7 +741,7 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b_crc *packet, int f if ((calcCrc == rxCrc) && (seq1 == packet->hdr.seq - 1) && ((seq1 ^ seq2) == tester)) { /* Resend of previously processed packet. */ - rc = write(sio, ACK_STR, 1); + rc = xmodem_tty_write(sio, ACK_STR, 1); if (rc < 0) { tio_error_print("Write acknowlegdement packet to serial failed"); @@ -755,18 +764,18 @@ static int xmrecv_receive_packet(int sio, struct xpacket_128b_crc *packet, int f else { /* The data is good. Process the packet then ACK it to the sender. */ - rc = write(fd, packet->data, sizeof(packet->data)); + rc = xmodem_tty_write(fd, packet->data, sizeof(packet->data)); if (rc < 0) { tio_error_print("Problem writing to file"); - rc = write(sio, CAN_STR, 1); + rc = xmodem_tty_write(sio, CAN_STR, 1); if (rc < 0) { tio_error_print("Write cancel packet to serial failed"); } return ERR_FATAL; } - rc = write(sio, ACK_STR, 1); + rc = xmodem_tty_write(sio, ACK_STR, 1); if (rc < 0) { tio_error_print("Write acknowlegdement packet to serial failed"); @@ -840,7 +849,7 @@ int xmodem_receive(int sio, int fd, bool use_crc) } else if (err == ERR || err == ERR_TMO) { - rc = write(sio, NAK_STR, 1); + rc = xmodem_tty_write(sio, NAK_STR, 1); if (rc < 0) { tio_error_print("Writing not acknowledge packet to serial failed"); @@ -855,7 +864,7 @@ int xmodem_receive(int sio, int fd, bool use_crc) } else if (err == ERR_USER_CAN) { - rc = write(sio, CAN_STR, 1); + rc = xmodem_tty_write(sio, CAN_STR, 1); if (rc < 0) { tio_error_print("Writing cancel to serial failed"); @@ -871,7 +880,7 @@ int xmodem_receive(int sio, int fd, bool use_crc) case EOT: /* End of Transfer */ - rc = write(sio, ACK_STR, 1); + rc = xmodem_tty_write(sio, ACK_STR, 1); if (rc < 0) { tio_error_print("Write acknowlegdement packet to serial failed"); @@ -889,7 +898,7 @@ int xmodem_receive(int sio, int fd, bool use_crc) break; default: - tio_error_print("Unexpected character received waiting for next packet"); + tio_error_print("Unexpected character received waiting for next packet (0x%02x)", resp); return ERR; break; } From 445a21b80710c9864cc690d940e624a02d51aca7 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 18 Jan 2026 15:39:12 +0900 Subject: [PATCH 31/53] Add bidirectional redirection to shell command execution function (Ctrl-t R and --exec option) The "shell command execution feature" previously only supported the ability to transfer a command's stdout and stderr to a device. To support bidirectional commands, a feature will be added to connect input from a device to the command's stdin. Since some communication commands (sz, rz) require stderr to be kept local, a feature will also be added to not transfer stderr to a device if the command string begins with a '?'. On Linux, you can terminate a running command by pressing Ctrl-t R again while the command is running (this uses the command /usr/bin/pkill internally). --- src/misc.c | 137 +++++++++++++++++++++++++++++++++++++++++++++-------- src/misc.h | 4 ++ src/tty.c | 11 ++++- src/tty.h | 1 + 4 files changed, 131 insertions(+), 22 deletions(-) diff --git a/src/misc.c b/src/misc.c index ec39f51..eff7e3d 100644 --- a/src/misc.c +++ b/src/misc.c @@ -28,6 +28,9 @@ #include #include #include "print.h" +#include "misc.h" + +static pid_t shell_command_pid = 0; void delay(long ms) { @@ -111,7 +114,7 @@ int read_poll(int fd, void *data, size_t len, int timeout) return 0; } -ssize_t write_poll(int fd, void *data, size_t len, int timeout) +ssize_t write_poll(int fd, const void *data, size_t len, int timeout) { struct pollfd fds; ssize_t ret = 0; @@ -234,46 +237,72 @@ bool match_patterns(const char *string, const char *patterns) // specified filedescriptor, and runs command. int execute_shell_command(int fd, const char *command) { -#define READ_END 0 -#define WRITE_END 1 - pid_t pid; + #define READ_END 0 + #define WRITE_END 1 int status; - int pipe_fd[2]; + int pipefd_c2p[2]; + int pipefd_p2c[2]; + +#if defined(__linux__) + static bool done_once = false; + if (!done_once) + { + atexit(&terminate_shell_command); + done_once = true; + } +#endif // Create Pipes - if (pipe(pipe_fd) == -1) + if (pipe(pipefd_c2p) == -1 || pipe(pipefd_p2c) == -1) { tio_error_print("pipe() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); } // Fork a child process - pid = fork(); - if (pid == -1) + shell_command_pid = fork(); + if (shell_command_pid == -1) { // Error occurred tio_error_print("fork() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); } - else if (pid == 0) + else if (shell_command_pid == 0) { // Child process + close(pipefd_c2p[READ_END]); + close(pipefd_p2c[WRITE_END]); tio_printf("Executing shell command '%s'", command); - // Redirect stdout and stderr to the file descriptor - close(pipe_fd[READ_END]); - if (dup2(pipe_fd[WRITE_END], STDOUT_FILENO) == -1 || dup2(pipe_fd[WRITE_END], STDERR_FILENO) == -1) + // Redirect stdin and stdout to the parent-pipe + if (dup2(pipefd_c2p[WRITE_END], STDOUT_FILENO) == -1 || + dup2(pipefd_p2c[READ_END], STDIN_FILENO) == -1) { tio_error_print("dup2() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); } + // command prefix '?' excludes stderr from redirection + if (command[0] == '?') + { + command += 1; + } + else + { + if (dup2(pipefd_c2p[WRITE_END], STDERR_FILENO) == -1) + { + tio_error_print("dup2() failed (%s)", strerror(errno)); + exit(EXIT_FAILURE); + } + } + // Execute the shell command execl("/bin/sh", "sh", "-c", command, (char *)NULL); // If execlp() returns, it means an error occurred - close(pipe_fd[WRITE_END]); + close(pipefd_c2p[WRITE_END]); + close(pipefd_p2c[READ_END]); perror("execlp"); tio_error_print("execlp() failed (%s)", strerror(errno)); exit(EXIT_FAILURE); @@ -281,23 +310,69 @@ int execute_shell_command(int fd, const char *command) else { // Parent process + fd_set rdfs; + int maxfd; char buf[BUFSIZ]; int bytes; - // Read pipe and transfer to tty device. - close(pipe_fd[WRITE_END]); - while ( (bytes = read(pipe_fd[READ_END], buf, sizeof(buf))) > 0) + close(pipefd_c2p[WRITE_END]); + close(pipefd_p2c[READ_END]); + + while (true) { - if (tty_write(fd, buf, bytes) < 0) + FD_ZERO(&rdfs); + FD_SET(fd, &rdfs); + FD_SET(pipefd_c2p[READ_END], &rdfs); + maxfd = MAX(fd, pipefd_c2p[READ_END]); + + /* Block until input becomes available or timeout */ + status = select(maxfd + 1, &rdfs, NULL, NULL, NULL); + if (status < 0) { - tio_warning_printf("Could not write to tty device"); + tio_warning_printf("select() failed(%s)", strerror(errno)); + break; + } + + if (FD_ISSET(fd, &rdfs)) + { + bytes = read(fd, buf, sizeof(buf)); + if (bytes <= 0) + { + tio_warning_printf("Could not read from tty device"); + break; + } + rx_total += bytes; + write(pipefd_p2c[WRITE_END], buf, bytes); + } + + if (FD_ISSET(pipefd_c2p[READ_END], &rdfs)) + { + // Read pipe and transfer to tty device. + bytes = read(pipefd_c2p[READ_END], buf, sizeof(buf)); + if (bytes < 0) + { + tio_warning_printf("Could not write to tty device"); + } + else if (bytes == 0) + { + // Shell command has finished. + break; + } + + if (tty_write(fd, buf, bytes) < 0) + { + tio_warning_printf("Could not write to tty device"); + } + tty_sync(fd); } } - tty_sync(fd); - close(pipe_fd[READ_END]); + + close(pipefd_p2c[WRITE_END]); + close(pipefd_c2p[READ_END]); // Wait for the child process to finish - waitpid(pid, &status, 0); + waitpid(shell_command_pid, &status, 0); + shell_command_pid = 0; if (WIFEXITED(status)) { @@ -313,6 +388,26 @@ int execute_shell_command(int fd, const char *command) return 0; } +#if defined(__linux__) + +void terminate_shell_command(void) +{ + // If previous shell command pid is remain, terminate it. + if (shell_command_pid != 0) + { + #define PKILL_BUFSIZ 80 + char pkill_buf[PKILL_BUFSIZ] = {0}; + int bytes; + bytes = snprintf(pkill_buf, PKILL_BUFSIZ, "/usr/bin/pkill -P %d", shell_command_pid); + if (bytes > 0 && bytes < PKILL_BUFSIZ) + { + system(pkill_buf); + } + } +} + +#endif + void clear_line() { printf("\r\033[K"); diff --git a/src/misc.h b/src/misc.h index 9f26d82..c064bde 100644 --- a/src/misc.h +++ b/src/misc.h @@ -38,3 +38,7 @@ double get_current_time(void); bool match_patterns(const char *string, const char *patterns); int execute_shell_command(int fd, const char *command); void clear_line(); + +#if defined(__linux__) +void terminate_shell_command(void); +#endif diff --git a/src/tty.c b/src/tty.c index 441ea3d..3cf6628 100644 --- a/src/tty.c +++ b/src/tty.c @@ -175,7 +175,7 @@ char key_hit = 0xff; const char* device_name = NULL; GList *device_list = NULL; static struct termios tio, tio_raw, tio_old, stdout_new, stdout_old, stdin_new, stdin_old; -static unsigned long rx_total = 0, tx_total = 0; +unsigned long rx_total = 0, tx_total = 0; static bool connected = false; static bool standard_baudrate = true; static void (*printchar)(char c); @@ -557,6 +557,15 @@ void *tty_stdin_input_thread(void *arg) tio_printf("Flushed data I/O buffers"); tcflush(device_fd, TCIOFLUSH); break; +#if defined(__linux__) + case KEY_SHIFT_R: + if (state == STATE_EXEC_SHELL_COMMAND) + { + tio_printf("Terminated shell command"); + terminate_shell_command(); + } + break; +#endif default: break; } diff --git a/src/tty.h b/src/tty.h index 5135394..987e57f 100644 --- a/src/tty.h +++ b/src/tty.h @@ -81,6 +81,7 @@ typedef enum extern const char *device_name; extern bool interactive_mode; extern state_t state; +extern unsigned long rx_total, tx_total; void stdout_configure(void); void stdin_configure(void); From e1d1c6f2bcc62114e48285247627aa2411aca829 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Mon, 19 Jan 2026 22:48:19 +0900 Subject: [PATCH 32/53] Fix incorrect condition for clearing input map and soft-flow in raw mode --- src/tty.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/tty.c b/src/tty.c index 3cf6628..44ea558 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1710,11 +1710,8 @@ void tty_configure(void) /* Create raw-mode configuration */ memcpy(&tio_raw, &tio, sizeof(tio_raw)); - if (option.flow == FLOW_SOFT) - { - tio_raw.c_iflag &= ~(IXON | IXOFF | IXANY); - tio_raw.c_iflag &= ~(INLCR | IGNCR | ICRNL); - } + tio_raw.c_iflag &= ~(IXON | IXOFF | IXANY); + tio_raw.c_iflag &= ~(INLCR | IGNCR | ICRNL); } void tty_reconfigure(void) From 0da8731c0be8ecb65e45b6d9adaa08dca6de3944 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Mon, 19 Jan 2026 22:54:34 +0900 Subject: [PATCH 33/53] Fix tty_configure to change settings depending on the state of the raw option --- src/tty.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tty.c b/src/tty.c index 44ea558..088a876 100644 --- a/src/tty.c +++ b/src/tty.c @@ -1721,7 +1721,7 @@ void tty_reconfigure(void) if (connected) { /* Activate new port settings */ - tcsetattr(device_fd, TCSANOW, &tio); + tty_tcsetattr(device_fd); } } From c009ed755cf651b8829895bd0ef343d1e65588c3 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 1 Feb 2026 11:56:29 +0900 Subject: [PATCH 34/53] Modified the Lua API timeout argument specification to be an extension of the v3.9 specification Fix the v3.9-incompatible changes in 8e02cde ("Lua API Timeout Specification Changes to Enable Non-Blocking Reads", November 15, 2025) to make them compatible. The timeout arguments for tio.expect/s(), tio.read(), and tio.readline() have been changed as follows: - The timeout is in milliseconds, and the default is 0, which means to wait forever (as in v3.9). - A negative value means to nowait. A constant table (tio.C) has also been added, defining tio.C.FOREVER to 0 and tio.C.NOWAIT to -1. --- src/misc.h | 3 ++- src/script.c | 37 +++++++++++++++++++++++++++++-------- src/tty.c | 4 ++-- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/misc.h b/src/misc.h index c064bde..6edbb9c 100644 --- a/src/misc.h +++ b/src/misc.h @@ -24,7 +24,8 @@ #include #include -#define WRITE_POLL_FOREVER (-1) +#define POLL_NOWAIT (0) +#define POLL_FOREVER (-1) #define UNUSED(expr) do { (void)(expr); } while (0) void delay(long ms); diff --git a/src/script.c b/src/script.c index 45e9fea..2bf4a66 100644 --- a/src/script.c +++ b/src/script.c @@ -58,11 +58,14 @@ static char script_init[] = " local ri = arg.RI or -1\n" " tio.line_set(dtr, rts, cts, dsr, cd, ri)\n" "end\n" -"tio.EXPECT_CLEANUP_READ_SIZE = 4096\n" +"tio.C = {}\n" +"tio.C.EXPECT_CLEANUP_READ_SIZE = 4096\n" +"tio.C.WAIT_FOREVER = 0\n" +"tio.C.NOWAIT = -1\n" "tio.expect = function(pattern, timeout)\n" " local str = ''\n" " while true do\n" -" local astr = tio.read(tio.EXPECT_CLEANUP_READ_SIZE, 0)\n" +" local astr = tio.read(tio.C.EXPECT_CLEANUP_READ_SIZE, tio.C.NOWAIT)\n" " local c = nil\n" " if astr == nil then\n" " c = tio.read(1, timeout)\n" @@ -83,7 +86,7 @@ static char script_init[] = " patterns = { patterns }\n" " end\n" " while true do\n" -" local astr = tio.read(tio.EXPECT_CLEANUP_READ_SIZE, 0)\n" +" local astr = tio.read(tio.C.EXPECT_CLEANUP_READ_SIZE, tio.C.NOWAIT)\n" " local c = nil\n" " if astr == nil then\n" " c = tio.read(1, timeout)\n" @@ -319,7 +322,7 @@ static int api_write(lua_State *L) do { - ret = write_poll(device_fd, string, len, WRITE_POLL_FOREVER); + ret = write_poll(device_fd, string, len, POLL_FOREVER); if (ret < 0) return luaL_error(L, "%s", strerror(errno)); @@ -364,13 +367,22 @@ static int api_twrite(lua_State *L) static int api_read(lua_State *L) { int size = luaL_checkinteger(L, 1); - int timeout = luaL_optinteger(L, 2, -1); // ms, negative value means forever. + int timeout = luaL_optinteger(L, 2, 0); // ms, zero value means forever, negative value means nowait. if (device_fd == 0) { return luaL_error(L, "tty device not ready"); } + // For C API, the values for forever and nowait are swapped. + int timeout_c; + if (timeout > 0) + timeout_c = timeout; + else if (timeout == 0) + timeout_c = POLL_FOREVER; + else if (timeout < 0) + timeout_c = POLL_NOWAIT; + luaL_Buffer buffer; luaL_buffinit(L, &buffer); @@ -382,7 +394,7 @@ static int api_read(lua_State *L) char *p = luaL_prepbuffer(&buffer); #endif - ssize_t ret = read_poll(device_fd, p, size, timeout); + ssize_t ret = read_poll(device_fd, p, size, timeout_c); if (ret < 0) return luaL_error(L, "%s", strerror(errno)); @@ -406,7 +418,7 @@ static int api_read(lua_State *L) // lua: string = tio.readline(timeout) static int api_readline(lua_State *L) { - int timeout = luaL_optinteger(L, 1, -1); // ms, negative value means forever. + int timeout = luaL_optinteger(L, 1, 0); // ms, zero value means forever, negative value means nowait. luaL_Buffer b; char ch; @@ -415,11 +427,20 @@ static int api_readline(lua_State *L) return luaL_error(L, "tty device not ready"); } + // For C API, the values for forever and nowait are swapped. + int timeout_c; + if (timeout > 0) + timeout_c = timeout; + else if (timeout == 0) + timeout_c = POLL_FOREVER; + else if (timeout < 0) + timeout_c = POLL_NOWAIT; + luaL_buffinit(L, &b); luaL_prepbuffer(&b); while (true) { - int ret = read_poll(device_fd, &ch, 1, timeout); + int ret = read_poll(device_fd, &ch, 1, timeout_c); if (ret < 0) return luaL_error(L, "%s", strerror(errno)); diff --git a/src/tty.c b/src/tty.c index 088a876..4c3d40c 100644 --- a/src/tty.c +++ b/src/tty.c @@ -302,7 +302,7 @@ void tty_sync(int fd) while (remain > 0) { - count = write_poll(fd, cp, remain, WRITE_POLL_FOREVER); + count = write_poll(fd, cp, remain, POLL_FOREVER); if (count < 0) { // Error @@ -360,7 +360,7 @@ static ssize_t tty_raw_write(int fd) continue; } - retval = write_poll(fd, &tty_buffer[i], 1, WRITE_POLL_FOREVER); + retval = write_poll(fd, &tty_buffer[i], 1, POLL_FOREVER); if (retval < 0) { // Error From b7ccd28300e1fdce0a626103029bb0f7416b5df7 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 3 Feb 2026 22:42:45 +0900 Subject: [PATCH 35/53] Add script-init-file option The script-init-file is the file to be loaded and executed when the interpreter starts. If this option is defined, it will be executed after the tio module is loaded as a built-in module. Example of .tioconfig: script-init-file = /home/user/.tio-script-init.lua Example of option specification: tio --script-init-file /home/user/.tio-script-init.lua --- src/configfile.c | 1 + src/options.c | 11 +++++++++++ src/options.h | 1 + src/script.c | 8 ++++++++ 4 files changed, 21 insertions(+) diff --git a/src/configfile.c b/src/configfile.c index 686cae9..d9323a7 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -294,6 +294,7 @@ static void config_parse_keys(GKeyFile *key_file, char *group) string = NULL; } config_get_bool(key_file, group, "mute", &option.mute); + config_get_string(key_file, group, "script-init-file", &option.script_init_filename, NULL); config_get_string(key_file, group, "script", &option.script, NULL); config_get_string(key_file, group, "script-file", &option.script_filename, NULL); config_get_string(key_file, group, "script-run", &string, NULL); diff --git a/src/options.c b/src/options.c index d85a5c6..cf96712 100644 --- a/src/options.c +++ b/src/options.c @@ -51,6 +51,7 @@ enum opt_t OPT_ALERT, OPT_COMPLETE_PROFILES, OPT_MUTE, + OPT_SCRIPT_INIT_FILE, OPT_SCRIPT, OPT_SCRIPT_FILE, OPT_SCRIPT_RUN, @@ -107,6 +108,7 @@ struct option_t option = .rs485_delay_rts_after_send = -1, .alert = ALERT_NONE, .complete_profiles = false, + .script_init_filename = NULL, .script = NULL, .script_filename = NULL, .script_run = SCRIPT_RUN_ALWAYS, @@ -933,6 +935,10 @@ void options_print() { tio_printf(" Socket: %s", option.socket); } + if (option.script_init_filename != NULL) + { + tio_printf(" Script init file: %s", option.script_init_filename); + } if (option.script_filename != NULL) { tio_printf(" Script file: %s", option.script_filename); @@ -1005,6 +1011,7 @@ void options_parse(int argc, char *argv[]) {"rs-485-config", required_argument, 0, OPT_RS485_CONFIG }, {"alert", required_argument, 0, OPT_ALERT }, {"mute", no_argument, 0, OPT_MUTE }, + {"script-init-file", required_argument, 0, OPT_SCRIPT_INIT_FILE }, {"script", required_argument, 0, OPT_SCRIPT }, {"script-file", required_argument, 0, OPT_SCRIPT_FILE }, {"script-run", required_argument, 0, OPT_SCRIPT_RUN }, @@ -1179,6 +1186,10 @@ void options_parse(int argc, char *argv[]) option.mute = true; break; + case OPT_SCRIPT_INIT_FILE: + option.script_init_filename = optarg; + break; + case OPT_SCRIPT: option.script = optarg; break; diff --git a/src/options.h b/src/options.h index 337768c..6589c16 100644 --- a/src/options.h +++ b/src/options.h @@ -94,6 +94,7 @@ struct option_t int32_t rs485_delay_rts_after_send; alert_t alert; bool complete_profiles; + char *script_init_filename; char *script; char *script_filename; script_run_t script_run; diff --git a/src/script.c b/src/script.c index 2bf4a66..f66f0bd 100644 --- a/src/script.c +++ b/src/script.c @@ -636,6 +636,14 @@ static lua_State *script_interp_new(void) // Initialize globals script_set_globals(L); + // Execute script-init file + if (option.script_init_filename) { + if (luaL_dofile(L, option.script_init_filename)) { + tio_warning_printf("lua: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + } + return L; } From 2996e411fe6a7b804402bd2f5eef70e142fc9a79 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Tue, 3 Feb 2026 23:10:56 +0900 Subject: [PATCH 36/53] Remove the interpreter-restart in the script/script-file option The lua interpreter is started immediately after tio started, so this was redundant. --- src/script.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/script.c b/src/script.c index f66f0bd..af2743d 100644 --- a/src/script.c +++ b/src/script.c @@ -728,15 +728,13 @@ void script_run_as_specified_by_options(void) if (option.script_filename != NULL) { tio_printf("Running script %s", option.script_filename); - if (script_interp_new()) - script_file_run(script_interp, option.script_filename); + script_file_run(script_interp, option.script_filename); } else if (option.script != NULL) { - tio_printf("Running script"); - if (script_interp_new()) - script_buffer_run(script_interp, option.script); + tio_printf("Running script !%s", option.script); + script_buffer_run(script_interp, option.script); } } From 8722b410a762f0f7acc3288db38a488ab02c905c Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sat, 7 Feb 2026 17:06:43 +0900 Subject: [PATCH 37/53] Add description of script-init-file option to help --- src/options.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/options.c b/src/options.c index cf96712..7e7eecf 100644 --- a/src/options.c +++ b/src/options.c @@ -182,6 +182,7 @@ void option_print_help(char *argv[]) printf(" --rs-485-config Set RS-485 configuration\n"); printf(" --alert bell|blink|none Alert on connect/disconnect (default: none)\n"); printf(" --mute Mute tio messages\n"); + printf(" --script-init-file Set initial script file to run at startup\n"); printf(" --script Run script from string\n"); printf(" --script-file Run script from file\n"); printf(" --script-run once|always|never Run script on connect (default: always)\n"); From 60bc7e9cfea3ffcd5460ceea2002cf1a588eab07 Mon Sep 17 00:00:00 2001 From: yabu76 Date: Sun, 8 Feb 2026 16:17:30 +0900 Subject: [PATCH 38/53] Add user key-script mapping function (--keymap, Ctrl-t k) User key-script mapping function: You can specify the mappings as @= @=... @=. Script-description is script-filename or '!'script-commands. After that, When you press ctrl-t and , tio executes . This user keymap takes precedence over the default settings (except for ctrl-t q). Example of startup option: tio /dev/ttyUSB1 --keymap '@1=!print(tio.banner()) @2=!tio.write("test\r") @ctrl-a=!tio.write("ctrl-a\r") @ctrl-j=test-script.tio' Example of .tioconfig: (note: backslash escape needed.) keymap = @1=!print(tio.banner()) @2=!tio.write("test\\r") @ctrl-a=!tio.write("ctrl-a\\r") @ctrl-j=test-script.tio --- src/configfile.c | 7 ++ src/misc.c | 10 +++ src/misc.h | 5 ++ src/options.c | 182 +++++++++++++++++++++++++++++++++++++++++++++++ src/options.h | 15 ++++ src/tty.c | 31 ++++++++ 6 files changed, 250 insertions(+) diff --git a/src/configfile.c b/src/configfile.c index d9323a7..a8ac75d 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -250,6 +250,13 @@ static void config_parse_keys(GKeyFile *key_file, char *group) g_free((void *)string); string = NULL; } + config_get_string(key_file, group, "keymap", &string, NULL); + if (string != NULL) + { + option_parse_key_mappings(string); + g_free((void *)string); + string = NULL; + } config_get_string(key_file, group, "color", &string, NULL); if (string != NULL) { diff --git a/src/misc.c b/src/misc.c index eff7e3d..62a21f1 100644 --- a/src/misc.c +++ b/src/misc.c @@ -57,6 +57,16 @@ int ctrl_key_code(unsigned char key) return -1; } +int ctrl_key_char(int key_code) +{ + if (key_code >= ('a' & ~0x60) && key_code <= ('z' & ~0x60)) + { + return key_code | 0x60; + } + + return -1; +} + bool regex_match(const char *string, const char *pattern) { regex_t regex; diff --git a/src/misc.h b/src/misc.h index 6edbb9c..f501e82 100644 --- a/src/misc.h +++ b/src/misc.h @@ -26,10 +26,15 @@ #define POLL_NOWAIT (0) #define POLL_FOREVER (-1) + +#define TOSTRING_(x) #x +#define TOSTR(x) TOSTRING_(x) + #define UNUSED(expr) do { (void)(expr); } while (0) void delay(long ms); int ctrl_key_code(unsigned char key); +int ctrl_key_char(int key_code); bool regex_match(const char *string, const char *pattern); unsigned long djb2_hash(const unsigned char *str); void *base62_encode(unsigned long num, char *output); diff --git a/src/options.c b/src/options.c index 7e7eecf..eb07c7c 100644 --- a/src/options.c +++ b/src/options.c @@ -63,6 +63,7 @@ enum opt_t OPT_EXEC, OPT_RAW, OPT_RAW_INTERACTIVE, + OPT_KEYMAP, }; // clang-format off @@ -134,9 +135,12 @@ struct option_t option = .map_o_ign_cr = false, .raw = RAW_ON_DELAY, .raw_interactive = RAW_OFF, + .keymap = NULL, }; // clang-format on +struct keymap_t keymaps[KEYMAP_MAX] = {0}; + void option_print_help(char *argv[]) { UNUSED(argv); @@ -174,6 +178,7 @@ void option_print_help(char *argv[]) printf(" --log-append Append to log file\n"); printf(" --log-strip Strip control characters and escape sequences\n"); printf(" -m, --map Map characters\n"); + printf(" --keymap Set key-script mappings\n"); printf(" -c, --color 0..255|bold|none|list Colorize tio text (default: bold)\n"); printf(" -S, --socket Redirect I/O to socket\n"); printf(" --raw off|on|on-nodelay Select raw mode for non-interactive use (default: on)\n"); @@ -945,6 +950,11 @@ void options_print() tio_printf(" Script file: %s", option.script_filename); tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); } + if (option.script != NULL) + { + tio_printf(" Script command: %s", option.script); + tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); + } } void options_parse(int argc, char *argv[]) @@ -1003,6 +1013,7 @@ void options_parse(int argc, char *argv[]) {"log-strip", no_argument, 0, OPT_LOG_STRIP }, {"socket", required_argument, 0, 'S' }, {"map", required_argument, 0, 'm' }, + {"keymap", required_argument, 0, OPT_KEYMAP }, {"color", required_argument, 0, 'c' }, {"input-mode", required_argument, 0, OPT_INPUT_MODE }, {"output-mode", required_argument, 0, OPT_OUTPUT_MODE }, @@ -1215,6 +1226,11 @@ void options_parse(int argc, char *argv[]) option_parse_raw(optarg, &option.raw_interactive); break; + case OPT_KEYMAP: + option.keymap = optarg; + option_parse_key_mappings(optarg); + break; + case 'v': printf("tio %s\n", VERSION); exit(EXIT_SUCCESS); @@ -1299,3 +1315,169 @@ void options_parse_final(int argc, char *argv[]) // clang-format on #endif } + +int keymap_set(char *key_str, int key_len, char *func_str, int func_len) +{ + char func_str_r[KEYMAP_FUNC_STR_MAX + 1]; + char *srcp; + int dst_ofs; + int key_ofs; + int empty_idx, matched_idx, idx; + bool found_empty = false; + bool found_matched = false; + bool unset_requested = false; + + if (key_str[key_len] != '\0' || func_str[func_len] != '\0') + { + return -1; + } + + /* key_str should not include spaces */ + key_ofs = 0; + for (key_ofs = 0; key_ofs < key_len; key_ofs++) + { + if (key_str[key_ofs] == ' ') + { + tio_error_printf("Key should not include space"); + return -1; + } + } + + /* check disallowed key_str */ + if (strcmp(key_str, "q") == 0) + { + tio_error_printf("Key %s is immutable", key_str); + return -1; + } + + /* remove prefix spaces and postfix spaces from func_str */ + for (srcp = func_str; *srcp != '\0'; srcp++) + { + if (*srcp != ' ') + { + break; + } + } + strncpy(func_str_r, srcp, KEYMAP_KEY_STR_MAX); + func_str_r[KEYMAP_KEY_STR_MAX] = '\0'; + for (dst_ofs = strlen(func_str_r) - 1; dst_ofs >= 0; dst_ofs--) + { + if (func_str_r[dst_ofs] != ' ') + { + func_str_r[dst_ofs + 1] = '\0'; + break; + } + } + if (strcmp(func_str_r, "nil") == 0 || func_str_r[0] == '\0') + { + unset_requested = true; + } + + /* search for entry which key matched or is empty */ + for (idx = 0; idx < KEYMAP_MAX; idx++) + { + if (found_empty == false && keymaps[idx].key[0] == '\0') + { + empty_idx = idx; + found_empty = true; + } + if (found_matched == false && strcmp(keymaps[idx].key, key_str) == 0) + { + matched_idx = idx; + found_matched = true; + } + if (found_empty && found_matched) + { + break; + } + } + + /* update entry */ + if (unset_requested) + { + if (found_matched) + { + keymaps[matched_idx].key[0] = '\0'; + keymaps[matched_idx].func[0] = '\0'; + } + } + else /* set requested */ + { + if (found_matched) + { + strcpy(keymaps[matched_idx].key, key_str); + strcpy(keymaps[matched_idx].func, func_str); + } + else if (found_empty) + { + strcpy(keymaps[empty_idx].key, key_str); + strcpy(keymaps[empty_idx].func, func_str); + } + else + { + tio_error_printf("Too many keymaps", key_str); + return -1; + } + } + return 0; +} + +void keymaps_print(const char *title, int indent) +{ + int idx; + bool keymap_title_done = false; + + for (idx = 0; idx < KEYMAP_MAX; idx++) + { + if (keymaps[idx].key[0] == '\0') + { + continue; + } + if (!keymap_title_done) + { + if (title[0] != '\0') + { + tio_printf("%s", title); + } + keymap_title_done = true; + } + tio_printf("%*sctrl-%c %s : %s", indent, " ", option.prefix_key, keymaps[idx].key, keymaps[idx].func); + } +} + +void option_parse_key_mappings(const char *keymap) +{ + char key_str[KEYMAP_KEY_STR_MAX + 1]; + char func_str[KEYMAP_FUNC_STR_MAX + 1]; + int key_len, func_len; + char *buffer; + char *cp; + + if (keymap == NULL) + { + return; + } + + /* Parse specified key mappings */ + buffer = strdup(keymap); + cp = strchr(buffer, '@'); + if (cp == NULL) + { + tio_error_print("Can't find keymap top character '@'"); + goto parse_end; + } + + while (sscanf(cp, "@%" TOSTR(KEYMAP_KEY_STR_MAX) "[^=]=%" TOSTR(KEYMAP_FUNC_STR_MAX) "[^@]", key_str, func_str) == 2) + { + key_len = strlen(key_str); + func_len = strlen(func_str); + keymap_set(key_str, key_len, func_str, func_len); + cp = strchr(cp + key_len + func_len + 2, '@'); + if (cp == NULL) + { + break; + } + } + parse_end: + free(buffer); +} diff --git a/src/options.h b/src/options.h index 6589c16..1ab12ea 100644 --- a/src/options.h +++ b/src/options.h @@ -105,6 +105,7 @@ struct option_t int hex_n_value; bool vt100; char *exec; + char *keymap; bool map_i_nl_cr; bool map_i_cr_nl; bool map_ign_cr; @@ -120,7 +121,18 @@ struct option_t bool map_o_ign_cr; }; +#define KEYMAP_MAX 32 +#define KEYMAP_KEY_STR_MAX 7 +#define KEYMAP_FUNC_STR_MAX 127 + +struct keymap_t +{ + char key[KEYMAP_KEY_STR_MAX + 1]; + char func[KEYMAP_FUNC_STR_MAX + 1]; +}; + extern struct option_t option; +extern struct keymap_t keymaps[KEYMAP_MAX]; void options_print(); void options_parse(int argc, char *argv[]); @@ -147,5 +159,8 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp); const char* option_timestamp_format_to_string(timestamp_t timestamp); void option_parse_mappings(const char *map); +void option_parse_key_mappings(const char *keymap); const char* option_raw_to_string(raw_t raw); + +void keymaps_print(const char *title, int indent); diff --git a/src/tty.c b/src/tty.c index 4c3d40c..4d5e136 100644 --- a/src/tty.c +++ b/src/tty.c @@ -119,6 +119,7 @@ #define KEY_I 0x69 #define KEY_J 0x6A #define KEY_SHIFT_J 0x4A +#define KEY_K 0x6B #define KEY_L 0x6C #define KEY_SHIFT_L 0x4C #define KEY_M 0x6D @@ -1099,6 +1100,26 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) return; } + // Handle user keymapped commands + for (int idx = 0; idx < KEYMAP_MAX; idx++) + { + if ((input_char >= 0x00) && (input_char <= 0x1f)) + { + int ctrl_key_ch = ctrl_key_char(input_char); + if ((ctrl_key_ch >= 0) && + (strncmp("ctrl-", keymaps[idx].key, 5) == 0) && (keymaps[idx].key[5] == ctrl_key_ch)) + { + script_run(keymaps[idx].func); + goto handle_commands_end; + } + } + else if (input_char == keymaps[idx].key[0] && keymaps[idx].key[1] == '\0') + { + script_run(keymaps[idx].func); + goto handle_commands_end; + } + } + // Handle commands switch (input_char) { @@ -1128,6 +1149,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" ctrl-%c x Send/Receive file via Xmodem", 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); + keymaps_print("User key commands:", 1); break; case KEY_SHIFT_L: @@ -1203,6 +1225,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) rs485_print_config(); } mappings_print(); + keymaps_print(" Keymaps:", 4); break; case KEY_E: @@ -1290,6 +1313,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) } break; + case KEY_K: + /* Set keymap */ + tio_subcmd_readln("Enter keymap @=|!