commit aaaf6cb80587dfee5f9389826d79d5b05dd40284 Author: Aaron Cook Date: Sun Feb 6 16:50:55 2022 -0600 Public bytenuts repository creation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c03c21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +.vscode/ +*.log +tags diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a7376d --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +CC=clang +CFLAGS=-g -Wall -MMD -pthread -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE +LDFLAGS=-lpthread -lncurses + +DIR_SRC=src +DIR_BUILD=build +DIR_OBJ=$(DIR_BUILD)/obj +DIR_BIN=$(DIR_BUILD)/bin + +TARGET=$(DIR_BIN)/bytenuts + +DEBUG ?= 0 +ifeq ($(DEBUG), 1) + CFLAGS += -DDEBUG +endif + +SRCS=$(wildcard $(DIR_SRC)/*.c) +OBJS=$(addprefix $(DIR_OBJ)/,$(addsuffix .o,$(basename $(SRCS)))) +DEPS=$(addprefix $(DIR_OBJ)/,$(addsuffix .d,$(basename $(SRCS)))) + +.PHONY: all install uninstall clean + +all: $(TARGET) + +install: + ln -s $(realpath $(TARGET)) /usr/local/bin/bytenuts + +uninstall: + rm -f /usr/local/bin/bytenuts + +$(TARGET): $(OBJS) + @mkdir -p $(dir $@) + @echo "link and compile for $@" + @$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(OBJS) + +$(DIR_OBJ)/$(DIR_SRC)/%.o: $(DIR_SRC)/%.c + @mkdir -p $(dir $@) + @echo "compile $<" + @$(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -rf $(DIR_BUILD) + +-include $(DEPS) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1893d7 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +``` +██████╗ ██╗ ██╗████████╗███████╗███╗ ██╗██╗ ██╗████████╗███████╗ +██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔════╝████╗ ██║██║ ██║╚══██╔══╝██╔════╝ +██████╔╝ ╚████╔╝ ██║ █████╗ ██╔██╗ ██║██║ ██║ ██║ ███████╗ +██╔══██╗ ╚██╔╝ ██║ ██╔══╝ ██║╚██╗██║██║ ██║ ██║ ╚════██║ +██████╔╝ ██║ ██║ ███████╗██║ ╚████║╚██████╔╝ ██║ ███████║ +╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚══════╝ +``` + +Bytenuts aims to be a light weight terminal replacement of GUI based serial communications apps like SecureCRT or ZOC. It allows for simple communication between the PC and serial device. + +Bytenuts has been tested and built on x86-64 and 32-bit ARM Ubuntu 18. + +## Features + +- Input queueing - The input line allows users to edit their command before sending the characters over the serial connection +- XModem transfers - Users can begin an XModem transfer of a file on disc +- 8-bit ANSI color support - 8-bit terminal color can be enabled +- Configuration - Bytenuts can be configured with the config file located at `~/.config/bytenuts/config` +- Input echoing - Bytenuts can echo user input rather than relying on the connected device to echo +- Command history - Simply press up/down arrow to load previous commands +- Output history - Use page up/down, home/end, and ctrl + up/down arrow to scroll through the output window +- Output logging - Output can be saved to a log file passed in with the `-l` option +- Quick commands - Pages of quick commands are loaded from `~/.config/bytenuts/commands[1-10]` + +## Planned Features + +Bytenuts is still very early in development. Here are some features that may get implemented. + +- Other transfer protocols - Maybe Kermit and ZModem + +## Un-Planned Features + +Bytenuts is not supposed to be the be-all end-all application, nor is it supposed to be able to do everything SecureCRT can. Here are some things it probably won't have. + +- Non-serial communications - No SSH, telnet, etc. +- Elaborate menuing - Bytenuts functions will be shortcut driven to focus on what's important (the serial communication) +- Script loading - Think like how you can load scripts in ZOC on SecureCRT. Write normal Python scripts if you want to do that kind of stuff. +- Multi connection support - Open another instance of Bytenuts if you need to +- Windows support - No + +## Launch Options + +Launching Bytenuts like `bytenuts -h` provides this help print: + +``` +USAGE + +bytenuts [OPTIONS] + +Configs get loaded from ${HOME}/.bytenuts/config (if file exists) + +OPTIONS + +-h: show this help +-b : set a baud rate (default 115200) +-l : log all output to the given file +-c : load a config from the given path rather than the default +``` + +## Navigation + +Controls like backspace, delete, end/home, and left/right arrow work as expected within the input buffer window. For the output window, you can use the following keys to scroll through it: + +- `ctrl + up/down arrow` - Scroll the output window up or down by one line +- `page up/down` - Scroll the output window up or down by half of the height of the output window +- `shift + home/end` - Jump to the beginning or the end of the output + +If the output window reaches the current line of output, then the output will continue scrolling in real time. Otherwise, the output is paused to continue viewing where you currently are. + +## Configuration File + +There are only three supported configs currently. Each config is defined like `=`. Here is a sample config: + +``` +colors=1 +echo=0 +no_crlf=0 +``` + +- `colors` - enable parsing of 8-bit ANSI color codes +- `echo` - echo input to the terminal in app +- `no_crlf` - just send a line feed (`\n`) for user input rather than carriage return + line feed (`\r\n`) + +Bytenuts looks for the configs at `~/.config/bytenuts/config`. + +## Commands + +Upon starting bytenuts you get this prompt: + +``` +Welcome to Bytenuts +To quit press ctrl-b q +To see all commands, press ctrl-b h +``` + +The help prompt lists the available commands. + +``` +Commands (lead with ctrl-b): + c: print available quick commands + 0-9: load the given quick command (0 is 10) + p, 0-9: select a different quick commands page (0 is 10) + i: view info/stats + x: start XModem upload with 128B payloads + X: start XModem upload with 1024B payloads + h: view this help + q: quit Bytenuts +``` + +## Quick Commands + +You can provide multiple pages of quick commands you can easily load in with `ctrl+b [0-9]` in the files `~/.config/bytenuts/commands`. Bytenuts will load pages starting from `commands1` until a `commands` no longer exists. The commands should be newline separated. When a command is loaded, the entire contents of the line is loaded into the input buffer. + +Bytenuts will only store the first ten commands for each page, so the first 10 lines of your command files will be used. + +On startup, command page 1 is selected. You can select which command page is currently loaded by doing `ctrl+p, [0-9]` to load the desired page (note that index 0 will correspond to `commands10`). + +## Bugs + +Check out known bugs in the [issues tab](https://github.com/cookthebook/bytenuts/issues?q=is%3Aissue+is%3Aopen+label%3Abug). + +## Building + +Building Bytenuts is very simple. All you need is clang and libncurses. Run `make` in the Bytenuts root directory to build. You can also install the build (creating a link in `/usr/local/bin` to the `build` directory) by running `sudo make install`. diff --git a/src/bytenuts.c b/src/bytenuts.c new file mode 100644 index 0000000..34fa4bd --- /dev/null +++ b/src/bytenuts.c @@ -0,0 +1,518 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bytenuts.h" +#include "cheerios.h" +#include "ingest.h" + +#define USAGE ( \ +"USAGE\n\n" \ +"bytenuts [OPTIONS] \n" \ +"\nConfigs get loaded from ${HOME}/.bytenuts/config (if file exists)\n" \ +"\nOPTIONS\n\n" \ +"-h: show this help\n" \ +"-b : set a baud rate (default 115200)\n" \ +"-l : log all output to the given file\n" \ +"-c : load a config from the given path rather than the default\n" \ +"--colors=<0|1>: turn 8-bit ANSI colors off/on\n" \ +"--echo=<0|1>: turn input echoing off/on\n" \ +"--no_crlf=<0|1>: choose to send LF and not CRLF on input\n" \ +"--escape=: change the default ctrl+b escape character\n" \ +) + +static int parse_args(int argc, char **argv); +static int load_configs(); +static speed_t string_to_speed(const char *speed); +static const char *speed_to_string(speed_t speed); + +static bytenuts_t bytenuts; + +int +bytenuts_run(int argc, char **argv) +{ + memset(&bytenuts, 0, sizeof(bytenuts_t)); + + if (parse_args(argc, argv)) { + printf(USAGE); + return -1; + } + + if (load_configs()) { + return -1; + } + + bytenuts.serial_fd = serial_open(bytenuts.config.serial_path, bytenuts.config.baud); + if (bytenuts.serial_fd < 0) { + return -1; + } + + /* use pseudo-terminals for testing purposes */ + if (!strcmp(bytenuts.config.serial_path, "/dev/ptmx")) { + grantpt(bytenuts.serial_fd); + unlockpt(bytenuts.serial_fd); + } + + // Initialize ncurses + initscr(); + raw(); + noecho(); +#if 0 + mousemask(BUTTON4_PRESSED | BUTTON5_PRESSED, NULL); +#endif + + // setup status, output, and input windows + bytenuts.status_win = newwin(1, COLS, LINES - 2, 0); + wmove(bytenuts.status_win, 0, 0); + wrefresh(bytenuts.status_win); + bytenuts.out_win = newwin(LINES - 2, COLS, 0, 0); + wmove(bytenuts.out_win, 0, 0); + wrefresh(bytenuts.out_win); + bytenuts.in_win = newwin(1, COLS, LINES - 1, 0); + keypad(bytenuts.in_win, TRUE); + wtimeout(bytenuts.in_win, 1); + wmove(bytenuts.in_win, 0, 0); + wrefresh(bytenuts.in_win); + + bytenuts.ingest_status = strdup(""); + bytenuts.cheerios_status = strdup(""); + bytenuts_set_status(STATUS_BYTENUTS, "%s", bytenuts.config.serial_path); + + // Initialize in and out threads + if (pthread_mutex_init(&bytenuts.lock, NULL)) { + return -1; + } + + if (pthread_mutex_init(&bytenuts.term_lock, NULL)) { + return -1; + } + + if (pthread_cond_init(&bytenuts.stop_cond, NULL)) { + return -1; + } + + cheerios_start(&bytenuts); + ingest_start(&bytenuts); + + if (!strcmp(bytenuts.config.serial_path, "/dev/ptmx")) { + char info[128]; + snprintf(info, sizeof(info), "Opened PTY port %s", ptsname(bytenuts.serial_fd)); + cheerios_info(info); + } + + pthread_mutex_lock(&bytenuts.lock); + pthread_cond_wait(&bytenuts.stop_cond, &bytenuts.lock); + pthread_mutex_unlock(&bytenuts.lock); + + bytenuts_kill(); + + return 0; +} + +int +bytenuts_stop() +{ + pthread_mutex_lock(&bytenuts.lock); + pthread_cond_signal(&bytenuts.stop_cond); + pthread_mutex_unlock(&bytenuts.lock); + return 0; +} + +void +bytenuts_kill() +{ + ingest_stop(); + cheerios_stop(); + + delwin(bytenuts.status_win); + delwin(bytenuts.in_win); + delwin(bytenuts.out_win); + endwin(); + + close(bytenuts.serial_fd); +} + +int +bytenuts_print_stats() +{ + char st_line[128]; + + sprintf(st_line, "colors: %s\r\n", bytenuts.config.colors ? "enabled" : "disabled"); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "echo: %s\r\n", bytenuts.config.echo ? "enabled" : "disabled"); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "no_crlf: %s\r\n", bytenuts.config.no_crlf ? "enabled" : "disabled"); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "escape: %c\r\n", bytenuts.config.escape); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "baud: %s\r\n", speed_to_string(bytenuts.config.baud)); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "config_path: %s\r\n", bytenuts.config.config_path); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "log_path: %s\r\n", bytenuts.config.log_path); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "serial_path: %s\r\n", bytenuts.config.serial_path); + cheerios_insert(st_line, strlen(st_line)); + + return 0; +} + +int +bytenuts_set_status(int user, const char *fmt, ...) +{ + int cx, cy; + char *new_status; + int len; + va_list ap; + + va_start(ap, fmt); + len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + new_status = calloc(1, len + 1); + va_start(ap, fmt); + vsnprintf(new_status, len+1, fmt, ap); + va_end(ap); + + pthread_mutex_lock(&bytenuts.lock); + switch (user) { + case STATUS_BYTENUTS: + if (bytenuts.bytenuts_status) + free(bytenuts.bytenuts_status); + bytenuts.bytenuts_status = new_status; + break; + case STATUS_INGEST: + if (bytenuts.ingest_status) + free(bytenuts.ingest_status); + bytenuts.ingest_status = new_status; + break; + case STATUS_CHEERIOS: + if (bytenuts.cheerios_status) + free(bytenuts.cheerios_status); + bytenuts.cheerios_status = new_status; + break; + case STATUS_CMDPAGE: + if (bytenuts.cmdpg_status) + free(bytenuts.cmdpg_status); + bytenuts.cmdpg_status = new_status; + break; + default: + free(new_status); + pthread_mutex_unlock(&bytenuts.lock); + return 0; + } + pthread_mutex_unlock(&bytenuts.lock); + + pthread_mutex_lock(&bytenuts.term_lock); + + getyx(bytenuts.in_win, cy, cx); + curs_set(0); + wmove(bytenuts.status_win, 0, 0); + + int width = getmaxx(bytenuts.status_win); + for (int i = 0; i < width - 1; i++) { + waddch(bytenuts.status_win, '-'); + } + waddch(bytenuts.status_win, '|'); + wmove(bytenuts.status_win, 0, 0); + wprintw( + bytenuts.status_win, "|--%s--|--%s--|--%s--|--%s--|", + bytenuts.bytenuts_status, bytenuts.ingest_status, + bytenuts.cheerios_status, bytenuts.cmdpg_status + ); + + wmove(bytenuts.in_win, cy, cx); + curs_set(1); + wrefresh(bytenuts.status_win); + refresh(); + + pthread_mutex_unlock(&bytenuts.term_lock); + + return 0; +} + +int +bytenuts_update_screen_size() +{ + pthread_mutex_lock(&bytenuts.term_lock); + + wresize(bytenuts.status_win, 1, COLS); + mvwin(bytenuts.status_win, LINES - 2, 0); + wrefresh(bytenuts.status_win); + + wresize(bytenuts.out_win, LINES - 2, COLS); + mvwin(bytenuts.out_win, 0, 0); + wrefresh(bytenuts.out_win); + + wresize(bytenuts.in_win, 1, COLS); + mvwin(bytenuts.in_win, LINES - 1, 0); + wrefresh(bytenuts.in_win); + + pthread_mutex_unlock(&bytenuts.term_lock); + + cheerios_insert("", 0); + + return 0; +} + +static int +parse_args(int argc, char **argv) +{ + if (argc < 2) + return -1; + + if (argc == 2 && !strcmp(argv[1], "-h")) + return -1; + + /* setup default options first */ + bytenuts.config = CONFIG_DEFAULT; + { + char *home = getenv("HOME"); + char path[] = "/.config/bytenuts/config"; + bytenuts.config.config_path = malloc(strlen(home) + strlen(path) + 1); + sprintf(bytenuts.config.config_path, "%s%s", home, path); + } + + for (int i = 1; i < argc - 1; i++) { + size_t arg_len = strlen(argv[i]); + + if (!strcmp(argv[i], "-h")) { + return -1; + } + else if (!strcmp(argv[i], "-b")) { + i++; + if (i == argc - 1) + return -1; + + bytenuts.config.baud = string_to_speed(argv[i]); + } + else if (!strcmp(argv[i], "-l")) { + i++; + if (i == argc - 1) + return -1; + + bytenuts.config.log_path = strdup(argv[i]); + } + else if (!strcmp(argv[i], "-c")) { + i++; + if (i == argc - 1) + return -1; + + bytenuts.config.config_path = strdup(argv[i]); + } + else if (arg_len == 10 && !memcmp(argv[i], "--colors=", 9)) { + if (argv[i][9] == '1') { + bytenuts.config.colors = 1; + } else if (argv[i][9] == '0') { + bytenuts.config.colors = 0; + } + bytenuts.config_overrides[0] = 1; + } + else if (arg_len == 8 && !memcmp(argv[i], "--echo=", 7)) { + if (argv[i][7] == '1') { + bytenuts.config.echo = 1; + } else if (argv[i][7] == '0') { + bytenuts.config.echo = 0; + } + bytenuts.config_overrides[1] = 1; + } + else if (arg_len == 11 && !memcmp(argv[i], "--no_crlf=", 10)) { + if (argv[i][10] == '1') { + bytenuts.config.no_crlf = 1; + } else if (argv[i][10] == '0') { + bytenuts.config.no_crlf = 0; + } + bytenuts.config_overrides[2] = 1; + } + else if (arg_len == 10 && !memcmp(argv[i], "--escape=", 9)) { + bytenuts.config.escape = argv[i][9]; + bytenuts.config_overrides[3] = 1; + } + else { + return -1; + } + } + + bytenuts.config.serial_path = strdup(argv[argc - 1]); + + return 0; +} + +static int +load_configs() +{ + FILE *fd = NULL; + char line[128]; + + fd = fopen(bytenuts.config.config_path, "r"); + + if (!fd) { + return 0; + } + + while (fgets(line, sizeof(line), fd) != NULL) { + if (!bytenuts.config_overrides[0] && !memcmp(line, "colors=", 7)) { + if (line[7] == '0') + bytenuts.config.colors = 0; + else if (line[7] == '1') + bytenuts.config.colors = 1; + } + else if (!bytenuts.config_overrides[1] && !memcmp(line, "echo=", 5)) { + if (line[5] == '0') + bytenuts.config.echo = 0; + else if (line[5] == '1') + bytenuts.config.echo = 1; + } + else if (!bytenuts.config_overrides[2] && !memcmp(line, "no_crlf=", 8)) { + if (line[8] == '0') + bytenuts.config.no_crlf = 0; + else if (line[8] == '1') + bytenuts.config.no_crlf = 1; + } + else if (!bytenuts.config_overrides[3] && !memcmp(line, "escape=", 7)) { + bytenuts.config.escape = line[7]; + } + } + + return 0; +} + +static speed_t +string_to_speed(const char *speed) +{ + if (!strcmp("50", speed)) + return B50; + else if (!strcmp("75", speed)) + return B75; + else if (!strcmp("110", speed)) + return B110; + else if (!strcmp("134", speed)) + return B134; + else if (!strcmp("150", speed)) + return B150; + else if (!strcmp("200", speed)) + return B200; + else if (!strcmp("300", speed)) + return B300; + else if (!strcmp("600", speed)) + return B600; + else if (!strcmp("1200", speed)) + return B1200; + else if (!strcmp("1800", speed)) + return B1800; + else if (!strcmp("2400", speed)) + return B2400; + else if (!strcmp("4800", speed)) + return B4800; + else if (!strcmp("9600", speed)) + return B9600; + else if (!strcmp("19200", speed)) + return B19200; + else if (!strcmp("38400", speed)) + return B38400; + else if (!strcmp("57600", speed)) + return B57600; + else if (!strcmp("115200", speed)) + return B1152000; + else if (!strcmp("230400", speed)) + return B230400; + else if (!strcmp("460800", speed)) + return B460800; + else if (!strcmp("500000", speed)) + return B500000; + else if (!strcmp("576000", speed)) + return B576000; + else if (!strcmp("921600", speed)) + return B921600; + else if (!strcmp("1000000", speed)) + return B1000000; + else if (!strcmp("1152000", speed)) + return B1152000; + else if (!strcmp("1500000", speed)) + return B1500000; + else if (!strcmp("2000000", speed)) + return B2000000; + else if (!strcmp("2500000", speed)) + return B2500000; + else if (!strcmp("3000000", speed)) + return B3000000; + else if (!strcmp("3500000", speed)) + return B3500000; + else if (!strcmp("4000000", speed)) + return B4000000; + else + return B0; +} + +static const char * +speed_to_string(speed_t speed) +{ + if (speed == B50) + return "B50"; + else if (speed == B75) + return "B75"; + else if (speed == B110) + return "B110"; + else if (speed == B134) + return "B134"; + else if (speed == B150) + return "B150"; + else if (speed == B200) + return "B200"; + else if (speed == B300) + return "B300"; + else if (speed == B600) + return "B600"; + else if (speed == B1200) + return "B1200"; + else if (speed == B1800) + return "B1800"; + else if (speed == B2400) + return "B2400"; + else if (speed == B4800) + return "B4800"; + else if (speed == B9600) + return "B9600"; + else if (speed == B19200) + return "B19200"; + else if (speed == B38400) + return "B38400"; + else if (speed == B57600) + return "B57600"; + else if (speed == B115200) + return "B1152000"; + else if (speed == B230400) + return "B230400"; + else if (speed == B460800) + return "B460800"; + else if (speed == B500000) + return "B500000"; + else if (speed == B576000) + return "B576000"; + else if (speed == B921600) + return "B921600"; + else if (speed == B1000000) + return "B1000000"; + else if (speed == B1152000) + return "B1152000"; + else if (speed == B1500000) + return "B1500000"; + else if (speed == B2000000) + return "B2000000"; + else if (speed == B2500000) + return "B2500000"; + else if (speed == B3000000) + return "B3000000"; + else if (speed == B3500000) + return "B3500000"; + else if (speed == B4000000) + return "B4000000"; + else + return ""; +} diff --git a/src/bytenuts.h b/src/bytenuts.h new file mode 100644 index 0000000..12d2487 --- /dev/null +++ b/src/bytenuts.h @@ -0,0 +1,75 @@ +#ifndef _BYTENUTES_H_ +#define _BYTENUTES_H_ + +#include +#include +#include +#include + +#include "serial.h" + +/* https://en.wikipedia.org/wiki/Control_character#How_control_characters_map_to_keyboards */ +#define CTRL(c) ((c)&31) + +typedef struct bytenuts_config_struct { + int colors; /* enable terminal colors, default 1 */ + int echo; /* echo user input to the output window, default 0 */ + int no_crlf; /* only send LF line endings, not CRLF, default 0 */ + char escape; /* which character is used for escape sequence */ + speed_t baud; /* baud rate */ + char *config_path; /* config file path */ + char *log_path; /* path to the log file (if it exists) */ + char *serial_path; /* path to the target serial device */ +} bytenuts_config_t; + +#define CONFIG_DEFAULT (bytenuts_config_t){ \ + .colors = 1, \ + .echo = 0, \ + .no_crlf = 0, \ + .escape = 'b', \ + .baud = B115200, \ + .config_path = NULL, \ + .log_path = NULL, \ + .serial_path = NULL, \ +} + +typedef struct bytenuts_struct { + int serial_fd; + bytenuts_config_t config; + int config_overrides[4]; + WINDOW *status_win; + WINDOW *out_win; + WINDOW *in_win; + pthread_mutex_t lock; + pthread_mutex_t term_lock; + pthread_cond_t stop_cond; + pthread_t in_thr; + pthread_t out_thr; + char *bytenuts_status; + char *ingest_status; + char *cheerios_status; + char *cmdpg_status; +} bytenuts_t; + +/* startup the application */ +int bytenuts_run(int argc, char **argv); + +/* cleanly stop threads */ +int bytenuts_stop(); + +/* perform kill routing for exiting */ +void bytenuts_kill(); + +/* print some bytenuts stats to the output window */ +int bytenuts_print_stats(); + +#define STATUS_BYTENUTS (0) +#define STATUS_INGEST (1) +#define STATUS_CHEERIOS (2) +#define STATUS_CMDPAGE (3) +/* set the status for the given thread */ +int bytenuts_set_status(int user, const char *fmt, ...); + +int bytenuts_update_screen_size(); + +#endif /* _BYTENUTES_H_ */ diff --git a/src/cheerios.c b/src/cheerios.c new file mode 100644 index 0000000..3b04418 --- /dev/null +++ b/src/cheerios.c @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cheerios.h" +#include "xmodem.h" + +static cheerios_t cheerios; + +static void *cheerios_thread(void *arg); +static int insert_buf(line_buffer_t *lines, const char *buf, size_t len); +static int write_lines(line_buffer_t *lines); +static int handle_color(line_buffer_t *lines, int line_idx, int *pos); +short curs_color(int fg); +static int newline(line_buffer_t *lines); + +int +cheerios_start(bytenuts_t *bytenuts) +{ + memset(&cheerios, 0, sizeof(cheerios_t)); + + cheerios.output = bytenuts->out_win; + cheerios.term_lock = &bytenuts->term_lock; + cheerios.ser_fd = bytenuts->serial_fd; + cheerios.lines.bot = -1; + + cheerios.config = &bytenuts->config; + + if (cheerios.config->log_path) { + cheerios.log = fopen(cheerios.config->log_path, "w"); + if (!cheerios.log) { + return -1; + } + } + + pthread_cond_init(&cheerios.cond, NULL); + pthread_mutex_init(&cheerios.lock, NULL); + cheerios.running = 1; + pthread_create(&cheerios.thr, NULL, cheerios_thread, NULL); + + if (cheerios.config->colors) { + start_color(); + use_default_colors(); + } + + return 0; +} + +int +cheerios_pause() +{ + pthread_mutex_lock(&cheerios.lock); + cheerios.mode = CHEERIOS_MODE_PAUSED; + pthread_mutex_unlock(&cheerios.lock); + + cheerios_info("Paused"); + return 0; +} + +int +cheerios_resume() +{ + pthread_mutex_lock(&cheerios.lock); + cheerios.mode = CHEERIOS_MODE_NORMAL; + pthread_cond_signal(&cheerios.cond); + pthread_mutex_unlock(&cheerios.lock); + + cheerios_info("Resumed"); + return 0; +} + +int +cheerios_goback(int lines) +{ + pthread_mutex_lock(&cheerios.lock); + + if (lines < 0) { /* go back as far as we can */ + cheerios.lines.bot = getmaxy(cheerios.output) % cheerios.lines.n_lines; + } + else { /* just increment the bot pointer */ + if (cheerios.lines.bot < 0) + cheerios.lines.bot = cheerios.lines.n_lines - 1; + + cheerios.lines.bot -= lines; + if (cheerios.lines.bot < 0) + cheerios.lines.bot = 0; + } + + write_lines(&cheerios.lines); + + pthread_mutex_unlock(&cheerios.lock); + + return 0; +} + +int +cheerios_gofwd(int lines) +{ + pthread_mutex_lock(&cheerios.lock); + + if (lines < 0) { /* go to front */ + cheerios.lines.bot = -1; + } + else { /* just increment the bot pointer */ + if (cheerios.lines.bot < 0) + cheerios.lines.bot = cheerios.lines.n_lines - 1; + + cheerios.lines.bot += lines; + if (cheerios.lines.bot >= cheerios.lines.n_lines) + cheerios.lines.bot = -1; + } + + write_lines(&cheerios.lines); + + pthread_mutex_unlock(&cheerios.lock); + + return 0; +} + +int +cheerios_stop() +{ + cheerios.running = 0; + pthread_join(cheerios.thr, NULL); + + return 0; +} + +int +cheerios_input(const char *line) +{ + size_t line_len = strlen(line); + + pthread_mutex_lock(&cheerios.lock); + + if (cheerios.config->echo) { + char *line_parsed = calloc(1, line_len + 5 + 1); + sprintf(line_parsed, ">> %s\r\n", line); + insert_buf(&cheerios.lines, line_parsed, strlen(line_parsed)); + free(line_parsed); + } + + write(cheerios.ser_fd, line, strlen(line)); + if (cheerios.config->no_crlf) + write(cheerios.ser_fd, "\n", 1); + else + write(cheerios.ser_fd, "\r\n", 2); + + pthread_mutex_unlock(&cheerios.lock); + return 0; +} + +int +cheerios_info(const char *line) +{ + size_t line_len = strlen(line); + char *line_parsed = calloc(1, line_len + 12 + 1); + sprintf(line_parsed, "BYTENUTS: %s\r\n", line); + + pthread_mutex_lock(&cheerios.lock); + + if (cheerios.lines.pos != 0) { + insert_buf(&cheerios.lines, "\r\n", 2); + } + insert_buf(&cheerios.lines, line_parsed, strlen(line_parsed)); + + pthread_mutex_unlock(&cheerios.lock); + return 0; +} + +int +cheerios_insert(const char *buf, size_t len) +{ + pthread_mutex_lock(&cheerios.lock); + insert_buf(&cheerios.lines, buf, len); + pthread_mutex_unlock(&cheerios.lock); + + return 0; +} + + +static void +__xmodem_callback(size_t sent, size_t total) +{ + char status_str[1024]; + + + snprintf( + status_str, sizeof(status_str), + "\r %.02f/%.02fKB (%.01f%%)", + (sent / 1024.0), + (total / 1024.0), + (sent * 100.0 / total) + ); + + cheerios_insert(status_str, strlen(status_str)); +} + +int +cheerios_xmodem(const char *path, int block_sz) +{ + struct stat st = { 0 }; + FILE *fd; + char info[128]; + + stat(path, &st); + + snprintf( + info, sizeof(info), + "Sending %s (%ld) with %dB payloads", + path, + st.st_size, + block_sz + ); + cheerios_info(info); + + fd = fopen(path, "r"); + if (!fd) { + cheerios_info("Failed to open file!"); + return -1; + } + + cheerios_pause(); + if (xmodem_send( + cheerios.ser_fd, + fileno(fd), + st.st_size, + block_sz, + __xmodem_callback + )) { + fclose(fd); + cheerios_resume(); + return -1; + } + + fclose(fd); + cheerios_resume(); + return 0; +} + +int +cheerios_print_stats() +{ + char st_line[128]; + + sprintf(st_line, "output line count: %d\r\n", cheerios.lines.n_lines); + cheerios_insert(st_line, strlen(st_line)); + + return 0; +} + +int +cheerios_getmaxy() +{ + int y, x; + getmaxyx(cheerios.output, y, x); + return y; +} + +int +cheerios_getmaxx() +{ + int y, x; + getmaxyx(cheerios.output, y, x); + return x; +} + +int +cheerios_set_window(WINDOW *win) +{ + pthread_mutex_lock(&cheerios.lock); + + delwin(cheerios.output); + cheerios.output = win; + write_lines(&cheerios.lines); + + pthread_mutex_unlock(&cheerios.lock); + + return 0; +} + +static void * +cheerios_thread(void *arg) +{ + char buf[1024]; + ssize_t read_ret = 0; + struct pollfd fds; + + while (cheerios.running) { + pthread_mutex_lock(&cheerios.lock); + + while (cheerios.mode == CHEERIOS_MODE_PAUSED) { + pthread_cond_wait(&cheerios.cond, &cheerios.lock); + } + + fds.fd = cheerios.ser_fd; + fds.events = POLLIN; + fds.revents = 0; + + if (poll(&fds, 1, 1) == 0) { + pthread_mutex_unlock(&cheerios.lock); + nanosleep(&(struct timespec){ 0, 100000 }, NULL); + continue; + } + + read_ret = read(cheerios.ser_fd, buf, sizeof(buf)); + if (read_ret > 0) { + insert_buf(&cheerios.lines, buf, read_ret); + } + + pthread_mutex_unlock(&cheerios.lock); + nanosleep(&(struct timespec){ 0, 100000 }, NULL); + } + + if (cheerios.log) + fclose(cheerios.log); + pthread_exit(NULL); + return NULL; +} + +static int +insert_buf(line_buffer_t *lines, const char *buf, size_t len) +{ + if (lines->n_lines == 0) + newline(lines); + + for (size_t i = 0; i < len; i++) { + if (cheerios.log && cheerios.mode == CHEERIOS_MODE_NORMAL) + fwrite(&buf[i], 1, 1, cheerios.log); + + /* line feed starts a new row */ + if (buf[i] == '\n') { + newline(lines); + } + /* carriage return just sets pos to 0 */ + else if (buf[i] == '\r') { + lines->pos = 0; + } + else { + if (lines->pos >= lines->line_lens[lines->n_lines - 1]) { + lines->line_lens[lines->n_lines - 1] = lines->pos + 1; + lines->lines[lines->n_lines - 1] = realloc( + lines->lines[lines->n_lines - 1], + sizeof(uint8_t) * lines->line_lens[lines->n_lines - 1] + ); + } + + lines->lines[lines->n_lines - 1][lines->pos] = buf[i]; + lines->pos++; + } + } + + write_lines(lines); + return 0; +} + +static int +write_lines(line_buffer_t *lines) +{ + int row = lines->bot; + int rows_printed = 0; + int window_height = getmaxy(cheerios.output); + + if (row < 0) + row = lines->n_lines - 1; + + pthread_mutex_lock(cheerios.term_lock); + + curs_set(0); + werase(cheerios.output); + + while (row >= 0 && rows_printed < window_height) { + wmove(cheerios.output, window_height - rows_printed - 1, 0); + + for (int i = 0; i < lines->line_lens[row]; i++) { + handle_color(lines, row, &i); + + if (i >= getmaxx(cheerios.output)) { + break; + } + + if (i < lines->line_lens[row]) { + waddch(cheerios.output, lines->lines[row][i]); + } + } + + rows_printed++; + row--; + } + + curs_set(1); + wrefresh(cheerios.output); + + pthread_mutex_unlock(cheerios.term_lock); + + if (lines->bot < 0) { + bytenuts_set_status(STATUS_CHEERIOS, "scrolling"); + } else { + bytenuts_set_status(STATUS_CHEERIOS, "locked"); + } + + return 0; +} + +static int +handle_color(line_buffer_t *lines, int line_idx, int *pos) +{ + short fg, bg = -1; + int enable = -1; + int p = *pos; + uint8_t *line = lines->lines[line_idx]; + int line_len = lines->line_lens[line_idx]; + + if (!cheerios.config->colors) + return 0; + + while (p < line_len) { + /* fg or bg color */ + if (!memcmp(&line[p], "\e[38;5;", 7) || !memcmp(&line[p], "\e[48;5;", 7)) { + char col_str[4] = { 0 }; + short *col = line[p + 2] == '3' ? &fg : &bg; + + p += 7; + if (p >= line_len) + break; + + for (int i = 0; i < 3; i++) { + if (line[p] == 'm') { + break; + } + else if (line[p] < '0' || line[p] > '9') { + col_str[0] = '\0'; + break; + } + + col_str[i] = line[p]; + p++; + } + + if (col_str[0] == '\0') + break; + + *col = strtol(col_str, NULL, 10); + p++; + } + /* enable our color */ + else if (!memcmp(&line[p], "\e[1m", 4)) { + p += 4; + enable = 1; + break; + } + /* disable our color */ + else if (!memcmp(&line[p], "\e[0m", 4)) { + p += 4; + enable = 0; + break; + } + /* this is not a color code */ + else { + break; + } + } + + if (enable == 0) { + for (int i = 0; i < NCOLOR_PAIRS; i++) { + if (lines->enabled_pairs[i]) { + wattroff(cheerios.output, COLOR_PAIR(i + 1)); + lines->enabled_pairs[i] = 0; + } + } + } + else if (enable == 1 && (fg >= 0 || bg >= 0)) { + int pair_pos = -1; + + if (fg < 0) + fg = COLOR_WHITE; + if (bg < 0) + bg = COLOR_BLACK; + + for (int i = 0; i < NCOLOR_PAIRS; i++) { + if (lines->color_pairs[i].fg == fg && lines->color_pairs[i].bg == bg) { + pair_pos = i; + break; + } + } + + if (pair_pos < 0) { + pair_pos = lines->color_pos; + init_pair(pair_pos + 1, fg, bg); + lines->color_pairs[pair_pos].fg = fg; + lines->color_pairs[pair_pos].bg = bg; + lines->color_pos = (lines->color_pos + 1) % NCOLOR_PAIRS; + } + + lines->enabled_pairs[pair_pos] = 1; + wattron(cheerios.output, COLOR_PAIR(pair_pos + 1)); + } + else { + return 0; + } + + *pos = p; + return 0; +} + +static int +newline(line_buffer_t *lines) +{ + lines->n_lines++; + lines->lines = realloc(lines->lines, sizeof(uint8_t *) * lines->n_lines); + lines->lines[lines->n_lines - 1] = NULL; + lines->line_lens = realloc(lines->line_lens, sizeof(int) * lines->n_lines); + lines->line_lens[lines->n_lines - 1] = 0; + lines->pos = 0; + return 0; +} diff --git a/src/cheerios.h b/src/cheerios.h new file mode 100644 index 0000000..d9fc2c9 --- /dev/null +++ b/src/cheerios.h @@ -0,0 +1,87 @@ +#ifndef _CHEERIOS_H_ +#define _CHEERIOS_H_ + +#include +#include +#include + +#include "bytenuts.h" + +typedef struct line_buffer_struct { + uint8_t **lines; + int *line_lens; + int n_lines; /* how many lines do we have */ + int pos; /* position of the cursor in the current line */ + int bot; /* index of the bottom line shown */ +#define NCOLOR_PAIRS (8) + /* support 8 color pairs on screen at once. + * COLOR_PAIR(pair_pos + 2) will get you your color. */ + struct { uint8_t fg; uint8_t bg; } color_pairs[NCOLOR_PAIRS]; + uint8_t enabled_pairs[NCOLOR_PAIRS]; /* which pairs are enabled */ + int color_pos; +} line_buffer_t; + +enum cheerios_mode_enum { + CHEERIOS_MODE_NORMAL = 0, + CHEERIOS_MODE_PAUSED, +}; + +typedef struct cheerios_struct { + pthread_mutex_t lock; + volatile int running; + pthread_t thr; + WINDOW *output; + pthread_mutex_t *term_lock; + int ser_fd; + line_buffer_t lines; + FILE *log; + bytenuts_config_t *config; + volatile int mode; + pthread_cond_t cond; +} cheerios_t; + +/* startup the output window thread */ +int cheerios_start(bytenuts_t *bytenuts); + +/* pause reading from the device */ +int cheerios_pause(); + +/* resume reading from the device */ +int cheerios_resume(); + +/* go back in the log history, this also stops the buffer from scrolling down + * if negative, jump to the back of the log */ +int cheerios_goback(int lines); + +/* go forward in the log history + * if a negative number is provided, go back to the start and resume scrolling */ +int cheerios_gofwd(int lines); + +/* stop the thread and release memory */ +int cheerios_stop(); + +/* send over a line of user input to the output + * do not terminate with a newline! */ +int cheerios_input(const char *line); + +/* output a line only to the terminal for info/prompt purposes + * do not terminate with a newline! */ +int cheerios_info(const char *line); + +/* directly insert a buffer to the output window */ +int cheerios_insert(const char *buf, size_t len); + +/* send the file at path over xmodem */ +int cheerios_xmodem(const char *path, int block_sz); + +int cheerios_print_stats(); + +/* getter for window height */ +int cheerios_getmaxy(); +/* getter for window width */ +int cheerios_getmaxx(); + +/* deletes the old window and sets the output window to the new one */ +int cheerios_set_window(WINDOW *win); + +#endif /* _CHEERIOS_H_ */ diff --git a/src/ingest.c b/src/ingest.c new file mode 100644 index 0000000..a98f3b8 --- /dev/null +++ b/src/ingest.c @@ -0,0 +1,804 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bytenuts.h" +#include "cheerios.h" +#include "ingest.h" + +static void *ingest_thread(void *arg); + +static ingest_t ingest; + +static int mode_normal(int ch); +static int mode_xmodem(int block_sz); +static int handle_functions(int ch); +static int inbuf_load(); +static int print_stats(); +static int auto_complete(); +static int read_cmd_page(int idx); +static void update_cmd_pg_status(); + +int +ingest_start(bytenuts_t *bytenuts) +{ + char *HOME; + + memset(&ingest, 0, sizeof(ingest)); + + ingest.input = bytenuts->in_win; + ingest.term_lock = &bytenuts->term_lock; + ingest.config = &bytenuts->config; + ingest.cmd_pg_cur = -1; + + HOME = getenv("HOME"); + if (HOME) { + char *cwd = getcwd(NULL, 0); + int idx = 0; + + chdir(HOME); + + while (!read_cmd_page(idx)) { + idx++; + } + + chdir(cwd); + free(cwd); + } + + if (ingest.cmd_pgs_n > 0) { + ingest.cmd_pg_cur = 0; + } + update_cmd_pg_status(); + + ingest.running = 1; + if (pthread_create(&ingest.thr, NULL, ingest_thread, NULL)) { + return -1; + } + + return 0; +} + +int +ingest_stop() +{ + ingest.running = 0; + pthread_join(ingest.thr, NULL); + return 0; +} + +static void * +ingest_thread(void *arg) +{ + int esc_flag; + int ch; + + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); + ingest.inpos = 0; + + cheerios_insert( + "Welcome to Bytenuts\r\n" + "To quit press ctrl-b q\r\n" + "To see all commands, press ctrl-b h\r\n", + 82 + ); + bytenuts_set_status(STATUS_INGEST, "normal"); + + while (ingest.running) { + esc_flag = 0; + + pthread_mutex_lock(ingest.term_lock); + ch = wgetch(ingest.input); + pthread_mutex_unlock(ingest.term_lock); + + if (ch == ERR) { + nanosleep(&(struct timespec){ 0, 100000 }, NULL); + continue; + } + + if (ch == CTRL(ingest.config->escape)) { + int should_quit = 0; + int should_continue = 0; + + bytenuts_set_status(STATUS_INGEST, "control"); + + while (1) { + pthread_mutex_lock(ingest.term_lock); + ch = wgetch(ingest.input); + pthread_mutex_unlock(ingest.term_lock); + + if (ch != ERR) + break; + nanosleep(&(struct timespec){ 0, 100000 }, NULL); + } + + switch (ch) { + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '0': + { + bytenuts_set_status(STATUS_INGEST, "normal"); + should_continue = 1; + + if (ingest.cmd_pgs_n == 0) + break; + + int idx; + char *toload; + size_t toload_len; + char **cmds = ingest.cmd_pgs[ingest.cmd_pg_cur].cmds; + int cmds_n = ingest.cmd_pgs[ingest.cmd_pg_cur].cmds_n; + + if (ch == '0') + ch += 10; + idx = ch - '1'; + + if (idx >= cmds_n) + break; + + toload = cmds[idx]; + toload_len = strlen(toload); + + memcpy(ingest.inbuf, toload, toload_len); + ingest.inpos = toload_len; + ingest.inlen = toload_len; + inbuf_load(); + break; + } + case 'c': + { + bytenuts_set_status(STATUS_INGEST, "normal"); + should_continue = 1; + + if (ingest.cmd_pgs_n == 0) { + cheerios_insert("No quick commands\r\n", 19); + break; + } + + char **cmds = ingest.cmd_pgs[ingest.cmd_pg_cur].cmds; + int cmds_n = ingest.cmd_pgs[ingest.cmd_pg_cur].cmds_n; + char *cmd_str; + size_t cmd_str_len; + + cmd_str_len = snprintf( + NULL, 0, + "Quick Commands (pg.%d of %d):\r\n", + ingest.cmd_pg_cur + 1, + ingest.cmd_pgs_n + ); + cmd_str = calloc(1, cmd_str_len + 1); + sprintf( + cmd_str, + "Quick Commands (pg.%d of %d):\r\n", + ingest.cmd_pg_cur + 1, + ingest.cmd_pgs_n + ); + + cheerios_insert(cmd_str, cmd_str_len); + for (int i = 0; i < cmds_n; i++) { + + cmd_str_len = snprintf( + NULL, 0, + "%4d: %s\r\n", + i+1, cmds[i] + ); + cmd_str = calloc(1, cmd_str_len + 1); + sprintf(cmd_str, "%4d: %s\r\n", i+1, cmds[i]); + + cheerios_insert(cmd_str, cmd_str_len); + free(cmd_str); + } + + break; + } + case 'p': + { + bytenuts_set_status(STATUS_CMDPAGE, "selecting..."); + + while (1) { + pthread_mutex_lock(ingest.term_lock); + ch = wgetch(ingest.input); + pthread_mutex_unlock(ingest.term_lock); + + if (ch != ERR) + break; + nanosleep(&(struct timespec){ 0, 100000 }, NULL); + } + + should_continue = 1; + bytenuts_set_status(STATUS_INGEST, "normal"); + + if (ch >= '1' || ch <= '9') { + int pg = ch - '1'; + + if (pg >= 0 && pg < ingest.cmd_pgs_n) { + ingest.cmd_pg_cur = pg; + } + } + + update_cmd_pg_status(); + break; + } + case 'i': + print_stats(); + bytenuts_set_status(STATUS_INGEST, "normal"); + should_continue = 1; + break; + case 'q': + bytenuts_stop(); + should_quit = 1; + break; + case 'x': + ingest.mode = INGEST_MODE_XMODEM; + bytenuts_set_status(STATUS_INGEST, "xmodem"); + break; + case 'X': + ingest.mode = INGEST_MODE_XMODEM1K; + bytenuts_set_status(STATUS_INGEST, "xmodem1k"); + break; + case 'h': + cheerios_insert( + "Commands (lead with ctrl-b):\r\n" + " c: print available quick commands\r\n" + " 0-9: load the given quick command (0 is 10)\r\n" + " p: select a different quick commands page\r\n" + " i: view info/stats\r\n" + " x: start XModem upload with 128B payloads\r\n" + " X: start XModem upload with 1024B payloads\r\n" + " h: view this help\r\n" + " q: quit Bytenuts\r\n", + 313 + ); + bytenuts_set_status(STATUS_INGEST, "normal"); + should_continue = 1; + break; + default: + bytenuts_set_status(STATUS_INGEST, "normal"); + should_continue = 1; + break; + } + + if (should_quit) { + break; + } else if (should_continue) { + continue; + } + } + + switch (ingest.mode) { + case INGEST_MODE_NORMAL: + mode_normal(ch); + break; + case INGEST_MODE_XMODEM: + mode_xmodem(128); + break; + case INGEST_MODE_XMODEM1K: + mode_xmodem(1024); + break; + default: + break; + } + + sched_yield(); + } + + pthread_exit(NULL); + return NULL; +} + +static int +mode_normal(int ch) +{ + if (handle_functions(ch)) + return 0; + + switch (ch) { + case '\n': /* send inputs */ + if (cheerios_input(ingest.inbuf)) { + break; + } + + /* store in history */ + ingest.history_pos = ingest.history_len; + if (ingest.inlen > 0) { + ingest.history_len++; + ingest.history = realloc(ingest.history, ingest.history_len * sizeof(char *)); + ingest.history[ingest.history_pos] = strdup(ingest.inbuf); + ingest.history_pos++; + } + + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); + ingest.inpos = 0; + ingest.inlen = 0; + inbuf_load(); + + break; + case KEY_UP: /* load in history */ + if (ingest.history_pos == 0) { /* top of history */ + break; + } else if (ingest.history_pos == ingest.history_len) { /* we have not loaded any history */ + strcpy(ingest.tmp_history, ingest.inbuf); + } + + ingest.history_pos--; + strcpy(ingest.inbuf, ingest.history[ingest.history_pos]); + ingest.inlen = strlen(ingest.inbuf); + ingest.inpos = ingest.inlen; + inbuf_load(); + + break; + case KEY_DOWN: /* load in history */ + if (ingest.history_pos == ingest.history_len) { /* no history loaded */ + break; + } else if (ingest.history_pos == ingest.history_len - 1) { /* load back temp storage */ + ingest.history_pos++; + strcpy(ingest.inbuf, ingest.tmp_history); + ingest.inlen = strlen(ingest.inbuf); + ingest.inpos = ingest.inlen; + } else { + ingest.history_pos++; + strcpy(ingest.inbuf, ingest.history[ingest.history_pos]); + ingest.inlen = strlen(ingest.inbuf); + ingest.inpos = ingest.inlen; + } + + inbuf_load(); + + break; + default: + { + char tmp[1024]; + int tmp_len; + + if (ingest.inpos == sizeof(ingest.inbuf) - 1) + break; + + tmp_len = ingest.inlen - ingest.inpos; + memcpy(tmp, &ingest.inbuf[ingest.inpos], tmp_len); + ingest.inbuf[ingest.inpos] = (char)ch; + ingest.inpos++; + ingest.inlen++; + memcpy(&ingest.inbuf[ingest.inpos], tmp, tmp_len); + + if (ingest.inlen == sizeof(ingest.inbuf)) { + ingest.inbuf[ingest.inlen - 1] = '\0'; + ingest.inlen--; + } + + inbuf_load(); + + break; + } + } + + return 0; +} + +static int +mode_xmodem(int block_sz) +{ + int quit_flag = 0; + + strcpy(ingest.tmp_history, ingest.inbuf); + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); + ingest.inlen = 0; + ingest.inpos = 0; + if (ingest.prepend) + free(ingest.prepend); + ingest.prepend = strdup("Give me a path (ctrl-c to stop): "); + inbuf_load(); + + while (ingest.running) { + int ch; + + pthread_mutex_lock(ingest.term_lock); + ch = wgetch(ingest.input); + pthread_mutex_unlock(ingest.term_lock); + + if (ch == ERR || handle_functions(ch)) { + nanosleep(&(struct timespec){ 0, 100000 }, NULL); + continue; + } + + switch (ch) { + case '\n': + pthread_mutex_lock(ingest.term_lock); + werase(ingest.input); + wmove(ingest.input, 0, 0); + wprintw(ingest.input, "Sending..."); + wrefresh(ingest.input); + pthread_mutex_unlock(ingest.term_lock); + + cheerios_gofwd(-1); + cheerios_xmodem(ingest.inbuf, block_sz); + quit_flag = 1; + break; + case CTRL('c'): + quit_flag = 1; + break; + case '\t': /* tab complete */ + auto_complete(); + inbuf_load(); + break; + default: + { + char tmp[1024]; + int tmp_len; + + if (ingest.inpos == sizeof(ingest.inbuf) - 1) + break; + + tmp_len = ingest.inlen - ingest.inpos; + memcpy(tmp, &ingest.inbuf[ingest.inpos], tmp_len); + ingest.inbuf[ingest.inpos] = (char)ch; + ingest.inpos++; + ingest.inlen++; + memcpy(&ingest.inbuf[ingest.inpos], tmp, tmp_len); + + if (ingest.inlen == sizeof(ingest.inbuf)) { + ingest.inbuf[ingest.inlen - 1] = '\0'; + ingest.inlen--; + } + + inbuf_load(); + + break; + } + } + + if (quit_flag) { + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); + strcpy(ingest.inbuf, ingest.tmp_history); + ingest.inlen = strlen(ingest.inbuf); + ingest.inpos = ingest.inlen; + ingest.mode = INGEST_MODE_NORMAL; + free(ingest.prepend); + ingest.prepend = NULL; + + bytenuts_set_status(STATUS_INGEST, "normal"); + inbuf_load(); + return 0; + } + } + + return 0; +} + +static int +handle_functions(int ch) +{ + // char st[128]; + // sprintf(st, "%d", ch); + // cheerios_info(st); + + switch (ch) { + case KEY_PPAGE: /* page up */ + cheerios_goback(cheerios_getmaxy() / 2); + return 1; + case KEY_NPAGE: /* page down */ + cheerios_gofwd(cheerios_getmaxy() / 2); + return 1; + case KEY_SHOME: /* to back of log */ + cheerios_goback(-1); + return 1; + case KEY_SEND: /* go to front of log */ + cheerios_gofwd(-1); + return 1; + case 566: /* ctrl + up arrow */ + cheerios_goback(1); + return 1; + case 525: /* ctrl + down arrow */ + cheerios_gofwd(1); + return 1; + case KEY_RESIZE: + bytenuts_update_screen_size(); + return 1; + case KEY_BACKSPACE: + { + if (ingest.inpos == 0) { + return 1; + } else if (ingest.inpos == ingest.inlen) { + ingest.inpos--; + ingest.inlen--; + ingest.inbuf[ingest.inlen] = '\0'; + inbuf_load(); + return 1; + } + + char tmp[1024]; + int tmp_len = ingest.inlen - ingest.inpos; + + memcpy(tmp, &ingest.inbuf[ingest.inpos], ingest.inlen - ingest.inpos); + ingest.inpos--; + ingest.inlen--; + memcpy(&ingest.inbuf[ingest.inpos], tmp, tmp_len); + ingest.inbuf[ingest.inlen] = '\0'; + inbuf_load(); + + return 1; + } + case KEY_DC: /* delete key */ + { + char tmp[1024]; + int tmp_len; + + if (ingest.inpos == ingest.inlen) { + return 1; + } else if (ingest.inpos == ingest.inlen - 1) { + ingest.inbuf[ingest.inpos] = '\0'; + ingest.inlen--; + inbuf_load(); + return 1; + } + + tmp_len = ingest.inlen - ingest.inpos - 1; + memcpy(tmp, &ingest.inbuf[ingest.inpos + 1], tmp_len); + ingest.inlen--; + memcpy(&ingest.inbuf[ingest.inpos], tmp, tmp_len); + ingest.inbuf[ingest.inlen] = '\0'; + inbuf_load(); + + return 1; + } + case KEY_LEFT: + if (ingest.inpos == 0) + return 1; + ingest.inpos--; + inbuf_load(); + return 1; + case KEY_RIGHT: + if (ingest.inpos == ingest.inlen) + return 1; + ingest.inpos++; + inbuf_load(); + return 1; + case KEY_END: + ingest.inpos = ingest.inlen; + inbuf_load(); + return 1; + case KEY_HOME: + ingest.inpos = 0; + inbuf_load(); + return 1; +#if 0 + case KEY_MOUSE: + { + MEVENT ev; + if (getmouse(&ev) != OK) + return 1; + + if (ev.bstate & BUTTON4_PRESSED) { /* scroll up */ + cheerios_goback(1); + } else if (ev.bstate & BUTTON5_PRESSED) { /* scroll down */ + cheerios_gofwd(1); + } + + return 1; + } +#endif + default: + return 0; + } +} + +static int +inbuf_load() +{ + int cx, cy = -1; + + pthread_mutex_lock(ingest.term_lock); + + werase(ingest.input); + wmove(ingest.input, 0, 0); + if (ingest.prepend) + wprintw(ingest.input, "%s", ingest.prepend); + + for (int i = 0; i < ingest.inlen; i++) { + if (i == ingest.inpos) + getyx(ingest.input, cy, cx); + waddch(ingest.input, ingest.inbuf[i]); + } + + if (cx < 0) { + cx = ingest.inlen; + cy = 0; + } + + wmove(ingest.input, cy, cx); + wrefresh(ingest.input); + + pthread_mutex_unlock(ingest.term_lock); + + return 0; +} + +static int +print_stats() +{ + char st_line[128]; + + sprintf(st_line, "\r\nSTATS\r\n"); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "input line count: %d\r\n", ingest.history_len); + cheerios_insert(st_line, strlen(st_line)); + + cheerios_print_stats(); + bytenuts_print_stats(); + + return 0; +} + +static int +auto_complete() +{ + char cmd[1024]; + char line[1024]; + char **matches = NULL; + int nmatches = 0; + FILE *sh = NULL; + char *inbuf_cpy = NULL; + char *inbuf_basename = NULL; + size_t inbuf_basename_len; + + /* retrieve directory listing with ls */ + inbuf_cpy = strdup(ingest.inbuf); + if (ingest.inlen > 0 && ingest.inbuf[ingest.inlen - 1] == '/') { + snprintf( + cmd, sizeof(cmd), + "cd %s 2> /dev/null && ls -1 2> /dev/null", + ingest.inbuf + ); + inbuf_basename = ""; + } else { + snprintf( + cmd, sizeof(cmd), + "cd $(dirname \"%s\") 2> /dev/null && ls -1 2> /dev/null", + ingest.inbuf + ); + inbuf_basename = basename(inbuf_cpy); + } + inbuf_basename_len = strlen(inbuf_basename); + + sh = popen(cmd, "r"); + while (fgets(line, sizeof(line), sh)) { + size_t line_len = strlen(line); + while (line[line_len - 1] == '\n' || line[line_len - 1] == '\r') { + line[line_len - 1] = '\0'; + line_len = strlen(line); + } + + if (!memcmp(inbuf_basename, line, inbuf_basename_len)) { + nmatches++; + matches = realloc(matches, sizeof(char *) * nmatches); + matches[nmatches - 1] = strdup(line); + } + } + + /* the cd/ls just failed, ignore output */ + int ret = pclose(sh); + if (ret) + goto auto_complete_cleanup; + + if (nmatches == 0) + goto auto_complete_cleanup; + + /* check if we can fill the inbuf */ + int offset = inbuf_basename_len; + + while (1) { + char add = matches[0][offset]; + + for (int i = 1; i < nmatches; i++) { + if (add != matches[i][offset]) { + add = '\0'; + break; + } + } + + if (add == '\0') + break; + + offset++; + } + + if (offset > inbuf_basename_len) { + size_t add_len = offset - inbuf_basename_len; + + if (add_len + ingest.inlen >= sizeof(ingest.inbuf)) + add_len = sizeof(ingest.inbuf) - ingest.inlen - 1; + + memcpy(&ingest.inbuf[ingest.inlen], &matches[0][inbuf_basename_len], add_len); + ingest.inlen += add_len; + ingest.inpos = ingest.inlen; + } + + /* auto complete directory '/' */ + if (nmatches == 1) { + struct stat st; + + stat(ingest.inbuf, &st); + if ( + S_ISDIR(st.st_mode) && + ingest.inlen < sizeof(ingest.inbuf) - 1 && + (ingest.inpos == 0 || ingest.inbuf[ingest.inpos - 1] != '/') + ) { + ingest.inbuf[ingest.inpos] = '/'; + ingest.inpos++; + ingest.inlen++; + } + } + +auto_complete_cleanup: + if (matches) { + for (int i = 0; i < nmatches; i++) { + free(matches[i]); + } + free(matches); + } + + if (inbuf_cpy) + free(inbuf_cpy); + + return 0; +} + +static int +read_cmd_page(int idx) +{ + char fname[128]; + FILE *fd; + char line[1024]; + + snprintf(fname, sizeof(fname), ".config/bytenuts/commands%d", idx+1); + + fd = fopen(fname, "r"); + if (!fd) + return -1; + + ingest.cmd_pgs_n++; + ingest.cmd_pgs = realloc( + ingest.cmd_pgs, + sizeof(*ingest.cmd_pgs) * ingest.cmd_pgs_n + ); + memset(&ingest.cmd_pgs[idx], 0, sizeof(*ingest.cmd_pgs)); + + while (fgets(line, sizeof(line), fd)) { + size_t line_len = strlen(line); + + /* strip CRLF */ + while (line_len > 0 && (line[line_len-1] == '\n' || line[line_len-1] == '\r')) { + line_len--; + line[line_len] = '\0'; + } + + ingest.cmd_pgs[idx].cmds_n++; + ingest.cmd_pgs[idx].cmds[ingest.cmd_pgs[idx].cmds_n-1] = strdup(line); + } + + fclose(fd); + return 0; +} + +static void +update_cmd_pg_status() +{ + if (ingest.cmd_pgs_n > 0) { + bytenuts_set_status(STATUS_CMDPAGE, "pg.%d", ingest.cmd_pg_cur+1); + } else { + bytenuts_set_status(STATUS_CMDPAGE, "n/a"); + } +} diff --git a/src/ingest.h b/src/ingest.h new file mode 100644 index 0000000..b9fcda2 --- /dev/null +++ b/src/ingest.h @@ -0,0 +1,47 @@ +#ifndef _INGEST_H_ +#define _INGEST_H_ + +#include +#include + +#include "bytenuts.h" + +enum ingest_mode_enum { + INGEST_MODE_NORMAL = 0, + INGEST_MODE_XMODEM, + INGEST_MODE_XMODEM1K, +}; + +typedef struct ingest_struct { + WINDOW *input; + pthread_mutex_t *term_lock; + pthread_t thr; + volatile int running; + char *prepend; /* string to be prepended in the prompt */ + char inbuf[1024]; + int inpos; /* cursor position within the inbuf itself */ + int inlen; /* length of the input buffer */ + char **history; /* array of strings representing the command history */ + char tmp_history[1024]; /* copy of inbuf before we load in history */ + int history_len; + int history_pos; /* where we currently reside in the history */ + int mode; + bytenuts_config_t *config; +#define INGEST_COMMAND_PGSZ (10) + /* command pages loaded on startup */ + struct { + char *cmds[INGEST_COMMAND_PGSZ]; + int cmds_n; + } *cmd_pgs; + int cmd_pgs_n; + /* what command page is currently selected */ + int cmd_pg_cur; +} ingest_t; + +/* startup the input window thread */ +int ingest_start(bytenuts_t *bytenuts); + +/* stop the input window thread */ +int ingest_stop(); + +#endif /* _INGEST_H_ */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..b75957c --- /dev/null +++ b/src/main.c @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "bytenuts.h" + +static void sigint_handler(int s); + +int +main(int argc, char **argv) +{ + struct sigaction act; + act.sa_handler = sigint_handler; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; + sigaction(SIGINT, &act, NULL); + + if (bytenuts_run(argc, argv)) { + bytenuts_stop(); + exit(1); + } + + exit(0); +} + +static void +sigint_handler(int s) +{ + bytenuts_kill(); + exit(1); +} diff --git a/src/serial.c b/src/serial.c new file mode 100644 index 0000000..364fcbe --- /dev/null +++ b/src/serial.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include +#include + +#include "serial.h" + +int +serial_open(const char *path, speed_t bps) +{ + int fd; + struct termios otio; + struct termios ntio; + + fd = open(path, O_RDWR | O_NOCTTY | O_NDELAY); + if (fd == -1) { + return -1; + } + + tcgetattr(fd, &otio); + tcgetattr(fd, &ntio); + + cfsetispeed(&ntio, bps); + cfsetospeed(&ntio, bps); + + /* https://www.cmrr.umn.edu/~strupp/serial.html */ + /* 8N1 */ + ntio.c_cflag |= (CLOCAL | CREAD); + ntio.c_cflag &= ~(CSIZE|PARENB|CSTOPB); + ntio.c_cflag |= (CS8); /* Set size after setting CSIZE mask */ + ntio.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXOFF|IXANY); + ntio.c_oflag &= ~(OPOST); + ntio.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); + + ntio.c_cc[VINTR] = 0; /* Ctrl-c */ + ntio.c_cc[VQUIT] = 0; /* Ctrl-\ */ + ntio.c_cc[VERASE] = 0; /* del */ + ntio.c_cc[VKILL] = 0; /* @ */ + ntio.c_cc[VEOF] = 4; /* Ctrl-d */ + ntio.c_cc[VTIME] = 0; /* inter-character timer unused */ + ntio.c_cc[VMIN] = 0; /* non-blocking */ + ntio.c_cc[VSWTC] = 0; /* '\0' */ + ntio.c_cc[VSTART] = 0; /* Ctrl-q */ + ntio.c_cc[VSTOP] = 0; /* Ctrl-s */ + ntio.c_cc[VSUSP] = 0; /* Ctrl-z */ + ntio.c_cc[VEOL] = 0; /* '\0' */ + ntio.c_cc[VREPRINT] = 0; /* Ctrl-r */ + ntio.c_cc[VDISCARD] = 0; /* Ctrl-u */ + ntio.c_cc[VWERASE] = 0; /* Ctrl-w */ + ntio.c_cc[VLNEXT] = 0; /* Ctrl-v */ + ntio.c_cc[VEOL2] = 0; /* '\0' */ + + tcflush(fd, TCIFLUSH); + tcsetattr(fd, TCSANOW, &ntio); + + return fd; +} diff --git a/src/serial.h b/src/serial.h new file mode 100644 index 0000000..9478298 --- /dev/null +++ b/src/serial.h @@ -0,0 +1,8 @@ +#ifndef _SERIAL_H_ +#define _SERIAL_H_ + +#include + +int serial_open(const char *path, speed_t bps); + +#endif /* _SERIAL_H_ */ diff --git a/src/xmodem.c b/src/xmodem.c new file mode 100644 index 0000000..45cee5f --- /dev/null +++ b/src/xmodem.c @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "xmodem.h" + +int _xmodem_wait(int fd, uint8_t *ret, int timeout_ms); + +uint16_t _xmodem_crc(void *buf, size_t len); +uint8_t _xmodem_csum(void *buf, size_t len); +void _xmodem_status_print(size_t sent, size_t total); + +int +xmodem_send( + int dest_fd, + int in_fd, + size_t sz, + int block_sz, + void (*status_cb)(size_t sent, size_t total) +) +{ + uint8_t start_byte; + size_t sent; + uint8_t *buf; + size_t buf_sz; + uint8_t packet_num; + int retries; + int ret = 0; + + if (block_sz != 128 && block_sz != 1024) { + return -1; + } + + /* wait for start byte, 'C' for CRC, NAK for CSUM */ + for (int i = 0; i < 11; i++) { + if (i == 10) { + return XMODEM_ERR_TIMEOUT; + } + + if ( + !_xmodem_wait(dest_fd, &start_byte, XMODEM_TIMEOUT) && + (start_byte == XMODEM_CRC || start_byte == XMODEM_NAK) + ) { + break; + } + } + + /* start sendin' bytes */ + buf_sz = 3 + block_sz; /* SOH/STX | idx | ~idx | payload */ + if (start_byte == XMODEM_NAK) { + buf_sz += 1; + } else { + buf_sz += 2; + } + buf = malloc(buf_sz); + sent = 0; + packet_num = 1; /* yes, this starts at 1 */ + + while (sent < sz) { + size_t read_len; + + if (block_sz == 128) { + buf[0] = XMODEM_SOH; + } else { + buf[0] = XMODEM_STX; + } + + buf[1] = packet_num; + buf[2] = ~packet_num; + + if ((sz - sent) >= block_sz) { + read_len = block_sz; + } else { + /* back fill payload with pad */ + read_len = sz - sent; + for (int i = read_len; i < block_sz; i++) { + buf[i] = XMODEM_PAD; + } + } + + if (read(in_fd, &buf[3], read_len) != read_len) { + return XMODEM_ERR_FILEIO; + } + + if (start_byte == XMODEM_CRC) { + /* write CRC big endian */ + uint16_t crc = _xmodem_crc(&buf[3], block_sz); + buf[buf_sz-2] = (uint8_t)((0xFF00 & crc) >> 8); + buf[buf_sz-1] = (uint8_t)(0x00FF & crc); + } else { + buf[buf_sz-1] = _xmodem_csum(&buf[3], block_sz); + } + + /* try to send this packet until we get an ACK */ + retries = 0; + while (1) { + uint8_t recv_char; + + if (retries == 10) { + ret = XMODEM_ERR_TIMEOUT; + goto end_of_transmission; + } + + if (write(dest_fd, buf, buf_sz) != buf_sz){ + return XMODEM_ERR_WRITE; + } + + if (_xmodem_wait(dest_fd, &recv_char, XMODEM_TIMEOUT)) { + return XMODEM_ERR_READ; + } + + if (recv_char == XMODEM_ACK) { + break; + } + + retries++; + } + + sent += read_len; + packet_num++; /* expect overflow */ + status_cb(sent, sz); + } + +end_of_transmission: + retries = 0; + while (1) { + uint8_t ack_char; + + if (retries == 10) { + return XMODEM_ERR_TIMEOUT; + } + + write(dest_fd, &(uint8_t){XMODEM_EOT}, 1); + + if ( + !_xmodem_wait(dest_fd, &ack_char, XMODEM_TIMEOUT) && + ack_char == XMODEM_ACK + ) { + break; + } + + retries++; + } + + return ret; +} + +int +_xmodem_wait(int fd, uint8_t *ret, int timeout_ms) +{ + struct pollfd pfd; + + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + + if (poll(&pfd, 1, timeout_ms) <= 0) { + return -1; + } + + read(fd, ret, 1); + + return 0; +} + +uint16_t +_xmodem_crc(void *buf, size_t len) +{ + uint16_t crc; + char i; + + crc = 0; + while (--len >= 0) + { + crc = crc ^ (uint16_t) (*(uint8_t *)buf++) << 8; + i = 8; + do + { + if (crc & 0x8000) + crc = crc << 1 ^ 0x1021; + else + crc = crc << 1; + } while(--i); + } + + return (crc); +} + +uint8_t +_xmodem_csum(void *buf, size_t len) +{ + uint8_t ret = 0; + + for (size_t i = 0; i < len; i++) { + ret += ((uint8_t *)buf)[i]; + } + + return ret; +} diff --git a/src/xmodem.h b/src/xmodem.h new file mode 100644 index 0000000..ecc0092 --- /dev/null +++ b/src/xmodem.h @@ -0,0 +1,48 @@ +#ifndef _XMODEM_H_ +#define _XMODEM_H_ + +#include + +#include "serial.h" + +#define XMODEM_SOH (0x01) +#define XMODEM_STX (0x02) +#define XMODEM_EOT (0x04) +#define XMODEM_ACK (0x06) +#define XMODEM_NAK (0x15) +#define XMODEM_ETB (0x17) +#define XMODEM_CAN (0x18) +#define XMODEM_CRC (0x43) /* 'C' */ +#define XMODEM_PAD (0x1A) + +#define XMODEM_TIMEOUT (10000) /* in ms */ + +#define XMODEM_ERR_TIMEOUT (1) /* ACK timeout */ +#define XMODEM_ERR_WRITE (2) /* Failed to write to file descriptor */ +#define XMODEM_ERR_FILEIO (3) /* Error on reading from file descriptor */ +#define XMODEM_ERR_READ (4) /* Failed to read from the serial connection */ +#define XMODEM_ERR_CANCEL (5) /* Sender cancelled the transmission */ +#define XMODEM_ERR_DONE (6) /* Sender finished the transmission */ + +/* + * Following this setup: https://pythonhosted.org/xmodem/xmodem.html + * Choose to send `sz` bytes from `in_fd` to `dest_fd` via XModem with a payload + * length of `block_sz`. + */ +int xmodem_send( + int dest_fd, + int in_fd, + size_t sz, + int block_sz, + void (*status_cb)(size_t sent, size_t total) +); + +#if 0 +/* Receive an xmodem message + * + * If sz is non-negative, then only that many bytes will actually be written + * to fd. But the entire xmodem transfer will be completed */ +int xmodem_recv(int in_fd, int dest_fd, ssize_t sz); +#endif + +#endif /* _XMODEM_H_ */