diff --git a/CMakeLists.txt b/CMakeLists.txt index 8072175..468e647 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,9 +3,15 @@ cmake_minimum_required(VERSION 3.16) project(fxSDK VERSION 2.3.1 LANGUAGES C) +option(FXLINK_DISABLE_UDISKS2 "Do not build the UDisks2-based features of fxlink") + find_package(PkgConfig REQUIRED) find_package(PNG REQUIRED) pkg_check_modules(libusb REQUIRED libusb-1.0 IMPORTED_TARGET) +# pkg_check_modules(libudev libudev IMPORTED_TARGET) +if(NOT FXLINK_DISABLE_UDISKS2) + pkg_check_modules(udisks2 REQUIRED udisks2 IMPORTED_TARGET) +endif() set(CMAKE_INSTALL_MESSAGE LAZY) set(SRC "${CMAKE_CURRENT_SOURCE_DIR}") @@ -26,6 +32,16 @@ add_custom_command(OUTPUT "${BIN}/fxsdk.sh" DEPENDS "${SRC}/fxsdk/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/main.c + fxlink/properties.c fxlink/ud2.c fxlink/util.c) +target_link_libraries(fxlink PkgConfig::libusb) # PkgConfig::libudev +target_include_directories(fxlink PRIVATE "${BIN}/include/fxlink") +if(NOT FXLINK_DISABLE_UDISKS2) + target_link_libraries(fxlink PkgConfig::udisks2) +endif() + # Install rules # fxsdk @@ -37,3 +53,5 @@ install(TARGETS fxg1a) # fxconv install(PROGRAMS fxconv/fxconv-main.py TYPE BIN RENAME fxconv) install(FILES fxconv/fxconv.py TYPE BIN) +#fxlink +install(TARGETS fxlink) diff --git a/fxlink/config.h.in b/fxlink/config.h.in new file mode 100644 index 0000000..377a65e --- /dev/null +++ b/fxlink/config.h.in @@ -0,0 +1,11 @@ +//--- +// 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 + +#endif /* FXLINK_CONFIG_H */ diff --git a/fxlink/filter.c b/fxlink/filter.c new file mode 100644 index 0000000..ecac728 --- /dev/null +++ b/fxlink/filter.c @@ -0,0 +1,194 @@ +#include "filter.h" +#include "util.h" +#include +#include +#include + +//--- +// Property parser +//--- + +/* skip_spaces(): Skip spaces, returns true if end of string is reached */ +bool skip_spaces(char const **input) +{ + while(isspace(**input)) (*input)++; + return (**input == 0); +} +/* isword(): Identify valid word characters for the filter */ +bool isword(int 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) +{ + 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) +{ + *name = *value = NULL; + if(skip_spaces(input)) return T_END; + + if(**input == ',' || **input == ';') { + (*input)++; + return (*input)[-1]; + } + + if(!isword(**input)) { + wrn("expected property name in filter, skipping '%c'", **input); + (*input)++; + return lex(input, name, value); + } + *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 + *value = read_word(input); + return T_PROP; +} + +filter_t *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); + 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; + } + + /* Add a new property to the current option */ + if(!strcmp(name, "p7") && !value) + option->p7 = true; + else if(!strcmp(name, "mass_storage") && !value) + option->mass_storage = true; + else if(!strcmp(name, "series_cg") && !value) + option->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"); + + 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) +{ + 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; + } + } +} + +void filter_clean_udisks2(filter_t *filter) +{ + /* Every property can be used */ + (void)filter; +} + +bool filter_match(properties_t const *props, filter_t const *filter) +{ + /* No filter is a pass-through */ + if(!filter || !filter->length) + return true; + + for(size_t i = 0; i < filter->length; i++) { + if(properties_match(props, &filter->options[i])) + return true; + } + return false; +} + +void filter_print(FILE *fp, filter_t const *filter) +{ + #define output(...) { \ + if(sep) fprintf(fp, ", "); \ + fprintf(fp, __VA_ARGS__); \ + 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); + } +} diff --git a/fxlink/filter.h b/fxlink/filter.h new file mode 100644 index 0000000..e3ad861 --- /dev/null +++ b/fxlink/filter.h @@ -0,0 +1,49 @@ +//--- +// 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 new file mode 100644 index 0000000..e08d727 --- /dev/null +++ b/fxlink/fxlink.h @@ -0,0 +1,21 @@ +//--- +// fxlink:fxlink - Application logic +//--- + +#ifndef FXLINK_FXLINK_H +#define FXLINK_FXLINK_H + +#include +#include "filter.h" +#include "util.h" + +/* Main function for -l */ +int main_list(filter_t *filter, delay_t *delay, libusb_context *context); + +/* Main function for -b */ +int main_blocks(filter_t *filter, delay_t *delay); + +/* Main function for -s */ +int main_send(filter_t *filter, delay_t *delay, char **files); + +#endif /* FXLINK_FXLINK_H */ diff --git a/fxlink/main.c b/fxlink/main.c new file mode 100644 index 0000000..6b44cca --- /dev/null +++ b/fxlink/main.c @@ -0,0 +1,308 @@ +#include "config.h" +#include "fxlink.h" +#include "util.h" +#include "properties.h" +#include "filter.h" +#include "usb.h" + +#include +#include +#include +#include +#include +#include + +/* Main functions for each mdoe */ +int main_list(filter_t *filter, delay_t *delay, libusb_context *context); +int main_test(libusb_device *device, libusb_context *context); + +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" +"\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 devics).\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" +" --test Communication tests by Lephe (libusb) [WIP!]\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" +"\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"; + +int main(int argc, char **argv) +{ + int rc=1, mode=0, error=0, option=0, loglevel=LIBUSB_LOG_LEVEL_ERROR; + delay_t delay = delay_seconds(0); + filter_t *filter = NULL; + + //--- + // Command-line argument parsing + //--- + + enum { TEST=1, LIBUSB_LOG }; + const struct option longs[] = { + { "help", no_argument, NULL, 'h' }, + { "list", no_argument, NULL, 'l' }, + { "blocks", no_argument, NULL, 'b' }, + { "send", no_argument, NULL, 's' }, + { "test", no_argument, NULL, TEST }, + { "libusb-log", required_argument, NULL, LIBUSB_LOG }, + }; + + while(option >= 0 && option != '?') + switch((option = getopt_long(argc, argv, "hlbsf:w::", longs, NULL))) + { + case 'h': + fprintf(stderr, help_string, argv[0]); + return 0; + case 'l': + case 'b': + case 's': + case TEST: + mode = option; + break; + case LIBUSB_LOG: + if(!strcmp(optarg, "NONE")) + loglevel = LIBUSB_LOG_LEVEL_NONE; + else if(!strcmp(optarg, "ERROR")) + loglevel = LIBUSB_LOG_LEVEL_ERROR; + else if(!strcmp(optarg, "WARNING")) + loglevel = LIBUSB_LOG_LEVEL_WARNING; + else if(!strcmp(optarg, "INFO")) + loglevel = LIBUSB_LOG_LEVEL_INFO; + else if(!strcmp(optarg, "DEBUG")) + loglevel = LIBUSB_LOG_LEVEL_DEBUG; + else fprintf(stderr, "warning: ignoring log level '%s'; should be " + "NONE, ERROR, WARNING, INFO or DEBUG\n", optarg); + break; + case 'w': + if(!optarg) { + delay = delay_infinite(); + break; + } + char *end; + int seconds = strtol(optarg, &end, 10); + if(seconds < 0 || *end != 0) { + error = err("invalid delay '%s'\n", optarg); + break; + } + delay = delay_seconds(seconds); + break; + case 'f': + filter = filter_parse(optarg); + break; + case '?': + error = 1; + } + + if(mode == 's' && optind == argc) + error = err("send mode requires additional arguments (file names)"); + + /* No arguments or bad arguments */ + if(error) + return 1; + if(!mode) { + fprintf(stderr, help_string, argv[0]); + return 1; + } + + //--- + // libusb initialization + //--- + + libusb_context *context = NULL; + + /* Initialize libusb for corresponding modes */ + if(mode == 'l' || mode == TEST) { + if((rc = libusb_init(&context))) + return libusb_err(rc, "error initializing libusb"); + libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, loglevel); + } + + //--- + // Main functions + //--- + + if(mode == 'l') { + rc = main_list(filter, &delay, context); + } + else if(mode == 'b') { + #ifndef FXLINK_DISABLE_UDISKS2 + rc = main_blocks(filter, &delay); + #else + rc = err("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"); + #endif + } + else if(mode == TEST) { + libusb_device *dev = NULL; + int rc = usb_unique_wait(filter, &delay, context, &dev); + + if(rc == FILTER_NONE) + printf("No device found.\n"); + else if(rc == FILTER_MULTIPLE) + printf("Multiple devices found, ambiguous!\n"); + else if(rc == FILTER_UNIQUE) { + rc = main_test(dev, context); + libusb_unref_device(dev); + } + } + + if(context) libusb_exit(context); + 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; +} + +//--- +// WIP tests +//--- + +int main_test(libusb_device *dev, libusb_context *context) +{ + libusb_device_handle *dh; + int rc; + (void)context; + + if((rc = libusb_open(dev, &dh))) + return libusb_err(rc, "cannot open device %s", usb_id(dev)); + + printf("driver active: %d\n", libusb_kernel_driver_active(dh, 0)); + + if((rc = libusb_claim_interface(dh, 0))) { + libusb_close(dh); + return libusb_err(rc, "cannot claim interface on %s", usb_id(dev)); + } + + uint8_t buffer[512]; + int transferred; + + rc = libusb_bulk_transfer(dh, 0x81, buffer, 512, &transferred, 2000); + + if(rc) + rc = libusb_err(rc, "cannot perform bulk transfer on %s", usb_id(dev)); + else { + printf("Got some data!\n"); + fwrite(buffer, 1, transferred, stdout); + rc = 0; + } + + libusb_release_interface(dh, 0); + libusb_close(dh); + return rc; +} + +/* 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/properties.c b/fxlink/properties.c new file mode 100644 index 0000000..726c6cd --- /dev/null +++ b/fxlink/properties.c @@ -0,0 +1,42 @@ +#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 new file mode 100644 index 0000000..0b988a7 --- /dev/null +++ b/fxlink/properties.h @@ -0,0 +1,65 @@ +//--- +// 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/ud2.c b/fxlink/ud2.c new file mode 100644 index 0000000..5eae092 --- /dev/null +++ b/fxlink/ud2.c @@ -0,0 +1,383 @@ +#include "config.h" +#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 +//--- + +int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr) +{ + GError *error = NULL; + + UDisksClient *udc = udisks_client_new_sync(NULL, &error); + if(error) + return err("cannot open udisks2 client: %s", 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"); + } + + *udc_ptr = udc; + *udm_ptr = udm; + return 0; +} + +void ud2_end(UDisksClient *udc, __attribute__((unused)) UDisksManager *udm) +{ + g_object_unref(udc); +} + +gchar **ud2_block_devices(UDisksManager *udm) +{ + gchar **blocks = NULL; + GVariant *args = g_variant_new("a{sv}", NULL); + 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); + } + + return blocks; +} + +UDisksBlock *ud2_block(UDisksClient *udc, gchar const *name) +{ + UDisksObject *obj = udisks_client_get_object(udc, name); + return obj ? udisks_object_get_block(obj) : NULL; +} + +UDisksDrive *ud2_drive(UDisksClient *udc, gchar const *name) +{ + UDisksObject *obj = udisks_client_get_object(udc, name); + return obj ? udisks_object_get_drive(obj) : NULL; +} + +UDisksFilesystem *ud2_filesystem(UDisksClient *udc, gchar const *name) +{ + UDisksObject *obj = udisks_client_get_object(udc, name); + return obj ? udisks_object_get_filesystem(obj) : NULL; +} + +//--- +// Matching and properties +//--- + +bool is_casio_drive(UDisksDrive *drive) +{ + return strstr(udisks_drive_get_vendor(drive), "CASIO") != NULL; +} + +properties_t ud2_properties(UDisksDrive *drive) +{ + properties_t props = { 0 }; + props.p7 = false; + props.mass_storage = true; + + if(!strcmp(udisks_drive_get_model(drive), "ColorGraph")) + props.series_cg = true; + else if(!strcmp(udisks_drive_get_model(drive), "Calculator")) + props.series_g3 = true; + + 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; + + return props; +} + +int ud2_unique_matching(filter_t const *filter, UDisksClient *udc, + UDisksManager *udm, UDisksBlock **block_ptr, UDisksDrive **drive_ptr, + UDisksFilesystem **fs_ptr) +{ + int status = FILTER_NONE; + bool error; + + UDisksBlock *block = NULL; + UDisksDrive *drive = NULL; + UDisksFilesystem *fs = NULL; + + for_udisks2_devices(it, udc, udm, &error) { + if(!filter_match(&it.props, filter)) continue; + + /* Already found a device before */ + if(status == FILTER_UNIQUE) { + status = FILTER_MULTIPLE; + g_object_unref(fs); + g_object_unref(drive); + g_object_unref(block); + block = NULL; + drive = NULL; + fs = NULL; + break; + } + + /* First device: record it */ + block = g_object_ref(it.block); + drive = g_object_ref(it.drive); + fs = g_object_ref(it.fs); + status = FILTER_UNIQUE; + } + if(error) + return FILTER_ERROR; + + if(block_ptr) *block_ptr = block; + else g_object_unref(block); + + if(drive_ptr) *drive_ptr = drive; + else g_object_unref(drive); + + if(fs_ptr) *fs_ptr = fs; + else 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) +{ + 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; + udisks_client_settle(udc); + } +} + +//--- +// Iteration on UDisks2 devices +//--- + +ud2_iterator_t ud2_iter_start(UDisksClient *udc, UDisksManager *udm, + bool *error) +{ + ud2_iterator_t it = { .udc = udc }; + + it.devices = ud2_block_devices(udm); + if(!it.devices) { + it.done = true; + if(error) *error = true; + return it; + } + + it.index = -1; + ud2_iter_next(&it); + if(error) *error = false; + return it; +} + +void ud2_iter_next(ud2_iterator_t *it) +{ + if(it->done == true) return; + + /* Free the resources from the previous iteration */ + if(it->fs) g_object_unref(it->fs); + if(it->drive) g_object_unref(it->drive); + if(it->block) g_object_unref(it->block); + it->block = NULL; + it->drive = NULL; + it->fs = NULL; + + /* Load the next device */ + if(!it->devices[++it->index]) { + it->done = true; + } + else { + gchar const *path = it->devices[it->index]; + + it->block = ud2_block(it->udc, path); + if(!it->block) return ud2_iter_next(it); + + /* Skip non-CASIO devices right away */ + it->drive = ud2_drive(it->udc, udisks_block_get_drive(it->block)); + if(!it->drive || !is_casio_drive(it->drive)) + return ud2_iter_next(it); + + /* Only consider file systems (not partition tables) */ + it->fs = ud2_filesystem(it->udc, path); + if(!it->fs) return ud2_iter_next(it); + + it->props = ud2_properties(it->drive); + } + + if(it->done) + g_strfreev(it->devices); +} + +//--- +// Main functions +//--- + +int main_blocks(filter_t *filter, delay_t *delay) +{ + filter_clean_udisks2(filter); + + UDisksClient *udc = NULL; + UDisksManager *udm = NULL; + if(ud2_start(&udc, &udm)) return 1; + + ud2_unique_wait(filter, delay, udc, udm, NULL, NULL, NULL); + + int total_devices = 0; + bool error; + + for_udisks2_devices(it, udc, udm, &error) { + if(!filter_match(&it.props, filter)) continue; + + if(total_devices > 0) printf("\n"); + + if(it.props.series_cg) + printf("fx-CG series USB Mass Storage filesystem\n"); + else if(it.props.series_g3) + printf("G-III series USB Mass Storage filesystem\n"); + else + printf("Unknown USB Mass Storage filesystem\n"); + + printf(" Block device: %s\n", + udisks_block_get_device(it.block)); + printf(" Identification: Vendor: %s, Model: %s\n", + 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); + + gchar const * const * mount_points = + udisks_filesystem_get_mount_points(it.fs); + if(!mount_points || !mount_points[0]) { + printf(" Mounted: no\n"); + } + else for(int i = 0; mount_points[i]; i++) { + printf(" Mounted at: %s\n", mount_points[i]); + } + + printf(" Properties: "); + properties_print(stdout, &it.props); + printf("\n"); + + total_devices++; + } + + if(!error && !total_devices) + printf("No%s device found.\n", filter ? " matching" : ""); + + ud2_end(udc, udm); + 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 { + waitpid(pid, NULL, 0); + } + + /* Unmount the filesystem and eject the device if we mounted it */ + if(mounted_here) { + 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/ud2.h b/fxlink/ud2.h new file mode 100644 index 0000000..4c3cc56 --- /dev/null +++ b/fxlink/ud2.h @@ -0,0 +1,70 @@ +//--- +// fxlink:ud2 - UDisks2 functions +//--- + +#ifndef FXLINK_UD2_H +#define FXLINK_UD2_H + +#ifndef FXLINK_DISABLE_UDISKS2 + +#include +#include "config.h" +#include "properties.h" +#include "filter.h" +#include "util.h" + +/* ud2_properties(): Determine properties of a UDisks2 USB drive */ +properties_t ud2_properties(UDisksDrive *drive); + +/* 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, + 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); + +//--- +// Iteration on UDisks2 devices +//--- + +typedef struct { + /* Current block, associated drive and filesystem */ + UDisksBlock *block; + UDisksDrive *drive; + UDisksFilesystem *fs; + /* Device properties */ + properties_t props; + /* Whether the iteration has finished */ + bool done; + + /* Internal indicators: list of devices and current index */ + gchar **devices; + int index; + /* Client for object queries */ + UDisksClient *udc; + +} ud2_iterator_t; + +/* ud2_iter_start(): Start an iteration on UDisks2 devices + If the first step fails, returns an iterator with (done = true) and sets + (*error) to true; otherwise, sets (*error) to false. */ +ud2_iterator_t ud2_iter_start(UDisksClient *udc, UDisksManager *udm, + bool *error); + +/* ud2_iter_next(): Iterate to the next UDisks2 device */ +void ud2_iter_next(ud2_iterator_t *it); + +/* Convenience for-loop macro for iteration */ +#define for_udisks2_devices(NAME, udc, udm, error) \ + for(ud2_iterator_t NAME = ud2_iter_start(udc, udm, error); \ + !NAME.done; ud2_iter_next(&NAME)) if(!NAME.done) + +#endif /* FXLINK_DISABLE_UDISKS2 */ + +#endif /* FXLINK_UD2_H */ diff --git a/fxlink/usb.c b/fxlink/usb.c new file mode 100644 index 0000000..c726597 --- /dev/null +++ b/fxlink/usb.c @@ -0,0 +1,159 @@ +#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 new file mode 100644 index 0000000..c254bd5 --- /dev/null +++ b/fxlink/usb.h @@ -0,0 +1,137 @@ +//--- +// 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 new file mode 100644 index 0000000..db8550f --- /dev/null +++ b/fxlink/util.c @@ -0,0 +1,32 @@ +#include "util.h" +#include +#include + +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 new file mode 100644 index 0000000..535005e --- /dev/null +++ b/fxlink/util.h @@ -0,0 +1,60 @@ +//--- +// 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_OPT__(,) __VA_ARGS__); \ + 1; \ +}) +/* Fatal error that includes a libusb error message */ +#define libusb_err(rc, fmt, ...) ({ \ + fprintf(stderr, "error: " fmt ": %s\n" __VA_OPT__(,) __VA_ARGS__, \ + libusb_strerror(rc)); \ + 1; \ +}) +/* Warning message */ +#define wrn(fmt, ...) \ + fprintf(stderr, "warning: " fmt "\n" __VA_OPT__(,) __VA_ARGS__) + +/* Warning that includes a libusb error message */ +#define libusb_wrn(rc, fmt, ...) \ + fprintf(stderr, "error: " fmt ": %s\n" __VA_OPT__(,) __VA_ARGS__, \ + libusb_strerror(rc)) + +//--- +// 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 */