diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f1bfa7..bbf59c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ endif() if(NOT FXLINK_DISABLE_SDL2) pkg_check_modules(sdl2 REQUIRED sdl2 IMPORTED_TARGET) endif() +pkg_check_modules(ncurses REQUIRED ncurses IMPORTED_TARGET) set(CMAKE_INSTALL_MESSAGE LAZY) set(SRC "${CMAKE_CURRENT_SOURCE_DIR}") @@ -28,7 +29,7 @@ add_executable(fxgxa fxgxa/dump.c fxgxa/edit.c fxgxa/file.c fxgxa/icon.c target_include_directories(fxgxa PUBLIC fxgxa/) target_link_libraries(fxgxa PkgConfig::libpng) -# fxg1a as a symlink (for compatibility= +# fxg1a as a symlink (for compatibility) add_custom_target(fxg1a ALL COMMAND ${CMAKE_COMMAND} -E create_symlink "fxgxa" "fxg1a") @@ -42,12 +43,31 @@ add_custom_command(OUTPUT "${BIN}/fxsdk.sh" add_custom_target(fxsdk ALL DEPENDS "${BIN}/fxsdk.sh") # fxlink -configure_file(fxlink/config.h.in "${BIN}/include/fxlink/config.h") -add_executable(fxlink fxlink/usb.c fxlink/filter.c fxlink/interactive.c fxlink/push.c - fxlink/main.c fxlink/png.c fxlink/properties.c fxlink/ud2.c fxlink/util.c - fxlink/protocol.c fxlink/sdl2.c) -target_link_libraries(fxlink PkgConfig::libpng PkgConfig::libusb) -target_include_directories(fxlink PRIVATE "${BIN}/include/fxlink") +configure_file(fxlink/include/fxlink/config.h.in + "${BIN}/include/fxlink/config.h") +add_executable(fxlink + fxlink/defs.c + fxlink/devices.c + fxlink/filter.c + fxlink/logging.c + fxlink/main.c + fxlink/protocol.c + fxlink/modes/interactive.c + fxlink/modes/list.c + fxlink/modes/push.c + fxlink/modes/tui-interactive.c + fxlink/modes/udisks2.c + fxlink/tooling/libpng.c + fxlink/tooling/sdl2.c + fxlink/tooling/udisks2.c + fxlink/tui/input.c + fxlink/tui/layout.c + fxlink/tui/render.c) +target_link_libraries(fxlink + PkgConfig::libpng PkgConfig::libusb PkgConfig::ncurses -lm) +target_include_directories(fxlink PRIVATE + "${BIN}/include" + "${SRC}/fxlink/include") if(NOT FXLINK_DISABLE_UDISKS2) target_link_libraries(fxlink PkgConfig::udisks2) endif() diff --git a/fxlink/config.h.in b/fxlink/config.h.in deleted file mode 100644 index 70a58ef..0000000 --- a/fxlink/config.h.in +++ /dev/null @@ -1,14 +0,0 @@ -//--- -// fxlink:config - Compile-time configuration -//--- - -#ifndef FXLINK_CONFIG_H -#define FXLINK_CONFIG_H - -/* Disables UDisks2 interfaces for systems that don't use it. */ -#cmakedefine FXLINK_DISABLE_UDISKS2 - -/* Disable SDL2 interfaces. */ -#cmakedefine FXLINK_DISABLE_SDL2 - -#endif /* FXLINK_CONFIG_H */ diff --git a/fxlink/defs.c b/fxlink/defs.c new file mode 100644 index 0000000..444574b --- /dev/null +++ b/fxlink/defs.c @@ -0,0 +1,148 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include +#include +#include +#include +#include +#include + +char const *fmt_to_ANSI(int format) +{ + static char buf[64]; + int n = 0; + + strcpy(buf, "\e[0m"); + n += 4; + + int FG = fmt_FG(format); + int BG = fmt_BG(format); + + if(FG != 0) + n += sprintf(buf+n, "\e[%dm", 30 + FG - 1); + if(BG != 0) + n += sprintf(buf+n, "\e[%dm", 40 + BG - 1); + if(fmt_BOLD(format)) + strcpy(buf+n, "\e[1m"), n += 4; + if(fmt_DIM(format)) + strcpy(buf+n, "\e[2m"), n += 4; + if(fmt_ITALIC(format)) + strcpy(buf+n, "\e[3m"), n += 4; + + return buf; +} + +char *fxlink_gen_file_name(char const *path, char const *name, + char const *suffix) +{ + char *filename = NULL; + int counter = 1; + + time_t time_raw; + struct tm time_bd; + time(&time_raw); + localtime_r(&time_raw, &time_bd); + + while(1) { + asprintf(&filename, "%s/fxlink-%.16s-%04d.%02d.%02d-%02dh%02d-%d%s", + path, name, time_bd.tm_year + 1900, time_bd.tm_mon + 1, + time_bd.tm_mday, time_bd.tm_hour, time_bd.tm_min, counter, suffix); + if(!filename) + continue; + + /* Try to find a name for a file that doesn't exist */ + if(access(filename, F_OK) == -1) + break; + + free(filename); + counter++; + } + + return filename; +} + +int fxlink_multipoll(int timeout, struct pollfd *fds1, int count1, ...) +{ + /* Convenience macro to iterate on file descriptor arrays */ + #define FOREACH_FD_ARRAY(FDS, COUNT, COUNT_ACC, BODY) do { \ + struct pollfd *FDS; int COUNT, COUNT_ACC; va_list args; \ + va_start(args, count1); \ + for(FDS = fds1, COUNT = count1, COUNT_ACC = 0; FDS; \ + COUNT_ACC += COUNT, \ + FDS = va_arg(args, struct pollfd *), \ + COUNT = va_arg(args, int)) { BODY } \ + va_end(args); } while(0) + + /* Determine total number of file descriptors to watch */ + int total = 0; + FOREACH_FD_ARRAY(fds, count, count_acc, { + total += count; + }); + + struct pollfd *concat = malloc(total * sizeof *concat); + if(!concat) + return -ENOMEM; + + /* Copy from individual arrays to full array */ + FOREACH_FD_ARRAY(fds, count, count_acc, { + memcpy(concat + count_acc, fds, count * sizeof *fds); + }); + + int rc = poll(concat, total, timeout); + + /* Copy back from full array to individual arrays */ + FOREACH_FD_ARRAY(fds, count, count_acc, { + memcpy(fds, concat + count_acc, count * sizeof *fds); + }); + + free(concat); + return rc; +} + +char const *fxlink_size_string(int bytes) +{ + static char str[32]; + + if(bytes > 1000000) + sprintf(str, "%d.%d MB", bytes / 1000000, (bytes % 1000000) / 100000); + else if(bytes > 1000) + sprintf(str, "%d.%d kB", bytes / 1000, (bytes % 1000) / 100); + else + sprintf(str, "%d B", bytes); + + return str; +} + +delay_t delay_none(void) +{ + return 0; +} +delay_t delay_seconds(int seconds) +{ + return seconds * 4; +} +delay_t delay_infinite(void) +{ + return -1; +} + +bool delay_cycle(delay_t *delay) +{ + if(*delay == 0) return true; + + struct timespec spec = { .tv_sec=0, .tv_nsec=250000000 }; + int rc; + + /* Account for interrupts in the nanosleep(2) call */ + struct timespec req = spec; + do rc = nanosleep(&req, &req); + while(rc == -1 && errno == EINTR); + + if(*delay > 0) (*delay)--; + return false; +} diff --git a/fxlink/devices.c b/fxlink/devices.c new file mode 100644 index 0000000..86e0986 --- /dev/null +++ b/fxlink/devices.c @@ -0,0 +1,812 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include +#include +#include +#include + +char const *fxlink_device_id(struct fxlink_device const *fdev) +{ + static char str[32]; + sprintf(str, "%03d:%03d", fdev->busNumber, fdev->deviceAddress); + return str; +} + +char const *fxlink_device_status_string(struct fxlink_device const *fdev) +{ + switch(fdev->status) { + case FXLINK_FDEV_STATUS_PENDING: + return "PENDING"; + case FXLINK_FDEV_STATUS_IGNORED: + return "IGNORED"; + case FXLINK_FDEV_STATUS_ERROR: + return "ERROR"; + case FXLINK_FDEV_STATUS_IDLE: + return "IDLE"; + case FXLINK_FDEV_STATUS_CONNECTED: + return "CONNECTED"; + default: + return ""; + } +} + +char const *fxlink_device_system_string(struct fxlink_device const *fdev) +{ + if(!fdev->calc) + return ""; + + switch(fdev->calc->system) { + case FXLINK_CALC_SYSTEM_UNKNOWN: + return "UNKNOWN"; + case FXLINK_CALC_SYSTEM_LINKSCSI: + return "LINKSCSI"; + case FXLINK_CALC_SYSTEM_CESG502: + return "CESG502"; + case FXLINK_CALC_SYSTEM_GINT: + return "GINT"; + default: + return ""; + } +} + +static bool is_casio_calculator(int idVendor, int idProduct) +{ + return idVendor == 0x07cf && (idProduct == 0x6101 || idProduct == 0x6102); +} + +static struct libusb_config_descriptor *active_config_descriptor( + struct fxlink_device const *fdev) +{ + struct libusb_config_descriptor *cd = NULL; + int rc = libusb_get_active_config_descriptor(fdev->dp, &cd); + if(rc != 0) { + if(rc != LIBUSB_ERROR_NOT_FOUND) + elog_libusb(rc, "cannot request config descriptor"); + return NULL; + } + return cd; +} + +/* Gather a list of a classes; either one is specified for the entire device in + the device descriptor, either one is specified for each interface in + interface descriptors. */ +static bool find_interface_classes(struct fxlink_device *fdev) +{ + struct fxlink_calc *calc = fdev->calc; + struct libusb_device_descriptor dc; + libusb_get_device_descriptor(fdev->dp, &dc); + + /* Fixed class specified in the device descriptor */ + if(dc.bDeviceClass != LIBUSB_CLASS_PER_INTERFACE) { + calc->interface_count = 1; + calc->classes = malloc(1 * sizeof *calc->classes); + calc->classes[0] = (dc.bDeviceClass << 8) | dc.bDeviceSubClass; + return true; + } + + /* Class specified by the interface descriptors */ + struct libusb_config_descriptor *cd = active_config_descriptor(fdev); + if(!cd) + return false; + + calc->interface_count = cd->bNumInterfaces; + calc->classes = malloc(cd->bNumInterfaces * sizeof *calc->classes); + + for(int i = 0; i < cd->bNumInterfaces; i++) { + struct libusb_interface const *intf = &cd->interface[i]; + struct libusb_interface_descriptor const *id = &intf->altsetting[0]; + + calc->classes[i] = (id->bInterfaceClass << 8) | id->bInterfaceSubClass; + if(calc->classes[i] == 0xff77 && calc->fxlink_inum < 0) + calc->fxlink_inum = i; + } + return true; +} + +/* Find the endpoints for the fxlink interface. */ +static bool find_fxlink_endpoints(struct fxlink_device *fdev, bool quiet) +{ + struct fxlink_calc *calc = fdev->calc; + struct fxlink_comm *comm = calloc(1, sizeof *comm); + if(!comm) { + elog("find_fxlink_endpoints(): %m\n"); + return false; + } + fdev->comm = comm; + comm->ep_bulk_IN = 0xff; + comm->ep_bulk_OUT = 0xff; + + struct libusb_config_descriptor *cd = active_config_descriptor(fdev); + if(!cd) { + free(comm); + return false; + } + + struct libusb_interface const *intf = &cd->interface[calc->fxlink_inum]; + struct libusb_interface_descriptor const *id = &intf->altsetting[0]; + + if(!quiet) { + hlog("calculators %s", fxlink_device_id(fdev)); + log_("fxlink interface has %d endpoints\n", id->bNumEndpoints); + } + + for(int i = 0; i < id->bNumEndpoints; i++) { + struct libusb_endpoint_descriptor const *ed = &id->endpoint[i]; + + int dir = ed->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK; + int type = ed->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK; + + if(dir == LIBUSB_ENDPOINT_OUT + && type == LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK) + comm->ep_bulk_OUT = ed->bEndpointAddress; + if(dir == LIBUSB_ENDPOINT_IN + && type == LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK) + comm->ep_bulk_IN = ed->bEndpointAddress; + } + + return true; +} + +/* Determine system based on interface heuristics */ +static void determine_system_type(struct fxlink_device *fdev) +{ + struct fxlink_calc *calc = fdev->calc; + if(!calc) + return; + + calc->system = FXLINK_CALC_SYSTEM_UNKNOWN; + + /* Single class of type SCSI -> LINKSCSI */ + if(calc->interface_count == 1 && calc->classes[0] == 0x0806) { + calc->system = FXLINK_CALC_SYSTEM_LINKSCSI; + return; + } + /* Single class of type vendor-specific 00 -> CESG502 */ + if(calc->interface_count == 1 && calc->classes[0] == 0xff00) { + calc->system = FXLINK_CALC_SYSTEM_CESG502; + return; + } + /* Has an fxlink interface -> GINT */ + for(int i = 0; i < calc->interface_count; i++) { + if(calc->classes[i] == 0xff77) { + calc->system = FXLINK_CALC_SYSTEM_GINT; + return; + } + } +} + +/* Retrieve the serial. Returns a duplicated copy, NULL on error. */ +static char *retrieve_serial_number(struct fxlink_device *fdev) +{ + if(!fdev->dh) + return NULL; + + struct libusb_device_descriptor dc; + libusb_get_device_descriptor(fdev->dp, &dc); + + if(!dc.iSerialNumber) + return NULL; + + char str[256]; + int rc = libusb_get_string_descriptor_ascii(fdev->dh, dc.iSerialNumber, + (void *)str, 256); + if(rc < 0) { + wlog_libusb(rc, "could not retrieve serial number"); + return NULL; + } + + /* LINK sends a 12-byte serial number with four leading 0. Remove them */ + char *serial = str; + if(rc == 12 && !strncmp(serial, "0000", 4)) + serial += 4; + return strdup(serial); +} + +void fxlink_device_analysis_1(struct fxlink_device *fdev, bool quiet) +{ + struct fxlink_calc *calc = calloc(1, sizeof *calc); + if(!calc) { + elog("analyze_calculator(): %m\n"); + return; + } + + fdev->calc = calc; + calc->system = FXLINK_CALC_SYSTEM_UNKNOWN; + calc->fxlink_inum = -1; + if(!find_interface_classes(fdev)) { + fdev->status = FXLINK_FDEV_STATUS_ERROR; + return; + } + + if(!quiet) { + hlog("calculators %s", fxlink_device_id(fdev)); + log_("%1$d interface%2$s, class code%2$s", calc->interface_count, + calc->interface_count != 1 ? "s" : ""); + + for(int i = 0; i < calc->interface_count; i++) { + log_(" %02x.%02x", calc->classes[i] >> 8, calc->classes[i] & 0xff); + if(i == calc->fxlink_inum) + log_("(*)"); + } + log_("\n"); + } + + determine_system_type(fdev); + + /* Don't open SCSI devices because we can't interact with them (the kernel + already claims the interface) and doing so will prevent them from + subspending and disconnecting */ + if(calc->system == FXLINK_CALC_SYSTEM_LINKSCSI) { + fdev->status = FXLINK_FDEV_STATUS_IGNORED; + return; + } + + if(calc->fxlink_inum >= 0 && !find_fxlink_endpoints(fdev, quiet)) { + hlog("calculators %s", fxlink_device_id(fdev)); + elog("non-conforming fxlink interface!\n"); + fdev->status = FXLINK_FDEV_STATUS_ERROR; + return; + } + + int rc = libusb_open(fdev->dp, &fdev->dh); + + if(!quiet) + hlog("calculators %s", fxlink_device_id(fdev)); + if(rc != 0) { + elog("opening device failed: %s\n", libusb_strerror(rc)); + fdev->status = FXLINK_FDEV_STATUS_ERROR; + return; + } + if(!quiet) + log_("successfully opened device!\n"); + + /* Don't detach kernel drivers to avoid breaking the Mass Storage + communications if fxlink is ever started while the native LINK + application is running! */ + libusb_set_auto_detach_kernel_driver(fdev->dh, false); +} + +void fxlink_device_analysis_2(struct fxlink_device *fdev) +{ + if(fdev->status != FXLINK_FDEV_STATUS_PENDING) + return; + + fdev->calc->serial = retrieve_serial_number(fdev); + fdev->status = FXLINK_FDEV_STATUS_IDLE; +} + +bool fxlink_device_claim_fxlink(struct fxlink_device *fdev) +{ + /* Only connect to calculators with an fxlink interface */ + if(!fdev->comm || fdev->status != FXLINK_FDEV_STATUS_IDLE) + return false; + + /* Allocate transfer data */ + fdev->comm->buffer_IN_size = 2048; + fdev->comm->buffer_IN = malloc(fdev->comm->buffer_IN_size); + if(!fdev->comm->buffer_IN) { + elog("could not allocate buffer for bulk IN transfer\n"); + fdev->status = FXLINK_FDEV_STATUS_ERROR; + return false; + } + + hlog("calculators %s", fxlink_device_id(fdev)); + int rc = libusb_claim_interface(fdev->dh, fdev->calc->fxlink_inum); + if(rc != 0) { + elog_libusb(rc, "claiming fxlink interface #%d failed", + fdev->calc->fxlink_inum); + fdev->status = FXLINK_FDEV_STATUS_ERROR; + return false; + } + + fdev->comm->claimed = true; + fdev->status = FXLINK_FDEV_STATUS_CONNECTED; + log_("successfully claimed interface!\n"); + return true; +} + +void fxlink_device_get_properties(struct fxlink_device const *fdev, + struct fxlink_filter *p) +{ + memset(p, 0, sizeof *p); + + if(fdev->idProduct == 0x6101) + p->p7 = true; + if(fdev->idProduct == 0x6102) + p->mass_storage = true; + if(fdev->calc) { + p->serial = fdev->calc->serial; + p->intf_cesg502 = (fdev->calc->system == FXLINK_CALC_SYSTEM_CESG502); + p->intf_fxlink = (fdev->calc->fxlink_inum >= 0); + } +} + +void fxlink_device_interrupt_transfers(struct fxlink_device *fdev) +{ + struct fxlink_comm *comm = fdev->comm; + if(!comm) + return; + + /* Interrupt transfers if any are still running */ + if(comm->tr_bulk_IN && !comm->cancelled_IN) { + libusb_cancel_transfer(comm->tr_bulk_IN); + comm->cancelled_IN = true; + } + if(comm->tr_bulk_OUT && !comm->cancelled_OUT) { + libusb_cancel_transfer(comm->tr_bulk_OUT); + comm->cancelled_OUT = true; + } +} + +void fxlink_device_cleanup(struct fxlink_device *fdev) +{ + /* Close the device if it's open */ + if(fdev->dh) { + /* Release the fxlink interface if it's claimed */ + if(fdev->comm && fdev->comm->claimed) + libusb_release_interface(fdev->dh, fdev->calc->fxlink_inum); + + libusb_close(fdev->dh); + fdev->dh = NULL; + } + + /* Free associated memory */ + if(fdev->calc) { + free(fdev->calc->classes); + free(fdev->calc->serial); + free(fdev->calc); + } + free(fdev->comm); + + /* Unreference libusb devices so it can also be freed */ + libusb_unref_device(fdev->dp); + fdev->dp = NULL; +} + +//--- +// Bulk transfers +//--- + +/* Note: this function is run by the even handler and can't do any crazy libusb + stuff like sync I/O or getting descriptors. */ +static void bulk_IN_callback(struct libusb_transfer *transfer) +{ + struct fxlink_device *fdev = transfer->user_data; + struct fxlink_comm *comm = fdev->comm; + + bool resubmit = false; + void *data = transfer->buffer; + int data_size = transfer->actual_length; + + if(transfer->status != LIBUSB_TRANSFER_COMPLETED) + hlog("calculators %s", fxlink_device_id(fdev)); + + switch(transfer->status) { + case LIBUSB_TRANSFER_COMPLETED: + /* Start or continue an fxlink transfer. When finished, don't resubmit, + instead have the user pick up the message before continuing. */ + resubmit = true; + if(comm->ftransfer_IN) { + fxlink_transfer_receive(comm->ftransfer_IN, data, data_size); + if(fxlink_transfer_complete(comm->ftransfer_IN)) + resubmit = false; + } + else { + comm->ftransfer_IN = fxlink_transfer_make_IN(data, data_size); + } + break; + + /* Error: drop data and don't resubmit */ + case LIBUSB_TRANSFER_ERROR: + log_("transfer error (%d bytes dropped)\n", data_size); + break; + /* Timeout: drop data, but resubmit */ + case LIBUSB_TRANSFER_TIMED_OUT: + log_("transfer timed out (%d bytes dropped)\n", data_size); + break; + /* Cancelled transfer: drop data and don't try to resubmit */ + case LIBUSB_TRANSFER_CANCELLED: + log_("transfer cancelled (%d bytes dropped)\n", data_size); + break; + /* Stall: treat as an error */ + case LIBUSB_TRANSFER_STALL: + log_("transfer stalled (%d bytes dropped)\n", data_size); + break; + /* Overflow: treat as an error (should not happen because we set our buffer + size to a multiple of the maximum packet size) */ + case LIBUSB_TRANSFER_OVERFLOW: + log_("transfer overflowed (%d bytes dropped)\n", data_size); + break; + /* No device: this is normal */ + case LIBUSB_TRANSFER_NO_DEVICE: + log_("stop listening (calculator disconnected)\n"); + break; + } + + /* Resubmit transfer so we can get new data as soon as possible */ + if(resubmit) { + libusb_submit_transfer(comm->tr_bulk_IN); + } + else { + libusb_free_transfer(comm->tr_bulk_IN); + comm->tr_bulk_IN = NULL; + } +} + +void fxlink_device_start_bulk_IN(struct fxlink_device *fdev) +{ + if(!fdev->comm || !fdev->comm->claimed || fdev->comm->tr_bulk_IN) + return; + + fdev->comm->tr_bulk_IN = libusb_alloc_transfer(0); + if(!fdev->comm->tr_bulk_IN) { + elog("allocation of bulk IN transfer failed\n"); + return; + } + + libusb_fill_bulk_transfer(fdev->comm->tr_bulk_IN, + fdev->dh, /* Device handle */ + fdev->comm->ep_bulk_IN, /* Endpoint */ + fdev->comm->buffer_IN, /* Buffer */ + fdev->comm->buffer_IN_size, /* Buffer size */ + bulk_IN_callback, fdev, /* Callback function and argument */ + -1 /* Timeout */ + ); + + int rc = libusb_submit_transfer(fdev->comm->tr_bulk_IN); + if(rc < 0) { + elog("bulk IN transfer failed to submit: %s\n", libusb_strerror(rc)); + fdev->status = FXLINK_FDEV_STATUS_ERROR; + return; + } + + // hlog("calculators %s", fxlink_device_id(fdev)); + // log_("submitted new IN transfer (no timeout)\n"); +} + +struct fxlink_message *fxlink_device_finish_bulk_IN(struct fxlink_device *fdev) +{ + struct fxlink_comm *comm = fdev->comm; + + if(!comm || !comm->ftransfer_IN) + return NULL; + if(!fxlink_transfer_complete(comm->ftransfer_IN)) + return NULL; + + struct fxlink_message *msg = fxlink_transfer_finish_IN(comm->ftransfer_IN); + if(!msg) + return NULL; + + int version_major = (msg->version >> 8) & 0xff; + int version_minor = msg->version & 0xff; + + hlog("calculators %s", fxlink_device_id(fdev)); + log_("new message (v%d.%d): %.16s:%.16s, %s\n", + version_major, version_minor, + msg->application, msg->type, fxlink_size_string(msg->size)); + comm->ftransfer_IN = NULL; + return msg; +} + +//--- +// Polled file descriptor tracking +//--- + +static void generate_poll_fds(struct fxlink_pollfds *tracker) +{ + /* Get the set of libusb file descriptors to poll for news */ + struct libusb_pollfd const **usb_fds = libusb_get_pollfds(tracker->ctx); + int usb_n = 0; + + if(!usb_fds) { + elog("libusb_get_pollfds() returned NULL, devices will probably not " + "be detected!\n"); + free(tracker->fds); + tracker->fds = NULL; + tracker->count = 0; + return; + } + + hlog("libusb"); + log_("fds to poll:"); + for(int i = 0; usb_fds[i] != NULL; i++) { + log_(" %d(%s%s)", + usb_fds[i]->fd, + (usb_fds[i]->events & POLLIN) ? "i" : "", + (usb_fds[i]->events & POLLOUT) ? "o" : ""); + } + if(!usb_fds[0]) + log_(" (none)"); + log_("\n"); + + while(usb_fds[usb_n]) + usb_n++; + + /* Allocate a bunch of `struct pollfd` and also monitor STDIN_FILENO */ + tracker->count = usb_n; + tracker->fds = realloc(tracker->fds, usb_n * sizeof *tracker->fds); + + for(int i = 0; i < usb_n; i++) { + tracker->fds[i].fd = usb_fds[i]->fd; + tracker->fds[i].events = usb_fds[i]->events; + } +} + +static void handle_add_poll_fd(int fd, short events, void *data) +{ + (void)fd; + (void)events; + generate_poll_fds(data); +} + +static void handle_remove_poll_fd(int fd, void *data) +{ + (void)fd; + generate_poll_fds(data); +} + +void fxlink_pollfds_track(struct fxlink_pollfds *tracker, libusb_context *ctx) +{ + memset(tracker, 0, sizeof *tracker); + tracker->ctx = ctx; + + libusb_set_pollfd_notifiers(ctx, handle_add_poll_fd, handle_remove_poll_fd, + tracker); + generate_poll_fds(tracker); +} + +void fxlink_pollfds_stop(struct fxlink_pollfds *tracker) +{ + libusb_set_pollfd_notifiers(tracker->ctx, NULL, NULL, NULL); + free(tracker->fds); + memset(tracker, 0, sizeof *tracker); +} + +//--- +// Device tracking +//--- + +static void enumerate_devices(libusb_context *ctx, + struct fxlink_device_list *list) +{ + libusb_device **libusb_list = NULL; + int new_count = libusb_get_device_list(ctx, &libusb_list); + + if(new_count < 0) { + elog("libusb_get_device_list() failed with error %d\n", new_count); + return; + } + + /* We now diff the previous array with the current one */ + struct fxlink_device *new_fdevs = calloc(new_count, sizeof *new_fdevs); + if(!new_fdevs) { + elog("enumerate_devices(): %m\n"); + return; + } + int k = 0; + + /* First copy over any device that is still connected */ + for(int i = 0; i < list->count; i++) { + struct fxlink_device *fdev = &list->devices[i]; + assert(fdev->dp != NULL); + + bool still_connected = false; + for(int j = 0; j < new_count; j++) + still_connected = still_connected || (libusb_list[j] == fdev->dp); + + if(still_connected) + new_fdevs[k++] = list->devices[i]; + else { + fxlink_device_cleanup(fdev); + hlog("devices %s", fxlink_device_id(fdev)); + log_("disconnected\n"); + } + } + + /* Then add all the new ones */ + for(int j = 0; j < new_count; j++) { + libusb_device *dp = libusb_list[j]; + struct libusb_device_descriptor dc; + + bool already_known = false; + for(int i = 0; i < list->count; i++) + already_known = already_known || (list->devices[i].dp == dp); + if(already_known) + continue; + + libusb_ref_device(dp); + libusb_get_device_descriptor(dp, &dc); + + new_fdevs[k].dp = dp; + new_fdevs[k].status = FXLINK_FDEV_STATUS_PENDING; + new_fdevs[k].busNumber = libusb_get_bus_number(dp); + new_fdevs[k].deviceAddress = libusb_get_device_address(dp); + new_fdevs[k].idVendor = dc.idVendor; + new_fdevs[k].idProduct = dc.idProduct; + new_fdevs[k].calc = NULL; + new_fdevs[k].comm = NULL; + + hlog("devices %s", fxlink_device_id(&new_fdevs[k])); + + if(is_casio_calculator(dc.idVendor, dc.idProduct)) { + log_("new CASIO calculator (%04x:%04x)\n", + dc.idVendor, dc.idProduct); + fxlink_device_analysis_1(&new_fdevs[k], false); + } + else { + log_("new non-CASIO-calculator device (%04x:%04x)\n", + dc.idVendor, dc.idProduct); + new_fdevs[k].status = FXLINK_FDEV_STATUS_IGNORED; + } + k++; + } + + assert(k == new_count); + free(list->devices); + list->devices = new_fdevs; + list->count = new_count; + libusb_free_device_list(libusb_list, true); +} + +static int handle_hotplug(libusb_context *ctx, libusb_device *device, + libusb_hotplug_event event, void *user_data) +{ + /* Note that due to threading considerations in libusb, a device may be + notified for hotplugging twice, or may depart without ever having been + notified for arrival. */ + (void)device; + + if(event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) + enumerate_devices(ctx, user_data); + else if(event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) + enumerate_devices(ctx, user_data); + else + wlog("unhandled libusb hotplug event of type %d\n", event); + + return 0; +} + +bool fxlink_device_list_track(struct fxlink_device_list *list, + libusb_context *ctx) +{ + memset(list, 0, sizeof *list); + + if(!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + elog("libusb doesn't handle hotplug; devices may not be detected\n"); + return false; + } + + list->ctx = ctx; + libusb_hotplug_register_callback(ctx, + /* Both arriving and departing devices */ + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, + /* Perform an initial enumeration right now */ + LIBUSB_HOTPLUG_ENUMERATE, + LIBUSB_HOTPLUG_MATCH_ANY, /* vendorId */ + LIBUSB_HOTPLUG_MATCH_ANY, /* productId */ + LIBUSB_HOTPLUG_MATCH_ANY, /* deviceClass */ + handle_hotplug, list, &list->hotplug_handle); + + return true; +} + +void fxlink_device_list_refresh(struct fxlink_device_list *list) +{ + for(int i = 0; i < list->count; i++) { + struct fxlink_device *fdev = &list->devices[i]; + /* Finish analysis */ + if(fdev->calc && fdev->status == FXLINK_FDEV_STATUS_PENDING) + fxlink_device_analysis_2(fdev); + } +} + +bool fxlink_device_list_interrupt(struct fxlink_device_list *list) +{ + bool still_running = false; + + for(int i = 0; i < list->count; i++) { + struct fxlink_device *fdev = &list->devices[i]; + fxlink_device_interrupt_transfers(fdev); + + still_running |= fdev->comm && fdev->comm->tr_bulk_IN; + still_running |= fdev->comm && fdev->comm->tr_bulk_OUT; + } + + return still_running; +} + +void fxlink_device_list_stop(struct fxlink_device_list *list) +{ + if(!list->ctx) + return; + + libusb_hotplug_deregister_callback(list->ctx, list->hotplug_handle); + + /* Now free the device list proper */ + for(int i = 0; i < list->count; i++) + fxlink_device_cleanup(&list->devices[i]); + + free(list->devices); + memset(list, 0, sizeof *list); +} + +//--- +// Simplified device enumeration +//--- + +struct fxlink_device *fxlink_device_find(libusb_context *ctx, + struct fxlink_filter const *filter) +{ + libusb_device **list = NULL; + int count = libusb_get_device_list(ctx, &list); + struct fxlink_device *fdev = NULL; + + if(count < 0) { + elog("libusb_get_device_list() failed with error %d\n", count); + return NULL; + } + + /* Look for any suitable calculator */ + for(int i = 0; i < count; i++) { + libusb_device *dp = list[i]; + struct libusb_device_descriptor dc; + + libusb_get_device_descriptor(dp, &dc); + if(!is_casio_calculator(dc.idVendor, dc.idProduct)) + continue; + + /* Since we found a calculator, make the fdev and check further */ + fdev = calloc(1, sizeof *fdev); + if(!fdev) { + elog("failed to allocate device structure: %m\n"); + continue; + } + + libusb_ref_device(dp); + fdev->dp = dp; + fdev->status = FXLINK_FDEV_STATUS_PENDING; + fdev->busNumber = libusb_get_bus_number(dp); + fdev->deviceAddress = libusb_get_device_address(dp); + fdev->idVendor = dc.idVendor; + fdev->idProduct = dc.idProduct; + fdev->calc = NULL; + fdev->comm = NULL; + fxlink_device_analysis_1(fdev, true); + fxlink_device_analysis_2(fdev); + + if(fdev->status == FXLINK_FDEV_STATUS_IDLE) { + /* Check the filter */ + struct fxlink_filter properties; + fxlink_device_get_properties(fdev, &properties); + + /* Success: return that device (no interfaces claimed) */ + if(fxlink_filter_match(&properties, filter)) + break; + } + + /* Failure: free it and try the next one */ + fxlink_device_cleanup(fdev); + free(fdev); + fdev = NULL; + } + + libusb_free_device_list(list, true); + return fdev; +} + +struct fxlink_device *fxlink_device_find_wait(libusb_context *ctx, + struct fxlink_filter const *filter, delay_t *delay) +{ + while(true) { + struct fxlink_device *fdev = fxlink_device_find(ctx, filter); + if(fdev) + return fdev; + if(delay_cycle(delay)) + return NULL; + } +} diff --git a/fxlink/filter.c b/fxlink/filter.c index ecac728..530ca7e 100644 --- a/fxlink/filter.c +++ b/fxlink/filter.c @@ -1,184 +1,153 @@ -#include "filter.h" -#include "util.h" +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include +#include #include #include -#include //--- -// Property parser +// Filter parser //--- -/* skip_spaces(): Skip spaces, returns true if end of string is reached */ -bool skip_spaces(char const **input) +/* Identify property separating characters to be skipped */ +static bool issep(int c) { - while(isspace(**input)) (*input)++; - return (**input == 0); + return (c == ' ' || c == '\t' || c == '\n' || c == ','); } -/* isword(): Identify valid word characters for the filter */ -bool isword(int c) +/* Identify valid word characters for the filter */ +static bool isword(int c) { - return c && !strchr(" \t\n,;=", c); + return c && !strchr(" \t\n,=", c); } -/* read_word(): Copy the next word in the string, assumes word is non-empty */ -char *read_word(char const **input) +/* Copy the next word in the string, assumes word is non-empty */ +static char *read_word(char const **input) { char const *str = *input; while(**input && isword(**input)) (*input)++; return strndup(str, *input - str); } -enum { - T_END, /* End of string */ - T_PROP, /* Property; (*name) and (*value) are set */ - T_COMMA = ',', /* Comma character (property separator) */ - T_SEMI = ';', /* Semicolon character (option separator) */ -}; - -/* lex(): Read a token from the input source - Returns the token type, updates (*input) and sets (*name) and (*value) to - either NULL or some freshly-allocated copies of the name and (optional) - value of the property (always NULL unless T_PROP is returned). The caller - should free() both. */ -static int lex(char const **input, char **name, char **value) +/* Reads a property from the input source. Advances *input and sets *name and + *value to newly-allocated copies of the name and (optional) value of the + property (T_PROP). Both should be free()'d after use. At the end of the + input, returns false and sets *name = *value = NULL. */ +static bool read_property(char const **input, char **name, char **value) { *name = *value = NULL; - if(skip_spaces(input)) return T_END; - if(**input == ',' || **input == ';') { + while(issep(**input)) (*input)++; - return (*input)[-1]; - } + if(!**input) + return false; if(!isword(**input)) { - wrn("expected property name in filter, skipping '%c'", **input); - (*input)++; - return lex(input, name, value); + elog("expected property name in filter, found '%c'; stopping\n", + **input); + return false; } *name = read_word(input); - if(skip_spaces(input) || **input != '=') - return T_PROP; - (*input)++; - if(skip_spaces(input)) - wrn("no value after '=' in filter property '%s'", *name); - else if(!isword(**input)) - wrn("ignoring invalid value for filter property '%s'", *name); - else + if(**input == '=') { + (*input)++; *value = read_word(input); - return T_PROP; + } + return true; } -filter_t *filter_parse(char const *input) +struct fxlink_filter *fxlink_filter_parse(char const *input) { char *name=NULL, *value=NULL; - int t; - - /* Create an initial filter with a single otion */ - filter_t *filter = malloc(sizeof *filter); - if(!filter) return NULL; - filter->options = calloc(1, sizeof(properties_t)); - if(!filter->options) { - free(filter); + struct fxlink_filter *filter = calloc(1, sizeof *filter); + if(!filter) return NULL; - } - filter->length = 1; - - /* Current option */ - properties_t *option = &filter->options[0]; - - while((t = lex(&input, &name, &value)) != T_END) { - /* Ignore property separators (tokens are already separated) */ - if(t == ',') continue; - - /* Add a new option in the filter */ - if(t == ';') { - size_t new_size = (filter->length + 1) * sizeof(properties_t); - properties_t *new_options = realloc(filter->options, new_size); - if(!new_options) continue; - - filter->options = new_options; - option = &filter->options[filter->length++]; - *option = (properties_t){ 0 }; - continue; - } + while(read_property(&input, &name, &value)) { /* Add a new property to the current option */ if(!strcmp(name, "p7") && !value) - option->p7 = true; + filter->p7 = true; else if(!strcmp(name, "mass_storage") && !value) - option->mass_storage = true; + filter->mass_storage = true; else if(!strcmp(name, "series_cg") && !value) - option->series_cg = true; + filter->series_cg = true; else if(!strcmp(name, "series_g3") && !value) - option->series_g3 = true; - else if(!strcmp(name, "serial_number") && value) { - option->serial_number = strdup(value); - } - else wrn("ignoring invalid filter property: '%s' %s value", name, - value ? "with" : "without"); + filter->series_g3 = true; + else if(!strcmp(name, "intf_fxlink") && !value) + filter->intf_fxlink = true; + else if(!strcmp(name, "intf_cesg502") && !value) + filter->intf_cesg502 = true; + else if(!strcmp(name, "serial") && value) + filter->serial = strdup(value); + else if(!strcmp(name, "serial_number") && value) // Old name + filter->serial = strdup(value); + else wlog("ignoring invalid filter property: '%s' (%s value)\n", + name, value ? "with" : "without"); free(name); free(value); } - return filter; } -void filter_free(filter_t *filter) -{ - if(!filter) return; - - for(size_t i = 0; i < filter->length; i++) - free(filter->options[i].serial_number); - - free(filter->options); - free(filter); -} - //--- // Filtering API //--- -void filter_clean_libusb(filter_t *filter) +void fxlink_filter_clean_libusb(struct fxlink_filter *filter) { - if(!filter) return; + if(!filter) + return; - for(size_t i = 0; i < filter->length; i++) { - properties_t *prop = &filter->options[i]; - - /* Suppress series_cg and series_g3, which are based off the USB Mass - Storage metadata provided only by UDisks2 */ - if(prop->series_cg) { - wrn("ignoring series_cg in libusb filter (cannot be detected)"); - prop->series_cg = false; - } - if(prop->series_g3) { - wrn("ignoring series_g3 in libusb filter (cannot be detected)"); - prop->series_g3 = false; - } + /* Suppress series_cg and series_g3, which are based off the SCSI metadata + provided only to UDisks2 */ + if(filter->series_cg) { + wlog("ignoring series_cg in libusb filter (cannot be detected)\n"); + filter->series_cg = false; + } + if(filter->series_g3) { + wlog("ignoring series_g3 in libusb filter (cannot be detected)\n"); + filter->series_g3 = false; } } -void filter_clean_udisks2(filter_t *filter) +void fxlink_filter_clean_udisks2(struct fxlink_filter *filter) { /* Every property can be used */ (void)filter; } -bool filter_match(properties_t const *props, filter_t const *filter) +bool fxlink_filter_match( + struct fxlink_filter const *props, + struct fxlink_filter const *filter) { /* No filter is a pass-through */ - if(!filter || !filter->length) + if(!filter) return true; - for(size_t i = 0; i < filter->length; i++) { - if(properties_match(props, &filter->options[i])) - return true; - } - return false; + if(filter->p7 && !props->p7) + return false; + if(filter->mass_storage && !props->mass_storage) + return false; + if(filter->intf_fxlink && !props->intf_fxlink) + return false; + if(filter->intf_cesg502 && !props->intf_cesg502) + return false; + if(filter->series_cg && !props->series_cg) + return false; + if(filter->series_g3 && !props->series_g3) + return false; + if(filter->serial && + (!props->serial || strcmp(filter->serial, props->serial))) + return false; + + return true; } -void filter_print(FILE *fp, filter_t const *filter) +void fxlink_filter_print(FILE *fp, struct fxlink_filter const *filter) { #define output(...) { \ if(sep) fprintf(fp, ", "); \ @@ -186,9 +155,26 @@ void filter_print(FILE *fp, filter_t const *filter) sep = true; \ } - for(size_t i = 0; i < filter->length; i++) { - if(i > 0) printf("; "); - properties_t *prop = &filter->options[i]; - properties_print(fp, prop); - } + bool sep = false; + if(filter->p7) + output("p7"); + if(filter->mass_storage) + output("mass_storage"); + if(filter->intf_fxlink) + output("intf_fxlink"); + if(filter->intf_cesg502) + output("intf_cesg502"); + if(filter->series_cg) + output("series_cg"); + if(filter->series_g3) + output("series_g3"); + if(filter->serial) + output("serial=%s", filter->serial); +} + +void fxlink_filter_free(struct fxlink_filter *filter) +{ + if(filter) + free(filter->serial); + free(filter); } diff --git a/fxlink/filter.h b/fxlink/filter.h deleted file mode 100644 index e3ad861..0000000 --- a/fxlink/filter.h +++ /dev/null @@ -1,49 +0,0 @@ -//--- -// fxlink:filter - Property-based device filtering -//--- - -#ifndef FXLINK_FILTER_H -#define FXLINK_FILTER_H - -#include "properties.h" -#include -#include - -/* filter_t: An OR-combination of property filters - - Attributes of properties_t objects have AND-semantics when used as filters; - all of them must match. For more flexibility, the command-line allows the - user to specify an OR-combination of such filters, called "options". */ -typedef struct { - /* Array of options to be matched against; not terminated. */ - properties_t *options; - /* Length of (options). */ - size_t length; - -} filter_t; - -/* Return values for backend-specific matching functions */ -enum { - FILTER_UNIQUE = 0, - FILTER_NONE = 1, - FILTER_MULTIPLE = 2, - FILTER_ERROR = 3, -}; - -/* filter_parse(): Parse a filter string */ -filter_t *filter_parse(char const *specification); -/* filter_free(): Free a created by filter_parse() */ -void filter_free(filter_t *filter); - -/* filter_clean_libusb(): Disable filter properties unsupported for libusb */ -void filter_clean_libusb(filter_t *filter); -/* filter_clean_udisks2(): Disable filter properties unsupported for udisks2 */ -void filter_clean_udisks2(filter_t *filter); - -/* filter_match(): Check whether some properties match the supplied filter */ -bool filter_match(properties_t const *props, filter_t const *filter); - -/* filter_print(): Print a parser filter (one-line; for debugging) */ -void filter_print(FILE *fp, filter_t const *filter); - -#endif /* FXLINK_FILTER_H */ diff --git a/fxlink/fxlink.h b/fxlink/fxlink.h index 08399eb..841df57 100644 --- a/fxlink/fxlink.h +++ b/fxlink/fxlink.h @@ -1,36 +1,42 @@ -//--- -// fxlink:fxlink - Application logic -//--- - -#ifndef FXLINK_FXLINK_H -#define FXLINK_FXLINK_H +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.fxlink: Options and mode functions +#pragma once +#include #include -#include "filter.h" -#include "util.h" -struct fxlink_options -{ - bool quiet; - bool force_unmount; +/* Global and command-line options. */ +struct fxlink_options { + /* If not NULL, gets a copy of all text messages received in either + interactive mode */ FILE *log_file; + /* Extra details (mainly interactive messages) */ + bool verbose; }; extern struct fxlink_options options; /* Main function for -l */ -int main_list(filter_t *filter, delay_t *delay, libusb_context *context); +int main_list(struct fxlink_filter *filter, delay_t *delay, + libusb_context *context); /* Main function for -b */ -int main_blocks(filter_t *filter, delay_t *delay); +int main_blocks(struct fxlink_filter *filter, delay_t *delay); /* Main function for -s */ -int main_send(filter_t *filter, delay_t *delay, char **files); +int main_send(struct fxlink_filter *filter, delay_t *delay, char **files); /* Main function for -i */ -int main_interactive(filter_t *filter, delay_t *delay, libusb_context *context); +int main_interactive(struct fxlink_filter *filter, delay_t *delay, + libusb_context *context); + +/* Main function for -t */ +int main_tui_interactive(libusb_context *context); /* Main function for -p */ -int main_push(filter_t *filter, delay_t *delay, libusb_context *context, char **files); - -#endif /* FXLINK_FXLINK_H */ +int main_push(struct fxlink_filter *filter, delay_t *delay, + libusb_context *context, char **files); diff --git a/fxlink/include/fxlink/config.h.in b/fxlink/include/fxlink/config.h.in new file mode 100644 index 0000000..fa44496 --- /dev/null +++ b/fxlink/include/fxlink/config.h.in @@ -0,0 +1,17 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.config: Compile-time configuration + +#pragma once + +/* Disables UDisks2 interfaces for systems that don't use it. */ +#cmakedefine FXLINK_DISABLE_UDISKS2 + +/* Disable SDL2 interfaces. */ +#cmakedefine FXLINK_DISABLE_SDL2 + +/* fxSDK version */ +#define FXLINK_VERSION "@CMAKE_PROJECT_VERSION@" diff --git a/fxlink/include/fxlink/defs.h b/fxlink/include/fxlink/defs.h new file mode 100644 index 0000000..8eaeb0a --- /dev/null +++ b/fxlink/include/fxlink/defs.h @@ -0,0 +1,111 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.defs: Utility definitions and functions + +#pragma once +#include +#include +#include +#include +#include +#include + +static inline int min(int x, int y) +{ + return (x < y) ? x : y; +} +static inline int max(int x, int y) +{ + return (x > y) ? x : y; +} +static inline int clamp(int x, int min, int max) +{ + return (x < min) ? min : (x > max) ? max : x; +} + +//--- +// Text formatting +// +// We stick to ANSI style and provide 8 colors (foreground and background) with +// bold/italic/dim attributes. The standard ANSI escape translation is provided +// here while the ncurses versions is implemented by the TUI. +//--- + +enum { + /* Main colors */ + FMT_BLACK = 0x01, + FMT_RED = 0x02, + FMT_GREEN = 0x03, + FMT_YELLOW = 0x04, + FMT_BLUE = 0x05, + FMT_MAGENTA = 0x06, + FMT_CYAN = 0x07, + FMT_WHITE = 0x08, + /* Background colors */ + FMT_BGBLACK = 0x10, + FMT_BGRED = 0x20, + FMT_BGGREEN = 0x30, + FMT_BGYELLOW = 0x40, + FMT_BGBLUE = 0x50, + FMT_BGMAGENTA = 0x60, + FMT_BGCYAN = 0x70, + FMT_BGWHITE = 0x80, + /* Modifiers */ + FMT_BOLD = 0x100, + FMT_DIM = 0x200, + FMT_ITALIC = 0x400, +}; + +#define fmt_FG(fmt) ((fmt) & 0xf) +#define fmt_BG(fmt) (((fmt) >> 4) & 0xf) +#define fmt_BOLD(fmt) (((fmt) & FMT_BOLD) != 0) +#define fmt_DIM(fmt) (((fmt) & FMT_DIM) != 0) +#define fmt_ITALIC(fmt) (((fmt) & FMT_ITALIC) != 0) + +/* Returns the escape sequence that switches to the desired format. The + returned pointer is to a static buffer overwritten on the next call. */ +char const *fmt_to_ANSI(int format); + +//--- +// Misc. +//--- + +/* Generates a unique name for a file to be stored in `path`, with `name` as a + component and the provided `suffix`. The generated path looks like + + /fxlink--2021.05.09-19h23-1 + + with the `-1` suffix being chosen as to avoid overriding existing files. + Returns a newly-allocated string to be free()'d after use. */ +char *fxlink_gen_file_name(char const *path, char const *name, + char const *suffix); + +/* Modified poll with a variable number of arrays. */ +int fxlink_multipoll(int timeout, struct pollfd *fds1, int count1, ...); + +/* Write out the given size (in bytes) in a human-readable form. Returns a + pointer to a statically-allocated string. */ +char const *fxlink_size_string(int bytes); + +//--- +// Delay +//--- + +/* An expandable allocated time used to wait for devices */ +typedef int delay_t; + +/* Builds an empty delay. */ +delay_t delay_none(void); +/* Builds a delay that lasts the specified number of seconds. */ +delay_t delay_seconds(int seconds); +/* Builds an infinite delay. */ +delay_t delay_infinite(void); + +/* Returns `true` if the delay has expired; otherwise, waits for a short while + (250 ms), decreases the supplied delay pointer, and returns `false`. Never + returns `true` after waiting (even if the delay just expired) so the caller + can attempt their task one last time before giving up on a timeout. */ +bool delay_cycle(delay_t *delay); diff --git a/fxlink/include/fxlink/devices.h b/fxlink/include/fxlink/devices.h new file mode 100644 index 0000000..96d6bf6 --- /dev/null +++ b/fxlink/include/fxlink/devices.h @@ -0,0 +1,325 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.devices: Device management and state tracking +// +// This module provides definitions for fxlink's view of devices and their +// states. There are three "tiers" of devices depending on how much fxlink +// interacts with them: +// +// - Every connected USB device is inspected and given a `struct fxlink_device` +// which track metadata like vendor and product IDs. A device at this level +// isn't very useful so fxlink won't do anything with them. +// +// - Devices with the vendor and product IDs of CASIO calculators are opened; +// their configuration descriptor is analyzed to determine what interfaces +// they offer, and some communication is performed to retrieve information +// like the serial number of the calculator. This information is stored in a +// `struct fxlink_calc` structure linked in the `calc` field of the main +// device structure. Only after this stage can device filters be applied. +// +// The key operation to do next is claiming an interface so communication can +// start. fxlink devices can be used for manual communication using the device +// pointer `fdev->dp` and the device handle `fdev->dh`. This is useful for +// communication with arbitrary interfaces like the CESG502 interface used by +// CASIO's Comm syscalls. However, when gint is used and the fxlink-specific +// interface (protocol) is opened, this header provides more automatic tools. +// +// - When calculators expose an fxlink interface, that interface can be claimed +// with fxlink_device_claim_fxlink(). This module finds relevant endpoints, +// allocates transfer buffers/utilities, and exposes a higher-level message- +// based interface. Associated data is stored in a `struct fxlink_comm` +// structure linked in the `comm` field of the main device structure. +// +// In addition to handling single devices, this header provides two types of +// tools to find devices: +// +// 1. Tracking tools, which are used to watch connected devices in real-time. +// These are asynchronous/non-blocking, keep track of what devices we've +// seen before and overall are the most flexible. These are used by the TUI. +// +// 2. "Simplified enumeration" functions which simply search for a single +// device matching a filter. These are relevant when handling multiple +// connections are not a concern, and used eg. by legacy interactive mode. +//--- + +#pragma once +#include +#include +#include +#include + +/* Device information tracked for every USB device. */ +struct fxlink_device { + /* libusb device pointer. This is libusb_ref_device()'d for the entire + lifetime of the structure (either managed by the device list or obtained + by simplified enumeration and freed by fxlink_device_cleanup()). */ + libusb_device *dp; + /* libusb device handle (NULL when the device is not open) */ + libusb_device_handle *dh; + /* Device status (an FDEV_STATUS_* enumerated value) */ + uint8_t status; + + /* Standard USB information */ + short busNumber; + short deviceAddress; + uint16_t idVendor; + uint16_t idProduct; + + /* Calculator data. This field is present whenever a device is a CASIO + calculator. Information can be accessed freely. */ + struct fxlink_calc *calc; + /* Communication data for the fxlink interface. This field is present when + calculators have an fxlink interface. It can be used when not NULL but + note that the device status might still be IGNORED (if filtered out) or + ERROR (if communication errors occurs) in which case communication might + not be allowed. */ + struct fxlink_comm *comm; +}; + +enum { + /* The device has just been connected to the host, and we have yet to + inspect it or open it. We don't know yet whether there are any supported + interfaces. */ + FXLINK_FDEV_STATUS_PENDING, + /* The device is ignored by fxlink. This is either because it's not a CASIO + calculator or because it was excluded by a device filter. The device + might have been opened (to find out the serial number) but it is now + closed and no interfaces are claimed. */ + FXLINK_FDEV_STATUS_IGNORED, + /* The device could not be used due to an error: access denied, interface + already claimed, transient errors, etc. */ + FXLINK_FDEV_STATUS_ERROR, + /* The device is a calculator and it's not ignored, but no interfaces have + been claimed yet. This header only sets the status to CONNECTED when + using an fxlink interface, but that status is mostly cosmetic and it's + entirely possible to manually claim and use an interface (eg. CESG502) + while the device is in the IDLE state. The `comm` field may or may not + be present depending on whether there is an fxlink interface. */ + FXLINK_FDEV_STATUS_IDLE, + /* The device is a calculator, is not ignored, has an fxlink interface and + that interface was claimed. The `comm` field is non-NULL and can be + checked to be determine whether communication is going on. */ + FXLINK_FDEV_STATUS_CONNECTED, +}; + +/* Return a string representation of the device's bus number and device + address, which make up a human-readable a locally unique device ID. The + address of a static string is returned. */ +char const *fxlink_device_id(struct fxlink_device const *fdev); + +/* Return a string representation of the current status. */ +char const *fxlink_device_status_string(struct fxlink_device const *fdev); + +/* Analyze a CASIO calculator device to reach "calculator" tier. + + These functions can be called on CASIO calculators (idVendor 07cf, idProduct + 6101 or 6102). They determine available interfaces, open the device, and + initialize the `calc` field with information about the device's interfaces. + If an fxlink interface is found, they also initialize the `comm` field with + static information (but doesn't claim the interface). + + After analysis, either the device status is ERROR, or it is IDLE and + interfaces are ready to be claimed. + + fxlink_device_analysis_1() is generally called in an event handling context, + so its capabilities are limited. fxlink_device_analysis_2() is called after + event handling finishes to finalize analysis. Both calls are managed by the + device list or simplified enumeration functions so direct calls are normally + not needed. `quiet` suppresses logs. */ +void fxlink_device_analysis_1(struct fxlink_device *fdev, bool quiet); +void fxlink_device_analysis_2(struct fxlink_device *fdev); + +/* Determine the filter properties of a calculator. This can be used for any + device at the calculator tier. This function only reads the device structure + and does not communicate. */ +void fxlink_device_get_properties(struct fxlink_device const *fdev, + struct fxlink_filter *properties); + +/* Device information tracked for CASIO calculators. */ +struct fxlink_calc { + /* System running on the calculator (a CALC_SYSTEM_* enumerated value) */ + uint8_t system; + /* Number of interfaces */ + uint8_t interface_count; + /* fxlink interface number (-1 if not found) */ + int8_t fxlink_inum; + /* List of interface classes (interface_count elements). Each element is a + two-byte value, MSB being class and LSB subclass. */ + uint16_t *classes; + /* Serial number (obtained with string descriptor SETUP request) */ + char *serial; +}; + +enum { + /* The device is running an unidentified system. */ + FXLINK_CALC_SYSTEM_UNKNOWN, + /* The device is using LINK app's SCSI (Mass Storage) interface. */ + FXLINK_CALC_SYSTEM_LINKSCSI, + /* The device is using the OS' native bulk interface with Comm syscalls. */ + FXLINK_CALC_SYSTEM_CESG502, + /* The device is using gint's USB driver. */ + FXLINK_CALC_SYSTEM_GINT, +}; + +/* Return a string representation of the calc determined system. */ +char const *fxlink_device_system_string(struct fxlink_device const *fdev); + +/* Device state tracked for communication targets. */ +struct fxlink_comm { + /* Whether the fxlink interface could be claimed */ + bool claimed; + /* Endpoints of the fxlink interface */ + uint8_t ep_bulk_IN; + uint8_t ep_bulk_OUT; + + /* Current IN transfer */ + struct libusb_transfer *tr_bulk_IN; + /* IN transfer buffer and its size */ + uint8_t *buffer_IN; + int buffer_IN_size; + /* fxlink message construction for the IN transfer */ + struct fxlink_transfer *ftransfer_IN; + /* Cancellation flag */ + bool cancelled_IN; + /* Completed transfer objects (to be checked by user every frame) */ + struct fxlink_message *message_IN; + + /* Current OUT transfer */ + struct libusb_transfer *tr_bulk_OUT; + /* fxlink message construction for the OUT transfer */ + struct fxlink_transfer *ftransfer_OUT; + /* Cancellation flag */ + bool cancelled_OUT; +}; + +/* Claim the fxlink interface (for a device that has one). Returns false and + sets the status to ERROR on failure. */ +bool fxlink_device_claim_fxlink(struct fxlink_device *fdev); + +/* Start an IN transfer on the device if none is currently running, so that the + device structure is always ready to receive data from the calculator. */ +void fxlink_device_start_bulk_IN(struct fxlink_device *fdev); + +/* Finish an IN transfer and obtain the completed message. This function should + be checked every frame as it will return a non-NULL pointer as soon as the + message is completed and the device will only start a new bulk IN transfer + until the message is moved out by this function. */ +struct fxlink_message *fxlink_device_finish_bulk_IN( + struct fxlink_device *fdev); + +/* Interrupt any active transfers on the device. */ +void fxlink_device_interrupt_transfers(struct fxlink_device *fdev); + +/* Clean everything that needs to be cleaned for the device to be destroyed. + This closes the libusb device handle, frees dynamically allocated memory, + etc. This function finishes immediately so no transfers must be running + when it is call; use fxlink_device_interrupt_transfers() first. */ +void fxlink_device_cleanup(struct fxlink_device *fdev); + +//--- +// Polled file descriptor tracking +// +// fxlink has some asynchronous main loops where events from libusb are mixed +// with user input. We poll libusb file descriptors, along with other event +// sources such as stdin, to listen to all event sources at the same time. This +// is possible on Linux because libusb exposes its active file descriptors. The +// following utility is a wrapper to track them, built around libusb's pollfd +// notification API. +//--- + +/* Tracker for libusb file descriptors to be polled in a main loop. */ +struct fxlink_pollfds { + /* libusb context to be tracked (must remain constant) */ + libusb_context *ctx; + /* Array of file descriptors currently being polled */ + struct pollfd *fds; + /* Number of elements in `fds` */ + int count; +}; + +/* Start tracking file descriptors for a given context. This sets up notifiers + so tracker->fds and tracker->count will be updated automatically every time + libusb events are processed. */ +void fxlink_pollfds_track(struct fxlink_pollfds *tracker, libusb_context *ctx); + +/* Stop tracking file descriptors and free tracker's resources. This must be + called before the tracker structure gets destroyed. */ +void fxlink_pollfds_stop(struct fxlink_pollfds *tracker); + +//--- +// Device tracking +// +// For real-time interactions and device detection it is useful to keep an eye +// on connected devices, process incoming devices as well as properly free +// resources for disconnected devices. libusb has a hotplug notification +// mechanism, which we wrap here into a dynamic device list. +// +// The list stays constantly updated by means of the hotplug notification and +// initializes an fxlink_device structure for every device. It does all the +// "safe" work, ie. everything that doesn't involve actual communication. For +// instance, it fills in the fields of both the fxlink_device and fxlink_calc +// structures, but it doesn't query serial numbers. +//--- + +/* A dynamically updated list of all connected devices. */ +struct fxlink_device_list { + /* libusb context that we track the devices for */ + libusb_context *ctx; + /* Callback handle */ + libusb_hotplug_callback_handle hotplug_handle; + /* Array of connected devices */ + struct fxlink_device *devices; + /* Number of elements in `devices` */ + int count; +}; + +/* Start tracking connected devices for the given context. The list will update + whenever libusb events are processed and new devices will be available (ie. + in the IDLE state) after calling fxlink_device_list_refresh(). */ +bool fxlink_device_list_track(struct fxlink_device_list *list, + libusb_context *ctx); + +/* Refresh devices. This should be called after handling libusb events; it + finishes analysis for new devices. After calling this function, devices are + ready to be filtered and interfaces claimed. */ +void fxlink_device_list_refresh(struct fxlink_device_list *list); + +/* Interrupt transfers on all devices in the list. This is non-blocking; + returns true if some transfers are still running. Call this in a loop until + it returns false, while handling libusb events at each iteration. */ +bool fxlink_device_list_interrupt(struct fxlink_device_list *list); + +/* Stop tracking connected devices, free the device list and disable the + hotplug notification. Make sure all devices are closed and communications + stopped before calling this function. */ +void fxlink_device_list_stop(struct fxlink_device_list *list); + +//--- +// Simplified device enumeration +// +// These functions provides a simpler way to find devices. Given a filter they +// will try to find a device that matches, and build a device structure when +// they find one. These are only suitable to open a single device since they +// don't track which fxlink devices have been created. +//--- + +/* Finds a libusb device that matches the provided filter. + + If one is found, returns a newly-allocated fxlink device. The device will be + opened and have its device handler in the `comm->dh` field. Additionally, if + there is an fxlink interface, that interface will be claimed and + communication tools from this header will be initialized. + + If no matching device is found, NULL is returned; this is a non-blocking + function. Note that this function will interact with devices to determine + their property (ie. serial number) even when it returns NULL. */ +struct fxlink_device *fxlink_device_find(libusb_context *ctx, + struct fxlink_filter const *filter); + +/* Same as fxlink_device_find(), but waits for the specified delay. */ +// TODO: fxlink_device_find_wait(): Use a struct timespec, get rid of delay_t? +struct fxlink_device *fxlink_device_find_wait(libusb_context *ctx, + struct fxlink_filter const *filter, delay_t *delay); diff --git a/fxlink/include/fxlink/filter.h b/fxlink/include/fxlink/filter.h new file mode 100644 index 0000000..e300521 --- /dev/null +++ b/fxlink/include/fxlink/filter.h @@ -0,0 +1,84 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.filter: Backend-agnostic device filters +// +// Most automatic functions in fxlink only connect with a single calculator. +// When several calculators are connected it is useful to narrow down the +// targeted machine programmatically, which is done with a device filter. +// +// A device filter is a simple conjunction of properties detected with the +// generic metadata of the device, as provided by the backend's API. Depending +// on the backend not all properties can be detected; for instance, fx-CG and +// G-III calculators have the same idVendor/idProduct pair and cannot be +// distinguished on the device descriptor alone, however they report different +// identities in SCSI queries and can be differentiated with UDisks2. +// +// The same type `struct fxlink_properties` is used both to represents devices' +// properties and filters. Every property has a "total information order" where +// values can be compared for how specific they are. A device matches a filter +// if its detected properties are more specific than the filter's requirements. +// So far the order is: +// - For booleans: true more specific than false +// - For serial number: any string more specific than NULL +//--- + +#pragma once +#include +#include + +/* Detected devices' properties; also used for filters. */ +struct fxlink_filter { + /* The calculator uses Protocol 7/CESG502. Detected with idProduct 0x6101. + These calcs don't support SCSI, so this is always false in UDisks2. */ + bool p7; + /* The calculator supports Mass Storage/SCSI. Detected with idProduct + 0x6102. Always true in UDisks2. */ + bool mass_storage; + /* The calculator has an fxlink interface. Always false in UDisks2. */ + bool intf_fxlink; + /* The calculator has a CESG502 interface. Always false in UDisks2. */ + bool intf_cesg502; + /* The calculator is from the fx-CG/G-III series. Detected with the SCSI + drive `model` metadata. Only available in UDisks2. */ + bool series_cg; + bool series_g3; + /* Serial number. Requires write access to obtain in libusb (with a STRING + descriptor request) because it's not cached. free() after use. */ + char *serial; +}; + +/* Return values for back-end specific matching functions. */ +enum { + /* A unique calculator matching the filter was found */ + FXLINK_FILTER_UNIQUE, + /* No calculator matching the filter was found */ + FXLINK_FILTER_NONE, + /* Multiple calculators matching the filter were found */ + FXLINK_FILTER_MULTIPLE, + /* An error occurred while trying to enumerate devices */ + FXLINK_FILTER_ERROR, +}; + +/* Parse a filter string into a structure */ +struct fxlink_filter *fxlink_filter_parse(char const *specification); + +/* Disable filter properties not supported by each backend (with a warning + emitted for each such ignored property) */ +void fxlink_filter_clean_libusb(struct fxlink_filter *filter); +void fxlink_filter_clean_udisks2(struct fxlink_filter *filter); + +/* Determines whether a set of concrete properties matches a filter. Returns + whether all the fields of `props` are more specific than their counterparts + in `filter`. */ +bool fxlink_filter_match( + struct fxlink_filter const *props, + struct fxlink_filter const *filter); + +/* Print a set of properties to a stream. Outputs a single line. */ +void fxlink_filter_print(FILE *fp, struct fxlink_filter const *props); + +/* Free a filter structure and its fields */ +void fxlink_filter_free(struct fxlink_filter *filter); diff --git a/fxlink/include/fxlink/logging.h b/fxlink/include/fxlink/logging.h new file mode 100644 index 0000000..5f716ce --- /dev/null +++ b/fxlink/include/fxlink/logging.h @@ -0,0 +1,49 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.logging: Logging interface and log redirection +// +// This header provides basic logging utilities. You've seen these a thousand +// times over. The only features are supporting colors from , +// redirecting libusb logs to this interface automatically, and redirecting +// this interface to either the terminal or the ncurses TUI. +//--- + +#pragma once +#include +#include +#include + +/* Type of the log handling function out of this interface */ +typedef void fxlink_log_handler_t(int display_fmt, char const *str); + +/* Default handler; prints to stderr with ANSI escapes. */ +fxlink_log_handler_t fxlink_log_stderr; + +/* Set the logging function. Default is fxlink_log_stderr. */ +void fxlink_log_set_handler(fxlink_log_handler_t *handler); + +/* Redirect libusb log to this module's log hander. */ +void fxlink_log_grab_libusb_logs(void); + +/* Log a message. All of the logging functions accept a printf()-style format + with corresponding arguments, and return 1. */ +int log_(char const *fmt, ...); +/* Log a message with a certain display format (color/bold/italic). */ +int flog(int display_fmt, char const *fmt, ...); +/* Like log(), but adds a red "error:" in front. */ +int elog(char const *fmt, ...); +/* Like log(), but adds a yellow "warning:" in front. */ +int wlog(char const *fmt, ...); +/* Like log(), but for headers (with current time in gray in square brackets, + and provided text in yellow followed by ":"). */ +int hlog(char const *fmt, ...); + +/* Warning that includes a libusb error message. */ +#define wlog_libusb(RC, FMT, ...) \ + wlog(FMT ": %s\n", ##__VA_ARGS__, libusb_strerror(RC)) +/* Error that includes a libusb error message. */ +#define elog_libusb(RC, FMT, ...) \ + elog(FMT ": %s\n", ##__VA_ARGS__, libusb_strerror(RC)) diff --git a/fxlink/include/fxlink/protocol.h b/fxlink/include/fxlink/protocol.h new file mode 100644 index 0000000..5c45962 --- /dev/null +++ b/fxlink/include/fxlink/protocol.h @@ -0,0 +1,140 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.protocol: Basic fxlink-specific communications procotol +// +// This header implements fxlink's run-of-the-mill communications protocol. +// Messages are typed with an application/type pair and their length must be +// announced in the header. In most modes when a message with an application +// field other than "fxlink" is specified, an external executable is invoked, +// which provides a basic form of automation. +// +// Message headers are transferred in little-endian format but some message +// types are defined to have big-endian contents to avoid excessive amounts of +// work on the calculator side. +//--- + +/* TODO: fxlink protocol: avoid having to specify message length in header */ + +#pragma once +#include + +/* Message. The header consists of every field but the `data` pointer, and it + must arrive entirely within the first transaction for a message to be + recognized. */ +struct fxlink_message { + /* Protocol version, in format 0x0000MMmm */ + uint32_t version; + /* Total size of message, in bytes (excluding this header) */ + uint32_t size; + /* Size of individual transfers (usually 2048 bytes) */ + /* TODO: fxlink protocol: get rid of transfer size field (blank it out) */ + uint32_t transfer_size; + + /* Application name (NUL-padded but might not be NUL-terminated) */ + char application[16]; + /* Message type (NUL-padded but might not be NUL-terminated) */ + char type[16]; + + /** End of actual header in protocol **/ + + /* Padding for alignment */ + int _padding; + /* Pointer to message data, with `size` bytes */ + void *data; +}; + +#define FXLINK_MESSAGE_HEADER_SIZE (offsetof(struct fxlink_message, _padding)) + +/* Subheader for the built-in image message type. */ +struct fxlink_message_image_header { + /* Image width and height, in pixels */ + uint32_t width; + uint32_t height; + /* Pixel format, one of the FXLINK_MESSAGE_IMAGE_* values below */ + int pixel_format; +}; + +enum { + /* Image is an array of big-endian uint16_t values with RGB565 format */ + FXLINK_MESSAGE_IMAGE_RGB565, + /* Image is an array of bits in black-and-white format */ + FXLINK_MESSAGE_IMAGE_MONO, + /* Image is two consecutive mono arrays, one for light, one for dark */ + FXLINK_MESSAGE_IMAGE_GRAY, +}; + +/* Format for raw decoded images to be used with other APIs. */ +struct fxlink_message_image_raw { + /* Width and height in pixels */ + int width; + int height; + /* Allocated array of `height` pointers, each pointing to `3*width` bytes + of memory containing the RGB data of pixels from left to right. */ + uint8_t **data; +}; + +/* Check whether a message has a certain application and type. If type is NULL, + checks the application only (any type is accepted). */ +bool fxlink_message_is_apptype(struct fxlink_message const *message, + char const *application, char const *type); + +/* Check if a message is one of common built-in fxlink messages. */ +#define fxlink_message_is_fxlink_text(MESSAGE) \ + fxlink_message_is_apptype(MESSAGE, "fxlink", "text") +#define fxlink_message_is_fxlink_image(MESSAGE) \ + fxlink_message_is_apptype(MESSAGE, "fxlink", "image") +#define fxlink_message_is_fxlink_video(MESSAGE) \ + fxlink_message_is_apptype(MESSAGE, "fxlink", "video") + +/* Decode an image message into a raw image structure. */ +struct fxlink_message_image_raw *fxlink_message_image_decode( + struct fxlink_message const *msg); + +/* Free a raw image structure. */ +void fxlink_message_image_raw_free(struct fxlink_message_image_raw *raw); + +/* Free memory associated with a message. If free_data is true, also frees the + internal data buffer. You should set the flag for messages you received. */ +void fxlink_message_free(struct fxlink_message *message, bool free_data); + +//--- +// Tools for crafting and receiving messages +//--- + +/* Data for an inbound or outbound transfer in progress. This structure can be + created as soon as a header has been received or filled. */ +struct fxlink_transfer { + /* Message header and data buffer */ + struct fxlink_message msg; + /* Transfer direction (FXLINK_TRANSFER_{IN,OUT}) */ + uint8_t direction; + /* Size of data sent or received so far */ + uint32_t processed_size; +}; + +enum { + /* Transfer is inbound (calculator -> fxlink) */ + FXLINK_TRANSFER_IN, + /* Transfer is outbound (fxlink -> calculator) */ + FXLINK_TRANSFER_OUT, +}; + +/* Make an inbound transfer structure starting with the provided data (the data + obtained in the first IN transaction to be decoded). This function grabs the + header from the data, allocates a buffer of a suitable size and starts + saving the message's contents. Returns NULL on error (invalid or incomplete + header, out of memory, etc). */ +struct fxlink_transfer *fxlink_transfer_make_IN(void *data, int size); + +/* If the provided IN transfer is finished, extract the message and free the + transfer pointer. Otherwise, return NULL. */ +struct fxlink_message *fxlink_transfer_finish_IN(struct fxlink_transfer *tr); + +/* Append data to a previously-initialized inbound transfer. */ +void fxlink_transfer_receive(struct fxlink_transfer *tr, void *data, int size); + +/* Check whether a transfer is complete. */ +bool fxlink_transfer_complete(struct fxlink_transfer const *tr); diff --git a/fxlink/include/fxlink/tooling/libpng.h b/fxlink/include/fxlink/tooling/libpng.h new file mode 100644 index 0000000..79d9d61 --- /dev/null +++ b/fxlink/include/fxlink/tooling/libpng.h @@ -0,0 +1,15 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tooling.libpng: Utilities based on libpng + +#pragma once +#include +#include + +/* Save a raw image decoded from an fxlink message to a PNG file. Returns + zero on success. */ +int fxlink_libpng_save_raw(struct fxlink_message_image_raw *raw, + char const *path); diff --git a/fxlink/include/fxlink/tooling/sdl2.h b/fxlink/include/fxlink/tooling/sdl2.h new file mode 100644 index 0000000..2f1051a --- /dev/null +++ b/fxlink/include/fxlink/tooling/sdl2.h @@ -0,0 +1,24 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tooling.sdl2: Utilities based on the SDL2 library +// +// Note: all the functions in this file are "neutralized" if the compile-time +// option FXLINK_DISABLE_SDL2 is set. See . +//--- + +#pragma once +#include + +#ifndef FXLINK_DISABLE_SDL2 +#include +#endif + +/* Display a raw image on the window. If now window has been opened yet, one is + created automatically. */ +void fxlink_sdl2_display_raw(struct fxlink_message_image_raw const *raw); + +/* Handle SDL events. This should be called regularly from the main thread. */ +void fxlink_sdl2_handle_events(void); diff --git a/fxlink/ud2.h b/fxlink/include/fxlink/tooling/udisks2.h similarity index 56% rename from fxlink/ud2.h rename to fxlink/include/fxlink/tooling/udisks2.h index 4c3cc56..05e4862 100644 --- a/fxlink/ud2.h +++ b/fxlink/include/fxlink/tooling/udisks2.h @@ -1,33 +1,39 @@ -//--- -// fxlink:ud2 - UDisks2 functions -//--- - -#ifndef FXLINK_UD2_H -#define FXLINK_UD2_H +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tooling.udisks2: Utilities based on the UDisks2 library +#pragma once +#include #ifndef FXLINK_DISABLE_UDISKS2 #include -#include "config.h" -#include "properties.h" -#include "filter.h" -#include "util.h" +#include /* ud2_properties(): Determine properties of a UDisks2 USB drive */ -properties_t ud2_properties(UDisksDrive *drive); +struct fxlink_filter ud2_properties(UDisksDrive *drive); + +/* Initialize a connection to the UDisks2 service via D-Bus. */ +int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr); + +/* Close the connection to the UDisks2 service. */ +void ud2_end(UDisksClient *udc, UDisksManager *udm); /* ud2_unique_matching(): Device matching the provided filter, if unique Similar to usb_unique_matching(), please refer to "usb.h" for details. There are just many more inputs and outputs. */ -int ud2_unique_matching(filter_t const *filter, UDisksClient *udc, +int ud2_unique_matching(struct fxlink_filter const *filter, UDisksClient *udc, UDisksManager *udm, UDisksBlock **block, UDisksDrive **drive, UDisksFilesystem **fs); /* ud2_unique_wait(): Wait for a device matching the provided filter to connect - Like usb_unique_wait(), please see "usb.h" for details. */ -int ud2_unique_wait(filter_t const *filter, delay_t *delay, UDisksClient *udc, - UDisksManager *udm, UDisksBlock **block, UDisksDrive **drive, - UDisksFilesystem **fs); + Returns an FXLINK_FILTER_* code. If a unique device is found, sets *udc, + *udm, *block, *drive and *fs accordingly. */ +int ud2_unique_wait(struct fxlink_filter const *filter, delay_t *delay, + UDisksClient *udc, UDisksManager *udm, UDisksBlock **block, + UDisksDrive **drive, UDisksFilesystem **fs); //--- // Iteration on UDisks2 devices @@ -39,7 +45,7 @@ typedef struct { UDisksDrive *drive; UDisksFilesystem *fs; /* Device properties */ - properties_t props; + struct fxlink_filter props; /* Whether the iteration has finished */ bool done; @@ -66,5 +72,3 @@ void ud2_iter_next(ud2_iterator_t *it); !NAME.done; ud2_iter_next(&NAME)) if(!NAME.done) #endif /* FXLINK_DISABLE_UDISKS2 */ - -#endif /* FXLINK_UD2_H */ diff --git a/fxlink/include/fxlink/tui/input.h b/fxlink/include/fxlink/tui/input.h new file mode 100644 index 0000000..0edb6a4 --- /dev/null +++ b/fxlink/include/fxlink/tui/input.h @@ -0,0 +1,71 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tui.input: Asynchronous readline-style console input +// +// This header provides a basic asynchronous line edition mechanic attached to +// an ncurses window. It might be possible to use readline directly, but this +// is also a good exercise. +//--- + +#pragma once +#include +#include + +/* Input field attached to a window. This object only needs to be instantiated + once for multiple inputs. */ +struct fxlink_TUI_input { + /* Line contents, NUL-terminated. The buffer might be larger. */ + char *data; + /* Size of contents (not counting the NUL) */ + int size; + /* Allocated size (always ≥ size+1) */ + int alloc_size; + /* Cursor position within string */ + int cursor; + /* Attached ncurses window */ + WINDOW *win; + /* Original cursor position within window at start of input */ + uint16_t wx, wy; +}; + +//--- +// Text manipulation functions +//--- + +/* Initialize the input at least init_chars characters of content available. + Returns false on error. Previous contents are not freed! */ +bool fxlink_TUI_input_init(struct fxlink_TUI_input *in, WINDOW *win, + int init_chars); + +/* Clean up a line and free its contents. */ +void fxlink_TUI_input_free(struct fxlink_TUI_input *in); + +/* Realloc the line to ensure n characters plus a NUL can be written. */ +bool fxlink_TUI_input_alloc(struct fxlink_TUI_input *in, int n); + +/* Insert n characters at position p. */ +bool fxlink_TUI_input_insert(struct fxlink_TUI_input *in, int p, + char const *str, int n); + +/* Remove n characters at position p. Returns the number of characters + actually removed after bounds checking. */ +int fxlink_TUI_input_delete(struct fxlink_TUI_input *in, int p, int n); + +//-- +// Rendering functions +//--- + +/* Clear the input up to the original cursor position */ +void fxlink_TUI_input_clear(struct fxlink_TUI_input *in); + +/* Redraw the input (needed after non-appending edits) */ +void fxlink_TUI_input_redraw(struct fxlink_TUI_input *in); + +/* Clear the screen as with C-l */ +void fxlink_TUI_input_clearscreen(struct fxlink_TUI_input *in); + +/* getch() for an input (usually called when there *is* input */ +bool fxlink_TUI_input_getch(struct fxlink_TUI_input *in, WINDOW *logWindow); diff --git a/fxlink/include/fxlink/tui/layout.h b/fxlink/include/fxlink/tui/layout.h new file mode 100644 index 0000000..386e110 --- /dev/null +++ b/fxlink/include/fxlink/tui/layout.h @@ -0,0 +1,113 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tui.layout: Utility for laying out TUI windows in a flexbox treee +// +// This module can be used to set up a flexbox tree for window layout in the +// TUI. A `struct fxlink_TUI_box` is either an ncurses window, or a flexbox +// which arranges its children horizontally or vertically. +// +// Each box has some geometry settings: +// - w and h are initially set to the natural content size (can be 0) +// - min_w, min_h, max_w, max_h constrain the range of acceptable sizes +// - stretch_x and stretch_y indicate the box's tendency to grow +// - strech_force allows stretching beyond max_w/max_h (rarely needed) +// +// Most of the module is free-standing. Boxes should be created bottom to top, +// ie. windows first and then progressively larger groups. Specifying a WINDOW +// pointer in windows is optional; it is only used by fxlink_TUI_apply_layout() +// to actually configure ncurses windows. The root box of the tree should be +// kept in memory as it is used for rendering windows borders on the background +// window. +// +// Space distribution is initiated by a call to box_layout_root() after all the +// boxes have been created to form the tree. The root box receives the provided +// screen space, and splits it recursively between children. Boxes get their +// requested content size (clamped to minimum/maximum size) and any space left +// is distributed in proportion with stretch factors. Overflows are possible if +// the screen is too small to accommodate for everyone's content size, so the +// application of the layout should account for that and clamp to the screen +// size again (fxlink_TUI_apply_layout() does that). +//--- + +#pragma once +#include +#include +#include + +#define FXLINK_TUI_BOX_MAXSIZE 8 + +/* Box either holding an ncurses WINDOW or arranging children in a flexbox. */ +struct fxlink_TUI_box { + /* Position and size, excluding the border (which is shared). After layout, + x/y is the absolute position and w/h the allocated size. Other geometric + settings are only relevant during layout. */ + uint16_t x, y, w, h; + /* Size constraints */ + uint16_t min_w, min_h; + uint16_t max_w, max_h; + /* Stretch factor */ + uint8_t stretch_x, stretch_y; + /* Stretch beyond limits */ + bool stretch_force; + /* Box subdivision type: BOX_WINDOW, BOX_HORIZ and BOX_VERT */ + short type; + + union { + /* Valid for type == FXLINK_TUI_BOX_WINDOW */ + struct { + char const *title; + WINDOW **win; + } window; + /* Valid for type == FXLINK_TUI_BOX_{HORIZONTAL,VERTICAL} */ + struct fxlink_TUI_box *children[FXLINK_TUI_BOX_MAXSIZE]; + }; +}; + +enum { + /* Box is an ncurses windows. Before layout, the natural size of the + content is set in w and h (default 0), size constraints are set in + {min,max}_{w,h} (default 0/65535) and stretch rates in stretch_x, + stretch_y (default 1) and strech_force (default false). */ + FXLINK_TUI_BOX_WINDOW, + /* Box is a horizontal of vertical flexbox. Before layout, children are + specified and they induce a natural content size. Size constraints are + stretch rates are specified as for windows. */ + FXLINK_TUI_BOX_HORIZONTAL, + FXLINK_TUI_BOX_VERTICAL, +}; + +/* Make a window box. The title is used for the border rendering function in + the TUI rendering utils. The window pointer is optional and only needed for + fxlink_TUI_apply_layout(). */ +struct fxlink_TUI_box *fxlink_TUI_box_mk_window(char const *title, WINDOW **w); +/* Make a vertical box with a fixed list of children */ +struct fxlink_TUI_box *fxlink_TUI_box_mk_vertical( + struct fxlink_TUI_box *child1, ... /*, NULL */); +/* Make a horizontal box with a fixed list of children */ +struct fxlink_TUI_box *fxlink_TUI_box_mk_horizontal( + struct fxlink_TUI_box *child1, ... /*, NULL */); + +/* Specify the minimum size, maximum size and stretch rate of a box */ +void fxlink_TUI_box_minsize(struct fxlink_TUI_box *box, int min_w, int min_h); +void fxlink_TUI_box_maxsize(struct fxlink_TUI_box *box, int max_w, int max_h); +void fxlink_TUI_box_stretch(struct fxlink_TUI_box *box, + int stretch_x, int stretch_y, bool force); + +/* Recursively print box and children starting at specified indent level */ +void fxlink_TUI_box_print(FILE *fp, struct fxlink_TUI_box const *b, int level); + +/* Layout a root box for the specified available screen space. This accounts + for 1-unit borders around and between windows. For a full-screen window tree + x/y would be set to 0 and w/h to the screen size, but the tree can also be + laid out to occupy only a subset of screen space. */ +void fxlink_TUI_box_layout(struct fxlink_TUI_box *root_box, + int x, int y, int w, int h); + +/* Recursively apply the layout. This function resizes and moves ncurses + windows to fit the space allocated in the boxes. Returns false if an ncurses + error causes one of the windows to become NULL. */ +bool fxlink_TUI_apply_layout(struct fxlink_TUI_box *root_box); + diff --git a/fxlink/include/fxlink/tui/render.h b/fxlink/include/fxlink/tui/render.h new file mode 100644 index 0000000..c0d8885 --- /dev/null +++ b/fxlink/include/fxlink/tui/render.h @@ -0,0 +1,32 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// +// fxlink.tui.render: TUI rendering utilities + +#pragma once +#include +#include + +/* printf to a window. */ +#define print wprintw +/* printf with an ncurses attribute for the whole string. */ +void aprint(WINDOW *win, int attr, char const *format, ...); +/* printf with an format for the whole string. */ +void fprint(WINDOW *win, int display_fmt, char const *format, ...); + +/* Hard-coded color scheme */ +#define FMT_FILENAME FMT_CYAN +#define FMT_SIZE FMT_MAGENTA +#define FMT_HEADER (FMT_CYAN | FMT_ITALIC) +#define FMT_BGSELECTED (FMT_BGWHITE | FMT_BLACK) + +/* Translate text format into ncurses attributes. */ +int fmt_to_ncurses_attr(int display_fmt); + +/* Recursively render borders around a box and its children. */ +void fxlink_TUI_render_borders(struct fxlink_TUI_box const *box); + +/* Render the window titles of all windows in the tree. */ +void fxlink_TUI_render_titles(struct fxlink_TUI_box const *box); diff --git a/fxlink/interactive.c b/fxlink/interactive.c deleted file mode 100644 index d4f6e25..0000000 --- a/fxlink/interactive.c +++ /dev/null @@ -1,254 +0,0 @@ -#include "config.h" -#include "fxlink.h" -#include "util.h" -#include "properties.h" -#include "filter.h" -#include "protocol.h" -#include "usb.h" -#include "png.h" -#include "sdl2.h" - -#include -#include -#include -#include - -/* Video capture trackers, to avoid spamming terminal with messages */ -static int last_message_was_video = 0; -static int video_frame_count = 0; - -static bool message_new(message_t *msg, usb_fxlink_header_t const *h) -{ - int version_major = (h->version >> 8) & 0xff; - int version_minor = (h->version) & 0xff; - - if(!strncmp(h->application,"fxlink",16) && !strncmp(h->type,"video",16)) { - if(last_message_was_video) - fprintf(stderr, "\r"); - last_message_was_video = 1; - } - else { - if(last_message_was_video) - fprintf(stderr, "\n"); - last_message_was_video = 0; - } - - if(!options.quiet) { - fprintf(stderr, "New message (v%d.%d): application '%.16s', type " - "'%.16s', size %d bytes", version_major, version_minor, - h->application, h->type, h->size); - } - - if(last_message_was_video) - fprintf(stderr, " [video frame #%d]", ++video_frame_count); - else if(!options.quiet) - fprintf(stderr, "\n"); - - msg->output = malloc(h->size); - if(!msg->output) { - err("cannot allocate memory for message of %d bytes", h->size); - return false; - } - - msg->header = *h; - msg->size_read = 0; - msg->valid = true; - return true; -} - -static void message_finish(message_t *msg) -{ - char const *path = "."; - - if(!strncmp(msg->header.application, "fxlink", 16)) { - if(!strncmp(msg->header.type, "image", 16)) { - usb_fxlink_image_t *img = (void *)msg->output; - char *filename = gen_file_name(path, msg->header.type, "png"); - - uint8_t **row_pointers = fxlink_protocol_decode_image(msg); - fxlink_png_save(row_pointers, img->width, img->height, filename); - - printf("Saved image (%dx%d, format=%d) to '%s'\n", - img->width, img->height, img->pixel_format, filename); - free(row_pointers); - free(filename); - return; - } - - if(!strncmp(msg->header.type, "text", 16)) { - if(!options.quiet) - printf("------------------\n"); - fwrite(msg->output, 1, msg->header.size, stdout); - if(!options.quiet) { - if(msg->output[msg->header.size - 1] != '\n') printf("\n"); - printf("------------------\n"); - } - - if(options.log_file) - fwrite(msg->output, 1, msg->header.size, options.log_file); - return; - } - - if(!strncmp(msg->header.type, "video", 16)) { - usb_fxlink_image_t *img = (void *)msg->output; - uint8_t **row_pointers = fxlink_protocol_decode_image(msg); - -#ifndef FXLINK_DISABLE_SDL2 - sdl2_stream(row_pointers, img->width, img->height); -#else - warn("SDL2 support disabled, skipping video frame!"); -#endif - return; - } - } - - static char combined_type[33]; - snprintf(combined_type, 33, "%.16s-%.16s", msg->header.application, - msg->header.type); - /* Default to saving to a blob */ - char *filename = gen_file_name(path, combined_type, "bin"); - FILE *fp = fopen(filename, "wb"); - if(!fp) { - err("could not save to '%s': %m", filename); - return; - } - - fwrite(msg->output, 1, msg->header.size, fp); - fclose(fp); - fprintf(stderr, "Saved as blob to '%s'\n", filename); - free(filename); -} - -static void message_output(message_t *msg, void *buffer, int size) -{ - int data_left = msg->header.size - msg->size_read; - - if(size > data_left) { - err("Too much data in message, dropping %d bytes", size - data_left); - size = data_left; - } - - memcpy(msg->output + msg->size_read, buffer, size); - - msg->size_read += size; - if(msg->size_read >= msg->header.size) { - bool is_video = !strncmp(msg->header.application, "fxlink", 16) && - !strncmp(msg->header.type, "video", 16); - if(!is_video && !options.quiet) - fprintf(stderr, "Successfully read %d bytes\n", msg->size_read); - message_finish(msg); - msg->valid = false; - } -} - -int main_interactive(filter_t *filter, delay_t *delay, libusb_context *context) -{ - libusb_device *dev = NULL; - libusb_device_handle *dh = NULL; - - /* Wait for a device to be connected */ - filter_clean_libusb(filter); - int rc = usb_unique_wait(filter, delay, context, &dev); - - if(rc == FILTER_NONE) { - printf("No device found.\n"); - return 1; - } - else if(rc == FILTER_MULTIPLE) { - printf("Multiple devices found, ambiguous!\n"); - return 1; - } - - if((rc = libusb_open(dev, &dh))) { - rc = libusb_err(rc, "cannot open device %s", usb_id(dev)); - goto end; - } - - /* Don't detach kernel drivers to avoid breaking the Mass Storage - communications if fxlink is ever started while the native LINK - application is running! */ - libusb_set_auto_detach_kernel_driver(dh, false); - - if((rc = libusb_claim_interface(dh, 0))) { - rc = libusb_err(rc, "cannot claim interface on %s", usb_id(dev)); - goto end; - } - - printf("Connected to %s, starting test.\n", usb_id(dev)); - - /* This buffer is used to receive messages; if the header is not complete - it is left in the buffer, hence the extra room */ - __attribute__((aligned(4))) - static uint8_t buffer[2048 + sizeof(usb_fxlink_header_t)] = { 0 }; - /* Amount of data in the buffer */ - int buffer_size = 0; - - /* Current message */ - message_t msg = { 0 }; - - while(1) - { -#ifndef FXLINK_DISABLE_SDL2 - sdl2_tick(); -#endif - - int transferred = -1; - rc = libusb_bulk_transfer(dh, 0x81, buffer + buffer_size, 2048, - &transferred, 500); - - if(rc == LIBUSB_ERROR_NO_DEVICE) { - if(last_message_was_video) - fprintf(stderr, "\n"); - printf("Disconnected, leaving.\n"); - break; - } - else if(rc && rc != LIBUSB_ERROR_TIMEOUT) { - rc = libusb_err(rc, "bulk transfer failed on %s", usb_id(dev)); - continue; - } - if(transferred <= 0) continue; - - buffer_size += transferred; - - /* If there is an unfinished message, continue working on it */ - if(msg.valid) { - message_output(&msg, buffer, buffer_size); - buffer_size = 0; - } - - /* If the header is not yet fully transmitted, wait */ - usb_fxlink_header_t *h = (void *)buffer; - if(buffer_size < (int)sizeof *h) continue; - - /* Handle a new message */ - if(h->version == 0x00000100) { - int data_size = buffer_size - sizeof *h; - - if(!message_new(&msg, h)) - printf("dropping %d bytes\n", data_size); - else - message_output(&msg, buffer + sizeof *h, data_size); - - buffer_size = 0; - continue; - } - else { - err("invalid header, dropping %d bytes", transferred); - buffer_size = 0; - } - } - - /* Save last unfinished message */ - if(buffer_size > 0) { - printf("%d bytes not collected dropped\n", buffer_size); - } - rc = 0; - -end: - if(dh) { - libusb_release_interface(dh, 0); - libusb_close(dh); - } - if(dev) libusb_unref_device(dev); - return rc; -} diff --git a/fxlink/logging.c b/fxlink/logging.c new file mode 100644 index 0000000..55daca6 --- /dev/null +++ b/fxlink/logging.c @@ -0,0 +1,93 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include +#include +#include + +static fxlink_log_handler_t *log_handler = fxlink_log_stderr; + +void fxlink_log_stderr(int display_fmt, char const *str) +{ + fputs(fmt_to_ANSI(display_fmt), stderr); + fputs(str, stderr); + fputs(fmt_to_ANSI(0), stderr); +} + +void fxlink_log_set_handler(fxlink_log_handler_t *handler) +{ + log_handler = handler ? handler : fxlink_log_stderr; +} + +static void handle_libusb_log(libusb_context *ctx, enum libusb_log_level level, + char const *str) +{ + (void)ctx; + (void)level; + log_handler(0, str); +} + +void fxlink_log_grab_libusb_logs(void) +{ + libusb_set_log_cb(NULL, handle_libusb_log, LIBUSB_LOG_CB_GLOBAL); +} + +int flogv(int display_fmt, char const *fmt, va_list args) +{ + char *str = NULL; + vasprintf(&str, fmt, args); + if(str) { + log_handler(display_fmt, str); + free(str); + } + return 1; +} + +#define LOG_VA_ARGS(FMT, STMTS) do { \ + va_list _args; \ + va_start(_args, FMT); \ + STMTS; \ + va_end(_args); \ +} while(0) + +int log_(char const *fmt, ...) +{ + LOG_VA_ARGS(fmt, flogv(0, fmt, _args)); + return 1; +} + +int flog(int display_fmt, char const *fmt, ...) +{ + LOG_VA_ARGS(fmt, flogv(display_fmt, fmt, _args)); + return 1; +} + +int elog(char const *fmt, ...) +{ + flog(FMT_RED, "error: "); + LOG_VA_ARGS(fmt, flogv(0, fmt, _args)); + return 1; +} + +int wlog(char const *fmt, ...) +{ + flog(FMT_YELLOW, "warning: "); + LOG_VA_ARGS(fmt, flogv(0, fmt, _args)); + return 1; +} + +int hlog(char const *fmt, ...) +{ + time_t t = time(NULL); + struct tm tm; + localtime_r(&t, &tm); + + flog(FMT_WHITE | FMT_DIM, "[%02d:%02d] ", tm.tm_hour, tm.tm_min); + LOG_VA_ARGS(fmt, flogv(FMT_YELLOW | FMT_DIM, fmt, _args)); + flog(FMT_YELLOW | FMT_DIM, ": "); + return 1; +} diff --git a/fxlink/main.c b/fxlink/main.c index 0dced44..df78491 100644 --- a/fxlink/main.c +++ b/fxlink/main.c @@ -1,9 +1,12 @@ -#include "config.h" +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + #include "fxlink.h" -#include "util.h" -#include "properties.h" -#include "filter.h" -#include "usb.h" +#include +#include #include #include @@ -11,71 +14,64 @@ #include #include #include - -int main_test(libusb_device *device, libusb_context *context); +#include static const char *help_string = -"usage: %1$s -l [options...]\n" -" %1$s -b [options...]\n" -" %1$s -s [options...] \n" -" %1$s --test\n" +"usage: %1$s (-l|-b|-t) [General options]\n" +" %1$s -i [-r] [--fxlink-log[=]] [General options]\n" +" %1$s -p [General options]\n" +" %1$s -s ... [General options]\n" "\n" -"fxlink interacts with CASIO calculators of the fx-9860G and fx-CG 50 series\n" -"over the USB port, through mass storage and custom USB protocols. Depending\n" -"on the mode, fxlink uses libusb (for discovery and USB communication)or\n" -"the UDisks2 library (to mount and use Mass Storage devices).\n" +"fxlink interacts with CASIO calculators of the fx and fx-CG families over\n" +"the USB port, using libusb. It can also transfer files for Mass Storage\n" +"calculators using UDisks2.\n" "\n" -"Operating modes:\n" -" -l, --list List detected calculators on the USB ports (libusb)\n" -" -b, --blocks List detected Mass Storage filesystems (udisks2)\n" -" -s, --send Send a file to a Mass Storage calculator (udisks2)\n" -" -i, --interactive Interactive messaging with a gint add-in (libusb)\n" -" -p, --push Push a .bin file to the Add-In Push app (libusb)\n" +"Standard (libusb) modes:\n" +" -l, --list List detected calculators on the USB ports\n" +" -i, --interactive Messaging with a gint add-in (calc -> PC only)\n" +" -t, --tui TUI interactive mode\n" +" -p, --push Push a .bin file to the Add-In Push app\n" +"\n" +"Mass Storage (UDisks2) modes:\n" +" -b, --blocks List detected Mass Storage calculators\n" +" -s, --send Send a file to a Mass Storage calc and unmount it\n" "\n" "General options:\n" -" -w DELAY Wait up to this many seconds for a calculator to\n" -" connect. If DELAY is unspecified, wait indefinitely.\n" -" -f FILTER Filter which calculators can be detected and used\n" -" --libusb-log=LEVEL libusb log level: NONE, ERROR, WARNING, INFO, DEBUG\n" -" --fxlink-log[=FILE] Log fxlink text messages into FILE (append mode). If\n" -" FILE is missing, a timestamp-name will be generated.\n" -" -q, --quiet Quite mode (minimum verbosity)\n" -" -u, --unmount In -s mode, always unmount the disk, even if it was\n" -" mounted by someone else\n" -" -r, --repeat In interactive mode, reconnect infinitely if the\n" -" calculator disconnects (implies -w)\n" +" -v, --verbose Increased verbosity (mostly in -i/-t)\n" +" -w Wait this many seconds for a calculator to connect\n" +" -w Wait indefinitely for a calculator to connect\n" +" -f Filter which calculators we connect to (see below)\n" +" --libusb-log=LEVEL libusb log level (NONE, ERROR, WARNING, INFO, DEBUG)\n" +"\n" +"Mode-specific options:\n" +" --fxlink-log[=FILE] -i: Append fxlink text messages to FILE. Without\n" +" argument, a unique name is generated.\n" +" -r, --repeat -i: Reconnect if the calc disconnects (implies -w)\n" "\n" "Device filters:\n" -" A device filter is a comma-separated list of properties that a device has\n" -" to match in order to be listed or used, such as 'p7,serial=00000001'.\n" -" Several filters can be separated with a semicolon, in which case a device\n" -" will be considered as long as it matches one of the filters. For example,\n" -" 'p7 ; mass_storage,serial=IGQcGRe9'.\n" -"\n" -" The following properties are defined; the libraries in which each can be\n" -" detected and used is indicated in brackets.\n" -" p7 Matches Protocol 7 calculators (all the FX models\n" -" except the G-III). [libusb, udisks2]\n" -" mass_storage Matches Mass Storage calculators (the CG series and\n" -" the G-III). [libusb, udisks2]\n" -" series_cg Matches CG-series calculators. [udisks2]\n" -" series_g3 Matches G-III series calculators. [udisks2]\n" -" serial_number=ID Matches this specific serial number. Requires write\n" -" access to the device in libusb. [libusb, udisks2]\n"; +" A device filter narrows down what devices we list or connect to by\n" +" requiring a list of properties, such as \"p7,serial=00000001\". The\n" +" following properties can be tested:\n" +" - p7 Protocol 7 calcs (all fx models that use FA-124)\n" +" - mass_storage Mass Storage calcs (fx-CG models and the G-III)\n" +" - series_cg fx-CG models [udisks2 only]\n" +" - series_g3 G-III models [udisks2 only]\n" +" - serial= This serial number (needs write access in libusb)\n"; /* Global options */ struct fxlink_options options; int main(int argc, char **argv) { - int rc=1, mode=0, error=0, option=0, loglevel=LIBUSB_LOG_LEVEL_ERROR; + int rc=1, mode=0, error=0, option=0, loglevel=LIBUSB_LOG_LEVEL_WARNING; delay_t delay = delay_seconds(0); - filter_t *filter = NULL; + struct fxlink_filter *filter = NULL; bool repeat = false; - options.quiet = false; - options.force_unmount = false; options.log_file = NULL; + options.verbose = false; + + setlocale(LC_ALL, ""); //--- // Command-line argument parsing @@ -88,16 +84,20 @@ int main(int argc, char **argv) { "blocks", no_argument, NULL, 'b' }, { "send", no_argument, NULL, 's' }, { "interactive", no_argument, NULL, 'i' }, + { "tui", no_argument, NULL, 't' }, { "push", no_argument, NULL, 'p' }, { "libusb-log", required_argument, NULL, LIBUSB_LOG }, - { "quiet", no_argument, NULL, 'q' }, { "fxlink-log", optional_argument, NULL, LOG_TO_FILE }, - { "unmount", no_argument, NULL, 'u' }, { "repeat", no_argument, NULL, 'r' }, + { "verbose", no_argument, NULL, 'v' }, + /* Deprecated options ignored for compatibility: */ + { "quiet", no_argument, NULL, 'q' }, + { "unmount", no_argument, NULL, 'u' }, + { NULL }, }; while(option >= 0 && option != '?') - switch((option = getopt_long(argc, argv, "hlbsipquf:w::r", longs, NULL))) + switch((option = getopt_long(argc, argv, "hlbsitpquf:w::rv", longs, NULL))) { case 'h': fprintf(stderr, help_string, argv[0]); @@ -106,6 +106,7 @@ int main(int argc, char **argv) case 'b': case 's': case 'i': + case 't': case 'p': mode = option; break; @@ -124,20 +125,25 @@ int main(int argc, char **argv) "NONE, ERROR, WARNING, INFO or DEBUG\n", optarg); break; case 'q': - options.quiet = true; + /* Ignored: -q, --quiet used to control some messages but is now + supplanted by not setting -v */ break; case 'u': - options.force_unmount = true; + /* Ignored: -u, --unmount used to force unmounting filesystems after -s + (which is now the only behavior) */ break; case 'r': repeat = true; delay = delay_infinite(); break; + case 'v': + options.verbose = true; + break; case LOG_TO_FILE: if(optarg) options.log_file = fopen(optarg, "a"); else { - char *name = gen_file_name(".", "logfile", "log"); + char *name = fxlink_gen_file_name(".", "logfile", ".log"); printf("--fxlink-log will output in '%s'\n", name); options.log_file = fopen(name, "a"); free(name); @@ -151,25 +157,25 @@ int main(int argc, char **argv) char *end; int seconds = strtol(optarg, &end, 10); if(seconds < 0 || *end != 0) { - error = err("invalid delay '%s'\n", optarg); + error = elog("invalid delay '%s'\n", optarg); break; } delay = delay_seconds(seconds); break; case 'f': - filter = filter_parse(optarg); + filter = fxlink_filter_parse(optarg); break; case '?': error = 1; } if(mode == 's' && optind == argc) - error = err("send mode requires additional arguments (file names)"); + error = elog("send mode requires additional arguments (file names)\n"); if(mode == 'p' && optind == argc) - error = err("push mode requires a file name"); + error = elog("push mode requires a file name\n"); if(mode == 'p' && optind < argc-1) - error = err("push mode only accepts one file name"); + error = elog("push mode only accepts one file name\n"); /* No arguments or bad arguments */ if(error) @@ -179,6 +185,10 @@ int main(int argc, char **argv) return 1; } + /* Default filter */ + if(filter == NULL) + filter = calloc(1, sizeof *filter); + //--- // libusb initialization //--- @@ -186,10 +196,11 @@ int main(int argc, char **argv) libusb_context *context = NULL; /* Initialize libusb for corresponding modes */ - if(mode == 'l' || mode == 'i' || mode == 'p') { + if(mode == 'l' || mode == 'i' || mode == 't' || mode == 'p') { if((rc = libusb_init(&context))) - return libusb_err(rc, "error initializing libusb"); + return elog_libusb(rc, "error initializing libusb"); libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, loglevel); + fxlink_log_grab_libusb_logs(); } //--- @@ -203,14 +214,14 @@ int main(int argc, char **argv) #ifndef FXLINK_DISABLE_UDISKS2 rc = main_blocks(filter, &delay); #else - rc = err("this fxlink was built without UDisks2; -b is disabled"); + rc = elog("this fxlink was built without UDisks2; -b is disabled"); #endif } else if(mode == 's') { #ifndef FXLINK_DISABLE_UDISKS2 rc = main_send(filter, &delay, argv + optind); #else - rc = err("this fxlink was built without UDisks2; -s is disabled"); + rc = elog("this fxlink was built without UDisks2; -s is disabled"); #endif } else if(mode == 'i') { @@ -219,99 +230,17 @@ int main(int argc, char **argv) } while(repeat); } + else if(mode == 't') { + rc = main_tui_interactive(context); + } else if(mode == 'p') { rc = main_push(filter, &delay, context, argv + optind); } + fxlink_filter_free(filter); if(context) libusb_exit(context); if(options.log_file) fclose(options.log_file); return rc; } - -//--- -// Device list -//--- - -int main_list(filter_t *filter, delay_t *delay, libusb_context *context) -{ - /* Wait for a device to be connected */ - filter_clean_libusb(filter); - usb_unique_wait(filter, delay, context, NULL); - - int total_devices = 0; - bool error; - - for_libusb_devices(it, context, &error) { - if(!filter_match(&it.props, filter)) continue; - - if(total_devices > 0) printf("\n"); - - if(it.dc.idProduct == 0x6101) - printf("fx-9860G series (Protocol 7) calculator\n"); - else if(it.dc.idProduct == 0x6102) - printf("fx-CG or G-III series (USB Mass Storage) calculator\n"); - else - printf("Unknown calculator (idProduct: %04x)\n", it.dc.idProduct); - - printf(" Device location: Bus %d, Port %d, Device %d\n", - libusb_get_bus_number(it.dev), - libusb_get_port_number(it.dev), - libusb_get_device_address(it.dev)); - printf(" Identification: idVendor: %04x, idProduct: %04x\n", - it.dc.idVendor, it.dc.idProduct); - /* FIXME: This assumes a short path (no hub or dual-device) */ - printf(" Guessed sysfs path: /sys/bus/usb/devices/%d-%d/\n", - libusb_get_bus_number(it.dev), - libusb_get_port_number(it.dev)); - - char *serial = it.dh ? usb_serial_number(it.dh) : NULL; - if(serial) - printf(" Serial number: %s\n", serial); - free(serial); - - printf(" Properties: "); - properties_print(stdout, &it.props); - printf("\n"); - - total_devices++; - } - - if(!error && !total_devices) - printf("No%s device found.\n", filter ? " matching" : ""); - return 0; -} - -//--- -// libudev tests; work but not useful yet -//--- - -#if 0 -#include -int main_udev_test(libusb_device *dev) -{ - struct udev *udev = NULL; - struct udev_device *udev_device = NULL; - - udev = udev_new(); - if(!udev) return err("cannot create udev context"); - - static char sys_path[128]; - sprintf(sys_path, "/sys/bus/usb/devices/%d-%d", - libusb_get_bus_number(dev), - libusb_get_port_number(dev)); - - udev_device = udev_device_new_from_syspath(udev, sys_path); - if(!udev_device) { - udev_unref(udev); - return err("cannot get udev device for %s", sys_path); - } - - printf("Device number: %ld\n", udev_device_get_devnum(udev_device)); - printf("Device devnode: %s\n", udev_device_get_devnode(udev_device)); - - if(udev) udev_unref(udev); - return 0; -} -#endif diff --git a/fxlink/modes/interactive.c b/fxlink/modes/interactive.c new file mode 100644 index 0000000..1ed7ab7 --- /dev/null +++ b/fxlink/modes/interactive.c @@ -0,0 +1,168 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include "../fxlink.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static void handle_new_message(struct fxlink_device *fdev, + struct fxlink_message *msg) +{ + char const *path = "."; + + if(fxlink_message_is_fxlink_image(msg)) { + 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); + hlog("calculators %s", fxlink_device_id(fdev)); + log_("saved image (%dx%d, format=%d) to '%s'\n", + img->width, img->height, img->pixel_format, filename); + } + free(filename); + return; + } + + if(fxlink_message_is_fxlink_text(msg)) { + char const *str = msg->data; + + if(options.verbose) + printf("------------------\n"); + fwrite(str, 1, msg->size, stdout); + if(str[msg->size - 1] != '\n') { + if(!options.verbose) + printf("\e[30;47m%%\e[0m"); + printf("\n"); + } + if(options.verbose) { + printf("------------------\n"); + } + if(options.verbose) { + for(size_t i = 0; i < msg->size; i++) { + printf(" %02x", str[i]); + if((i & 15) == 15 || i == msg->size - 1) + printf("\n"); + } + } + + if(options.log_file) + fwrite(str, 1, msg->size, options.log_file); + return; + } + + if(fxlink_message_is_fxlink_video(msg)) { + struct fxlink_message_image_raw *raw = + fxlink_message_image_decode(msg); + if(raw) { + fxlink_sdl2_display_raw(raw); + fxlink_message_image_raw_free(raw); + } + return; + } + + /* 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); +} + +int main_interactive(struct fxlink_filter *filter, delay_t *delay, + libusb_context *ctx) +{ + /* Wait for a device to be connected */ + fxlink_filter_clean_libusb(filter); + filter->intf_fxlink = true; + + struct fxlink_device *fdev = fxlink_device_find_wait(ctx, filter, delay); + if(!fdev) { + printf("No device found.\n"); + return 1; + } + if(!fxlink_device_claim_fxlink(fdev)) { + fxlink_device_cleanup(fdev); + free(fdev); + return 1; + } + + hlog("interactive"); + log_("connected to %s\n", fxlink_device_id(fdev)); + + /* Buffer used to receive messages */ + static uint8_t buffer[2048]; + /* Current message */ + struct fxlink_transfer *tr = NULL; + + while(1) { + fxlink_sdl2_handle_events(); + + int transferred = -1; + int rc = libusb_bulk_transfer(fdev->dh, fdev->comm->ep_bulk_IN, buffer, + sizeof buffer, &transferred, 500 /* ms */); + + if(rc == LIBUSB_ERROR_NO_DEVICE) { + hlog("interactive"); + log_("disconnected, leaving\n"); + break; + } + else if(rc && rc != LIBUSB_ERROR_TIMEOUT) { + elog_libusb(rc, "bulk transfer failed on %s", + fxlink_device_id(fdev)); + continue; + } + if(transferred <= 0) + continue; + + /* Either start a new message or continue an unfinished one */ + if(tr == NULL) + tr = fxlink_transfer_make_IN(buffer, transferred); + else + fxlink_transfer_receive(tr, buffer, transferred); + + if(tr && fxlink_transfer_complete(tr)) { + struct fxlink_message *msg = fxlink_transfer_finish_IN(tr); + if(msg) { + handle_new_message(fdev, msg); + fxlink_message_free(msg, true); + } + tr = NULL; + } + } + + /* Warning for unfinished transfer */ + if(tr) { + wlog("unfinished transfer interrupted by disconnection\n"); + // TODO: Proper way to free a transfer without finishing it + free(tr); + } + + fxlink_device_cleanup(fdev); + free(fdev); + return 0; +} diff --git a/fxlink/modes/list.c b/fxlink/modes/list.c new file mode 100644 index 0000000..d6492c9 --- /dev/null +++ b/fxlink/modes/list.c @@ -0,0 +1,104 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include "../fxlink.h" +#include +#include +#include +#include + +static int print_devices(struct fxlink_device_list const *list, + struct fxlink_filter const *filter) +{ + int total_devices = 0; + + for(int i = 0; i < list->count; i++) { + struct fxlink_device const *fdev = &list->devices[i]; + if(!fdev->calc) + continue; + + struct fxlink_filter properties; + fxlink_device_get_properties(fdev, &properties); + if(!fxlink_filter_match(&properties, filter)) + continue; + + if(total_devices > 0) + printf("\n"); + + if(fdev->idProduct == 0x6101) + printf("fx-9860G series (Protocol 7) calculator\n"); + else if(fdev->idProduct == 0x6102) + printf("fx-CG or G-III series (USB Mass Storage) calculator\n"); + else + printf("Unknown calculator (idProduct: %04x)\n", fdev->idProduct); + + printf(" Device location: Bus %d, Port %d, Device %d\n", + libusb_get_bus_number(fdev->dp), + libusb_get_port_number(fdev->dp), + libusb_get_device_address(fdev->dp)); + printf(" Identification: idVendor: %04x, idProduct: %04x\n", + fdev->idVendor, fdev->idProduct); + /* FIXME: This assumes a short path (no hub or dual-device) */ + printf(" Guessed sysfs path: /sys/bus/usb/devices/%d-%d/\n", + libusb_get_bus_number(fdev->dp), + libusb_get_port_number(fdev->dp)); + + if(fdev->calc->serial) + printf(" Serial number: %s\n", fdev->calc->serial); + + printf(" System: %s (%s)\n", + fxlink_device_system_string(fdev), + fxlink_device_status_string(fdev)); + + printf(" Interfaces: "); + for(int i = 0; i < fdev->calc->interface_count; i++) + printf(" %02x.%02x", + fdev->calc->classes[i] >> 8, fdev->calc->classes[i] & 0xff); + printf("\n"); + + printf(" Properties: "); + fxlink_filter_print(stdout, &properties); + printf("\n"); + + total_devices++; + } + + return total_devices; +} + +static void discard_logs(int display_fmt, char const *str) +{ + (void)display_fmt; + (void)str; +} + +int main_list(struct fxlink_filter *filter, delay_t *delay, + libusb_context *ctx) +{ + /* Silence all logs for this mode */ + fxlink_log_set_handler(discard_logs); + + struct fxlink_device_list list; + struct timeval zero_tv = { 0 }; + fxlink_device_list_track(&list, ctx); + + while(1) { + libusb_handle_events_timeout(ctx, &zero_tv); + fxlink_device_list_refresh(&list); + + int n = print_devices(&list, filter); + if(n > 0) + break; + + if(delay_cycle(delay)) { + printf("No%s device found.\n", filter ? " matching" : ""); + break; + } + } + + fxlink_device_list_stop(&list); + return 0; +} diff --git a/fxlink/push.c b/fxlink/modes/push.c similarity index 52% rename from fxlink/push.c rename to fxlink/modes/push.c index 4f2423e..65073e6 100644 --- a/fxlink/push.c +++ b/fxlink/modes/push.c @@ -1,21 +1,25 @@ -#include "config.h" -#include "fxlink.h" -#include "util.h" -#include "properties.h" -#include "filter.h" -#include "usb.h" +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include "../fxlink.h" +#include +#include +#include -#include #include #include #include -int main_push(filter_t *filter, delay_t *delay, libusb_context *context, char** files) +int main_push(struct fxlink_filter *filter, delay_t *delay, + libusb_context *ctx, char **files) { int rc = 1; - libusb_device *dev = NULL; - libusb_device_handle *dh = NULL; + struct fxlink_device *fdev = NULL; + /* Load binary file */ FILE *fp = fopen(files[0], "rb"); if (!fp) { printf("error: Unable to open file %s\n", files[0]); @@ -35,45 +39,36 @@ int main_push(filter_t *filter, delay_t *delay, libusb_context *context, char** fclose(fp); /* Wait for a device to be connected */ - filter_clean_libusb(filter); - rc = usb_unique_wait(filter, delay, context, &dev); + fxlink_filter_clean_libusb(filter); + filter->intf_cesg502 = true; - if(rc == FILTER_NONE) { + fdev = fxlink_device_find_wait(ctx, filter, delay); + if(!fdev) { printf("No device found.\n"); return 1; } - else if(rc == FILTER_MULTIPLE) { - printf("Multiple devices found, ambiguous!\n"); - return 1; - } - if((rc = libusb_open(dev, &dh))) { - rc = libusb_err(rc, "cannot open device %s", usb_id(dev)); + /* The device uses CESG502, so drive the interface manually */ + if((rc = libusb_claim_interface(fdev->dh, 0))) { + hlog("calculators %s", fxlink_device_id(fdev)); + rc = elog_libusb(rc, "cannot claim interface 0"); goto end; } - /* Don't detach kernel drivers to avoid breaking the Mass Storage - communications if fxlink is ever started while the native LINK - application is running! */ - libusb_set_auto_detach_kernel_driver(dh, false); - - if((rc = libusb_claim_interface(dh, 0))) { - rc = libusb_err(rc, "cannot claim interface on %s", usb_id(dev)); - goto end; - } - - printf("Connected to %s\n", usb_id(dev)); + hlog("push"); + log_("connected to %s\n", fxlink_device_id(fdev)); // Wait to receive "USB loader ready" over USB bulk transfer uint8_t buf[18]; while (1) { int actual_length; - rc = libusb_bulk_transfer(dh, 0x82, buf, sizeof(buf) - 1, &actual_length, 0); + rc = libusb_bulk_transfer(fdev->dh, 0x82, buf, sizeof(buf) - 1, + &actual_length, 0); buf[sizeof(buf) - 1] = 0; // if (rc == LIBUSB_ERROR_TIMEOUT) continue; if (rc) { - rc = libusb_err(rc, "cannot receive data: %s", usb_id(dev)); + rc = elog_libusb(rc, "cannot receive data"); goto end; } if (actual_length == 0) continue; @@ -99,9 +94,10 @@ int main_push(filter_t *filter, delay_t *delay, libusb_context *context, char** sizebuf[1] = (fsize >> 16) & 0xFF; sizebuf[2] = (fsize >> 8) & 0xFF; sizebuf[3] = fsize & 0xFF; - rc = libusb_bulk_transfer(dh, 0x01, sizebuf, sizeof(sizebuf), NULL, 0); + rc = libusb_bulk_transfer(fdev->dh, 0x01, sizebuf, sizeof(sizebuf), NULL, + 0); if (rc) { - rc = libusb_err(rc, "cannot send size: %s", usb_id(dev)); + rc = elog_libusb(rc, "cannot send size"); goto end; } @@ -110,9 +106,10 @@ int main_push(filter_t *filter, delay_t *delay, libusb_context *context, char** int sent = 0; while (sent < fsize) { int actual_length; - rc = libusb_bulk_transfer(dh, 0x01, filebuf + sent, fsize - sent, &actual_length, 0); + rc = libusb_bulk_transfer(fdev->dh, 0x01, filebuf + sent, fsize - sent, + &actual_length, 0); if (rc) { - rc = libusb_err(rc, "cannot send data: %s", usb_id(dev)); + rc = elog_libusb(rc, "cannot send data"); goto end; } sent += actual_length; @@ -120,10 +117,10 @@ int main_push(filter_t *filter, delay_t *delay, libusb_context *context, char** printf("Sent %d bytes\n", sent); end: - if(dh) { - libusb_release_interface(dh, 0); - libusb_close(dh); + if(fdev) { + libusb_release_interface(fdev->dh, 0); + fxlink_device_cleanup(fdev); + free(fdev); } - if(dev) libusb_unref_device(dev); return rc; } diff --git a/fxlink/modes/tui-interactive.c b/fxlink/modes/tui-interactive.c new file mode 100644 index 0000000..45473c8 --- /dev/null +++ b/fxlink/modes/tui-interactive.c @@ -0,0 +1,532 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include "../fxlink.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static struct TUIData { + /* SIGWINCH flag */ + bool resize_needed; + /* ncurses window panels */ + WINDOW *wStatus; + WINDOW *wLogs; + WINDOW *wTransfers; + WINDOW *wTextOutput; + WINDOW *wConsole; + /* Root box */ + struct fxlink_TUI_box *bRoot; + /* Application data */ + struct fxlink_pollfds polled_fds; + struct fxlink_device_list devices; + +} 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(IN->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 , 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); +} + +int main_tui_interactive(libusb_context *ctx) +{ + if(!TUI_setup()) + return elog("error: failed to setup ncurses TUI o(x_x)o\n"); + + /* 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); + + 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); + char const *prompt = "> "; + + print(TUI.wConsole, "%s", prompt); + TUI_render_all(true); + TUI_refresh_all(true); + + struct fxlink_TUI_input input; + fxlink_TUI_input_init(&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; + } + + 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; + if(command[0] != 0) + log_("command: '%s'\n", command); + if(!strcmp(command, "q")) + break; + fxlink_TUI_input_free(&input); + print(TUI.wConsole, "%s", prompt); + fxlink_TUI_input_init(&input, TUI.wConsole, 16); + TUI_refresh_console(); + } + } + + if(update_usb) { + libusb_handle_events_timeout(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); + } + + /* 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); + } + } + + TUI_render_all(false); + TUI_refresh_all(false); + } + + if(update_sdl) { + fxlink_sdl2_handle_events(); + } + } + + 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; +} diff --git a/fxlink/modes/udisks2.c b/fxlink/modes/udisks2.c new file mode 100644 index 0000000..72053b9 --- /dev/null +++ b/fxlink/modes/udisks2.c @@ -0,0 +1,125 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#ifndef FXLINK_DISABLE_UDISKS2 + +#include "../fxlink.h" +#include +#include +#include + +#include +#include +#include + +int main_send(struct fxlink_filter *filter, delay_t *delay, char **files) +{ + fxlink_filter_clean_udisks2(filter); + GError *error = NULL; + char **argv = NULL; + int rc = 0; + + UDisksClient *udc = NULL; + UDisksManager *udm = NULL; + if(ud2_start(&udc, &udm)) return 1; + + UDisksBlock *block = NULL; + UDisksDrive *drive = NULL; + UDisksFilesystem *fs = NULL; + rc = ud2_unique_wait(filter, delay, udc, udm, &block, &drive, &fs); + if(rc != FXLINK_FILTER_UNIQUE) { + rc = 1; + goto end; + } + + /* Determine a mount folder, mounting the volume if needed */ + gchar *folder = NULL; + + gchar const *dev = udisks_block_get_device(block); + gchar const * const * mount_points = + udisks_filesystem_get_mount_points(fs); + + if(!mount_points || !mount_points[0]) { + GVariant *args = g_variant_new("a{sv}", NULL); + udisks_filesystem_call_mount_sync(fs, args, &folder, NULL, &error); + if(error) { + rc = elog("cannot mount %s: %s\n", dev, error->message); + goto end; + } + printf("Mounted %s to %s.\n", dev, folder); + } + else { + folder = strdup(mount_points[0]); + printf("Already mounted at %s.\n", folder); + } + + /* Copy files with external cp(1) */ + int file_count = 0; + while(files[file_count]) file_count++; + + argv = malloc((file_count + 3) * sizeof *argv); + if(!argv) { + rc = elog("cannot allocate argv array for cp(1)\n"); + goto end; + } + argv[0] = "cp"; + for(int i = 0; files[i]; i++) + argv[i+1] = files[i]; + argv[file_count+1] = folder; + argv[file_count+2] = NULL; + + /* Print command */ + printf("Running cp"); + for(int i = 1; argv[i]; i++) printf(" '%s'", argv[i]); + printf("\n"); + + pid_t pid = fork(); + + if(pid == 0) { + execvp("cp", argv); + } + else if(pid == -1) { + rc = elog("failed to fork to invoke cp\n"); + goto end; + } + else { + int wstatus; + waitpid(pid, &wstatus, 0); + + if(!WIFEXITED(wstatus)) + elog("process did not terminate normally\n"); + else if(WEXITSTATUS(wstatus) != 0) { + elog("process terminated with error %d\n", WEXITSTATUS(wstatus)); + } + } + + /* Unmount the filesystem and eject the device */ + GVariant *args = g_variant_new("a{sv}", NULL); + udisks_filesystem_call_unmount_sync(fs, args, NULL, &error); + if(error) + elog("while unmounting %s: %s\n", dev, error->message); + else + printf("Unmounted %s.\n", dev); + + args = g_variant_new("a{sv}", NULL); + udisks_drive_call_power_off_sync(drive, args, NULL, &error); + if(error) + elog("while ejecting %s: %s\n", dev, error->message); + else + printf("Ejected %s.\n", dev); + +end: + free(folder); + if(argv) free(argv); + if(fs) g_object_unref(fs); + if(drive) g_object_unref(drive); + if(block) g_object_unref(block); + if(udc && udm) ud2_end(udc, udm); + return rc; +} + +#endif /* FXLINK_DISABLE_UDISKS2 */ diff --git a/fxlink/png.c b/fxlink/png.c deleted file mode 100644 index 89f9453..0000000 --- a/fxlink/png.c +++ /dev/null @@ -1,42 +0,0 @@ -#include "png.h" -#include "util.h" -#include - -/* fxlink_png_save(): Save a bitmap into a PNG file */ -int fxlink_png_save(png_byte **row_pointers, int width, int height, - char const *path) -{ - png_struct *png = png_create_write_struct(PNG_LIBPNG_VER_STRING, - NULL, NULL, NULL); - if(!png) - return err("failed to write PNG: png_create_write_struct"); - - png_infop info = png_create_info_struct(png); - if(!info) - return err("failed to write PNG: png_create_info_struct"); - - FILE *fp = fopen(path, "wb"); - if(!fp) { - png_destroy_write_struct(&png, &info); - return err("failed to open '%s' to write PNG: %m", path); - } - - if(setjmp(png_jmpbuf(png))) { - fclose(fp); - png_destroy_write_struct(&png, &info); - return 1; - } - - png_init_io(png, fp); - png_set_IHDR(png, info, - width, height, 8, - PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, - PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - - png_write_info(png, info); - png_write_image(png, row_pointers); - png_write_end(png, NULL); - png_destroy_write_struct(&png, &info); - fclose(fp); - return 0; -} diff --git a/fxlink/png.h b/fxlink/png.h deleted file mode 100644 index acc76a7..0000000 --- a/fxlink/png.h +++ /dev/null @@ -1,14 +0,0 @@ -//--- -// fxlink:png - Tools to output PNG images with libpng -//--- - -#ifndef FXLINK_PNG_H -#define FXLINK_PNG_H - -#include - -/* fxlink_png_save(): Save a bitmap into a PNG file */ -int fxlink_png_save(png_byte **row_pointers, int width, int height, - char const *path); - -#endif /* FXLINK_PNG_H */ diff --git a/fxlink/properties.c b/fxlink/properties.c deleted file mode 100644 index 726c6cd..0000000 --- a/fxlink/properties.c +++ /dev/null @@ -1,42 +0,0 @@ -#include "properties.h" -#include -#include -#include - -bool properties_match(properties_t const *props, properties_t const *option) -{ - if(option->p7 && !props->p7) - return false; - if(option->mass_storage && !props->mass_storage) - return false; - if(option->series_cg && !props->series_cg) - return false; - if(option->series_g3 && !props->series_g3) - return false; - if(option->serial_number && (!props->serial_number || - strcmp(option->serial_number, props->serial_number))) - return false; - - return true; -} - -void properties_print(FILE *fp, properties_t const *props) -{ - #define output(...) { \ - if(sep) fprintf(fp, ", "); \ - fprintf(fp, __VA_ARGS__); \ - sep = true; \ - } - - bool sep = false; - if(props->p7) - output("p7"); - if(props->mass_storage) - output("mass_storage"); - if(props->series_cg) - output("series_cg"); - if(props->series_g3) - output("series_g3"); - if(props->serial_number) - output("serial_number=%s", props->serial_number); -} diff --git a/fxlink/properties.h b/fxlink/properties.h deleted file mode 100644 index 0b988a7..0000000 --- a/fxlink/properties.h +++ /dev/null @@ -1,65 +0,0 @@ -//--- -// fxlink:properties - Detected models and properties of devices -//--- - -#ifndef FXLINK_PROPERTIES_H -#define FXLINK_PROPERTIES_H - -#include -#include -#include - -/* properties_t: Type of properties that can be detected on a device - - This structure lists all the properties that fxlink can detect on connected - devices. These properties help identify the devices in interactive use, and - accurately specify which calculators to interact with when several models - are connected simultaneously or when the same command in run against - different models in a script. - - Depending on the backend and access privileges, not all properties can be - detected. The backends supporting each property are listed in brackets at - the end of each description. - - An instance of this structure can also be used as a filter. In order for the - semantics of filtering to work out, every attribute needs to have a "total - information order", meaning that two values can be compared for how specific - they are. A device will match a filter if and only if all of its properties - are more specific than the values provided in the filter. - - Currently the order is "true more specific than false" for all booleans and - "any value more specific than NULL" for the serial number. Properties that - cannot be detected by back-ends are reset to their least specific value (ie. - ignored). */ -typedef struct { - - /* The calculator is a Protocol 7 calculator (idProduct: 0x6101). This - makes no sense in UDisks2 as a P7 calculator has no disks, therefore - this property is always false in UDisks2. [libusb, udisks2] */ - bool p7; - /* The calculator is a Mass Storage calculator (idProduct: 0x6102). All - devices detected in UDisks2 are of this type. [libusb, udisks2] */ - bool mass_storage; - /* The calculator is an fx-CG series. [udisks2] */ - bool series_cg; - /* The calculator is a G-III series. [udisks2] */ - bool series_g3; - - /* Serial number. This can only be obtained in libusb if the user has - write access to the device, because libusb needs to send a request for - the STRING descriptor holding the serial number. [libusb, udisks2] */ - char *serial_number; - -} properties_t; - -/* properties_match(): Check whether some properties match a given option - - Returns true if (props) is more specific than (option), meaning that every - property mentioned in (option) is indeed set in (props). This is a building - block for filter_match() and probably doesn't need to be used directly. */ -bool properties_match(properties_t const *props, properties_t const *option); - -/* properties_print(): Print a property set (one-line) */ -void properties_print(FILE *fp, properties_t const *props); - -#endif /* FXLINK_PROPERTIES_H */ diff --git a/fxlink/protocol.c b/fxlink/protocol.c index e901007..87e23ca 100644 --- a/fxlink/protocol.c +++ b/fxlink/protocol.c @@ -1,140 +1,288 @@ -#include "protocol.h" -#include "util.h" +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// -#include -#include +#include +#include #include +#include +#include +#include "fxlink.h" + +bool fxlink_message_is_apptype(struct fxlink_message const *msg, + char const *application, char const *type) +{ + return memchr(application, '\0', 17) != NULL + && memchr(type, '\0', 17) != NULL + && !strncmp(msg->application, application, 16) + && !strncmp(msg->type, type, 16); +} + +void fxlink_message_free(struct fxlink_message *message, bool free_data) +{ + if(free_data) + free(message->data); + free(message); +} //--- -// Image format +// Image decoding //--- static int img_bytes_per_row(int format, int width) { - if(format == USB_FXLINK_IMAGE_RGB565) - return 2 * width; - if(format == USB_FXLINK_IMAGE_MONO) - return (width + 7) >> 3; - if(format == USB_FXLINK_IMAGE_GRAY) - return 2 * ((width + 7) >> 3); + if(format == FXLINK_MESSAGE_IMAGE_RGB565) + return 2 * width; + if(format == FXLINK_MESSAGE_IMAGE_MONO) + return (width + 7) >> 3; + if(format == FXLINK_MESSAGE_IMAGE_GRAY) + return 2 * ((width + 7) >> 3); - return 0; + return 0; } -static void decode_rgb565(void *pixels, int width, int height, int size, - uint8_t **row_pointers) +static struct fxlink_message_image_raw *decode_rgb565(void const *pixels, + int size, struct fxlink_message_image_raw *raw) { - int bpr = img_bytes_per_row(USB_FXLINK_IMAGE_RGB565, width); + int bpr = img_bytes_per_row(FXLINK_MESSAGE_IMAGE_RGB565, raw->width); - for(int y = 0; y < height; y++) { - void *row = pixels + y * bpr; + for(int y = 0; y < raw->height; y++) { + void const *row = pixels + y * bpr; - for(int x = 0; x < width; x++) { - /* Don't read past the read buffer if the image is incomplete */ - void *input = row + 2 * x; - uint16_t color = 0; - if(input - pixels + 2 <= size) color = *(uint16_t *)input; + for(int x = 0; x < raw->width; x++) { + /* Don't read past the read buffer if the image is incomplete */ + void const *input = row + 2 * x; + uint16_t color = 0; + if(input - pixels + 2 <= size) color = *(uint16_t *)input; - color = ((color & 0xff00) >> 8) | ((color & 0x00ff) << 8); + color = ((color & 0xff00) >> 8) | ((color & 0x00ff) << 8); - row_pointers[y][3*x+0] = (color >> 11) << 3; - row_pointers[y][3*x+1] = ((color >> 5) & 0x3f) << 2; - row_pointers[y][3*x+2] = (color & 0x1f) << 3; - } - } + raw->data[y][3*x+0] = (color >> 11) << 3; + raw->data[y][3*x+1] = ((color >> 5) & 0x3f) << 2; + raw->data[y][3*x+2] = (color & 0x1f) << 3; + } + } + return raw; } -static void decode_mono(void *pixels, int width, int height, int size, - uint8_t **row_pointers) +static struct fxlink_message_image_raw *decode_mono(void const *pixels, + int size, struct fxlink_message_image_raw *raw) { - int bpr = img_bytes_per_row(USB_FXLINK_IMAGE_MONO, width); + int bpr = img_bytes_per_row(FXLINK_MESSAGE_IMAGE_MONO, raw->width); - for(int y = 0; y < height; y++) { - void *row = pixels + y * bpr; + for(int y = 0; y < raw->height; y++) { + void const *row = pixels + y * bpr; - for(int x = 0; x < width; x++) { - /* Don't read past the read buffer if the image is incomplete */ - void *input = row + (x >> 3); - int byte = 0; - if(input - pixels + 1 <= size) byte = *(uint8_t *)input; - int color = (byte & (0x80 >> (x & 7))) ? 0 : 255; + for(int x = 0; x < raw->width; x++) { + /* Don't read past the read buffer if the image is incomplete */ + void const *input = row + (x >> 3); + int byte = 0; + if(input - pixels + 1 <= size) byte = *(uint8_t *)input; + int color = (byte & (0x80 >> (x & 7))) ? 0 : 255; - row_pointers[y][3*x+0] = color; - row_pointers[y][3*x+1] = color; - row_pointers[y][3*x+2] = color; - } - } + raw->data[y][3*x+0] = color; + raw->data[y][3*x+1] = color; + raw->data[y][3*x+2] = color; + } + } + return raw; } -static void decode_gray(void *pixels, int width, int height, int size, - uint8_t **row_pointers) +static struct fxlink_message_image_raw *decode_gray(void const *pixels, + int size, struct fxlink_message_image_raw *raw) { - int bpr = img_bytes_per_row(USB_FXLINK_IMAGE_MONO, width); + int bpr = img_bytes_per_row(FXLINK_MESSAGE_IMAGE_MONO, raw->width); - for(int k = 0; k < 2 * height; k++) { - void *row = pixels + k * bpr; - int y = k % height; + for(int k = 0; k < 2 * raw->height; k++) { + void const *row = pixels + k * bpr; + int y = k % raw->height; - for(int x = 0; x < width; x++) { - /* Don't read past the read buffer if the image is incomplete */ - void *input = row + (x >> 3); - int byte = 0; - if(input - pixels + 1 <= size) byte = *(uint8_t *)input; + for(int x = 0; x < raw->width; x++) { + /* Don't read past the read buffer if the image is incomplete */ + void const *input = row + (x >> 3); + int byte = 0; + if(input - pixels + 1 <= size) byte = *(uint8_t *)input; - int color = (byte & (0x80 >> (x & 7))); - /* Everything is inverted */ - if(!color) color = (k >= height) ? 0xaa : 0x55; - else color = 0x00; + int color = (byte & (0x80 >> (x & 7))); + /* Everything is inverted */ + if(!color) color = (k >= raw->height) ? 0xaa : 0x55; + else color = 0x00; - row_pointers[y][3*x+0] += color; - row_pointers[y][3*x+1] += color; - row_pointers[y][3*x+2] += color; - } - } + raw->data[y][3*x+0] += color; + raw->data[y][3*x+1] += color; + raw->data[y][3*x+2] += color; + } + } + return raw; } -uint8_t **fxlink_protocol_decode_image(message_t *msg) +struct fxlink_message_image_raw *fxlink_message_image_decode( + struct fxlink_message const *msg) { - usb_fxlink_image_t *img = (void *)msg->output; - void *pixels = msg->output + sizeof *img; + if(!fxlink_message_is_fxlink_image(msg) + && !fxlink_message_is_fxlink_video(msg)) + return NULL; - /* Compute the amount of data for the specified image format and size */ - int bytes_per_row = img_bytes_per_row(img->pixel_format, img->width); - int expected_size = img->height * bytes_per_row; + struct fxlink_message_image_header *img = msg->data; + void *pixels = msg->data + sizeof *img; + int pixels_size = msg->size - sizeof *img; - /* Check that the correct amount of data was sent */ - int size = msg->size_read - sizeof *img; - if(size < expected_size) - printf("warning: got %d bytes but needed %d, image will be " - "incomplete\n", size, expected_size); - if(size > expected_size) - printf("warning: got %d bytes but needed %d for image, dropping extra" - "\n", size, expected_size); + /* Determine the expected data size */ + int bytes_per_row = img_bytes_per_row(img->pixel_format, img->width); + int expected_size = img->height * bytes_per_row; - /* Allocate row pointers */ - uint8_t **row_pointers = malloc(img->height*sizeof *row_pointers); - if(!row_pointers) { - err("failed to write allocate memory to decode image"); - return NULL; - } + if(pixels_size < expected_size) + wlog("image has %d bytes but needs %d, it will be incomplete\n", + pixels_size, expected_size); + if(pixels_size > expected_size) + wlog("image has %d bytes but only needs %d, dropping extra\n", + pixels_size, expected_size); - for(size_t y = 0; y < img->height; y++) { - row_pointers[y] = calloc(img->width, 3); - if(!row_pointers[y]) { - err("failed to write allocate memory to decode image"); - for(size_t i = 0 ; i < y; i++) free(row_pointers[i]); - free(row_pointers); - return NULL; - } - } + /* Allocate memory for the decoded pixels */ + struct fxlink_message_image_raw *raw = malloc(sizeof *raw); + uint8_t **data = calloc(img->height, sizeof(uint8_t *)); + if(!raw || !data) + goto alloc_error; + for(size_t i = 0; i < img->height; i++) { + data[i] = calloc(img->width, 3); + if(!data[i]) + goto alloc_error; + } - /* Decode image */ - if(img->pixel_format == USB_FXLINK_IMAGE_RGB565) - decode_rgb565(pixels, img->width, img->height, size, row_pointers); - if(img->pixel_format == USB_FXLINK_IMAGE_MONO) - decode_mono(pixels, img->width, img->height, size, row_pointers); - if(img->pixel_format == USB_FXLINK_IMAGE_GRAY) - decode_gray(pixels, img->width, img->height, size, row_pointers); + raw->width = img->width; + raw->height = img->height; + raw->data = data; - return row_pointers; + if(img->pixel_format == FXLINK_MESSAGE_IMAGE_RGB565) + return decode_rgb565(pixels, pixels_size, raw); + if(img->pixel_format == FXLINK_MESSAGE_IMAGE_MONO) + return decode_mono(pixels, pixels_size, raw); + if(img->pixel_format == FXLINK_MESSAGE_IMAGE_GRAY) + return decode_gray(pixels, pixels_size, raw); + + elog("unknown pixel format: %d\n", img->pixel_format); + fxlink_message_image_raw_free(raw); + return NULL; + +alloc_error: + elog("cannot allocate memory for decoded image: %m\n"); + fxlink_message_image_raw_free(raw); + return NULL; +} + +void fxlink_message_image_raw_free(struct fxlink_message_image_raw *raw) +{ + if(raw->data){ + for(int i = 0; i < raw->height; i++) + free(raw->data[i]); + } + free(raw->data); + free(raw); +} + +//--- +// Tools for crafting and receiving messages +//--- + +/* Check whether a header is valid. It's important for this function to be as + strict as possible so that picking up communication in the wrong spot + doesn't get out of control by recognizing messages everywhere. */ +static bool header_valid(struct fxlink_message const *msg) +{ + if(msg->version == 0x00000100) { + /* Transfer size larger than 16 MB: impossible for so many reasons */ + if(msg->transfer_size > 0xffffff) + return false; + /* Non-printable characters in application/type names */ + for(size_t i = 0; i < sizeof msg->application; i++) { + if(msg->application[i] && !isprint(msg->application[i])) + return false; + } + for(size_t i = 0; i < sizeof msg->type; i++) { + if(msg->type[i] && !isprint(msg->type[i])) + return false; + } + return true; + } + return false; +} + +struct fxlink_transfer *fxlink_transfer_make_IN(void *data, int size) +{ + int header_size = FXLINK_MESSAGE_HEADER_SIZE; + + if(size < header_size) { + elog("cannot read message: header too short (%d < %zu bytes)\n", + size, header_size); + return NULL; + } + + struct fxlink_transfer *tr = calloc(1, sizeof *tr); + if(!tr) { + elog("cannot allocate transfer structure: %m\n"); + return NULL; + } + + memcpy(&tr->msg, data, header_size); + if(!header_valid(&tr->msg)) { + elog("cannot read message: invalid header (%d bytes dropped)\n", + size); + free(tr); + return NULL; + } + + tr->msg.data = malloc(tr->msg.size); + tr->direction = FXLINK_TRANSFER_IN; + tr->processed_size = 0; + + if(!tr->msg.data) { + elog("cannot allocate buffer for %d bytes\n", tr->msg.size); + free(tr); + return NULL; + } + + fxlink_transfer_receive(tr, data + header_size, size - header_size); + return tr; +} + +struct fxlink_message *fxlink_transfer_finish_IN(struct fxlink_transfer *tr) +{ + if(tr->direction != FXLINK_TRANSFER_IN || !fxlink_transfer_complete(tr)) + return NULL; + + struct fxlink_message *msg = malloc(sizeof *msg); + if(!msg) { + elog("cannot allocate message structure: %m\n"); + return NULL; + } + + if(options.verbose) + log_("extracting completed message (%d bytes)\n", tr->msg.size); + + /* Shallow copy the malloc()'d data pointer */ + memcpy(msg, &tr->msg, sizeof *msg); + free(tr); + return msg; +} + +void fxlink_transfer_receive(struct fxlink_transfer *tr, void *data, int size) +{ + int remaining_size = tr->msg.size - tr->processed_size; + if(size > remaining_size) { + elog("message too long, dropping %d bytes\n", size - remaining_size); + size = remaining_size; + } + + if(options.verbose) + log_("got %d bytes (out of %d left)\n", size, remaining_size); + + memcpy(tr->msg.data + tr->processed_size, data, size); + tr->processed_size += size; +} + +bool fxlink_transfer_complete(struct fxlink_transfer const *tr) +{ + return tr->processed_size >= tr->msg.size; } diff --git a/fxlink/protocol.h b/fxlink/protocol.h deleted file mode 100644 index 9595e3e..0000000 --- a/fxlink/protocol.h +++ /dev/null @@ -1,71 +0,0 @@ -//--- -// fxlink:protocol - Custom fxlink protocol -//--- - -#ifndef FXLINK_PROTOCOL_H -#define FXLINK_PROTOCOL_H - -#include -#include - -/* See the gint source for details on the protocol */ -typedef struct -{ - uint32_t version; - uint32_t size; - uint32_t transfer_size; - - char application[16]; - char type[16]; - -} usb_fxlink_header_t; - -/* Subheader for the fxlink built-in "image" type */ -typedef struct -{ - uint32_t width; - uint32_t height; - int pixel_format; - -} usb_fxlink_image_t; - -/* Pixel formats */ -typedef enum -{ - /* Image is an array of *big-endian* uint16_t with RGB565 format */ - USB_FXLINK_IMAGE_RGB565 = 0, - /* Image is an array of bits in mono format */ - USB_FXLINK_IMAGE_MONO, - /* Image is two consecutive mono arrays, one for light, one for dark */ - USB_FXLINK_IMAGE_GRAY, - -} usb_fxlink_image_format_t; - -//--- -// Utilities in this implementation -//--- - -/* Message currently being transferred */ -typedef struct -{ - usb_fxlink_header_t header; - /* Valid when we are reading a message */ - bool valid; - /* Data already read in this message */ - uint32_t size_read; - /* Data buffer */ - char *output; - -} message_t; - -/* fxlink_protocol_decode_image(): Decode an image into RGB888 format - - This function decodes the message into an RGB888 image and returns an array - of row pointers with the image data (free the array and each element of the - array after use). - - If there are not enough bytes in the input, it pads with zeros, and if there - are extra bytes, they are dropped; in both cases a warning is printed. */ -uint8_t **fxlink_protocol_decode_image(message_t *msg); - -#endif /* FXLINK_PROTOCOL_H */ diff --git a/fxlink/sdl2.c b/fxlink/sdl2.c deleted file mode 100644 index 7b3590f..0000000 --- a/fxlink/sdl2.c +++ /dev/null @@ -1,82 +0,0 @@ -#include "config.h" -#ifndef FXLINK_DISABLE_SDL2 - -#include "sdl2.h" -#include "util.h" - -static SDL_Window *window = NULL; - -static int init(size_t width, size_t height) -{ - if(!SDL_WasInit(SDL_INIT_VIDEO)) { - int rc = SDL_Init(SDL_INIT_VIDEO); - if(rc < 0) - return err("Cannot initialize SDL: %s\n", SDL_GetError()); - } - - window = SDL_CreateWindow("fxlink", SDL_WINDOWPOS_CENTERED, - SDL_WINDOWPOS_CENTERED, width, height, 0); - return 0; -} - -__attribute__((destructor)) -static void quit(void) -{ - if(!window) - return; - SDL_DestroyWindow(window); - window = NULL; -} - -/* Generate an RGB888 surface from image data. */ -static SDL_Surface *surface_for_image(uint8_t **RGB888_rows, int width, - int height) -{ - /* Little endian setup for RGB */ - SDL_Surface *s = SDL_CreateRGBSurface(0, width, height, 24, - 0x000000ff, 0x0000ff00, 0x0000ff00, 0); - if(!s) { - err("Cannot create surface for image"); - return NULL; - } - - for(int i = 0; i < height; i++) - memcpy(s->pixels + i * s->pitch, RGB888_rows[i], width * 3); - - return s; -} - -void sdl2_stream(uint8_t **RGB888_rows, int width, int height) -{ - if(!window && init(width, height)) - return; - - int current_w, current_h; - SDL_GetWindowSize(window, ¤t_w, ¤t_h); - if(current_w != width || current_h != height) - SDL_SetWindowSize(window, width, height); - - SDL_Surface *src = surface_for_image(RGB888_rows, width, height); - if(!src) - return; - - SDL_Surface *dst = SDL_GetWindowSurface(window); - SDL_BlitSurface(src, NULL, dst, NULL); - SDL_FreeSurface(src); - - SDL_UpdateWindowSurface(window); -} - -void sdl2_tick(void) -{ - if(!window) - return; - - SDL_Event e; - while(SDL_PollEvent(&e)) { - if(e.type == SDL_QUIT) - quit(); - } -} - -#endif /* FXLINK_DISABLE_SDL2 */ diff --git a/fxlink/sdl2.h b/fxlink/sdl2.h deleted file mode 100644 index 5133870..0000000 --- a/fxlink/sdl2.h +++ /dev/null @@ -1,23 +0,0 @@ -//--- -// fxlink:sdl2 - SDL2 functions -//--- - -#ifndef FXLINK_SDL2_H -#define FXLINK_SDL2_H - -#ifndef FXLINK_DISABLE_SDL2 - -#include - -/* sdl2_stream(): Display a streaming image on the window - This function opens or reuses an SDL2 window to display an image. */ -void sdl2_stream(uint8_t **RGB888_rows, int width, int height); - -/* sdl2_tick(): Handle SDL events - This just needs to be called regularly from the main thread, to respond to - events on the window. */ -void sdl2_tick(void); - -#endif /* FXLINK_DISABLE_SDL2 */ - -#endif /* FXLINK_SDL2_H */ diff --git a/fxlink/tooling/libpng.c b/fxlink/tooling/libpng.c new file mode 100644 index 0000000..b2dee1e --- /dev/null +++ b/fxlink/tooling/libpng.c @@ -0,0 +1,46 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include + +int fxlink_libpng_save_raw(struct fxlink_message_image_raw *raw, + char const *path) +{ + png_struct *png = png_create_write_struct(PNG_LIBPNG_VER_STRING, + NULL, NULL, NULL); + if(!png) + return elog("failed to write PNG: png_create_write_struct\n"); + + png_infop info = png_create_info_struct(png); + if(!info) + return elog("failed to write PNG: png_create_info_struct\n"); + + FILE *fp = fopen(path, "wb"); + if(!fp) { + png_destroy_write_struct(&png, &info); + return elog("failed to open '%s' to write PNG: %m\n", path); + } + + if(setjmp(png_jmpbuf(png))) { + fclose(fp); + png_destroy_write_struct(&png, &info); + return 1; + } + + png_init_io(png, fp); + png_set_IHDR(png, info, + raw->width, raw->height, 8, + PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png, info); + png_write_image(png, raw->data); + png_write_end(png, NULL); + png_destroy_write_struct(&png, &info); + fclose(fp); + return 0; +} diff --git a/fxlink/tooling/sdl2.c b/fxlink/tooling/sdl2.c new file mode 100644 index 0000000..487a236 --- /dev/null +++ b/fxlink/tooling/sdl2.c @@ -0,0 +1,98 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include + +#ifdef FXLINK_DISABLE_SDL2 + +void fxlink_sdl2_display_raw(struct fxlink_message_image_raw const *raw) +{ + log_("SDL2 disabled at compiled-time, skipping frame"); +} + +void fxlink_sdl2_handle_events(void) +{ +} + +#else /* FXLINK_DISABLE_SDL2 */ + +static SDL_Window *window = NULL; + +static int init(size_t width, size_t height) +{ + if(!SDL_WasInit(SDL_INIT_VIDEO)) { + int rc = SDL_Init(SDL_INIT_VIDEO); + if(rc < 0) + return elog("cannot initialize SDL: %s\n", SDL_GetError()); + } + + window = SDL_CreateWindow("fxlink", SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, width, height, 0); + return 0; +} + +__attribute__((destructor)) +static void quit(void) +{ + if(!window) + return; + SDL_DestroyWindow(window); + window = NULL; +} + +/* Generate an RGB888 surface from image data. */ +static SDL_Surface *surface_for_image(uint8_t **RGB888_rows, int width, + int height) +{ + /* Little endian setup for RGB */ + SDL_Surface *s = SDL_CreateRGBSurface(0, width, height, 24, + 0x000000ff, 0x0000ff00, 0x0000ff00, 0); + if(!s) { + elog("cannot create surface for image\n"); + return NULL; + } + + for(int i = 0; i < height; i++) + memcpy(s->pixels + i * s->pitch, RGB888_rows[i], width * 3); + + return s; +} + +void fxlink_sdl2_display_raw(struct fxlink_message_image_raw const *raw) +{ + if(!window && init(raw->width, raw->height)) + return; + + int current_w, current_h; + SDL_GetWindowSize(window, ¤t_w, ¤t_h); + if(current_w != raw->width || current_h != raw->height) + SDL_SetWindowSize(window, raw->width, raw->height); + + SDL_Surface *src = surface_for_image(raw->data, raw->width, raw->height); + if(!src) + return; + + SDL_Surface *dst = SDL_GetWindowSurface(window); + SDL_BlitSurface(src, NULL, dst, NULL); + SDL_FreeSurface(src); + + SDL_UpdateWindowSurface(window); +} + +void fxlink_sdl2_handle_events(void) +{ + if(!window) + return; + + SDL_Event e; + while(SDL_PollEvent(&e)) { + if(e.type == SDL_QUIT) + quit(); + } +} + +#endif /* FXLINK_DISABLE_SDL2 */ diff --git a/fxlink/ud2.c b/fxlink/tooling/udisks2.c similarity index 53% rename from fxlink/ud2.c rename to fxlink/tooling/udisks2.c index 9cbb44a..814cc98 100644 --- a/fxlink/ud2.c +++ b/fxlink/tooling/udisks2.c @@ -1,20 +1,15 @@ -#include "config.h" +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include #ifndef FXLINK_DISABLE_UDISKS2 -#include "ud2.h" -#include "fxlink.h" -#include "util.h" -#include "properties.h" -#include "filter.h" - -#include -#include -#include -#include - -//--- -// UDisks2 utility functions -//--- +#include +#include +#include int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr) { @@ -22,12 +17,12 @@ int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr) UDisksClient *udc = udisks_client_new_sync(NULL, &error); if(error) - return err("cannot open udisks2 client: %s", error->message); + return elog("cannot open udisks2 client: %s\n", error->message); UDisksManager *udm = udisks_client_get_manager(udc); if(!udm) { g_object_unref(udc); - return err("udisks2 daemon does not seem to be running"); + return elog("udisks2 daemon does not seem to be running\n"); } *udc_ptr = udc; @@ -35,8 +30,9 @@ int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr) return 0; } -void ud2_end(UDisksClient *udc, __attribute__((unused)) UDisksManager *udm) +void ud2_end(UDisksClient *udc, UDisksManager *udm) { + (void)udm; g_object_unref(udc); } @@ -47,9 +43,8 @@ gchar **ud2_block_devices(UDisksManager *udm) GError *error = NULL; udisks_manager_call_get_block_devices_sync(udm,args,&blocks,NULL,&error); - if(error) { - err("cannot list udisks2 block devices: %s", error->message); - } + if(error) + elog("cannot list udisks2 block devices: %s\n", error->message); return blocks; } @@ -81,9 +76,9 @@ bool is_casio_drive(UDisksDrive *drive) return strstr(udisks_drive_get_vendor(drive), "CASIO") != NULL; } -properties_t ud2_properties(UDisksDrive *drive) +struct fxlink_filter ud2_properties(UDisksDrive *drive) { - properties_t props = { 0 }; + struct fxlink_filter props = { 0 }; props.p7 = false; props.mass_storage = true; @@ -94,17 +89,17 @@ properties_t ud2_properties(UDisksDrive *drive) gchar const *s = udisks_drive_get_serial(drive); /* LINK sends a 12-byte serial number with four leading 0. Remove them */ - if(s && strlen(s) == 12 && !strncmp(s, "0000", 4)) s+= 4; - props.serial_number = (char *)s; + if(s && strlen(s) == 12 && !strncmp(s, "0000", 4)) s += 4; + props.serial = strdup(s); return props; } -int ud2_unique_matching(filter_t const *filter, UDisksClient *udc, +int ud2_unique_matching(struct fxlink_filter const *filter, UDisksClient *udc, UDisksManager *udm, UDisksBlock **block_ptr, UDisksDrive **drive_ptr, UDisksFilesystem **fs_ptr) { - int status = FILTER_NONE; + int status = FXLINK_FILTER_NONE; bool error; UDisksBlock *block = NULL; @@ -112,11 +107,12 @@ int ud2_unique_matching(filter_t const *filter, UDisksClient *udc, UDisksFilesystem *fs = NULL; for_udisks2_devices(it, udc, udm, &error) { - if(!filter_match(&it.props, filter)) continue; + if(!fxlink_filter_match(&it.props, filter)) + continue; /* Already found a device before */ - if(status == FILTER_UNIQUE) { - status = FILTER_MULTIPLE; + if(status == FXLINK_FILTER_UNIQUE) { + status = FXLINK_FILTER_MULTIPLE; g_object_unref(fs); g_object_unref(drive); g_object_unref(block); @@ -130,31 +126,37 @@ int ud2_unique_matching(filter_t const *filter, UDisksClient *udc, block = g_object_ref(it.block); drive = g_object_ref(it.drive); fs = g_object_ref(it.fs); - status = FILTER_UNIQUE; + status = FXLINK_FILTER_UNIQUE; } if(error) - return FILTER_ERROR; + return FXLINK_FILTER_ERROR; - if(block_ptr) *block_ptr = block; - else g_object_unref(block); + if(block_ptr) + *block_ptr = block; + else if(block) + g_object_unref(block); - if(drive_ptr) *drive_ptr = drive; - else g_object_unref(drive); + if(drive_ptr) + *drive_ptr = drive; + else if(drive) + g_object_unref(drive); - if(fs_ptr) *fs_ptr = fs; - else g_object_unref(fs); + if(fs_ptr) + *fs_ptr = fs; + else if(fs) + g_object_unref(fs); return status; } -int ud2_unique_wait(filter_t const *filter, delay_t *delay, UDisksClient *udc, - UDisksManager *udm, UDisksBlock **block, UDisksDrive **drive, - UDisksFilesystem **fs) +int ud2_unique_wait(struct fxlink_filter const *filter, delay_t *delay, + UDisksClient *udc, UDisksManager *udm, UDisksBlock **block, + UDisksDrive **drive, UDisksFilesystem **fs) { while(true) { int rc = ud2_unique_matching(filter, udc, udm, block, drive, fs); - if(rc != FILTER_NONE) return rc; - if(delay_cycle(delay)) return FILTER_NONE; + if(rc != FXLINK_FILTER_NONE) return rc; + if(delay_cycle(delay)) return FXLINK_FILTER_NONE; udisks_client_settle(udc); } } @@ -223,9 +225,9 @@ void ud2_iter_next(ud2_iterator_t *it) // Main functions //--- -int main_blocks(filter_t *filter, delay_t *delay) +int main_blocks(struct fxlink_filter *filter, delay_t *delay) { - filter_clean_udisks2(filter); + fxlink_filter_clean_udisks2(filter); UDisksClient *udc = NULL; UDisksManager *udm = NULL; @@ -237,7 +239,8 @@ int main_blocks(filter_t *filter, delay_t *delay) bool error; for_udisks2_devices(it, udc, udm, &error) { - if(!filter_match(&it.props, filter)) continue; + if(!fxlink_filter_match(&it.props, filter)) + continue; if(total_devices > 0) printf("\n"); @@ -254,8 +257,8 @@ int main_blocks(filter_t *filter, delay_t *delay) udisks_drive_get_vendor(it.drive), udisks_drive_get_model(it.drive)); - if(it.props.serial_number) - printf(" Serial number: %s\n", it.props.serial_number); + if(it.props.serial) + printf(" Serial number: %s\n", it.props.serial); gchar const * const * mount_points = udisks_filesystem_get_mount_points(it.fs); @@ -267,7 +270,7 @@ int main_blocks(filter_t *filter, delay_t *delay) } printf(" Properties: "); - properties_print(stdout, &it.props); + fxlink_filter_print(stdout, &it.props); printf("\n"); total_devices++; @@ -280,112 +283,4 @@ int main_blocks(filter_t *filter, delay_t *delay) return 0; } -int main_send(filter_t *filter, delay_t *delay, char **files) -{ - filter_clean_udisks2(filter); - GError *error = NULL; - char **argv = NULL; - int rc = 0; - - UDisksClient *udc = NULL; - UDisksManager *udm = NULL; - if(ud2_start(&udc, &udm)) return 1; - - UDisksBlock *block = NULL; - UDisksDrive *drive = NULL; - UDisksFilesystem *fs = NULL; - rc = ud2_unique_wait(filter, delay, udc, udm, &block, &drive, &fs); - if(rc != FILTER_UNIQUE) { - rc = 1; - goto end; - } - - /* Determine a mount folder, mounting the volume if needed */ - gchar *folder = NULL; - bool mounted_here = false; - - gchar const *dev = udisks_block_get_device(block); - gchar const * const * mount_points = - udisks_filesystem_get_mount_points(fs); - - if(!mount_points || !mount_points[0]) { - GVariant *args = g_variant_new("a{sv}", NULL); - udisks_filesystem_call_mount_sync(fs, args, &folder, NULL, &error); - if(error) { - rc = err("cannot mount %s: %s", dev, error->message); - goto end; - } - printf("Mounted %s to %s.\n", dev, folder); - mounted_here = true; - } - else { - folder = strdup(mount_points[0]); - printf("Already mounted at %s.\n", folder); - mounted_here = false; - } - - /* Copy files with external cp(1) */ - int file_count = 0; - while(files[file_count]) file_count++; - - argv = malloc((file_count + 3) * sizeof *argv); - if(!argv) { - rc = err("cannot allocate argv array for cp(1)"); - goto end; - } - argv[0] = "cp"; - for(int i = 0; files[i]; i++) - argv[i+1] = files[i]; - argv[file_count+1] = folder; - argv[file_count+2] = NULL; - - /* Print command */ - printf("Running cp"); - for(int i = 1; argv[i]; i++) printf(" '%s'", argv[i]); - printf("\n"); - - pid_t pid = fork(); - - if(pid == 0) { - execvp("cp", argv); - } - else if(pid == -1) { - rc = err("failed to fork to invoke cp"); - goto end; - } - else { - int wstatus; - waitpid(pid, &wstatus, 0); - - if(!WIFEXITED(wstatus)) - err("process did not terminate normally"); - else if(WEXITSTATUS(wstatus) != 0) { - err("process terminated with error %d", - WEXITSTATUS(wstatus)); - } - } - - /* Unmount the filesystem and eject the device if we mounted it */ - if(mounted_here || options.force_unmount) { - GVariant *args = g_variant_new("a{sv}", NULL); - udisks_filesystem_call_unmount_sync(fs, args, NULL, &error); - if(error) err("while unmounting %s: %s", dev, error->message); - else printf("Unmounted %s.\n", dev); - - args = g_variant_new("a{sv}", NULL); - udisks_drive_call_power_off_sync(drive, args, NULL, &error); - if(error) err("while ejecting %s: %s", dev, error->message); - else printf("Ejected %s.\n", dev); - } - -end: - free(folder); - if(argv) free(argv); - if(fs) g_object_unref(fs); - if(drive) g_object_unref(drive); - if(block) g_object_unref(block); - if(udc && udm) ud2_end(udc, udm); - return rc; -} - #endif /* FXLINK_DISABLE_UDISKS2 */ diff --git a/fxlink/tui/input.c b/fxlink/tui/input.c new file mode 100644 index 0000000..d865543 --- /dev/null +++ b/fxlink/tui/input.c @@ -0,0 +1,338 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include +#include +#include + +//--- +// Text manipulation functions +//--- + +bool fxlink_TUI_input_init(struct fxlink_TUI_input *in, WINDOW *win, + int prealloc_size) +{ + char *data = malloc(prealloc_size + 1); + if(!data) + return false; + + data[0] = 0; + in->data = data; + in->size = 0; + in->alloc_size = prealloc_size + 1; + in->cursor = 0; + in->win = win; + + getyx(win, in->wy, in->wx); + scrollok(win, false); + + return true; +} + +void fxlink_TUI_input_free(struct fxlink_TUI_input *in) +{ + free(in->data); + memset(in, 0, sizeof *in); +} + +bool fxlink_TUI_input_alloc(struct fxlink_TUI_input *in, int n) +{ + if(in->alloc_size >= n + 1) + return true; + + /* Always increase the size by at least 16 so we can insert many times in a + row without worrying about excessive allocations. */ + int newsize = (in->alloc_size + 16 > n + 1) ? in->alloc_size + 16 : n + 1; + char *newdata = realloc(in->data, newsize); + if(!newdata) + return false; + + in->data = newdata; + in->alloc_size = newsize; + return true; +} + +bool fxlink_TUI_input_insert(struct fxlink_TUI_input *in, int p, + char const *str, int n) +{ + if(p < 0 || p > in->size) + return false; + if(!fxlink_TUI_input_alloc(in, in->size + n)) + return false; + + /* Move the end of the string (plus the NUL) n bytes forward */ + memmove(in->data + p + n, in->data + p, in->size - p + 1); + memcpy(in->data + p, str, n); + in->size += n; + return true; +} + +int fxlink_TUI_input_delete(struct fxlink_TUI_input *in, int p, int n) +{ + if(n >= in->size - p) + n = in->size - p; + + if(n < 0) + return 0; + + /* Move the end of the string (plus the NUL) n bytes backwards */ + memmove(in->data + p, in->data + p + n, in->size - n - p + 1); + in->size -= n; + return n; +} + +//-- +// Rendering functions +//--- + +bool fxlink_TUI_input_echo(struct fxlink_TUI_input *in, chtype ch) +{ + int x, y, w, h; + getyx(in->win, y, x); + getmaxyx(in->win, h, w); + + bool needs_scroll = (x == w-1 && y == h-1); + bool can_scroll = in->wy > 0; + + if(!needs_scroll) { + waddch(in->win, ch); + return true; + } + else if(can_scroll) { + waddch(in->win, ch); + scrollok(in->win, true); + scroll(in->win); + scrollok(in->win, false); + wmove(in->win, y, 0); + in->wy--; + return true; + } + else { + return false; + } +} + +void fxlink_TUI_input_clear(struct fxlink_TUI_input *in) +{ + int w, h; + getmaxyx(in->win, h, w); + + /* Clear from the end of screen to the start of the input */ + int current_y = h - 1; + + while(current_y > in->wy) { + mvwhline(in->win, current_y, 0, ' ', w); + current_y--; + } + mvwhline(in->win, in->wy, in->wx, ' ', w - in->wx); +} + +void fxlink_TUI_input_redraw(struct fxlink_TUI_input *in) +{ + fxlink_TUI_input_clear(in); + + int cursor_x, cursor_y; + + waddnstr(in->win, in->data, in->cursor); + getyx(in->win, cursor_y, cursor_x); + waddnstr(in->win, in->data + in->cursor, in->size - in->cursor); + + wmove(in->win, cursor_y, cursor_x); +} + +void fxlink_TUI_input_clearscreen(struct fxlink_TUI_input *in) +{ + int current_y, current_x; + getyx(in->win, current_y, current_x); + + scrollok(in->win, true); + wscrl(in->win, in->wy); + scrollok(in->win, false); + + wmove(in->win, current_y - in->wy, current_x); + in->wy = 0; +} + +struct CSI { + int param1; + int param2; + char cmd; + char record[32]; +}; + +static struct CSI parse_CSI(WINDOW *win, WINDOW *logWindow) +{ + struct CSI csi = { 0 }; + int i = 0; + size_t j = 0; + + csi.param1 = -1; + csi.param2 = -1; + + while(!csi.cmd && j < sizeof csi.record) { + chtype ch = wgetch(win); + if((int)ch == ERR) { + wprintw(logWindow, "error: invalid CSI escape: "); + waddnstr(logWindow, csi.record, j); + waddch(logWindow, '\n'); + return (struct CSI){ 0 }; + } + + int c = ch & A_CHARTEXT; + if(isdigit(c)) { + if(i == 0) { + csi.param1 = csi.param1 * 10 + (c - '0'); + } + else { + csi.param2 = csi.param2 * 10 + (c - '0'); + } + } + else if(c == ';' && i == 0) + i = 1; + else if(strchr("ABCD", c)) + csi.cmd = c; + else { + wprintw(logWindow, "unknow CSI escape\n"); + return (struct CSI){ 0 }; + } + } + + return csi; +} + +bool fxlink_TUI_input_getch(struct fxlink_TUI_input *in, WINDOW *logWindow) +{ + chtype ch = wgetch(in->win); + int c = ch & A_CHARTEXT; + struct CSI csi = { 0 }; + + /* Parse CSI escapes */ + if(c == '\e') { + chtype next = wgetch(in->win); + if((int)next == ERR) + return false; + + int n = next & A_CHARTEXT; + /* CSI */ + if(n == '[') + csi = parse_CSI(in->win, logWindow); + /* Scrolling (ignored) */ + else if(n == 'O' || n == 'P') + wgetch(in->win); + else + wprintw(logWindow, "error: invalid escape start ^[%c\n", n); + + c = 0; + } + + /* Event left after a SIGWINCH */ + if(ch == KEY_RESIZE) + return false; + + /* : Remove last letter */ + if(c == 0x7f) { + if(in->cursor > 0) { + fxlink_TUI_input_delete(in, in->cursor - 1, 1); + in->cursor--; + fxlink_TUI_input_redraw(in); + } + } + /* C-d: Delete to the right */ + else if(c == 'D'-64) { + if(in->cursor < in->size) { + fxlink_TUI_input_delete(in, in->cursor, 1); + fxlink_TUI_input_redraw(in); + } + } + /* C-b: Back one character */ + else if(c == 'B'-64) { + if(in->cursor > 0) { + in->cursor--; + fxlink_TUI_input_redraw(in); + } + } + /* C-f: Forward one character */ + else if(c == 'F'-64) { + if(in->cursor < in->size) { + in->cursor++; + fxlink_TUI_input_redraw(in); + } + } + /* C-a: Move cursor to start of line */ + else if(c == 'A'-64) { + if(in->cursor != 0) { + in->cursor = 0; + fxlink_TUI_input_redraw(in); + } + } + /* C-e: Move cursor to end of line */ + else if(c == 'E'-64) { + if(in->cursor != in->size) { + in->cursor = in->size; + fxlink_TUI_input_redraw(in); + } + } + /* C-l: Clear the screen (moves prompt to first line) */ + else if(c == 'L'-64) { + fxlink_TUI_input_clearscreen(in); + } + /* C-k: Kill to end of line */ + else if(c == 'K'-64) { + fxlink_TUI_input_delete(in, in->cursor, in->size - in->cursor); + fxlink_TUI_input_redraw(in); + } + /* : Move cursor to the left */ + else if(csi.cmd == 'D') { + int distance = (csi.param1 >= 0) ? csi.param1 : 1; + if(in->cursor > 0) { + in->cursor = max(0, in->cursor - distance); + fxlink_TUI_input_redraw(in); + } + } + /* : Move cursor to the right */ + else if(csi.cmd == 'C') { + int distance = (csi.param1 >= 0) ? csi.param1 : 1; + if(in->cursor < in->size) { + in->cursor = min(in->size, in->cursor + distance); + fxlink_TUI_input_redraw(in); + } + } + // : Delete to the right + // Meta-F: Move forward a word + // Meta-B: Move backward a word + // Meta- or Meta-: Kill to end of current word or eat next word + // Ctrl-W: Kill to previous whitespace + + else if(c == '\n') { + in->cursor = in->size; + fxlink_TUI_input_redraw(in); + scrollok(in->win, true); + waddch(in->win, '\n'); + return true; + } + else if(c != 0) { + if(fxlink_TUI_input_echo(in, ch)) { + char c_char = c; + fxlink_TUI_input_insert(in, in->cursor, &c_char, 1); + in->cursor++; + fxlink_TUI_input_redraw(in); + } + } + else if(csi.cmd) { + wprintw(logWindow, "unhandled escape: %c (%d,%d)\n", + csi.cmd, csi.param1, csi.param2); + } + + /* Debug input as it is being typed + if(c != 0) { + wprintw(logWindow, "ch:%04x [%.*s|%.*s]\n", + ch, in->cursor, in->data, + in->size - in->cursor, in->data + in->cursor); + } */ + + return false; +} diff --git a/fxlink/tui/layout.c b/fxlink/tui/layout.c new file mode 100644 index 0000000..9005271 --- /dev/null +++ b/fxlink/tui/layout.c @@ -0,0 +1,452 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include +#include + +typedef unsigned int uint; + +//--- +// Flexbox-like allocation algorithm (copy/pasted from JustUI) +// +// The basic idea of space redistribution is to give each widget extra space +// proportional to their stretch rates in the relevant direction. However, the +// addition of maximum size constraints means that widgets can decline some of +// the extra space being allocated. +// +// This system defines the result of expansion as a function of the "expansion +// factor". As the expansion factor increases, every widget stretches at a +// speed proportional to its stretch rate, until it reaches its maximum size. +// +// Extra widget size +// | +// + .-------- Maximum size +// | .` +// | .` <- Slope: widget stretch rate +// |.` +// 0 +-------+------> Expansion factor +// 0 ^ +// Breaking point +// +// The extra space allocated to widgets is the sum of this function for every +// widget considered for expansion. Since every widget has a possibly different +// breaking point, a maximal interval of expansion factor that has no breaking +// point is called a "run". During each run, the slope for the total space +// remains constant, and a unit of expansion factor corresponds to one pixel +// being allocated in the container. Thus, whenever the expansion factor +// increases of (slope), every widget (w) gets (w->stretch) new pixels. +// +// The functions below simulate the expansion by determining the breaking +// points of the widgets and allocating extra space during each run. Once the +// total extra space allocated reaches the available space, simulation stops +// and the allocation is recorded by assigning actual size to widgets. +//--- + +/* This "expansion" structure tracks information relating to a single child + widget during the space distribution process. */ +typedef struct { + /* Child index */ + uint8_t id; + /* Stretch rate, sum of stretch rates is the "slope" */ + uint8_t stretch; + /* Maximum size augmentation */ + int16_t max; + /* Extra space allocate in the previous runs, in pixels */ + float allocated; + /* Breaking point for the current run, as a number of pixels to distribute + to the whole system */ + float breaking_point; +} exp_t; + +/* Determine whether a widget can expand any further. */ +static bool can_expand(exp_t *e) +{ + return (e->stretch > 0 && e->allocated < e->max); +} + +/* Compute the slope for the current run. */ +static uint compute_slope(exp_t elements[], size_t n) +{ + uint slope = 0; + for(size_t i = 0; i < n; i++) { + if(can_expand(&elements[i])) slope += elements[i].stretch; + } + return slope; +} + +/* Compute the breaking point for every expanding widget. Returns the amount of + pixels to allocate in order to reach the next breaking point. */ +static float compute_breaking_points(exp_t elements[], size_t n, uint slope) +{ + float closest = HUGE_VALF; + + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + if(!can_expand(e)) continue; + + /* Up to (e->max - e->allocated) pixels can be added to this widget. + With the factor of (slope / e->stretch), we get the number of pixels + to add to the container in order to reach the threshold. */ + e->breaking_point = (e->max - e->allocated) * (slope / e->stretch); + closest = fminf(e->breaking_point, closest); + } + + return closest; +} + +/* Allocate floating-point space to widgets. This is the core of the + distribution system, it produces (e->allocated) for every element. */ +static void allocate_space(exp_t elements[], size_t n, float available) +{ + /* One iteration per run */ + while(available > 0) { + /* Slope for this run; if zero, no more widget can grow */ + uint slope = compute_slope(elements, n); + if(!slope) break; + + /* Closest breaking point, amount of space to distribute this run */ + float breaking = compute_breaking_points(elements, n, slope); + float run_budget = fminf(breaking, available); + + /* Give everyone their share of run_budget */ + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + if(!can_expand(e)) continue; + + e->allocated += (run_budget * e->stretch) / slope; + } + + available -= run_budget; + } +} + +/* Stable insertion sort: order children by decreasing fractional allocation */ +static void sort_by_fractional_allocation(exp_t elements[], size_t n) +{ + for(size_t spot = 0; spot < n - 1; spot++) { + /* Find the element with the max fractional value in [spot..size] */ + float max_frac = 0; + int max_frac_who = -1; + + for(size_t i = spot; i < n; i++) { + exp_t *e = &elements[i]; + + float frac = e->allocated - floorf(e->allocated); + + if(max_frac_who < 0 || frac > max_frac) { + max_frac = frac; + max_frac_who = i; + } + } + + /* Give that element the spot */ + exp_t temp = elements[spot]; + elements[spot] = elements[max_frac_who]; + elements[max_frac_who] = temp; + } +} + +static int compare_ids(void const *ptr1, void const *ptr2) +{ + exp_t const *e1 = ptr1; + exp_t const *e2 = ptr2; + return e1->id - e2->id; +} + +/* Round allocations so that they add up to the available space */ +static void round_allocations(exp_t elements[], size_t n, int available_space) +{ + /* Prepare to give everyone the floor of their allocation */ + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + available_space -= floorf(e->allocated); + } + + /* Sort by decreasing fractional allocation then add one extra pixel to + the (available_space) children with highest fractional allocation */ + sort_by_fractional_allocation(elements, n); + + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + e->allocated = floorf(e->allocated); + + if(can_expand(e) && (int)i < available_space) e->allocated += 1; + } + + /* Sort back by IDs for final ordering */ + qsort(elements, n, sizeof *elements, compare_ids); +} + +//--- +// TUI layout +//--- + +static struct fxlink_TUI_box *mkbox(void) +{ + struct fxlink_TUI_box *b = calloc(1, sizeof *b); + if(b) { + b->max_w = 0xffff; + b->max_h = 0xffff; + b->stretch_x = 1; + b->stretch_y = 1; + } + return b; +} + +struct fxlink_TUI_box *fxlink_TUI_box_mk_window(char const *title, WINDOW **w) +{ + struct fxlink_TUI_box *b = mkbox(); + if(b) { + b->type = FXLINK_TUI_BOX_WINDOW; + b->window.title = title; + b->window.win = w; + } + return b; +} + +static struct fxlink_TUI_box *mkcontainer(int type, + struct fxlink_TUI_box *child, va_list args) +{ + struct fxlink_TUI_box *b = mkbox(); + if(b) { + b->type = type; + int i = 0; + + while(child && i < FXLINK_TUI_BOX_MAXSIZE) { + b->children[i++] = child; + child = va_arg(args, struct fxlink_TUI_box *); + } + } + return b; +} + +struct fxlink_TUI_box *fxlink_TUI_box_mk_vertical( + struct fxlink_TUI_box *child1, ...) +{ + va_list args; + va_start(args, child1); + return mkcontainer(FXLINK_TUI_BOX_VERTICAL, child1, args); + va_end(args); +} + +struct fxlink_TUI_box *fxlink_TUI_box_mk_horizontal( + struct fxlink_TUI_box *child1, ...) +{ + va_list args; + va_start(args, child1); + return mkcontainer(FXLINK_TUI_BOX_HORIZONTAL, child1, args); + va_end(args); +} + +void fxlink_TUI_box_minsize(struct fxlink_TUI_box *box, int min_w, int min_h) +{ + box->min_w = min_w; + box->min_h = min_h; +} + +void fxlink_TUI_box_maxsize(struct fxlink_TUI_box *box, int max_w, int max_h) +{ + box->max_w = max_w; + box->max_h = max_h; +} + +void fxlink_TUI_box_stretch(struct fxlink_TUI_box *box, int stretch_x, + int stretch_y, bool force) +{ + box->stretch_x = stretch_x; + box->stretch_y = stretch_y; + box->stretch_force = force; +} + +void fxlink_TUI_box_print(FILE *fp, struct fxlink_TUI_box const *b, int level) +{ + for(int i = 0; i < level * 4; i++) + fputc(' ', fp); + + fprintf(fp, "type="); + if(b->type == FXLINK_TUI_BOX_WINDOW) + fprintf(fp, "WINDOW '%s'", b->window.title); + if(b->type == FXLINK_TUI_BOX_VERTICAL) + fprintf(fp, "VERTICAL"); + if(b->type == FXLINK_TUI_BOX_HORIZONTAL) + fprintf(fp, "HORIZONTAL"); + + fprintf(fp, " x=%d y=%d w=", b->x, b->y); + + if(b->min_w > 0) + fprintf(fp, "(%d)<", b->min_w); + fprintf(fp, "%d", b->w); + if(b->max_w < 0xffff) + fprintf(fp, "<(%d)", b->max_w); + + fprintf(fp, " h="); + + if(b->min_h > 0) + fprintf(fp, "(%d)<", b->min_h); + fprintf(fp, "%d", b->h); + if(b->max_h < 0xffff) + fprintf(fp, "<(%d)", b->max_h); + + fprintf(fp, "\n"); + + if(b->type != FXLINK_TUI_BOX_WINDOW) { + for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && b->children[i]; i++) + fxlink_TUI_box_print(fp, b->children[i], level+1); + } +} + +static void box_do_layout(struct fxlink_TUI_box *box) +{ + if(box->type == FXLINK_TUI_BOX_WINDOW) + return; + + int horiz = (box->type == FXLINK_TUI_BOX_HORIZONTAL); + size_t child_count = 0; + while(child_count < FXLINK_TUI_BOX_MAXSIZE && box->children[child_count]) + child_count++; + int spacing = 1; + + /* Content width and height */ + int cw = box->w; + int ch = box->h; + /* Allocatable width and height (which excludes spacing) */ + int total_spacing = (child_count - 1) * spacing; + int aw = cw - (horiz ? total_spacing : 0); + int ah = ch - (horiz ? 0 : total_spacing); + /* Length along the main axis, including spacing */ + int length = 0; + + /* Expanding widgets' information for extra space distribution */ + size_t n = child_count; + exp_t elements[n]; + + for(size_t i = 0; i < child_count; i++) { + struct fxlink_TUI_box *child = box->children[i]; + + /* Maximum size to enforce: this is the acceptable size closest to our + allocatable size */ + int max_w = clamp(aw, child->min_w, child->max_w); + int max_h = clamp(ah, child->min_h, child->max_h); + + /* Start by setting every child to an acceptable size */ + child->w = clamp(child->w, child->min_w, max_w); + child->h = clamp(child->h, child->min_h, max_h); + + /* Initialize expanding widgets' information */ + elements[i].id = i; + elements[i].allocated = 0.0f; + elements[i].breaking_point = -1.0f; + + /* Determine natural length along the container, and stretch child + along the perpendicular direction if possible */ + + if(i > 0) + length += spacing; + if(horiz) { + length += child->w; + if(child->stretch_y > 0) child->h = max_h; + + elements[i].stretch = child->stretch_x; + elements[i].max = max(max_w - child->w, 0); + + if(child->stretch_force && child->stretch_x > 0) + elements[i].max = max(aw - child->w, 0); + } + else { + length += child->h; + if(child->stretch_x > 0) child->w = max_w; + + elements[i].stretch = child->stretch_y; + elements[i].max = max(max_h - child->h, 0); + + if(child->stretch_force && child->stretch_y > 0) + elements[i].max = max(ah - child->h, 0); + } + } + + /* Distribute extra space along the line */ + int extra_space = (horiz ? cw : ch) - length; + allocate_space(elements, n, extra_space); + round_allocations(elements, n, extra_space); + + /* Update widgets for extra space */ + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + struct fxlink_TUI_box *child = box->children[e->id]; + + if(horiz) + child->w += e->allocated; + else + child->h += e->allocated; + } + + /* Position everyone */ + int position = 0; + + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + struct fxlink_TUI_box *child = box->children[e->id]; + + if(horiz) { + child->x = box->x + position; + child->y = box->y + (ch - child->h) / 2; + position += child->w + spacing; + } + else { + child->x = box->x + (cw - child->w) / 2; + child->y = box->y + position; + position += child->h + spacing; + } + } + + for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && box->children[i]; i++) + box_do_layout(box->children[i]); +} + +void fxlink_TUI_box_layout(struct fxlink_TUI_box *b, + int x, int y, int w, int h) +{ + b->x = x + 1; + b->y = y + 1; + b->w = w - 2; + b->h = h - 2; + return box_do_layout(b); +} + +static bool box_apply(struct fxlink_TUI_box *box, + int rx, int ry, int rw, int rh) +{ + if(box->type == FXLINK_TUI_BOX_WINDOW) { + if(!box->window.win) + return true; + + /* Ensure window is of non-zero size and in-bounds */ + int x = clamp(box->x, rx, rx + rw - 1); + int y = clamp(box->y, ry, ry + rh - 1); + int w = clamp(box->w, 1, rw - (x - rx)); + int h = clamp(box->h, 1, rh - (y - ry)); + + if(!*box->window.win) { + *box->window.win = newwin(h, w, y, x); + } + else { + wresize(*box->window.win, h, w); + mvwin(*box->window.win, y, x); + } + return (*box->window.win != NULL); + } + + bool success = true; + for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && box->children[i]; i++) + success = success && box_apply(box->children[i], rx, ry, rw, rh); + return success; +} + +bool fxlink_TUI_apply_layout(struct fxlink_TUI_box *root) +{ + return box_apply(root, root->x, root->y, root->w, root->h); +} diff --git a/fxlink/tui/render.c b/fxlink/tui/render.c new file mode 100644 index 0000000..bd1f819 --- /dev/null +++ b/fxlink/tui/render.c @@ -0,0 +1,108 @@ +//---------------------------------------------------------------------------// +// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. // +// |:::| Made by Lephe' as part of the fxSDK. // +// \___/ License: MIT // +//---------------------------------------------------------------------------// + +#include +#include + +void aprint(WINDOW *win, int attr, char const *format, ...) +{ + va_list args; + va_start(args, format); + wattron(win, attr); + vw_printw(win, format, args); + wattroff(win, attr); + va_end(args); +} + +void fprint(WINDOW *win, int display_fmt, char const *format, ...) +{ + int attr = fmt_to_ncurses_attr(display_fmt); + + va_list args; + va_start(args, format); + wattron(win, attr); + vw_printw(win, format, args); + wattroff(win, attr); + va_end(args); +} + +int fmt_to_ncurses_attr(int format) +{ + /* Get the color pair for FG/BG following our custom encoding (which is + compatible with the default for colors without a background) */ + int FG = fmt_FG(format); + int BG = fmt_BG(format); + int attr = COLOR_PAIR(9*BG + FG); + + if(fmt_BOLD(format)) + attr |= A_BOLD; + if(fmt_DIM(format)) + attr |= A_DIM; + if(fmt_ITALIC(format)) + attr |= A_ITALIC; + + return attr; +} + +enum { TOP=1, RIGHT=2, BOTTOM=4, LEFT=8 }; + +static void TUI_add_borders(int x, int y, int directions) +{ + chtype borders[16] = { + ' ', '\0', '\0', ACS_LLCORNER, + '\0', ACS_VLINE, ACS_ULCORNER, ACS_LTEE, + '\0', ACS_LRCORNER, ACS_HLINE, ACS_BTEE, + ACS_URCORNER, ACS_RTEE, ACS_TTEE, ACS_PLUS, + }; + + chtype ch = mvinch(y, x); + + int dirs = 0; + for(int i = 0; i < 16; i++) { + if(borders[i] == ch) + dirs = i; + } + mvaddch(y, x, borders[dirs | directions]); +} + +void fxlink_TUI_render_borders(struct fxlink_TUI_box const *b) +{ + int x = b->x, y = b->y; + + if(b->type == FXLINK_TUI_BOX_WINDOW) { + for(int dx = 0; dx < b->w; dx++) { + TUI_add_borders(x+dx, y-1, LEFT | RIGHT); + TUI_add_borders(x+dx, y+b->h, LEFT | RIGHT); + } + for(int dy = 0; dy < b->h; dy++) { + TUI_add_borders(x-1, y+dy, TOP | BOTTOM); + TUI_add_borders(x+b->w, y+dy, TOP | BOTTOM); + } + + TUI_add_borders(x-1, y-1, RIGHT | BOTTOM); + TUI_add_borders(x+b->w, y-1, LEFT | BOTTOM); + TUI_add_borders(x-1, y+b->h, RIGHT | TOP); + TUI_add_borders(x+b->w, y+b->h, LEFT | TOP); + } + else { + for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && b->children[i]; i++) + fxlink_TUI_render_borders(b->children[i]); + } +} + +void fxlink_TUI_render_titles(struct fxlink_TUI_box const *box) +{ + if(box->type == FXLINK_TUI_BOX_WINDOW) { + attron(A_BOLD); + mvaddch(box->y-1, box->x, ' '); + addstr(box->window.title); + addch(' '); + attroff(A_BOLD); + return; + } + for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && box->children[i]; i++) + fxlink_TUI_render_titles(box->children[i]); +} diff --git a/fxlink/usb.c b/fxlink/usb.c deleted file mode 100644 index c726597..0000000 --- a/fxlink/usb.c +++ /dev/null @@ -1,159 +0,0 @@ -#include "usb.h" -#include "fxlink.h" -#include "util.h" -#include -#include -#include - -char const *usb_id(libusb_device *dev) -{ - static char id[32]; - sprintf(id, "%d:%d", - libusb_get_bus_number(dev), - libusb_get_device_address(dev)); - return id; -} - -char *usb_serial_number(libusb_device_handle *dh) -{ - struct libusb_device_descriptor dc; - libusb_device *dev = libusb_get_device(dh); - - if(libusb_get_device_descriptor(dev, &dc)) - return NULL; - if(!dc.iSerialNumber) - return NULL; - - char serial[256]; - int length = libusb_get_string_descriptor_ascii(dh, dc.iSerialNumber, - (unsigned char *)serial, 256); - if(length < 0) - return NULL; - - /* LINK sends a 12-byte serial number with four leading 0. Remove them */ - int start = (length == 12 && !strncmp(serial, "0000", 4)) ? 4 : 0; - return strndup(serial + start, length - start); -} - -properties_t usb_properties(struct libusb_device_descriptor *dc, - libusb_device_handle *dh) -{ - properties_t props = { 0 }; - - /* Type of calculator based on USB behavior, detected by idProduct */ - if(dc->idProduct == 0x6101) - props.p7 = true; - if(dc->idProduct == 0x6102) - props.mass_storage = true; - if(dh) - props.serial_number = usb_serial_number(dh); - - return props; -} - -int usb_unique_matching(filter_t const *filter, libusb_context *context, - libusb_device **dev) -{ - libusb_device *unique = NULL; - int status = FILTER_NONE; - bool error; - - for_libusb_devices(it, context, &error) { - if(!filter_match(&it.props, filter)) continue; - - /* Already found a device before */ - if(unique) { - status = FILTER_MULTIPLE; - libusb_unref_device(unique); - unique = NULL; - break; - } - - /* First device: record it */ - unique = libusb_ref_device(it.dev); - status = FILTER_UNIQUE; - } - if(error) - return FILTER_ERROR; - - /* Don't keep the reference to the device if we're not returning it */ - if(unique && !dev) - libusb_unref_device(unique); - if(unique && dev) - *dev = unique; - - return status; -} - -int usb_unique_wait(filter_t const *filter, delay_t *delay, - libusb_context *context, libusb_device **dev) -{ - while(true) { - int rc = usb_unique_matching(filter, context, dev); - - /* If a device is found, multiple devices are found, or an error - occurs, forward the result; wait only if nothing was found */ - if(rc != FILTER_NONE) return rc; - if(delay_cycle(delay)) return FILTER_NONE; - } -} - -//--- -// Iteration on libusb devices -//--- - -usb_iterator_t usb_iter_start(libusb_context *context, bool *error) -{ - usb_iterator_t it = { 0 }; - - it.device_count = libusb_get_device_list(context, &it.devices); - if(it.device_count < 0) { - libusb_err(it.device_count, "cannot get libusb device list"); - it.done = true; - if(error) *error = true; - return it; - } - - it.index = -1; - usb_iter_next(&it); - if(error) *error = false; - return it; -} - -void usb_iter_next(usb_iterator_t *it) -{ - if(it->done == true) return; - int rc; - - /* Free the resources from the previous iteration */ - if(it->dh) - libusb_close(it->dh); - it->dev = NULL; - it->dh = NULL; - - /* Load the next device */ - if(++it->index >= it->device_count) { - it->done = true; - } - else { - it->dev = it->devices[it->index]; - - if((rc = libusb_get_device_descriptor(it->dev, &it->dc))) { - libusb_err(rc, "cannot get descriptor for device %s", - usb_id(it->dev)); - return usb_iter_next(it); - } - - /* Ignore non-CASIO devices */ - if(it->dc.idVendor != 0x07cf) - return usb_iter_next(it); - - if((rc = libusb_open(it->dev, &it->dh))) - libusb_wrn(rc, "cannot open device %s", usb_id(it->dev)); - - it->props = usb_properties(&it->dc, it->dh); - } - - if(it->done) - libusb_free_device_list(it->devices, true); -} diff --git a/fxlink/usb.h b/fxlink/usb.h deleted file mode 100644 index c254bd5..0000000 --- a/fxlink/usb.h +++ /dev/null @@ -1,137 +0,0 @@ -//--- -// fxlink:usb - libusb functions -//--- - -#ifndef FXLINK_USB_H -#define FXLINK_USB_H - -#include "util.h" -#include "properties.h" -#include "filter.h" -#include - -/* usb_properties(): Determine as many properties of the device as possible - - If the device can be opened, an open handle should be supplied as (dh). This - is used to determine the serial number; if the device cannot be opened, the - serial number is omitted from the device properties. - - @dc Device descriptor - @dh Open handle if the device can be opened, or NULL - -> Returns detected properties of the device. */ -properties_t usb_properties(struct libusb_device_descriptor *dc, - libusb_device_handle *dh); - -/* usb_unique_matching(): Device that matches the provided filter, if unique - - This function runs through the list of devices provided by libusb and - determines whether there is exactly one device matching the filter. If so, - a pointer to this device is set in (*dev) and FILTER_UNIQUE is returned. The - device is referenced and should be un-referenced after use for the data to - be freed. If (dev) is NULL, the pointer is not recorded and not referenced. - - If there are no devices matching the filter, (*dev) is unchanged and this - function returns FILTER_NONE. If several devices match the filter, (*dev) is - unchanged and FILTER_MULTIPLE is returned. If an error occurs and the - function cannot complete, an error is printed and FILTER_ERROR is returned. - - @filter Device filter to refine the search - @context Previously-initialized libusb context - @dev Output: unique device matching the filter (may be NULL) - -> Returns one of FILTER_{UNIQUE,NONE,MULTIPLE,ERROR}. */ -int usb_unique_matching(filter_t const *filter, libusb_context *context, - libusb_device **dev); - -/* usb_unique_wait(): Wait for a device matching the provided filter to connect - - This function waits up to the provided delay for a device matching the - specified filter to be connected. It calls usb_unique_matching() several - times per second to check for new devices being attached and initialized. - - If several devices are connected when usb_unique_wait() is first called, or - several devices are connected between two calls to usb_unique_matching(), - this function returns FILTER_MULTIPLE. As soon as a unique matching device - is found, the pointer is referenced and set in (*dev) if (dev) is not NULL, - and FILTER_UNIQUE is returned, regardless of whether other matching devices - are attached before the end of the wait period. - - If no matching device is attached during the specified period, this function - returns FILTER_NONE. If an error occurs during scanning, it returns - FILTER_ERROR. - - @filter Device filter to refine the search - @delay Time resource to use delay from - @context Previously-initialized libusb context - @dev Output: unique device matching the filter (can be NULL) - -> Returns one of FILTER_{UNIQUE,NONE,MULTIPLE,ERROR}. */ -int usb_unique_wait(filter_t const *filter, delay_t *delay, - libusb_context *context, libusb_device **dev); - -//--- -// Iteration on libusb devices -//--- - -typedef struct { - /* Current device and its device descriptor */ - libusb_device *dev; - struct libusb_device_descriptor dc; - /* If the device can be opened, its open handle, otherwise NULL */ - libusb_device_handle *dh; - /* Device properties */ - properties_t props; - /* Whether the iteration has finished */ - bool done; - - /* Internal indicators: list of devices and current index */ - libusb_device **devices; - int device_count; - int index; - -} usb_iterator_t; - -/* usb_iter_start(): Start an iteration on libusb devices - If the first step fails, returns an iterator with (done = true) and sets - (*error) to true; otherwise, sets (*error) to false. */ -usb_iterator_t usb_iter_start(libusb_context *context, bool *error); - -/* usb_iter_next(): Iterate to the next libusb device */ -void usb_iter_next(usb_iterator_t *it); - -/* Convenience for-loop macro for iteration */ -#define for_libusb_devices(NAME, context, error) \ - for(usb_iterator_t NAME = usb_iter_start(context, error); \ - !NAME.done; usb_iter_next(&NAME)) if(!NAME.done) - -//--- -// Miscellaneous -//--- - -/* usb_id(): Printable address-based identifier for error messages - This function is used in error messages to describe the device on which an - error occurred in a useful way. The pointer returned is to a static buffer - that changes at every call to this function, and should only be used briefly - to generate messages. */ -char const *usb_id(libusb_device *dev); - -/* usb_serial_number(): Serial number advertised by the device - - This function returns the serial number (as presented with iSerialNumber in - the device descriptor) of the provided device, which may or may not be - present. - - Serial numbers for CASIO calculators normally have 8 letters. The LINK - application presents a 12-character code with "0000" prepended. This - function detects this quirk and only returns the last 8 characters. gint's - driver doesn't send to "0000" prefix. - - This function requires the device to be open in order to send the request - for the STRING descriptor, and cannot be used if the process user doesn't - have write access to the device. - - @dh Open device handle - -> Returns a freshly-allocated copy of the serial number string, to be - free()'d after use, or NULL if the serial number is unspecified or cannot - be retrieved. */ -char *usb_serial_number(libusb_device_handle *dh); - -#endif /* FXLINK_USB_H */ diff --git a/fxlink/util.c b/fxlink/util.c deleted file mode 100644 index b7850d8..0000000 --- a/fxlink/util.c +++ /dev/null @@ -1,61 +0,0 @@ -#include "util.h" -#include -#include -#include -#include -#include - -char *gen_file_name(char const *path, char const *name, char const *suffix) -{ - char *filename = NULL; - int counter = 1; - - time_t time_raw; - struct tm time_bd; - time(&time_raw); - localtime_r(&time_raw, &time_bd); - - while(1) { - asprintf(&filename, "%s/fxlink-%.16s-%04d.%02d.%02d-%02dh%02d-%d.%s", - path, name, time_bd.tm_year + 1900, time_bd.tm_mon + 1, - time_bd.tm_mday, time_bd.tm_hour, time_bd.tm_min, counter, suffix); - if(!filename) continue; - - /* Try to find a name for a file that doesn't exist */ - if(access(filename, F_OK) == -1) break; - - free(filename); - counter++; - } - - return filename; -} - -delay_t delay_none(void) -{ - return 0; -} -delay_t delay_seconds(int seconds) -{ - return seconds * 4; -} -delay_t delay_infinite(void) -{ - return -1; -} - -bool delay_cycle(delay_t *delay) -{ - if(*delay == 0) return true; - - struct timespec spec = { .tv_sec=0, .tv_nsec=250000000 }; - int rc; - - /* Account for interrupts in the nanosleep(2) call */ - struct timespec req = spec; - do rc = nanosleep(&req, &req); - while(rc == -1 && errno == EINTR); - - if(*delay > 0) (*delay)--; - return false; -} diff --git a/fxlink/util.h b/fxlink/util.h deleted file mode 100644 index d9c4a83..0000000 --- a/fxlink/util.h +++ /dev/null @@ -1,76 +0,0 @@ -//--- -// fxlink:util - Utility functions and error reporting mechanisms -//--- - -#ifndef FXLINK_UTIL_H -#define FXLINK_UTIL_H - -#include -#include - -/* Literal error message printed to stderr, evaluates to 1 for a combined - return/exit() call */ -#define err(fmt, ...) ({ \ - fprintf(stderr, "error: " fmt "\n", ##__VA_ARGS__); \ - 1; \ -}) -/* Fatal error that includes a libusb error message */ -#define libusb_err(rc, fmt, ...) ({ \ - fprintf(stderr, "error: " fmt ": %s\n", ##__VA_ARGS__, \ - libusb_strerror(rc)); \ - 1; \ -}) -/* Warning message */ -#define wrn(fmt, ...) \ - fprintf(stderr, "warning: " fmt "\n", ##__VA_ARGS__) - -/* Warning that includes a libusb error message */ -#define libusb_wrn(rc, fmt, ...) \ - fprintf(stderr, "error: " fmt ": %s\n", ##__VA_ARGS__, \ - libusb_strerror(rc)) - -//--- -// File name generation -//--- - -/* gen_file_name(): Generate a unique timestamp-based file name - - This function generates a unique name for a file to be stored in [path], - with the specified [name] component, and the provided [suffix]. The - generated path looks like - - /fxlink--2021.05.09-19h23-1. - - with the [-1] suffix being chosen as to avoid overriding extisting files. - Returns a newly-allocated string to be freed after use. */ -char *gen_file_name(char const *path, char const *name, char const *suffix); - -//--- -// Delay -//--- - -/* delay_t: An expandable allocated time used to wait for devices */ -typedef int delay_t; - -/* delay_none(): No delay allowed */ -delay_t delay_none(void); -/* delay_seconds(): Initial delay from a duration in seconds */ -delay_t delay_seconds(int seconds); -/* delay_infinite(): Delay that can run through delay_cycle() indefinitely */ -delay_t delay_infinite(void); - -/* delay_cycle(): Wait for a short cycle - - This function returns (true) if the delay has expired; otherwise, it waits - for a short while (250 ms), decreases the supplied delay pointer, and - returns (false). - - Not returning (true) after waiting, even if the delay expires then, allows - the caller to perform the task they were waiting on one last time before - giving up. - - @delay Input-output: Delay resource to take time from - -> Return (true) if (*delay) has expired, or (false) after waiting. */ -bool delay_cycle(delay_t *delay); - -#endif /* FXLINK_UTIL_H */