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}