/** * 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.zwave.internal.protocol; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang.ArrayUtils; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveCommandClass.CommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveSecurityCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveWakeUpCommandClass; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class represents a message which is used in serial API * interface to communicate with usb Z-Wave stick * * A ZWave serial message frame is made up as follows * Byte 0 : SOF (Start of Frame) 0x01 * Byte 1 : Length of frame - number of bytes to follow * Byte 2 : Request (0x00) or Response (0x01) * Byte 3 : Message Class (see SerialMessageClass) * Byte 4+: Message Class data >> Message Payload * Byte x : Last byte is checksum * * @author Victor Belov * @author Brian Crosby * @author Chris Jackson * @since 1.3.0 */ public class SerialMessage { private static final Logger logger = LoggerFactory.getLogger(SerialMessage.class); private final static AtomicLong sequence = new AtomicLong(); public static final int TRANSMIT_OPTIONS_NOT_SET = 0; private long sequenceNumber; private byte[] messagePayload; private int messageLength = 0; private SerialMessageType messageType; private SerialMessageClass messageClass; private SerialMessagePriority priority; private SerialMessageClass expectedReply; protected int messageNode = 255; private int transmitOptions = TRANSMIT_OPTIONS_NOT_SET; private int callbackId = 0; private boolean transactionCanceled = false; private boolean ackPending = false; /** * Indicates whether the serial message is valid. */ public boolean isValid = false; /** * Indicates the number of retry attempts left */ public int attempts = 3; /** * Constructor. Creates a new instance of the SerialMessage class. */ public SerialMessage() { logger.trace("Creating empty message"); messagePayload = new byte[] {}; } /** * Constructor. Creates a new instance of the SerialMessage class using the * specified message class and message type. An expected reply can be given * to indicate that a transaction is complete. The priority indicates the * priority to send the message with. Higher priority messages are taken from * the send queue earlier than lower priority messages. * * @param messageClass the message class to use * @param messageType the message type to use * @param expectedReply the expected Reply for this messaage * @param priority the message priority */ public SerialMessage(SerialMessageClass messageClass, SerialMessageType messageType, SerialMessageClass expectedReply, SerialMessagePriority priority) { this(255, messageClass, messageType, expectedReply, priority); } /** * Constructor. Creates a new instance of the SerialMessage class using the * specified message class and message type. An expected reply can be given * to indicate that a transaction is complete. The priority indicates the * priority to send the message with. Higher priority messages are taken from * the send queue earlier than lower priority messages. * * @param nodeId the node the message is destined for * @param messageClass the message class to use * @param messageType the message type to use * @param expectedReply the expected Reply for this messaage * @param priority the message priority */ public SerialMessage(int nodeId, SerialMessageClass messageClass, SerialMessageType messageType, SerialMessageClass expectedReply, SerialMessagePriority priority) { logger.debug(String.format("NODE %d: Creating empty message of class = %s (0x%02X), type = %s (0x%02X)", new Object[] { nodeId, messageClass, messageClass.key, messageType, messageType.ordinal() })); this.sequenceNumber = sequence.getAndIncrement(); this.messageClass = messageClass; this.messageType = messageType; this.messagePayload = new byte[] {}; this.messageNode = nodeId; this.expectedReply = expectedReply; this.priority = priority; } /** * Constructor. Creates a new instance of the SerialMessage class from a * specified buffer. * * @param buffer the buffer to create the SerialMessage from. */ public SerialMessage(byte[] buffer) { this(255, buffer); } /** * Constructor. Creates a new instance of the SerialMessage class from a * specified buffer, and subsequently sets the node ID. * * @param nodeId the node the message is destined for * @param buffer the buffer to create the SerialMessage from. */ public SerialMessage(int nodeId, byte[] buffer) { logger.trace("NODE {}: Creating new SerialMessage from buffer = {}", nodeId, SerialMessage.bb2hex(buffer)); messageLength = buffer.length - 2; // buffer[1]; byte messageCheckSumm = calculateChecksum(buffer); byte messageCheckSummReceived = buffer[messageLength + 1]; if (messageCheckSumm == messageCheckSummReceived) { logger.trace("NODE {}: Checksum matched", nodeId); isValid = true; } else { logger.trace("NODE {}: Checksum error. Calculated = 0x%02X, Received = 0x%02X", nodeId, messageCheckSumm, messageCheckSummReceived); isValid = false; return; } this.priority = SerialMessagePriority.High; this.messageType = buffer[2] == 0x00 ? SerialMessageType.Request : SerialMessageType.Response; this.messageClass = SerialMessageClass.getMessageClass(buffer[3] & 0xFF); this.messagePayload = ArrayUtils.subarray(buffer, 4, messageLength + 1); this.messageNode = nodeId; logger.trace("NODE {}: Message payload = {}", getMessageNode(), SerialMessage.bb2hex(messagePayload)); } /** * Converts a byte array to a hexadecimal string representation * * @param bb the byte array to convert * @return string the string representation */ static public String bb2hex(byte[] bb) { StringBuilder result = new StringBuilder(); for (int i = 0; i < bb.length; i++) { result.append(String.format("%02X ", bb[i])); } return result.toString(); } /** * Converts a byte to a hexadecimal string representation * * @param bb the byte to convert * @return string the string representation */ public static String b2hex(byte b) { return String.format("%02X ", b); } /** * Calculates a checksum for the specified buffer. * * @param buffer the buffer to calculate. * @return the checksum value. */ private static byte calculateChecksum(byte[] buffer) { byte checkSum = (byte) 0xFF; for (int i = 1; i < buffer.length - 1; i++) { checkSum = (byte) (checkSum ^ buffer[i]); } logger.trace(String.format("Calculated checksum = 0x%02X", checkSum)); return checkSum; } /** * Returns a string representation of this SerialMessage object. * The string contains message class, message type and buffer contents. * {@inheritDoc} */ @Override public String toString() { return String.format("Message: class = %s (0x%02X), type = %s (0x%02X), payload = %s, callbackid = %s", new Object[] { messageClass, messageClass.key, messageType, messageType.ordinal(), SerialMessage.bb2hex(this.getMessagePayload()), getCallbackId() }); }; /** * Gets the SerialMessage as a byte array. * * @return the message */ public byte[] getMessageBuffer() { ByteArrayOutputStream resultByteBuffer = new ByteArrayOutputStream(); byte[] result; resultByteBuffer.write((byte) 0x01); int messageLength = messagePayload.length + (this.messageClass == SerialMessageClass.SendData && this.messageType == SerialMessageType.Request ? 5 : 3); // calculate and set length resultByteBuffer.write((byte) messageLength); resultByteBuffer.write((byte) messageType.ordinal()); resultByteBuffer.write((byte) messageClass.getKey()); try { resultByteBuffer.write(messagePayload); } catch (IOException e) { logger.error("Error getting message buffer: ", e); } // Callback ID and transmit options for a Send Data message. if (this.messageClass == SerialMessageClass.SendData && this.messageType == SerialMessageType.Request) { resultByteBuffer.write(transmitOptions); resultByteBuffer.write(callbackId); } // Make space in the array for the checksum resultByteBuffer.write((byte) 0x00); // Convert to a byte array result = resultByteBuffer.toByteArray(); // Calculate the checksum result[result.length - 1] = 0x01; result[result.length - 1] = calculateChecksum(result); logger.debug("Assembled message buffer = " + SerialMessage.bb2hex(result)); return result; } /** * Check whether an object is equal to this serial message. * A serial message is considered equal when: * - the object passed in is a serial message. * - the message class is equal * - the message type is equal * - the expected reply is equal * - the payload is equal * * @param obj the object to compare this message with. */ @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!obj.getClass().equals(this.getClass())) { return false; } SerialMessage other = (SerialMessage) obj; if (other.messageClass != this.messageClass) { return false; } if (other.messageType != this.messageType) { return false; } if (other.expectedReply != this.expectedReply) { return false; } return Arrays.equals(other.messagePayload, this.messagePayload); } /** * Gets the message type (Request / Response). * * @return the message type */ public SerialMessageType getMessageType() { return messageType; } /** * Gets the message class. This is the function it represents. * * @return */ public SerialMessageClass getMessageClass() { return messageClass; } /** * Returns the Node Id for / from this message. * * @return the messageNode */ public int getMessageNode() { return messageNode; } /** * Gets the message payload. * * @return the message payload */ public byte[] getMessagePayload() { return messagePayload; } /** * Gets a byte of the message payload at the specified index. * The byte is returned as an integer between 0x00 (0) and 0xFF (255). * * @param index the index of the byte to return. * @return an integer between 0x00 (0) and 0xFF (255). */ public int getMessagePayloadByte(int index) { return messagePayload[index] & 0xFF; } /** * Sets the message payload. * * @param messagePayload */ public void setMessagePayload(byte[] messagePayload) { this.messagePayload = messagePayload; } /** * Gets the transmit options for this SendData Request. * * @return the transmitOptions */ public int getTransmitOptions() { return transmitOptions; } /** * Sets the transmit options for this SendData Request. * * @param transmitOptions the transmitOptions to set */ public void setTransmitOptions(int transmitOptions) { this.transmitOptions = transmitOptions; } /** * Gets the callback ID for this SendData Request. * * @return the callbackId */ public int getCallbackId() { return callbackId; } /** * Sets the callback ID for this SendData Request * * @param callbackId the callbackId to set */ public void setCallbackId(int callbackId) { this.callbackId = callbackId; } /** * Gets the expected reply for this message. * * @return the expectedReply */ public SerialMessageClass getExpectedReply() { return expectedReply; } /** * Returns the priority of this Serial message. * * @return the priority */ public SerialMessagePriority getPriority() { return priority; } /** * Sets the priority of this Serial message. * * @param p the new priority */ public void setPriority(SerialMessagePriority p) { priority = p; } /** * Indicates that the transaction for the incoming message is canceled by a command class */ public void setTransactionCanceled() { transactionCanceled = true; } /** * Indicates that the transaction for the incoming message is canceled by a command class * * @return the transactionCanceled */ public boolean isTransactionCanceled() { return transactionCanceled; } /** * Sets the ACK as received. */ public void setAckRecieved() { logger.trace("Ack Pending cleared"); this.ackPending = false; } /** * If we require an ACK from the controller, then set true */ public void setAckRequired() { this.ackPending = true; } /** * Returns true is there is an ack pending from the controller * * @return true if still waiting on the ack */ public boolean isAckPending() { return this.ackPending; } /** * Sets the flag to say the ack has been received from the controller. * This ensures that we don't complete a transaction if we receive the final * response from the device before the controller acks our request. * This seems to be possible from some devices, or possibly if the device * happens to send the data we're about to request at the same time we * request it (since the data received from a device as part of a * transaction is NOT linked in any way to the transaction). */ public void setTransactionAcked() { this.ackPending = false; } /** * Identifies if transmit options have been set yet for this SendData Req * * @return true if they were set */ public boolean areTransmitOptionsSet() { return transmitOptions != TRANSMIT_OPTIONS_NOT_SET; } /** * Serial message type enumeration. Indicates whether the message * is a request or a response. * * @author Jan-Willem Spuij * @since 1.3.0 */ public enum SerialMessageType { Request, // 0x00 Response // 0x01 } /** * Serial message priority enumeration. Indicates the message priority. * Queue priority concept -: * Immediate: Messages that must be sent at highest priority. * Generally this is reserved for battery devices so we send * messages while they are awake. The high priority allows their * messages to jump the queue. * High: Other high priority messages * Set: Messages relating to SET commands. * This should only be used for SET type commands that need to * be responsive - eg light switches, or things that are expected * to occur quickly. * Get: Messages relating to GET commands. * Most messages relating to reading data use this priority. * Config: Messages relating to system configuration. * This can be GET or SET type commands, but these are things that * don't need to be responsive. * Poll: Messages relating to polling. * Normally these are GET commands, but the system overrides the * priority to the lowest so they don't cause any impact on the * system. * * @author Jan-Willem Spuij * @author Chris Jackson * @since 1.3.0 */ public enum SerialMessagePriority { Immediate, High, Set, Get, Config, Poll } /** * Serial message class enumeration. Enumerates the different messages * that can be exchanged with the controller. * * @author Jan-Willem Spuij * @since 1.3.0 */ public enum SerialMessageClass { SerialApiGetInitData(0x02, "SerialApiGetInitData"), // Request initial information about devices in network SerialApiApplicationNodeInfo(0x03, "SerialApiApplicationNodeInfo"), // Set controller node information ApplicationCommandHandler(0x04, "ApplicationCommandHandler"), // Handle application command GetControllerCapabilities(0x05, "GetControllerCapabilities"), // Request controller capabilities (primary role, // SUC/SIS availability) SerialApiSetTimeouts(0x06, "SerialApiSetTimeouts"), // Set Serial API timeouts SerialApiGetCapabilities(0x07, "SerialApiGetCapabilities"), // Request Serial API capabilities SerialApiSoftReset(0x08, "SerialApiSoftReset"), // Soft reset. Restarts Z-Wave chip RfReceiveMode(0x10, "RfReceiveMode"), // Power down the RF section of the stick SetSleepMode(0x11, "SetSleepMode"), // Set the CPU into sleep mode SendNodeInfo(0x12, "SendNodeInfo"), // Send Node Information Frame of the stick SendData(0x13, "SendData"), // Send data. SendDataMulti(0x14, "SendDataMulti"), GetVersion(0x15, "GetVersion"), // Request controller hardware version SendDataAbort(0x16, "SendDataAbort"), // Abort Send data. RfPowerLevelSet(0x17, "RfPowerLevelSet"), // Set RF Power level SendDataMeta(0x18, "SendDataMeta"), GetRandom(0x1c, "GetRandom"), // ??? MemoryGetId(0x20, "MemoryGetId"), // ??? MemoryGetByte(0x21, "MemoryGetByte"), // Get a byte of memory. MemoryPutByte(0x22, "MemoryPutByte"), ReadMemory(0x23, "ReadMemory"), // Read memory. WriteMemory(0x24, "WriteMemory"), SetLearnNodeState(0x40, "SetLearnNodeState"), // ??? IdentifyNode(0x41, "IdentifyNode"), // Get protocol info (baud rate, listening, etc.) for a given node SetDefault(0x42, "SetDefault"), // Reset controller and node info to default (original) values NewController(0x43, "NewController"), // ??? ReplicationCommandComplete(0x44, "ReplicationCommandComplete"), // Replication send data complete ReplicationSendData(0x45, "ReplicationSendData"), // Replication send data AssignReturnRoute(0x46, "AssignReturnRoute"), // Assign a return route from the specified node to the controller DeleteReturnRoute(0x47, "DeleteReturnRoute"), // Delete all return routes from the specified node RequestNodeNeighborUpdate(0x48, "RequestNodeNeighborUpdate"), // Ask the specified node to update its neighbors // (then read them from the controller) ApplicationUpdate(0x49, "ApplicationUpdate"), // Get a list of supported (and controller) command classes AddNodeToNetwork(0x4a, "AddNodeToNetwork"), // Control the addnode (or addcontroller) process...start, stop, // etc. RemoveNodeFromNetwork(0x4b, "RemoveNodeFromNetwork"), // Control the removenode (or removecontroller) // process...start, stop, etc. CreateNewPrimary(0x4c, "CreateNewPrimary"), // Control the createnewprimary process...start, stop, etc. ControllerChange(0x4d, "ControllerChange"), // Control the transferprimary process...start, stop, etc. SetLearnMode(0x50, "SetLearnMode"), // Put a controller into learn mode for replication/ receipt of // configuration info AssignSucReturnRoute(0x51, "AssignSucReturnRoute"), // Assign a return route to the SUC EnableSuc(0x52, "EnableSuc"), // Make a controller a Static Update Controller RequestNetworkUpdate(0x53, "RequestNetworkUpdate"), // Network update for a SUC(?) SetSucNodeID(0x54, "SetSucNodeID"), // Identify a Static Update Controller node id DeleteSUCReturnRoute(0x55, "DeleteSUCReturnRoute"), // Remove return routes to the SUC GetSucNodeId(0x56, "GetSucNodeId"), // Try to retrieve a Static Update Controller node id (zero if no SUC // present) SendSucId(0x57, "SendSucId"), RequestNodeNeighborUpdateOptions(0x5a, "RequestNodeNeighborUpdateOptions"), // Allow options for request node // neighbor update RequestNodeInfo(0x60, "RequestNodeInfo"), // Get info (supported command classes) for the specified node RemoveFailedNodeID(0x61, "RemoveFailedNodeID"), // Mark a specified node id as failed IsFailedNodeID(0x62, "IsFailedNodeID"), // Check to see if a specified node has failed ReplaceFailedNode(0x63, "ReplaceFailedNode"), // Remove a failed node from the controller's list (?) GetRoutingInfo(0x80, "GetRoutingInfo"), // Get a specified node's neighbor information from the controller LockRoute(0x90, "LockRoute"), SerialApiSlaveNodeInfo(0xA0, "SerialApiSlaveNodeInfo"), // Set application virtual slave node information ApplicationSlaveCommandHandler(0xA1, "ApplicationSlaveCommandHandler"), // Slave command handler SendSlaveNodeInfo(0xA2, "ApplicationSlaveCommandHandler"), // Send a slave node information frame SendSlaveData(0xA3, "SendSlaveData"), // Send data from slave SetSlaveLearnMode(0xA4, "SetSlaveLearnMode"), // Enter slave learn mode GetVirtualNodes(0xA5, "GetVirtualNodes"), // Return all virtual nodes IsVirtualNode(0xA6, "IsVirtualNode"), // Virtual node test WatchDogEnable(0xB6, "WatchDogEnable"), WatchDogDisable(0xB7, "WatchDogDisable"), WatchDogKick(0xB6, "WatchDogKick"), RfPowerLevelGet(0xBA, "RfPowerLevelSet"), // Get RF Power level GetLibraryType(0xBD, "GetLibraryType"), // Gets the type of ZWave library on the stick SendTestFrame(0xBE, "SendTestFrame"), // Send a test frame to a node GetProtocolStatus(0xBF, "GetProtocolStatus"), SetPromiscuousMode(0xD0, "SetPromiscuousMode"), // Set controller into promiscuous mode to listen to all frames PromiscuousApplicationCommandHandler(0xD1, "PromiscuousApplicationCommandHandler"); /** * A mapping between the integer code and its corresponding ZWaveMessage * value to facilitate lookup by code. */ private static Map<Integer, SerialMessageClass> codeToMessageClassMapping; private int key; private String label; private SerialMessageClass(int key, String label) { this.key = key; this.label = label; } private static void initMapping() { codeToMessageClassMapping = new HashMap<Integer, SerialMessageClass>(); for (SerialMessageClass s : values()) { codeToMessageClassMapping.put(s.key, s); } } /** * Lookup function based on the generic device class code. * * @param i the code to lookup * @return enumeration value of the generic device class. */ public static SerialMessageClass getMessageClass(int i) { if (codeToMessageClassMapping == null) { initMapping(); } return codeToMessageClassMapping.get(i); } /** * Returns the enumeration key. * * @return the key */ public int getKey() { return key; } /** * Returns the enumeration label. * * @return the label */ public String getLabel() { return label; } } /** * Comparator Class. Compares two serial messages with each other based on * node status (awake / sleep), priority and sequence number. * * @author Jan-Willem Spuij * @since 1.3.0 */ public static class SerialMessageComparator implements Comparator<SerialMessage> { private final ZWaveController controller; /** * Constructor. Creates a new instance of the SerialMessageComparator class. * * @param controller the {@link ZWaveController to use} */ public SerialMessageComparator(ZWaveController controller) { this.controller = controller; } /** * Compares a serial message to another serial message. * Used by the priority queue to order messages. * * @param arg0 the first serial message to compare the other to. * @param arg1 the other serial message to compare the first one to. */ @Override public int compare(SerialMessage arg0, SerialMessage arg1) { // ZWaveSecurityCommandClass.SECURITY_NONCE_REPORT trumps all final boolean arg0NonceReport = ZWaveSecurityCommandClass.isSecurityNonceReportMessage(arg0); final boolean arg1NonceReport = ZWaveSecurityCommandClass.isSecurityNonceReportMessage(arg1); if (arg0NonceReport && !arg1NonceReport) { return -1; } else if (arg1NonceReport && !arg0NonceReport) { return 1; } // they both are or both aren't, continue to logic below boolean arg0Awake = false; boolean arg0Listening = true; boolean arg1Awake = false; boolean arg1Listening = true; if ((arg0.getMessageClass() == SerialMessageClass.RequestNodeInfo || arg0.getMessageClass() == SerialMessageClass.SendData)) { ZWaveNode node = this.controller.getNode(arg0.getMessageNode()); if (node != null && !node.isListening() && !node.isFrequentlyListening()) { arg0Listening = false; ZWaveWakeUpCommandClass wakeUpCommandClass = (ZWaveWakeUpCommandClass) node .getCommandClass(CommandClass.WAKE_UP); if (wakeUpCommandClass != null && wakeUpCommandClass.isAwake()) { arg0Awake = true; } } } if ((arg1.getMessageClass() == SerialMessageClass.RequestNodeInfo || arg1.getMessageClass() == SerialMessageClass.SendData)) { ZWaveNode node = this.controller.getNode(arg1.getMessageNode()); if (node != null && !node.isListening() && !node.isFrequentlyListening()) { arg1Listening = false; ZWaveWakeUpCommandClass wakeUpCommandClass = (ZWaveWakeUpCommandClass) node .getCommandClass(CommandClass.WAKE_UP); if (wakeUpCommandClass != null && wakeUpCommandClass.isAwake()) { arg1Awake = true; } } } // messages for awake nodes get priority over // messages for sleeping (or listening) nodes. if (arg0Awake && !arg1Awake) { return -1; } else if (arg1Awake && !arg0Awake) { return 1; } // messages for listening nodes get priority over // non listening nodes. if (arg0Listening && !arg1Listening) { return -1; } else if (arg1Listening && !arg0Listening) { return 1; } int res = arg0.priority.compareTo(arg1.priority); if (res == 0 && arg0 != arg1) { res = (arg0.sequenceNumber < arg1.sequenceNumber ? -1 : 1); } return res; } } }