diff --git a/.gitignore b/.gitignore index 8c03c21..662296d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ .vscode/ +.vim/ *.log -tags +tags \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8634b5a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/PDCurses"] + path = lib/PDCurses + url = git@github.com:wmcbrine/PDCurses.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abf5af7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Aaron Cook + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile index 3a7376d..02282fd 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,39 @@ -CC=clang -CFLAGS=-g -Wall -MMD -pthread -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE -LDFLAGS=-lpthread -lncurses +OS=LINUX 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 +CFLAGS=-Wall -MMD -pthread -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=199309L +LIB_DEPS= +LIB_OBJS= 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 +ifeq ($(OS), LINUX) + CC=clang + LDFLAGS=-lncurses -lpthread + TARGET=$(DIR_BIN)/bytenuts +else + CC=x86_64-w64-mingw32-gcc + CFLAGS+=-Ilib/PDCurses/wincon/ -Ilib/PDCurses/ + LDFLAGS=-lpthread -static + LIB_DEPS+=PDCurses + LIB_OBJS+=lib/PDCurses/wincon/*.o + TARGET=$(DIR_BIN)/bytenuts.exe +endif + +DEBUG ?= 0 +ifeq ($(DEBUG), 1) + CFLAGS += -g +else + CFLAGS += -O2 +endif + +.PHONY: all install uninstall clean PDCurses all: $(TARGET) @@ -28,16 +43,19 @@ install: uninstall: rm -f /usr/local/bin/bytenuts -$(TARGET): $(OBJS) +$(TARGET): $(OBJS) $(LIB_DEPS) @mkdir -p $(dir $@) @echo "link and compile for $@" - @$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(OBJS) + @$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(OBJS) $(LIB_OBJS) $(DIR_OBJ)/$(DIR_SRC)/%.o: $(DIR_SRC)/%.c @mkdir -p $(dir $@) @echo "compile $<" @$(CC) $(CFLAGS) -c $< -o $@ +PDCurses: + cd lib/PDCurses/wincon && $(MAKE) -j CC=x86_64-w64-mingw32-gcc + clean: rm -rf $(DIR_BUILD) diff --git a/README.md b/README.md index b1893d7..a0c3a82 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,11 @@ Bytenuts has been tested and built on x86-64 and 32-bit ARM Ubuntu 18. - 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]` +- Session resumption - Bytenuts can load the previous instance's commands and serial output -## Planned Features +Sample screenshot running in Windows Terminal and WSL: -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 +![Bytenuts Screenshot](Screenshot.png) ## Launch Options @@ -50,12 +39,41 @@ bytenuts [OPTIONS] Configs get loaded from ${HOME}/.bytenuts/config (if file exists) -OPTIONS + 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 +-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. + +-r|--resume + Resume the previous instance of bytenuts. + +--colors=<0|1> + Turn 8-bit ANSI colors off/on. + +--echo=<0|1> + Turn input echoing off/on. + +--no_crlf=<0|1> + Choose to send LF and not CRLF on input. + +--escape= + Change the default ctrl+b escape character. + +--inter_cmd_to= + Set the intercommand timeout in milliseconds (default is 10ms). + +--time_fmt= + Time format as used by strftime to prepend to every log line. ``` ## Navigation @@ -70,17 +88,23 @@ If the output window reaches the current line of output, then the output will co ## Configuration File -There are only three supported configs currently. Each config is defined like `=`. Here is a sample config: +Many of the launch options have a corresponding configuration token that can be saved to a config file. Each config is defined like `=`. Here is a sample config: ``` colors=1 echo=0 no_crlf=0 +escape=a +inter_cmd_to=100 +time_fmt=%X %m/%d %Z|> ``` - `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`) +- `escape` - change what character is used as an escape sequence for commands (e.g. if set to `escape=a`, Bytenuts can be exited with `ctrl+a, q`) +- `inter_cmd_to` - Set a timeout in milliseconds that must be met. Useful for pasting in multiple lines and ensuring a short delay in between the commands. +- `time_fmt` - The time format string (see `man 3 strftime`) to be prepended to every line in the log file (will not get printed in the console view) Bytenuts looks for the configs at `~/.config/bytenuts/config`. @@ -100,14 +124,24 @@ 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) + p: select a different quick commands page i: view info/stats x: start XModem upload with 128B payloads X: start XModem upload with 1024B payloads + H: enter/exit hex buffer mode h: view this help q: quit Bytenuts ``` +### Hex Buffer Mode +When the `ctrl+b H` command has been issued for the first time, you will enter hex buffer mode. In this mode, the input buffer is interpreted as a hex string and will be converted to its byte equivalent before it gets sent to the target. Example inputs: + +- `746573740d0a` gets sent as `test\r\n` +- `d0a` gets sent as `\r\n` as it assumed the leading `0` has been omitted +- `74657374xd0a` gets sent as `test` since the `xd` byte contains an invalid hex character + +Hex buffer mode can be turned off with a second `ctrl+b H`. + ## 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. @@ -116,10 +150,16 @@ Bytenuts will only store the first ten commands for each page, so the first 10 l 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`). +## Session Resumption + +Bytenuts caches the current instance's command list and output buffer in `~/.config/bytenuts/inbuf.log` and `~/.config/bytenuts/outbuf.log` respectively. If launched with the `-r` flag, Bytenuts will load in the input and output history from those files. The `outbuf.log` can also serve as a backup log if one forgot to launch with the `-l` flag. + +While Bytenuts is running, the process will be writing its backup output and input to `~/.config/bytenuts/outbuf..log` and `~/.config/bytenuts/inbuf..log` and rename those files to the paths without the PID upon exit. This allows for multiple processes to be open without writing to the same log files. Thus, the `-r` flag will load the most recently exited Bytenuts instance. + ## 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`. +Building Bytenuts is very simple. All you need is clang and libncurses (`sudo apt install clang libncurses5-dev`). 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/Screenshot.png b/Screenshot.png new file mode 100644 index 0000000..1b9c0a2 Binary files /dev/null and b/Screenshot.png differ diff --git a/lib/PDCurses b/lib/PDCurses new file mode 160000 index 0000000..5c62af0 --- /dev/null +++ b/lib/PDCurses @@ -0,0 +1 @@ +Subproject commit 5c62af03e9a05e3a3ae8c8354c1234b772dcf4b0 diff --git a/src/bstr.c b/src/bstr.c new file mode 100644 index 0000000..b508ad6 --- /dev/null +++ b/src/bstr.c @@ -0,0 +1,117 @@ +#include +#include +#include +#include + +#include "bstr.h" + +typedef struct bstr_history_struct { + char **strs; /* strings in our history, in reverse chronological order */ + int len; /* number of strings in the history */ + int pos; /* current position index in the history */ +} bstr_history_t; + +char * +bstr_print(char *base, const char *fmt, ...) +{ + char *new_buf; + size_t new_len; + va_list ap; + + va_start(ap, fmt); + new_len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + new_buf = calloc(1, new_len + 1); + + va_start(ap, fmt); + vsprintf(new_buf, fmt, ap); + va_end(ap); + + if (!base) { + return new_buf; + } else { + size_t baselen = strlen(base); + + base = realloc(base, baselen + new_len + 1); + memcpy(&base[baselen], new_buf, new_len); + base[baselen + new_len] = '\0'; + + free(new_buf); + return base; + } +} + +bstr_history_handle +bstr_history_create(void) +{ + bstr_history_t *ret = calloc(1, sizeof(bstr_history_t)); + ret->pos = -1; + return ret; +} + +const char * +bstr_history_atpos(bstr_history_handle hist) +{ + if ((hist->len == 0) || (hist->pos < 0)) { + return NULL; + } else { + return hist->strs[hist->pos]; + } +} + +void +bstr_history_new_entry(bstr_history_handle hist, const char *str) +{ + /* stored in reverse chronological order */ + hist->len++; + hist->strs = realloc(hist->strs, sizeof(char *) * hist->len); + hist->strs[hist->len-1] = strdup(str); +} + +void +bstr_history_older(bstr_history_handle hist) +{ + if (hist->len == 0) + return; + + if (hist->pos == -1) { + hist->pos = hist->len - 1; + } else { + hist->pos--; + } + + if (hist->pos < 0) + hist->pos = 0; +} + +void +bstr_history_newer(bstr_history_handle hist) +{ + if ((hist->len == 0) || (hist->pos == -1)) + return; + + hist->pos++; + if (hist->pos >= hist->len) + hist->pos = -1; +} + +void +bstr_history_unset_pos(bstr_history_handle hist) +{ + hist->pos = -1; +} + +void +bstr_history_destroy(bstr_history_handle hist) +{ + if (!hist) + return; + + if (hist->len > 0) { + for (int i = 0; i < hist->len; i++) { + free(hist->strs[i]); + } + free(hist->strs); + } +} diff --git a/src/bstr.h b/src/bstr.h new file mode 100644 index 0000000..b87585f --- /dev/null +++ b/src/bstr.h @@ -0,0 +1,40 @@ +#ifndef _BSTR_H_ +#define _BSTR_H_ + +#include +#include + +typedef struct bstr_history_struct * bstr_history_handle; + + +/* printf onto the end of base, reallocating and returning that buffer + * can provide NULL for no base */ +__attribute__((format(printf, 2, 3))) +char *bstr_print(char *base, const char *fmt, ...); + +/* Create a new bstr_history struct. The position of the history will start as + * unset. */ +bstr_history_handle bstr_history_create(void); + +/* return the string at the current history position (NULL if empty or position + * is not set) */ +const char *bstr_history_atpos(bstr_history_handle hist); + +/* Put a new entry into the history */ +void bstr_history_new_entry(bstr_history_handle hist, const char *str); + +/* Move the postition index one entry older. If the history position is unset, + * it is set to the newest entry. */ +void bstr_history_older(bstr_history_handle hist); + +/* Move the position index one entry newer. If the position is at the newest + * entry, it becomes unset. If the position is unset, it stays unset. */ +void bstr_history_newer(bstr_history_handle hist); + +/* Unset the history position index */ +void bstr_history_unset_pos(bstr_history_handle hist); + +/* destroy/free a bstr_history */ +void bstr_history_destroy(bstr_history_handle hist); + +#endif /* _BSTR_H_ */ diff --git a/src/bytenuts.c b/src/bytenuts.c index 34fa4bd..7d8af20 100644 --- a/src/bytenuts.c +++ b/src/bytenuts.c @@ -1,11 +1,14 @@ -#include +#ifdef __MINGW32__ +# include +#else +# include +#endif #include #include #include #include #include #include -#include #include #include "bytenuts.h" @@ -16,21 +19,24 @@ "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" \ +"\n OPTIONS\n=========\n\n" \ +"-h\n Show this help.\n\n" \ +"-b \n Set a baud rate (default 115200).\n\n" \ +"-l \n Log all output to the given file.\n\n" \ +"-c \n Load a config from the given path rather than the default.\n\n" \ +"-r|--resume\n Resume the previous instance of bytenuts.\n\n" \ +"--colors=<0|1>\n Turn 8-bit ANSI colors off/on.\n\n" \ +"--echo=<0|1>\n Turn input echoing off/on.\n\n" \ +"--no_crlf=<0|1>\n Choose to send LF and not CRLF on input.\n\n" \ +"--escape=\n Change the default ctrl+b escape character.\n\n" \ +"--inter_cmd_to=\n Set the intercommand timeout in milliseconds (default is 10ms).\n\n" \ +"--time_fmt=\n Time format as used by strftime to prepend to every log line.\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 int read_state(); +static int load_state(); static bytenuts_t bytenuts; @@ -48,16 +54,28 @@ bytenuts_run(int argc, char **argv) return -1; } + if (bytenuts.resume) { + read_state(); + } + bytenuts.serial_fd = serial_open(bytenuts.config.serial_path, bytenuts.config.baud); - if (bytenuts.serial_fd < 0) { + if (bytenuts.serial_fd == SERIAL_INVALID) { + printf( + "Failed to open serial port \"%s\"\r\n", + bytenuts.config.serial_path + ); return -1; } + printf("Opened \"%s\"\r\n", bytenuts.config.serial_path); + +#ifndef __MINGW32__ /* use pseudo-terminals for testing purposes */ if (!strcmp(bytenuts.config.serial_path, "/dev/ptmx")) { grantpt(bytenuts.serial_fd); unlockpt(bytenuts.serial_fd); } +#endif // Initialize ncurses initscr(); @@ -100,11 +118,24 @@ bytenuts_run(int argc, char **argv) cheerios_start(&bytenuts); ingest_start(&bytenuts); +#ifndef __MINGW32__ 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); } +#endif + + if (bytenuts.resume) { + load_state(); + } else { + cheerios_print( + "Welcome to Bytenuts\r\n" + "To quit press ctrl-%c q\r\n" + "To see all commands, press ctrl-%c h\r\n", + bytenuts.config.escape, bytenuts.config.escape + ); + } pthread_mutex_lock(&bytenuts.lock); pthread_cond_wait(&bytenuts.stop_cond, &bytenuts.lock); @@ -135,7 +166,7 @@ bytenuts_kill() delwin(bytenuts.out_win); endwin(); - close(bytenuts.serial_fd); + serial_close(bytenuts.serial_fd); } int @@ -151,7 +182,7 @@ bytenuts_print_stats() 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)); + sprintf(st_line, "baud: %ld\r\n", 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)); @@ -159,6 +190,10 @@ bytenuts_print_stats() 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)); + sprintf(st_line, "inter_cmd_to: %d\r\n", bytenuts.config.inter_cmd_to); + cheerios_insert(st_line, strlen(st_line)); + sprintf(st_line, "time_fmt: %s\r\n", bytenuts.config.time_fmt); + cheerios_insert(st_line, strlen(st_line)); return 0; } @@ -240,6 +275,8 @@ bytenuts_set_status(int user, const char *fmt, ...) int bytenuts_update_screen_size() { + char *old_status = strdup(bytenuts.bytenuts_status); + pthread_mutex_lock(&bytenuts.term_lock); wresize(bytenuts.status_win, 1, COLS); @@ -256,7 +293,10 @@ bytenuts_update_screen_size() pthread_mutex_unlock(&bytenuts.term_lock); + bytenuts_set_status(STATUS_BYTENUTS, old_status); cheerios_insert("", 0); + ingest_refresh(); + free(old_status); return 0; } @@ -273,10 +313,17 @@ parse_args(int argc, char **argv) /* setup default options first */ bytenuts.config = CONFIG_DEFAULT; { +#ifdef __MINGW32__ + char *home = getenv("HOMEPATH"); + char path[] = "\\.config\\bytenuts\\config"; + bytenuts.config.config_path = malloc(strlen(home) + strlen(path) + 4); + sprintf(bytenuts.config.config_path, "C:\\%s%s", home, path); +#else 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); +#endif } for (int i = 1; i < argc - 1; i++) { @@ -290,7 +337,7 @@ parse_args(int argc, char **argv) if (i == argc - 1) return -1; - bytenuts.config.baud = string_to_speed(argv[i]); + bytenuts.config.baud = strtol(argv[i], NULL, 10); } else if (!strcmp(argv[i], "-l")) { i++; @@ -334,6 +381,20 @@ parse_args(int argc, char **argv) bytenuts.config.escape = argv[i][9]; bytenuts.config_overrides[3] = 1; } + else if (arg_len > 15 && !memcmp(argv[i], "--inter_cmd_to=", 15)) { + long cmd_to = strtol(&argv[i][15], NULL, 10); + if (cmd_to >= 0) { + bytenuts.config.inter_cmd_to = cmd_to; + bytenuts.config_overrides[4] = 1; + } + } + else if (arg_len > 11 && !memcmp(argv[i], "--time_fmt=", 11)) { + bytenuts.config.time_fmt = strdup(&argv[i][11]); + bytenuts.config_overrides[5] = 1; + } + else if (!strcmp(argv[i], "--resume") || !strcmp(argv[i], "-r")) { + bytenuts.resume = 1; + } else { return -1; } @@ -378,141 +439,103 @@ load_configs() else if (!bytenuts.config_overrides[3] && !memcmp(line, "escape=", 7)) { bytenuts.config.escape = line[7]; } + else if (!bytenuts.config_overrides[4] && !memcmp(line, "inter_cmd_to=", 13)) { + long cmd_to = strtol(&line[13], NULL, 10); + if (cmd_to >= 0) { + bytenuts.config.inter_cmd_to = cmd_to; + } + } + else if (!bytenuts.config_overrides[5] && !memcmp(line, "time_fmt=", 9)) { + size_t time_fmt_len; + bytenuts.config.time_fmt = strdup(&line[9]); + + time_fmt_len = strlen(bytenuts.config.time_fmt); + while ( + time_fmt_len > 0 && + ( + bytenuts.config.time_fmt[time_fmt_len-1] == '\r' || + bytenuts.config.time_fmt[time_fmt_len-1] == '\n' + ) + ) { + bytenuts.config.time_fmt[time_fmt_len-1] = 0; + time_fmt_len--; + } + } } return 0; } -static speed_t -string_to_speed(const char *speed) +static int +read_state() { - 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; + struct stat st; + char *buf; + char *cwd; + char *home = getenv("HOME"); + FILE *fd; + + if (!home) + return 0; + + cwd = getcwd(NULL, 0); + chdir(home); + + fd = fopen(".config/bytenuts/inbuf.log", "r"); + stat(".config/bytenuts/inbuf.log", &st); + if (fd && st.st_size > 0) { + buf = calloc(1, st.st_size + 1); + while (fgets(buf, st.st_size+1, fd)) { + bytenuts.state.history_len++; + bytenuts.state.history = realloc( + bytenuts.state.history, + sizeof(char *) * bytenuts.state.history_len + ); + bytenuts.state.history[bytenuts.state.history_len-1] = strdup(buf); + } + + free(buf); + } + if (fd) + fclose(fd); + + stat(".config/bytenuts/outbuf.log", &st); + fd = fopen(".config/bytenuts/outbuf.log", "r"); + + if (fd && st.st_size > 0) { + bytenuts.state.buf = malloc(st.st_size); + bytenuts.state.buf_len = st.st_size; + fread(bytenuts.state.buf, 1, st.st_size, fd); + } + if (fd) + fclose(fd); + + chdir(cwd); + free(cwd); + + return 0; } -static const char * -speed_to_string(speed_t speed) +static int +load_state() { - 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 ""; + if (bytenuts.state.history) { + ingest_set_history(bytenuts.state.history, bytenuts.state.history_len); + + for (int i = 0; i < bytenuts.state.history_len; i++) { + free(bytenuts.state.history[i]); + } + free(bytenuts.state.history); + bytenuts.state.history = NULL; + bytenuts.state.history_len = 0; + } + + if (bytenuts.state.buf) { + cheerios_insert(bytenuts.state.buf, bytenuts.state.buf_len); + free(bytenuts.state.buf); + bytenuts.state.buf = NULL; + bytenuts.state.buf_len = 0; + } + + return 0; } diff --git a/src/bytenuts.h b/src/bytenuts.h index 12d2487..0bb0ce3 100644 --- a/src/bytenuts.h +++ b/src/bytenuts.h @@ -1,25 +1,41 @@ #ifndef _BYTENUTES_H_ #define _BYTENUTES_H_ -#include +#ifdef __MINGW32__ +# include +#else +# include +#endif #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_state_struct { + char **history; /* commands from previous session */ + int history_len; + char *buf; /* previous buffer */ + size_t buf_len; +} bytenuts_state_t; + 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 */ + long 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 */ + uint32_t inter_cmd_to; /* inter command timeout in ms */ + /* time format to be prepended to all log lines in the output file only, + * NULL for no time prepended */ + char *time_fmt; } bytenuts_config_t; #define CONFIG_DEFAULT (bytenuts_config_t){ \ @@ -27,16 +43,20 @@ typedef struct bytenuts_config_struct { .echo = 0, \ .no_crlf = 0, \ .escape = 'b', \ - .baud = B115200, \ + .baud = 115200, \ .config_path = NULL, \ .log_path = NULL, \ .serial_path = NULL, \ + .inter_cmd_to = 10, \ + .time_fmt = NULL, \ } typedef struct bytenuts_struct { - int serial_fd; + serial_t serial_fd; bytenuts_config_t config; - int config_overrides[4]; + int config_overrides[6]; + int resume; + bytenuts_state_t state; WINDOW *status_win; WINDOW *out_win; WINDOW *in_win; diff --git a/src/cheerios.c b/src/cheerios.c index 3b04418..40f1eff 100644 --- a/src/cheerios.c +++ b/src/cheerios.c @@ -1,5 +1,8 @@ -#include -#include +#ifdef __MINGW32__ +# include +#else +# include +#endif #include #include #include @@ -17,13 +20,15 @@ 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); +static int handle_color(line_buffer_t *lines, int line_idx, int *pos, int apply); short curs_color(int fg); static int newline(line_buffer_t *lines); int cheerios_start(bytenuts_t *bytenuts) { + char *home = getenv("HOME"); + memset(&cheerios, 0, sizeof(cheerios_t)); cheerios.output = bytenuts->out_win; @@ -40,6 +45,26 @@ cheerios_start(bytenuts_t *bytenuts) } } + if (home) { + pid_t pid; + int name_len; + + /* ensure that a process has a unique log */ + pid = getpid(); + name_len = snprintf( + NULL, 0, + "%s/.config/bytenuts/outbuf.%lld.log", + home, (long long)pid + ); + cheerios.backup_filename = calloc(1, name_len + 1); + snprintf( + cheerios.backup_filename, name_len + 1, + "%s/.config/bytenuts/outbuf.%lld.log", + home, (long long)pid + ); + cheerios.backup = fopen(cheerios.backup_filename, "w"); + } + pthread_cond_init(&cheerios.cond, NULL); pthread_mutex_init(&cheerios.lock, NULL); cheerios.running = 1; @@ -129,31 +154,28 @@ cheerios_stop() { cheerios.running = 0; pthread_join(cheerios.thr, NULL); - return 0; } int -cheerios_input(const char *line) +cheerios_input(const char *buf, size_t len) { - size_t line_len = strlen(line); + size_t len_left = len; + size_t p = 0; pthread_mutex_lock(&cheerios.lock); + while (len_left > 0) { + ssize_t tmp = serial_write(cheerios.ser_fd, &buf[p], len_left); - 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); + if (tmp < 0) { + pthread_mutex_unlock(&cheerios.lock); + return -1; + } + + len_left -= tmp; } - - 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; } @@ -175,6 +197,29 @@ cheerios_info(const char *line) return 0; } +int +cheerios_print(const char *fmt, ...) +{ + va_list ap; + char *buf; + size_t len; + + va_start(ap, fmt); + len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + buf = calloc(1, len+1); + + va_start(ap, fmt); + vsprintf(buf, fmt, ap); + va_end(ap); + + cheerios_insert(buf, len); + free(buf); + + return 0; +} + int cheerios_insert(const char *buf, size_t len) { @@ -187,17 +232,18 @@ cheerios_insert(const char *buf, size_t len) static void -__xmodem_callback(size_t sent, size_t total) +__xmodem_callback(size_t sent, size_t total, int ack_fails) { char status_str[1024]; snprintf( status_str, sizeof(status_str), - "\r %.02f/%.02fKB (%.01f%%)", + "\r %.02f/%.02fKB (%.01f%%, %d ACK fails)", (sent / 1024.0), (total / 1024.0), - (sent * 100.0 / total) + (sent * 100.0 / total), + ack_fails ); cheerios_insert(status_str, strlen(status_str)); @@ -261,6 +307,7 @@ cheerios_getmaxy() { int y, x; getmaxyx(cheerios.output, y, x); + (void)x; return y; } @@ -269,6 +316,7 @@ cheerios_getmaxx() { int y, x; getmaxyx(cheerios.output, y, x); + (void)y; return x; } @@ -291,7 +339,6 @@ cheerios_thread(void *arg) { char buf[1024]; ssize_t read_ret = 0; - struct pollfd fds; while (cheerios.running) { pthread_mutex_lock(&cheerios.lock); @@ -300,17 +347,7 @@ cheerios_thread(void *arg) 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)); + read_ret = serial_read(cheerios.ser_fd, buf, sizeof(buf)); if (read_ret > 0) { insert_buf(&cheerios.lines, buf, read_ret); } @@ -321,6 +358,33 @@ cheerios_thread(void *arg) if (cheerios.log) fclose(cheerios.log); + + if (cheerios.backup) { + char *out_filename; + int out_filename_len; + char *home = getenv("HOME"); + + /* do not chdir as other threads can still be running */ + out_filename_len = snprintf( + NULL, 0, + "%s/.config/bytenuts/outbuf.log", + home + ); + out_filename = calloc(1, out_filename_len + 1); + snprintf( + out_filename, out_filename_len + 1, + "%s/.config/bytenuts/outbuf.log", + home + ); + + /* move this processes log to the path that can be loaded on resumption */ + fclose(cheerios.backup); + rename(cheerios.backup_filename, out_filename); + + free(out_filename); + free(cheerios.backup_filename); + } + pthread_exit(NULL); return NULL; } @@ -332,8 +396,12 @@ insert_buf(line_buffer_t *lines, const char *buf, size_t len) 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); + if (cheerios.mode == CHEERIOS_MODE_NORMAL) { + if (cheerios.log) + fwrite(&buf[i], 1, 1, cheerios.log); + if (cheerios.backup) + fwrite(&buf[i], 1, 1, cheerios.backup); + } /* line feed starts a new row */ if (buf[i] == '\n') { @@ -367,32 +435,96 @@ write_lines(line_buffer_t *lines) int row = lines->bot; int rows_printed = 0; int window_height = getmaxy(cheerios.output); + int window_width = getmaxx(cheerios.output); + line_buffer_t lines_wrapped = { 0 }; if (row < 0) row = lines->n_lines - 1; + while (row >= 0 && rows_printed < window_height) { + /* we can print the whole line */ + if (lines->line_lens[row] <= window_width) { + lines_wrapped.n_lines++; + lines_wrapped.lines = realloc( + lines_wrapped.lines, + sizeof(uint8_t *) * lines_wrapped.n_lines + ); + lines_wrapped.line_lens = realloc( + lines_wrapped.line_lens, + sizeof(int) * lines_wrapped.n_lines + ); + + lines_wrapped.lines[lines_wrapped.n_lines - 1] = lines->lines[row]; + lines_wrapped.line_lens[lines_wrapped.n_lines - 1] = lines->line_lens[row]; + + rows_printed++; + } + /* we have to split the line */ + else { + int n_split = lines->line_lens[row] / window_width; + + if ((lines->line_lens[row] % window_width) == 0) + n_split--; + + lines_wrapped.n_lines += n_split + 1; + lines_wrapped.lines = realloc( + lines_wrapped.lines, + sizeof(uint8_t *) * lines_wrapped.n_lines + ); + lines_wrapped.line_lens = realloc( + lines_wrapped.line_lens, + sizeof(int) * lines_wrapped.n_lines + ); + + /* fill out lines that fill the width */ + for (int i = 0; i < n_split; i++) { + lines_wrapped.lines[lines_wrapped.n_lines - 1 - i] = + &lines->lines[row][i * window_width]; + lines_wrapped.line_lens[lines_wrapped.n_lines - 1 - i] = + window_width; + } + /* left over */ + lines_wrapped.lines[lines_wrapped.n_lines - 1 - n_split] = + &lines->lines[row][n_split * window_width]; + + if ((lines->line_lens[row] % window_width) == 0) { + lines_wrapped.line_lens[lines_wrapped.n_lines - 1 - n_split] = window_width; + } else { + lines_wrapped.line_lens[lines_wrapped.n_lines - 1 - n_split] = + lines->line_lens[row] % window_width; + } + + rows_printed += n_split + 1; + } + + row--; + } + pthread_mutex_lock(cheerios.term_lock); curs_set(0); werase(cheerios.output); - while (row >= 0 && rows_printed < window_height) { + /* the copying process reverses the order of the rows... */ + row = 0; + rows_printed = 0; + while (row < lines_wrapped.n_lines && 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); + for (int i = 0; i < lines_wrapped.line_lens[row]; i++) { + handle_color(&lines_wrapped, row, &i, 1); if (i >= getmaxx(cheerios.output)) { break; } - if (i < lines->line_lens[row]) { - waddch(cheerios.output, lines->lines[row][i]); + if (i < lines_wrapped.line_lens[row]) { + waddch(cheerios.output, lines_wrapped.lines[row][i]); } } rows_printed++; - row--; + row++; } curs_set(1); @@ -406,11 +538,16 @@ write_lines(line_buffer_t *lines) bytenuts_set_status(STATUS_CHEERIOS, "locked"); } + if (lines_wrapped.lines) + free(lines_wrapped.lines); + if (lines_wrapped.line_lens) + free(lines_wrapped.line_lens); + return 0; } static int -handle_color(line_buffer_t *lines, int line_idx, int *pos) +handle_color(line_buffer_t *lines, int line_idx, int *pos, int apply) { short fg, bg = -1; int enable = -1; @@ -422,8 +559,17 @@ handle_color(line_buffer_t *lines, int line_idx, int *pos) return 0; while (p < line_len) { + int fit_col = (p + 7) <= line_len; + int fit_en = (p + 4) <= line_len; + /* fg or bg color */ - if (!memcmp(&line[p], "\e[38;5;", 7) || !memcmp(&line[p], "\e[48;5;", 7)) { + if ( + fit_col && + ( + !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; @@ -451,13 +597,13 @@ handle_color(line_buffer_t *lines, int line_idx, int *pos) p++; } /* enable our color */ - else if (!memcmp(&line[p], "\e[1m", 4)) { + else if (fit_en && !memcmp(&line[p], "\e[1m", 4)) { p += 4; enable = 1; break; } /* disable our color */ - else if (!memcmp(&line[p], "\e[0m", 4)) { + else if (fit_en && !memcmp(&line[p], "\e[0m", 4)) { p += 4; enable = 0; break; @@ -468,6 +614,11 @@ handle_color(line_buffer_t *lines, int line_idx, int *pos) } } + if (!apply) { + *pos = p; + return 0; + } + if (enable == 0) { for (int i = 0; i < NCOLOR_PAIRS; i++) { if (lines->enabled_pairs[i]) { @@ -502,9 +653,6 @@ handle_color(line_buffer_t *lines, int line_idx, int *pos) lines->enabled_pairs[pair_pos] = 1; wattron(cheerios.output, COLOR_PAIR(pair_pos + 1)); } - else { - return 0; - } *pos = p; return 0; @@ -519,5 +667,22 @@ newline(line_buffer_t *lines) lines->line_lens = realloc(lines->line_lens, sizeof(int) * lines->n_lines); lines->line_lens[lines->n_lines - 1] = 0; lines->pos = 0; + + if (cheerios.config->time_fmt && (cheerios.log || cheerios.backup)) { + char tstr[128]; + time_t now; + struct tm *tinfo; + size_t tstr_len; + + time(&now); + tinfo = localtime(&now); + tstr_len = strftime(tstr, 128, cheerios.config->time_fmt, tinfo); + + if (cheerios.log) + fwrite(tstr, 1, tstr_len, cheerios.log); + if (cheerios.backup) + fwrite(tstr, 1, tstr_len, cheerios.backup); + } + return 0; } diff --git a/src/cheerios.h b/src/cheerios.h index d9fc2c9..7e48360 100644 --- a/src/cheerios.h +++ b/src/cheerios.h @@ -1,8 +1,13 @@ #ifndef _CHEERIOS_H_ #define _CHEERIOS_H_ -#include +#ifdef __MINGW32__ +# include +#else +# include +#endif #include +#include #include #include "bytenuts.h" @@ -32,9 +37,11 @@ typedef struct cheerios_struct { pthread_t thr; WINDOW *output; pthread_mutex_t *term_lock; - int ser_fd; + serial_t ser_fd; line_buffer_t lines; - FILE *log; + FILE *log; /* log file which was opened with -l */ + char *backup_filename; /* realpath to the backup outbuf.pid.log */ + FILE *backup; /* backup log file object */ bytenuts_config_t *config; volatile int mode; pthread_cond_t cond; @@ -60,14 +67,17 @@ 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); +/* send over a line of user input to the output */ +int cheerios_input(const char *buf, size_t len); /* output a line only to the terminal for info/prompt purposes * do not terminate with a newline! */ int cheerios_info(const char *line); +/* Like cheerios_insert except using printf formatting */ +__attribute__((format(printf, 1, 2))) +int cheerios_print(const char *fmt, ...); + /* directly insert a buffer to the output window */ int cheerios_insert(const char *buf, size_t len); diff --git a/src/ingest.c b/src/ingest.c index a98f3b8..e417ee2 100644 --- a/src/ingest.c +++ b/src/ingest.c @@ -1,5 +1,9 @@ #include -#include +#ifdef __MINGW32__ +# include +#else +# include +#endif #include #include #include @@ -8,10 +12,12 @@ #include #include #include +#include #include "bytenuts.h" #include "cheerios.h" #include "ingest.h" +#include "timer_math.h" static void *ingest_thread(void *arg); @@ -19,12 +25,20 @@ static ingest_t ingest; static int mode_normal(int ch); static int mode_xmodem(int block_sz); +static int mode_hex(int ch); 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(); +static int print_stats(void); +static int auto_complete(void); +static int read_cmd_page(const char *home_dir, int idx); +static void update_cmd_pg_status(void); +static void inter_command_wait(void); + +#ifndef KEY_CUP +# define KEY_CUP (566) +#endif +#ifndef KEY_CDOWN +# define KEY_CDOWN (525) +#endif int ingest_start(bytenuts_t *bytenuts) @@ -37,20 +51,39 @@ ingest_start(bytenuts_t *bytenuts) ingest.term_lock = &bytenuts->term_lock; ingest.config = &bytenuts->config; ingest.cmd_pg_cur = -1; + ingest.xmodem_hist = bstr_history_create(); + + /* ensure character sequences work in all terminals */ + define_key("\033[1;5A", KEY_CUP); + define_key("\033[1;5B", KEY_CDOWN); + define_key("\033[1;2H", KEY_SHOME); + define_key("\033[1;2F", KEY_SEND); HOME = getenv("HOME"); if (HOME) { - char *cwd = getcwd(NULL, 0); int idx = 0; + int name_len; + pid_t pid; - chdir(HOME); - - while (!read_cmd_page(idx)) { + while (!read_cmd_page(HOME, idx)) { idx++; } - chdir(cwd); - free(cwd); + /* ensure that a process has a unique log */ + pid = getpid(); + name_len = snprintf( + NULL, 0, + "%s/.config/bytenuts/inbuf.%lld.log", + HOME, (long long)pid + ); + ingest.history_filename = calloc(1, name_len + 1); + snprintf( + ingest.history_filename, name_len + 1, + "%s/.config/bytenuts/inbuf.%lld.log", + HOME, (long long)pid + ); + + ingest.history_fd = fopen(ingest.history_filename, "w"); } if (ingest.cmd_pgs_n > 0) { @@ -58,6 +91,10 @@ ingest_start(bytenuts_t *bytenuts) } update_cmd_pg_status(); + clock_gettime(CLOCK_REALTIME, &ingest.cmd_ts); + /* ensure first command is not delayed */ + timer_sub_ms(&ingest.cmd_ts, ingest.config->inter_cmd_to); + ingest.running = 1; if (pthread_create(&ingest.thr, NULL, ingest_thread, NULL)) { return -1; @@ -74,26 +111,126 @@ ingest_stop() return 0; } +int +ingest_refresh() +{ + static int start = 0; + int startx = 0, starty = 0; + int cx = -1, cy = -1; + int win_len = getmaxx(ingest.input) + 1; + + pthread_mutex_lock(ingest.term_lock); + + werase(ingest.input); + wmove(ingest.input, 0, 0); + if (ingest.prepend) { + wprintw(ingest.input, "%s", ingest.prepend); + getyx(ingest.input, starty, startx); + } + + if (start > ingest.inpos) { + start = ingest.inpos; + } + /* this is not perfect as some characters are wider than one space, but it + * makes the algorithm for printing less redundant */ + else if (ingest.inpos > (start + win_len)) { + start = ingest.inpos - win_len; + } + + int i = 0; + int hit_cursor = 0; + while (1) { + int p = i + start; + int iy, ix; + i++; + + getyx(ingest.input, iy, ix); + + /* print the cursor at this position */ + if (p == ingest.inpos) { + cy = iy; + cx = ix; + hit_cursor = 1; + } + + /* nothing left to write... */ + if (p >= ingest.inlen) + break; + + /* we want to leave a blank character for printing the cursor at the end of the line */ + if (ix >= (win_len-2)) { + /* we printed the cursor, so we can break */ + if (hit_cursor) + break; + /* otherwise, we didn't print the cursor, so start over */ + i = 0; + start++; + wmove(ingest.input, starty, startx); + continue; + } + + waddch(ingest.input, ingest.inbuf[p]); + } + + if (cx < 0) { + cx = ingest.inlen < win_len ? ingest.inlen : win_len - 1; + cy = 0; + } + + wmove(ingest.input, cy, cx); + wrefresh(ingest.input); + + pthread_mutex_unlock(ingest.term_lock); + + return 0; +} + +int +ingest_set_history(char **history, int history_len) +{ + if (ingest.history) { + for (int i = 0; ingest.history[i] != NULL; i++) { + free(ingest.history[i]); + } + free(ingest.history); + } + + ingest.history = calloc(1, sizeof(char *) * (history_len+1)); + for (int i = 0; i < history_len; i++) { + size_t item_len = strlen(history[i]); + ingest.history[i] = strdup(history[i]); + + while ( + ingest.history[i][item_len-1] == '\n' || + ingest.history[i][item_len-1] == '\r' + ) { + item_len--; + ingest.history[i][item_len] = '\0'; + } + + if (ingest.history_fd) { + fwrite(ingest.history[i], 1, item_len, ingest.history_fd); + fwrite("\n", 1, 1, ingest.history_fd); + } + } + + ingest.history_len = history_len; + ingest.history_pos = ingest.history_len; + + 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); @@ -156,7 +293,7 @@ ingest_thread(void *arg) memcpy(ingest.inbuf, toload, toload_len); ingest.inpos = toload_len; ingest.inlen = toload_len; - inbuf_load(); + ingest_refresh(); break; } case 'c': @@ -250,18 +387,29 @@ ingest_thread(void *arg) ingest.mode = INGEST_MODE_XMODEM1K; bytenuts_set_status(STATUS_INGEST, "xmodem1k"); break; + case 'H': + if (ingest.mode == INGEST_MODE_NORMAL) { + ingest.mode = INGEST_MODE_HEX; + bytenuts_set_status(STATUS_INGEST, "hex"); + } else { + ingest.mode = INGEST_MODE_NORMAL; + bytenuts_set_status(STATUS_INGEST, "normal"); + } + should_continue = 1; + break; case 'h': - cheerios_insert( - "Commands (lead with ctrl-b):\r\n" + cheerios_print( + "Commands (lead with ctrl-%c):\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: enter/exit hex buffer mode\r\n" " h: view this help\r\n" " q: quit Bytenuts\r\n", - 313 + ingest.config->escape ); bytenuts_set_status(STATUS_INGEST, "normal"); should_continue = 1; @@ -289,6 +437,9 @@ ingest_thread(void *arg) case INGEST_MODE_XMODEM1K: mode_xmodem(1024); break; + case INGEST_MODE_HEX: + mode_hex(ch); + break; default: break; } @@ -296,6 +447,34 @@ ingest_thread(void *arg) sched_yield(); } + if (ingest.history_fd) { + char *in_filename; + int in_filename_len; + char *home = getenv("HOME"); + + /* do not chdir as other threads can still be running */ + in_filename_len = snprintf( + NULL, 0, + "%s/.config/bytenuts/inbuf.log", + home + ); + in_filename = calloc(1, in_filename_len + 1); + snprintf( + in_filename, in_filename_len + 1, + "%s/.config/bytenuts/inbuf.log", + home + ); + + /* move this processes history to the path that can be loaded on resumption */ + fclose(ingest.history_fd); + rename(ingest.history_filename, in_filename); + + free(in_filename); + free(ingest.history_filename); + } + + bstr_history_destroy(ingest.xmodem_hist); + pthread_exit(NULL); return NULL; } @@ -307,9 +486,22 @@ mode_normal(int ch) return 0; switch (ch) { - case '\n': /* send inputs */ - if (cheerios_input(ingest.inbuf)) { - break; + case '\n': { /* send inputs */ + size_t inlen = strlen(ingest.inbuf); + const char *ending; + + if (ingest.config->no_crlf) + ending = "\n"; + else + ending = "\r\n"; + + inter_command_wait(); + cheerios_input(ingest.inbuf, inlen); + cheerios_input(ending, strlen(ending)); + if (ingest.config->echo) { + cheerios_insert(">> ", 3); + cheerios_insert(ingest.inbuf, inlen); + cheerios_insert("\r\n", 2); } /* store in history */ @@ -319,14 +511,19 @@ mode_normal(int ch) ingest.history = realloc(ingest.history, ingest.history_len * sizeof(char *)); ingest.history[ingest.history_pos] = strdup(ingest.inbuf); ingest.history_pos++; + if (ingest.history_fd) { + fwrite(ingest.inbuf, 1, strlen(ingest.inbuf), ingest.history_fd); + fwrite("\n", 1, 1, ingest.history_fd); + } } memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); ingest.inpos = 0; ingest.inlen = 0; - inbuf_load(); + ingest_refresh(); break; + } case KEY_UP: /* load in history */ if (ingest.history_pos == 0) { /* top of history */ break; @@ -335,10 +532,11 @@ mode_normal(int ch) } ingest.history_pos--; + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); strcpy(ingest.inbuf, ingest.history[ingest.history_pos]); ingest.inlen = strlen(ingest.inbuf); ingest.inpos = ingest.inlen; - inbuf_load(); + ingest_refresh(); break; case KEY_DOWN: /* load in history */ @@ -346,17 +544,19 @@ mode_normal(int ch) break; } else if (ingest.history_pos == ingest.history_len - 1) { /* load back temp storage */ ingest.history_pos++; + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); strcpy(ingest.inbuf, ingest.tmp_history); ingest.inlen = strlen(ingest.inbuf); ingest.inpos = ingest.inlen; } else { ingest.history_pos++; + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); strcpy(ingest.inbuf, ingest.history[ingest.history_pos]); ingest.inlen = strlen(ingest.inbuf); ingest.inpos = ingest.inlen; } - inbuf_load(); + ingest_refresh(); break; default: @@ -379,7 +579,7 @@ mode_normal(int ch) ingest.inlen--; } - inbuf_load(); + ingest_refresh(); break; } @@ -400,7 +600,7 @@ mode_xmodem(int block_sz) if (ingest.prepend) free(ingest.prepend); ingest.prepend = strdup("Give me a path (ctrl-c to stop): "); - inbuf_load(); + ingest_refresh(); while (ingest.running) { int ch; @@ -416,6 +616,8 @@ mode_xmodem(int block_sz) switch (ch) { case '\n': + bstr_history_new_entry(ingest.xmodem_hist, ingest.inbuf); + pthread_mutex_lock(ingest.term_lock); werase(ingest.input); wmove(ingest.input, 0, 0); @@ -432,8 +634,36 @@ mode_xmodem(int block_sz) break; case '\t': /* tab complete */ auto_complete(); - inbuf_load(); + ingest_refresh(); break; + case KEY_BACKSPACE: + case KEY_UP: + /* fall-through */ + case KEY_DOWN: + { + const char *filename; + + if (ch == KEY_UP) { + bstr_history_older(ingest.xmodem_hist); + } else { + bstr_history_newer(ingest.xmodem_hist); + } + + filename = bstr_history_atpos(ingest.xmodem_hist); + if (!filename) { + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); + ingest.inpos = 0; + ingest.inlen = 0; + ingest_refresh(); + break; + } + + strncpy(ingest.inbuf, filename, sizeof(ingest.inbuf)); + ingest.inpos = strlen(ingest.inbuf); + ingest.inlen = ingest.inpos; + ingest_refresh(); + break; + } default: { char tmp[1024]; @@ -454,7 +684,7 @@ mode_xmodem(int block_sz) ingest.inlen--; } - inbuf_load(); + ingest_refresh(); break; } @@ -469,8 +699,10 @@ mode_xmodem(int block_sz) free(ingest.prepend); ingest.prepend = NULL; + bstr_history_unset_pos(ingest.xmodem_hist); + bytenuts_set_status(STATUS_INGEST, "normal"); - inbuf_load(); + ingest_refresh(); return 0; } } @@ -478,6 +710,90 @@ mode_xmodem(int block_sz) return 0; } +static int +mode_hex(int ch) +{ + if (handle_functions(ch)) + return 0; + + switch (ch) { + case '\n': { /* send inputs */ + char hexbuf[sizeof(ingest.inbuf)/2]; + size_t inbuflen = strlen(ingest.inbuf); + size_t hexlen = 0; + /* assume leading 0 on odd length buffers */ + if (inbuflen % 2 != 0) { + memmove(&ingest.inbuf[1], ingest.inbuf, inbuflen); + ingest.inbuf[0] = '0'; + inbuflen++; + } + + for (int i = 0; i < inbuflen; i += 2) { + char tmp = ingest.inbuf[i+2]; + char *inval; + long ch; + + ingest.inbuf[i+2] = '\0'; + ch = strtol(&ingest.inbuf[i], &inval, 16); + ingest.inbuf[i+2] = tmp; + + /* strtol could not parse a character */ + if (inval != &ingest.inbuf[i+2]) + break; + + hexbuf[i/2] = ch; + hexlen++; + } + + inter_command_wait(); + cheerios_input(hexbuf, hexlen); + if (ingest.config->echo) { + cheerios_insert(">> (hex)\r\n", 10); + for (int i = 0; i < hexlen; i++) { + if ( + i > 0 && + i != hexlen-1 && + i % 16 == 0 + ) { + cheerios_insert("\r\n", 2); + } + cheerios_insert(&ingest.inbuf[i*2], 2); + cheerios_insert(" ", 1); + } + cheerios_insert("\r\n", 2); + } + + /* 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++; + if (ingest.history_fd) { + fwrite(ingest.inbuf, 1, strlen(ingest.inbuf), ingest.history_fd); + fwrite("\n", 1, 1, ingest.history_fd); + } + } + + memset(ingest.inbuf, 0, sizeof(ingest.inbuf)); + ingest.inpos = 0; + ingest.inlen = 0; + ingest_refresh(); + + break; + } + + case KEY_UP: /* load in history */ + case KEY_DOWN: /* load in history */ + default: + mode_normal(ch); + break; + } + + return 0; +} + static int handle_functions(int ch) { @@ -498,10 +814,10 @@ handle_functions(int ch) case KEY_SEND: /* go to front of log */ cheerios_gofwd(-1); return 1; - case 566: /* ctrl + up arrow */ + case KEY_CUP: /* ctrl + up arrow */ cheerios_goback(1); return 1; - case 525: /* ctrl + down arrow */ + case KEY_CDOWN: /* ctrl + down arrow */ cheerios_gofwd(1); return 1; case KEY_RESIZE: @@ -515,7 +831,7 @@ handle_functions(int ch) ingest.inpos--; ingest.inlen--; ingest.inbuf[ingest.inlen] = '\0'; - inbuf_load(); + ingest_refresh(); return 1; } @@ -527,7 +843,7 @@ handle_functions(int ch) ingest.inlen--; memcpy(&ingest.inbuf[ingest.inpos], tmp, tmp_len); ingest.inbuf[ingest.inlen] = '\0'; - inbuf_load(); + ingest_refresh(); return 1; } @@ -541,7 +857,7 @@ handle_functions(int ch) } else if (ingest.inpos == ingest.inlen - 1) { ingest.inbuf[ingest.inpos] = '\0'; ingest.inlen--; - inbuf_load(); + ingest_refresh(); return 1; } @@ -550,7 +866,7 @@ handle_functions(int ch) ingest.inlen--; memcpy(&ingest.inbuf[ingest.inpos], tmp, tmp_len); ingest.inbuf[ingest.inlen] = '\0'; - inbuf_load(); + ingest_refresh(); return 1; } @@ -558,21 +874,21 @@ handle_functions(int ch) if (ingest.inpos == 0) return 1; ingest.inpos--; - inbuf_load(); + ingest_refresh(); return 1; case KEY_RIGHT: if (ingest.inpos == ingest.inlen) return 1; ingest.inpos++; - inbuf_load(); + ingest_refresh(); return 1; case KEY_END: ingest.inpos = ingest.inlen; - inbuf_load(); + ingest_refresh(); return 1; case KEY_HOME: ingest.inpos = 0; - inbuf_load(); + ingest_refresh(); return 1; #if 0 case KEY_MOUSE: @@ -595,37 +911,6 @@ handle_functions(int ch) } } -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() { @@ -757,13 +1042,17 @@ auto_complete_cleanup: } static int -read_cmd_page(int idx) +read_cmd_page(const char *home_dir, int idx) { char fname[128]; FILE *fd; char line[1024]; - snprintf(fname, sizeof(fname), ".config/bytenuts/commands%d", idx+1); + snprintf( + fname, sizeof(fname), + "%s/.config/bytenuts/commands%d", + home_dir, idx+1 + ); fd = fopen(fname, "r"); if (!fd) @@ -797,8 +1086,24 @@ static void update_cmd_pg_status() { if (ingest.cmd_pgs_n > 0) { - bytenuts_set_status(STATUS_CMDPAGE, "pg.%d", ingest.cmd_pg_cur+1); + bytenuts_set_status(STATUS_CMDPAGE, "cmd pg.%d", ingest.cmd_pg_cur+1); } else { bytenuts_set_status(STATUS_CMDPAGE, "n/a"); } } + +static void +inter_command_wait() +{ + struct timespec target; + struct timespec now; + + clock_gettime(CLOCK_REALTIME, &now); + memcpy(&target, &ingest.cmd_ts, sizeof(struct timespec)); + + timer_add_ms(&target, ingest.config->inter_cmd_to); + timer_sub(&target, &now); + + nanosleep(&target, NULL); + clock_gettime(CLOCK_REALTIME, &ingest.cmd_ts); +} diff --git a/src/ingest.h b/src/ingest.h index b9fcda2..bb64216 100644 --- a/src/ingest.h +++ b/src/ingest.h @@ -1,15 +1,23 @@ #ifndef _INGEST_H_ #define _INGEST_H_ -#include +#ifdef __MINGW32__ +# include +#else +# include +#endif #include +#include +#include +#include "bstr.h" #include "bytenuts.h" enum ingest_mode_enum { INGEST_MODE_NORMAL = 0, INGEST_MODE_XMODEM, INGEST_MODE_XMODEM1K, + INGEST_MODE_HEX, }; typedef struct ingest_struct { @@ -18,11 +26,13 @@ typedef struct ingest_struct { pthread_t thr; volatile int running; char *prepend; /* string to be prepended in the prompt */ - char inbuf[1024]; + char inbuf[4096]; 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 */ + char *history_filename; /* realpath of inbuf.pid.log */ + FILE *history_fd; + char tmp_history[4096]; /* copy of inbuf before we load in history */ int history_len; int history_pos; /* where we currently reside in the history */ int mode; @@ -36,6 +46,8 @@ typedef struct ingest_struct { int cmd_pgs_n; /* what command page is currently selected */ int cmd_pg_cur; + struct timespec cmd_ts; /* last command sent timestamp */ + bstr_history_handle xmodem_hist; /* xmodem filename transfer history */ } ingest_t; /* startup the input window thread */ @@ -44,4 +56,10 @@ int ingest_start(bytenuts_t *bytenuts); /* stop the input window thread */ int ingest_stop(); +/* refresh the inbuf window */ +int ingest_refresh(); + +/* Set the the command history directly. Values are copied */ +int ingest_set_history(char **history, int history_len); + #endif /* _INGEST_H_ */ diff --git a/src/main.c b/src/main.c index b75957c..6e1e64b 100644 --- a/src/main.c +++ b/src/main.c @@ -4,16 +4,20 @@ #include "bytenuts.h" +#ifndef __MINGW32__ static void sigint_handler(int s); +#endif int main(int argc, char **argv) { +#ifndef __MINGW32__ struct sigaction act; act.sa_handler = sigint_handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, NULL); +#endif if (bytenuts_run(argc, argv)) { bytenuts_stop(); @@ -23,9 +27,11 @@ main(int argc, char **argv) exit(0); } +#ifndef __MINGW32__ static void sigint_handler(int s) { bytenuts_kill(); exit(1); } +#endif diff --git a/src/paths.c b/src/paths.c new file mode 100644 index 0000000..7c4de57 --- /dev/null +++ b/src/paths.c @@ -0,0 +1,99 @@ +#include +#include +#include +#include + +#include "bstr.h" +#include "paths.h" + +#if __MINGW32__ +# define BSTR_LLD "%I64d" +#else +# define BSTR_LLD "%lld" +#endif + +char * +paths_append(char *base, const char *suffix) +{ + size_t baselen; + const char *sep = ""; + + if (!base) + return strdup(suffix); + + baselen = strlen(base); + + if ( + (baselen == 0) || + ((base[baselen-1] != '/') && (base[baselen-1] == '\\')) + ) { + /* no separator at end of path */ +#if __MINGW32__ + sep = "\\"; +#else + sep = "/"; +#endif + } + + return bstr_print(base, "%s%s", sep, suffix); +} + +char * +paths_bnconf_dir() +{ +#ifdef __MINGW32__ + char *home = getenv("HOMEPATH"); +#else + char *home = getenv("HOME"); +#endif + char *ret = NULL; + + if (!home) + return NULL; + +#ifdef __MINGW32__ + ret = strdup("C:\\"); +#endif + ret = paths_append(ret, home); + ret = paths_append(ret, ".config/bytenuts"); + + /* TODO free environment variable on Windows? */ + return ret; +} + +char * +paths_bnconf_default() +{ + char *ret = paths_bnconf_dir(); + + if (!ret) + return NULL; + + return paths_append(ret, "config"); +} + +char * +paths_command_file(int idx) +{ + char *ret = paths_bnconf_dir(); + + if (!ret) + return NULL; + + return bstr_print(ret, "/commands%d", idx); +} + +char * +paths_logfile(const char *prefix, long long pid) +{ + char *ret = paths_bnconf_dir(); + + if (!ret) + return NULL; + + if (pid > 0) { + return bstr_print(ret, "/%s." BSTR_LLD ".log", prefix, pid); + } else { + return bstr_print(ret, "/%s.log", prefix); + } +} diff --git a/src/paths.h b/src/paths.h new file mode 100644 index 0000000..179fafe --- /dev/null +++ b/src/paths.h @@ -0,0 +1,20 @@ +#ifndef _PATHS_H_ +#define _PATHS_H_ + +/* Realloc the base path and add the suffix to it, "/"s handled nicely */ +char *paths_append(char *base, const char *suffix); + +/* Get the ~/.config/bytenuts directory */ +char *paths_bnconf_dir(); + +/* Get the default bytenuts config filepath */ +char *paths_bnconf_default(); + +/* Generate the path to the command file */ +char *paths_command_file(int idx); + +/* Generate a logfile path of the form ~/.config/bytenuts/[.].log + * PID only inserted if >0 */ +char *paths_logfile(const char *prefix, long long pid); + +#endif /* _PATHS_H_ */ diff --git a/src/serial.c b/src/serial.c index 364fcbe..1368584 100644 --- a/src/serial.c +++ b/src/serial.c @@ -2,28 +2,158 @@ #include #include #include -#include #include +#include +#include #include "serial.h" -int -serial_open(const char *path, speed_t bps) +#ifdef __MINGW32__ +# include + +serial_t +serial_open(const char *comport, long bps) { + /* referred to: https://github.com/waynix/SPinGW/tree/master */ + HANDLE serial = CreateFile( + comport, + GENERIC_READ | GENERIC_WRITE, + 0, + 0, + OPEN_EXISTING, + 0, + 0 + ); + DCB serial_params = { 0 }; + COMMTIMEOUTS timeouts = { 0 }; + + if (serial == INVALID_HANDLE_VALUE) { + return SERIAL_INVALID; + } + + serial_params.DCBlength = sizeof(serial_params); + if (!GetCommState(serial, &serial_params)) { + return SERIAL_INVALID; + } + + /* 8n1 */ + serial_params.BaudRate = bps; + serial_params.ByteSize = 8; + serial_params.StopBits = 1; + serial_params.Parity = NOPARITY; + + if (!SetCommState(serial, &serial_params)) { + return SERIAL_INVALID; + } + + timeouts.ReadIntervalTimeout = 0; + timeouts.ReadTotalTimeoutConstant = 0; + timeouts.ReadTotalTimeoutMultiplier = 0; + timeouts.WriteTotalTimeoutConstant = 0; + timeouts.WriteTotalTimeoutMultiplier = 0; + + if (!SetCommTimeouts(serial, &timeouts)) { + return SERIAL_INVALID; + } + + return serial; +} + +ssize_t +serial_read(serial_t serial, void *buf, size_t len) +{ + DWORD ret = 0; + + if (!ReadFile(serial, buf, len, &ret, NULL)) { + return -1; + } + + return ret; +} + +ssize_t +serial_read_to(serial_t serial, void *buf, size_t len, unsigned int to_ms) +{ + DWORD ret = -1; + size_t p = 0; + size_t len_left = len; + + while (to_ms > 0) { + DWORD tmp = 0; + + if (!ReadFile(serial, &((uint8_t *)buf)[p], len_left, &tmp, NULL)) { + return ret; + } + + if (tmp > 0) { + if (ret < 0) { + ret = tmp; + } else { + ret += tmp; + } + + len_left -= tmp; + p += tmp; + } + + if (len_left == 0) { + return ret; + } + + /* sleep 1ms */ + nanosleep(&(struct timespec){ 0, 1000000 }, NULL); + to_ms--; + } + + if (ret < 0) { + ret = 0; + } + + return ret; +} + +ssize_t +serial_write(serial_t serial, const void *buf, size_t len) +{ + DWORD ret = 0; + + if (!WriteFile(serial, buf, len, &ret, NULL)) { + return -1; + } + + return ret; +} + +int +serial_close(serial_t serial) +{ + return (CloseHandle(serial) != 0); +} + +#else +# include +# include + +static speed_t long_to_speed(long bps); + +serial_t +serial_open(const char *path, long bps) +{ + speed_t speed = long_to_speed(bps); int fd; struct termios otio; struct termios ntio; fd = open(path, O_RDWR | O_NOCTTY | O_NDELAY); if (fd == -1) { - return -1; + return SERIAL_INVALID; } tcgetattr(fd, &otio); tcgetattr(fd, &ntio); - cfsetispeed(&ntio, bps); - cfsetospeed(&ntio, bps); + cfsetispeed(&ntio, speed); + cfsetospeed(&ntio, speed); /* https://www.cmrr.umn.edu/~strupp/serial.html */ /* 8N1 */ @@ -57,3 +187,107 @@ serial_open(const char *path, speed_t bps) return fd; } + +ssize_t +serial_read(serial_t serial, void *buf, size_t len) +{ + return serial_read_to(serial, buf, len, 0); +} + +ssize_t +serial_read_to(serial_t serial, void *buf, size_t len, unsigned int to_ms) +{ + struct pollfd fds; + fds.fd = serial; + fds.events = POLLIN; + fds.revents = 0; + + if (poll(&fds, 1, to_ms) == 0) { + return 0; + } + + return read(serial, buf, len); +} + +ssize_t +serial_write(serial_t serial, const void *buf, size_t len) +{ + return write(serial, buf, len); +} + +int +serial_close(serial_t serial) +{ + return close(serial); +} + +static speed_t +long_to_speed(long bps) +{ + switch (bps) { + case 50: + return B50; + case 75: + return B75; + case 110: + return B110; + case 134: + return B134; + case 150: + return B150; + case 200: + return B200; + case 300: + return B300; + case 600: + return B600; + case 1200: + return B1200; + case 1800: + return B1800; + case 2400: + return B2400; + case 4800: + return B4800; + case 9600: + return B9600; + case 19200: + return B19200; + case 38400: + return B38400; + case 57600: + return B57600; + case 115200: + return B115200; + case 230400: + return B230400; + case 460800: + return B460800; + case 500000: + return B500000; + case 576000: + return B576000; + case 921600: + return B921600; + case 1000000: + return B1000000; + case 1152000: + return B1152000; + case 1500000: + return B1500000; + case 2000000: + return B2000000; + case 2500000: + return B2500000; + case 3000000: + return B3000000; + case 3500000: + return B3500000; + case 4000000: + return B4000000; + default: + return B0; + } +} + +#endif /* __MINGW32__ */ diff --git a/src/serial.h b/src/serial.h index 9478298..eff7835 100644 --- a/src/serial.h +++ b/src/serial.h @@ -1,8 +1,31 @@ #ifndef _SERIAL_H_ #define _SERIAL_H_ -#include +#ifdef __MINGW32__ +# include +typedef HANDLE serial_t; +#else +typedef int serial_t; +#endif -int serial_open(const char *path, speed_t bps); +#define SERIAL_INVALID (serial_t)(-1) + +/* Open a serial port with the given speed and return a handle that can be used + * in the other serial APIs*/ +serial_t serial_open(const char *path, long bps); + +/* Read up to len bytes (non-blocking), length read is returned */ +ssize_t serial_read(serial_t serial, void *buf, size_t len); + +/* Read up to len bytes, or until to_ms milliseconds have elapsed, length read + * is returned */ +ssize_t serial_read_to(serial_t serial, void *buf, size_t len, unsigned int to_ms); + +/* Write len bytes onto the serial port, actual number of bytes written is + * returned */ +ssize_t serial_write(serial_t serial, const void *buf, size_t len); + +/* Close the serial file */ +int serial_close(serial_t serial); #endif /* _SERIAL_H_ */ diff --git a/src/timer_math.c b/src/timer_math.c new file mode 100644 index 0000000..bce2efd --- /dev/null +++ b/src/timer_math.c @@ -0,0 +1,71 @@ +#include "timer_math.h" + +int +timer_cmp(const struct timespec *a, const struct timespec *b) +{ + if (a->tv_sec < b->tv_sec) { + return -1; + } + else if (a->tv_sec > b->tv_sec) { + return 1; + } + else if (a->tv_nsec < b->tv_nsec) { + return -1; + } + else if (a->tv_nsec > b->tv_nsec) { + return 1; + } + else { + return 0; + } +} + +void +timer_add(struct timespec *a, const struct timespec *b) +{ + a->tv_sec += b->tv_sec; + a->tv_nsec += b->tv_nsec; + + if (a->tv_nsec > 1000000000) { + a->tv_sec += 1; + a->tv_nsec -= 1000000000; + } +} + +void +timer_add_ms(struct timespec *ts, uint32_t ms) +{ + struct timespec tmp = { + .tv_sec = ms / 1000, + .tv_nsec = (ms % 1000) * 1000000 + }; + timer_add(ts, &tmp); +} + +void +timer_sub(struct timespec *a, const struct timespec *b) +{ + if (timer_cmp(a, b) <= 0) { + a->tv_sec = 0; + a->tv_nsec = 0; + return; + } + + a->tv_sec -= b->tv_sec; + a->tv_nsec -= b->tv_nsec; + + if (a->tv_nsec < 0) { + a->tv_nsec += 1000000000; + a->tv_sec -= 1; + } +} + +void +timer_sub_ms(struct timespec *ts, uint32_t ms) +{ + struct timespec tmp = { + .tv_sec = ms / 1000, + .tv_nsec = (ms % 1000) * 1000000 + }; + timer_sub(ts, &tmp); +} diff --git a/src/timer_math.h b/src/timer_math.h new file mode 100644 index 0000000..99cdcd9 --- /dev/null +++ b/src/timer_math.h @@ -0,0 +1,27 @@ +#ifndef _TIMER_MATH_H_ +#define _TIMER_MATH_H_ + +#include +#include + +/* NOTE + * All math here is assuming that the second and nanosecond values of the + * timespecs are all non-negative, and nanoseconds is in the range [0, 999999999] */ + +/* Compare the two timespecs + * Return -1 if a < b, 0 if a = b, and 1 if a > b */ +int timer_cmp(const struct timespec *a, const struct timespec *b); + +/* Add the timespec value of b to a */ +void timer_add(struct timespec *a, const struct timespec *b); + +/* Add ms milliseconds to the timespec*/ +void timer_add_ms(struct timespec *ts, uint32_t ms); + +/* Subtract time b from a. If b > a, then a is set to 0 */ +void timer_sub(struct timespec *a, const struct timespec *b); + +/* Subtract ms milliseconds from ts. If ts would become negative, it is set to 0 */ +void timer_sub_ms(struct timespec *ts, uint32_t ms); + +#endif /* _TIMER_MATH_H_ */ diff --git a/src/xmodem.c b/src/xmodem.c index e004422..3752184 100644 --- a/src/xmodem.c +++ b/src/xmodem.c @@ -1,4 +1,3 @@ -#include #include #include #include @@ -8,7 +7,7 @@ #include "xmodem.h" -int _xmodem_wait(int fd, uint8_t *ret, int timeout_ms); +int _xmodem_wait(serial_t fd, uint8_t *ret, int timeout_ms); uint16_t _xmodem_crc(const uint8_t *buf, size_t len); uint8_t _xmodem_csum(const uint8_t *buf, size_t len); @@ -16,11 +15,11 @@ void _xmodem_status_print(size_t sent, size_t total); int xmodem_send( - int dest_fd, + serial_t dest_fd, int in_fd, size_t sz, int block_sz, - void (*status_cb)(size_t sent, size_t total) + void (*status_cb)(size_t sent, size_t total, int ack_fails) ) { uint8_t start_byte; @@ -30,6 +29,7 @@ xmodem_send( uint8_t packet_num; int retries; int ret = 0; + int ack_fails = 0; if (block_sz != 128 && block_sz != 1024) { return -1; @@ -105,7 +105,7 @@ xmodem_send( goto end_of_transmission; } - if (write(dest_fd, buf, buf_sz) != buf_sz){ + if (serial_write(dest_fd, buf, buf_sz) != buf_sz){ return XMODEM_ERR_WRITE; } @@ -115,14 +115,17 @@ xmodem_send( if (recv_char == XMODEM_ACK) { break; + } else if (recv_char == XMODEM_CAN) { + return XMODEM_ERR_CANCEL; } retries++; + ack_fails++; } sent += read_len; packet_num++; /* expect overflow */ - status_cb(sent, sz); + status_cb(sent, sz, ack_fails); } end_of_transmission: @@ -134,7 +137,7 @@ end_of_transmission: return XMODEM_ERR_TIMEOUT; } - write(dest_fd, &(uint8_t){XMODEM_EOT}, 1); + serial_write(dest_fd, &(uint8_t){XMODEM_EOT}, 1); if ( !_xmodem_wait(dest_fd, &ack_char, XMODEM_TIMEOUT) && @@ -150,20 +153,12 @@ end_of_transmission: } int -_xmodem_wait(int fd, uint8_t *ret, int timeout_ms) +_xmodem_wait(serial_t 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) { + if (serial_read_to(fd, ret, 1, timeout_ms) != 1) { return -1; } - read(fd, ret, 1); - return 0; } diff --git a/src/xmodem.h b/src/xmodem.h index ecc0092..c9a44ca 100644 --- a/src/xmodem.h +++ b/src/xmodem.h @@ -30,11 +30,11 @@ * length of `block_sz`. */ int xmodem_send( - int dest_fd, + serial_t dest_fd, int in_fd, size_t sz, int block_sz, - void (*status_cb)(size_t sent, size_t total) + void (*status_cb)(size_t sent, size_t total, int ack_fails) ); #if 0