/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.piface.internal; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import org.openhab.binding.piface.PifaceBindingProvider; import org.openhab.binding.piface.internal.PifaceBindingConfig.BindingType; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.binding.BindingProvider; import org.openhab.core.items.Item; import org.openhab.core.library.items.ContactItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.types.Command; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Binding which communicates with (one or many) Raspberry Pis with PiFace boards. * It registers as listener of the Pis to accomplish real bidirectional communication. * * @author Ben Jones * @since 1.3.0 */ public class PifaceBinding extends AbstractActiveBinding<PifaceBindingProvider>implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(PifaceBinding.class); private static final String CONFIG_KEY_HOST = "host"; private static final String CONFIG_KEY_LISTENER_PORT = "listenerport"; private static final String CONFIG_KEY_MONITOR_PORT = "monitorport"; private static final String CONFIG_KEY_SOCKET_TIMEOUT = "sockettimeout"; private static final String CONFIG_KEY_MAX_RETRIES = "maxretries"; private static final int DEFAULT_LISTENER_PORT = 15432; private static final int DEFAULT_MONITOR_PORT = 15433; private static final int DEFAULT_SOCKET_TIMEOUT_MS = 1000; private static final int DEFAULT_MAX_RETRIES = 3; // list of Piface nodes loaded from the binding configuration private final Map<String, PifaceNode> pifaceNodes = new HashMap<String, PifaceNode>(); // keeps track of all Piface pins which require initial read requests private final List<PifaceBindingConfig> bindingConfigsToInitialise = Collections .synchronizedList(new ArrayList<PifaceBindingConfig>()); // the Piface pin initialiser, which runs in a separate thread private final PifaceInitialiser initialiser = new PifaceInitialiser(); // default watchdog interval (defaults to 1 minute) private long watchdogInterval = 60000L; @Override protected String getName() { return "PiFace Watchdog Service"; } @Override protected long getRefreshInterval() { return watchdogInterval; } /** * @{inheritDoc} */ @Override public void execute() { if (!bindingsExist()) { logger.debug("There is no existing PiFace binding configuration => watchdog cycle aborted!"); return; } // send a watchdog ping to each node for (Map.Entry<String, PifaceNode> entry : pifaceNodes.entrySet()) { String pifaceId = entry.getKey(); PifaceNode node = entry.getValue(); int value = sendWatchdog(node); for (String itemName : getItemNamesForPin(pifaceId, BindingType.WATCHDOG, 0)) { updateItemState(itemName, value); } } } /** * @{inheritDoc} */ @Override public void activate() { initialiser.start(); } /** * @{inheritDoc} */ @Override public void deactivate() { stopMonitors(); pifaceNodes.clear(); providers.clear(); initialiser.setInterrupted(true); } private void startMonitors() { // start the monitor threads for each configured PiFace device for (PifaceNode node : pifaceNodes.values()) { node.startMonitor(); } } private void stopMonitors() { // stop the monitor threads for each configured PiFace device for (PifaceNode node : pifaceNodes.values()) { node.stopMonitor(); } } /** * {@inheritDoc} */ @Override public void bindingChanged(BindingProvider provider, String itemName) { if (provider instanceof PifaceBindingProvider) { PifaceBindingProvider pifaceProvider = (PifaceBindingProvider) provider; PifaceBindingConfig bindingConfig = pifaceProvider.getPifaceBindingConfig(itemName); if (bindingConfig != null && isInitialisableBindingConfig(bindingConfig)) { bindingConfigsToInitialise.add(bindingConfig); } } } /** * {@inheritDoc} */ @Override public void allBindingsChanged(BindingProvider provider) { if (provider instanceof PifaceBindingProvider) { PifaceBindingProvider pifaceProvider = (PifaceBindingProvider) provider; for (String itemName : pifaceProvider.getItemNames()) { PifaceBindingConfig bindingConfig = pifaceProvider.getPifaceBindingConfig(itemName); if (bindingConfig != null && isInitialisableBindingConfig(bindingConfig)) { bindingConfigsToInitialise.add(bindingConfig); } } } } private boolean isInitialisableBindingConfig(PifaceBindingConfig bindingConfig) { // only want to initialise each binding once if (bindingConfigsToInitialise.contains(bindingConfig)) { return false; } // ignore watchdog configs - don't need to initialise these return bindingConfig.getBindingType() != BindingType.WATCHDOG; } private void updateItemState(String itemName, int value) { Class<? extends Item> itemType = getItemType(itemName); if (itemType.equals(SwitchItem.class)) { if (value == 0) { eventPublisher.postUpdate(itemName, OnOffType.OFF); } else if (value == 1) { eventPublisher.postUpdate(itemName, OnOffType.ON); } } if (itemType.equals(ContactItem.class)) { if (value == 0) { eventPublisher.postUpdate(itemName, OpenClosedType.OPEN); } else if (value == 1) { eventPublisher.postUpdate(itemName, OpenClosedType.CLOSED); } } } /** * @{inheritDoc} */ @Override public void internalReceiveCommand(String itemName, Command command) { for (PifaceBindingProvider provider : getProvidersForItemName(itemName)) { PifaceBindingConfig pin = provider.getPifaceBindingConfig(itemName); if (pin == null) { logger.warn("No Piface pin configuration exists for '" + itemName + "'"); continue; } if (pin.getBindingType() == PifaceBindingConfig.BindingType.IN) { logger.warn("Unable to send a command to a Piface 'IN' pin - these pin types are read-only"); continue; } String pifaceId = pin.getPifaceId(); int pinNumber = pin.getPinNumber(); PifaceNode node = pifaceNodes.get(pifaceId); if (node == null) { logger.warn("No Piface node for id " + pifaceId); continue; } try { if (command.equals(OnOffType.ON) || command.equals(OpenClosedType.CLOSED)) { sendDigitalWrite(node, pinNumber, 1); } else { sendDigitalWrite(node, pinNumber, 0); } } catch (ErrorResponseException e) { logger.error("Failed to send digital write to " + node + " pin " + pinNumber); } } } private int sendWatchdog(PifaceNode node) { try { sendCommand(node, PifaceCommand.WATCHDOG_CMD.toByte(), PifaceCommand.WATCHDOG_ACK.toByte(), 0, 0); return 1; } catch (ErrorResponseException e) { return 0; } } private void sendDigitalWrite(PifaceNode node, int pinNumber, int pinValue) throws ErrorResponseException { sendCommand(node, PifaceCommand.DIGITAL_WRITE_CMD.toByte(), PifaceCommand.DIGITAL_WRITE_ACK.toByte(), pinNumber, pinValue); } /** * Request read state of output pins * * @param node The node to read pins on * @return Each bit represents the state of one output pin * @throws ErrorResponseException */ private byte sendReadOutputPins(PifaceNode node) throws ErrorResponseException { return sendCommand(node, PifaceCommand.READ_OUT_CMD.toByte(), PifaceCommand.READ_OUT_ACK.toByte(), 0, 0); } private int sendDigitalRead(PifaceNode node, int pinNumber) { try { byte response = sendCommand(node, PifaceCommand.DIGITAL_READ_CMD.toByte(), PifaceCommand.DIGITAL_READ_ACK.toByte(), pinNumber, 0); return response; } catch (ErrorResponseException e) { return -1; } } private byte sendCommand(PifaceNode node, byte command, byte commandAck, int pinNumber, int pinValue) throws ErrorResponseException { int attempt = 1; while (attempt <= node.maxRetries) { try { return sendCommand(node, command, commandAck, pinNumber, pinValue, attempt); } catch (ErrorResponseException e) { attempt++; } } String msg = "Command failed " + node.maxRetries + " times. Stopping."; logger.warn(msg); throw new ErrorResponseException(msg); } private byte sendCommand(PifaceNode node, byte command, byte commandAck, int pinNumber, int pinValue, int attempt) throws ErrorResponseException { logger.debug("Sending command (" + command + ") to " + node.host + ":" + node.listenerPort + " for pin " + pinNumber + " (value=" + pinValue + ")"); logger.debug("Attempt " + attempt + "..."); DatagramSocket socket = null; try { socket = new DatagramSocket(); socket.setSoTimeout(node.socketTimeout); InetAddress inetAddress = InetAddress.getByName(node.host); // send the packet byte[] sendData = new byte[] { command, (byte) pinNumber, (byte) pinValue }; DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, inetAddress, node.listenerPort); socket.send(sendPacket); // read the response byte[] receiveData = new byte[16]; DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); socket.receive(receivePacket); // check the response is valid if (receiveData[0] == PifaceCommand.ERROR_ACK.toByte()) { String msg = "Error 'ack' received"; logger.error(msg); throw new ErrorResponseException(msg); } if (receiveData[0] != commandAck) { String msg = "Unexpected 'ack' code received - expecting " + commandAck + " but got " + receiveData[0]; logger.error(msg); throw new ErrorResponseException(msg); } if (receiveData[1] != pinNumber) { String msg = "Invalid pin received - expecting " + pinNumber + " but got " + receiveData[1]; logger.error(msg); throw new ErrorResponseException(msg); } // return the data value logger.debug("Command successfully sent and acknowledged (returned " + receiveData[2] + ")"); return receiveData[2]; } catch (IOException e) { String msg = "Failed to send command (" + command + ") to " + node.host + ":" + node.listenerPort + " (attempt " + attempt + ")"; logger.error(msg, e); throw new ErrorResponseException(msg); } finally { if (socket != null) { socket.close(); } } } private List<PifaceBindingProvider> getProvidersForItemName(String itemName) { List<PifaceBindingProvider> providers = new ArrayList<PifaceBindingProvider>(); for (PifaceBindingProvider provider : this.providers) { if (provider.getItemNames().contains(itemName)) { providers.add(provider); } } return providers; } private List<String> getItemNamesForPin(String pifaceId, PifaceBindingConfig.BindingType pinType, int pinNumber) { List<String> itemNames = new ArrayList<String>(); for (PifaceBindingProvider provider : this.providers) { itemNames.addAll(provider.getItemNames(pifaceId, pinType, pinNumber)); } return itemNames; } private Class<? extends Item> getItemType(String itemName) { for (PifaceBindingProvider provider : getProvidersForItemName(itemName)) { return provider.getItemType(itemName); } throw new RuntimeException("Could not determine item type for " + itemName); } protected void addBindingProvider(PifaceBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(PifaceBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } @Override @SuppressWarnings("rawtypes") public void updated(Dictionary config) throws ConfigurationException { // stop any existing monitor threads stopMonitors(); pifaceNodes.clear(); if (config != null) { Enumeration keys = config.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String value = (String) config.get(key); // the config-key enumeration contains additional keys that we // don't want to process here ... if ("service.pid".equals(key)) { continue; } if ("watchdog.interval".equalsIgnoreCase(key)) { watchdogInterval = Integer.parseInt(value); continue; } String[] keyParts = key.split("\\."); String pifaceId = keyParts[0]; String configKey = keyParts[1]; if (!pifaceNodes.containsKey(pifaceId)) { pifaceNodes.put(pifaceId, new PifaceNode(pifaceId)); } PifaceNode pifaceNode = pifaceNodes.get(pifaceId); if (configKey.equals(CONFIG_KEY_HOST)) { pifaceNode.host = value; } else if (configKey.equals(CONFIG_KEY_LISTENER_PORT)) { pifaceNode.listenerPort = Integer.parseInt(value); } else if (configKey.equals(CONFIG_KEY_MONITOR_PORT)) { pifaceNode.monitorPort = Integer.parseInt(value); } else if (configKey.equals(CONFIG_KEY_SOCKET_TIMEOUT)) { pifaceNode.socketTimeout = Integer.parseInt(value); } else if (configKey.equals(CONFIG_KEY_MAX_RETRIES)) { pifaceNode.maxRetries = Integer.parseInt(value); } else { throw new ConfigurationException(key, "Unrecognised configuration parameter: " + configKey); } } // start the monitor threads for each PiFace node startMonitors(); // starts the watch dog thread setProperlyConfigured(true); } } private class PifaceNode { final PifaceNodeMonitor monitor; String host; int listenerPort = DEFAULT_LISTENER_PORT; int monitorPort = DEFAULT_MONITOR_PORT; int socketTimeout = DEFAULT_SOCKET_TIMEOUT_MS; int maxRetries = DEFAULT_MAX_RETRIES; PifaceNode(String pifaceId) { this.monitor = new PifaceNodeMonitor(pifaceId); } void startMonitor() { monitor.setMonitorPort(monitorPort); if (monitor.isAlive()) { return; } monitor.start(); } void stopMonitor() { monitor.setInterrupted(true); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PifaceNode that = (PifaceNode) o; if (listenerPort != that.listenerPort) { return false; } return !(host != null ? !host.equals(that.host) : that.host != null); } @Override public int hashCode() { int result = host != null ? host.hashCode() : 0; result = 31 * result + listenerPort; return result; } } private class PifaceNodeMonitor extends Thread { private final String pifaceId; private int monitorPort = -1; private boolean interrupted = false; public PifaceNodeMonitor(String piFaceId) { super("Piface Monitor Thread [" + piFaceId + "]"); this.pifaceId = piFaceId; } public void setMonitorPort(int monitorPort) { this.monitorPort = monitorPort; } public void setInterrupted(boolean interrupted) { this.interrupted = interrupted; } @Override public void run() { logger.debug(getName() + " started monitoring port " + monitorPort); DatagramSocket socket = null; try { socket = new DatagramSocket(monitorPort); byte[] receiveData = new byte[100]; DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); while (!interrupted) { // block on this port waiting for a message socket.receive(receivePacket); int pinNumber = receiveData[0]; int pinValue = receiveData[1]; logger.debug(getName() + " received message for pin " + pinNumber + " (value=" + pinValue + ")"); for (String itemName : getItemNamesForPin(pifaceId, PifaceBindingConfig.BindingType.IN, pinNumber)) { updateItemState(itemName, pinValue); } } } catch (IOException e) { logger.error(getName() + " failed", e); } finally { if (socket != null) { socket.close(); } } } } /** * The PifaceInitialiser runs as a separate thread. Whenever new Piface pin bindings are added, it takes care that * read * requests are sent to initialise the pin state. * * @author Ben Jones * @since 1.3.0 * */ private class PifaceInitialiser extends Thread { private boolean interrupted = false; public PifaceInitialiser() { super("Piface Initialiser"); } public void setInterrupted(boolean interrupted) { this.interrupted = interrupted; } @Override public void run() { // as long as no interrupt is requested, continue running while (!interrupted) { if (bindingConfigsToInitialise.size() > 0) { // we first clone the list, so that it stays unmodified ArrayList<PifaceBindingConfig> clonedList = new ArrayList<PifaceBindingConfig>( bindingConfigsToInitialise); initialiseBindingConfigs(clonedList); } // just wait before looping again try { sleep(1000L); } catch (InterruptedException e) { interrupted = true; } } } private void initialiseBindingConfigs(ArrayList<PifaceBindingConfig> clonedList) { // Read output pins state only once pr node Map<PifaceNode, Byte> outputPinsOnNode = new HashMap<PifaceNode, Byte>(); for (PifaceBindingConfig bindingConfig : clonedList) { try { // the Piface node might not have been read from the binding config yet so just skip // and we will try again on the next loop PifaceNode node = pifaceNodes.get(bindingConfig.getPifaceId()); if (node == null) { continue; } // Handling IN pin if (bindingConfig.getBindingType() == BindingType.IN) { int value = sendDigitalRead(node, bindingConfig.getPinNumber()); updateItemStates(bindingConfig, value); } // Handling OUT pin if (bindingConfig.getBindingType() == BindingType.OUT) { Byte value = outputPinsOnNode.get(node); if (value == null) { value = sendReadOutputPins(node); outputPinsOnNode.put(node, value); } byte onOffValueForPin = (byte) ((value >>> bindingConfig.getPinNumber()) & 1); logger.debug("State of output pin " + bindingConfig.getPinNumber() + " is " + onOffValueForPin); updateItemStates(bindingConfig, onOffValueForPin); } bindingConfigsToInitialise.remove(bindingConfig); } catch (Exception e) { logger.warn("Failed to initialise value for Piface pin {} ({}): {}", new Object[] { bindingConfig.getPinNumber(), bindingConfig.getPifaceId(), e.getMessage() }); } } } private void updateItemStates(PifaceBindingConfig bindingConfig, int value) { for (String itemName : getItemNamesForPin(bindingConfig.getPifaceId(), bindingConfig.getBindingType(), bindingConfig.getPinNumber())) { updateItemState(itemName, value); } } } }