/* term.c version 0.3 Copyright (c) Andy Goth , 2003-2004 I place this code under the GNU General Public License. Share and enjoy. Ideas: - poll(). - Runtime configuration. - Command-line options. - System, user, and command-line specified rc file(s). - Logging (receive file). - Commands: - Suspend. (~z) - Execute program. (~e) - Connect program stdin to local stdin, local file, or serial line? - Connect stdout/stderr to local stdout, local file, or serial line? - Will lrzsz, ckermit, chat, slip, pppd, etc. be possible? - Terminal control options. - CR/LF handling. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include /* Configuration */ #define CFG_LINE_DEV "/dev/ttyS0" #define CFG_DATA_BITS CS8 /* CS5, CS6, CS7, CS8 */ #define CFG_STOP_BITS 0 /* 0, CSTOPB */ #define CFG_PARITY PARENB /* 0, PARENB, PARODD */ #define CFG_BAUD_RATE B38400 /* ..., B9600, ..., B57600, ... */ #define CFG_ESCAPE '~' #define CFG_BUF_SIZE 4096 /* Useful macros. */ #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define HEX_TO_INT(c) (((c) >= 'a' && (c) <= 'f') ? ((c) + 10 - 'a') : \ (((c) >= 'A' && (c) <= 'F') ? ((c) + 10 - 'A') : \ (((c) >= '0' && (c) <= '9') ? ((c) + 0 - '0') : (-1)))) #define INT_TO_HEX(i) (((i) >= 0 && (i) <= 9 ) ? ((i) - 0 + '0') : \ (((i) >= 10 && (i) <= 15 ) ? ((i) - 10 + 'a') : ('?'))) /* Variables. */ static int tty_fd = -1; /* File descriptor for terminal. */ static int line_fd = -1; /* File descriptor for serial port. */ static int file_fd = -1; /* File descriptor for input data. */ static int tty_raw = 0; /* Is the tty raw or cooked? */ static int esc_enable = 1; /* Are escape sequences recognized? */ static int quiet = 0; /* Quiet mode? */ static int file_mode = 0; /* True if sending from a data file. */ static int hex_mode = 0; /* Hexadecimal or character mode? */ static char read_buf[CFG_BUF_SIZE]; /* Read buffer for serial port. */ static char write_buf[CFG_BUF_SIZE]; /* Write buffer for serial port. */ static int write_buf_count = 0; /* Number of bytes in write buffer. */ static int write_buf_ins = 0; /* Index of next insertion. */ static int write_buf_ext = 0; /* Index of next extraction. */ static char* print_buf = NULL; /* Message print buffer. */ static int print_buf_cap = 0; /* Current size of print_buf. */ static char* name_buf = NULL; /* Filename buffer. */ static int name_buf_pos = 0; /* Position in name buffer. */ static int name_buf_cap = 0; /* Current size of name buffer. */ /* Function prototypes. */ static void* xmalloc (size_t size); static void* xrealloc (void* ptr, size_t size); static void print (char* text, ...); static void cleanup_atexit(void); static void cleanup_signal(int sig_num); static int config_tty (int mode); static int config_line (int mode); static int putc_line (char ch); static void write_line (void); static void read_line (void); static void read_tty (void); static void read_file (void); /* Everything cool is done here. */ int main(int argc, char** argv) { int ret, fd_max; fd_set read_fds, write_fds; /* Arrange for proper shutdown. */ atexit(cleanup_atexit); signal(SIGINT, cleanup_signal); signal(SIGTERM, cleanup_signal); signal(SIGQUIT, cleanup_signal); /* Open and configure serial device. */ line_fd = open(CFG_LINE_DEV, O_RDWR | O_NONBLOCK); if (line_fd < 0) { print("open(\"%s\"): %m\n", CFG_LINE_DEV); exit(EXIT_FAILURE); } if (config_line(1) < 0) { exit(EXIT_FAILURE); } print("Connected\n"); /* "Open" and configure terminal. */ tty_fd = STDIN_FILENO; config_tty(1); /* Main loop. */ while (1) { /* Assemble the read and write fd_sets. */ FD_ZERO(&read_fds); FD_ZERO(&write_fds); if (write_buf_count == 0) { /* No data is waiting to be written to the serial port. */ FD_SET(tty_fd, &read_fds); FD_SET(line_fd, &read_fds); if (file_mode) { FD_SET(file_fd, &read_fds); fd_max = MAX(MAX(tty_fd, line_fd), file_fd); } else { fd_max = MAX(tty_fd, line_fd); } } else if (write_buf_count < CFG_BUF_SIZE) { /* Outgoing serial data is backlogged. */ FD_SET(tty_fd, &read_fds); FD_SET(line_fd, & read_fds); FD_SET(line_fd, &write_fds); if (file_mode) { FD_SET(file_fd, &read_fds); fd_max = MAX(MAX(tty_fd, line_fd), file_fd); } else { fd_max = MAX(tty_fd, line_fd); } } else { /* No write buffer space remaining. */ FD_SET(line_fd, & read_fds); FD_SET(line_fd, &write_fds); fd_max = line_fd; } /* Wait for readability or writability. */ ret = select(fd_max + 1, &read_fds, &write_fds, NULL, NULL); if (ret < 0) { /* Error. */ print("select(): %m\n"); exit(EXIT_FAILURE); } else if (ret == 0) { /* False alarm. */ continue; } /* Incoming serial data? */ if (FD_ISSET(line_fd, &read_fds)) { read_line(); if (--ret == 0) continue; } /* User data file? */ if (file_mode && FD_ISSET(file_fd, &read_fds)) { read_file(); if (--ret == 0) continue; } /* User input? */ if (FD_ISSET(tty_fd, &read_fds)) { read_tty(); if (--ret == 0) continue; } /* Can write to the serial port? */ if (FD_ISSET(line_fd, &write_fds)) { write_line(); if (--ret == 0) continue; } /* Shouldn't happen. */ print("select() failure...?\n"); exit(EXIT_FAILURE); } /* Again, shouldn't happen. */ return EXIT_FAILURE; } /* Allocates a block of memory. */ static void* xmalloc(size_t size) { void* data = malloc(size); if (data == NULL) { fprintf(stderr, "xmalloc(%d): Out of memory\n", size); if (tty_raw) fprintf(stderr, "\r"); exit(EXIT_FAILURE); } return data; } /* Reallocates a block of memory. */ static void* xrealloc(void* ptr, size_t size) { void* data = realloc(ptr, size); if (data == NULL) { fprintf(stderr, "xrealloc(%d): Out of memory\n", size); if (tty_raw) fprintf(stderr, "\r"); exit(EXIT_FAILURE); } return data; } /* Prints a formatted message. All text goes to stderr since stdout is * reserved for received data. If the terminal is in raw mode, \r's are * appended to each \n. */ static void print(char* text, ...) { va_list ap; int len, n, count = 0; char* p, *pprev; if (quiet) return; if (print_buf_cap == 0) { print_buf_cap = 32; print_buf = xmalloc(print_buf_cap); } while (1) { /* Try to convert format string to a text buffer. */ va_start(ap, text); len = vsnprintf(print_buf, print_buf_cap, text, ap); va_end(ap); /* Interpret result. */ if (len > -1 && len < print_buf_cap) { break; } else if (len > -1) { print_buf_cap = len + 1; } else { print_buf_cap *= 2; } /* Grow. */ print_buf = xrealloc(print_buf, print_buf_cap); } /* For raw mode, convert \n to \r\n. */ n = len + 1; if (tty_raw) { /* Determine how many characters will be inserted. */ p = print_buf; while ((p = strchr(p, '\n')) != NULL) { ++count; ++len; ++p; } /* Grow the buffer if necessary. */ if (n + count > print_buf_cap) { print_buf_cap = n + count; print_buf = xrealloc(print_buf, print_buf_cap); } /* Insert a \r before every \n. */ p = print_buf; pprev = print_buf + n; while (count > 0) { p = memrchr(print_buf, '\n', n); memmove(p + count, p, pprev - p); --count; p[count] = '\r'; n -= pprev - p; pprev = p; } } /* Display. */ write(STDERR_FILENO, print_buf, len); } /* Handles program termination. */ static void cleanup_atexit(void) { if (line_fd >= 0) { print("Disconnected\n"); config_line(0); } if (file_fd >= 0) { if (close(file_fd) != 0) { print("close(\"%s\"): %m\n", name_buf); } } if (name_buf_cap != 0) free(name_buf); if (tty_raw) config_tty(0); if (print_buf_cap != 0) free(print_buf); } /* Handles program termination. */ static void cleanup_signal(int signum) { exit(EXIT_SUCCESS); } /* Configures the terminal. */ static int config_tty(int mode) { int new_tty_raw; static int state = 0; static struct termios tty_save, tty_current; if (state == -1) { /* Prior failure. Just give up. */ return -1; } else if (state == 0) { /* First time call? Initialize tty_current. */ if (!isatty(tty_fd)) { state = -1; esc_enable = 0; return -1; } else if (tcgetattr(tty_fd, &tty_current) != 0) { print("Unable to sense terminal settings: %m\n"); state = -1; return -1; } tty_save = tty_current; state = 1; } if (mode) { /* Enable raw mode. */ tty_current.c_oflag &= ~OPOST; tty_current.c_iflag &= ~(ICRNL | IGNCR | INLCR); tty_current.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ECHOCTL | ECHOPRT | ECHOKE | ICRNL | ISIG); tty_current.c_cc[VTIME] = 0; tty_current.c_cc[VMIN] = 1; new_tty_raw = 1; } else { /* Restore original settings. */ tty_current = tty_save; new_tty_raw = 0; } /* Do it. */ if (tcsetattr(tty_fd, TCSANOW, &tty_current) != 0) { print("Unable to change terminal mode: %m\n"); state = -1; return -1; } tty_raw = new_tty_raw; return 0; } /* Configures the serial port. */ static int config_line(int mode) { static int state = 0; static struct termios line_save, line_current; if (state == 0) { /* First time. Initialize line_current. */ if (tcgetattr(line_fd, &line_current) != 0) { print("Unable to sense serial line settings: %m\n"); return -1; } line_save = line_current; state = 1; } if (mode) { /* Configure away. */ line_current.c_iflag &= ~(IXON | IXANY | IXOFF | ISTRIP | INLCR | ICRNL | IGNCR | IUCLC); line_current.c_iflag |= (IXOFF); line_current.c_oflag &= ~(OLCUC | ONLCR | OCRNL | ONOCR | ONLRET); line_current.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL | ISIG); line_current.c_cflag &= ~(CBAUD | PARENB | CBAUDEX); line_current.c_cflag |= (CLOCAL | CFG_BAUD_RATE | CFG_PARITY | CFG_DATA_BITS | CFG_STOP_BITS); line_current.c_cc[VTIME] = 0; line_current.c_cc[VMIN] = 1; } else { /* Restore original settings. */ line_current = line_save; } /* Do it. */ if (tcsetattr(line_fd, TCSANOW, &line_current) != 0) { print("Unable to configure serial line: %m\n"); return -1; } } /* Attempts to write one character to the serial port. */ static int putc_line(char ch) { int ret; if (write_buf_count < CFG_BUF_SIZE) { /* Buffer this character. */ write_buf[write_buf_ins] = ch; ++write_buf_count; ++write_buf_ins; if (write_buf_ins == CFG_BUF_SIZE) write_buf_ins = 0; return 0; } else { /* No space remaining in write buffer: drop data on the floor. :^( */ print("putc_line(%d): buffer overflow; discarding byte\n", (int)ch); return -1; } } /* Sends the write buffer to the serial line. */ static void write_line(void) { int ret, count; while (write_buf_count > 0) { if (write_buf_ins <= write_buf_ext) { count = CFG_BUF_SIZE - write_buf_ext; } else { count = write_buf_count; } ret = write(line_fd, &write_buf[write_buf_ext], count); if (ret < 0) { if (errno != EAGAIN && errno != EBUSY) { print("write(\"%s\"): %m\n", CFG_LINE_DEV); exit(EXIT_FAILURE); } else { return; } } else { write_buf_count -= ret; write_buf_ext += ret; if (write_buf_ext == CFG_BUF_SIZE) write_buf_ext = 0; } } } /* Reads data from the serial port and displays it. */ static void read_line(void) { int ret = read(line_fd, &read_buf, CFG_BUF_SIZE); if (ret < 0) { if (errno != EAGAIN && errno != EBUSY) { print("read(\"%s\"): %m\n", CFG_LINE_DEV); exit(EXIT_FAILURE); } else { return; } } else if (ret > 0) { if (hex_mode) { char buf[] = {'?', '?'}; int i; for (i = 0; i < ret; ++i) { buf[0] = INT_TO_HEX((read_buf[i] >> 4) & 15); buf[1] = INT_TO_HEX((read_buf[i] >> 0) & 15); write(STDOUT_FILENO, buf, sizeof(buf)); } } else { write(STDOUT_FILENO, read_buf, ret); } } } /* Processes user input coming from the keyboard. */ static void read_tty(void) { static int escape = 0; static int read_name = 0; static int msd = -1, lsd = -1; int ret; char ch; /* Reading from the keyboard... */ ret = read(tty_fd, &ch, 1); if (ret == -1) { print("read(stdin): %m\n"); exit(EXIT_FAILURE); } else if (ret == 0) { return; } if (read_name) { /* Getting the filename... */ if (ch == 13) { /* Enter. */ read_name = 0; if (name_buf_pos == 0) { print("cancelled\n"); } else { name_buf[name_buf_pos] = 0; name_buf_pos = 0; file_fd = open(name_buf, O_RDONLY | O_NONBLOCK); if (file_fd == -1) { print("\nopen(\"%s\"): %m\n", name_buf); } else { print("\nSending file... "); file_mode = 1; } } } else if (ch == 8 || ch == 127) { /* Backspace. */ if (name_buf_pos > 0) { print("\e[1D\e[1X"); --name_buf_pos; } } else if (ch == 21) { /* Clear line. */ if (name_buf_pos > 0) { print("\e[%1$dD\e[%1$dX", name_buf_pos); name_buf_pos = 0; } } else if (ch == 27 || ch == 3) { /* Cancel. */ if (name_buf_pos > 0) { print("\e[%1$dD\e[%1$dX", name_buf_pos); } print("cancelled\n"); read_name = 0; name_buf_pos = 0; } else if (ch >= 32 && ch <= 126) { /* Any ASCII character. */ print("%c", ch); name_buf[name_buf_pos] = ch; ++name_buf_pos; if (name_buf_pos == name_buf_cap) { name_buf_cap *= 2; name_buf = xrealloc(name_buf, name_buf_cap); } } } else if (esc_enable && escape) { /* Processing an escape command... */ escape = 0; switch (ch) { case CFG_ESCAPE: /* Two escapes in a row. */ if (!file_mode) { putc_line(ch); } break; case '.': /* Quit. */ exit(EXIT_SUCCESS); case 'x': /* Toggle hex mode. */ if (hex_mode) { print("Hex mode disabled\n"); hex_mode = 0; } else { print("Hex mode enabled\n"); hex_mode = 1; msd = -1; } break; case 's': /* Send file. */ if (!file_mode) { print("Send file: "); read_name = 1; if (name_buf_cap == 0) { name_buf_cap = 32; name_buf = xmalloc(name_buf_cap); } name_buf_pos = 0; name_buf[0] = 0; } else { /* Cancel. */ print("cancelled\n"); if (close(file_fd) != 0) { print("close(\"%s\"): %m\n", name_buf); } file_mode = 0; file_fd = -1; } break; case '?': print("%1$c%1$c Send a literal \"%1$c\"\n", CFG_ESCAPE); print("%c. Disconnect and exit program\n", CFG_ESCAPE); print("%cx Toggle hex mode\n", CFG_ESCAPE); print("%cs Send file or cancel send in progress\n", CFG_ESCAPE); print("%c? Command summary\n", CFG_ESCAPE); break; default: /* Unknown escape sequence. */ if (ch >= ' ' && ch <= '~') { print("Unknown command \"%1$c%2$c\"; type \"%1$c?\" for help\n", CFG_ESCAPE, ch); } else { print("Unknown command; type \"%c?\" for help\n", CFG_ESCAPE); } } } else { /* Processing ordinary data... */ if (ch == CFG_ESCAPE) { /* Escape character. */ escape = 1; } else if (hex_mode) { if (!file_mode) { /* Potential hexadecimal digit. */ if (msd == -1) { /* Most significant nibble. */ msd = HEX_TO_INT(ch); } else { /* Least significant nibble. */ lsd = HEX_TO_INT(ch); if (lsd != -1) { putc_line((msd << 4) | (lsd)); msd = lsd = -1; } } } } else { /* Anything else. */ if (!file_mode) { putc_line(ch); } } } } /* Sends data from a datafile to the serial line. */ static void read_file(void) { int ret, count; /* Fill up write buffer as much as possible. */ while (write_buf_count < CFG_BUF_SIZE) { if (write_buf_ins < write_buf_ext) { count = write_buf_ext - write_buf_ins; } else { count = CFG_BUF_SIZE - write_buf_ins; } ret = read(file_fd, &write_buf[write_buf_ins], count); if (ret <= 0) { if (ret < 0) { if (errno == EAGAIN || errno == EBUSY) break; print( "read(\"%s\"): %m\n", name_buf); } if (close(file_fd) != 0) { print("close(\"%s\"): %m\n", name_buf); } else if (ret == 0) { print("done\n"); } file_fd = -1; file_mode = 0; break; } else { write_buf_ins += ret; write_buf_count += ret; if (write_buf_ins == CFG_BUF_SIZE) write_buf_ins = 0; } } } /* vim: set ts=4 sts=4 sw=4 tw=80 et: */