/**
* 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.insteonplm.internal.driver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
import org.openhab.binding.insteonplm.internal.device.DeviceType;
import org.openhab.binding.insteonplm.internal.device.DeviceTypeLoader;
import org.openhab.binding.insteonplm.internal.device.InsteonAddress;
import org.openhab.binding.insteonplm.internal.device.InsteonDevice;
import org.openhab.binding.insteonplm.internal.device.ModemDBBuilder;
import org.openhab.binding.insteonplm.internal.message.FieldException;
import org.openhab.binding.insteonplm.internal.message.Msg;
import org.openhab.binding.insteonplm.internal.message.MsgFactory;
import org.openhab.binding.insteonplm.internal.message.MsgListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Port class represents a port, that is a connection to either an Insteon modem either through
* a serial or USB port, or via an Insteon Hub.
* It does the initialization of the port, and (via its inner classes IOStreamReader and IOStreamWriter)
* manages the reading/writing of messages on the Insteon network.
*
* The IOStreamReader and IOStreamWriter class combined implement the somewhat tricky flow control protocol.
* In combination with the MsgFactory class, the incoming data stream is turned into a Msg structure
* for further processing by the upper layers (MsgListeners).
*
* A write queue is maintained to pace the flow of outgoing messages. Sending messages back-to-back
* can lead to dropped messages.
*
*
* @author Bernd Pfrommer
* @author Daniel Pfrommer
* @since 1.5.0
*/
public class Port {
private static final Logger logger = LoggerFactory.getLogger(Port.class);
/**
* The ReplyType is used to keep track of the state of the serial port receiver
*/
enum ReplyType {
GOT_ACK,
WAITING_FOR_ACK,
GOT_NACK
}
private IOStream m_ioStream = null;
private String m_devName = "INVALID";
private String m_logName = "INVALID";
private Modem m_modem = null;
private IOStreamReader m_reader = null;
private IOStreamWriter m_writer = null;
private final int m_readSize = 1024; // read buffer size
private Thread m_readThread = null;
private Thread m_writeThread = null;
private boolean m_running = false;
private boolean m_modemDBComplete = false;
private MsgFactory m_msgFactory = new MsgFactory();
private Driver m_driver = null;
private ModemDBBuilder m_mdbb = null;
private ArrayList<MsgListener> m_listeners = new ArrayList<MsgListener>();
private LinkedBlockingQueue<Msg> m_writeQueue = new LinkedBlockingQueue<Msg>();
/**
* Constructor
*
* @param devName the name of the port, i.e. '/dev/insteon'
* @param d The Driver object that manages this port
*/
public Port(String devName, Driver d) {
m_devName = devName;
m_driver = d;
m_logName = devName;
m_modem = new Modem();
addListener(m_modem);
m_ioStream = IOStream.s_create(devName);
m_reader = new IOStreamReader();
m_writer = new IOStreamWriter();
m_mdbb = new ModemDBBuilder(this);
}
public synchronized boolean isModemDBComplete() {
return (m_modemDBComplete);
}
public boolean isRunning() {
return m_running;
}
public InsteonAddress getAddress() {
return m_modem.getAddress();
}
public String getDeviceName() {
return m_devName;
}
public Driver getDriver() {
return m_driver;
}
public void setModemDBRetryTimeout(int timeout) {
m_mdbb.setRetryTimeout(timeout);
}
public void addListener(MsgListener l) {
synchronized (m_listeners) {
if (!m_listeners.contains(l)) {
m_listeners.add(l);
}
}
}
public void removeListener(MsgListener l) {
synchronized (m_listeners) {
if (m_listeners.remove(l)) {
// logger.debug("removed listener from port");
}
}
}
/**
* Clear modem database that has been queried so far.
*/
public void clearModemDB() {
logger.debug("clearing modem db!");
HashMap<InsteonAddress, ModemDBEntry> dbes = getDriver().lockModemDBEntries();
dbes.clear();
getDriver().unlockModemDBEntries();
}
/**
* Starts threads necessary for reading and writing
*/
public void start() {
logger.debug("starting port {}", m_logName);
if (m_running) {
logger.debug("port {} already running, not started again", m_logName);
}
if (!m_ioStream.open()) {
logger.debug("failed to open port {}", m_logName);
return;
}
m_readThread = new Thread(m_reader);
m_writeThread = new Thread(m_writer);
m_readThread.setName(m_logName + " Reader");
m_writeThread.setName(m_logName + " Writer");
m_readThread.start();
m_writeThread.start();
m_modem.initialize();
m_mdbb.start(); // start downloading the device list
m_running = true;
}
/**
* Stops all threads
*/
public void stop() {
if (!m_running) {
logger.debug("port {} not running, no need to stop it", m_logName);
return;
}
if (m_mdbb != null) {
m_mdbb = null;
}
if (m_readThread != null) {
m_readThread.interrupt();
}
if (m_writeThread != null) {
m_writeThread.interrupt();
}
logger.debug("waiting for read thread to exit for port {}", m_logName);
try {
if (m_readThread != null) {
m_readThread.join();
}
} catch (InterruptedException e) {
logger.debug("got interrupted waiting for read thread to exit.");
}
logger.debug("waiting for write thread to exit for port {}", m_logName);
try {
if (m_writeThread != null) {
m_writeThread.join();
}
} catch (InterruptedException e) {
logger.debug("got interrupted waiting for write thread to exit.");
}
logger.debug("all threads for port {} stopped.", m_logName);
m_ioStream.close();
m_running = false;
synchronized (m_listeners) {
m_listeners.clear();
}
}
/**
* Adds message to the write queue
*
* @param m message to be added to the write queue
* @throws IOException
*/
public void writeMessage(Msg m) throws IOException {
if (m == null) {
logger.error("trying to write null message!");
throw new IOException("trying to write null message!");
}
if (m.getData() == null) {
logger.error("trying to write message without data!");
throw new IOException("trying to write message without data!");
}
try {
m_writeQueue.add(m);
logger.trace("enqueued msg: {}", m);
} catch (IllegalStateException e) {
logger.error("cannot write message {}, write queue is full!", m);
}
}
/**
* Gets called by the modem database builder when the modem database is complete
*/
public void modemDBComplete() {
synchronized (this) {
m_modemDBComplete = true;
}
m_driver.modemDBComplete(this);
}
/**
* The IOStreamReader uses the MsgFactory to turn the incoming bytes into
* Msgs for the listeners. It also communicates with the IOStreamWriter
* to implement flow control (tell the IOStreamWriter that it needs to retransmit,
* or the reply message has been received correctly).
*
* @author Bernd Pfrommer
*/
class IOStreamReader implements Runnable {
private ReplyType m_reply = ReplyType.GOT_ACK;
private Object m_replyLock = new Object();
private boolean m_dropRandomBytes = false; // set to true for fault injection
/**
* Helper function for implementing synchronization between reader and writer
*
* @return reference to the RequesReplyLock
*/
public Object getRequestReplyLock() {
return m_replyLock;
}
@Override
public void run() {
logger.debug("starting reader...");
byte[] buffer = new byte[2 * m_readSize];
Random rng = new Random();
try {
for (int len = -1; (len = m_ioStream.read(buffer, 0, m_readSize)) > 0;) {
if (m_dropRandomBytes && rng.nextInt(100) < 20) {
len = dropBytes(buffer, len);
}
m_msgFactory.addData(buffer, len);
processMessages();
}
} catch (InterruptedException e) {
logger.debug("reader thread got interrupted!");
}
logger.error("reader thread exiting!");
}
private void processMessages() {
try {
// must call processData() until we get a null pointer back
for (Msg m = m_msgFactory.processData(); m != null; m = m_msgFactory.processData()) {
toAllListeners(m);
notifyWriter(m);
}
} catch (IOException e) {
// got bad data from modem,
// unblock those waiting for ack
logger.warn("bad data received: {}", e.getMessage());
synchronized (getRequestReplyLock()) {
if (m_reply == ReplyType.WAITING_FOR_ACK) {
logger.warn("got bad data back, must assume message was acked.");
m_reply = ReplyType.GOT_ACK;
getRequestReplyLock().notify();
}
}
}
}
private void notifyWriter(Msg msg) {
synchronized (getRequestReplyLock()) {
if (m_reply == ReplyType.WAITING_FOR_ACK) {
if (!msg.isUnsolicited()) {
m_reply = (msg.isPureNack() ? ReplyType.GOT_NACK : ReplyType.GOT_ACK);
logger.trace("signaling receipt of ack: {}", (m_reply == ReplyType.GOT_ACK));
getRequestReplyLock().notify();
} else if (msg.isPureNack()) {
m_reply = ReplyType.GOT_NACK;
logger.trace("signaling receipt of pure nack");
getRequestReplyLock().notify();
} else {
logger.trace("got unsolicited message");
}
}
}
}
/**
* Drops bytes randomly from buffer to simulate errors seen
* from the InsteonHub using the raw interface
*
* @param buffer byte buffer from which to drop bytes
* @param len original number of valid bytes in buffer
* @return length of byte buffer after dropping from it
*/
private int dropBytes(byte[] buffer, int len) {
final int DROP_RATE = 2; // in percent
Random rng = new Random();
ArrayList<Byte> l = new ArrayList<Byte>();
for (int i = 0; i < len; i++) {
if (rng.nextInt(100) >= DROP_RATE) {
l.add(new Byte(buffer[i]));
}
}
for (int i = 0; i < l.size(); i++) {
buffer[i] = l.get(i);
}
return (l.size());
}
@SuppressWarnings("unchecked")
private void toAllListeners(Msg msg) {
// When we deliver the message, the recipient
// may in turn call removeListener() or addListener(),
// thereby corrupting the very same list we are iterating
// through. That's why we make a copy of it, and
// iterate through the copy.
ArrayList<MsgListener> tempList = null;
synchronized (m_listeners) {
tempList = (ArrayList<MsgListener>) m_listeners.clone();
}
for (MsgListener l : tempList) {
l.msg(msg, m_devName); // deliver msg to listener
}
}
/**
* Blocking wait for ack or nack from modem.
* Called by IOStreamWriter for flow control.
*
* @return true if retransmission is necessary
*/
public boolean waitForReply() {
m_reply = ReplyType.WAITING_FOR_ACK;
while (m_reply == ReplyType.WAITING_FOR_ACK) {
try {
logger.trace("writer waiting for ack.");
// There have been cases observed, in particular for
// the Hub, where we get no ack or nack back, causing the binding
// to hang in the wait() below, because unsolicited messages
// do not trigger a notify(). For this reason we request retransmission
// if the wait() times out.
getRequestReplyLock().wait(30000); // be patient for 30 msec
if (m_reply == ReplyType.WAITING_FOR_ACK) { // timeout expired without getting ACK or NACK
logger.trace("writer timeout expired, asking for retransmit!");
m_reply = ReplyType.GOT_NACK;
break;
} else {
logger.trace("writer got ack: {}", (m_reply == ReplyType.GOT_ACK));
}
} catch (InterruptedException e) {
break; // done for the day...
}
}
return (m_reply == ReplyType.GOT_NACK);
}
}
/**
* Writes messages to the port. Flow control is implemented following Insteon
* documents to avoid over running the modem.
*
* @author Bernd Pfrommer
*/
class IOStreamWriter implements Runnable {
private static final int WAIT_TIME = 200; // milliseconds
@Override
public void run() {
logger.debug("starting writer...");
while (true) {
try {
// this call blocks until the lock on the queue is released
logger.trace("writer checking message queue");
Msg msg = m_writeQueue.take();
if (msg.getData() == null) {
logger.error("found null message in write queue!");
} else {
logger.debug("writing ({}): {}", msg.getQuietTime(), msg);
// To debug race conditions during startup (i.e. make the .items
// file definitions be available *before* the modem link records,
// slow down the modem traffic with the following statement:
// Thread.sleep(500);
synchronized (m_reader.getRequestReplyLock()) {
m_ioStream.write(msg.getData());
while (m_reader.waitForReply()) {
Thread.sleep(WAIT_TIME);
logger.trace("retransmitting msg: {}", msg);
m_ioStream.write(msg.getData());
}
}
// if rate limited, need to sleep now.
if (msg.getQuietTime() > 0) {
Thread.sleep(msg.getQuietTime());
}
}
} catch (InterruptedException e) {
logger.error("got interrupted exception in write thread");
break;
} catch (Exception e) {
logger.error("got exception in write thread:", e);
}
}
logger.debug("exiting writer thread!");
}
}
/**
* Class to get info about the modem
*/
class Modem implements MsgListener {
private InsteonDevice m_device = null;
InsteonAddress getAddress() {
return (m_device == null) ? new InsteonAddress() : (m_device.getAddress());
}
InsteonDevice getDevice() {
return m_device;
}
@Override
public void msg(Msg msg, String fromPort) {
try {
if (msg.isPureNack()) {
return;
}
if (msg.getByte("Cmd") == 0x60) {
// add the modem to the device list
InsteonAddress a = new InsteonAddress(msg.getAddress("IMAddress"));
String prodKey = "0x000045";
DeviceType dt = DeviceTypeLoader.s_instance().getDeviceType(prodKey);
if (dt == null) {
logger.error("unknown modem product key: {} for modem: {}.", prodKey, a);
} else {
m_device = InsteonDevice.s_makeDevice(dt);
m_device.setAddress(a);
m_device.setProductKey(prodKey);
m_device.setDriver(m_driver);
m_device.setIsModem(true);
m_device.addPort(fromPort);
logger.debug("found modem {} in device_types: {}", a, m_device.toString());
m_mdbb.updateModemDB(a, Port.this, null);
}
// can unsubscribe now
removeListener(this);
}
} catch (FieldException e) {
logger.error("error parsing im info reply field: ", e);
}
}
public void initialize() {
try {
Msg m = Msg.s_makeMessage("GetIMInfo");
writeMessage(m);
} catch (IOException e) {
logger.error("modem init failed!", e);
}
}
}
}