/** * 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.primare.internal.protocol; import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Date; import java.util.Enumeration; import java.util.Timer; import java.util.TimerTask; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gnu.io.CommPort; import gnu.io.CommPortIdentifier; import gnu.io.NoSuchPortException; import gnu.io.SerialPort; import gnu.io.UnsupportedCommOperationException; /** * Connector for Primare serial communication. * * @author Pauli Anttila, Evert van Es, Veli-Pekka Juslin * @since 1.7.0 */ public class PrimareSerialConnector extends PrimareConnector { private static final Logger logger = LoggerFactory.getLogger(PrimareSerialConnector.class); /** Connection test interval in milliseconds **/ private static final int CONNECTION_TEST_INTERVAL = 15000; // Lock for synchronized sendBytes private final Object oLock = new Object(); private String serialPortName = null; private SerialPort serialPort = null; private DataInputStream inStream = null; private OutputStream outStream = null; private DataListener dataListener = null; private ConnectionSupervisor connectionSupervisor = null; public PrimareSerialConnector(String deviceId, String serialPortName, PrimareMessageFactory mf, PrimareResponseFactory rf) { this.deviceId = deviceId; this.serialPortName = serialPortName; this.messageFactory = mf; this.responseFactory = rf; } public static <T extends PrimareSerialConnector> T newForModel(String m, String deviceId, String serialPortName) { T pc = null; if (m == null) { logger.error("connectorForModel called with null argument"); return null; } if ("SP31.7".equals(m) || "SP31".equals(m) || "SPA20".equals(m) || "SPA21".equals(m)) { pc = (T) new org.openhab.binding.primare.internal.protocol.spa20.PrimareSPA20SerialConnector(deviceId, serialPortName); } else { logger.error("Could not find PrimareSerialConnector for Primare model {m}"); } return pc; } @Override public String toString() { return String.format("[%s on %s]", deviceId, serialPortName); } @Override public void connect() { logger.debug("Connecting to {}", this.toString()); try { connectSerial(); } catch (Exception unimportant) { } // Start connection supervisor regardless of initial connection status if (connectionSupervisor == null) { logger.trace("Starting connection supervisor for {}", this.toString()); connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL); logger.trace("Started connection supervisor for {}", this.toString()); } } @Override public void disconnect() { disconnectSerial(); } @Override public void sendBytes(byte[] msg) throws IOException { if (isConnected()) { logger.trace("Sending (hex) [{}] to {} via Serial", PrimareUtils.byteArrayToHex(msg), this.toString()); try { // Synchronize, since both main and supervisor threads // will be writing to outStream synchronized (oLock) { outStream.write(msg); outStream.flush(); } bytesSentAt = new Date(); logger.trace("Sent and flushed (hex) [{}] to {} via Serial", PrimareUtils.byteArrayToHex(msg), this.toString()); } catch (IOException e) { connectionBroken = true; throw e; } } } @Override public boolean isConnected() { return serialPort != null && !connectionBroken; } private void connectSerial() throws Exception { logger.debug("Initializing serial port {}", serialPortName); try { CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(serialPortName); CommPort commPort = portIdentifier.open(this.getClass().getName(), 2000); serialPort = (SerialPort) commPort; try { serialPort.setSerialPortParams(4800, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); serialPort.enableReceiveThreshold(1); serialPort.disableReceiveTimeout(); } catch (UnsupportedCommOperationException unimportant) { // We might have a perfectly usable PTY even if above operations are unsupported } ; inStream = new DataInputStream(serialPort.getInputStream()); outStream = serialPort.getOutputStream(); outStream.flush(); if (inStream.markSupported()) { inStream.reset(); } logger.debug("Starting DataListener for {}", PrimareSerialConnector.this.toString()); dataListener = new DataListener(); dataListener.start(); logger.debug("Starting DataListener for {}", PrimareSerialConnector.this.toString()); sendInitMessages(); } catch (NoSuchPortException e) { logger.error("No such port: {}", serialPortName); Enumeration portList = CommPortIdentifier.getPortIdentifiers(); if (portList.hasMoreElements()) { StringBuilder sb = new StringBuilder(); while (portList.hasMoreElements()) { CommPortIdentifier portId = (CommPortIdentifier) portList.nextElement(); sb.append(String.format("%s ", portId.getName())); } logger.error("The following communications ports are available: {}", sb.toString().trim()); } else { logger.error("There are no communications ports available"); } logger.error("You may consider OpenHAB startup parameter [ -Dgnu.io.rxtx.SerialPorts={} ]", serialPortName); throw e; } } private void disconnectSerial() { logger.debug("Disconnecting {}", PrimareSerialConnector.this.toString()); if (dataListener != null) { logger.trace("{} interrupt serial listener", PrimareSerialConnector.this.toString()); dataListener.interrupt(); } if (outStream != null) { logger.trace("{} close serial out stream", PrimareSerialConnector.this.toString()); IOUtils.closeQuietly(outStream); } if (inStream != null) { logger.trace("{} close serial in stream", PrimareSerialConnector.this.toString()); IOUtils.closeQuietly(inStream); } if (serialPort != null) { logger.trace("{} close serial port", PrimareSerialConnector.this.toString()); serialPort.close(); } dataListener = null; serialPort = null; outStream = null; inStream = null; logger.debug("Connection to {} closed", PrimareSerialConnector.this.toString()); } public class DataListener extends Thread { boolean interrupted = false; DataListener() { } @Override public void run() { logger.debug("Data listener for {} started", PrimareSerialConnector.this.toString()); // continue running as long as we are connected and no interrupt is requested while (!connectionBroken && !interrupted) { try { waitStateMessages(); } catch (IOException e) { logger.error("Reading from serial port failed", e); // Mark connection broken, supervisor thread will clean up connectionBroken = true; } catch (InterruptedException e) { interrupted = true; } } logger.debug("Data listener for {} stopped", PrimareSerialConnector.this.toString()); } /** * Read bytes from inStream * * @throws IOException * @throws InterruptedException **/ private void waitStateMessages() throws IOException, InterruptedException { logger.debug("Entered waitStateMessages loop for {}", PrimareSerialConnector.this.toString()); while (true) { logger.trace("waitStateMessages - waiting data from {}", PrimareSerialConnector.this.toString()); byte b = inStream.readByte(); bytesReceivedAt = new Date(); buffer[total++] = b; // Access byte[] buffer and consume bytes 0 .. total-1 if DLE ETX has been seen parseData(total - 1); if (Thread.interrupted()) { throw new InterruptedException(); } } } } private class ConnectionSupervisor { private Timer timer; public ConnectionSupervisor(int milliseconds) { logger.debug("Connection {} supervisor started, interval {} milliseconds", PrimareSerialConnector.this.toString(), milliseconds); timer = new Timer(); timer.schedule(new ConnectionSupervisorTask(), milliseconds, milliseconds); } class ConnectionSupervisorTask extends TimerTask { @Override public void run() { logger.debug("Scheduled connection supervisor task started for {}", PrimareSerialConnector.this.toString()); if (connectionBroken) { // unclear connection state, cleanup disconnectSerial(); } if (!isConnected()) { logger.trace("No connection, connecting to {}", PrimareSerialConnector.this.toString()); try { connectSerial(); } catch (Exception unimportant) { logger.trace("Still no connection after retry, failed to connect to {}", PrimareSerialConnector.this.toString()); } } else { try { sendPingMessages(); } catch (Exception unimportant) { logger.trace("Send ping message to {} failed", PrimareSerialConnector.this.toString()); // unclear connection state, cleanup disconnectSerial(); } } } } } }