fxlink: usable TUI command setup + gintctl test commands

This commit is contained in:
Lephenixnoir 2023-03-26 11:41:55 +02:00
parent cef9d21076
commit 3f4aa1e750
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
6 changed files with 937 additions and 288 deletions

View File

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

379
fxlink/tui/command-util.c Normal file
View File

@ -0,0 +1,379 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "tui.h"
#include "command-util.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
//---
// 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;
}

157
fxlink/tui/command-util.h Normal file
View File

@ -0,0 +1,157 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/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("<NAME>", <TYPE>(<NAME>), <TYPE>(<NAME>), ...) {
// /* normal code... */
// return <STATUS>;
// }
//
// 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 <fxlink/defs.h>
//---
// 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

View File

@ -5,236 +5,269 @@
//---------------------------------------------------------------------------//
#include "tui.h"
#include "command-util.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdarg.h>
#include <assert.h>
//---
// 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 ? "<PASSED> ":"<FAILED> ");
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;
}

View File

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

View File

@ -16,6 +16,8 @@
#include <ncurses.h>
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);