-
-
Notifications
You must be signed in to change notification settings - Fork 428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UsbSerialDiscovery service based on Javax-Usb #3930
base: main
Are you sure you want to change the base?
Changes from 22 commits
14e3349
0790943
68569d3
69bda66
2f41474
4e1f44b
b073ad6
e513770
0c0ec4e
c466b17
d2e5890
1a74415
5f5f580
2f7f05b
f0e7dd2
da6043b
c821522
c5de645
6b4aeb0
afe6af4
02c6243
38d5df4
3ab3915
a4dc730
9eaecac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<classpath> | ||
<classpathentry kind="src" output="target/classes" path="src/main/java"> | ||
<attributes> | ||
<attribute name="optional" value="true"/> | ||
<attribute name="maven.pomderived" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="annotationpath" value="target/dependency"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="annotationpath" value="target/dependency"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="src" output="target/test-classes" path="src/test/java"> | ||
<attributes> | ||
<attribute name="optional" value="true"/> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="test" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="output" path="target/classes"/> | ||
</classpath> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<projectDescription> | ||
<name>org.openhab.core.config.discovery.usbserial.javaxusb</name> | ||
<comment></comment> | ||
<projects> | ||
</projects> | ||
<buildSpec> | ||
<buildCommand> | ||
<name>org.eclipse.jdt.core.javabuilder</name> | ||
<arguments> | ||
</arguments> | ||
</buildCommand> | ||
<buildCommand> | ||
<name>org.eclipse.m2e.core.maven2Builder</name> | ||
<arguments> | ||
</arguments> | ||
</buildCommand> | ||
</buildSpec> | ||
<natures> | ||
<nature>org.eclipse.jdt.core.javanature</nature> | ||
<nature>org.eclipse.m2e.core.maven2Nature</nature> | ||
</natures> | ||
</projectDescription> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||||||
|
||||||||
<modelVersion>4.0.0</modelVersion> | ||||||||
|
||||||||
<parent> | ||||||||
<groupId>org.openhab.core.bundles</groupId> | ||||||||
<artifactId>org.openhab.core.reactor.bundles</artifactId> | ||||||||
<version>4.2.0-SNAPSHOT</version> | ||||||||
</parent> | ||||||||
|
||||||||
<artifactId>org.openhab.core.config.discovery.usbserial.javaxusb</artifactId> | ||||||||
|
||||||||
<name>openHAB Core :: Bundles :: Configuration USB-Serial Discovery via 'javax.usb'</name> | ||||||||
|
||||||||
<dependencies> | ||||||||
<dependency> | ||||||||
<groupId>org.openhab.core.bundles</groupId> | ||||||||
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId> | ||||||||
<version>${project.version}</version> | ||||||||
</dependency> | ||||||||
<dependency> | ||||||||
<groupId>net.java.dev.jna</groupId> | ||||||||
<artifactId>jna-platform</artifactId> | ||||||||
<version>5.13.0</version> | ||||||||
</dependency> | ||||||||
<dependency> | ||||||||
<groupId>javax.usb</groupId> | ||||||||
<artifactId>usb-api</artifactId> | ||||||||
<version>1.0.2</version> | ||||||||
</dependency> | ||||||||
<dependency> | ||||||||
<groupId>org.usb4java</groupId> | ||||||||
<artifactId>usb4java-javax</artifactId> | ||||||||
<version>1.3.0</version> | ||||||||
</dependency> | ||||||||
</dependencies> | ||||||||
|
||||||||
<build> | ||||||||
<plugins> | ||||||||
<plugin> | ||||||||
<groupId>org.apache.maven.plugins</groupId> | ||||||||
<artifactId>maven-dependency-plugin</artifactId> | ||||||||
<version>3.1.1</version> | ||||||||
<executions> | ||||||||
<execution> | ||||||||
<id>embed-dependencies</id> | ||||||||
<goals> | ||||||||
<goal>unpack-dependencies</goal> | ||||||||
</goals> | ||||||||
<configuration> | ||||||||
<includeScope>runtime</includeScope> | ||||||||
<includeTypes>jar</includeTypes> | ||||||||
<includeGroupIds>org.usb4java,javax.usb</includeGroupIds> | ||||||||
<outputDirectory>${project.build.directory}/classes</outputDirectory> | ||||||||
<overWriteReleases>true</overWriteReleases> | ||||||||
<overWriteSnapshots>true</overWriteSnapshots> | ||||||||
<excludeTransitive>false</excludeTransitive> | ||||||||
<type>jar</type> | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can exclude the Linux and Windows libraries by adding this:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed it only contains darwin-x86-64 libraries so it will not work on Apple silicon based Macs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Nor on Arm based ones .. although TBH I am not sure if any such exist(ed).. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. .. and I even wonder if MacOS does not (still) have an inner core based on Linux .. I know that it used to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's based on Unix because Linux didn't even exist yet back then. 🦖 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To summarise: this seems to be a Mac specific issue; neither you nor I have a Mac to test on; so we can't really judge if this PR adds value, or if the existing Discovery component added value prior to this. So the question is what shall we do with this one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's wait for the feedback of a maintainer using macOS. IIRC there is also x86 emulation on the Macs using Apple silicon. I'm not familiar with how the emulation works. It could be that it only works well when all code runs on the x86 emulator and not just a single library. |
||||||||
</configuration> | ||||||||
</execution> | ||||||||
</executions> | ||||||||
</plugin> | ||||||||
</plugins> | ||||||||
</build> | ||||||||
|
||||||||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* <p> | ||
* 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<UsbSerialDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>(); | ||
private final Duration scanInterval = Duration.ofSeconds(15); | ||
private final ScheduledExecutorService scheduler; | ||
|
||
private Set<UsbSerialDeviceInformation> 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<UsbSerialDeviceInformation> scanResult = scanAllUsbDevicesInformation(); | ||
Set<UsbSerialDeviceInformation> added = setDifference(scanResult, lastScanResult); | ||
Set<UsbSerialDeviceInformation> removed = setDifference(lastScanResult, scanResult); | ||
Set<UsbSerialDeviceInformation> unchanged = setDifference(scanResult, added); | ||
|
||
lastScanResult = scanResult; | ||
|
||
removed.stream().forEach(this::announceRemovedDevice); | ||
added.stream().forEach(this::announceAddedDevice); | ||
unchanged.stream().forEach(this::announceAddedDevice); | ||
} | ||
|
||
private <T> Set<T> setDifference(Set<T> set1, Set<T> set2) { | ||
Set<T> 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<UsbSerialDeviceInformation> 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<UsbSerialDeviceInformation> scanChildUsbDeviceInformation(UsbHub usbHub) { | ||
Set<UsbSerialDeviceInformation> result = new HashSet<>(); | ||
|
||
@SuppressWarnings("unchecked") | ||
List<UsbDevice> 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<UsbInterface> 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
javax.usb.services = org.usb4java.javax.Services |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.