fxlink: new tool with features from libusb and (optionally) UDisks2

This commit is contained in:
Lephenixnoir 2021-04-03 11:58:30 +02:00
parent 1a295958ee
commit c059e2395d
No known key found for this signature in database
GPG key ID: 1BBA026E13FC0495
14 changed files with 1549 additions and 0 deletions

View file

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

11
fxlink/config.h.in Normal file
View file

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

194
fxlink/filter.c Normal file
View file

@ -0,0 +1,194 @@
#include "filter.h"
#include "util.h"
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
//---
// 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);
}
}

49
fxlink/filter.h Normal file
View file

@ -0,0 +1,49 @@
//---
// fxlink:filter - Property-based device filtering
//---
#ifndef FXLINK_FILTER_H
#define FXLINK_FILTER_H
#include "properties.h"
#include <stddef.h>
#include <stdio.h>
/* 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 */

21
fxlink/fxlink.h Normal file
View file

@ -0,0 +1,21 @@
//---
// fxlink:fxlink - Application logic
//---
#ifndef FXLINK_FXLINK_H
#define FXLINK_FXLINK_H
#include <libusb.h>
#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 */

308
fxlink/main.c Normal file
View file

@ -0,0 +1,308 @@
#include "config.h"
#include "fxlink.h"
#include "util.h"
#include "properties.h"
#include "filter.h"
#include "usb.h"
#include <libusb.h>
#include <getopt.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
/* 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...] <FILES...>\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 <libudev.h>
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

42
fxlink/properties.c Normal file
View file

@ -0,0 +1,42 @@
#include "properties.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
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);
}

65
fxlink/properties.h Normal file
View file

@ -0,0 +1,65 @@
//---
// fxlink:properties - Detected models and properties of devices
//---
#ifndef FXLINK_PROPERTIES_H
#define FXLINK_PROPERTIES_H
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
/* 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 */

383
fxlink/ud2.c Normal file
View file

@ -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 <udisks/udisks.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//---
// 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 */

70
fxlink/ud2.h Normal file
View file

@ -0,0 +1,70 @@
//---
// fxlink:ud2 - UDisks2 functions
//---
#ifndef FXLINK_UD2_H
#define FXLINK_UD2_H
#ifndef FXLINK_DISABLE_UDISKS2
#include <udisks/udisks.h>
#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 */

159
fxlink/usb.c Normal file
View file

@ -0,0 +1,159 @@
#include "usb.h"
#include "fxlink.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>
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);
}

137
fxlink/usb.h Normal file
View file

@ -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 <libusb.h>
/* 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 */

32
fxlink/util.c Normal file
View file

@ -0,0 +1,32 @@
#include "util.h"
#include <time.h>
#include <errno.h>
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;
}

60
fxlink/util.h Normal file
View file

@ -0,0 +1,60 @@
//---
// fxlink:util - Utility functions and error reporting mechanisms
//---
#ifndef FXLINK_UTIL_H
#define FXLINK_UTIL_H
#include <stdio.h>
#include <stdbool.h>
/* 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 */