diff --git a/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/.classpath b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/.classpath new file mode 100644 index 00000000000..ba3768f9fb8 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/.classpath @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/.project b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/.project new file mode 100644 index 00000000000..ad2f4ec236a --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/.project @@ -0,0 +1,23 @@ + + + org.openhab.core.config.discovery.usbserial.javaxusb + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/NOTICE b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/NOTICE new file mode 100644 index 00000000000..6c17d0d8a45 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/NOTICE @@ -0,0 +1,14 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-core + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/pom.xml b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/pom.xml new file mode 100644 index 00000000000..f4510e871ad --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.core.config.discovery.usbserial.javaxusb + + openHAB Core :: Bundles :: Configuration USB-Serial Discovery via 'javax.usb' + + + + org.openhab.core.bundles + org.openhab.core.config.discovery.usbserial + ${project.version} + + + net.java.dev.jna + jna-platform + 5.13.0 + + + javax.usb + usb-api + 1.0.2 + + + org.usb4java + usb4java-javax + 1.3.0 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 + + + embed-dependencies + + unpack-dependencies + + + runtime + jar + org.usb4java,javax.usb + ${project.build.directory}/classes + true + true + false + jar + + + + + + + + diff --git a/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/src/main/java/org/openhab/core/config/discovery/usbserial/javaxusb/internal/JavaxUsbSerialDiscovery.java b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/src/main/java/org/openhab/core/config/discovery/usbserial/javaxusb/internal/JavaxUsbSerialDiscovery.java new file mode 100644 index 00000000000..6539e97891e --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/src/main/java/org/openhab/core/config/discovery/usbserial/javaxusb/internal/JavaxUsbSerialDiscovery.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery.usbserial.javaxusb.internal; + +import java.io.UnsupportedEncodingException; +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.usb.UsbConfiguration; +import javax.usb.UsbDevice; +import javax.usb.UsbDeviceDescriptor; +import javax.usb.UsbDisconnectedException; +import javax.usb.UsbException; +import javax.usb.UsbHostManager; +import javax.usb.UsbHub; +import javax.usb.UsbInterface; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.ThreadFactoryBuilder; +import org.openhab.core.config.discovery.usbserial.UsbSerialDeviceInformation; +import org.openhab.core.config.discovery.usbserial.UsbSerialDiscovery; +import org.openhab.core.config.discovery.usbserial.UsbSerialDiscoveryListener; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.jna.Platform; + +/** + * This is a {@link UsbSerialDiscovery} implementation component that scans the system for USB devices by means of the + * {@link org.usb4java} library implementation of the {@link javax.usb} interface. + *

+ * It provides USB coverage on non Linux Operating Systems. Linux is already better covered by the scanners in the + * {@link org.openhab.core.config.discovery.usbserial.linuxsysfs} module. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +@Component(service = UsbSerialDiscovery.class, name = JavaxUsbSerialDiscovery.SERVICE_NAME) +public class JavaxUsbSerialDiscovery implements UsbSerialDiscovery { + + protected static final String SERVICE_NAME = "usb-serial-discovery-javaxusb"; + + private final Logger logger = LoggerFactory.getLogger(JavaxUsbSerialDiscovery.class); + private final Set discoveryListeners = new CopyOnWriteArraySet<>(); + private final Duration scanInterval = Duration.ofSeconds(15); + private final ScheduledExecutorService scheduler; + + private Set lastScanResult = new HashSet<>(); + private @Nullable ScheduledFuture scanTask; + + @Activate + public JavaxUsbSerialDiscovery() { + scheduler = Executors.newSingleThreadScheduledExecutor( + ThreadFactoryBuilder.create().withName(SERVICE_NAME).withDaemonThreads(true).build()); + } + + private void announceAddedDevice(UsbSerialDeviceInformation deviceInfo) { + for (UsbSerialDiscoveryListener listener : discoveryListeners) { + listener.usbSerialDeviceDiscovered(deviceInfo); + } + } + + private void announceRemovedDevice(UsbSerialDeviceInformation deviceInfo) { + for (UsbSerialDiscoveryListener listener : discoveryListeners) { + listener.usbSerialDeviceRemoved(deviceInfo); + } + } + + @Deactivate + public void deactivate() { + stopBackgroundScanning(); + lastScanResult.clear(); + } + + @Override + public synchronized void doSingleScan() { + Set scanResult = scanAllUsbDevicesInformation(); + Set added = setDifference(scanResult, lastScanResult); + Set removed = setDifference(lastScanResult, scanResult); + Set unchanged = setDifference(scanResult, added); + + lastScanResult = scanResult; + + removed.stream().forEach(this::announceRemovedDevice); + added.stream().forEach(this::announceAddedDevice); + unchanged.stream().forEach(this::announceAddedDevice); + } + + private Set setDifference(Set set1, Set set2) { + Set result = new HashSet<>(set1); + result.removeAll(set2); + return result; + } + + @Override + public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) { + discoveryListeners.add(listener); + for (UsbSerialDeviceInformation deviceInfo : lastScanResult) { + listener.usbSerialDeviceDiscovered(deviceInfo); + } + } + + @Override + public void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener) { + discoveryListeners.remove(listener); + } + + /** + * Traverse the USB tree for devices that are children of the ROOT hub, and return a set of USB device information. + * + * @return a set of USB device information. + */ + private Set scanAllUsbDevicesInformation() { + try { + return scanChildUsbDeviceInformation(UsbHostManager.getUsbServices().getRootUsbHub()); + } catch (SecurityException | UsbException e) { + logger.warn("Error getting USB device information: {}", e.getMessage()); + return Set.of(); + } + } + + /** + * Traverse the USB tree for devices that are children of the given hub, and return a set of USB device information. + * + * @param usbHub the hub whose children are to be found. + * @return a set of USB device information. + */ + private Set scanChildUsbDeviceInformation(UsbHub usbHub) { + Set result = new HashSet<>(); + + @SuppressWarnings("unchecked") + List deviceList = usbHub.getAttachedUsbDevices(); + + deviceList.forEach(usbDevice -> { + if (usbDevice.isUsbHub()) { + result.addAll(scanChildUsbDeviceInformation((UsbHub) usbDevice)); + } else { + UsbDeviceDescriptor d = usbDevice.getUsbDeviceDescriptor(); + short vendorId = d.idVendor(); + short productId = d.idProduct(); + + String manufacturer = null; + String product = null; + String serialNumber = null; + + /* + * Note: the getString() calls below may fail depending on the Operating System: + * - on Windows if no libusb device driver is installed for the device. + * - on Linux if the user has no write permission on the USB device file. + */ + try { + manufacturer = usbDevice.getString(d.iManufacturer()); + product = usbDevice.getString(d.iProduct()); + serialNumber = usbDevice.getString(d.iSerialNumber()); + } catch (UnsupportedEncodingException | UsbDisconnectedException | UsbException e) { + // ignore because this would be a 'normal' runtime failure + } + + String serialPort = ""; + int interfaceNumber = 0; + String interfaceDescription = null; + + UsbConfiguration configuration = usbDevice.getActiveUsbConfiguration(); + if (configuration == null) { + UsbSerialDeviceInformation usbDeviceInfo = new UsbSerialDeviceInformation(vendorId, productId, + serialNumber, manufacturer, product, interfaceNumber, interfaceDescription, serialPort); + + result.add(usbDeviceInfo); + logger.trace("Added device: {}", usbDeviceInfo); + } else { + @SuppressWarnings("unchecked") + List interfaces = configuration.getUsbInterfaces(); + for (UsbInterface ifx : interfaces) { + try { + interfaceDescription = ifx.getInterfaceString(); + } catch (UnsupportedEncodingException | UsbDisconnectedException | UsbException e) { + interfaceDescription = null; + } + + UsbSerialDeviceInformation usbDeviceInfo = new UsbSerialDeviceInformation(vendorId, productId, + serialNumber, manufacturer, product, interfaceNumber, interfaceDescription, serialPort); + + result.add(usbDeviceInfo); + logger.trace("Added device: {}", usbDeviceInfo); + interfaceNumber++; + } + } + } + }); + + return result; + } + + @Override + public synchronized void startBackgroundScanning() { + if (Platform.isWindows() || Platform.isLinux()) { + return; + } + ScheduledFuture scanTask = this.scanTask; + if (scanTask == null || scanTask.isDone()) { + this.scanTask = scheduler.scheduleWithFixedDelay(() -> doSingleScan(), 0, scanInterval.toSeconds(), + TimeUnit.SECONDS); + } + } + + @Override + public synchronized void stopBackgroundScanning() { + ScheduledFuture scanTask = this.scanTask; + if (scanTask != null) { + scanTask.cancel(false); + } + this.scanTask = null; + } +} diff --git a/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/src/main/resources/javax.usb.properties b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/src/main/resources/javax.usb.properties new file mode 100644 index 00000000000..50e944e4b68 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery.usbserial.javaxusb/src/main/resources/javax.usb.properties @@ -0,0 +1 @@ +javax.usb.services = org.usb4java.javax.Services diff --git a/bundles/pom.xml b/bundles/pom.xml index 8f6fd77f721..67c8bf6176d 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -38,6 +38,7 @@ org.openhab.core.config.discovery.addon.usb org.openhab.core.config.discovery.mdns org.openhab.core.config.discovery.usbserial + org.openhab.core.config.discovery.usbserial.javaxusb org.openhab.core.config.discovery.usbserial.linuxsysfs org.openhab.core.config.discovery.usbserial.ser2net org.openhab.core.config.discovery.usbserial.windowsregistry diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 94d766da68a..c0b0e15ab27 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -520,6 +520,11 @@ mvn:org.openhab.core.bundles/org.openhab.core.config.serial/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial/${project.version} + + req:osgi.native;filter:="(osgi.native.osname=MacOS*)" + mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.javaxusb/${project.version} + + req:osgi.native;filter:="(osgi.native.osname=Linux)" mvn:org.openhab.core.bundles/org.openhab.core.config.discovery.usbserial.linuxsysfs/${project.version}