Public bytenuts repository creation

This commit is contained in:
Aaron Cook 2022-02-06 16:50:55 -06:00
commit aaaf6cb805
14 changed files with 2576 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
build/
.vscode/
*.log
tags

44
Makefile Normal file
View file

@ -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)

125
README.md Normal file
View file

@ -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] <serial path>
Configs get loaded from ${HOME}/.bytenuts/config (if file exists)
OPTIONS
-h: show this help
-b <baud>: set a baud rate (default 115200)
-l <path>: log all output to the given file
-c <path>: 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 `<name>=<value>`. 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<idx>`. Bytenuts will load pages starting from `commands1` until a `commands<idx>` 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`.

518
src/bytenuts.c Normal file
View file

@ -0,0 +1,518 @@
#include <ncurses.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
#include "bytenuts.h"
#include "cheerios.h"
#include "ingest.h"
#define USAGE ( \
"USAGE\n\n" \
"bytenuts [OPTIONS] <serial path>\n" \
"\nConfigs get loaded from ${HOME}/.bytenuts/config (if file exists)\n" \
"\nOPTIONS\n\n" \
"-h: show this help\n" \
"-b <baud>: set a baud rate (default 115200)\n" \
"-l <path>: log all output to the given file\n" \
"-c <path>: 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=<char>: 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 "";
}

75
src/bytenuts.h Normal file
View file

@ -0,0 +1,75 @@
#ifndef _BYTENUTES_H_
#define _BYTENUTES_H_
#include <ncurses.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#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_ */

523
src/cheerios.c Normal file
View file

@ -0,0 +1,523 @@
#include <ncurses.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#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;
}

87
src/cheerios.h Normal file
View file

@ -0,0 +1,87 @@
#ifndef _CHEERIOS_H_
#define _CHEERIOS_H_
#include <ncurses.h>
#include <pthread.h>
#include <stdio.h>
#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_ */

804
src/ingest.c Normal file
View file

@ -0,0 +1,804 @@
#include <libgen.h>
#include <ncurses.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#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");
}
}

47
src/ingest.h Normal file
View file

@ -0,0 +1,47 @@
#ifndef _INGEST_H_
#define _INGEST_H_
#include <ncurses.h>
#include <pthread.h>
#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_ */

31
src/main.c Normal file
View file

@ -0,0 +1,31 @@
#include <sched.h>
#include <signal.h>
#include <stdlib.h>
#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);
}

59
src/serial.c Normal file
View file

@ -0,0 +1,59 @@
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
#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;
}

8
src/serial.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef _SERIAL_H_
#define _SERIAL_H_
#include <termios.h>
int serial_open(const char *path, speed_t bps);
#endif /* _SERIAL_H_ */

203
src/xmodem.c Normal file
View file

@ -0,0 +1,203 @@
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
#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;
}

48
src/xmodem.h Normal file
View file

@ -0,0 +1,48 @@
#ifndef _XMODEM_H_
#define _XMODEM_H_
#include <stdio.h>
#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_ */