Skip to content
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

Add ser2net mDNS USB serial discovery #2519

Merged
merged 3 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bom/openhab-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial.ser2net</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.upnp</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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-11">
<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 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.ser2net</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,30 @@
<?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 http://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>3.2.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.core.config.discovery.usbserial.ser2net</artifactId>

<name>openHAB Core :: Bundles :: Configuration USB-Serial Discovery using ser2net mDNS scanning</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery.usbserial</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.transport.mdns</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* Copyright (c) 2010-2021 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.ser2net.internal;

import java.time.Duration;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;

import org.eclipse.jdt.annotation.NonNullByDefault;
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.openhab.core.io.transport.mdns.MDNSClient;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A {@link UsbSerialDiscovery} that implements background discovery of RFC2217 by listening to
* ser2net mDNS service events.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
@Component(service = UsbSerialDiscovery.class)
public class Ser2NetUsbSerialDiscovery implements ServiceListener, UsbSerialDiscovery {

private final Logger logger = LoggerFactory.getLogger(Ser2NetUsbSerialDiscovery.class);

static final String SERVICE_TYPE = "_iostream._tcp.local.";

static final String PROPERTY_PROVIDER = "provider";
static final String PROPERTY_DEVICE_TYPE = "devicetype";
static final String PROPERTY_GENSIO_STACK = "gensiostack";

static final String PROPERTY_VENDOR_ID = "idVendor";
static final String PROPERTY_PRODUCT_ID = "idProduct";

static final String PROPERTY_SERIAL_NUMBER = "serial";
static final String PROPERTY_MANUFACTURER = "manufacturer";
static final String PROPERTY_PRODUCT = "product";

static final String PROPERTY_INTERFACE_NUMBER = "bInterfaceNumber";
static final String PROPERTY_INTERFACE = "interface";

static final String SER2NET = "ser2net";
static final String SERIALUSB = "serialusb";
static final String TELNET_RFC2217_TCP = "telnet(rfc2217),tcp";

static final Duration SINGLE_SCAN_DURATION = Duration.ofSeconds(5);
static final String SERIAL_PORT_NAME_FORMAT = "rfc2217://%s:%s";

private final Set<UsbSerialDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
private final MDNSClient mdnsClient;

private boolean backgroundDiscoveryEnabled = false;

private Set<UsbSerialDeviceInformation> lastScanResult = new HashSet<>();

@Activate
public Ser2NetUsbSerialDiscovery(final @Reference MDNSClient mdnsClient) {
this.mdnsClient = mdnsClient;
}

@Override
public void registerDiscoveryListener(UsbSerialDiscoveryListener listener) {
discoveryListeners.add(listener);
}

@Override
public void unregisterDiscoveryListener(UsbSerialDiscoveryListener listener) {
discoveryListeners.remove(listener);
}

@Override
public synchronized void startBackgroundScanning() {
backgroundDiscoveryEnabled = true;
cweitkamp marked this conversation as resolved.
Show resolved Hide resolved
mdnsClient.addServiceListener(SERVICE_TYPE, this);
logger.debug("Started ser2net USB-Serial mDNS background discovery");
}

@Override
public synchronized void stopBackgroundScanning() {
backgroundDiscoveryEnabled = false;
mdnsClient.removeServiceListener(SERVICE_TYPE, this);
logger.debug("Stopped ser2net USB-Serial mDNS background discovery");
}

@Override
public synchronized void doSingleScan() {
logger.debug("Starting ser2net USB-Serial mDNS single discovery scan");

Set<UsbSerialDeviceInformation> scanResult = Stream.of(mdnsClient.list(SERVICE_TYPE, SINGLE_SCAN_DURATION))
.map(this::createUsbSerialDeviceInformation) //
.filter(Optional::isPresent) //
.map(Optional::get) //
.collect(Collectors.toSet());

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);

logger.debug("Completed ser2net USB-Serial mDNS single discovery scan");
}

private <T> Set<T> setDifference(Set<T> set1, Set<T> set2) {
Set<T> result = new HashSet<>(set1);
result.removeAll(set2);
return Set.copyOf(result);
cweitkamp marked this conversation as resolved.
Show resolved Hide resolved
}

private void announceAddedDevice(UsbSerialDeviceInformation deviceInfo) {
for (UsbSerialDiscoveryListener listener : discoveryListeners) {
listener.usbSerialDeviceDiscovered(deviceInfo);
}
}

private void announceRemovedDevice(UsbSerialDeviceInformation deviceInfo) {
for (UsbSerialDiscoveryListener listener : discoveryListeners) {
listener.usbSerialDeviceRemoved(deviceInfo);
}
}

@Override
public void serviceAdded(@NonNullByDefault({}) ServiceEvent event) {
if (backgroundDiscoveryEnabled) {
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
deviceInfo.ifPresent(this::announceAddedDevice);
}
}

@Override
public void serviceRemoved(@NonNullByDefault({}) ServiceEvent event) {
if (backgroundDiscoveryEnabled) {
Optional<UsbSerialDeviceInformation> deviceInfo = createUsbSerialDeviceInformation(event.getInfo());
deviceInfo.ifPresent(this::announceRemovedDevice);
}
}

@Override
public void serviceResolved(@NonNullByDefault({}) ServiceEvent event) {
serviceAdded(event);
}

private Optional<UsbSerialDeviceInformation> createUsbSerialDeviceInformation(ServiceInfo serviceInfo) {
String provider = serviceInfo.getPropertyString(PROPERTY_PROVIDER);
String deviceType = serviceInfo.getPropertyString(PROPERTY_DEVICE_TYPE);
String gensioStack = serviceInfo.getPropertyString(PROPERTY_GENSIO_STACK);

// Check ser2net specific properties when present
if (SER2NET.equals(provider) && (deviceType != null && !SERIALUSB.equals(deviceType))
|| (gensioStack != null && !TELNET_RFC2217_TCP.equals(gensioStack))) {
logger.debug("Skipping creation of UsbSerialDeviceInformation based on {}", serviceInfo);
return Optional.empty();
}

try {
int vendorId = Integer.parseInt(serviceInfo.getPropertyString(PROPERTY_VENDOR_ID), 16);
int productId = Integer.parseInt(serviceInfo.getPropertyString(PROPERTY_PRODUCT_ID), 16);

String serialNumber = serviceInfo.getPropertyString(PROPERTY_SERIAL_NUMBER);
String manufacturer = serviceInfo.getPropertyString(PROPERTY_MANUFACTURER);
String product = serviceInfo.getPropertyString(PROPERTY_PRODUCT);

int interfaceNumber = Integer.parseInt(serviceInfo.getPropertyString(PROPERTY_INTERFACE_NUMBER), 16);
String interfaceDescription = serviceInfo.getPropertyString(PROPERTY_INTERFACE);

String serialPortName = String.format(SERIAL_PORT_NAME_FORMAT, serviceInfo.getHostAddresses()[0],
serviceInfo.getPort());

UsbSerialDeviceInformation deviceInfo = new UsbSerialDeviceInformation(vendorId, productId, serialNumber,
manufacturer, product, interfaceNumber, interfaceDescription, serialPortName);
logger.debug("Created {} based on {}", deviceInfo, serviceInfo);
return Optional.of(deviceInfo);
} catch (NumberFormatException e) {
logger.debug("Failed to create UsbSerialDeviceInformation based on {}", serviceInfo, e);
return Optional.empty();
}
}
}
Loading