[epsonprojector] Add ESC/VP.net handshake for projectors with built-in ethernet (#9375)

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein
2020-12-25 08:44:59 -06:00
committed by GitHub
parent 750ea5fd2d
commit 6c86f8d366
8 changed files with 343 additions and 22 deletions

View File

@@ -12,6 +12,8 @@
*/
package org.openhab.binding.epsonprojector.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@@ -20,18 +22,28 @@ import org.openhab.core.thing.ThingTypeUID;
* used across the whole binding.
*
* @author Yannick Schaus - Initial contribution
* @author Michael Lobstein - Updated for OH3
*/
@NonNullByDefault
public class EpsonProjectorBindingConstants {
private static final String BINDING_ID = "epsonprojector";
public static final String BINDING_ID = "epsonprojector";
public static final int DEFAULT_PORT = 3629;
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_PROJECTOR_SERIAL = new ThingTypeUID(BINDING_ID, "projector-serial");
public static final ThingTypeUID THING_TYPE_PROJECTOR_TCP = new ThingTypeUID(BINDING_ID, "projector-tcp");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PROJECTOR_SERIAL,
THING_TYPE_PROJECTOR_TCP);
// Some Channel types
public static final String CHANNEL_TYPE_POWER = "power";
public static final String CHANNEL_TYPE_POWERSTATE = "powerstate";
public static final String CHANNEL_TYPE_LAMPTIME = "lamptime";
// Config properties
public static final String THING_PROPERTY_HOST = "host";
public static final String THING_PROPERTY_PORT = "port";
public static final String THING_PROPERTY_MAC = "macAddress";
}

View File

