001package jmri.util.usb; 002 003import java.io.UnsupportedEncodingException; 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.List; 007import javax.annotation.Nonnull; 008import javax.annotation.CheckForNull; 009import javax.usb.UsbConfiguration; 010import javax.usb.UsbDevice; 011import javax.usb.UsbDeviceDescriptor; 012import javax.usb.UsbDisconnectedException; 013import javax.usb.UsbException; 014import javax.usb.UsbHostManager; 015import javax.usb.UsbHub; 016import javax.usb.UsbInterface; 017import javax.usb.UsbNotActiveException; 018import javax.usb.UsbNotClaimedException; 019import javax.usb.UsbNotOpenException; 020import javax.usb.UsbPipe; 021import javax.usb.UsbPort; 022import javax.usb.event.UsbPipeDataEvent; 023import javax.usb.event.UsbPipeErrorEvent; 024import javax.usb.event.UsbPipeListener; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028/** 029 * USB utilities. 030 * 031 * @author George Warner Copyright (c) 2017-2018 032 * @since 4.9.6 033 */ 034public final class UsbUtil { 035 036 /** 037 * Prevent construction, since this is a stateless utility class 038 */ 039 private UsbUtil() { 040 // prevent construction, since this is a stateless utility class 041 } 042 043 /** 044 * Get all USB devices. 045 * 046 * @return a list of all UsbDevice's 047 */ 048 public static List<UsbDevice> getAllDevices() { 049 return getMatchingDevices((short) 0, (short) 0, null); 050 } 051 052 /** 053 * Get matching USB devices. 054 * 055 * @param idVendor the vendor id to match (zero matches any) 056 * @param idProduct the product id to match (zero matches any) 057 * @param serialNumber the serial number to match (null matches any) 058 * @return a list of matching UsbDevices 059 */ 060 public static List<UsbDevice> getMatchingDevices(short idVendor, short idProduct, @CheckForNull String serialNumber) { 061 return findUsbDevices(null, idVendor, idProduct, serialNumber); 062 } 063 064 /** 065 * Get matching USB device. 066 * 067 * @param idVendor the vendor id to match (zero matches any) 068 * @param idProduct the product id to match (zero matches any) 069 * @param serialNumber the serial number to match (null matches any) 070 * @param idLocation the location to match 071 * @return the matching UsbDevice or null if no match could be found 072 */ 073 @CheckForNull 074 public static UsbDevice getMatchingDevice(short idVendor, short idProduct, @CheckForNull String serialNumber, @Nonnull String idLocation) { 075 for (UsbDevice usbDevice : findUsbDevices(null, idVendor, idProduct, serialNumber)) { 076 String locationID = getLocation(usbDevice); 077 if (locationID.equals(idLocation)) { 078 return usbDevice; 079 } 080 } 081 return null; 082 } 083 084 /** 085 * Get a USB device's full product (manufacturer + product) name. 086 * 087 * @param usbDevice the USB device to get the full product name of 088 * @return the full product name or null if the product name is not encoded 089 * in the device 090 */ 091 @CheckForNull 092 public static String getFullProductName(@Nonnull UsbDevice usbDevice) { 093 String result = null; 094 try { 095 String manufacturer = usbDevice.getManufacturerString(); 096 String product = usbDevice.getProductString(); 097 if (product != null) { 098 if (manufacturer == null || product.startsWith(manufacturer)) { 099 result = product; 100 } else { 101 result = Bundle.getMessage("UsbDevice", manufacturer, product); 102 } 103 } 104 } catch (UsbException | UnsupportedEncodingException ex) { 105 log.error("Unable to read data from {}", usbDevice, ex); 106 } catch (UsbDisconnectedException ex) { 107 log.error("Unable to read data from disconnected device {}", usbDevice); 108 } 109 return result; 110 } 111 112 /** 113 * Get a USB device's serial number. 114 * 115 * @param usbDevice the USB device to get the serial number of 116 * @return serial number 117 */ 118 @CheckForNull 119 public static String getSerialNumber(@Nonnull UsbDevice usbDevice) { 120 try { 121 return usbDevice.getSerialNumberString(); 122 } catch (UsbException | UnsupportedEncodingException | UsbDisconnectedException ex) { 123 log.error("Unable to get serial number of {}", usbDevice); 124 } 125 return null; 126 } 127 128 /** 129 * Get a unique value that represents the device's location in the USB 130 * device topology. 131 * <p> 132 * The location is a series of USB ports separated by colons (:) starting 133 * from the root hub (a virtual hub maintained by the operating system), 134 * represented as {@code USB} in the location, passing through hubs (which 135 * may be virtual or physical), to the port the requested device is plugged 136 * into. 137 * <p> 138 * <strong>Note:</strong> this method should only be used to uniquely 139 * identify USB devices in combination with consideration of the USB device 140 * product ID, vendor ID, and serial number, as using this alone could mean 141 * that two devices with the same product and vendor IDs, but different 142 * serial numbers could be misidentified if unplugged and reconnected in 143 * ports previously used by the other device, or if the hub does not 144 * consistently enumerate ports the same way. 145 * 146 * @param usbDevice the device to get the location of 147 * @return the location 148 */ 149 public static String getLocation(@Nonnull UsbDevice usbDevice) { 150 UsbDevice device = usbDevice; 151 StringBuilder path = new StringBuilder(); 152 while (device != null) { 153 UsbPort port = device.getParentUsbPort(); 154 if (port == null) { 155 break; 156 } 157 path.append(Byte.toString(port.getPortNumber())).append(':'); 158 device = port.getUsbHub(); 159 } 160 return String.format("USB%s", path.reverse().toString()); 161 } 162 163 /** 164 * Recursive routine to collect USB devices. 165 * 166 * @param usbHub the hub who's devices we want to collect (null for 167 * root) 168 * @param idVendor the vendor id to match against 169 * @param idProduct the product id to match against 170 * @param serialNumber the serial number to match against 171 */ 172 @Nonnull 173 private static List<UsbDevice> findUsbDevices( 174 @CheckForNull UsbHub usbHub, 175 short idVendor, 176 short idProduct, 177 @CheckForNull String serialNumber) { 178 if (usbHub == null) { 179 try { 180 return findUsbDevices(UsbHostManager.getUsbServices().getRootUsbHub(), idVendor, idProduct, serialNumber); 181 } catch (UsbException | SecurityException ex) { 182 log.error("Exception: {}", ex.toString()); 183 return new ArrayList<>(); // abort with an empty list 184 } 185 } 186 187 @SuppressWarnings("unchecked") // cast required by UsbHub API 188 List<UsbDevice> usbDevices = usbHub.getAttachedUsbDevices(); 189 190 List<UsbDevice> devices = new ArrayList<>(); 191 usbDevices.forEach((usbDevice) -> { 192 if (usbDevice instanceof UsbHub) { 193 UsbHub childUsbHub = (UsbHub) usbDevice; 194 devices.addAll(findUsbDevices(childUsbHub, idVendor, idProduct, serialNumber)); 195 } else { 196 UsbDeviceDescriptor usbDeviceDescriptor = usbDevice.getUsbDeviceDescriptor(); 197 try { 198 if (((idVendor == 0) || (idVendor == usbDeviceDescriptor.idVendor())) 199 && ((idProduct == 0) || (idProduct == usbDeviceDescriptor.idProduct())) 200 && ((serialNumber == null) || serialNumber.equals(usbDevice.getSerialNumberString()))) { 201 devices.add(usbDevice); 202 } 203 } catch (UsbException | UnsupportedEncodingException | UsbDisconnectedException ex) { 204 log.error("Unable to request serial number from device {}", usbDevice, ex); 205 } 206 } 207 }); 208 return devices; 209 } 210 211 /** 212 * Read message synchronously. 213 * 214 * @param iface the interface 215 * @param endPoint the end point 216 */ 217 public static void readMessage(@Nonnull UsbInterface iface, byte endPoint) { 218 219 try { 220 iface.claim((UsbInterface usbInterface) -> true); 221 UsbPipe pipe = iface.getUsbEndpoint(endPoint).getUsbPipe(); 222 pipe.open(); 223 224 byte[] data = new byte[8]; 225 int received = pipe.syncSubmit(data); 226 log.debug("{} bytes received", received); 227 228 pipe.close(); 229 230 } catch (IllegalArgumentException | UsbDisconnectedException | UsbException | UsbNotActiveException | UsbNotClaimedException | UsbNotOpenException ex) { 231 log.error("Unable to read message", ex); 232 } finally { 233 try { 234 iface.release(); 235 } catch (UsbNotActiveException | UsbDisconnectedException | UsbException ex) { 236 log.error("Unable to release USB device", ex); 237 } 238 } 239 } 240 241 /** 242 * Read message asynchronously. 243 * 244 * @param iface the interface 245 * @param endPoint the end point 246 */ 247 public static void readMessageAsynch(@Nonnull UsbInterface iface, byte endPoint) { 248 249 try { 250 iface.claim((UsbInterface usbInterface) -> true); 251 252 UsbPipe pipe = iface.getUsbEndpoint(endPoint).getUsbPipe(); 253 254 pipe.open(); 255 256 pipe.addUsbPipeListener(new UsbPipeListener() { 257 @Override 258 public void errorEventOccurred(UsbPipeErrorEvent event) { 259 log.error("UsbPipeErrorEvent: {}", event, event.getUsbException()); 260 } 261 262 @Override 263 public void dataEventOccurred(UsbPipeDataEvent event) { 264 byte[] data = event.getData(); 265 if (log.isDebugEnabled()) { // avoid array->string conversion unless debugging 266 log.debug("bytes received: {}", Arrays.toString(data)); 267 } 268 } 269 }); 270 pipe.close(); 271 } catch (UsbDisconnectedException | UsbException | UsbNotActiveException | UsbNotClaimedException | UsbNotOpenException ex) { 272 log.error("Unable to read USB message.", ex); 273 } finally { 274 try { 275 iface.release(); 276 } catch (UsbNotActiveException | UsbDisconnectedException | UsbException ex) { 277 log.error("Unable to release USB device.", ex); 278 } 279 } 280 } 281 282 /** 283 * Get USB device interface. 284 * 285 * @param device the USB device 286 * @param index the USB interface index 287 * @return the USB interface 288 */ 289 public static UsbInterface getDeviceInterface(@Nonnull UsbDevice device, byte index) { 290 UsbConfiguration configuration = device.getActiveUsbConfiguration(); 291 if (configuration != null) { 292 return configuration.getUsbInterface(index); 293 } 294 return null; 295 } 296 297 private final static Logger log = LoggerFactory.getLogger(UsbUtil.class); 298}