fxsdk/fxlink/tui/tui-interactive.c
Lephenixnoir 85d7fcf9e9
libfxlink: fix race condition leading to lost messages
Basically if the calculator sends two messages in a row, it is possible
for a single libusb_handle_events() to get both. And the comm structure
wasn't designed for that, because it could buffer only one message at a
time, which the user needed to read after event handling.

The comm structure now has a 16-message buffer, which should be more
than enough for any single event handling loop. On the user level this
has implications in that fxlink_device_finish_bulk_IN() must be called
*in a loop* after each event handling cycle.

Reported in https://git.planet-casio.com/Lephenixnoir/gint/pulls/27
2024-03-24 19:25:35 +01:00

592 lines
17 KiB
C

//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "../fxlink.h"
#include "tui.h"
#include <fxlink/tooling/libpng.h>
#include <fxlink/tooling/sdl2.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
/*
Plan for the TUI command set.
fxlink commands:
select Select the calculator we're talking to
remote-control Enable/disable/setup remote calculator control
Protocol commands (executed by the calculator):
/identify Send calculator identification information
/echo Echo a message
/screenshot Take a screenshot
/video Enable/disable video capture
Application-specific commands:
gintctl read-long Transmission tests (long messages)
gintctl read-alignment Pipe reading alignment
gintctl bench USB transfer speed benchmark
*/
struct TUIData TUI = { 0 };
//---
// TUI management and rendering
//---
static bool TUI_setup_windows(void)
{
struct fxlink_TUI_box
*bTransfers, *bConsole, *bStatus, *bLogs, *bTextOutput,
*bLeft, *bRight;
bTransfers = fxlink_TUI_box_mk_window("Transfers", &TUI.wTransfers);
bConsole = fxlink_TUI_box_mk_window("Console", &TUI.wConsole);
bStatus = fxlink_TUI_box_mk_window("Status", &TUI.wStatus);
bLogs = fxlink_TUI_box_mk_window("Logs", &TUI.wLogs);
bTextOutput = fxlink_TUI_box_mk_window("Text output from calculators",
&TUI.wTextOutput);
fxlink_TUI_box_stretch(bLogs, 1, 2, false);
bLeft = fxlink_TUI_box_mk_vertical(bTextOutput, bConsole, NULL);
bRight = fxlink_TUI_box_mk_vertical(bStatus, bLogs, bTransfers, NULL);
fxlink_TUI_box_stretch(bLeft, 2, 1, false);
fxlink_TUI_box_stretch(bRight, 3, 1, false);
TUI.bRoot = fxlink_TUI_box_mk_horizontal(bLeft, bRight, NULL);
fxlink_TUI_box_layout(TUI.bRoot, 0, 1, getmaxx(stdscr), getmaxy(stdscr)-1);
return fxlink_TUI_apply_layout(TUI.bRoot);
}
static void TUI_free_windows(void)
{
if(TUI.wStatus) delwin(TUI.wStatus);
if(TUI.wLogs) delwin(TUI.wLogs);
if(TUI.wTransfers) delwin(TUI.wTransfers);
if(TUI.wTextOutput) delwin(TUI.wTextOutput);
if(TUI.wConsole) delwin(TUI.wConsole);
}
static void TUI_refresh_all(bool refresh_bg)
{
if(refresh_bg)
wrefresh(stdscr);
wrefresh(TUI.wStatus);
wrefresh(TUI.wLogs);
wrefresh(TUI.wTransfers);
wrefresh(TUI.wTextOutput);
wrefresh(TUI.wConsole);
}
static void TUI_refresh_console(void)
{
wrefresh(TUI.wLogs);
wrefresh(TUI.wConsole);
}
static void TUI_render_status(void)
{
WINDOW *win = TUI.wStatus;
werase(win);
int w, h, y = 1;
getmaxyx(win, h, w);
wmove(win, 0, 1);
fprint(win, FMT_HEADER,
"Device Status ID System Classes");
if(TUI.devices.count == 0) {
mvwaddstr(win, 1, 1, "(no devices)");
y++;
}
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_calc *calc = fdev->calc;
if(fdev->status == FXLINK_FDEV_STATUS_CONNECTED) {
wattron(win, fmt_to_ncurses_attr(FMT_BGSELECTED));
mvwhline(win, y, 0, ' ', w);
}
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, fxlink_device_status_string(fdev));
mvwprintw(win, y, 21, "%04x:%04x", fdev->idVendor, fdev->idProduct);
if(calc) {
mvwaddstr(win, y, 32, fxlink_device_system_string(fdev));
wmove(win, y, 41);
for(int i = 0; i < calc->interface_count; i++)
wprintw(win, " %02x.%02x", calc->classes[i] >> 8,
calc->classes[i] & 0xff);
}
wattroff(win, fmt_to_ncurses_attr(FMT_BGSELECTED));
y++;
}
bool has_comms = false;
for(int i = 0; i < TUI.devices.count; i++)
has_comms = has_comms || (TUI.devices.devices[i].comm != NULL);
wmove(win, y+1, 1);
fprint(win, FMT_HEADER, "Device Status Serial IN OUT");
y += 2;
if(!has_comms) {
mvwaddstr(win, y, 1, "(no communications)");
}
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_calc *calc = fdev->calc;
struct fxlink_comm *comm = fdev->comm;
if(!comm)
continue;
if(y >= h) {
y++;
break;
}
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, fxlink_device_status_string(fdev));
mvwaddstr(win, y, 21, calc->serial ? calc->serial : "(null)");
mvwprintw(win, y, 31, "%02x%c", comm->ep_bulk_IN,
comm->tr_bulk_IN != NULL ? '*' : '.');
mvwprintw(win, y, 36, "%02x%c", comm->ep_bulk_OUT,
comm->tr_bulk_OUT != NULL ? '*' : '.');
y++;
}
if(y > h) {
wmove(win, h-1, w-6);
fprint(win, FMT_BGSELECTED, "(more)");
}
}
static void progress_bar(WINDOW *win, int width, int done, int total)
{
char const *ramp[9] = { " ", "", "", "", "", "", "", "", "" };
int progress = ((int64_t)done * width * 8) / total;
int percent = (int64_t)done * 100 / total;
wprintw(win, "%3d%% │", percent);
for(int i = 0; i < width; i++) {
int block_width = min(progress, 8);
waddstr(win, ramp[block_width]);
progress = max(progress-8, 0);
}
waddstr(win, "");
}
static void TUI_render_transfers(void)
{
WINDOW *win = TUI.wTransfers;
int y = 1;
werase(win);
wmove(win, 0, 1);
fprint(win, FMT_HEADER, "Device Dir. Size Progress");
bool has_transfers = false;
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_comm *comm = fdev->comm;
if(!comm)
continue;
struct fxlink_transfer *IN = comm->ftransfer_IN;
struct fxlink_transfer *OUT = comm->ftransfer_OUT;
if(IN) {
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, "IN");
mvwaddstr(win, y, 16, fxlink_size_string(IN->msg.size));
wmove(win, y, 26);
progress_bar(win, 32, IN->processed_size, IN->msg.size);
has_transfers = true;
y++;
}
if(OUT) {
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, "OUT");
mvwaddstr(win, y, 16, fxlink_size_string(OUT->msg.size));
has_transfers = true;
y++;
}
}
if(!has_transfers)
mvwaddstr(win, 1, 1, "(no transfers)");
}
static void TUI_render_all(bool with_borders)
{
if(with_borders) {
erase();
fxlink_TUI_render_borders(TUI.bRoot);
fxlink_TUI_render_titles(TUI.bRoot);
/* Title bar */
int w = getmaxx(stdscr);
attron(fmt_to_ncurses_attr(FMT_BGSELECTED));
mvhline(0, 0, ' ', w);
char const *str = "fxlink " FXLINK_VERSION " (TUI interactive mode)";
mvaddstr(0, w/2 - strlen(str)/2, str);
attroff(fmt_to_ncurses_attr(FMT_BGSELECTED));
}
TUI_render_status();
TUI_render_transfers();
}
static void TUI_SIGWINCH_handler(int sig)
{
(void)sig;
TUI.resize_needed = true;
}
static bool TUI_setup(void)
{
memset(&TUI, 0, sizeof TUI);
/* Set up the SINGWINCH handler */
struct sigaction WINCH;
sigaction(SIGWINCH, NULL, &WINCH);
WINCH.sa_handler = TUI_SIGWINCH_handler;
sigaction(SIGWINCH, &WINCH, NULL);
/* Initialize the main screen */
initscr();
start_color();
use_default_colors();
/* Set up our color pairs. These are FG/BG combinations from the FMT_*
enumerated colors in <fxlink/defs.h>, ordered BG-major. */
for(int bg = 0; bg < 9; bg++) {
for(int fg = 0; fg < 9; fg++)
init_pair(9*bg + fg, fg - 1, bg - 1);
}
if(!TUI_setup_windows())
return false;
/* Allow ncurses to scroll text-based windows*/
scrollok(TUI.wConsole, 1);
scrollok(TUI.wLogs, 1);
scrollok(TUI.wTextOutput, 1);
wmove(TUI.wConsole, 0, 0);
wmove(TUI.wLogs, 0, 0);
wmove(TUI.wTextOutput, 0, 0);
/* Make getch() non-blocking (though it also doesn't interpret escape
sequences anymore!) */
cbreak();
wtimeout(TUI.wLogs, 0);
wtimeout(TUI.wTextOutput, 0);
wtimeout(TUI.wConsole, 0);
/* Disable echo so we can edit input as it's being typed */
noecho();
return true;
}
static void TUI_quit(void)
{
TUI_free_windows();
endwin();
}
//---
// Interactive TUI
//---
static void handle_image(struct fxlink_message *msg, char const *path)
{
struct fxlink_message_image_header *img = msg->data;
char *filename = fxlink_gen_file_name(path, msg->type, ".png");
struct fxlink_message_image_raw *raw = fxlink_message_image_decode(msg);
if(raw) {
fxlink_libpng_save_raw(raw, filename);
fxlink_message_image_raw_free(raw);
log_("saved image (%dx%d, format=%d) to '%s'\n",
img->width, img->height, img->pixel_format, filename);
}
free(filename);
}
static void handle_text(struct fxlink_message *msg)
{
char const *str = msg->data;
WINDOW *win = TUI.wTextOutput;
if(options.verbose)
waddstr(win, "------------------\n");
waddnstr(win, str, msg->size);
if(options.verbose) {
if(str[msg->size - 1] != '\n')
waddch(win, '\n');
waddstr(win, "------------------\n");
}
if(options.verbose) {
for(size_t i = 0; i < msg->size; i++) {
print(win, " %02x", str[i]);
if((i & 15) == 15 || i == msg->size - 1)
print(win, "\n");
}
}
if(options.log_file)
fwrite(str, 1, msg->size, options.log_file);
}
static void handle_video(struct fxlink_message *msg)
{
struct fxlink_message_image_raw *raw = fxlink_message_image_decode(msg);
if(!raw)
return;
fxlink_sdl2_display_raw(raw);
fxlink_message_image_raw_free(raw);
}
static void fxlink_interactive_handle_message(struct fxlink_message *msg)
{
char const *path = ".";
if(fxlink_message_is_fxlink_image(msg))
return handle_image(msg, path);
if(fxlink_message_is_fxlink_text(msg))
return handle_text(msg);
if(fxlink_message_is_fxlink_video(msg))
return handle_video(msg);
/* Default to saving to a blob */
static char combined_type[48];
sprintf(combined_type, "%.16s-%.16s", msg->application, msg->type);
char *filename = fxlink_gen_file_name(path, combined_type, ".bin");
FILE *fp = fopen(filename, "wb");
if(!fp) {
elog("could not save to '%s': %m\n", filename);
return;
}
fwrite(msg->data, 1, msg->size, fp);
fclose(fp);
log_("saved blob to '%s'\n", filename);
free(filename);
}
static void handle_fxlink_log(int display_fmt, char const *str)
{
int attr = fmt_to_ncurses_attr(display_fmt);
wattron(TUI.wLogs, attr);
waddstr(TUI.wLogs, 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(fxlink_device_ready_to_connect(fdev)
&& fxlink_device_has_fxlink_interface(fdev)) {
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 */
fxlink_device_list_track(&TUI.devices, ctx);
/* Set up file descriptor tracking */
fxlink_pollfds_track(&TUI.polled_fds, ctx);
/* Initial render */
print(TUI.wConsole, "fxlink version %s (libusb/TUI interactive mode)\n",
FXLINK_VERSION);
char const *prompt = "> ";
print(TUI.wConsole, "%s", prompt);
TUI_render_all(true);
TUI_refresh_all(true);
fxlink_TUI_input_init(&TUI.input, TUI.wConsole, 16);
while(1) {
bool has_command;
bool activity = TUI_core_update(true, false, &has_command);
/* 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;
while((msg = fxlink_device_finish_bulk_IN(fdev))) {
fxlink_interactive_handle_message(msg);
fxlink_message_free(msg, true);
fxlink_device_start_bulk_IN(fdev);
}
}
/* Check for console commands */
if(has_command) {
char *command = TUI.input.data;
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);
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);
}
}
while(fxlink_device_list_interrupt(&TUI.devices))
libusb_handle_events(ctx);
fxlink_device_list_stop(&TUI.devices);
fxlink_pollfds_stop(&TUI.polled_fds);
fxlink_log_set_handler(NULL);
TUI_quit();
return 0;
}