@@ -137,12 +137,13 @@ public class EpsonProjectorDevice {
return response;
}
private String splitResponse(@Nullable String response) throws EpsonProjectorException {
private String splitResponse(@Nullable String response)
throws EpsonProjectorCommandException, EpsonProjectorException {
if (response != null && !"".equals(response)) {
String[] pieces = response.split("=");
if (pieces.length < 2) {
throw new EpsonProjectorException("Invalid response from projector: " + response);
throw new EpsonProjectorCommandException("Invalid response from projector: " + response);
}
return pieces[1].trim();

View File

@@ -14,11 +14,6 @@ package org.openhab.binding.epsonprojector.internal;
import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.epsonprojector.internal.handler.EpsonProjectorHandler;
@@ -42,9 +37,6 @@ import org.osgi.service.component.annotations.Reference;
@NonNullByDefault
@Component(configurationPid = "binding.epsonprojector", service = ThingHandlerFactory.class)
public class EpsonProjectorHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(THING_TYPE_PROJECTOR_SERIAL, THING_TYPE_PROJECTOR_TCP).collect(Collectors.toSet()));
private final SerialPortManager serialPortManager;
@Override

View File

@@ -12,6 +12,8 @@
*/
package org.openhab.binding.epsonprojector.internal.connector;
import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.DEFAULT_PORT;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -32,6 +34,7 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class EpsonProjectorTcpConnector implements EpsonProjectorConnector {
private static final String ESC_VP_HANDSHAKE = "ESC/VP.net\u0010\u0003\u0000\u0000\u0000\u0000";
private final Logger logger = LoggerFactory.getLogger(EpsonProjectorTcpConnector.class);
private final String ip;
@@ -58,6 +61,16 @@ public class EpsonProjectorTcpConnector implements EpsonProjectorConnector {
} catch (IOException e) {
throw new EpsonProjectorException(e);
}
// Projectors with built in Ethernet listen on 3629, we must send the handshake to initialize the connection
if (port == DEFAULT_PORT) {
try {
String response = sendMessage(ESC_VP_HANDSHAKE, 5000);
logger.debug("Response to initialisation of ESC/VP.net is: {}", response);
} catch (EpsonProjectorException e) {
logger.debug("Error within initialisation of ESC/VP.net: {}", e.getMessage());
}
}
}
@Override

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2020 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.binding.epsonprojector.internal.discovery;
import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.*;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
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;
/**
* The {@link EpsonProjectoreDiscoveryService} class implements a service
* for discovering Epson projectors using the AMX Device Discovery protocol.
*
* @author Mark Hilbush - Initial contribution
* @author Michael Lobstein - Adapted for the Epson Projector binding
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.epsonprojector")
public class EpsonProjectorDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(EpsonProjectorDiscoveryService.class);
private @Nullable ScheduledFuture<?> epsonDiscoveryJob;
// Discovery parameters
public static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
public static final int BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC = 10;
private NetworkAddressService networkAddressService;
private boolean terminate = false;
@Activate
public EpsonProjectorDiscoveryService(@Reference NetworkAddressService networkAddressService) {
super(SUPPORTED_THING_TYPES_UIDS, 0, BACKGROUND_DISCOVERY_ENABLED);
this.networkAddressService = networkAddressService;
epsonDiscoveryJob = null;
terminate = false;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
protected void startBackgroundDiscovery() {
if (epsonDiscoveryJob == null) {
terminate = false;
logger.debug("Starting background discovery job in {} seconds", BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC);
epsonDiscoveryJob = scheduler.schedule(this::discover, BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC,
TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
ScheduledFuture<?> epsonDiscoveryJob = this.epsonDiscoveryJob;
if (epsonDiscoveryJob != null) {
terminate = true;
epsonDiscoveryJob.cancel(false);
this.epsonDiscoveryJob = null;
}
}
@Override
public void startScan() {
}
@Override
public void stopScan() {
}
private synchronized void discover() {
logger.debug("Discovery job is running");
MulticastListener epsonMulticastListener;
String local = "127.0.0.1";
try {
String ip = networkAddressService.getPrimaryIpv4HostAddress();
epsonMulticastListener = new MulticastListener((ip != null ? ip : local));
} catch (SocketException se) {
logger.debug("Discovery job got Socket exception creating multicast socket: {}", se.getMessage());
return;
} catch (IOException ioe) {
logger.debug("Discovery job got IO exception creating multicast socket: {}", ioe.getMessage());
return;
}
while (!terminate) {
boolean beaconReceived;
try {
// Wait for a discovery beacon.
beaconReceived = epsonMulticastListener.waitForBeacon();
} catch (IOException ioe) {
logger.debug("Discovery job got exception waiting for beacon: {}", ioe.getMessage());
beaconReceived = false;
}
if (beaconReceived) {
// We got a discovery beacon. Process it as a potential new thing
Map<String, Object> properties = new HashMap<>();
String uid = epsonMulticastListener.getUID();
properties.put(THING_PROPERTY_HOST, epsonMulticastListener.getIPAddress());
properties.put(THING_PROPERTY_PORT, DEFAULT_PORT);
logger.trace("Projector with UID {} discovered at IP: {}", uid, epsonMulticastListener.getIPAddress());
ThingUID thingUid = new ThingUID(THING_TYPE_PROJECTOR_TCP, uid);
logger.trace("Creating epson projector discovery result for: {}, IP={}", uid,
epsonMulticastListener.getIPAddress());
thingDiscovered(DiscoveryResultBuilder.create(thingUid).withProperties(properties)
.withLabel("Epson Projector " + uid).withProperty(THING_PROPERTY_MAC, uid)
.withRepresentationProperty(THING_PROPERTY_MAC).build());
}
}
epsonMulticastListener.shutdown();
logger.debug("Discovery job is exiting");
}
}

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2020 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.binding.epsonprojector.internal.discovery;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MulticastListener} class is responsible for listening for the Epson projector device announcement
* beacons on the multicast address, and then extracting the data fields out of the received datagram.
*
* @author Mark Hilbush - Initial contribution
* @author Michael Lobstein - Adapted for the Epson Projector binding
*/
@NonNullByDefault
public class MulticastListener {
private final Logger logger = LoggerFactory.getLogger(MulticastListener.class);
private MulticastSocket socket;
// Epson-specific properties defined in this binding
private String uid = "";
private String ipAddress = "";
// Epson projector devices announce themselves on a multicast port
private static final String EPSON_MULTICAST_GROUP = "239.255.250.250";
private static final int EPSON_MULTICAST_PORT = 9131;
// How long to wait in milliseconds for a discovery beacon
public static final int DEFAULT_SOCKET_TIMEOUT_SEC = 3000;
/*
* Constructor joins the multicast group, throws IOException on failure.
*/
public MulticastListener(String ipv4Address) throws IOException, SocketException {
InetAddress ifAddress = InetAddress.getByName(ipv4Address);
logger.debug("Discovery job using address {} on network interface {}", ifAddress.getHostAddress(),
NetworkInterface.getByInetAddress(ifAddress).getName());
socket = new MulticastSocket(EPSON_MULTICAST_PORT);
socket.setInterface(ifAddress);
socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_SEC);
InetAddress mcastAddress = InetAddress.getByName(EPSON_MULTICAST_GROUP);
socket.joinGroup(mcastAddress);
logger.debug("Multicast listener joined multicast group {}:{}", EPSON_MULTICAST_GROUP, EPSON_MULTICAST_PORT);
}
public void shutdown() {
logger.debug("Multicast listener closing down multicast socket");
socket.close();
}
/*
* Wait on the multicast socket for an announcement beacon. Return false on socket timeout or error.
* Otherwise, parse the beacon for information about the device.
*/
public boolean waitForBeacon() throws IOException {
byte[] bytes = new byte[600];
boolean beaconFound;
// Wait for a device to announce itself
logger.trace("Multicast listener waiting for datagram on multicast port");
DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length);
try {
socket.receive(msgPacket);
beaconFound = true;
logger.trace("Multicast listener got datagram of length {} from multicast port: {}", msgPacket.getLength(),
msgPacket.toString());
} catch (SocketTimeoutException e) {
beaconFound = false;
}
if (beaconFound) {
// Get the device properties from the announcement beacon
parseAnnouncementBeacon(msgPacket);
}
return beaconFound;
}
/*
* Parse the announcement beacon into the elements needed to create the thing.
*
* Example Epson beacon:
* AMXB<-UUID=000048746B33><-SDKClass=VideoProjector><-GUID=EPSON_EMP001><-Revision=1.0.0>
*/
private void parseAnnouncementBeacon(DatagramPacket packet) {
String beacon = (new String(packet.getData(), StandardCharsets.UTF_8)).trim();
logger.trace("Multicast listener parsing announcement packet: {}", beacon);
clearProperties();
if (beacon.toUpperCase().contains("EPSON") && beacon.toUpperCase().contains("VIDEOPROJECTOR")) {
ipAddress = packet.getAddress().getHostAddress();
parseEpsonAnnouncementBeacon(beacon);
} else {
logger.debug("Multicast listener doesn't know how to parse beacon: {}", beacon);
}
}
private void parseEpsonAnnouncementBeacon(String beacon) {
String[] parameterList = beacon.split("<-");
for (String parameter : parameterList) {
String[] keyValue = parameter.split("=");
if (keyValue.length != 2) {
continue;
}
if (keyValue[0].contains("UUID")) {
uid = keyValue[1].substring(0, keyValue[1].length() - 1);
}
}
}
private void clearProperties() {
uid = "";
ipAddress = "";
}
public String getUID() {
return uid;
}
public String getIPAddress() {
return ipAddress;
}
}

View File

@@ -56,7 +56,8 @@
<thing-type id="projector-tcp">
<label>Epson Projector - TCP/IP</label>
<description>An Epson projector which supports the ESC/VP21 protocol via a serial over IP connection</description>
<description>An Epson projector which supports the ESC/VP21 protocol via the built-in ethernet port or a serial over
IP connection</description>
<channels>
<channel id="power" typeId="power"/>
@@ -89,15 +90,18 @@
<channel id="errmessage" typeId="errmessage"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description>
<parameter name="host" type="text" required="true">
<label>Host</label>
<context>network-address</context>
<description>IP address for the serial over IP device</description>
<description>IP address for the projector or serial over IP device</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="true">
<label>Port</label>
<description>Port for the serial over IP device</description>
<description>Port for the projector or serial over IP device</description>
<default>3629</default>
</parameter>
<parameter name="pollingInterval" type="integer" min="5" max="60" unit="s" required="false">
<label>Polling interval</label>