mirror of
https://github.com/RPCS3/libusb.git
synced 2026-07-22 00:55:23 -04:00
487 lines
16 KiB
C
487 lines
16 KiB
C
/*
|
|
* xusb: libusb-winusb specific test program, (c) 2009 Pete Batard
|
|
* based on lsusb, copyright (C) 2007 Daniel Drake <dsd@gentoo.org>
|
|
*
|
|
* This test program tries to access an USB device through WinUSB.
|
|
* To access your device, modify this source and add your VID/PID.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#ifdef _MSC_VER
|
|
#include <config_msvc.h>
|
|
#else
|
|
#include <config.h>
|
|
#endif
|
|
#include <stdio.h>
|
|
#include <sys/types.h>
|
|
#include <inttypes.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
|
|
#include <libusb/libusb.h>
|
|
|
|
#ifdef OS_WINDOWS
|
|
#include <windows.h>
|
|
#define msleep(msecs) Sleep(msecs)
|
|
#else
|
|
#include <unistd.h>
|
|
#define msleep(msecs) usleep(1000*msecs)
|
|
#endif
|
|
|
|
static int inline perr(char const *format, ...)
|
|
{
|
|
va_list args;
|
|
int r;
|
|
|
|
va_start (args, format);
|
|
r = vfprintf(stderr, format, args);
|
|
va_end(args);
|
|
|
|
return r;
|
|
}
|
|
|
|
#define ERR_EXIT(errcode) do { perr(" libusb error: %d\n", errcode); return -1; } while (0)
|
|
#define CALL_CHECK(fcall) do { r=fcall; if (r < 0) ERR_EXIT(r); } while (0);
|
|
#define B(x) (((x)!=0)?1:0)
|
|
#define be_to_int32(buf) (((buf)[0]<<24)|((buf)[1]<<16)|((buf)[2]<<8)|(buf)[3])
|
|
|
|
// HID Class-Specific Requests values. See section 7.2 of the HID specifications
|
|
#define HID_GET_REPORT 0x01
|
|
#define HID_GET_IDLE 0x02
|
|
#define HID_GET_PROTOCOL 0x03
|
|
#define HID_SET_REPORT 0x09
|
|
#define HID_SET_IDLE 0x0A
|
|
#define HID_SET_PROTOCOL 0x0B
|
|
#define HID_REPORT_TYPE_INPUT 0x01
|
|
#define HID_REPORT_TYPE_OUTPUT 0x02
|
|
#define HID_REPORT_TYPE_FEATURE 0x03
|
|
|
|
// Mass Storage Requests values. See section 3 of the Bulk-Only Mass Storage Class specifications
|
|
#define BOMS_RESET 0xFF
|
|
#define BOMS_GET_MAX_LUN 0xFE
|
|
|
|
// Section 5.1: Command Block Wrapper (CBW)
|
|
struct command_block_wrapper {
|
|
uint8_t dCBWSignature[4];
|
|
uint32_t dCBWTag;
|
|
uint32_t dCBWDataTransferLength;
|
|
uint8_t bmCBWFlags;
|
|
uint8_t bCBWLUN;
|
|
uint8_t bCBWCBLength;
|
|
uint8_t CBWCB[16];
|
|
};
|
|
|
|
// Section 5.2: Command Status Wrapper (CSW)
|
|
struct command_status_wrapper {
|
|
uint8_t dCSWSignature[4];
|
|
uint32_t dCSWTag;
|
|
uint32_t dCSWDataResidue;
|
|
uint8_t bCSWStatus;
|
|
};
|
|
|
|
static uint8_t cdb_length[256] = {
|
|
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
|
06,06,06,06,06,06,06,06,06,06,06,06,06,06,06,06, // 0
|
|
06,06,06,06,06,06,06,06,06,06,06,06,06,06,06,06, // 1
|
|
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, // 2
|
|
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, // 3
|
|
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, // 4
|
|
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10, // 5
|
|
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, // 6
|
|
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, // 7
|
|
16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16, // 8
|
|
16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16, // 9
|
|
12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, // A
|
|
12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12, // B
|
|
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, // C
|
|
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, // D
|
|
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, // E
|
|
00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, // F
|
|
};
|
|
|
|
enum test_type {
|
|
USE_XBOX,
|
|
USE_KEY,
|
|
USE_JTAG,
|
|
} test_mode;
|
|
uint16_t VID, PID;
|
|
|
|
// The XBOX Controller is really a HID device that got its HID Report Descriptors
|
|
// removed by Microsoft.
|
|
// Input/Output reports described at http://euc.jp/periphs/xbox-controller.ja.html
|
|
int display_xbox_status(libusb_device_handle *handle)
|
|
{
|
|
int r;
|
|
uint8_t input_report[20];
|
|
printf("Retrieving XBox Input Report...\n");
|
|
CALL_CHECK(libusb_control_transfer(handle, LIBUSB_ENDPOINT_IN|LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE,
|
|
HID_GET_REPORT, (HID_REPORT_TYPE_INPUT<<8)|0x00, 0, input_report, 20, 1000));
|
|
printf("D-pad: %02X\n", input_report[2]&0x0F);
|
|
printf("Start:%d, Back:%d, Left Stick Press:%d, Right Stick Press:%d\n", B(input_report[2]&0x10), B(input_report[2]&0x20),
|
|
B(input_report[2]&0x40), B(input_report[2]&0x80));
|
|
// A, B, X, Y, Black, White are pressure sensitive
|
|
printf("A:%d, B:%d, X:%d, Y:%d, White:%d, Black:%d\n", input_report[4], input_report[5],
|
|
input_report[6], input_report[7], input_report[9], input_report[8]);
|
|
printf("Left Trigger: %d, Right Trigger: %d\n", input_report[10], input_report[11]);
|
|
printf("Left Analog (X,Y): (%d,%d)\n", (int16_t)((input_report[13]<<8)|input_report[12]),
|
|
(int16_t)((input_report[15]<<8)|input_report[14]));
|
|
printf("Right Analog (X,Y): (%d,%d)\n", (int16_t)((input_report[17]<<8)|input_report[16]),
|
|
(int16_t)((input_report[19]<<8)|input_report[18]));
|
|
return 0;
|
|
}
|
|
|
|
int set_xbox_actuators(libusb_device_handle *handle, uint8_t left, uint8_t right)
|
|
{
|
|
int r;
|
|
uint8_t output_report[6];
|
|
|
|
printf("Writing XBox Controller Output Report...\n");
|
|
|
|
memset(output_report, 0, sizeof(output_report));
|
|
output_report[1] = sizeof(output_report);
|
|
output_report[3] = left;
|
|
output_report[5] = right;
|
|
|
|
CALL_CHECK(libusb_control_transfer(handle, LIBUSB_ENDPOINT_OUT|LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE,
|
|
HID_SET_REPORT, (HID_REPORT_TYPE_OUTPUT<<8)|0x00, 0, output_report,06, 1000));
|
|
return 0;
|
|
}
|
|
|
|
int send_mass_storage_command(libusb_device_handle *handle, uint8_t endpoint, uint8_t lun,
|
|
uint8_t *cdb, uint8_t direction, int data_length, uint32_t *ret_tag)
|
|
{
|
|
static uint32_t tag = 1;
|
|
uint8_t cdb_len;
|
|
int r, size;
|
|
struct command_block_wrapper cbw;
|
|
|
|
if (cdb == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
if (endpoint & LIBUSB_ENDPOINT_IN) {
|
|
perr("send_mass_storage_command: cannot send command on IN endpoint\n");
|
|
return -1;
|
|
}
|
|
|
|
cdb_len = cdb_length[cdb[0]];
|
|
if ((cdb_len == 0) || (cdb_len > sizeof(cbw.CBWCB))) {
|
|
perr("send_mass_storage_command: don't know how to handle this command (%02X, length %d)\n",
|
|
cdb[0], cdb_len);
|
|
return -1;
|
|
}
|
|
|
|
memset(&cbw, 0, sizeof(cbw));
|
|
cbw.dCBWSignature[0] = 'U';
|
|
cbw.dCBWSignature[1] = 'S';
|
|
cbw.dCBWSignature[2] = 'B';
|
|
cbw.dCBWSignature[3] = 'C';
|
|
*ret_tag = tag;
|
|
cbw.dCBWTag = tag++;
|
|
cbw.dCBWDataTransferLength = data_length;
|
|
cbw.bmCBWFlags = direction;
|
|
cbw.bCBWLUN = lun;
|
|
cbw.bCBWCBLength = cdb_len;
|
|
memcpy(cbw.CBWCB, cdb, cdb_len);
|
|
|
|
CALL_CHECK(libusb_bulk_transfer(handle, endpoint, (unsigned char*)&cbw, 15+cdb_len, &size, 1000));
|
|
printf("sent %d bytes (confirmed %d)\n", 15+cdb_len, size);
|
|
return 0;
|
|
}
|
|
|
|
int get_mass_storage_status(libusb_device_handle *handle, uint8_t endpoint, uint32_t expected_tag)
|
|
{
|
|
int r, size;
|
|
struct command_status_wrapper csw;
|
|
|
|
CALL_CHECK(libusb_bulk_transfer(handle, endpoint, (unsigned char*)&csw, 13, &size, 1000));
|
|
if (size != 13) {
|
|
perr("get_mass_storage_status: received %d bytes (expected 13)\n", size);
|
|
return -1;
|
|
}
|
|
if (csw.dCSWTag != expected_tag) {
|
|
perr("get_mass_storage_status: mismatched tags (expected %08X, received %08X)\n",
|
|
expected_tag, csw.dCSWTag);
|
|
return -1;
|
|
}
|
|
printf("Mass Storage Status: %02X (%s)\n", csw.bCSWStatus, csw.bCSWStatus?"FAILED":"Success");
|
|
if (csw.dCSWTag != expected_tag)
|
|
return -1;
|
|
if (csw.bCSWStatus)
|
|
return -2; // request Get Sense
|
|
|
|
return 0;
|
|
}
|
|
|
|
void get_sense(libusb_device_handle *handle, uint8_t endpoint_in, uint8_t endpoint_out)
|
|
{
|
|
uint8_t cdb[16]; // SCSI Command Descriptor Block
|
|
uint8_t sense[18];
|
|
uint32_t expected_tag;
|
|
int size;
|
|
|
|
// Request Sense
|
|
printf("Request Sense...\n");
|
|
memset(sense, 0, sizeof(sense));
|
|
memset(cdb, 0, sizeof(cdb));
|
|
cdb[0] = 0x03; // Request Sense
|
|
cdb[4] = 0x12;
|
|
|
|
send_mass_storage_command(handle, endpoint_out, 0, cdb, LIBUSB_ENDPOINT_IN, 0x12, &expected_tag);
|
|
libusb_bulk_transfer(handle, endpoint_in, (unsigned char*)&sense, 0x12, &size, 1000);
|
|
printf("received %d bytes\n", size);
|
|
|
|
if ((sense[0] != 0x70) && (sense[0] != 0x71)) {
|
|
perr("ERROR No sense data\n");
|
|
} else {
|
|
perr("ERROR Sense: %02X %02X %02X\n", sense[2]&0x0F, sense[12], sense[13]);
|
|
}
|
|
get_mass_storage_status(handle, endpoint_in, expected_tag);
|
|
}
|
|
|
|
// Mass Storage device to test bulk transfers (non destructive test)
|
|
int test_mass_storage(libusb_device_handle *handle)
|
|
{
|
|
int r, i, size;
|
|
uint8_t lun;
|
|
uint32_t expected_tag;
|
|
uint32_t max_lba, block_size;
|
|
double device_size;
|
|
uint8_t cdb[16]; // SCSI Command Descriptor Block
|
|
uint8_t buffer[512];
|
|
if (buffer == NULL) {
|
|
perr("failed to allocate mass storage test buffer\n");
|
|
return -1;
|
|
}
|
|
|
|
printf("Sending Mass Storage Reset...\n");
|
|
CALL_CHECK(libusb_control_transfer(handle, LIBUSB_ENDPOINT_OUT|LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE,
|
|
BOMS_RESET, 0, 0, NULL, 0, 1000));
|
|
printf("Getting Max LUN...\n");
|
|
CALL_CHECK(libusb_control_transfer(handle, LIBUSB_ENDPOINT_IN|LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE,
|
|
BOMS_GET_MAX_LUN, 0, 0, &lun, 1, 1000));
|
|
printf(" Max LUN = %d\n", lun);
|
|
|
|
// Send Inquiry
|
|
printf("Sending Inquiry...\n");
|
|
memset(buffer, 0, sizeof(buffer));
|
|
memset(cdb, 0, sizeof(cdb));
|
|
cdb[0] = 0x12; // Inquiry
|
|
cdb[4] = 0x60; // Inquiry data size
|
|
|
|
send_mass_storage_command(handle, 0x01, 0, cdb, LIBUSB_ENDPOINT_IN, 0x60, &expected_tag);
|
|
CALL_CHECK(libusb_bulk_transfer(handle, 0x81, (unsigned char*)&buffer, 0x60, &size, 1000));
|
|
printf("received %d bytes\n", size);
|
|
printf("VID:PID:REV:SPE %s:%s:%s:%s\n", &buffer[8], &buffer[16], &buffer[32], &buffer[38]);
|
|
if (get_mass_storage_status(handle, 0x81, expected_tag) == -2) {
|
|
get_sense(handle, 0x81, 0x01);
|
|
}
|
|
|
|
|
|
// Read capacity
|
|
printf("Reading Capacity...\n");
|
|
memset(buffer, 0, sizeof(buffer));
|
|
memset(cdb, 0, sizeof(cdb));
|
|
cdb[0] = 0x25; // Read Capacity
|
|
|
|
send_mass_storage_command(handle, 0x01, 0, cdb, LIBUSB_ENDPOINT_IN, 0x08, &expected_tag);
|
|
CALL_CHECK(libusb_bulk_transfer(handle, 0x81, (unsigned char*)&buffer, 0x08, &size, 1000));
|
|
printf("received %d bytes\n", size);
|
|
max_lba = be_to_int32(&buffer[0]);
|
|
block_size = be_to_int32(&buffer[4]);
|
|
device_size = ((double)(max_lba+1))*block_size/(1024*1024*1024);
|
|
printf("Max LBA: %08X, Block Size: %08X (%.2f GB)\n", max_lba, block_size, device_size);
|
|
if (get_mass_storage_status(handle, 0x81, expected_tag) == -2) {
|
|
get_sense(handle, 0x81, 0x01);
|
|
}
|
|
|
|
// Send Read
|
|
printf("Try to read 512 bytes...\n");
|
|
memset(buffer, 0, sizeof(buffer));
|
|
memset(cdb, 0, sizeof(cdb));
|
|
|
|
// cdb[0] = 0x28; // Read(10)
|
|
// cdb[7] = 0x02; // 0x200 bytes
|
|
|
|
cdb[0] = 0xA8; // Read(12)
|
|
cdb[8] = 0x02; // 0x200 bytes
|
|
|
|
send_mass_storage_command(handle, 0x01, 0, cdb, LIBUSB_ENDPOINT_IN, 0x200, &expected_tag);
|
|
libusb_bulk_transfer(handle, 0x81, (unsigned char*)&buffer, 0x200, &size, 5000);
|
|
printf("READ: received %d bytes\n", size);
|
|
if (get_mass_storage_status(handle, 0x81, expected_tag) == -2) {
|
|
get_sense(handle, 0x81, 0x01);
|
|
} else {
|
|
for(i=0; i<0x10; i++) {
|
|
printf(" %02X", buffer[i]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int test_device(uint16_t vid, uint16_t pid)
|
|
{
|
|
libusb_device_handle *handle;
|
|
libusb_device *dev;
|
|
struct libusb_config_descriptor *conf_desc;
|
|
const struct libusb_endpoint_descriptor *endpoint;
|
|
int i, j, k, r;
|
|
int iface, nb_ifaces;
|
|
int test_scsi = 0;
|
|
struct libusb_device_descriptor dev_desc;
|
|
|
|
printf("Opening device...\n");
|
|
handle = libusb_open_device_with_vid_pid(NULL, vid, pid);
|
|
|
|
if (handle == NULL) {
|
|
perr(" failed.\n");
|
|
return -1;
|
|
}
|
|
|
|
dev = libusb_get_device(handle);
|
|
|
|
printf("reading device descriptor...\n");
|
|
CALL_CHECK(libusb_get_device_descriptor(dev, &dev_desc));
|
|
printf("length = %d\n", dev_desc.bLength);
|
|
printf("device class = %d\n", dev_desc.bDeviceClass);
|
|
printf("ser num = %d\n", dev_desc.iSerialNumber);
|
|
printf("VID:PID %04X:%04X\n", dev_desc.idVendor, dev_desc.idProduct);
|
|
printf("bcdDevice = %04X\n", dev_desc.bcdDevice);
|
|
printf("iMan:iProd:iSer %d:%d:%d\n", dev_desc.iManufacturer, dev_desc.iProduct, dev_desc.iSerialNumber);
|
|
printf("num confs = %d\n", dev_desc.bNumConfigurations);
|
|
|
|
printf("reading configuration descriptor...\n");
|
|
CALL_CHECK(libusb_get_config_descriptor(dev, 0, &conf_desc));
|
|
nb_ifaces = conf_desc->bNumInterfaces;
|
|
printf("num interfaces = %d\n", nb_ifaces);
|
|
for (i=0; i<conf_desc->bNumInterfaces; i++) {
|
|
for (j=0; j<conf_desc->usb_interface[i].num_altsetting; j++) {
|
|
printf("interface[%d].altsetting[%d]: num endpoints = %d\n",
|
|
i, j, conf_desc->usb_interface[i].altsetting[j].bNumEndpoints);
|
|
printf(" Class.SubClass.Protocol: %02X.%02X.%02X\n",
|
|
conf_desc->usb_interface[i].altsetting[j].bInterfaceClass,
|
|
conf_desc->usb_interface[i].altsetting[j].bInterfaceSubClass,
|
|
conf_desc->usb_interface[i].altsetting[j].bInterfaceProtocol);
|
|
if ( (conf_desc->usb_interface[i].altsetting[j].bInterfaceClass == LIBUSB_CLASS_MASS_STORAGE)
|
|
&& ( (conf_desc->usb_interface[i].altsetting[j].bInterfaceSubClass == 0x01)
|
|
|| (conf_desc->usb_interface[i].altsetting[j].bInterfaceSubClass == 0x06) ) ) {
|
|
// Mass storage devices that can use basic SCSI commands
|
|
test_scsi = -1;
|
|
}
|
|
for (k=0; k<conf_desc->usb_interface[i].altsetting[j].bNumEndpoints; k++) {
|
|
endpoint = &conf_desc->usb_interface[i].altsetting[j].endpoint[k];
|
|
printf(" endpoint[%d].address: %02X\n", k, endpoint->bEndpointAddress);
|
|
printf(" max packet size: %04X\n", endpoint->wMaxPacketSize);
|
|
printf(" polling interval: %02X\n", endpoint->bInterval);
|
|
}
|
|
}
|
|
}
|
|
libusb_free_config_descriptor(conf_desc);
|
|
|
|
for (iface = 0; iface < nb_ifaces; iface++)
|
|
{
|
|
printf("Claiming interface %d...\n", iface);
|
|
r = libusb_claim_interface(handle, iface);
|
|
if (r != LIBUSB_SUCCESS) {
|
|
if (iface == 0) {
|
|
// Maybe we need to detach the driver
|
|
perr("failed. Trying to detach driver...\n");
|
|
CALL_CHECK(libusb_detach_kernel_driver(handle, iface));
|
|
printf("Claiming interface again...\n");
|
|
CALL_CHECK(libusb_claim_interface(handle, iface));
|
|
} else {
|
|
printf("failed.\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (test_mode == USE_XBOX) {
|
|
CALL_CHECK(display_xbox_status(handle));
|
|
CALL_CHECK(set_xbox_actuators(handle, 128, 222));
|
|
msleep(2000);
|
|
CALL_CHECK(set_xbox_actuators(handle, 0, 0));
|
|
} else {
|
|
char string[128];
|
|
printf("Retieving string descriptor...\n");
|
|
CALL_CHECK(libusb_get_string_descriptor_ascii(handle, 2, string, 128));
|
|
printf("Got string: \"%s\"\n", string);
|
|
}
|
|
|
|
if (test_scsi) {
|
|
CALL_CHECK(test_mass_storage(handle));
|
|
}
|
|
|
|
for (iface = 0; iface<nb_ifaces; iface++) {
|
|
printf("Releasing interface %d...\n", iface);
|
|
libusb_release_interface(handle, iface);
|
|
}
|
|
|
|
printf("Closing device...\n");
|
|
libusb_close(handle);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
int r;
|
|
|
|
// Default test = Microsoft XBox Controller Type S - 1 interface
|
|
VID = 0x045E;
|
|
PID = 0x0289;
|
|
test_mode = USE_XBOX;
|
|
|
|
if (argc == 2) {
|
|
if ((argv[1][0] != '-') || (argv[1][1] == 'h')) {
|
|
printf("usage: %s [-h] [-j] [-k] [-x]\n", argv[0]);
|
|
printf(" -h: display usage\n");
|
|
printf(" -j: test OLIMEX ARM-USB-TINY JTAG, 2 channel composite device\n");
|
|
printf(" -k: test Generic 2 GB USB Key\n");
|
|
printf(" -x: test Microsoft XBox Controller Type S (default)\n");
|
|
return 0;
|
|
}
|
|
switch(argv[1][1]) {
|
|
case 'j':
|
|
// OLIMEX ARM-USB-TINY JTAG, 2 channel composite device - 2 interfaces
|
|
VID = 0x15BA;
|
|
PID = 0x0004;
|
|
test_mode = USE_JTAG;
|
|
break;
|
|
case 'k':
|
|
// Generic 2 GB USB Key (SCSI Transparent/Bulk Only) - 1 interface
|
|
VID = 0x0204;
|
|
PID = 0x6025;
|
|
test_mode = USE_KEY;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
r = libusb_init(NULL);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
test_device(VID, PID);
|
|
|
|
libusb_exit(NULL);
|
|
|
|
return 0;
|
|
}
|
|
|