/*

term.c
version 0.2

(c) Andy Goth <unununium@openverse.com>, 2003

I place this code under the GNU General Public License.  Share and enjoy.


Ideas:

- 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.
- Tidying.
  - Multiple source files?
  - Avoidance of #if's inside functions.

*/

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <printf.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>

/* 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           /* ..., B300, ..., B57600, ... */
#define CFG_ESCAPE    '~'
#define CFG_BUF_SIZE  4096

#define CFG_HEX_MODE  1
#define CFG_SEND_FILE 1

/* 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 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 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 buffer. */

#if CFG_HEX_MODE
static int hex_mode = 0;               /* Hexadecimal or character mode? */
#endif

#if CFG_SEND_FILE
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. */

static int file_mode = 0;              /* True if sending from a data file. */
static int file_fd   = -1;             /* File descriptor for input data. */
#endif

/* 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);
static printf_function         print_error;
static printf_arginfo_function print_error_arginfo;

/* Everything cool is done here. */
int main(int argc, char** argv)
{
    int ret;
    fd_set read_fds, write_fds;
    int fd_max;

    /* Set up error printing. */
    if (register_printf_function('!', print_error, print_error_arginfo) == -1) {
        fprintf(stderr, "Unable to register \"%%!\" printf function.\n");
    }

    /* 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\"): %!\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 CFG_SEND_FILE
            if (file_mode) {
                FD_SET(file_fd, &read_fds);
                fd_max = MAX(MAX(tty_fd, line_fd), file_fd);
            } else
#endif
                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 CFG_SEND_FILE
            if (file_mode) {
                FD_SET(file_fd, &read_fds);
                fd_max = MAX(MAX(tty_fd, line_fd), file_fd);
            } else
#endif
                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(): %!\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;
        }

#if CFG_SEND_FILE
        /* User data file? */
        if (file_mode && FD_ISSET(file_fd, &read_fds)) {
            read_file();
            if (--ret == 0) continue;
        }
#endif

        /* 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 CFG_SEND_FILE
    if (file_fd >= 0) {
        if (close(file_fd) != 0) {
            print("close(\"%s\"): %!\n", name_buf);
        }
    }
    if (name_buf_cap != 0) free(name_buf);
#endif

    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: %!\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: %!\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: %!\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);
#ifdef CBAUDEX
        line_current.c_cflag &= ~(CBAUDEX);
#endif
        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: %!\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;
    int 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\"): %!\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\"): %!\n", CFG_LINE_DEV);
            exit(EXIT_FAILURE);
        } else {
            return;
        }
    } else if (ret > 0) {
#if CFG_HEX_MODE
        char buf[] = {'?', '?', ' '};
        int i;
        if (hex_mode) {
            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, 3);
            }
        } else
#endif
            write(STDOUT_FILENO, read_buf, ret);

    }
}

/* Processes user input coming from the keyboard. */
static void read_tty(void)
{
    static int escape = 0;
#if CFG_SEND_FILE
    static int read_name = 0;
#endif
#if CFG_HEX_MODE
    static int msn = -1, lsn = -1;
#endif

    int ret;
    char ch;

    /* Reading from the keyboard... */
    ret = read(tty_fd, &ch, 1);
    if (ret == -1) {
        print("read(stdin): %!\n");
        exit(EXIT_FAILURE);
    } else if (ret == 0) {
        return;
    }

#if CFG_SEND_FILE
    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\"): %!\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
#endif
    if (esc_enable && escape) {
        /* Processing an escape command... */
        escape = 0;
        switch (ch) {
        case CFG_ESCAPE:
            /* Two escapes in a row. */
#if CFG_SEND_FILE
            if (!file_mode)
#endif
                putc_line(ch);
            break;
        case '.':
            /* Quit. */
            exit(EXIT_SUCCESS);
#if CFG_HEX_MODE
        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;
                msn = -1;
            }
            break;
#endif
#if CFG_SEND_FILE
        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\"): %!\n", name_buf);
                }
                file_mode = 0;
                file_fd = -1;
            }
            break;
#endif
        case '?':
            print("%1$c%1$c  Send a literal \"%1$c\"\n", CFG_ESCAPE);
            print("%c.  Disconnect and exit program\n",  CFG_ESCAPE);
#if CFG_HEX_MODE
            print("%cx  Toggle hex mode\n",              CFG_ESCAPE);
#endif
#if CFG_SEND_FILE
            print("%cs  Send file or cancel send in progress\n", CFG_ESCAPE);
#endif
            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;
#if CFG_HEX_MODE
        } else if (hex_mode) {
#if CFG_SEND_FILE
            if (!file_mode)
#endif
            {
                /* Potential hexadecimal digit. */
                if (msn == -1) {
                    /* Most significant nibble. */
                    msn = HEX_TO_INT(ch);
                } else {
                    /* Least significant nibble. */
                    lsn = HEX_TO_INT(ch);
                    if (lsn != -1) {
                        putc_line((msn << 4) | (lsn));
                        msn = lsn = -1;
                    }
                }
            }
#endif
        } else {
            /* Anything else. */
#if CFG_SEND_FILE
            if (!file_mode)
#endif
                putc_line(ch);
        }
    }
}

#if CFG_SEND_FILE
/* Sends data from a datafile to the serial line. */
static void read_file(void)
{
    int ret;
    int 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\"): %!\n", name_buf);
            }
            if (close(file_fd) != 0) {
                print("close(\"%s\"): %!\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;
        }
    }
}
#endif

/* Prints the current error. */
static int print_error(FILE* stream, const struct printf_info* info,
const void* const* args)
{
    int len = info->left ? -info->width : info->width;
    return fprintf(stream, "%*s", len, strerror(errno));
}

/* Gives information on the arguments taken by %!. */
static int print_error_arginfo(const struct printf_info* info, size_t n,
int* argtypes)
{
    return 0;
}

/* vim: set ts=4 sts=4 sw=4 tw=80 et: */
/* EOF */
