From 3f4aa1e75032456fc1ee02de9a611102cd0015e7 Mon Sep 17 00:00:00 2001 From: Lephenixnoir Date: Sun, 26 Mar 2023 11:41:55 +0200 Subject: [PATCH] fxlink: usable TUI command setup + gintctl test commands --- CMakeLists.txt | 1 + fxlink/tui/command-util.c | 379 ++++++++++++++++++++++++++++++++ fxlink/tui/command-util.h | 157 +++++++++++++ fxlink/tui/commands.c | 411 +++++++++++++++++++---------------- fxlink/tui/tui-interactive.c | 245 ++++++++++++--------- fxlink/tui/tui.h | 32 +++ 6 files changed, 937 insertions(+), 288 deletions(-) create mode 100644 fxlink/tui/command-util.c create mode 100644 fxlink/tui/command-util.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2815141..1cb068c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ add_executable(fxlink fxlink/tooling/sdl2.c fxlink/tooling/udisks2.c fxlink/tui/commands.c + fxlink/tui/command-util.c fxlink/tui/input.c fxlink/tui/layout.c fxlink/tui/render.c diff --git a/fxlink/tui/command-util.c b/fxlink/tui/command-util.c new file mode 100644 index 0000000..93035c4 --- /dev/null +++ b/fxlink/tui/command-util.c @@ -0,0 +1,379 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include "tui.h" +#include "command-util.h" +#include +#include +#include +#include + +//--- +// Command parsing utilities +//--- + +struct fxlink_tui_cmd fxlink_tui_cmd_parse(char const *input) +{ + struct fxlink_tui_cmd cmd; + cmd.argc = 0; + cmd.argv = NULL; + cmd.data = malloc(strlen(input) + 1); + if(!cmd.data) + return cmd; + + char const *escapes1 = "\\nter"; + char const *escapes2 = "\\\n\t\e\r"; + + /* Whether a new word needs to be created at the next character */ + bool word_finished = true; + /* Offset into cmd.data */ + int i = 0; + + /* Read words eagerly, appending to cmd.data as we go */ + for(int j = 0; input[j]; j++) { + int c = input[j]; + + /* Stop words at spaces */ + if(isspace(c)) { + if(!word_finished) + cmd.data[i++] = 0; + word_finished = true; + continue; + } + + /* Translate escapes */ + if(c == '\\') { + char *p = strchr(escapes1, input[j+1]); + if(p) { + c = escapes2[p - escapes1]; + j++; + } + } + + /* Add a new word if necessary */ + if(word_finished) { + cmd.argv = realloc(cmd.argv, (++cmd.argc) * sizeof *cmd.argv); + cmd.argv[cmd.argc - 1] = cmd.data + i; + word_finished = false; + } + + /* Copy literals */ + cmd.data[i++] = c; + } + + cmd.data[i++] = 0; + cmd.argv = realloc(cmd.argv, (cmd.argc + 1) * sizeof *cmd.argv); + cmd.argv[cmd.argc] = 0; + return cmd; +} + +void fxlink_tui_cmd_dump(struct fxlink_tui_cmd const *cmd) +{ + print(TUI.wConsole, "[%d]", cmd->argc); + for(int i = 0; i < cmd->argc; i++) { + char const *arg = cmd->argv[i]; + print(TUI.wConsole, " '%s'(%d)", arg, (int)strlen(arg)); + } + print(TUI.wConsole, "\n"); +} + +void fxlink_tui_cmd_free(struct fxlink_tui_cmd const *cmd) +{ + free(cmd->argv); + free(cmd->data); +} + +static struct fxlink_device *find_connected_device(void) +{ + /* TODO: Use the "selected" device */ + for(int i = 0; i < TUI.devices.count; i++) { + if(TUI.devices.devices[i].status == FXLINK_FDEV_STATUS_CONNECTED) + return &TUI.devices.devices[i]; + } + return NULL; +} + +bool fxlink_tui_parse_args(int argc, char const **argv, char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + + int i = 0; + char const *spec = fmt; + bool got_variadic = false; + + for(; *spec; i++, spec++) { + /* Implicit/silent specifiers */ + if(*spec == 'd') { + struct fxlink_device **ptr = va_arg(args, struct fxlink_device **); + *ptr = find_connected_device(); + if(!*ptr) { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "no device connected\n"); + goto failure; + } + /* Bad */ + i--; + continue; + } + + /* No specifier that consumes stuff allowed after '*' */ + if(got_variadic) { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "got specifiers '%s' after '*'\n", spec); + goto failure; + } + + /* Specifiers allowed even when there is no argument left */ + if(*spec == '*') { + char const ***ptr = va_arg(args, char const ***); + *ptr = argv + i; + got_variadic = true; + continue; + } + + /* Argument required beyond this point */ + if(i >= argc && *spec != '*') { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "too few arguments\n"); + goto failure; + } + + /* Standard specifiers */ + if(*spec == 's') { + char const **ptr = va_arg(args, char const **); + *ptr = argv[i]; + } + else if(*spec == 'i') { + int *ptr = va_arg(args, int *); + char *endptr; + long l = strtol(argv[i], &endptr, 0); + if(*endptr) { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "not a valid integer: '%s'\n", argv[i]); + goto failure; + } + *ptr = l; + } + } + + va_end(args); + return true; + +failure: + va_end(args); + return false; +} + +//--- +// Command tree +//--- + +struct node { + /* Command or subtree name */ + char *name; + /* true if tree node, false if raw command */ + bool is_tree; + + union { + struct node *children; /* is_subtree = true */ + int (*func)(int argc, char const **argv); /* is_subtree = false */ + }; + + /* Next sibling */ + struct node *next; +}; + +static struct node *node_mkcmd(char const *name, int (*func)()) +{ + assert(name); + struct node *cmd = calloc(1, sizeof *cmd); + cmd->name = strdup(name); + cmd->func = func; + return cmd; +} + +static struct node *node_mktree(char const *name) +{ + assert(name); + struct node *tree = calloc(1, sizeof *tree); + tree->is_tree = true; + tree->name = strdup(name); + return tree; +} + +static void node_free(struct node *node); + +static void node_free_chain(struct node *node) +{ + struct node *next; + while(node) { + next = node->next; + node_free(node); + node = next; + } +} + +static void node_free(struct node *node) +{ + free(node->name); + if(node->is_tree) { + node_free_chain(node->children); + free(node); + } +} + +static void node_tree_add(struct node *tree, struct node *node) +{ + assert(tree->is_tree); + node->next = tree->children; + tree->children = node; +} + +static struct node *node_tree_get(struct node const *tree, char const *name) +{ + assert(tree->is_tree); + for(struct node *n = tree->children; n; n = n->next) { + if(!strcmp(n->name, name)) + return n; + } + return NULL; +} + +static struct node *node_tree_get_or_make_subtree(struct node *tree, + char const *name) +{ + assert(tree->is_tree); + struct node *n = node_tree_get(tree, name); + if(n) + return n; + n = node_mktree(name); + node_tree_add(tree, n); + return n; +} + +static struct node *node_tree_get_path(struct node *tree, char const **path, + int *path_end_index) +{ + assert(tree->is_tree); + struct node *n = node_tree_get(tree, path[0]); + if(!n) + return NULL; + + (*path_end_index)++; + if(!n->is_tree) + return n; + + if(!path[1]) { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "'%s' takes a sub-command argument\n", path[0]); + return NULL; + } + return node_tree_get_path(n, path+1, path_end_index); +} + +static void node_insert_command(struct node *tree, char const **path, + int (*func)(), int i) +{ + assert(tree->is_tree); + + if(!path[i]) { + fprintf(stderr, "error: cannot register empty command!\n"); + return; + } + else if(!path[i+1]) { + struct node *cmd = node_tree_get(tree, path[i]); + if(cmd) { + fprintf(stderr, "error: '%s' already registred!\n", path[i]); + return; + } + node_tree_add(tree, node_mkcmd(path[i], func)); + } + else { + struct node *subtree = node_tree_get_or_make_subtree(tree, path[i]); + if(!subtree->is_tree) { + fprintf(stderr, "error: '%s' is not a category!\n", path[i]); + return; + } + return node_insert_command(subtree, path, func, i+1); + } +} + +static void node_dump(struct node const *node, int indent) +{ + print(TUI.wConsole, "%*s", 2*indent, ""); + + if(node->is_tree) { + print(TUI.wConsole, "%s\n", node->name); + struct node *child = node->children; + while(child) { + node_dump(child, indent+1); + child = child->next; + } + } + else { + print(TUI.wConsole, "%s: %p\n", node->name, node->func); + } +} + +static struct node *cmdtree = NULL; + +void fxlink_tui_register_cmd(char const *name, + int (*func)(int argc, char const **argv)) +{ + int i = 0; + while(name[i] && (isalpha(name[i]) || strchr("?/-_ ", name[i]))) + i++; + if(name[i] != 0) { + fprintf(stderr, "error: invalid command path '%s'\n", name); + return; + } + + if(!cmdtree) + cmdtree = node_mktree("(root)"); + + /* Parse as a command because why not */ + struct fxlink_tui_cmd path = fxlink_tui_cmd_parse(name); + node_insert_command(cmdtree, path.argv, func, 0); + + fxlink_tui_cmd_free(&path); +} + +__attribute__((destructor)) +static void free_command_tree(void) +{ + node_free(cmdtree); + cmdtree = NULL; +} + +void TUI_execute_command(char const *command) +{ + struct fxlink_tui_cmd cmd = fxlink_tui_cmd_parse(command); + if(cmd.argc < 1) + goto end; + + int args_index = 0; + struct node *node = node_tree_get_path(cmdtree, cmd.argv, &args_index); + + if(node) { + node->func(cmd.argc - args_index, cmd.argv + args_index); + /* ignore return code? */ + } + else { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "unrecognized command: "); + fxlink_tui_cmd_dump(&cmd); + } + +end: + fxlink_tui_cmd_free(&cmd); +} + +FXLINK_COMMAND("?cmdtree") +{ + node_dump(cmdtree, 0); + return 0; +} diff --git a/fxlink/tui/command-util.h b/fxlink/tui/command-util.h new file mode 100644 index 0000000..206d351 --- /dev/null +++ b/fxlink/tui/command-util.h @@ -0,0 +1,157 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tui.command-util: Preprocessor black magic for command definition +// +// This header provides the following method for declaring TUI commands that +// are automatically registered at startup, and are invoked with arguments +// pre-parsed: +// +// FXLINK_COMMAND("", (), (), ...) { +// /* normal code... */ +// return ; +// } +// +// The command name is a string. It can have multiple space-separated words as +// in "gintctl test", in which case it is matched against argv[0], argv[1], etc +// and the prefix ("gintctl") is automatically made into a sub-category command +// with relevant error messages. +// +// Each argument has a type and a name, as in INT(x). The type carries +// information on the parsing method, the acceptable range, and of course the +// actual runtime type of the argument. Available types are: +// +// Name Runtime type Meaning and range +// -------------------------------------------------------------------------- +// INT int Any integer +// STRING char const * Any string argument from argv[] +// VARIADIC char const ** End of the argv array (NULL-terminated) +// -------------------------------------------------------------------------- +// DEVICE struct fxlink_device * Selected device (implicit; never NULL) +// -------------------------------------------------------------------------- +// +// The function returns a status code, which is an integer. The entire command +// declaration might look like: +// +// FXLINK_COMMAND("gintctl test", INT(lower_bound), INT(upper_bound)) { +// int avg = (lower_bound + upper_bound) / 2; +// return 0; +// } +// +// I considered doing the entire thing in C++, but absolute preprocessor abuse +// is fun once in a while. +//--- + +#include + +//--- +// Shell-like command parsing (without the features) +//--- + +struct fxlink_tui_cmd { + int argc; + char const **argv; + char *data; +}; + +/* Parse a string into an argument vector */ +struct fxlink_tui_cmd fxlink_tui_cmd_parse(char const *input); + +/* Dump a command to TUI console for debugging */ +void fxlink_tui_cmd_dump(struct fxlink_tui_cmd const *cmd); + +/* Free a command */ +void fxlink_tui_cmd_free(struct fxlink_tui_cmd const *cmd); + +//--- +// Command registration and argument scanning +//--- + +/* Parse a list of arguments into structured data. The format is a string of + argument specifiers, each of which can be: + s String (char *) + d Integer (int) + * Other variadic arguments (char **) + (-- will probably be expanded later.) + Returns true if parsing succeeded, false otherwise (including if arguments + are missing) after printing an error message. */ +bool fxlink_tui_parse_args(int argc, char const **argv, char const *fmt, ...); + +/* Register a command with the specified name and invocation function. This can + be called manually or generated (along with the parser) using the macro + FXLINK_COMMAND. */ +void fxlink_tui_register_cmd(char const *name, + int (*func)(int argc, char const **argv)); + +/* Apply a macro to every variadic argument. _M1 is applied to the first + argument and _Mn is applied to all subsequent arguments. */ +#define MAPn(_M1,_Mn,...) __VA_OPT__(MAP_1(_M1,_Mn,__VA_ARGS__)) +#define MAP_1(_M1,_Mn,_X,...) _M1(_X) __VA_OPT__(MAP_2(_M1,_Mn,__VA_ARGS__)) +#define MAP_2(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_3(_M1,_Mn,__VA_ARGS__)) +#define MAP_3(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_4(_M1,_Mn,__VA_ARGS__)) +#define MAP_4(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_5(_M1,_Mn,__VA_ARGS__)) +#define MAP_5(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_6(_M1,_Mn,__VA_ARGS__)) +#define MAP_6(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_7(_M1,_Mn,__VA_ARGS__)) +#define MAP_7(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_8(_M1,_Mn,__VA_ARGS__)) +#define MAP_8(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_MAX_8_ARGS(_)) +#define MAP_MAX_8_ARGS() +/* Simpler version where the same macro is applied to all arguments */ +#define MAP(_M, ...) MAPn(_M, _M, ##__VA_ARGS__) + +/* Command declaration macro. Builds an invocation function and a registration + function so the command name doesn't have to be repeated. */ +#define FXLINK_COMMAND(_NAME, ...) DO_COMMAND1(_NAME, __COUNTER__, __VA_ARGS__) +/* This call forces __COUNTER__ to expand */ +#define DO_COMMAND1(...) DO_COMMAND(__VA_ARGS__) + +#define DO_COMMAND(_NAME, _COUNTER, ...) \ + static int ___command_ ## _COUNTER(); \ + static int ___invoke_command_ ## _COUNTER \ + (int ___argc, char const **___argv) { \ + MAP(MKVAR, ##__VA_ARGS__) \ + if(!fxlink_tui_parse_args(___argc, ___argv, \ + "" MAP(MKFMT, ##__VA_ARGS__) \ + MAP(MKPTR, ##__VA_ARGS__))) return 1; \ + return ___command_ ## _COUNTER( \ + MAPn(MKCALL_1, MKCALL_n, ##__VA_ARGS__)); \ + } \ + __attribute__((constructor)) \ + static void ___declare_command_ ## _COUNTER (void) { \ + fxlink_tui_register_cmd(_NAME, ___invoke_command_ ## _COUNTER); \ + } \ + static int ___command_ ## _COUNTER(MAPn(MKFML_1, MKFML_n, ##__VA_ARGS__)) + +/* Make the format string for an argument */ +#define MKFMT(_TV) MKFMT_ ## _TV +#define MKFMT_INT(_X) "i" +#define MKFMT_STRING(_X) "s" +#define MKFMT_VARIADIC(_X) "*" +#define MKFMT_DEVICE(_X) "d" + +/* Make the formal function parameter for an argument */ +#define MKFML_1(_TV) MKFML_ ## _TV +#define MKFML_n(_TV) , MKFML_1(_TV) +#define MKFML_INT(_X) int _X +#define MKFML_STRING(_X) char const * _X +#define MKFML_VARIADIC(_X) char const ** _X +#define MKFML_DEVICE(_X) struct fxlink_device * _X + +/* Create a variable */ +#define MKVAR(_TV) MKFML_1(_TV); + +/* Make a pointer to an argument (sadly we can't get the name directly) */ +#define MKPTR(_TV) , MKPTR_ ## _TV +#define MKPTR_INT(_X) &_X +#define MKPTR_STRING(_X) &_X +#define MKPTR_VARIADIC(_X) &_X +#define MKPTR_DEVICE(_X) &_X + +/* Pass a variable as a function argument */ +#define MKCALL_1(_TV) MKCALL_ ## _TV +#define MKCALL_n(_TV) , MKCALL_1(_TV) +#define MKCALL_INT(_X) _X +#define MKCALL_STRING(_X) _X +#define MKCALL_VARIADIC(_X) _X +#define MKCALL_DEVICE(_X) _X diff --git a/fxlink/tui/commands.c b/fxlink/tui/commands.c index be208c7..5afa8b3 100644 --- a/fxlink/tui/commands.c +++ b/fxlink/tui/commands.c @@ -5,236 +5,269 @@ //---------------------------------------------------------------------------// #include "tui.h" +#include "command-util.h" #include #include #include -#include +#include //--- -// Parsing utilities +// Standard commands //--- -struct command { - int argc; - char const **argv; - char *data; -}; - -static struct command parse_command(char const *input) +FXLINK_COMMAND("/echo", DEVICE(fdev), VARIADIC(argv)) { - struct command cmd; - cmd.argc = 0; - cmd.argv = NULL; - cmd.data = malloc(strlen(input) + 1); - if(!cmd.data) - return cmd; + int l = 5, j = 5; + for(int i = 0; argv[i]; i++) + l += strlen(argv[i]) + 1; - char const *escapes1 = "\\nter"; - char const *escapes2 = "\\\n\t\e\r"; + char *concat = malloc(l + 1); + strcpy(concat, "echo "); + for(int i = 0; argv[i]; i++) { + strcpy(concat + j, argv[i]); + j += strlen(argv[i]); + concat[j++] = (argv[i+1] == NULL) ? '\n' : ' '; + } + concat[j] = '\0'; - /* Whether a new word needs to be created at the next character */ - bool word_finished = true; - /* Offset into cmd.data */ - int i = 0; + fxlink_device_start_bulk_OUT(fdev, + "fxlink", "command", concat, l, true); + return 0; +} - /* Read words eagerly, appending to cmd.data as we go */ - for(int j = 0; input[j]; j++) { - int c = input[j]; +FXLINK_COMMAND("/identify", DEVICE(fdev)) +{ + fxlink_device_start_bulk_OUT(fdev, + "fxlink", "command", "identify", 8, false); + return 0; +} - /* Stop words at spaces */ - if(isspace(c)) { - if(!word_finished) - cmd.data[i++] = 0; - word_finished = true; - continue; - } +//--- +// gintctl commands +//--- - /* Translate escapes */ - if(c == '\\') { - char *p = strchr(escapes1, input[j+1]); - if(p) { - c = escapes2[p - escapes1]; - j++; - } - } +static char const *lipsum = + "When the war of the beasts brings about the world's end,\n" + "The goddess descends from the sky.\n" + "Wings of light and dark spread afar,\n" + "She guides us to bliss, her gift everlasting.\n" + "\n" + "Infinite in mystery is the gift of the goddess.\n" + "We seek it thus, and take to the sky.\n" + "Ripples form on the water's surface;\n" + "The wandering soul knows no rest.\n" + "\n" + "There is no hate, only joy,\n" + "For you are beloved by the goddess.\n" + "Hero of the dawn, healer of worlds,\n" + "Dreams of the morrow hath the shattered soul.\n" + "Pride is lost -- wings stripped away, the end is nigh.\n" + "\n" + "My friend, do you fly away now?\n" + "To a world that abhors you and I?\n" + "All that awaits you is a somber morrow\n" + "No matter where the winds may blow.\n" + "My friend, your desire\n" + "Is the bringer of life, the gift of the goddess.\n" + "Even if the morrow is barren of promises,\n" + "Nothing shall forestall my return.\n" + "\n" + "My friend, the fates are cruel.\n" + "There are no dreams, no honor remains.\n" + "The arrow has left the bow of the goddess.\n" + "My soul, corrupted by vengeance,\n" + "Hath endured torment to find the end of the journey\n" + "In my own salvation and your eternal slumber.\n" + "Legend shall speak of sacrifice at world's end\n" + "The wind sails over the water's surface, quietly, but surely.\n" + "\n" + "Even if the morrow is barren of promises,\n" + "Nothing shall forestall my return.\n" + "To become the dew that quenches the lands,\n" + "To spare the sands, the seas, the skies,\n" + "I offer thee this silent sacrifice.\n"; - /* Add a new word if necessary */ - if(word_finished) { - cmd.argv = realloc(cmd.argv, (++cmd.argc) * sizeof *cmd.argv); - cmd.argv[cmd.argc - 1] = cmd.data + i; - word_finished = false; - } - - /* Copy literals */ - cmd.data[i++] = c; +FXLINK_COMMAND("gintctl echo-bounds", DEVICE(fdev), INT(count)) +{ + if(count < 0 || count > 8192) { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "count should be 0..8192 (not %d)\n", count); + return 1; } - cmd.data[i++] = 0; - return cmd; + uint32_t *data = malloc(count * 4); + for(int i = 0; i < count; i++) + data[i] = i; + fxlink_device_start_bulk_OUT(fdev, + "gintctl", "echo-bounds", data, count * 4, true); + return 0; } -static void dump_command(struct command const *cmd) +FXLINK_COMMAND("gintctl garbage", DEVICE(fdev), INT(count)) { - print(TUI.wConsole, "command(%d)", cmd->argc); - for(int i = 0; i < cmd->argc; i++) { - char const *arg = cmd->argv[i]; - print(TUI.wConsole, " '%s'(%d)", arg, (int)strlen(arg)); + if(count < 0 || count > 8192) { + fprint(TUI.wConsole, FMT_RED, "error: "); + print(TUI.wConsole, "count should be 0..8192 (not %d)\n", count); + return 1; } - print(TUI.wConsole, "\n"); + + uint32_t *data = malloc(count * 4); + for(int i = 0; i < count; i++) + data[i] = i + 0xdead0000; + fxlink_device_start_bulk_OUT(fdev, + "gintctl", "garbage", data, count * 4, true); + return 0; } -static void free_command(struct command *cmd) -{ - free(cmd->argv); - free(cmd->data); -} - -/* Parse a list of arguments into strings/integers/etc. The format is a string - of argument specifiers, which can be: - s String - d Integer - * Other variadic arguments (integer holding first index) - (-- will probably be expanded later.) - Returns true if parsing succeeded, false otherwise (including if arguments - are missing) after printing an error message. */ -static bool parse_arguments(int argc, char const **argv, char const *fmt, ...) +static void status(bool b, char const *fmt, ...) { va_list args; va_start(args, fmt); - - int i = 1; - char const *spec = fmt; - - while(*spec) { - if(i >= argc) { - fprint(TUI.wConsole, FMT_RED, "error: "); - print(TUI.wConsole, "too few arguments\n"); - goto failure; - } - - /* Parse an argument specifier */ - if(*spec == 's') { - char const **ptr = va_arg(args, char const **); - *ptr = argv[i]; - } - else if(*spec == 'd') { - int *ptr = va_arg(args, int *); - char *endptr; - long l = strtol(argv[i], &endptr, 0); - if(*endptr) { - fprint(TUI.wConsole, FMT_RED, "error: "); - print(TUI.wConsole, "not a valid integer: '%s'\n", argv[i]); - goto failure; - } - *ptr = l; - } - - spec++; - } - + fprint(TUI.wConsole, b ? FMT_GREEN : FMT_RED, b ? " ":" "); + vw_printw(TUI.wConsole, fmt, args); va_end(args); - return true; - -failure: - va_end(args); - return false; } -static bool run_no_device(int argc, char const **argv) +static void unit_echo(struct fxlink_device *fdev, char const *str, + char const *description) { - (void)argc; - (void)argv; - return false; + char *echo = malloc(5 + strlen(str) + 1); + strcpy(echo, "echo "); + strcat(echo, str); + + fxlink_device_start_bulk_OUT(fdev, + "fxlink", "command", echo, strlen(echo), true); + + struct fxlink_message *msg = NULL; + while(TUI_wait_message(fdev, "fxlink", "text", &msg)) { + bool success = + msg->size == strlen(str) + && !strncmp(msg->data, str, msg->size); + if(description) + status(success, "%s\n", description); + else + status(success, "echo of '%s': '%.*s' (%d)\n", str, msg->size, + (char *)msg->data, msg->size); + } } -static void run_gintctl_command(int argc, char const **argv, - struct fxlink_device *fdev) +static void unit_echo_bounds(struct fxlink_device *fdev, int count) { - if(!strcmp(argv[0], "read-long")) { - int count = 0; - if(!parse_arguments(argc, argv, "d", &count)) - return; - if(count < 0 || count > 8192) - return; + char reference[256]; + sprintf(reference, "first=%08x last=%08x total=%d B\n", + 0, count-1, 4*count); - uint32_t *data = malloc(count * 4); - for(int i = 0; i < count; i++) - data[i] = i; - fxlink_device_start_bulk_OUT(fdev, - "gintctl", "echo-bounds", data, count * 4, true); + uint32_t *data = malloc(count * 4); + for(int i = 0; i < count; i++) + data[i] = i; + fxlink_device_start_bulk_OUT(fdev, + "gintctl", "echo-bounds", data, count * 4, true); + + struct fxlink_message *msg = NULL; + while(TUI_wait_message(fdev, "fxlink", "text", &msg)) { + bool success = + msg->size == strlen(reference) + && !strncmp(msg->data, reference, msg->size); + + status(success, "echo bounds %d B\n", count * 4); } } -static bool run_with_device(int argc, char const **argv, - struct fxlink_device *fdev) +static void unit_read_unaligned(struct fxlink_device *fdev, char const *str, + int kind) { - if(!strcmp(argv[0], "/echo")) { - int l = 5, j = 5; - for(int i = 1; i < argc; i++) - l += strlen(argv[i]) + 1; + char *payload = malloc(strlen(str) + 2); + sprintf(payload, "%c%s", kind, str); - char *concat = malloc(l + 1); - strcpy(concat, "echo "); - for(int i = 1; i < argc; i++) { - strcpy(concat + j, argv[i]); - j += strlen(argv[i]); - concat[j++] = (i == argc - 1) ? '\n' : ' '; - } - concat[j] = '\0'; + fxlink_device_start_bulk_OUT(fdev, + "gintctl", "read-unaligned", payload, strlen(payload), true); - fxlink_device_start_bulk_OUT(fdev, - "fxlink", "command", concat, l, true); + struct fxlink_message *msg = NULL; + while(TUI_wait_message(fdev, "fxlink", "text", &msg)) { + bool success = + msg->size == strlen(str) + && !strncmp(msg->data, str, msg->size); + if(strlen(str) < 20) + status(success, "unaligned echo type '%c' of '%s'\n", kind, str); + else + status(success, "unaligned echo type '%c' of %d-byte string\n", + kind, strlen(str)); } - else if(!strcmp(argv[0], "/identify")) { - fxlink_device_start_bulk_OUT(fdev, - "fxlink", "command", "identify", 8, false); - } - else if(!strcmp(argv[0], "gintctl")) { - if(argc <= 1) { - fprint(TUI.wConsole, FMT_RED, "error: "); - print(TUI.wConsole, "gintctl command needs sub-command\n"); - return true; - } - run_gintctl_command(argc-1, argv+1, fdev); - return true; - } - else - return false; - return true; } -void TUI_execute_command(char const *command) +static void test_read_basic(struct fxlink_device *fdev) { - struct command cmd = parse_command(command); - (void)dump_command; - - /* Connected device (TODO: Use the "selected" device) */ - struct fxlink_device *fdev = NULL; - for(int i = 0; i < TUI.devices.count; i++) { - if(TUI.devices.devices[i].status == FXLINK_FDEV_STATUS_CONNECTED) { - fdev = &TUI.devices.devices[i]; - break; - } - } - - bool b = run_no_device(cmd.argc, cmd.argv); - if(b) { - free_command(&cmd); - return; - } - - /* The following commands require a connected device */ - if(!fdev) { - print(TUI.wConsole, "no connected device!\n"); - return; - } - print(TUI.wConsole, "using device %s (%s)\n", - fxlink_device_id(fdev), fdev->calc->serial); - - b = run_with_device(cmd.argc, cmd.argv, fdev); - if(!b) { - fprint(TUI.wConsole, FMT_RED, "error: "); - print(TUI.wConsole, "unrecognized command '%s'\n", cmd.argv[0]); - } - free_command(&cmd); + unit_echo(fdev, "123", NULL); + unit_echo(fdev, "1234", NULL); + unit_echo(fdev, "12345", NULL); + unit_echo(fdev, "123456", NULL); + unit_echo(fdev, lipsum, "echo of better lorem ipsum"); +} + +static void test_read_buffers(struct fxlink_device *fdev) +{ + /* 128 and 384 bytes -> less than a packet */ + unit_echo_bounds(fdev, 32); + unit_echo_bounds(fdev, 96); + /* 512 bytes -> exactly one packet */ + unit_echo_bounds(fdev, 128); + unit_echo_bounds(fdev, 128); + unit_echo_bounds(fdev, 128); + /* 516 and 768 -> one packet and a short one */ + unit_echo_bounds(fdev, 129); + unit_echo_bounds(fdev, 192); + /* 1024 bytes -> exactly two packets */ + unit_echo_bounds(fdev, 256); + /* 2044 bytes -> just shy of a full buffer */ + unit_echo_bounds(fdev, 511); + /* 2048 bytes -> a full buffer */ + unit_echo_bounds(fdev, 512); + unit_echo_bounds(fdev, 512); + /* 2300 bytes -> more than a full buffer */ + unit_echo_bounds(fdev, 575); + /* 6000 bytes -> non-integral number of full buffers but more than 2 */ + unit_echo_bounds(fdev, 1500); + /* 8192 bytes -> "large" amount of full buffers */ + unit_echo_bounds(fdev, 2048); +} + +static void test_read_unaligned(struct fxlink_device *fdev) +{ + char const *alpha = "aBcDeFgHiJkLmNoPqR"; + + for(int i = 1; i <= 9; i++) + unit_read_unaligned(fdev, alpha, '0' + i); + unit_read_unaligned(fdev, alpha, 'i'); + unit_read_unaligned(fdev, alpha, 'r'); + + for(int i = 1; i <= 9; i++) + unit_read_unaligned(fdev, lipsum, '0' + i); + unit_read_unaligned(fdev, lipsum, 'i'); + unit_read_unaligned(fdev, lipsum, 'r'); +} + +FXLINK_COMMAND("gintctl test read-basic", DEVICE(fdev)) +{ + test_read_basic(fdev); + return 0; +} + +FXLINK_COMMAND("gintctl test read-buffers", DEVICE(fdev)) +{ + test_read_buffers(fdev); + return 0; +} + +FXLINK_COMMAND("gintctl test read-unaligned", DEVICE(fdev)) +{ + test_read_unaligned(fdev); + return 0; +} + +FXLINK_COMMAND("gintctl test all", DEVICE(fdev)) +{ + test_read_basic(fdev); + test_read_buffers(fdev); + test_read_unaligned(fdev); + return 0; } diff --git a/fxlink/tui/tui-interactive.c b/fxlink/tui/tui-interactive.c index 9103a80..cf31cf5 100644 --- a/fxlink/tui/tui-interactive.c +++ b/fxlink/tui/tui-interactive.c @@ -399,11 +399,130 @@ static void handle_fxlink_log(int display_fmt, char const *str) wattroff(TUI.wLogs, attr); } +bool TUI_core_update(bool allow_console, bool auto_refresh, bool *has_command) +{ + struct timeval zero_tv = { 0 }; + struct timeval usb_timeout; + struct pollfd stdinfd = { .fd = STDIN_FILENO, .events = POLLIN }; + + int rc = libusb_get_next_timeout(TUI.ctx, &usb_timeout); + int timeout = -1; + if(rc > 0) + timeout = usb_timeout.tv_sec * 1000 + usb_timeout.tv_usec / 1000; + bool timeout_is_libusb = true; + /* Time out at least every 100 ms so we can handle SDL events */ + if(timeout < 0 || timeout > 100) { + timeout = 100; + timeout_is_libusb = false; + } + + if(has_command) + *has_command = false; + + rc = fxlink_multipoll(timeout, + &stdinfd, 1, TUI.polled_fds.fds, TUI.polled_fds.count, NULL); + + if(rc < 0 && errno != EINTR) { + elog("poll: %s\n", strerror(errno)); + return false; + } + if(rc < 0 && errno == EINTR) + return false; + + /* Handle SIGWINCH */ + if(TUI.resize_needed) { + endwin(); + refresh(); + TUI_setup_windows(); + TUI.resize_needed = false; + TUI_render_all(true); + TUI_refresh_all(true); + return false; + } + + /* Determine which even source was activated */ + bool stdin_activity = (stdinfd.revents & POLLIN) != 0; + bool usb_activity = false; + for(int i = 0; i < TUI.polled_fds.count; i++) + usb_activity |= (TUI.polled_fds.fds[i].revents != 0); + + /* Determine what to do. We update the console on stdin activity. We + update libusb on USB activity or appropriate timeout. We update SDL + events on any timeout. */ + bool update_console = stdin_activity; + bool update_usb = usb_activity || (rc == 0 && timeout_is_libusb); + bool update_sdl = (rc == 0); + + if(allow_console && update_console) { + bool finished = fxlink_TUI_input_getch(&TUI.input, TUI.wLogs); + TUI_refresh_console(); + if(has_command) + *has_command = finished; + } + + if(update_usb) { + libusb_handle_events_timeout(TUI.ctx, &zero_tv); + fxlink_device_list_refresh(&TUI.devices); + + for(int i = 0; i < TUI.devices.count; i++) { + struct fxlink_device *fdev = &TUI.devices.devices[i]; + + /* Check for devices ready to connect to */ + if(fdev->status == FXLINK_FDEV_STATUS_IDLE && fdev->comm + && fdev->comm->ep_bulk_IN != 0xff) { + if(fxlink_device_claim_fxlink(fdev)) + fxlink_device_start_bulk_IN(fdev); + } + } + } + + if(update_sdl) { + fxlink_sdl2_handle_events(); + } + + bool refresh = update_console || update_usb; + if(auto_refresh) { + TUI_render_all(false); + TUI_refresh_all(false); + } + return refresh; +} + +bool TUI_wait_message(struct fxlink_device *fdev, + char const *application, char const *type, struct fxlink_message **msg_ptr) +{ + if(*msg_ptr) { + fxlink_message_free(*msg_ptr, true); + fxlink_device_start_bulk_IN(fdev); + return false; + } + + while(1) { + TUI_core_update(false, true, NULL); + + /* Check for new messages */ + struct fxlink_message *msg = fxlink_device_finish_bulk_IN(fdev); + if(msg) { + if(fxlink_message_is_apptype(msg, application, type)) { + *msg_ptr = msg; + return true; + } + else { + fxlink_interactive_handle_message(msg); + fxlink_message_free(msg, true); + fxlink_device_start_bulk_IN(fdev); + } + } + } +} + int main_tui_interactive(libusb_context *ctx) { if(!TUI_setup()) return elog("error: failed to setup ncurses TUI o(x_x)o\n"); + TUI.ctx = ctx; + /* Redirect fxlink logs to the logging window in the TUI */ fxlink_log_set_handler(handle_fxlink_log); /* Set up hotplug notification */ @@ -411,10 +530,6 @@ int main_tui_interactive(libusb_context *ctx) /* Set up file descriptor tracking */ fxlink_pollfds_track(&TUI.polled_fds, ctx); - struct timeval zero_tv = { 0 }; - struct timeval usb_timeout; - struct pollfd stdinfd = { .fd = STDIN_FILENO, .events = POLLIN }; - /* Initial render */ print(TUI.wConsole, "fxlink version %s (libusb/TUI interactive mode)\n", FXLINK_VERSION); @@ -424,113 +539,45 @@ int main_tui_interactive(libusb_context *ctx) TUI_render_all(true); TUI_refresh_all(true); - struct fxlink_TUI_input input; - fxlink_TUI_input_init(&input, TUI.wConsole, 16); + fxlink_TUI_input_init(&TUI.input, TUI.wConsole, 16); while(1) { - int rc = libusb_get_next_timeout(ctx, &usb_timeout); - int timeout = -1; - if(rc > 0) - timeout = usb_timeout.tv_sec * 1000 + usb_timeout.tv_usec / 1000; - bool timeout_is_libusb = true; - /* Time out at least every 100 ms so we can handle SDL events */ - if(timeout < 0 || timeout > 100) { - timeout = 100; - timeout_is_libusb = false; - } + bool has_command; + bool activity = TUI_core_update(true, false, &has_command); - rc = fxlink_multipoll(timeout, - &stdinfd, 1, TUI.polled_fds.fds, TUI.polled_fds.count, NULL); - - if(rc < 0 && errno != EINTR) - elog("poll: %s\n", strerror(errno)); - - /* Handle SIGWINCH */ - if(TUI.resize_needed) { - endwin(); - refresh(); - TUI_setup_windows(); - TUI.resize_needed = false; - TUI_render_all(true); - TUI_refresh_all(true); - continue; - } - - /* Determine which even source was activated */ - bool stdin_activity = (stdinfd.revents & POLLIN) != 0; - bool usb_activity = false; - for(int i = 0; i < TUI.polled_fds.count; i++) - usb_activity |= (TUI.polled_fds.fds[i].revents != 0); - - /* Determine what to do. We update the console on stdin activity. We - update libusb on USB activity or appropriate timeout. We update SDL - events on any timeout. */ - bool update_console = stdin_activity; - bool update_usb = usb_activity || (rc == 0 && timeout_is_libusb); - bool update_sdl = (rc == 0); - - if(update_console) { - bool finished = fxlink_TUI_input_getch(&input, TUI.wLogs); - TUI_refresh_console(); - - if(finished) { - char *command = input.data; - bool refresh_all = false; - - if(command[0] != 0) - log_("command: '%s'\n", command); - if(!strcmp(command, "")) - {} - else if(!strcmp(command, "q") || !strcmp(command, "quit")) - break; - else { - TUI_execute_command(command); - refresh_all = true; - } - - fxlink_TUI_input_free(&input); - print(TUI.wConsole, "%s", prompt); - fxlink_TUI_input_init(&input, TUI.wConsole, 16); - - if(refresh_all) { - TUI_render_all(false); - TUI_refresh_all(false); - } - else - TUI_refresh_console(); + /* Check for devices with finished transfers */ + for(int i = 0; i < TUI.devices.count; i++) { + struct fxlink_device *fdev = &TUI.devices.devices[i]; + struct fxlink_message *msg = fxlink_device_finish_bulk_IN(fdev); + if(msg) { + fxlink_interactive_handle_message(msg); + fxlink_message_free(msg, true); + fxlink_device_start_bulk_IN(fdev); } } - if(update_usb) { - libusb_handle_events_timeout(ctx, &zero_tv); - fxlink_device_list_refresh(&TUI.devices); + /* Check for console commands */ + if(has_command) { + char *command = TUI.input.data; - for(int i = 0; i < TUI.devices.count; i++) { - struct fxlink_device *fdev = &TUI.devices.devices[i]; + if(command[0] != 0) + log_("command: '%s'\n", command); + if(!strcmp(command, "")) + {} + else if(!strcmp(command, "q") || !strcmp(command, "quit")) + break; + else + TUI_execute_command(command); - /* Check for devices ready to connect to */ - if(fdev->status == FXLINK_FDEV_STATUS_IDLE && fdev->comm - && fdev->comm->ep_bulk_IN != 0xff) { - if(fxlink_device_claim_fxlink(fdev)) - fxlink_device_start_bulk_IN(fdev); - } - - /* Check for devices with finished transfers */ - struct fxlink_message *msg=fxlink_device_finish_bulk_IN(fdev); - if(msg) { - fxlink_interactive_handle_message(msg); - fxlink_message_free(msg, true); - fxlink_device_start_bulk_IN(fdev); - } - } + fxlink_TUI_input_free(&TUI.input); + print(TUI.wConsole, "%s", prompt); + fxlink_TUI_input_init(&TUI.input, TUI.wConsole, 16); + } + if(activity) { TUI_render_all(false); TUI_refresh_all(false); } - - if(update_sdl) { - fxlink_sdl2_handle_events(); - } } while(fxlink_device_list_interrupt(&TUI.devices)) diff --git a/fxlink/tui/tui.h b/fxlink/tui/tui.h index 2258f67..d11ffa8 100644 --- a/fxlink/tui/tui.h +++ b/fxlink/tui/tui.h @@ -16,6 +16,8 @@ #include struct TUIData { + /* libusb context */ + libusb_context *ctx; /* SIGWINCH flag */ bool resize_needed; /* ncurses window panels */ @@ -29,8 +31,38 @@ struct TUIData { /* Application data */ struct fxlink_pollfds polled_fds; struct fxlink_device_list devices; + /* Main console input */ + struct fxlink_TUI_input input; }; extern struct TUIData TUI; +/* Run a single asynchronous update. This polls a bunch of file descriptors + along with a short timeout (< 1s). Returns true if there is any activity. + + If `allow_console` is true, console events are handled; otherwise they are + ignored so they can be collected by the main loop. Setting this parameter to + false is useful when waiting for messages in TUI commands. `has_command` is + set to whether there is a new command to be run at the console (only ever + true when `allow_console` is true). + + If `auto_refresh` is true, this function will refresh the TUI upon relevant + activity. */ +bool TUI_core_update(bool allow_console, bool auto_refresh, bool *has_command); + +/* Run the specified TUI command. */ void TUI_execute_command(char const *command); + +/* Wait for a message of a particular type to arrive, and then clean it up. + This function should be called in a loop, eg. + + struct fxlink_message *msg = NULL; + while(TUI_wait_message(fdev, "fxlink", "text", &msg)) { + // Handle msg... + } + + TUI_wait_message() will only return true once, however it will use the next + call to free the message, restart communication on the device, and reset + `msg` to NULL. */ +bool TUI_wait_message(struct fxlink_device *fdev, + char const *application, char const *type, struct fxlink_message **msg);