diff --git a/Makefile b/Makefile index 41098db..5344e35 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ PKG_CONFIG ?= pkg-config CC ?= gcc CFLAGS ?= -g -O0 -CFLAGS += -Wall -Wextra -Wno-zero-length-array -std=c99 -pedantic +CFLAGS += -Wall -Wextra -Wno-zero-length-array -std=c99 -pedantic -I. GIT_VERSION := $(shell git describe --match "v[0-9]*" --abbrev=8 --dirty --tags | cut -c2-) ifeq ($(GIT_VERSION),) GIT_VERSION := $(shell cat VERSION) @@ -37,17 +37,22 @@ else endif PROGRAM = uhubctl - -.PHONY: all install clean +SOURCES = $(PROGRAM).c mkjson.c +OBJECTS = $(SOURCES:.c=.o) all: $(PROGRAM) -$(PROGRAM): $(PROGRAM).c - $(CC) $(CPPFLAGS) $(CFLAGS) $@.c -o $@ $(LDFLAGS) +$(PROGRAM): $(OBJECTS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(OBJECTS) -o $@ $(LDFLAGS) + +%.o: %.c + $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ install: $(INSTALL_DIR) $(DESTDIR)$(sbindir) $(INSTALL_PROGRAM) $(PROGRAM) $(DESTDIR)$(sbindir) clean: - $(RM) $(PROGRAM).o $(PROGRAM).dSYM $(PROGRAM) + $(RM) $(OBJECTS) $(PROGRAM).dSYM $(PROGRAM) + +.PHONY: all install clean diff --git a/README.md b/README.md index 63cdf65..e114563 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,148 @@ are port numbers for all hubs in chain, starting from root hub for a given USB b This address is semi-stable - it will not change if you unplug/replug (or turn off/on) USB device into the same physical USB port (this method is also used in Linux kernel). +To get the status in machine-readable JSON format, use `-j` option: + + uhubctl -j + +This will output status of all hubs and ports in JSON format, making it easy to integrate +uhubctl with other tools and scripts. The JSON output includes all the same information +as the text output, including hub info, port status, connected devices, and their properties. + + +JSON Output +=========== + +The `-j` option enables JSON output for all commands, including status queries and power actions. +The JSON is pretty-printed with proper indentation for human readability. + +Status Query JSON Format +------------------------ + +When querying hub status, the output follows this structure: + +The `status` field provides three levels of detail: +- `raw`: Original hex value from USB hub +- `decoded`: Human-readable interpretation (e.g., "device_active", "powered_no_device") +- `bits`: Individual status bits broken down by name + +```json +{ + "hubs": [ + { + "location": "3-1.4", + "description": "05e3:0610 GenesysLogic USB2.1 Hub, USB 2.10, 4 ports, ppps", + "hub_info": { + "vid": "0x05e3", + "pid": "0x0610", + "usb_version": "2.10", + "nports": 4, + "ppps": "ppps" + }, + "ports": [ + { + "port": 1, + "status": { + "raw": "0x0103", + "decoded": "device_active", + "bits": { + "connection": true, + "enabled": true, + "powered": true, + "suspended": false, + "overcurrent": false, + "reset": false, + "highspeed": false, + "lowspeed": false + } + }, + "flags": { + "connection": true, + "enable": true, + "power": true + }, + "human_readable": { + "connection": "Device is connected", + "enable": "Port is enabled", + "power": "Port power is enabled" + }, + "speed": "USB1.1 Full Speed 12Mbps", + "speed_bps": 12000000, + "vid": "0x0403", + "pid": "0x6001", + "vendor": "FTDI", + "product": "FT232R USB UART", + "device_class": 0, + "class_name": "Composite Device", + "usb_version": "2.00", + "device_version": "6.00", + "serial": "A10KZP45", + "description": "0403:6001 FTDI FT232R USB UART A10KZP45" + } + ] + } + ] +} +``` + +Power Action JSON Events +------------------------ + +When performing power actions (on/off/toggle/cycle), uhubctl outputs real-time JSON events: + +```bash +uhubctl -j -a cycle -l 3-1.4 -p 1 -d 2 +``` + +Outputs events like: + +```json +{"event": "hub_status", "hub": "3-1.4", "description": "05e3:0610 GenesysLogic USB2.1 Hub, USB 2.10, 4 ports, ppps"} +{"event": "power_change", "hub": "3-1.4", "port": 1, "action": "off", "from_state": true, "to_state": false, "success": true} +{"event": "delay", "reason": "power_cycle", "duration_seconds": 2.0} +{"event": "power_change", "hub": "3-1.4", "port": 1, "action": "on", "from_state": false, "to_state": true, "success": true} +``` + +Event types include: +- `hub_status`: Initial hub information +- `power_change`: Port power state change +- `delay`: Wait period during power cycling +- `hub_reset`: Hub reset operation (when using `-R`) + +JSON Usage Examples +------------------- + +Find all FTDI devices and show how to control them: +```bash +uhubctl -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vendor == "FTDI") | + "Device: \(.description)\nControl: uhubctl -l \($hub.location) -p \(.port) -a off\n"' +``` + +Find a device by serial number: +```bash +SERIAL="A10KZP45" +uhubctl -j | jq -r --arg serial "$SERIAL" '.hubs[] | . as $hub | .ports[] | + select(.serial == $serial) | "Found at hub \($hub.location) port \(.port)"' +``` + +List all empty ports: +```bash +uhubctl -j | jq -r '.hubs[] | . as $hub | .ports[] | + select(.vid == null) | "Empty: hub \($hub.location) port \(.port)"' +``` + +Generate CSV of all connected devices: +```bash +echo "Location,Port,VID,PID,Vendor,Product,Serial" +uhubctl -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vid) | + "\($hub.location),\(.port),\(.vid),\(.pid),\(.vendor // ""),\(.product // ""),\(.serial // "")"' +``` + +Monitor power action results: +```bash +uhubctl -j -a cycle -l 3-1 -p 2 | jq 'select(.event == "power_change")' +``` + Linux USB permissions ===================== diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..f494841 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,149 @@ +# uhubctl JSON Output Examples + +This directory contains examples of how to use uhubctl's JSON output feature (`-j` flag) to programmatically work with USB hub information. + +## Requirements + +- uhubctl compiled with JSON support +- jq (command-line JSON processor) - install with: + - macOS: `brew install jq` + - Ubuntu/Debian: `sudo apt-get install jq` + - RedHat/Fedora: `sudo yum install jq` + +## Running the Examples + +```bash +./json_usage_examples.sh +``` + +## JSON Output Format + +The JSON output provides complete information about all USB hubs and connected devices: + +```json +{ + "hubs": [ + { + "location": "3-1", + "description": "Hub description string", + "hub_info": { + "vid": "0x05e3", + "pid": "0x0608", + "usb_version": "2.00", + "nports": 4, + "ppps": "ppps" + }, + "ports": [ + { + "port": 1, + "status": "0x0103", + "flags": { + "connection": true, + "enable": true, + "power": true + }, + "human_readable": { + "connection": "Device is connected", + "enable": "Port is enabled", + "power": "Port power is enabled" + }, + "speed": "USB2.0 High Speed 480Mbps", + "speed_bps": 480000000, + "vid": "0x0781", + "pid": "0x5567", + "vendor": "SanDisk", + "product": "Cruzer Blade", + "device_class": 0, + "class_name": "Mass Storage", + "usb_version": "2.00", + "device_version": "1.00", + "nconfigs": 1, + "serial": "4C530001234567891234", + "is_mass_storage": true, + "interfaces": [...], + "description": "0781:5567 SanDisk Cruzer Blade 4C530001234567891234" + } + ] + } + ] +} +``` + +## Common Use Cases + +### 1. Find Device by Serial Number +```bash +SERIAL="4C530001234567891234" +uhubctl -j | jq -r --arg s "$SERIAL" '.hubs[] | . as $h | .ports[] | select(.serial == $s) | "uhubctl -l \($h.location) -p \(.port) -a cycle"' +``` + +### 2. List All Mass Storage Devices +```bash +uhubctl -j | jq -r '.hubs[].ports[] | select(.is_mass_storage == true) | .description' +``` + +### 3. Find Empty Ports +```bash +uhubctl -j | jq -r '.hubs[] | . as $h | .ports[] | select(.vid == null) | "Hub \($h.location) Port \(.port)"' +``` + +### 4. Generate Control Commands for Device Type +```bash +# Power off all FTDI devices +uhubctl -j | jq -r '.hubs[] | . as $h | .ports[] | select(.vendor == "FTDI") | "uhubctl -l \($h.location) -p \(.port) -a off"' | bash +``` + +### 5. Monitor for Device Changes +```bash +# Save baseline +uhubctl -j > baseline.json + +# Later, check what changed +uhubctl -j | jq -r --argjson old "$(cat baseline.json)" ' + . as $new | + $old.hubs[].ports[] as $op | + $new.hubs[] | . as $h | + .ports[] | + select(.port == $op.port and $h.location == $op.hub_location and .vid != $op.vid) | + "Change at \($h.location):\(.port)"' +``` + +## JSON Fields Reference + +### Hub Object +- `location` - Hub location (e.g., "3-1", "1-1.4") +- `description` - Full hub description string +- `hub_info` - Parsed hub information + - `vid` - Vendor ID in hex + - `pid` - Product ID in hex + - `usb_version` - USB version string + - `nports` - Number of ports + - `ppps` - Power switching mode +- `ports` - Array of port objects + +### Port Object +- `port` - Port number +- `status` - Raw status value in hex +- `flags` - Boolean flags (only true values included) +- `human_readable` - Human-readable flag descriptions +- `speed` - Speed description string +- `speed_bps` - Speed in bits per second (numeric) +- `vid`, `pid` - Device vendor/product IDs +- `vendor`, `product` - Device vendor/product names +- `serial` - Device serial number +- `device_class` - USB device class code +- `class_name` - Device class name +- `is_mass_storage` - Boolean flag for mass storage devices +- `interfaces` - Array of interface descriptors + +### USB3-Specific Fields +- `port_speed` - Link speed (e.g., "5gbps") +- `link_state` - USB3 link state (e.g., "U0", "U3") + +## Tips + +1. Use `jq -r` for raw output (no quotes) +2. Use `select()` to filter results +3. Use `. as $var` to save context when diving into nested objects +4. Use `// "default"` to provide default values for missing fields +5. Combine with shell scripts for automation \ No newline at end of file diff --git a/examples/json_usage_examples.sh b/examples/json_usage_examples.sh new file mode 100755 index 0000000..0f3df8b --- /dev/null +++ b/examples/json_usage_examples.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# Examples of using $UHUBCTL JSON output with jq +# +# This script demonstrates various ways to parse and use the JSON output +# from $UHUBCTL to find devices and generate control commands. +# +# Requirements: uhubctl with -j support and jq installed +# + +# Use local uhubctl if available, otherwise use system version +UHUBCTL="uhubctl" +if [ -x "./uhubctl" ]; then + UHUBCTL="./uhubctl" +elif [ -x "../uhubctl" ]; then + UHUBCTL="../uhubctl" +fi + +echo "=== Example 1: Find all FTDI devices and their control paths ===" +echo "# This finds all FTDI devices and shows how to control them" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vendor == "FTDI") | "Device: \(.description)\nControl with: $UHUBCTL -l \($hub.location) -p \(.port) -a off\n"' + +echo -e "\n=== Example 2: Find a device by serial number ===" +echo "# This is useful when you have multiple identical devices" +SERIAL="A10KZP45" # Change this to your device's serial +$UHUBCTL -j | jq -r --arg serial "$SERIAL" '.hubs[] | . as $hub | .ports[] | select(.serial == $serial) | "Found device with serial \($serial):\n Location: \($hub.location)\n Port: \(.port)\n Control: $UHUBCTL -l \($hub.location) -p \(.port) -a cycle"' + +echo -e "\n=== Example 3: List all mass storage devices with their control commands ===" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.is_mass_storage == true) | "Mass Storage: \(.description)\n Power off: $UHUBCTL -l \($hub.location) -p \(.port) -a off\n Power on: $UHUBCTL -l \($hub.location) -p \(.port) -a on\n"' + +echo -e "\n=== Example 4: Find all USB3 devices (5Gbps or faster) ===" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.speed_bps >= 5000000000) | "\(.description) at \($hub.location):\(.port) - Speed: \(.speed)"' + +echo -e "\n=== Example 5: Create a device map for documentation ===" +echo "# This creates a sorted list of all connected devices" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vid) | "[\($hub.location):\(.port)] \(.vendor // "Unknown") \(.product // .description) (Serial: \(.serial // "N/A"))"' | sort + +echo -e "\n=== Example 6: Find empty ports where you can plug devices ===" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vid == null) | "Empty port available at hub \($hub.location) port \(.port)"' + +echo -e "\n=== Example 7: Generate power control script for specific device type ===" +echo "# This generates a script to control all FTDI devices" +echo "#!/bin/bash" +echo "# Script to control all FTDI devices" +echo "# Usage: $0 [on|off|cycle]" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vendor == "FTDI") | "# \(.description)\n$UHUBCTL -l \($hub.location) -p \(.port) -a $1"' + +echo -e "\n=== Example 8: Monitor device changes ===" +echo "# Save current state and compare later to see what changed" +echo "# Save current state:" +echo "$UHUBCTL -j > devices_before.json" +echo "# Later, check what changed:" +echo "$UHUBCTL -j > devices_after.json" +echo "diff <(jq -S . devices_before.json) <(jq -S . devices_after.json)" + +echo -e "\n=== Example 9: Find devices by class ===" +echo "# Find all Hub devices" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.class_name == "Hub") | "Hub: \(.description) at \($hub.location):\(.port)"' + +echo -e "\n=== Example 10: Export to CSV ===" +echo "# Export device list to CSV format" +echo "Location,Port,VID,PID,Vendor,Product,Serial,Speed" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.vid) | "\($hub.location),\(.port),\(.vid),\(.pid),\(.vendor // ""),\(.product // ""),\(.serial // ""),\(.speed)"' + +echo -e "\n=== Example 11: Find devices on specific hub ===" +echo "# Find all devices on hub 3-1" +LOCATION="3-1" +$UHUBCTL -j | jq -r --arg loc "$LOCATION" '.hubs[] | select(.location == $loc) | .ports[] | select(.vid) | "Port \(.port): \(.description)"' + +echo -e "\n=== Example 12: Power cycle all devices of a specific type ===" +echo "# This creates a one-liner to power cycle all USB mass storage devices" +echo -n "$UHUBCTL" +$UHUBCTL -j | jq -r '.hubs[] | . as $hub | .ports[] | select(.is_mass_storage == true) | " -l \($hub.location) -p \(.port)"' | tr '\n' ' ' +echo " -a cycle" \ No newline at end of file diff --git a/mkjson.c b/mkjson.c new file mode 100644 index 0000000..dec2803 --- /dev/null +++ b/mkjson.c @@ -0,0 +1,790 @@ +/* mkjson.c - a part of mkjson library + * + * Copyright (C) 2018 Jacek Wieczorek + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#include "mkjson.h" +#include +#include +#include +#include +#include + +// Works like asprintf, but it's always there +// I don't want the name to collide with anything +static int allsprintf( char **strp, const char *fmt, ... ) +{ + int len; + va_list ap; + va_start( ap, fmt ); + + #ifdef _GNU_SOURCE + // Just hand everything to vasprintf, if it's available + len = vasprintf( strp, fmt, ap ); + #else + // Or do it the manual way + char *buf; + len = vsnprintf( NULL, 0, fmt, ap ); + if ( len >= 0 ) + { + buf = malloc( ++len ); + if ( buf != NULL ) + { + // Hopefully, that's the right way to do it + va_end( ap ); + va_start( ap, fmt ); + + // Write and return the data + len = vsnprintf( buf, len, fmt, ap ); + if ( len >= 0 ) + { + *strp = buf; + } + else + { + free( buf ); + } + } + } + #endif + + va_end( ap ); + return len; +} + +// Calculate the escaped length of a string (excluding null terminator) +static size_t json_escaped_len(const char *str) +{ + size_t len = 0; + + if (!str) return 4; // "null" + + for (const char *p = str; *p; p++) { + switch (*p) { + case '"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + len += 2; // Escaped char: \X + break; + default: + if ((unsigned char)*p < 0x20) { + len += 6; // Unicode escape: \uXXXX + } else { + len += 1; // Regular char + } + break; + } + } + + return len; +} + +// Escape a string for JSON. Caller must free the returned string +static char *json_escape_string(const char *str) +{ + if (!str) { + char *null_str = malloc(5); + if (null_str) strcpy(null_str, "null"); + return null_str; + } + + size_t escaped_len = json_escaped_len(str); + char *escaped = malloc(escaped_len + 1); + if (!escaped) return NULL; + + char *dst = escaped; + for (const char *p = str; *p; p++) { + switch (*p) { + case '"': *dst++ = '\\'; *dst++ = '"'; break; + case '\\': *dst++ = '\\'; *dst++ = '\\'; break; + case '\b': *dst++ = '\\'; *dst++ = 'b'; break; + case '\f': *dst++ = '\\'; *dst++ = 'f'; break; + case '\n': *dst++ = '\\'; *dst++ = 'n'; break; + case '\r': *dst++ = '\\'; *dst++ = 'r'; break; + case '\t': *dst++ = '\\'; *dst++ = 't'; break; + default: + if ((unsigned char)*p < 0x20) { + // Control characters: use \uXXXX + sprintf(dst, "\\u%04x", (unsigned char)*p); + dst += 6; + } else { + *dst++ = *p; + } + break; + } + } + *dst = '\0'; + + return escaped; +} + +// Return JSON string built from va_arg arguments +// If no longer needed, should be passed to free() by user +char *mkjson( enum mkjson_container_type otype, int count, ... ) +{ + int i, len, goodchunks = 0, failure = 0; + char *json, *prefix, **chunks, ign; + + // Value - type and data + enum mkjson_value_type vtype; + const char *key; + long long int intval; + long double dblval; + const char *strval; + + // Since v0.9 count cannot be a negative value and datatype is indicated by a separate argument + // Since I'm not sure whether it's right to put assertions in libraries, the next line is commented out + // assert( count >= 0 && "After v0.9 negative count is prohibited; please use otype argument instead" ); + if ( count < 0 || ( otype != MKJSON_OBJ && otype != MKJSON_ARR ) ) return NULL; + + // Allocate chunk pointer array - on standard platforms each one should be NULL + chunks = calloc( count, sizeof( char* ) ); + if ( chunks == NULL ) return NULL; + + // This should rather be at the point of no return + va_list ap; + va_start( ap, count ); + + // Create chunks + for ( i = 0; i < count && !failure; i++ ) + { + // Get value type + vtype = va_arg( ap, enum mkjson_value_type ); + + // Get key + if ( otype == MKJSON_OBJ ) + { + key = va_arg( ap, char* ); + if ( key == NULL ) + { + failure = 1; + break; + } + } + else key = ""; + + // Generate prefix + if ( otype == MKJSON_OBJ ) + { + // Escape the key for object mode + char *escaped_key = json_escape_string( key ); + if ( !escaped_key ) + { + failure = 1; + break; + } + int ret = allsprintf( &prefix, "\"%s\": ", escaped_key ); + free( escaped_key ); + if ( ret == -1 ) + { + failure = 1; + break; + } + } + else + { + // Array mode - no prefix needed + if ( allsprintf( &prefix, "" ) == -1 ) + { + failure = 1; + break; + } + } + + // Depending on value type + ign = 0; + switch ( vtype ) + { + // Ignore string / JSON data + case MKJSON_IGN_STRING: + case MKJSON_IGN_JSON: + (void) va_arg( ap, const char* ); + ign = 1; + break; + + // Ignore string / JSON data and pass the pointer to free + case MKJSON_IGN_STRING_FREE: + case MKJSON_IGN_JSON_FREE: + free( va_arg( ap, char* ) ); + ign = 1; + break; + + // Ignore int / long long int + case MKJSON_IGN_INT: + case MKJSON_IGN_LLINT: + if ( vtype == MKJSON_IGN_INT ) + (void) va_arg( ap, int ); + else + (void) va_arg( ap, long long int ); + ign = 1; + break; + + // Ignore double / long double + case MKJSON_IGN_DOUBLE: + case MKJSON_IGN_LDOUBLE: + if ( vtype == MKJSON_IGN_DOUBLE ) + (void) va_arg( ap, double ); + else + (void) va_arg( ap, long double ); + ign = 1; + break; + + // Ignore boolean + case MKJSON_IGN_BOOL: + (void) va_arg( ap, int ); + ign = 1; + break; + + // Ignore null value + case MKJSON_IGN_NULL: + ign = 1; + break; + + // A null-terminated string + case MKJSON_STRING: + case MKJSON_STRING_FREE: + strval = va_arg( ap, const char* ); + + // If the pointer points to NULL, the string will be replaced with JSON null value + if ( strval == NULL ) + { + if ( allsprintf( chunks + i, "%snull", prefix ) == -1 ) + chunks[i] = NULL; + } + else + { + char *escaped = json_escape_string( strval ); + if ( escaped ) + { + if ( allsprintf( chunks + i, "%s\"%s\"", prefix, escaped ) == -1 ) + chunks[i] = NULL; + free( escaped ); + } + else + { + chunks[i] = NULL; + } + } + + // Optional free + if ( vtype == MKJSON_STRING_FREE ) + free( (char*) strval ); + break; + + // Embed JSON data + case MKJSON_JSON: + case MKJSON_JSON_FREE: + strval = va_arg( ap, const char* ); + + // If the pointer points to NULL, the JSON data is replaced with null value + if ( allsprintf( chunks + i, "%s%s", prefix, strval == NULL ? "null" : strval ) == -1 ) + chunks[i] = NULL; + + // Optional free + if ( vtype == MKJSON_JSON_FREE ) + free( (char*) strval ); + break; + + // int / long long int + case MKJSON_INT: + case MKJSON_LLINT: + if ( vtype == MKJSON_INT ) + intval = va_arg( ap, int ); + else + intval = va_arg( ap, long long int ); + + if ( allsprintf( chunks + i, "%s%lld", prefix, intval ) == -1 ) chunks[i] = NULL; + break; + + // double / long double + case MKJSON_DOUBLE: + case MKJSON_LDOUBLE: + if ( vtype == MKJSON_DOUBLE ) + dblval = va_arg( ap, double ); + else + dblval = va_arg( ap, long double ); + + if ( allsprintf( chunks + i, "%s%Lf", prefix, dblval ) == -1 ) chunks[i] = NULL; + break; + + // double / long double + case MKJSON_SCI_DOUBLE: + case MKJSON_SCI_LDOUBLE: + if ( vtype == MKJSON_SCI_DOUBLE ) + dblval = va_arg( ap, double ); + else + dblval = va_arg( ap, long double ); + + if ( allsprintf( chunks + i, "%s%Le", prefix, dblval ) == -1 ) chunks[i] = NULL; + break; + + // Boolean + case MKJSON_BOOL: + intval = va_arg( ap, int ); + if ( allsprintf( chunks + i, "%s%s", prefix, intval ? "true" : "false" ) == -1 ) chunks[i] = NULL; + break; + + // JSON null + case MKJSON_NULL: + if ( allsprintf( chunks + i, "%snull", prefix ) == -1 ) chunks[i] = NULL; + break; + + // Bad type specifier + default: + chunks[i] = NULL; + break; + } + + // Free prefix memory + free( prefix ); + + // NULL chunk without ignore flag indicates failure + if ( !ign && chunks[i] == NULL ) failure = 1; + + // NULL chunk now indicates ignore flag + if ( ign ) chunks[i] = NULL; + else goodchunks++; + } + + // We won't use ap anymore + va_end( ap ); + + // If everything is fine, merge chunks and create full JSON table + if ( !failure ) + { + // Get total length (this is without NUL byte) + len = 0; + for ( i = 0; i < count; i++ ) + if ( chunks[i] != NULL ) + len += strlen( chunks[i] ); + + // Total length = Chunks length + 2 brackets + separators + if ( goodchunks == 0 ) goodchunks = 1; + len = len + 2 + ( goodchunks - 1 ) * 2; + + // Allocate memory for the whole thing + json = calloc( len + 1, sizeof( char ) ); + if ( json != NULL ) + { + // Merge chunks (and do not overwrite the first bracket) + for ( i = 0; i < count; i++ ) + { + // Add separators: + // - not on the begining + // - always after valid chunk + // - between two valid chunks + // - between valid and ignored chunk if the latter isn't the last one + if ( i != 0 && chunks[i - 1] != NULL && ( chunks[i] != NULL || ( chunks[i] == NULL && i != count - 1 ) ) ) + strcat( json + 1, ", "); + + if ( chunks[i] != NULL ) + strcat( json + 1, chunks[i] ); + } + + // Add proper brackets + json[0] = otype == MKJSON_OBJ ? '{' : '['; + json[len - 1] = otype == MKJSON_OBJ ? '}' : ']'; + } + } + else json = NULL; + + // Free chunks + for ( i = 0; i < count; i++ ) + free( chunks[i] ); + free( chunks ); + + return json; +} + +// Helper function for pretty printing with indentation +static char *mkjson_array_internal( enum mkjson_container_type otype, const mkjson_arg *args, int indent_size, int current_depth ); + +// Null-terminated array version of mkjson +char *mkjson_array( enum mkjson_container_type otype, const mkjson_arg *args ) +{ + return mkjson_array_internal(otype, args, 0, 0); +} + +// Pretty-printed version with indentation +char *mkjson_array_pretty( enum mkjson_container_type otype, const mkjson_arg *args, int indent_size ) +{ + return mkjson_array_internal(otype, args, indent_size, 0); +} + +// Internal implementation that handles both pretty and compact output +static char *mkjson_array_internal( enum mkjson_container_type otype, const mkjson_arg *args, int indent_size, int current_depth ) +{ + // Pretty printing helpers + int pretty = (indent_size > 0); + char *indent = NULL; + char *newline = pretty ? "\n" : ""; + + // Create indent string for current depth + if (pretty) { + int indent_len = indent_size * current_depth; + indent = malloc(indent_len + 1); + if (!indent) return NULL; + memset(indent, ' ', indent_len); + indent[indent_len] = '\0'; + } else { + indent = ""; + } + + // Create indent for nested content + char *nested_indent = NULL; + if (pretty) { + int nested_len = indent_size * (current_depth + 1); + nested_indent = malloc(nested_len + 1); + if (!nested_indent) { + if (pretty) free(indent); + return NULL; + } + memset(nested_indent, ' ', nested_len); + nested_indent[nested_len] = '\0'; + } else { + nested_indent = ""; + } + + // Count arguments + int count = 0; + if (args != NULL) { + for (const mkjson_arg *arg = args; arg->type != 0; arg++) { + count++; + } + } + + // Allocate space for varargs + // We need 3 values per argument for objects (type, key, value) + // or 2 values per argument for arrays (type, value) + int vararg_count = count * (otype == MKJSON_OBJ ? 3 : 2); + void **varargs = calloc(vararg_count, sizeof(void*)); + if (varargs == NULL) return NULL; + + // Convert array to varargs format + int idx = 0; + for (int i = 0; i < count; i++) { + const mkjson_arg *arg = &args[i]; + + // Add type + varargs[idx++] = (void*)(intptr_t)arg->type; + + // Add key for objects + if (otype == MKJSON_OBJ) { + varargs[idx++] = (void*)arg->key; + } + + // Add value based on type + switch (arg->type) { + case MKJSON_STRING: + case MKJSON_STRING_FREE: + case MKJSON_JSON: + case MKJSON_JSON_FREE: + varargs[idx++] = (void*)arg->value.str_val; + break; + case MKJSON_INT: + varargs[idx++] = (void*)(intptr_t)arg->value.int_val; + break; + case MKJSON_LLINT: + varargs[idx++] = (void*)(intptr_t)arg->value.llint_val; + break; + case MKJSON_DOUBLE: + // For double, we need to pass by value, which is tricky + // Let's use a different approach + break; + case MKJSON_LDOUBLE: + // Same issue as double + break; + case MKJSON_BOOL: + varargs[idx++] = (void*)(intptr_t)arg->value.bool_val; + break; + case MKJSON_NULL: + // No value needed + idx--; + break; + default: + // Ignore types + if (arg->type < 0) { + // Handle based on the positive type + switch (-arg->type) { + case MKJSON_STRING: + case MKJSON_STRING_FREE: + case MKJSON_JSON: + case MKJSON_JSON_FREE: + varargs[idx++] = (void*)arg->value.str_val; + break; + case MKJSON_INT: + varargs[idx++] = (void*)(intptr_t)arg->value.int_val; + break; + case MKJSON_LLINT: + varargs[idx++] = (void*)(intptr_t)arg->value.llint_val; + break; + case MKJSON_BOOL: + varargs[idx++] = (void*)(intptr_t)arg->value.bool_val; + break; + default: + idx--; + break; + } + } else { + idx--; + } + break; + } + } + + // Actually, this approach won't work well with varargs + // Let's use a simpler approach that builds the JSON directly + free(varargs); + + // Build JSON manually to avoid varargs complexity + char *json = NULL; + int len = 0; + int capacity = 256; + json = malloc(capacity); + if (!json) { + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + + // Start with opening bracket + json[0] = otype == MKJSON_OBJ ? '{' : '['; + len = 1; + + // Add newline after opening bracket if pretty printing + if (pretty && count > 0) { + if (len + 1 > capacity) { + capacity *= 2; + char *new_json = realloc(json, capacity); + if (!new_json) { + free(json); + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + json = new_json; + } + json[len++] = '\n'; + } + + // Process each argument + int first = 1; + for (int i = 0; i < count; i++) { + const mkjson_arg *arg = &args[i]; + char *chunk = NULL; + char *key_escaped = NULL; + char *val_escaped = NULL; + int chunk_len = 0; + + // Skip ignore types + if (arg->type < 0) continue; + + // Add comma if not first + if (!first) { + int comma_space = pretty ? (1 + strlen(newline) + strlen(nested_indent)) : 2; + while (len + comma_space > capacity) { + capacity *= 2; + char *new_json = realloc(json, capacity); + if (!new_json) { + free(json); + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + json = new_json; + } + json[len++] = ','; + if (pretty) { + strcpy(json + len, newline); + len += strlen(newline); + strcpy(json + len, nested_indent); + len += strlen(nested_indent); + } else { + json[len++] = ' '; + } + } else if (pretty) { + // First item - add indent + int indent_space = strlen(nested_indent); + while (len + indent_space > capacity) { + capacity *= 2; + char *new_json = realloc(json, capacity); + if (!new_json) { + free(json); + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + json = new_json; + } + strcpy(json + len, nested_indent); + len += strlen(nested_indent); + } + first = 0; + + // For objects, add key + if (otype == MKJSON_OBJ && arg->key) { + key_escaped = json_escape_string(arg->key); + if (!key_escaped) { + free(json); + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + } + + // Format value based on type + switch (arg->type) { + case MKJSON_STRING: + case MKJSON_STRING_FREE: + val_escaped = json_escape_string(arg->value.str_val); + if (!val_escaped) { + free(key_escaped); + free(json); + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s\"%s\"", key_escaped, pretty ? " " : "", val_escaped); + } else { + allsprintf(&chunk, "\"%s\"", val_escaped); + } + free(val_escaped); + if (arg->type == MKJSON_STRING_FREE) { + free(arg->value.str_free_val); + } + break; + + case MKJSON_JSON: + case MKJSON_JSON_FREE: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%s", key_escaped, + pretty ? " " : "", arg->value.str_val ? arg->value.str_val : "null"); + } else { + allsprintf(&chunk, "%s", + arg->value.str_val ? arg->value.str_val : "null"); + } + if (arg->type == MKJSON_JSON_FREE) { + free(arg->value.str_free_val); + } + break; + + case MKJSON_INT: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%d", key_escaped, pretty ? " " : "", arg->value.int_val); + } else { + allsprintf(&chunk, "%d", arg->value.int_val); + } + break; + + case MKJSON_LLINT: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%lld", key_escaped, pretty ? " " : "", arg->value.llint_val); + } else { + allsprintf(&chunk, "%lld", arg->value.llint_val); + } + break; + + case MKJSON_DOUBLE: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%f", key_escaped, pretty ? " " : "", arg->value.dbl_val); + } else { + allsprintf(&chunk, "%f", arg->value.dbl_val); + } + break; + + case MKJSON_LDOUBLE: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%Lf", key_escaped, pretty ? " " : "", arg->value.ldbl_val); + } else { + allsprintf(&chunk, "%Lf", arg->value.ldbl_val); + } + break; + + case MKJSON_SCI_DOUBLE: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%e", key_escaped, pretty ? " " : "", arg->value.dbl_val); + } else { + allsprintf(&chunk, "%e", arg->value.dbl_val); + } + break; + + case MKJSON_SCI_LDOUBLE: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%Le", key_escaped, pretty ? " " : "", arg->value.ldbl_val); + } else { + allsprintf(&chunk, "%Le", arg->value.ldbl_val); + } + break; + + case MKJSON_BOOL: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%s%s", key_escaped, pretty ? " " : "", + arg->value.bool_val ? "true" : "false"); + } else { + allsprintf(&chunk, "%s", arg->value.bool_val ? "true" : "false"); + } + break; + + case MKJSON_NULL: + if (otype == MKJSON_OBJ) { + allsprintf(&chunk, "\"%s\":%snull", key_escaped, pretty ? " " : ""); + } else { + allsprintf(&chunk, "null"); + } + break; + + default: + break; + } + + free(key_escaped); + + // Add chunk to json + if (chunk) { + chunk_len = strlen(chunk); + while (len + chunk_len + 2 > capacity) { + capacity *= 2; + char *new_json = realloc(json, capacity); + if (!new_json) { free(json); free(chunk); return NULL; } + json = new_json; + } + strcpy(json + len, chunk); + len += chunk_len; + free(chunk); + } + } + + // Add closing bracket with proper indentation + if (pretty && count > 0) { + // Add newline and indent before closing bracket + int closing_space = strlen(newline) + strlen(indent) + 1; + while (len + closing_space > capacity) { + capacity *= 2; + char *new_json = realloc(json, capacity); + if (!new_json) { + free(json); + if (pretty) { free(indent); free(nested_indent); } + return NULL; + } + json = new_json; + } + strcpy(json + len, newline); + len += strlen(newline); + strcpy(json + len, indent); + len += strlen(indent); + } + + json[len++] = otype == MKJSON_OBJ ? '}' : ']'; + json[len] = '\0'; + + // Free allocated indent strings + if (pretty) { + free(indent); + free(nested_indent); + } + + return json; +} + diff --git a/mkjson.h b/mkjson.h new file mode 100644 index 0000000..28ad50d --- /dev/null +++ b/mkjson.h @@ -0,0 +1,68 @@ +/* mkjson.h - a part of mkjson library + * + * Copyright (C) 2018 Jacek Wieczorek + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +#ifndef MKJSON_H +#define MKJSON_H + +// JSON container types +enum mkjson_container_type +{ + MKJSON_ARR = 0, // An array + MKJSON_OBJ = 1 // An object (hash or whatever you call it) +}; + +// JSON data types +enum mkjson_value_type +{ + MKJSON_STRING = (int)('s'), // const char* - String data + MKJSON_STRING_FREE = (int)('f'), // char* - String data, but pointer is freed + MKJSON_JSON = (int)('r'), // const char* - JSON data (like string, but no quotes) + MKJSON_JSON_FREE = (int)('j'), // char* - JSON data, but pointer is freed + MKJSON_INT = (int)('i'), // int - An integer + MKJSON_LLINT = (int)('I'), // long long int - A long integer + MKJSON_DOUBLE = (int)('d'), // double - A double + MKJSON_LDOUBLE = (int)('D'), // long double - A long double + MKJSON_SCI_DOUBLE = (int)('e'), // double - A double with scientific notation + MKJSON_SCI_LDOUBLE = (int)('E'), // long double - A long double with scientific notation + MKJSON_BOOL = (int)('b'), // int - A boolean value + MKJSON_NULL = (int)('n'), // -- - JSON null value + + // These cause one argument of certain type to be ignored + MKJSON_IGN_STRING = (-MKJSON_STRING), + MKJSON_IGN_STRING_FREE = (-MKJSON_STRING_FREE), + MKJSON_IGN_JSON = (-MKJSON_JSON), + MKJSON_IGN_JSON_FREE = (-MKJSON_JSON_FREE), + MKJSON_IGN_INT = (-MKJSON_INT), + MKJSON_IGN_LLINT = (-MKJSON_LLINT), + MKJSON_IGN_DOUBLE = (-MKJSON_DOUBLE), + MKJSON_IGN_LDOUBLE = (-MKJSON_LDOUBLE), + MKJSON_IGN_BOOL = (-MKJSON_BOOL), + MKJSON_IGN_NULL = (-MKJSON_NULL) +}; + +extern char *mkjson( enum mkjson_container_type otype, int count, ... ); + +// Null-terminated array version - no need to count parameters +typedef struct { + enum mkjson_value_type type; + const char *key; // For objects only, can be NULL for arrays + union { + const char *str_val; + char *str_free_val; + int int_val; + long long int llint_val; + double dbl_val; + long double ldbl_val; + int bool_val; + } value; +} mkjson_arg; + +extern char *mkjson_array( enum mkjson_container_type otype, const mkjson_arg *args ); +extern char *mkjson_array_pretty( enum mkjson_container_type otype, const mkjson_arg *args, int indent_size ); + +#endif diff --git a/uhubctl.c b/uhubctl.c index c7c8629..e2a5b3d 100644 --- a/uhubctl.c +++ b/uhubctl.c @@ -20,6 +20,8 @@ #include #include +#include "mkjson.h" + #if defined(_WIN32) #include #include @@ -182,6 +184,40 @@ struct usb_port_status { #define HUB_CHAR_TTTT 0x0060 /* TT Think Time mask */ #define HUB_CHAR_PORTIND 0x0080 /* per-port indicators (LEDs) */ + +/* + * USB Speed definitions + * Reference: USB 3.2 Specification, Table 10-10 + */ +#define USB_SPEED_UNKNOWN 0 +#define USB_SPEED_LOW 1 /* USB 1.0/1.1 Low Speed: 1.5 Mbit/s */ +#define USB_SPEED_FULL 2 /* USB 1.0/1.1 Full Speed: 12 Mbit/s */ +#define USB_SPEED_HIGH 3 /* USB 2.0 High Speed: 480 Mbit/s */ +#define USB_SPEED_SUPER 4 /* USB 3.0 SuperSpeed: 5 Gbit/s */ +#define USB_SPEED_SUPER_PLUS 5 /* USB 3.1 SuperSpeed+: 10 Gbit/s */ +#define USB_SPEED_SUPER_PLUS_20 6 /* USB 3.2 SuperSpeed+ 20Gbps: 20 Gbit/s */ +#define USB_SPEED_USB4_20 7 /* USB4 20Gbps */ +#define USB_SPEED_USB4_40 8 /* USB4 40Gbps */ +#define USB_SPEED_USB4_80 9 /* USB4 Version 2.0: 80Gbps */ + +/* + * USB Port StatusS Speed masks + */ +#define USB_PORT_STAT_SPEED_MASK 0x1C00 + +/* + * USB 3.0 and 3.1 speed encodings +*/ +#define USB_PORT_STAT_SPEED_5GBPS 0x0000 +#define USB_PORT_STAT_SPEED_10GBPS 0x0400 +#define USB_PORT_STAT_SPEED_20GBPS 0x0800 + +/* + * Additional speed encodings for USB4 + */ +#define USB_PORT_STAT_SPEED_40GBPS 0x0C00 +#define USB_PORT_STAT_SPEED_80GBPS 0x1000 + /* List of all USB devices enumerated by libusb */ static struct libusb_device **usb_devs = NULL; @@ -190,6 +226,14 @@ struct descriptor_strings { char product[64]; char serial[64]; char description[512]; + /* Additional fields for JSON output */ + uint16_t vid; + uint16_t pid; + uint8_t device_class; + char class_name[64]; + uint16_t usb_version; + uint16_t device_version; + int is_mass_storage; }; struct hub_info { @@ -229,6 +273,8 @@ static int opt_exact = 0; /* exact location match - disable USB3 duality handl static int opt_reset = 0; /* reset hub after operation(s) */ static int opt_force = 0; /* force operation even on unsupported hubs */ static int opt_nodesc = 0; /* skip querying device description */ +static int opt_json = 0; /* output in JSON format */ + #if defined(__linux__) static int opt_nosysfs = 0; /* don't use the Linux sysfs port disable interface, even if available */ #if (LIBUSB_API_VERSION >= 0x01000107) /* 1.0.23 */ @@ -241,7 +287,7 @@ static int is_rpi_4b = 0; static int is_rpi_5 = 0; static const char short_options[] = - "l:L:n:a:p:d:r:w:s:H:hvefRN" + "l:L:n:a:p:d:r:w:s:H:vefRNjh" #if defined(__linux__) "S" #if (LIBUSB_API_VERSION >= 0x01000107) /* 1.0.23 */ @@ -272,10 +318,18 @@ static const struct option long_options[] = { #endif { "reset", no_argument, NULL, 'R' }, { "version", no_argument, NULL, 'v' }, + { "json", no_argument, NULL, 'j' }, { "help", no_argument, NULL, 'h' }, + { 0, 0, NULL, 0 }, }; +/* Forward declarations */ +static int is_mass_storage_device(struct libusb_device *dev); +static const char* get_primary_device_class_name(struct libusb_device *dev, struct libusb_device_descriptor *desc); +static struct libusb_device* find_device_on_hub_port(struct hub_info *hub, int port); +static void format_hex_id(char *buffer, size_t buffer_size, uint16_t value); +static void format_usb_version(char *buffer, size_t buffer_size, uint16_t version); static int print_usage(void) { @@ -305,6 +359,7 @@ static int print_usage(void) #endif "--reset, -R - reset hub after each power-on action, causing all devices to reassociate.\n" "--wait, -w - wait before repeat power off [%d ms].\n" + "--json, -j - output in JSON format.\n" "--version, -v - print program version.\n" "--help, -h - print this text.\n" "\n" @@ -331,6 +386,28 @@ static char* rtrim(char* str) return str; } +static char* trim(char* str) +{ + /* Trim leading spaces by moving content to the beginning */ + char* start = str; + while (isspace(*start)) { + start++; + } + + /* Move content to beginning if there were leading spaces */ + if (start != str) { + memmove(str, start, strlen(start) + 1); + } + + /* Trim trailing spaces */ + int i; + for (i = strlen(str)-1; i>=0 && isspace(str[i]); i--) { + str[i] = 0; + } + + return str; +} + /* * Convert port list into bitmap. * Following port list specifications are equivalent: @@ -404,9 +481,9 @@ static int get_computer_model(char *model, int len) } model[bytes_read] = 0; } else { - // devicetree is not available, try parsing /proc/cpuinfo instead. - // most Raspberry Pi have /proc/cpuinfo about 1KB, so 4KB buffer should be plenty: - char buffer[4096] = {0}; // fill buffer with all zeros + /* devicetree is not available, try parsing /proc/cpuinfo instead. */ + /* most Raspberry Pi have /proc/cpuinfo about 1KB, so 4KB buffer should be plenty: */ + char buffer[4096] = {0}; /* fill buffer with all zeros */ fd = open("/proc/cpuinfo", O_RDONLY); if (fd < 0) { return -1; @@ -523,7 +600,7 @@ static int get_hub_info(struct libusb_device *dev, struct hub_info *info) for (k=0; k < info->pn_len; k++) { char s[8]; snprintf(s, sizeof(s), "%s%d", k==0 ? "-" : ".", info->port_numbers[k]); - strcat(info->location, s); + strncat(info->location, s, sizeof(info->location) - strlen(info->location) - 1); } /* Get container_id: */ @@ -784,12 +861,12 @@ static int get_device_description(struct libusb_device * dev, struct descriptor_ if (desc.iManufacturer) { rc = libusb_get_string_descriptor_ascii(devh, desc.iManufacturer, (unsigned char*)ds->vendor, sizeof(ds->vendor)); - rtrim(ds->vendor); + trim(ds->vendor); } if (rc >= 0 && desc.iProduct) { rc = libusb_get_string_descriptor_ascii(devh, desc.iProduct, (unsigned char*)ds->product, sizeof(ds->product)); - rtrim(ds->product); + trim(ds->product); } if (rc >= 0 && desc.iSerialNumber) { rc = libusb_get_string_descriptor_ascii(devh, @@ -816,6 +893,20 @@ static int get_device_description(struct libusb_device * dev, struct descriptor_ } libusb_close(devh); } + + /* Populate additional fields for JSON output */ + ds->vid = desc.idVendor; + ds->pid = desc.idProduct; + ds->device_class = desc.bDeviceClass; + ds->usb_version = desc.bcdUSB; + ds->device_version = desc.bcdDevice; + ds->is_mass_storage = (dev && is_mass_storage_device(dev)) ? 1 : 0; + + /* Get device class name */ + const char* class_name = get_primary_device_class_name(dev, &desc); + strncpy(ds->class_name, class_name, sizeof(ds->class_name) - 1); + ds->class_name[sizeof(ds->class_name) - 1] = '\0'; + snprintf(ds->description, sizeof(ds->description), "%04x:%04x%s%s%s%s%s%s%s", id_vendor, id_product, @@ -827,6 +918,40 @@ static int get_device_description(struct libusb_device * dev, struct descriptor_ return 0; } +/* Helper function to find a device connected to a specific hub port */ +static struct libusb_device* find_device_on_hub_port(struct hub_info *hub, int port) +{ + struct libusb_device *udev = NULL; + int i = 0; + + while ((udev = usb_devs[i++]) != NULL) { + uint8_t dev_bus = libusb_get_bus_number(udev); + if (dev_bus != hub->bus) continue; + + uint8_t dev_pn[MAX_HUB_CHAIN]; + int dev_plen = get_port_numbers(udev, dev_pn, sizeof(dev_pn)); + if ((dev_plen == hub->pn_len + 1) && + (memcmp(hub->port_numbers, dev_pn, hub->pn_len) == 0) && + libusb_get_port_number(udev) == port) + { + return udev; + } + } + return NULL; +} + +/* Helper function to format hex IDs consistently */ +static void format_hex_id(char *buffer, size_t buffer_size, uint16_t value) +{ + snprintf(buffer, buffer_size, "0x%04x", value); +} + +/* Helper function to format USB version consistently */ +static void format_usb_version(char *buffer, size_t buffer_size, uint16_t version) +{ + snprintf(buffer, buffer_size, "%x.%02x", version >> 8, version & 0xFF); +} + /* * show status for hub ports @@ -858,24 +983,9 @@ static int print_port_status(struct hub_info * hub, int portmask) struct descriptor_strings ds; memset(&ds, 0, sizeof(ds)); - struct libusb_device * udev; - int i = 0; - while ((udev = usb_devs[i++]) != NULL) { - uint8_t dev_bus; - uint8_t dev_pn[MAX_HUB_CHAIN]; - int dev_plen; - dev_bus = libusb_get_bus_number(udev); - /* only match devices on the same bus: */ - if (dev_bus != hub->bus) continue; - dev_plen = get_port_numbers(udev, dev_pn, sizeof(dev_pn)); - if ((dev_plen == hub->pn_len + 1) && - (memcmp(hub->port_numbers, dev_pn, hub->pn_len) == 0) && - libusb_get_port_number(udev) == port) - { - rc = get_device_description(udev, &ds); - if (rc == 0) - break; - } + struct libusb_device *udev = find_device_on_hub_port(hub, port); + if (udev) { + get_device_description(udev, &ds); } if (!hub->super_speed) { @@ -890,11 +1000,12 @@ static int print_port_status(struct hub_info * hub, int portmask) if (port_status & USB_PORT_STAT_SUSPEND) printf(" suspend"); } } else { - if (!(port_status & USB_SS_PORT_STAT_POWER)) { + int power_mask = hub->super_speed ? USB_SS_PORT_STAT_POWER : USB_PORT_STAT_POWER; + if (!(port_status & power_mask)) { printf(" off"); } else { int link_state = port_status & USB_PORT_STAT_LINK_STATE; - if (port_status & USB_SS_PORT_STAT_POWER) printf(" power"); + if (port_status & power_mask) printf(" power"); if ((port_status & USB_SS_PORT_STAT_SPEED) == USB_PORT_STAT_SPEED_5GBPS) { @@ -1145,6 +1256,619 @@ static int usb_find_hubs(void) return hub_phys_count; } +int is_mass_storage_device(struct libusb_device *dev) +{ + struct libusb_config_descriptor *config; + int rc = 0; + if (libusb_get_config_descriptor(dev, 0, &config) == 0) { + for (int i = 0; i < config->bNumInterfaces; i++) { + const struct libusb_interface *interface = &config->interface[i]; + for (int j = 0; j < interface->num_altsetting; j++) { + const struct libusb_interface_descriptor *altsetting = &interface->altsetting[j]; + if (altsetting->bInterfaceClass == LIBUSB_CLASS_MASS_STORAGE) { + rc = 1; + goto out; + } + } + } + out: + libusb_free_config_descriptor(config); + } + return rc; +} + + + + +/* Helper function to determine port speed */ +void get_port_speed(int port_status, char** speed_str, int64_t* speed_bps, int super_speed) +{ + *speed_str = "Disconnected"; + *speed_bps = 0; + + if (port_status & USB_PORT_STAT_CONNECTION) { + /* Check if this is a USB3 hub first */ + if (super_speed) { + int speed_mask = port_status & USB_PORT_STAT_SPEED_MASK; + switch (speed_mask) { + case USB_PORT_STAT_SPEED_5GBPS: + *speed_str = "USB3.0 SuperSpeed 5 Gbps"; + *speed_bps = 5000000000LL; + break; + case USB_PORT_STAT_SPEED_10GBPS: + *speed_str = "USB 3.1 Gen 2 SuperSpeed+ 10 Gbps"; + *speed_bps = 10000000000LL; + break; + case USB_PORT_STAT_SPEED_20GBPS: + *speed_str = "USB 3.2 Gen 2x2 SuperSpeed+ 20 Gbps"; + *speed_bps = 20000000000LL; + break; + case USB_PORT_STAT_SPEED_40GBPS: + *speed_str = "USB4 40 Gbps"; + *speed_bps = 40000000000LL; + break; + case USB_PORT_STAT_SPEED_80GBPS: + *speed_str = "USB4 80 Gbps"; + *speed_bps = 80000000000LL; + break; + default: + *speed_str = "USB1.1 Full Speed 12Mbps"; + *speed_bps = 12000000; /* 12 Mbit/s (default for USB 1.1) */ + } + } else { + /* USB2 port - check speed bits */ + if (port_status & USB_PORT_STAT_LOW_SPEED) { + *speed_str = "USB1.0 Low Speed 1.5 Mbps"; + *speed_bps = 1500000; /* 1.5 Mbit/s */ + } else if (port_status & USB_PORT_STAT_HIGH_SPEED) { + *speed_str = "USB2.0 High Speed 480Mbps"; + *speed_bps = 480000000; /* 480 Mbit/s */ + } else { + /* USB 2.0 Full Speed (neither low nor high speed) */ + *speed_str = "USB1.1 Full Speed 12Mbps"; + *speed_bps = 12000000; /* 12 Mbit/s */ + } + } + } +} + +/* Helper function to get class name */ +const char* get_class_name(uint8_t class_code) +{ + switch(class_code) { + case LIBUSB_CLASS_PER_INTERFACE: + return "Per Interface"; + case LIBUSB_CLASS_AUDIO: + return "Audio"; + case LIBUSB_CLASS_COMM: + return "Communications"; + case LIBUSB_CLASS_HID: + return "Human Interface Device"; + case LIBUSB_CLASS_PHYSICAL: + return "Physical"; + case LIBUSB_CLASS_PRINTER: + return "Printer"; + case LIBUSB_CLASS_IMAGE: + return "Image"; + case LIBUSB_CLASS_MASS_STORAGE: + return "Mass Storage"; + case LIBUSB_CLASS_HUB: + return "Hub"; + case LIBUSB_CLASS_DATA: + return "Data"; + case LIBUSB_CLASS_SMART_CARD: + return "Smart Card"; + case LIBUSB_CLASS_CONTENT_SECURITY: + return "Content Security"; + case LIBUSB_CLASS_VIDEO: + return "Video"; + case LIBUSB_CLASS_PERSONAL_HEALTHCARE: + return "Personal Healthcare"; + case LIBUSB_CLASS_DIAGNOSTIC_DEVICE: + return "Diagnostic Device"; + case LIBUSB_CLASS_WIRELESS: + return "Wireless"; + case LIBUSB_CLASS_APPLICATION: + return "Application"; + case LIBUSB_CLASS_VENDOR_SPEC: + return "Vendor Specific"; + default: + return "Unknown"; + } +} +const char* get_primary_device_class_name(struct libusb_device *dev, struct libusb_device_descriptor *desc) +{ + if (desc->bDeviceClass != LIBUSB_CLASS_PER_INTERFACE) { + return get_class_name(desc->bDeviceClass); + } + + struct libusb_config_descriptor *config; + if (libusb_get_config_descriptor(dev, 0, &config) != 0) { + return "Unknown"; + } + + const char* primary_class = "Composite Device"; + for (int i = 0; i < config->bNumInterfaces; i++) { + const struct libusb_interface *interface = &config->interface[i]; + for (int j = 0; j < interface->num_altsetting; j++) { + const struct libusb_interface_descriptor *altsetting = &interface->altsetting[j]; + const char* interface_class_name = get_class_name(altsetting->bInterfaceClass); + + /* Prioritized classes */ + switch (altsetting->bInterfaceClass) { + case LIBUSB_CLASS_HID: + libusb_free_config_descriptor(config); + return interface_class_name; /* Human Interface Device */ + case LIBUSB_CLASS_MASS_STORAGE: + primary_class = interface_class_name; /* Mass Storage */ + break; + case LIBUSB_CLASS_AUDIO: + case LIBUSB_CLASS_VIDEO: + libusb_free_config_descriptor(config); + return interface_class_name; /* Audio or Video */ + case LIBUSB_CLASS_PRINTER: + libusb_free_config_descriptor(config); + return interface_class_name; /* Printer */ + case LIBUSB_CLASS_COMM: + case LIBUSB_CLASS_DATA: + if (strcmp(primary_class, "Composite Device") == 0) { + primary_class = "Communications"; /* CDC devices often have both COMM and DATA interfaces */ + } + break; + case LIBUSB_CLASS_SMART_CARD: + libusb_free_config_descriptor(config); + return interface_class_name; /* Smart Card */ + case LIBUSB_CLASS_CONTENT_SECURITY: + libusb_free_config_descriptor(config); + return interface_class_name; /* Content Security */ + case LIBUSB_CLASS_WIRELESS: + if (strcmp(primary_class, "Composite Device") == 0) { + primary_class = interface_class_name; /* Wireless Controller */ + } + break; + case LIBUSB_CLASS_APPLICATION: + if (strcmp(primary_class, "Composite Device") == 0) { + primary_class = interface_class_name; /* Application Specific */ + } + break; + /* Add more cases here if needed */ + } + } + } + + libusb_free_config_descriptor(config); + return primary_class; +} + +/* Helper function to create the status flags JSON object (using mkjson) */ +/* Only outputs flags that are true to reduce JSON size */ +/* Returns: Allocated JSON string. Caller must free() the returned string. */ +char* create_status_flags_json(int port_status, int super_speed) +{ + struct { + int mask; + const char* name; + } flag_defs[] = { + {USB_PORT_STAT_CONNECTION, "connection"}, + {USB_PORT_STAT_ENABLE, "enable"}, + {USB_PORT_STAT_SUSPEND, "suspend"}, + {USB_PORT_STAT_OVERCURRENT, "overcurrent"}, + {USB_PORT_STAT_RESET, "reset"}, + {super_speed ? USB_SS_PORT_STAT_POWER : USB_PORT_STAT_POWER, "power"}, + {super_speed ? 0 : USB_PORT_STAT_LOW_SPEED, "lowspeed"}, + {super_speed ? 0 : USB_PORT_STAT_HIGH_SPEED, "highspeed"}, + {USB_PORT_STAT_TEST, "test"}, + {USB_PORT_STAT_INDICATOR, "indicator"}, + {0, NULL} + }; + + /* Calculate exact buffer size needed for flags JSON */ + size_t buffer_size = 3; /* "{}" + null terminator */ + int active_count = 0; + + for (int i = 0; flag_defs[i].name != NULL; i++) { + if ((flag_defs[i].mask != 0) && (port_status & flag_defs[i].mask)) { + if (active_count > 0) buffer_size += 2; /* ", " */ + buffer_size += 1 + strlen(flag_defs[i].name) + 7; /* "name": true */ + active_count++; + } + } + + char* result = malloc(buffer_size); + if (!result) return NULL; + + char* ptr = result; + int remaining = buffer_size; + + int written = snprintf(ptr, remaining, "{"); + ptr += written; + remaining -= written; + + int first = 1; + for (int i = 0; flag_defs[i].name != NULL; i++) { + if ((flag_defs[i].mask != 0) && (port_status & flag_defs[i].mask)) { + written = snprintf(ptr, remaining, "%s\"%s\": true", + first ? "" : ", ", flag_defs[i].name); + ptr += written; + remaining -= written; + first = 0; + } + } + + snprintf(ptr, remaining, "}"); + return result; +} + +/* Helper function to create human-readable descriptions of set flags */ +/* Returns: Allocated JSON string. Caller must free() the returned string. */ +char* create_human_readable_json(int port_status, int super_speed) +{ + struct { + int mask; + const char* name; + const char* description; + } flag_defs[] = { + {USB_PORT_STAT_CONNECTION, "connection", "Device is connected"}, + {USB_PORT_STAT_ENABLE, "enable", "Port is enabled"}, + {USB_PORT_STAT_SUSPEND, "suspend", "Port is suspended"}, + {USB_PORT_STAT_OVERCURRENT, "overcurrent", "Over-current condition exists"}, + {USB_PORT_STAT_RESET, "reset", "Port is in reset state"}, + {super_speed ? USB_SS_PORT_STAT_POWER : USB_PORT_STAT_POWER, "power", "Port power is enabled"}, + {super_speed ? 0 : USB_PORT_STAT_LOW_SPEED, "lowspeed", "Low-speed device attached"}, + {super_speed ? 0 : USB_PORT_STAT_HIGH_SPEED, "highspeed", "High-speed device attached"}, + {USB_PORT_STAT_TEST, "test", "Port is in test mode"}, + {USB_PORT_STAT_INDICATOR, "indicator", "Port indicator control"}, + {0, NULL, NULL} + }; + + /* Calculate exact buffer size needed for human_readable JSON */ + size_t buffer_size = 3; /* "{}" + null terminator */ + int active_count = 0; + + for (int i = 0; flag_defs[i].name != NULL; i++) { + if ((flag_defs[i].mask != 0) && (port_status & flag_defs[i].mask)) { + if (active_count > 0) buffer_size += 2; /* ", " */ + buffer_size += 1 + strlen(flag_defs[i].name) + 4; /* "name": " */ + buffer_size += strlen(flag_defs[i].description) + 1; /* description" */ + active_count++; + } + } + + char* result = malloc(buffer_size); + if (!result) return NULL; + + char* ptr = result; + int remaining = buffer_size; + + int written = snprintf(ptr, remaining, "{"); + ptr += written; + remaining -= written; + + int first = 1; + for (int i = 0; flag_defs[i].name != NULL; i++) { + if ((flag_defs[i].mask != 0) && (port_status & flag_defs[i].mask)) { + written = snprintf(ptr, remaining, "%s\"%s\": \"%s\"", + first ? "" : ", ", flag_defs[i].name, flag_defs[i].description); + ptr += written; + remaining -= written; + first = 0; + } + } + + snprintf(ptr, remaining, "}"); + return result; +} + + + +/* Helper function to create status bits JSON object */ +/* Returns: Allocated JSON string. Caller must free() the returned string. */ +char* create_status_bits_json(int port_status, int super_speed) +{ + int power_mask = super_speed ? USB_SS_PORT_STAT_POWER : USB_PORT_STAT_POWER; + + mkjson_arg bits_args[] = { + { MKJSON_BOOL, "connection", .value.bool_val = (port_status & USB_PORT_STAT_CONNECTION) != 0 }, + { MKJSON_BOOL, "enabled", .value.bool_val = (port_status & USB_PORT_STAT_ENABLE) != 0 }, + { MKJSON_BOOL, "powered", .value.bool_val = (port_status & power_mask) != 0 }, + { MKJSON_BOOL, "suspended", .value.bool_val = (port_status & USB_PORT_STAT_SUSPEND) != 0 }, + { MKJSON_BOOL, "overcurrent", .value.bool_val = (port_status & USB_PORT_STAT_OVERCURRENT) != 0 }, + { MKJSON_BOOL, "reset", .value.bool_val = (port_status & USB_PORT_STAT_RESET) != 0 }, + { MKJSON_BOOL, "highspeed", .value.bool_val = !super_speed && (port_status & USB_PORT_STAT_HIGH_SPEED) != 0 }, + { MKJSON_BOOL, "lowspeed", .value.bool_val = !super_speed && (port_status & USB_PORT_STAT_LOW_SPEED) != 0 }, + { 0 } + }; + + return mkjson_array_pretty(MKJSON_OBJ, bits_args, 4); +} + +/* Helper function to decode port status into human-readable string */ +const char* decode_port_status(int port_status, int super_speed) +{ + if (port_status == 0x0000) return "no_power"; + + int power_mask = super_speed ? USB_SS_PORT_STAT_POWER : USB_PORT_STAT_POWER; + int has_power = (port_status & power_mask) != 0; + int has_connection = (port_status & USB_PORT_STAT_CONNECTION) != 0; + int is_enabled = (port_status & USB_PORT_STAT_ENABLE) != 0; + int is_suspended = (port_status & USB_PORT_STAT_SUSPEND) != 0; + int has_overcurrent = (port_status & USB_PORT_STAT_OVERCURRENT) != 0; + int in_reset = (port_status & USB_PORT_STAT_RESET) != 0; + + if (has_overcurrent) return "overcurrent"; + if (in_reset) return "resetting"; + if (!has_power) return "no_power"; + if (!has_connection) return "powered_no_device"; + if (!is_enabled) return "device_connected_not_enabled"; + if (is_suspended) return "device_suspended"; + return "device_active"; +} + +/* Create complete port status JSON string using mkjson */ +/* Returns: Allocated JSON string. Caller must free() the returned string. */ +char* create_port_status_json(int port, int port_status, const struct descriptor_strings* ds, struct libusb_device *dev, int super_speed) +{ + char status_hex[7]; + format_hex_id(status_hex, sizeof(status_hex), port_status); + + char* speed_str; + int64_t speed_bps; + get_port_speed(port_status, &speed_str, &speed_bps, super_speed); + + /* Create status object */ + const char* status_decoded = decode_port_status(port_status, super_speed); + char* status_bits_json = create_status_bits_json(port_status, super_speed); + + mkjson_arg status_args[] = { + { MKJSON_STRING, "raw", .value.str_val = status_hex }, + { MKJSON_STRING, "decoded", .value.str_val = status_decoded }, + { MKJSON_JSON_FREE, "bits", .value.str_free_val = status_bits_json }, + { 0 } + }; + char* status_json = mkjson_array_pretty(MKJSON_OBJ, status_args, 4); + + /* Get sub-objects */ + char *flags_json = create_status_flags_json(port_status, super_speed); + char *hr_json = create_human_readable_json(port_status, super_speed); + + /* For USB3 hubs, get link state and port speed */ + const char* link_state_str = NULL; + const char* port_speed = NULL; + if (super_speed) { + /* Check if this is a 5Gbps capable port */ + if ((port_status & USB_SS_PORT_STAT_SPEED) == USB_PORT_STAT_SPEED_5GBPS) { + port_speed = "5gbps"; + } + + int link_state = port_status & USB_PORT_STAT_LINK_STATE; + switch (link_state) { + case USB_SS_PORT_LS_U0: link_state_str = "U0"; break; + case USB_SS_PORT_LS_U1: link_state_str = "U1"; break; + case USB_SS_PORT_LS_U2: link_state_str = "U2"; break; + case USB_SS_PORT_LS_U3: link_state_str = "U3"; break; + case USB_SS_PORT_LS_SS_DISABLED: link_state_str = "SS.Disabled"; break; + case USB_SS_PORT_LS_RX_DETECT: link_state_str = "Rx.Detect"; break; + case USB_SS_PORT_LS_SS_INACTIVE: link_state_str = "SS.Inactive"; break; + case USB_SS_PORT_LS_POLLING: link_state_str = "Polling"; break; + case USB_SS_PORT_LS_RECOVERY: link_state_str = "Recovery"; break; + case USB_SS_PORT_LS_HOT_RESET: link_state_str = "HotReset"; break; + case USB_SS_PORT_LS_COMP_MOD: link_state_str = "Compliance"; break; + case USB_SS_PORT_LS_LOOPBACK: link_state_str = "Loopback"; break; + } + } + + /* Basic port info without device */ + if (!(port_status & USB_PORT_STAT_CONNECTION) || !dev) { + mkjson_arg basic_args[10]; /* Max possible args */ + int arg_idx = 0; + + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_INT, "port", .value.int_val = port }; + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_JSON_FREE, "status", .value.str_free_val = status_json }; + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_JSON_FREE, "flags", .value.str_free_val = flags_json }; + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_JSON_FREE, "human_readable", .value.str_free_val = hr_json }; + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "speed", .value.str_val = speed_str }; + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_LLINT, "speed_bps", .value.llint_val = speed_bps }; + + if (port_speed) { + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "port_speed", .value.str_val = port_speed }; + } + if (link_state_str) { + basic_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "link_state", .value.str_val = link_state_str }; + } + basic_args[arg_idx] = (mkjson_arg){ 0 }; /* Null terminator */ + + char *result = mkjson_array_pretty(MKJSON_OBJ, basic_args, 2); + + return result; + } + + /* Port with device - add device info */ + /* Note: device descriptor info is already available in ds from get_device_description */ + + /* Use device info from descriptor_strings (already populated by get_device_description) */ + char vendor_id[8], product_id[8]; + format_hex_id(vendor_id, sizeof(vendor_id), ds->vid); + format_hex_id(product_id, sizeof(product_id), ds->pid); + + /* Build USB and device versions */ + char usb_version[8], device_version[8]; + format_usb_version(usb_version, sizeof(usb_version), ds->usb_version); + format_usb_version(device_version, sizeof(device_version), ds->device_version); + + mkjson_arg device_args[25]; /* Max possible args */ + int arg_idx = 0; + + /* Basic port info */ + device_args[arg_idx++] = (mkjson_arg){ MKJSON_INT, "port", .value.int_val = port }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_JSON_FREE, "status", .value.str_free_val = status_json }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_JSON_FREE, "flags", .value.str_free_val = flags_json }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_JSON_FREE, "human_readable", .value.str_free_val = hr_json }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "speed", .value.str_val = speed_str }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_LLINT, "speed_bps", .value.llint_val = speed_bps }; + + /* Optional port info */ + if (port_speed) { + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "port_speed", .value.str_val = port_speed }; + } + if (link_state_str) { + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "link_state", .value.str_val = link_state_str }; + } + + /* Device identifiers */ + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "vid", .value.str_val = vendor_id }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "pid", .value.str_val = product_id }; + + /* Optional vendor/product strings */ + if (ds->vendor[0]) { + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "vendor", .value.str_val = ds->vendor }; + } + if (ds->product[0]) { + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "product", .value.str_val = ds->product }; + } + + /* Device class info */ + device_args[arg_idx++] = (mkjson_arg){ MKJSON_INT, "device_class", .value.int_val = ds->device_class }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "class_name", .value.str_val = ds->class_name }; + + /* Version info */ + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "usb_version", .value.str_val = usb_version }; + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "device_version", .value.str_val = device_version }; + + /* Optional serial */ + if (ds->serial[0]) { + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "serial", .value.str_val = ds->serial }; + } + + /* Optional mass storage flag */ + if (ds->is_mass_storage) { + device_args[arg_idx++] = (mkjson_arg){ MKJSON_BOOL, "is_mass_storage", .value.bool_val = ds->is_mass_storage }; + } + + device_args[arg_idx++] = (mkjson_arg){ MKJSON_STRING, "description", .value.str_val = ds->description[0] ? ds->description : NULL }; + + device_args[arg_idx] = (mkjson_arg){ 0 }; /* Null terminator */ + + char *result = mkjson_array_pretty(MKJSON_OBJ, device_args, 2); + + return result; +} + +/* Create JSON representation of a hub and its ports */ +/* Returns: Allocated JSON string. Caller must free() the returned string. */ +char* create_hub_json(struct hub_info* hub, int portmask) +{ + unsigned int vendor_id, product_id; + sscanf(hub->vendor, "%x:%x", &vendor_id, &product_id); + + char vendor_id_hex[8], product_id_hex[8]; + format_hex_id(vendor_id_hex, sizeof(vendor_id_hex), vendor_id); + format_hex_id(product_id_hex, sizeof(product_id_hex), product_id); + + char usb_version[16]; + format_usb_version(usb_version, sizeof(usb_version), hub->bcd_usb); + + const char* power_switching_mode; + switch (hub->lpsm) { + case HUB_CHAR_INDV_PORT_LPSM: + power_switching_mode = "ppps"; + break; + case HUB_CHAR_COMMON_LPSM: + power_switching_mode = "ganged"; + break; + default: + power_switching_mode = "nops"; + } + + /* Create hub_info object */ + mkjson_arg hub_info_args[] = { + { MKJSON_STRING, "vid", .value.str_val = vendor_id_hex }, + { MKJSON_STRING, "pid", .value.str_val = product_id_hex }, + { MKJSON_STRING, "usb_version", .value.str_val = usb_version }, + { MKJSON_INT, "nports", .value.int_val = hub->nports }, + { MKJSON_STRING, "ppps", .value.str_val = power_switching_mode }, + { 0 } + }; + char *hub_info_json = mkjson_array_pretty(MKJSON_OBJ, hub_info_args, 2); + + /* Create ports array */ + char *ports_array = NULL; + char *port_jsons[MAX_HUB_PORTS]; + int valid_ports = 0; + + struct libusb_device_handle* devh = NULL; + int rc = libusb_open(hub->dev, &devh); + if (rc == 0) { + for (int port = 1; port <= hub->nports; port++) { + if (portmask > 0 && (portmask & (1 << (port-1))) == 0) continue; + + int port_status = get_port_status(devh, port); + if (port_status == -1) continue; + + struct descriptor_strings ds; + bzero(&ds, sizeof(ds)); + struct libusb_device* udev = find_device_on_hub_port(hub, port); + if (udev) { + get_device_description(udev, &ds); + } + + port_jsons[valid_ports] = create_port_status_json(port, port_status, &ds, udev, hub->super_speed); + valid_ports++; + } + libusb_close(devh); + } + + /* Build the ports array manually */ + if (valid_ports == 0) { + mkjson_arg empty_args[] = { { 0 } }; + ports_array = mkjson_array_pretty(MKJSON_ARR, empty_args, 2); + } else { + /* Calculate total size needed */ + int total_size = 3; /* "[]" + null terminator */ + for (int i = 0; i < valid_ports; i++) { + total_size += strlen(port_jsons[i]); + if (i > 0) total_size += 2; /* ", " */ + } + + ports_array = malloc(total_size); + if (!ports_array) { + for (int i = 0; i < valid_ports; i++) { + free(port_jsons[i]); + } + return NULL; + } + + char* ptr = ports_array; + int remaining = total_size; + int written = snprintf(ptr, remaining, "["); + ptr += written; + remaining -= written; + for (int i = 0; i < valid_ports; i++) { + written = snprintf(ptr, remaining, "%s%s", i > 0 ? ", " : "", port_jsons[i]); + ptr += written; + remaining -= written; + free(port_jsons[i]); + } + snprintf(ptr, remaining, "]"); + } + + /* Create the final hub object */ + mkjson_arg hub_args[] = { + { MKJSON_STRING, "location", .value.str_val = hub->location }, + { MKJSON_STRING, "description", .value.str_val = hub->ds.description }, + { MKJSON_JSON, "hub_info", .value.str_val = hub_info_json }, + { MKJSON_JSON, "ports", .value.str_val = ports_array }, + { 0 } + }; + char *hub_json = mkjson_array_pretty(MKJSON_OBJ, hub_args, 2); + + free(hub_info_json); + free(ports_array); + + return hub_json; +} + + + int main(int argc, char *argv[]) { @@ -1156,6 +1880,9 @@ int main(int argc, char *argv[]) libusb_device_handle *sys_devh = NULL; #endif + /* Initialize opt_action to POWER_KEEP */ + opt_action = POWER_KEEP; + for (;;) { c = getopt_long(argc, argv, short_options, long_options, &option_index); if (c == -1) @@ -1245,15 +1972,18 @@ int main(int argc, char *argv[]) printf("%s\n", PROGRAM_VERSION); exit(0); break; - case 'h': - print_usage(); - exit(1); - break; case '?': /* getopt_long has already printed an error message here */ fprintf(stderr, "Run with -h to get usage info.\n"); exit(1); break; + case 'j': + opt_json = 1; + break; + case 'h': + print_usage(); + exit(1); + break; default: abort(); } @@ -1267,9 +1997,7 @@ int main(int argc, char *argv[]) rc = libusb_init(NULL); if (rc < 0) { - fprintf(stderr, - "Error initializing USB!\n" - ); + fprintf(stderr, "Error initializing USB!\n"); exit(1); } @@ -1303,9 +2031,7 @@ int main(int argc, char *argv[]) rc = libusb_get_device_list(NULL, &usb_devs); #endif if (rc < 0) { - fprintf(stderr, - "Cannot enumerate USB devices!\n" - ); + fprintf(stderr, "Cannot enumerate USB devices!\n"); rc = 1; goto cleanup; } @@ -1325,86 +2051,225 @@ int main(int argc, char *argv[]) goto cleanup; } - if (hub_phys_count > 1 && opt_action >= 0) { + if (hub_phys_count > 1 && opt_action != POWER_KEEP) { fprintf(stderr, "Error: changing port state for multiple hubs at once is not supported.\n" "Use -l to limit operation to one hub!\n" ); exit(1); } - int k; /* k=0 for power OFF, k=1 for power ON */ - for (k=0; k<2; k++) { /* up to 2 power actions - off/on */ - if (k == 0 && opt_action == POWER_ON ) - continue; - if (k == 1 && opt_action == POWER_OFF) - continue; - if (k == 1 && opt_action == POWER_KEEP) - continue; - /* if toggle requested, do it only once when `k == 0` */ - if (k == 1 && opt_action == POWER_TOGGLE) - continue; - int i; - for (i=0; i= 0) { + /* Output JSON event for power state change */ + mkjson_arg event_args[] = { + { MKJSON_STRING, "event", .value.str_val = "power_change" }, + { MKJSON_STRING, "hub", .value.str_val = hubs[i].location }, + { MKJSON_INT, "port", .value.int_val = port }, + { MKJSON_STRING, "action", .value.str_val = should_be_on ? "on" : "off" }, + { MKJSON_BOOL, "from_state", .value.bool_val = is_on }, + { MKJSON_BOOL, "to_state", .value.bool_val = should_be_on }, + { MKJSON_BOOL, "success", .value.bool_val = rc >= 0 }, + { 0 } + }; + char *event_json = mkjson_array_pretty(MKJSON_OBJ, event_args, 2); + printf("%s\n", event_json); + free(event_json); + } + } } + } + /* USB3 hubs need extra delay to actually turn off: */ + if (k==0 && hubs[i].super_speed) + sleep_ms(150); + + if (!opt_json) { + printf("Sent power %s request\n", should_be_on ? "on" : "off"); + printf("New status for hub %s [%s]\n", + hubs[i].location, hubs[i].ds.description); + print_port_status(&hubs[i], opt_ports); + } - if (is_on != should_be_on) { - rc = set_port_status(devh, &hubs[i], port, should_be_on); + if (k == 1 && opt_reset == 1) { + if (!opt_json) { + printf("Resetting hub...\n"); + } + rc = libusb_reset_device(devh); + if (!opt_json) { + if (rc < 0) { + perror("Reset failed!\n"); + } else { + printf("Reset successful!\n"); + } + } else { + /* Output JSON event for hub reset */ + mkjson_arg reset_args[] = { + { MKJSON_STRING, "event", .value.str_val = "hub_reset" }, + { MKJSON_STRING, "hub", .value.str_val = hubs[i].location }, + { MKJSON_BOOL, "success", .value.bool_val = rc >= 0 }, + { MKJSON_STRING, "status", .value.str_val = rc < 0 ? "failed" : "successful" }, + { 0 } + }; + char *reset_json = mkjson_array_pretty(MKJSON_OBJ, reset_args, 2); + printf("%s\n", reset_json); + free(reset_json); } } } - /* USB3 hubs need extra delay to actually turn off: */ - if (k==0 && hubs[i].super_speed) - sleep_ms(150); - printf("Sent power %s request\n", should_be_on ? "on" : "off"); - printf("New status for hub %s [%s]\n", - hubs[i].location, hubs[i].ds.description - ); - print_port_status(&hubs[i], opt_ports); + libusb_close(devh); - if (k == 1 && opt_reset == 1) { - printf("Resetting hub...\n"); - rc = libusb_reset_device(devh); - if (rc < 0) { - perror("Reset failed!\n"); - } else { - printf("Reset successful!\n"); - } + if (opt_json && hub_json_str) { + hub_jsons[hub_json_count++] = hub_json_str; + } + } + /* Handle delay between power off and power on for cycle/flash */ + if (k == 0 && (opt_action == POWER_CYCLE || opt_action == POWER_FLASH)) { + if (opt_json) { + /* Output JSON event for delay */ + mkjson_arg delay_args[] = { + { MKJSON_STRING, "event", .value.str_val = "delay" }, + { MKJSON_STRING, "reason", .value.str_val = opt_action == POWER_CYCLE ? "power_cycle" : "power_flash" }, + { MKJSON_DOUBLE, "duration_seconds", .value.dbl_val = opt_delay }, + { 0 } + }; + char *delay_json = mkjson_array_pretty(MKJSON_OBJ, delay_args, 2); + printf("%s\n", delay_json); + free(delay_json); } + sleep_ms((int)(opt_delay * 1000)); } - libusb_close(devh); } - if (k == 0 && (opt_action == POWER_CYCLE || opt_action == POWER_FLASH)) - sleep_ms((int)(opt_delay * 1000)); } + + if (opt_json && opt_action == POWER_KEEP) { + /* Only output hub status array when no power action is performed */ + /* For power actions, we output events in real-time instead */ + char *hubs_array; + if (hub_json_count == 0) { + mkjson_arg empty_args[] = { { 0 } }; + hubs_array = mkjson_array_pretty(MKJSON_ARR, empty_args, 2); + } else { + /* Calculate total size needed */ + int total_size = 3; /* "[]" + null terminator */ + for (int i = 0; i < hub_json_count; i++) { + total_size += strlen(hub_jsons[i]); + if (i > 0) total_size += 2; /* ", " */ + } + + hubs_array = malloc(total_size); + if (!hubs_array) { + for (int i = 0; i < hub_json_count; i++) { + free(hub_jsons[i]); + } + return rc; + } + + char* ptr = hubs_array; + int remaining = total_size; + + int written = snprintf(ptr, remaining, "["); + ptr += written; + remaining -= written; + + for (int i = 0; i < hub_json_count; i++) { + written = snprintf(ptr, remaining, "%s%s", i > 0 ? ", " : "", hub_jsons[i]); + ptr += written; + remaining -= written; + free(hub_jsons[i]); + } + + snprintf(ptr, remaining, "]"); + } + + /* Create the final JSON object */ + mkjson_arg final_args[] = { + { MKJSON_JSON, "hubs", .value.str_val = hubs_array }, + { 0 } + }; + char *json_str = mkjson_array_pretty(MKJSON_OBJ, final_args, 2); + + printf("%s\n", json_str); + free(json_str); + free(hubs_array); + } + rc = 0; cleanup: #if defined(__linux__) && (LIBUSB_API_VERSION >= 0x01000107)