diff --git a/README.adoc b/README.adoc index ec74a12..2852dd7 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,6 @@ = Temboo Library for Arduino = -This library allows an Arduino Yun to connect to the Temboo service. +This library allows an Arduino to connect to the Temboo service. == License == @@ -17,3 +17,13 @@ software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +-------------------------------------------------------------- + +This library includes elements of the Paho MQTT client, used +with permission under the Eclipse Public License - v1.0 +(http://www.eclipse.org/legal/epl-v10.html) and the Eclipse Distribution +License v1.0 (http://www.eclipse.org/org/documents/edl-v10.php). + +The Eclipse MQTT Paho client source is available here: +http://www.eclipse.org/paho/ diff --git a/extras/readme.txt b/extras/readme.txt deleted file mode 100644 index cb8b497..0000000 --- a/extras/readme.txt +++ /dev/null @@ -1,31 +0,0 @@ -Temboo Library for Arduino - -Installation ------------- - -For installation instructions, please visit: - -http://temboo.com/arduino/others/library-installation - -Examples, Tutorials & More --------------------------- - -You can find lots of examples, tutorials and info about Device Coder (Temboo's tool for automatically generating internet-connected sketch code) at the link below: - -http://temboo.com/arduino/ - -Compatibility -------------- - -This library has been tested with the following Arduino boards: - -* TRE -* Yún -* Due -* Mega -* Uno - -And the following internet shields: - -* Arduino Wifi Shield -* Arduino Ethernet Shield diff --git a/keywords.txt b/keywords.txt index 40d670d..49780a1 100644 --- a/keywords.txt +++ b/keywords.txt @@ -7,12 +7,16 @@ ####################################### Temboo KEYWORD1 +TembooMQTTEdgeDevice KEYWORD1 +TembooCoAPEdgeDevice KEYWORD1 ####################################### # Datatypes (KEYWORD2) ####################################### TembooChoreo KEYWORD2 +TembooCoAPChoreo KEYWORD2 +TembooMQTTChoreo KEYWORD2 ####################################### # Methods and Functions (KEYWORD2) @@ -29,3 +33,4 @@ addInput KEYWORD2 addOutputFilter KEYWORD2 setSettingsFileToWrite KEYWORD2 setSettingsFileToRead KEYWORD2 +setGatewayAddress KEYWORD2 diff --git a/library.properties b/library.properties index 9b743a3..1ba5993 100644 --- a/library.properties +++ b/library.properties @@ -6,5 +6,5 @@ paragraph=Use this library to connect your Arduino board to Temboo, making it si category=Communication url=http://www.temboo.com/arduino architectures=* -version=1.1.2 +version=1.1.3 core-dependencies=arduino (>=1.5.0) diff --git a/src/Temboo.cpp b/src/Temboo.cpp index f4eb85e..35a384a 100644 --- a/src/Temboo.cpp +++ b/src/Temboo.cpp @@ -24,60 +24,9 @@ #if defined (ARDUINO_AVR_YUN) || defined (ARDUINO_AVR_TRE) /////////////////////////////////////////////////////// -// BEGIN ARDUINO YUN AND TRE SUPPORT +// ARDUINO YUN AND TRE SUPPORT IN HEADER FILE /////////////////////////////////////////////////////// -#include - -void TembooChoreo::begin() { - Process::begin("temboo"); -} - -void TembooChoreo::setAccountName(const String& accountName) { - addParameter("-a" + accountName); -} - -void TembooChoreo::setAppKeyName(const String& appKeyName) { - addParameter("-u" + appKeyName); -} - -void TembooChoreo::setAppKey(const String& appKey) { - addParameter("-p" + appKey); -} - -void TembooChoreo::setChoreo(const String& choreo) { - addParameter("-c" + choreo); -} - -void TembooChoreo::setCredential(const String& credentialName) { - addParameter("-e" + credentialName); -} - -void TembooChoreo::setSavedInputs(const String& savedInputsName) { - addParameter("-e" + savedInputsName); -} - -void TembooChoreo::setProfile(const String& profileName) { - addParameter("-e" + profileName); -} - -void TembooChoreo::addInput(const String& inputName, const String& inputValue) { - addParameter("-i" + inputName + ":" + inputValue); -} - -void TembooChoreo::addOutputFilter(const String& outputName, const String& filterPath, const String& variableName) { - addParameter("-o" + outputName + ":" + filterPath + ":" + variableName); -} - -void TembooChoreo::setSettingsFileToWrite(const String& filePath) { - addParameter("-w" + filePath); -} - -void TembooChoreo::setSettingsFileToRead(const String& filePath) { - addParameter("-r" + filePath); -} - - #else //ARDUINO_AVR_YUN /////////////////////////////////////////////////////// @@ -241,6 +190,10 @@ int TembooChoreo::run(uint16_t timeoutSecs) { return run(INADDR_NONE, 80, timeoutSecs); } +int TembooChoreo::run(IPAddress addr, uint16_t port) { + return run(addr, port, TEMBOO_CHOREO_DEFAULT_TIMEOUT_SECS); +} + int TembooChoreo::run(IPAddress addr, uint16_t port, uint16_t timeoutSecs) { m_nextChar = NULL; @@ -351,7 +304,11 @@ int TembooChoreo::peek() { // If we're still sending the HTTP response code, // return the next character in that sequence. if (m_nextChar != NULL) { - return (int)*m_nextChar; + if(m_nextState != HTTP_CODE_VALUE) { + return (int)pgm_read_byte(m_nextChar); + } else { + return (int)*m_nextChar; + } } // Otherwise, return whatever is in the client buffer. diff --git a/src/Temboo.h b/src/Temboo.h index 881fe48..1be2971 100644 --- a/src/Temboo.h +++ b/src/Temboo.h @@ -35,18 +35,20 @@ class TembooChoreo : public Process { public: - void begin(); - void setAccountName(const String& accountName); - void setAppKeyName(const String& appKeyName); - void setAppKey(const String& appKey); - void setChoreo(const String& choreo); - void setCredential(const String& credentialName); - void setSavedInputs(const String& saveInputsName); - void setProfile(const String& profileName); - void addInput(const String& inputName, const String& inputValue); - void addOutputFilter(const String& filterName, const String& filterPath, const String& variableName); - void setSettingsFileToWrite(const String& filePath); - void setSettingsFileToRead(const String& filePath); + void begin() {Process::begin("temboo");} + void setAccountName(const String& accountName) { addParameter("-a" + accountName);} + void setAppKeyName(const String& appKeyName) { addParameter("-u" + appKeyName);} + void setAppKey(const String& appKey) { addParameter("-p" + appKey);} + void setChoreo(const String& choreo) { addParameter("-c" + choreo);} + void setCredential(const String& credentialName) { addParameter("-e" + credentialName);} + void setSavedInputs(const String& savedInputsName) { addParameter("-e" + savedInputsName);} + void setProfile(const String& profileName) { addParameter("-e" + profileName);} + void addInput(const String& inputName, const String& inputValue) { addParameter("-i" + inputName + ":" + inputValue);} + void addOutputFilter(const String& filterName, const String& filterPath, const String& variableName) { addParameter("-o" + filterName + ":" + filterPath + ":" + variableName);} + void setSettingsFileToWrite(const String& filePath) { addParameter("-w" + filePath);} + void setSettingsFileToRead(const String& filePath) { addParameter("-r" + filePath);} + unsigned int setGatewayAddress(const String& addr) { addParameter("-s" + addr);} + }; @@ -134,10 +136,11 @@ class TembooChoreo : public Stream { // run the choreo using the current input info int run(); - - // run the choreo on the Temboo server at the given IP address and port - // (used only when instructed by Temboo customer support.) + // run the choreo with a user specified timeout int run(uint16_t timeoutSecs); + + // run the choreo on the Temboo server at the given IP address and port + int run(IPAddress addr, uint16_t port); int run(IPAddress addr, uint16_t port, uint16_t timeoutSecs); void close(); diff --git a/src/TembooCoAPEdgeDevice.cpp b/src/TembooCoAPEdgeDevice.cpp new file mode 100644 index 0000000..098dea9 --- /dev/null +++ b/src/TembooCoAPEdgeDevice.cpp @@ -0,0 +1,935 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + + +#include "utility/TembooGlobal.h" +#include "utility/TembooCoAPSession.h" +#include "utility/TembooTags.h" + +#include "TembooCoAPEdgeDevice.h" + +#ifndef UINT16_MAX +#define UINT16_MAX (0xFFFF) +#endif + +#ifndef UINT32_MAX +#define UINT32_MAX (0xFFFFFFFF) +#endif + +//TODO: Maybe. Put these in PROGMEM and +// modify any code that uses them. +const char HTTP_CODE_PREFIX[] = "HTTP_CODE\x0A\x1F"; +const char HTTP_CODE_SUFFIX[] = "\x0A\x1E"; +const char TembooCoAPClient::URI_PATH[] = "exec"; +const char TIME_URI_PATH[] = "time"; + +uint16_t TembooCoAPChoreo::s_nextRequestId = 0; + +TembooCoAPClient::TembooCoAPClient(TembooCoAPIPStack& ipStack, IPAddress gatewayAddress, uint16_t gatewayPort) : + m_messageLayer(m_rxBuffer, sizeof(m_rxBuffer), ipStack), + m_rrLayer(m_messageLayer, m_rxBuffer, sizeof(m_rxBuffer)), + m_gatewayAddress(gatewayAddress), + m_gatewayPort(gatewayPort), + m_messageID(0), + m_state(STATE_IDLE), + m_blockSize(MAX_BLOCK_SIZE), + m_lastError(NO_ERROR), + m_dataLen(0), + m_respLen(0), + m_txIndex(0), + m_txByteCount(0), + m_respHttpCode(0) { + + memset(m_token, 0, sizeof(m_token)); + memset(m_dataBuffer, 0, sizeof(m_dataBuffer)); + memset(m_respBuffer, 0, sizeof(m_respBuffer)); + +} + + +void TembooCoAPClient::resetChoreo() { + memset(m_token, 0, sizeof(m_token)); + memset(m_dataBuffer, 0, sizeof(m_dataBuffer)); + memset(m_respBuffer, 0, sizeof(m_respBuffer)); + m_dataLen = 0; + m_respLen = 0; + m_txIndex = 0; + m_txByteCount = 0; + m_rxBlockNum = 0; + + m_rrLayer.setState(CoapRRLayer::STATE_IDLE); + m_messageLayer.setState(CoapMessageLayer::STATE_CLOSED); +} + +void TembooCoAPClient::begin(long seed){ + //RFC7252 "strongly recommends" that the initial + //value of messageID be randomized. There's no good way + //to do that reliably on many MCU boards. We will + //use random(), and can instruct the user to call randomSeed + //with input from an unused analog input if it's important to them. + randomSeed(seed); + m_messageID = random(0, UINT16_MAX); + +} + + +TembooCoAPClient::~TembooCoAPClient() { +} + + + +TembooCoAPClient::Result TembooCoAPClient::write(uint8_t value) { + if (m_dataLen < sizeof(m_dataBuffer)) { + m_dataBuffer[m_dataLen] = value; + m_dataLen++; + m_txByteCount = 0; + m_txIndex = 0; + return NO_ERROR; + } + return ERROR_BUFFER_FULL; +} + +TembooCoAPClient::Result TembooCoAPClient::saveResponse(uint8_t* values, uint16_t len) { + len = len < (sizeof(m_respBuffer) - m_respLen - 1) ? len : (sizeof(m_respBuffer) - m_respLen -1); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Saving payload to the buffer"); + if ( len > 0) { + memcpy(&m_respBuffer[m_respLen], values, len); + m_respLen += len; + return NO_ERROR; + } + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Buffer full, payload not saved"); + return ERROR_BUFFER_FULL; +} + + + +TembooCoAPClient::Result TembooCoAPClient::write(uint8_t* values, uint16_t len) { + Result rc = NO_ERROR; + while(NO_ERROR == rc && len > 0) { + rc = write(*values++); + len--; + } + return rc; +} + + + +uint16_t TembooCoAPClient::getNextMessageID() { + m_messageID++; + if (m_messageID == 0) { + m_messageID++; + } + return m_messageID; +} + + +TembooCoAPClient::Result TembooCoAPClient::generateToken() { + // 5.3.1. Token suggests the tokenID should be a random value + + for (int i = 0; i < 8; i++) { + m_token[i] = (rand() % 93) + 33; + } + m_token[8] = '\0'; + return NO_ERROR; +} + +TembooCoAPClient::Result TembooCoAPClient::sendBlockRequest(uint16_t msgID, uint32_t blockNum) { + + CoapMsg msg(m_txBuffer, sizeof (m_txBuffer)); + msg.setCode(CoapMsg::COAP_POST); + + if (msg.setToken((uint8_t*)m_token, strlen(m_token))) { + TEMBOO_TRACELN("err: setToken"); + return ERROR_MSG_TOKEN; + } + + msg.setId(msgID); + + if (msg.addOption(CoapMsg::COAP_OPTION_URI_PATH, (const uint8_t*)URI_PATH, strlen(URI_PATH))) { + TEMBOO_TRACELN("err: setURI"); + return ERROR_MSG_OPTION; + } + + uint8_t optionValue[3]; + uint16_t optionLen = 0; + + // If this is the last block in a series of blocks (or an only block) + // include a block2 option to let the server know what our + // desired block size is for the response. + + optionValue[0] = (blockNum & 0xF000) >> 12; + optionValue[1] = (blockNum & 0x0FF0) >> 4; + optionValue[2] = (blockNum & 0x000F) << 4; + optionValue[2] |= (0 ? 0x08 : 0); + optionValue[2] |= (m_blockSize >> 5) & 0x07; + + optionLen = 1; + if (optionValue[0] > 0) { + optionLen = 3; + } else if (optionValue[1] > 0) { + optionLen = 2; + } + if (msg.addOption(CoapMsg::COAP_OPTION_BLOCK2, (const uint8_t*)&optionValue[3 - optionLen], optionLen)) { + TEMBOO_TRACELN("err: block2"); + return ERROR_MSG_OPTION; + } + + if (m_rrLayer.reliableSend(msg, m_token, m_gatewayAddress, m_gatewayPort) != CoapRRLayer::NO_ERROR) { + TEMBOO_TRACELN("err: send"); + return ERROR_SENDING_MSG; + } + + return NO_ERROR; +} + + +TembooCoAPClient::Result TembooCoAPClient::sendBlock(uint16_t msgID, uint8_t* payload, size_t len, uint32_t blockNum, bool moreBlocks) { + + CoapMsg msg(m_txBuffer, sizeof (m_txBuffer)); + msg.setCode(CoapMsg::COAP_POST); + + if (msg.setToken((uint8_t*)m_token, strlen(m_token))) { + TEMBOO_TRACELN("err: setToken"); + return ERROR_MSG_TOKEN; + } + + msg.setId(msgID); + + if (msg.addOption(CoapMsg::COAP_OPTION_URI_PATH, (const uint8_t*)URI_PATH, strlen(URI_PATH))) { + TEMBOO_TRACELN("err: setURI"); + return ERROR_MSG_OPTION; + } + + uint8_t optionValue[3]; + uint16_t optionLen = 0; + + // If this is the last block in a series of blocks (or an only block) + // include a block2 option to let the server know what our + // desired block size is for the response. + if (!moreBlocksToSend()) { + optionValue[0] = (m_blockSize >> 5) & 0x07; + optionLen = (optionValue[0] > 0) ? 1 : 0; + if (msg.addOption(CoapMsg::COAP_OPTION_BLOCK2, (const uint8_t*)optionValue, optionLen)) { + TEMBOO_TRACELN("err: block2"); + return ERROR_MSG_OPTION; + } + } + + // If this is not the only block in the request, + // include the block1 option. + if (blockNum > 0 || moreBlocks) { + + optionValue[0] = (blockNum & 0xF000) >> 12; + optionValue[1] = (blockNum & 0x0FF0) >> 4; + optionValue[2] = (blockNum & 0x000F) << 4; + optionValue[2] |= (moreBlocks ? 0x08 : 0); + optionValue[2] |= (m_blockSize >> 5) & 0x07; + + optionLen = 1; + if (optionValue[0] > 0) { + optionLen = 3; + } else if (optionValue[1] > 0) { + optionLen = 2; + } + + if (msg.addOption(CoapMsg::COAP_OPTION_BLOCK1, (const uint8_t*)&optionValue[3 - optionLen], optionLen)) { + TEMBOO_TRACELN("err: block1"); + return ERROR_MSG_OPTION; + } + } + + if (msg.setPayload((uint8_t*)payload, len)) { + TEMBOO_TRACELN("err: setPayload"); + return ERROR_MSG_PAYLOAD; + } + + if (m_rrLayer.reliableSend(msg, m_token, m_gatewayAddress, m_gatewayPort) != CoapRRLayer::NO_ERROR) { + TEMBOO_TRACELN("err: send"); + return ERROR_SENDING_MSG; + } + + return NO_ERROR; +} + + +void TembooCoAPClient::adjustRequestBlockSize(CoapMsg& msg) { + + // A block1 option in a response means the server is + // requesting that we use a smaller block size. + uint16_t newBlockSize = msg.getBlock1Size(); + if (newBlockSize > 0 && newBlockSize < m_blockSize) { + m_blockSize = newBlockSize; + } +} + + +TembooCoAPClient::Result TembooCoAPClient::loop() { + + m_lastResult = NO_ERROR; + + switch (m_state) { + + case STATE_IDLE: + case STATE_RESPONSE_READY: + // Pump the receiver. + // We're not serving anything, so unless there's an outstanding + // request (which would mean we would be in STATE_WAITING, not STATE_IDLE), + // the R/R layer will reject or ignore any incoming traffic. + m_rrLayer.loop(); + + break; + case STATE_SEND_REQUEST: + case STATE_RESPONSE_STARTED: + case STATE_WAITING_FOR_RESPONSE: + // We're waiting for a response to an earlier request. + switch(m_rrLayer.loop()) { + + case CoapRRLayer::NO_ERROR: + // Nothing happened. Nothing to do. + break; + + case CoapRRLayer::RESPONSE_RECEIVED: { + + // A response to our request was received. + // It may have been a piggybacked ACK or a separate response + CoapMsg msg(m_rxBuffer, sizeof(m_rxBuffer), m_messageLayer.getRXByteCount()); + + // See if it has a BLOCK1 option. If so, make sure the + // block number matches the one we just sent. If the block + // numbers don't match, we're FUBAR, so abort the request. + // If they do match, adjust our request block size if the + // server requested a different (smaller) size. + + if (msg.getOptionCount(CoapMsg::COAP_OPTION_BLOCK1)) { + uint32_t ackBlockNum = msg.getBlock1Num(); + if (ackBlockNum != m_txBlockNum) { + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Block1 message number does not match"); + m_lastResult = ERROR_RECEIVING_RESPONSE; + if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + m_messageLayer.rejectMsg(msg); + } + resetChoreo(); + break; + } + adjustRequestBlockSize(msg); + } + + // Now deal with the response itself. + switch(msg.getCode()) { + case CoapMsg::COAP_CONTINUE: //2.31 + // 2.31 means the server is requesting the next block of the request. + // If there are no more blocks to send, we're FUBAR, so abort the + // request. Otherwise, send the next block. + if (m_txIndex >= m_dataLen) { + // no more data to send, bad news + resetChoreo(); + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Gateway requested too many blocks"); + break; + } + if (sendChoreoRequest() != NO_ERROR) { + resetChoreo(); + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Send Choreo request failed"); + } + break; + + case CoapMsg::COAP_REQUEST_ENTITY_INCOMPLETE: //4.08 + // 4.08 means the server is missing one or more blocks, so can't + // service the request. + // We're FUBAR, so abort the request. + resetChoreo(); + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Gateway returned 4.08"); + break; + + case CoapMsg::COAP_REQUEST_ENTITY_TOO_LARGE: //4.13 + // 4.13 means the server ran out of memory when receiving the + // request. + // We're FUBAR, so abort the request. + resetChoreo(); + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Gateway returned 4.13"); + break; + + default: + // Any response code other than the special ones above means the + // server has processed the request and is returning the final result, + // which may be in one or more blocks. If we haven't finished sending + // the request, we're FUBAR, so abort the request. Otherwise, process + // the response. + m_dataLen = 0; + if (moreBlocksToSend()) { + m_lastResult = ERROR_RECEIVING_RESPONSE; + if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + m_messageLayer.rejectMsg(msg); + } + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Response received before request finished"); + resetChoreo(); + m_state = STATE_IDLE; + break; + } else { + if (msg.getOptionCount(CoapMsg::COAP_OPTION_BLOCK2)) { + // The server is sending a multi-block response, make sure + // it's sending the response block we're expecting. + + uint32_t respBlockNum = msg.getBlock2Num(); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Block2 opt recv"); + + if (respBlockNum > m_rxBlockNum) { + // It sent a newer block than the one we're expecting, + // (i.e. we've somehow missed a block) + // that's an error. + m_lastResult = ERROR_RECEIVING_RESPONSE; + if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + m_messageLayer.rejectMsg(msg); + } + resetChoreo(); + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Received block out of order"); + break; + + } else if (respBlockNum < m_rxBlockNum) { + // It resent a block we've already received, + // (i.e. it didn't see our ACK), + // just accept (ACK) it again. + if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + m_messageLayer.acceptMsg(msg); + } + m_lastResult = NO_ERROR; + sendBlockRequest(m_messageID, m_rxBlockNum); + m_state = STATE_RESPONSE_STARTED; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Received previous block"); + + + } else { + // Server sent the next block we are expecting. + // Accept it and add the payload to our buffer. + bool block2More = msg.getBlock2More(); + m_respHttpCode = msg.getHTTPStatus(); + m_lastResult = saveResponse(msg.getPayload(), msg.getPayloadLen()); + if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + m_rxBlockNum = respBlockNum; + m_messageLayer.acceptMsg(msg); + } + if (block2More) { + m_rxBlockNum++; + m_messageID++; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Request next block2 msg"); + sendBlockRequest(m_messageID, m_rxBlockNum); + m_state = STATE_RESPONSE_STARTED; + } else { + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Final block2 msg recv"); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Response complete"); + m_state = STATE_RESPONSE_READY; + } + } + + } else { + // There's no Block2 option, so is either + // the one and only block in the response + // or an empty ack. + + // check if empty to handle final ack. If empty + // wait for CON with matching token and then + // request other blocks of response + + if (msg.getCode() == CoapMsg::COAP_EMPTY) { + m_rrLayer.setState(CoapRRLayer::STATE_WAITING); + m_messageLayer.setState(CoapMessageLayer::STATE_WAITING_FOR_CON); + m_state = STATE_WAITING_FOR_RESPONSE; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Empty ACK received, waiting for response"); + + } else { + m_respHttpCode = msg.getHTTPStatus(); + m_lastResult = saveResponse(msg.getPayload(), msg.getPayloadLen()); + m_messageLayer.acceptMsg(msg); + m_state = STATE_RESPONSE_READY; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Response complete"); + } + } + + } + } + + // Does it have a Block2 option? + + uint32_t responseBlockNum = msg.getBlock2Num(); + if (responseBlockNum == m_rxBlockNum && msg.getOptionCount(CoapMsg::COAP_OPTION_BLOCK2)) { + adjustRequestBlockSize(msg); + m_rxBlockNum++; + if (0 == m_rxBlockNum) { + clearData(); + } + } + break; + } + + case CoapRRLayer::ERROR_RECEIVING_RESPONSE: + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Error receiving response"); + break; + + case CoapRRLayer::RST_RECEIVED: + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("RST received"); + break; + + default: + // Anything else indicates a failure of some sort. Check + // the messageLayer lastResult for specifics. + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Request failed"); + m_lastResult = ERROR_REQUEST_FAILED; + m_state = STATE_IDLE; + + } + break; + default: + break; + } + + return m_lastResult; +} + +void TembooCoAPClient::cancelWait() { + if (STATE_WAITING_FOR_RESPONSE == m_state) { + m_messageLayer.cancelReliableSend(); + m_dataLen = 0; + m_state = STATE_IDLE; + } +} + + +bool TembooCoAPClient::moreBlocksToSend() { + uint16_t payloadLength = (m_dataLen - m_txByteCount) < m_blockSize ? (m_dataLen - m_txByteCount) : m_blockSize; + return ((m_txByteCount + payloadLength) < m_dataLen); +} + +TembooCoAPClient::Result TembooCoAPClient::sendChoreoRequest() { + uint16_t payloadLength = 0; + int16_t blockNum = 0; + + generateToken(); + + payloadLength = (m_dataLen - m_txByteCount) < m_blockSize ? (m_dataLen - m_txByteCount) : m_blockSize; + m_txBlockNum = m_txByteCount/m_blockSize; + bool moreBlocks = (m_txByteCount + payloadLength) < m_dataLen; + m_lastError = sendBlock(m_messageID, &m_dataBuffer[m_txIndex], payloadLength, m_txBlockNum, moreBlocks); + m_messageID++; + if (TembooCoAPClient::NO_ERROR == m_lastError) { + m_state = STATE_SEND_REQUEST; + m_txIndex += payloadLength; + m_txByteCount += payloadLength; + } else { + m_lastError = ERROR_SENDING_MSG; + m_state = STATE_ERROR; + } + + + return m_lastError; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + + + + + +TembooCoAPChoreo::TembooCoAPChoreo(TembooCoAPClient& client) : +m_client(client), +m_accountName(NULL), +m_appKeyName(NULL), +m_appKeyValue(NULL), +m_path(NULL), +m_requestId(0), +m_availableChars(0), +m_nextChar(NULL), +m_nextState(END) +{ +} + +TembooCoAPChoreo::~TembooCoAPChoreo() { +} + + +void TembooCoAPChoreo::setAccountName(const String& accountName) { + m_accountName = accountName.c_str(); +} + + +void TembooCoAPChoreo::setAccountName(const char* accountName) { + m_accountName = accountName; +} + + +void TembooCoAPChoreo::setAppKeyName(const String& appKeyName) { + m_appKeyName = appKeyName.c_str(); +} + + +void TembooCoAPChoreo::setAppKeyName(const char* appKeyName) { + m_appKeyName = appKeyName; +} + + +void TembooCoAPChoreo::setAppKey(const String& appKeyValue) { + m_appKeyValue = appKeyValue.c_str(); +} + + +void TembooCoAPChoreo::setAppKey(const char* appKeyValue) { + m_appKeyValue = appKeyValue; +} + + +void TembooCoAPChoreo::setChoreo(const String& path) { + m_path = path.c_str(); +} + + +void TembooCoAPChoreo::setChoreo(const char* path) { + m_path = path; +} + + +void TembooCoAPChoreo::setSavedInputs(const String& savedInputsName) { + m_preset.put(savedInputsName.c_str()); +} + + +void TembooCoAPChoreo::setSavedInputs(const char* savedInputsName) { + m_preset.put(savedInputsName); +} + + +void TembooCoAPChoreo::setCredential(const String& credentialName) { + m_preset.put(credentialName.c_str()); +} + + +void TembooCoAPChoreo::setCredential(const char* credentialName) { + m_preset.put(credentialName); +} + +void TembooCoAPChoreo::setProfile(const String& profileName) { + m_preset.put(profileName.c_str()); +} + + +void TembooCoAPChoreo::setProfile(const char* profileName) { + m_preset.put(profileName); +} + + + +void TembooCoAPChoreo::addInput(const String& inputName, const String& inputValue) { + m_inputs.put(inputName.c_str(), inputValue.c_str()); +} + + +void TembooCoAPChoreo::addInput(const char* inputName, const char* inputValue) { + m_inputs.put(inputName, inputValue); +} + + +void TembooCoAPChoreo::addInput(const char* inputName, const String& inputValue) { + m_inputs.put(inputName, inputValue.c_str()); +} + + +void TembooCoAPChoreo::addInput(const String& inputName, const char* inputValue) { + m_inputs.put(inputName.c_str(), inputValue); +} + + +void TembooCoAPChoreo::addOutputFilter(const char* outputName, const char* filterPath, const char* variableName) { + m_outputs.put(outputName, filterPath, variableName); +} + + +void TembooCoAPChoreo::addOutputFilter(const String& outputName, const char* filterPath, const char* variableName) { + m_outputs.put(outputName.c_str(), filterPath, variableName); +} + + +void TembooCoAPChoreo::addOutputFilter(const char* outputName, const String& filterPath, const char* variableName) { + m_outputs.put(outputName, filterPath.c_str(), variableName); +} + + +void TembooCoAPChoreo::addOutputFilter(const String& outputName, const String& filterPath, const char* variableName) { + m_outputs.put(outputName.c_str(), filterPath.c_str(), variableName); +} + + +void TembooCoAPChoreo::addOutputFilter(const char* outputName, const char* filterPath, const String& variableName) { + m_outputs.put(outputName, filterPath, variableName.c_str()); +} + + +void TembooCoAPChoreo::addOutputFilter(const String& outputName, const char* filterPath, const String& variableName) { + m_outputs.put(outputName.c_str(), filterPath, variableName.c_str()); +} + + +void TembooCoAPChoreo::addOutputFilter(const char* outputName, const String& filterPath, const String& variableName) { + m_outputs.put(outputName, filterPath.c_str(), variableName.c_str()); +} + + +void TembooCoAPChoreo::addOutputFilter(const String& outputName, const String& filterPath, const String& variableName) { + m_outputs.put(outputName.c_str(), filterPath.c_str(), variableName.c_str()); +} + +int TembooCoAPChoreo::waitForResponse(TembooTimer& timer) { + + int rc = SUCCESS; + while (m_client.getState() == TembooCoAPClient::STATE_RESPONSE_STARTED || m_client.getState() == TembooCoAPClient::STATE_WAITING_FOR_RESPONSE) { + if (timer.expired()) { + TEMBOO_TRACELN("ERROR: Choreo timeout"); + rc = TEMBOO_ERROR_TIMEOUT; + break; + } + m_client.loop(); + // While the buffer may be full, we need to receive all of the data + // from the gateway even though we discard it. We still return + // the buffer error code, but the user is still able to see what + // data was able to fit in the current buffer + if (m_client.getMessageState() != TembooCoAPClient::NO_ERROR && m_client.getMessageState() != TembooCoAPClient::ERROR_BUFFER_FULL) { + rc = FAILURE; + break; + } + } + + return rc; +} + +int TembooCoAPChoreo::run(uint16_t timeoutSecs) { + m_nextChar = NULL; + + if (IS_EMPTY(m_accountName)) { + return TEMBOO_ERROR_ACCOUNT_MISSING; + } + + if (IS_EMPTY(m_path)) { + return TEMBOO_ERROR_CHOREO_MISSING; + } + + if (IS_EMPTY(m_appKeyName)) { + return TEMBOO_ERROR_APPKEY_NAME_MISSING; + } + + if (IS_EMPTY(m_appKeyValue)) { + return TEMBOO_ERROR_APPKEY_MISSING; + } + int rc = 0; + + TembooTimer timer(timeoutSecs * 1000L); + + for (int i = 0; i < 2; i++) { + + m_client.resetChoreo(); + + TembooCoAPSession session(m_client); + m_requestId = s_nextRequestId++; + + m_respData = NULL; + m_availableChars = 0; + m_nextState = START; + uint16toa(0 , m_httpCodeStr); + + m_client.getNextMessageID(); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Sending request"); + rc = session.executeChoreo(m_requestId, m_accountName, m_appKeyName, m_appKeyValue, m_path, m_inputs, m_outputs, m_preset); + if (SUCCESS != rc) { + goto ErrorExit; + } + + // finish sending Choreo request + while (m_client.getState() == TembooCoAPClient::STATE_SEND_REQUEST) { + if(m_client.loop() == TembooCoAPClient::ERROR_REQUEST_FAILED) { + rc = m_client.getMessageState(); + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Choreo request failed"); + goto ErrorExit; + } + } + + // choreo request complete, wait for CON from gateway + // and then request the rest of the response + + rc = waitForResponse(timer); + if (SUCCESS != rc){ + rc = m_client.getMessageState(); + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Waiting for response failed"); + goto ErrorExit; + } else { + + m_respData = (char*)m_client.getPacketBuffer(); + uint16_t httpCode = m_client.getRespHttpCode(); + if (httpCode >= 700) { + httpCode = 0; + } + + uint16toa(httpCode, m_httpCodeStr); + m_availableChars = strlen(m_respData) + strlen(m_httpCodeStr) + strlen(HTTP_CODE_PREFIX) + strlen(HTTP_CODE_SUFFIX); + + m_nextChar = HTTP_CODE_PREFIX; + + //Unauthroized, need to update the time + if (httpCode == 401 && i == 0) { + find("x-temboo-time:"); + TembooCoAPSession::setTime((unsigned long)this->parseInt()); + } else { + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACE(m_availableChars); + TEMBOO_TRACELN(" CHARS"); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Response buffer data:"); + TEMBOO_TRACELN(m_respData); + + rc = m_client.getMessageState(); + break; + } + } + } + +ErrorExit: + if (SUCCESS != rc) { + TEMBOO_TRACE(" ERROR:"); + TEMBOO_TRACELN(rc); + } + return rc; +} + + +int TembooCoAPChoreo::available() { + return m_availableChars; +} + + +int TembooCoAPChoreo::peek() { + if (m_availableChars > 0) { + return (int)*m_nextChar; + } + return -1; +} + + +int TembooCoAPChoreo::read() { + + if (m_availableChars > 0) { + int c = 0; + switch(m_nextState) { + case START: + m_nextChar = HTTP_CODE_PREFIX; + c = (int)(*m_nextChar++); + m_nextState = HTTP_CODE_PRE; + break; + + case HTTP_CODE_PRE: + c = (int)(*m_nextChar++); + if ('\0' == *m_nextChar) { + m_nextState = HTTP_CODE_VALUE; + m_nextChar = m_httpCodeStr; + } + break; + + case HTTP_CODE_VALUE: + c = (int)(*m_nextChar++); + if (*m_nextChar == '\0') { + m_nextState = HTTP_CODE_SUF; + m_nextChar = HTTP_CODE_SUFFIX; + } + break; + + case HTTP_CODE_SUF: + c = (int)(*m_nextChar++); + if ('\0' == *m_nextChar) { + m_nextState = RESP_DATA; + m_nextChar = m_respData; + } + break; + + case RESP_DATA: + c = (int)(*m_nextChar++); + if ('\0' == *m_nextChar || m_availableChars <= 0) { + m_nextState = END; + } + break; + + case END: + default: + c = -1; + } + if (m_availableChars > 0) { + m_availableChars--; + } + return c; + } else { + return -1; + } +} + + +size_t TembooCoAPChoreo::write(uint8_t data) { + return 0; +} + + +void TembooCoAPChoreo::flush() { + m_nextChar = NULL; + m_nextState = END; + m_availableChars = 0; +} + diff --git a/src/TembooCoAPEdgeDevice.h b/src/TembooCoAPEdgeDevice.h new file mode 100644 index 0000000..603ecc6 --- /dev/null +++ b/src/TembooCoAPEdgeDevice.h @@ -0,0 +1,307 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef TEMBOOCOAP_H_ +#define TEMBOOCOAP_H_ + + +/////////////////////////////////////////////////////// +// BEGIN ARDUINO NON-YUN SUPPORT +/////////////////////////////////////////////////////// + +#include "Arduino.h" +#include "utility/TembooTimer.h" +#include "utility/TembooTags.h" + +#include "utility/TembooCoAPIPStack.h" +#include "utility/ChoreoInputSet.h" +#include "utility/ChoreoOutputSet.h" +#include "utility/ChoreoPreset.h" +#include "utility/CoapMsg.h" +#include "utility/CoapMessageLayer.h" +#include "utility/CoapRRLayer.h" + + +#define IS_EMPTY(s) (NULL == s || '\0' == *s) +#define DEFAULT_CHOREO_TIMEOUT 900 + +class TembooCoAPChoreo; + +class TembooCoAPClient { + public: + TembooCoAPClient(TembooCoAPIPStack& ipStack, IPAddress gatewayAddress, uint16_t gatewayPort = DEFAULT_COAP_PORT); + virtual ~TembooCoAPClient(); + void begin(long seed); + + enum Result { + NO_ERROR = 0, + ERROR_REQUEST_FAILED, + ERROR_BAD_RESPONSE, + ERROR_MSG_TOKEN, + ERROR_MSG_OPTION, + ERROR_MSG_PAYLOAD, + ERROR_SENDING_MSG, + ERROR_RECEIVING_MSG, + ERROR_BUFFER_FULL, + ERROR_INVALID_MSG, + ERROR_RECEIVING_RESPONSE + }; + + enum State { + STATE_IDLE, + STATE_TRANSMIT, + STATE_WAITING_FOR_RESPONSE, + STATE_RESPONSE_STARTED, + STATE_RESPONSE_READY, + STATE_NO_RESPONSE, + STATE_SEND_REQUEST, + STATE_ERROR + }; + + Result write(uint8_t value); + Result write(uint8_t* value, uint16_t len); + void clearData() {m_dataLen = 0;} + Result loop(); + Result sendChoreoRequest(); + uint8_t* getPacketBuffer() {return m_respBuffer;} + int32_t getPacketBufferSize() {return m_respLen;} + int32_t getPacketLength() {return 1000;} + int16_t getRespHttpCode() {return m_respHttpCode;} + State getState() {return m_state;} + Result sendBlockRequest(uint16_t msgID, uint32_t blockNum); + void resetChoreo(); + int getMessageState() {return m_lastResult;} + Result requestTime(uint16_t msgID); + + protected: + static const char URI_PATH[]; + Result m_lastResult; + bool moreBlocksToSend(); + + // MAX_BLOCK_SIZE *MUST* be one of the standard CoAP block sizes + // This size should be set with consideration to any network or + // hardware limitations on UDP packet size. + // (16, 32, 64, 128, 256, 512, or 1024). + static const int MAX_BLOCK_SIZE = 64; + + // MAX_PACKET_SIZE should be at least big enough to hold: + // for outgoing requests: + // 4 header bytes + // 6 token bytes (in our case, Spec says tokens can be up to 8 bytes). + // 1 byte for the URI PATH option (I *think*) + // 4 strlen(URI_PATH) bytes for the URI PATH option value. + // 3 bytes for a block1 option + // 3 bytes for a block1 option value + // 1 bytes for a block2 option (early response size negotiation) + // 1 bytes for a block2 option value + // ? 5 bytes for a size1 or size2 option + // ? 4 bytes for a sizeX option value + // 1 byte for the FF payload marker + // MAX_BLOCK_SIZE for the payload + // + // or 24 + MAX_BLOCK_SIZE + // + // HOWEVER... we need to consider the possibility that the server may + // use more options and thus send more bytes. So we should add as much + // extra space as we can reasonably afford so as to avoid buffer overflows. + + static const size_t MAX_PACKET_SIZE = 90; + + + static const size_t MAX_DATA_SIZE = 1000; + + static const uint16_t DEFAULT_COAP_PORT = 5683; + + CoapMessageLayer m_messageLayer; + CoapRRLayer m_rrLayer; + IPAddress m_gatewayAddress; + uint16_t m_gatewayPort; + uint16_t m_messageID; + State m_state; + uint16_t m_blockSize; + Result m_lastError; + + uint16_t m_dataLen; + uint8_t m_dataBuffer[MAX_DATA_SIZE]; + uint8_t m_respBuffer[MAX_DATA_SIZE]; + + char m_token[9]; + uint8_t m_txBuffer[MAX_PACKET_SIZE]; + uint8_t m_rxBuffer[MAX_PACKET_SIZE]; + uint32_t m_rxBlockNum; + int32_t m_txIndex; + int32_t m_txByteCount; + int32_t m_respLen; + uint32_t m_txBlockNum; + int16_t m_respHttpCode; + + Result generateToken(); + uint16_t getNextMessageID(); + + Result sendBlock(uint16_t msgID, uint8_t* payload, size_t len, uint32_t blockNum, bool moreBlocks); + void adjustRequestBlockSize(CoapMsg& msg); + void cancelWait(); + + int send(CoapMsg* msg); + int sendResetMsg(CoapMsg& msg); + int sendEmptyAckMsg(CoapMsg& msg); + int sendBlock2AckMsg(CoapMsg& msg); + bool isMsgResponse(CoapMsg& msg); + void handleBlockAck(CoapMsg& msg); + void handleBlock1InResponse(CoapMsg& msg); + Result saveResponse(uint8_t* values, uint16_t len); + + friend class TembooCoAPChoreo; +}; + +class TembooCoAPChoreo : public Stream { + public: + + // Constructor. + // client - an instance of a TembooCoAPClient. + // Used to communicate with a Temboo CoAP Gateway. + TembooCoAPChoreo(TembooCoAPClient& client); + ~TembooCoAPChoreo(); + + // Does nothing. Just for source compatibility with Yun code. + void begin() {;}; + + // Sets the account name to use when communicating with Temboo. + // (required) + void setAccountName(const String& accountName); + void setAccountName(const char* accountName); + + // Sets the application key name to use with choreo execution requests. + // (required) + void setAppKeyName(const String& appKeyName); + void setAppKeyName(const char* appKeyName); + + // Sets the application key value to use with choreo execution requests + // (required) + void setAppKey(const String& appKey); + void setAppKey(const char* appKey); + + // sets the name of the choreo to be executed. + // (required) + void setChoreo(const String& choreoPath); + void setChoreo(const char* choreoPath); + + + // sets the name of the saved inputs to use when executing the choreo + // (optional) + void setSavedInputs(const String& savedInputsName); + void setSavedInputs(const char* savedInputsName); + + void setCredential(const String& credentialName); + void setCredential(const char* credentialName); + + void setProfile(const String& profileName); + void setProfile(const char* profileName); + + // sets an input to be used when executing a choreo. + // (optional or required, depending on the choreo being executed.) + void addInput(const String& inputName, const String& inputValue); + void addInput(const char* inputName, const char* inputValue); + void addInput(const char* inputName, const String& inputValue); + void addInput(const String& inputName, const char* inputValue); + + // sets an output filter to be used to process the choreo output + // (optional) + void addOutputFilter(const char* filterName, const char* filterPath, const char* variableName); + void addOutputFilter(const String& filterName, const char* filterPath, const char* variableName); + void addOutputFilter(const char* filterName, const String& filterPath, const char* variableName); + void addOutputFilter(const String& filterName, const String& filterPath, const char* variableName); + void addOutputFilter(const char* filterName, const char* filterPath, const String& variableName); + void addOutputFilter(const String& filterName, const char* filterPath, const String& variableName); + void addOutputFilter(const char* filterName, const String& filterPath, const String& variableName); + void addOutputFilter(const String& filterName, const String& filterPath, const String& variableName); + + // run the choreo using the current input info + int run(uint16_t timeoutSecs = DEFAULT_CHOREO_TIMEOUT); + + char* getResponseData() {return m_respData;} + char* getHTTPResponseCode() {return m_httpCodeStr;} + + + // Stream interface + void close() {}; + int available(); + int read(); + int peek(); + void flush(); + + //Print interface + size_t write(uint8_t data); + + enum Error { + SUCCESS = 0, + FAILURE, + TEMBOO_ERROR_ACCOUNT_MISSING = 201, + TEMBOO_ERROR_CHOREO_MISSING = 203, + TEMBOO_ERROR_APPKEY_NAME_MISSING = 205, + TEMBOO_ERROR_APPKEY_MISSING = 207, + TEMBOO_ERROR_HTTP_ERROR = 223, + TEMBOO_ERROR_TIMEOUT = 225, + TEMBOO_ERROR_MEMORY = 900, + TEMBOO_ERROR_TCPIP_CONNECT_FAIL = 901, + TEMBOO_ERROR_NO_RESPONSE = 0xFFFF + }; + + + + protected: + static const size_t MAX_RESPONSE_SIZE = 900; + + TembooCoAPClient& m_client; + const char* m_accountName; + const char* m_appKeyName; + const char* m_appKeyValue; + const char* m_path; + + ChoreoInputSet m_inputs; + ChoreoOutputSet m_outputs; + ChoreoPreset m_preset; + + char m_httpCodeStr[4]; + char* m_respData; + + uint16_t m_requestId; + static uint16_t s_nextRequestId; + + // variables for the stream interface + size_t m_availableChars; + const char* m_nextChar; + enum State {START, + HTTP_CODE_PRE, + HTTP_CODE_VALUE, + HTTP_CODE_SUF, + RESP_DATA, + END}; + State m_nextState; + + protected: + int waitForResponse(TembooTimer& timer); + uint16_t getRequestId() {return m_requestId;} +}; + + +#endif //TEMBOO_H_ diff --git a/src/TembooMQTTEdgeDevice.cpp b/src/TembooMQTTEdgeDevice.cpp new file mode 100644 index 0000000..d94fff7 --- /dev/null +++ b/src/TembooMQTTEdgeDevice.cpp @@ -0,0 +1,861 @@ +/* +############################################################################### +# +# Temboo MQTT Edge Device library +# +# Copyright (C) 2015, Temboo Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +# +############################################################################### +*/ + + +/////////////////////////////////////////////////////// +// BEGIN ARDUINO NON-YUN SUPPORT +/////////////////////////////////////////////////////// + +#include + +#include "utility/TembooGlobal.h" +#include "utility/TembooMQTTSession.h" +#include "utility/MQTTPacket.h" + +#include "TembooMQTTEdgeDevice.h" + +#ifndef UINT16_MAX +#define UINT16_MAX (0xFFFF) +#endif + +static const char HTTP_CODE_PREFIX[] PROGMEM = "HTTP_CODE\x0A\x1F"; +static const char HTTP_CODE_SUFFIX[] PROGMEM = "\x0A\x1E"; + +static const char REQUEST_TOPIC_PREFIX[] PROGMEM = "/temboo/req/"; +static const char REQUEST_ACK_TOPIC_PREFIX[] PROGMEM = "/temboo/ack/"; +static const char RESPONSE_TOPIC_PREFIX[] PROGMEM = "/temboo/resp/"; +static const char RESPONSE_DATA_TOPIC_PREFIX[] PROGMEM = "/temboo/resp-data/"; + +// TIME_TOPIC must NOT be PROGMEM +static const char TIME_TOPIC[] = "/temboo/time"; + +static TembooMQTTChoreo* volatile g_currentChoreo = NULL; +uint16_t TembooMQTTChoreo::s_nextRequestId = 0; + +void handleDataMessage(MQTT::MessageData& md) { + + TEMBOO_TRACE("data: "); + + MQTT::Message& msg = md.message; + + char* c; + unsigned int count; + for (c = (char*)msg.payload, count = 0; *c != '\0' && isdigit(*c) && count < msg.payloadlen; c++) { + count++; + if (count > 5) { + TEMBOO_TRACELN(" long"); + return; + } + } + + if (count == 0) { + TEMBOO_TRACELN(" short"); + return; + } + + if (TAG_VALUE_SEPARATOR != *c) { + TEMBOO_TRACELN(" bad msg"); + return; + } + + // Replace the : with \0 so we can use strtoul. + *c = '\0'; + + unsigned long requestId = strtoul((char*)msg.payload, NULL, 10); + if (UINT16_MAX < requestId) { + TEMBOO_TRACELN(" bad id"); + return; + } + + if ((NULL == g_currentChoreo) || (g_currentChoreo->getRequestId() != (uint16_t)requestId)) { + TEMBOO_TRACE(" stale id: "); + TEMBOO_TRACELN(requestId); + return; + } + + g_currentChoreo->setResponseData(c + 1, msg.payloadlen - strlen((char*)msg.payload) - 1); + TEMBOO_TRACELN(" ok"); + +} + +bool validateUint16PairMessage(const char* msg) { + const char* c; + int count = 0; + for (c = msg; *c != '\0' && isdigit(*c); c++) { + count++; + if (count > 5) + return false; + } + + if (count == 0) { + return false; + } + + if (TAG_VALUE_SEPARATOR != *c) { + return false; + } + + c++; + for (count = 0; *c != '\0' && isdigit(*c); c++) { + count++; + if (count > 5) { + return false; + } + } + if (count == 0) { + return false; + } + return true; +} + + +void handleResponseMessage(MQTT::MessageData& md) { + + TEMBOO_TRACE("resp: "); + + MQTT::Message& msg = md.message; + + // Expected max length is 9 (for 65535:599) + if (msg.payloadlen > 9) { + TEMBOO_TRACELN("long"); + return; + } + + if (msg.payloadlen < 3) { + TEMBOO_TRACELN("short"); + return; + } + + // Copy the payload and nul terminate it; + char respStr[10]; + memcpy(respStr, msg.payload, msg.payloadlen); + respStr[msg.payloadlen] = '\0'; + + TEMBOO_TRACE(respStr); + + if (!validateUint16PairMessage(respStr)) { + TEMBOO_TRACELN(" bad msg"); + return; + } + + char* next; + unsigned long respCode = 0; + unsigned long requestId = strtoul(respStr, &next, 10); + + // validate only checks that the request ID + // has at least 1 but no more than 6 digits. + // so we have to check the actual value here. + if (UINT16_MAX < requestId) { + TEMBOO_TRACELN(" bad id"); + return; + } + + // If the request ID in the message doesn't match the + // current request ID, then it's a stale ack. + if ((NULL == g_currentChoreo) || (g_currentChoreo->getRequestId() != (uint16_t)requestId)) { + TEMBOO_TRACE(" stale id: "); + TEMBOO_TRACELN(respStr); + return; + } + + next++; + respCode = strtoul(next, NULL, 10); + + // Validate only checks that the ack code + // has at least 1 but no more than 6 digits, + // so we have to check the actual value here. + if (1000 <= respCode) { + TEMBOO_TRACELN(" bad code"); + return; + } + + // FINALLY, everything's OK. + // pass the value to the waiting choreo. + g_currentChoreo->setHTTPResponseCode(next); + TEMBOO_TRACELN(" ok"); + +} + + +void handleTimeMessage(MQTT::MessageData& md) { + + TEMBOO_TRACE("time: "); + + MQTT::Message& msg = md.message; + + // Time messages should be <= 10 characters long. + if (msg.payloadlen > 10) { + TEMBOO_TRACELN("long"); + return; + } + + if (msg.payloadlen == 0) { + TEMBOO_TRACELN("short"); + return; + } + + // Payload should consist only of digits (0 - 9) + for (unsigned int i = 0; i < msg.payloadlen; i++) { + if (! isdigit(((char*)msg.payload)[i])) { + TEMBOO_TRACELN("!digit"); + return; + } + } + + char timeStr[11]; + memcpy(timeStr, msg.payload, msg.payloadlen); + timeStr[msg.payloadlen] = '\0'; + + TEMBOO_TRACE(timeStr); + + uint32_t t = strtoul(timeStr, NULL, 10); + TembooMQTTSession::setTime(t); + + TEMBOO_TRACELN(" ok"); +} + +void handleAckMessage(MQTT::MessageData& md) { + + TEMBOO_TRACE("ack: "); + + MQTT::Message& msg = md.message; + + // Expected max length is 11 (for 65535:65535) + if (msg.payloadlen > 11) { + TEMBOO_TRACELN("long"); + return; + } + + if (msg.payloadlen == 0) { + TEMBOO_TRACELN("short"); + return; + } + + // Copy the payload and nul terminate it; + char ackStr[12]; + memcpy(ackStr, msg.payload, msg.payloadlen); + ackStr[msg.payloadlen] = '\0'; + + TEMBOO_TRACE(ackStr); + + if (!validateUint16PairMessage(ackStr)) { + TEMBOO_TRACELN(" bad msg"); + return; + } + + char* next; + unsigned long ackCode = TEMBOO_ERROR_FAILURE; + unsigned long requestId = strtoul(ackStr, &next, 10); + + // validate only checks that the request ID + // has at least 1 but no more than 6 digits. + // so we have to check the actual value here. + if (UINT16_MAX < requestId) { + TEMBOO_TRACELN(" bad id"); + return; + } + + // If the request ID in the message doesn't match the + // current request ID, then it's a stale ack. + if ((NULL == g_currentChoreo) || (g_currentChoreo->getRequestId() != (uint16_t)requestId)) { + TEMBOO_TRACE(" stale id: "); + TEMBOO_TRACELN(ackStr); + return; + } + + next++; + ackCode = strtoul(next, NULL, 10); + + // Validate only checks that the ack code + // has at least 1 but no more than 6 digits, + // so we have to check the actual value here. + if (UINT16_MAX < ackCode) { + TEMBOO_TRACELN(" bad code"); + return; + } + + // FINALLY, everything's OK. + // pass the value to the waiting choreo. + g_currentChoreo->setAckCode(ackCode); + TEMBOO_TRACELN(" ok"); + +} + + +TembooMQTTClient::TembooMQTTClient(TembooMQTTIPStack& ipStack, unsigned int commandTimeoutMs ) + : BaseClient (ipStack, commandTimeoutMs), + m_ipStack(ipStack) { + m_deviceId = NULL; + m_requestTopic = NULL; + m_ackTopic = NULL; + m_responseTopic = NULL; + m_dataTopic = NULL; +} + +void TembooMQTTClient::makeTopics() { + m_requestTopic = strCatNew_P(REQUEST_TOPIC_PREFIX, m_deviceId); + m_ackTopic = strCatNew_P(REQUEST_ACK_TOPIC_PREFIX, m_deviceId); + m_responseTopic = strCatNew_P(RESPONSE_TOPIC_PREFIX, m_deviceId); + m_dataTopic = strCatNew_P(RESPONSE_DATA_TOPIC_PREFIX, m_deviceId); +} + +int TembooMQTTClient::setDeviceId(char* id) { + int rc = TEMBOO_ERROR_FAILURE; + if (NULL == m_deviceId) { + m_deviceId = new char[strlen(id) + 1]; + if (NULL != m_deviceId) { + strcpy(m_deviceId, id); + makeTopics(); + rc = TEMBOO_ERROR_OK; + } else { + rc = TEMBOO_ERROR_MEMORY; + } + } + return rc; +} + +int TembooMQTTClient::setDeviceIdFromMac(byte (&mac)[6]) { + int rc = TEMBOO_ERROR_FAILURE; + + // Only do something if we don't already have a deviceId + if (NULL == m_deviceId) { + // generate the deviceId from the MAC address. + char macStr[13]; + for (unsigned int i = 0; i < sizeof(mac)/sizeof(*mac); i++) { + byte m = mac[i] >> 4; + macStr[2 * i] = m < 10 ? ('0' + m):('A' + m - 10); + m = mac[i] & 0x0F; + macStr[2 * i + 1] = m < 10 ? ('0' + m):('A' + m - 10); + } + macStr[12] = '\0'; + rc = setDeviceId(macStr); + } + return rc; +} + + +char* TembooMQTTClient::strCatNew_P(const char* s1, const char* s2) { + size_t len = strlen_P(s1) + strlen(s2) + 1; + char* result = new char[len]; + if (NULL != result) { + strcpy_P(result, s1); + strcat(result, s2); + } + return result; +} + +TembooMQTTClient::~TembooMQTTClient() { + if (NULL != m_requestTopic) delete[] m_requestTopic; + if (NULL != m_ackTopic) delete[] m_ackTopic; + if (NULL != m_responseTopic) delete[] m_responseTopic; + if (NULL != m_dataTopic) delete[] m_dataTopic; + if (NULL != m_deviceId) delete[] m_deviceId; +} + +int TembooMQTTClient::connect(const char* hostname, int port) { + int rc = TEMBOO_ERROR_FAILURE; + + if (NULL == m_deviceId) { + return TEMBOO_ERROR_DEVICE_ID_MISSING; + } + + TEMBOO_TRACE("DEVICE ID: "); + TEMBOO_TRACELN(m_deviceId); + + // Open a socket to the broker. + TEMBOO_TRACE("IP: "); + rc = m_ipStack.connect(hostname, port); + if (1 != rc) { + TEMBOO_TRACELN(rc); + rc = TEMBOO_ERROR_TCPIP_CONNECT_FAIL; + goto ErrorExit; + } else { + TEMBOO_TRACELN("OK"); + + // Establish an MQTT connection with the broker. + TEMBOO_TRACE("MQ "); + TEMBOO_TRACE(m_deviceId); + TEMBOO_TRACE(": "); + MQTTPacket_connectData data = MQTTPacket_connectData_initializer; + data.MQTTVersion = 3; + data.clientID.cstring = (char*)m_deviceId; + rc = connect(data); + if (MQTT::SUCCESS != rc) { + TEMBOO_TRACELN(rc); + rc = TEMBOO_ERROR_MQTT_CONNECT_FAIL; + goto ErrorExit; + } + TEMBOO_TRACELN("OK"); + } + + // Subscribe to the various topics we need to monitor. + //NOTE: 'subscribe()' does NOT copy the topic strings, + //so those strings must stay valid for the life of the connection. + + TEMBOO_TRACE("SUB:"); + //Yes, it's slightly risky counting on MQTT::SUCCESS always being 0. + rc = this->subscribe(m_ackTopic, MQTT::QOS1, handleAckMessage); + rc |= this->subscribe(m_responseTopic, MQTT::QOS1, handleResponseMessage); + rc |= this->subscribe(m_dataTopic, MQTT::QOS1, handleDataMessage); + rc |= this->subscribe(TIME_TOPIC, MQTT::QOS1, handleTimeMessage); + if (MQTT::SUCCESS != rc) { + TEMBOO_TRACELN("NO"); + rc = TEMBOO_ERROR_MQTT_SUBSCRIBE_FAIL; + goto ErrorExit; + } + TEMBOO_TRACELN("OK"); + + return TEMBOO_ERROR_OK; + +ErrorExit: + if (isConnected()) { + disconnect(); + } + m_ipStack.disconnect(); + return rc; +} + +bool TembooMQTTClient::isConnected() { + if (m_ipStack.isConnected()) { + return BaseClient::isConnected(); + } + return false; +} + +int TembooMQTTClient::sendChoreoRequest(const char* request, size_t len) { + MQTT::Message message; + + message.qos = MQTT::QOS0; + message.retained = false; + message.dup = false; + message.payload = (void*)request; + message.payloadlen = len; + int rc = this->publish(this->m_requestTopic, message); + return rc == MQTT::SUCCESS ? TEMBOO_ERROR_OK : rc; +} + +TembooMQTTChoreo::TembooMQTTChoreo(TembooMQTTClient& client) : + m_client(client), + m_accountName(NULL), + m_appKeyName(NULL), + m_appKeyValue(NULL), + m_path(NULL), + m_haveHttpCode(false), + m_ackCode(0), + m_haveAckCode(false), + m_haveData(false), + m_requestId(0), + m_availableChars(0), + m_nextChar(NULL), + m_nextState(END), + m_packetStatus(TEMBOO_ERROR_OK) +{ +} + +TembooMQTTChoreo::~TembooMQTTChoreo() { + memset(m_respData, '\0', sizeof(m_respData)/sizeof(m_respData[0])); +} + +void TembooMQTTChoreo::setAccountName(const String& accountName) { + m_accountName = accountName.c_str(); +} + + +void TembooMQTTChoreo::setAccountName(const char* accountName) { + m_accountName = accountName; +} + + +void TembooMQTTChoreo::setAppKeyName(const String& appKeyName) { + m_appKeyName = appKeyName.c_str(); +} + + +void TembooMQTTChoreo::setAppKeyName(const char* appKeyName) { + m_appKeyName = appKeyName; +} + + +void TembooMQTTChoreo::setAppKey(const String& appKeyValue) { + m_appKeyValue = appKeyValue.c_str(); +} + + +void TembooMQTTChoreo::setAppKey(const char* appKeyValue) { + m_appKeyValue = appKeyValue; +} + + +void TembooMQTTChoreo::setChoreo(const String& path) { + m_path = path.c_str(); +} + + +void TembooMQTTChoreo::setChoreo(const char* path) { + m_path = path; +} + + +void TembooMQTTChoreo::setSavedInputs(const String& savedInputsName) { + m_preset.put(savedInputsName.c_str()); +} + + +void TembooMQTTChoreo::setSavedInputs(const char* savedInputsName) { + m_preset.put(savedInputsName); +} + + +void TembooMQTTChoreo::setCredential(const String& credentialName) { + m_preset.put(credentialName.c_str()); +} + + +void TembooMQTTChoreo::setCredential(const char* credentialName) { + m_preset.put(credentialName); +} + +void TembooMQTTChoreo::setProfile(const String& profileName) { + m_preset.put(profileName.c_str()); +} + + +void TembooMQTTChoreo::setProfile(const char* profileName) { + m_preset.put(profileName); +} + + + +void TembooMQTTChoreo::addInput(const String& inputName, const String& inputValue) { + m_inputs.put(inputName.c_str(), inputValue.c_str()); +} + + +void TembooMQTTChoreo::addInput(const char* inputName, const char* inputValue) { + m_inputs.put(inputName, inputValue); +} + + +void TembooMQTTChoreo::addInput(const char* inputName, const String& inputValue) { + m_inputs.put(inputName, inputValue.c_str()); +} + + +void TembooMQTTChoreo::addInput(const String& inputName, const char* inputValue) { + m_inputs.put(inputName.c_str(), inputValue); +} + + +void TembooMQTTChoreo::addOutputFilter(const char* outputName, const char* filterPath, const char* variableName) { + m_outputs.put(outputName, filterPath, variableName); +} + + +void TembooMQTTChoreo::addOutputFilter(const String& outputName, const char* filterPath, const char* variableName) { + m_outputs.put(outputName.c_str(), filterPath, variableName); +} + + +void TembooMQTTChoreo::addOutputFilter(const char* outputName, const String& filterPath, const char* variableName) { + m_outputs.put(outputName, filterPath.c_str(), variableName); +} + + +void TembooMQTTChoreo::addOutputFilter(const String& outputName, const String& filterPath, const char* variableName) { + m_outputs.put(outputName.c_str(), filterPath.c_str(), variableName); +} + + +void TembooMQTTChoreo::addOutputFilter(const char* outputName, const char* filterPath, const String& variableName) { + m_outputs.put(outputName, filterPath, variableName.c_str()); +} + + +void TembooMQTTChoreo::addOutputFilter(const String& outputName, const char* filterPath, const String& variableName) { + m_outputs.put(outputName.c_str(), filterPath, variableName.c_str()); +} + + +void TembooMQTTChoreo::addOutputFilter(const char* outputName, const String& filterPath, const String& variableName) { + m_outputs.put(outputName, filterPath.c_str(), variableName.c_str()); +} + + +void TembooMQTTChoreo::addOutputFilter(const String& outputName, const String& filterPath, const String& variableName) { + m_outputs.put(outputName.c_str(), filterPath.c_str(), variableName.c_str()); +} + +int TembooMQTTChoreo::waitForResponse(volatile bool& var, TembooMQTTSession& session, ArduinoTimer& timer) { + + int rc = TEMBOO_ERROR_OK; + + while (!var) { + if (timer.expired()) { + rc = TEMBOO_ERROR_TIMEOUT; + break; + } + + if (!m_client.isConnected()) { + TEMBOO_TRACELN("DISCO"); + rc = TEMBOO_ERROR_MQTT_DISCONNECT; + break; + } + m_client.yield(YIELD_TIME_MILLIS); + } + return rc; +} + +void TembooMQTTChoreo::setHTTPResponseCode(char* respCodeStr) { + //NOTE: if we run in a true multithreaded environment, this code + //will have to be synchronized somehow. Checking the m_haveHttpCode + //flag is insufficient to prevent race conditions. + if (!m_haveHttpCode) { + strncpy(m_httpCodeStr, respCodeStr, sizeof(m_httpCodeStr)-1); + m_httpCodeStr[sizeof(m_httpCodeStr)-1] = '\0'; + m_haveHttpCode = true; + } +} + +void TembooMQTTChoreo::setResponseData(char* payload, size_t payloadLen) { + + //NOTE: if we run in a true multithreaded environment, this code + //will have to be synchronized somehow. Checking the m_haveData + //flag is insufficient to prevent race conditions. + if (!m_haveData) { + size_t len = 0; + if(payloadLen > MAX_MESSAGE_SIZE) { + m_packetStatus = TEMBOO_ERROR_MQTT_BUFFER_OVERFLOW; + len = sizeof(m_respData)/sizeof(m_respData[0])-1; + } else if (payloadLen > MAX_RESPONSE_SIZE) { + m_packetStatus = TEMBOO_ERROR_MQTT_DATA_TRUNCATED; + len = sizeof(m_respData)/sizeof(m_respData[0])-1; + } else { + len = payloadLen; + } + memcpy(m_respData, payload, len); + m_respData[len] = '\0'; + m_haveData = true; + } +} + +void TembooMQTTChoreo::setAckCode(uint16_t ackCode) { + //NOTE: if we run in a true multithreaded environment, this code + //will have to be synchronized somehow. Checking the m_haveAckCode + //flag is insufficient to prevent race conditions. + if (!m_haveAckCode) { + m_ackCode = ackCode; + m_haveAckCode = true; + } +} + +int TembooMQTTChoreo::run(uint16_t timeoutSecs) { + m_nextChar = NULL; + + if (!m_client.isConnected()) { + return TEMBOO_ERROR_MQTT_DISCONNECT; + } + + if (IS_EMPTY(m_accountName)) { + return TEMBOO_ERROR_ACCOUNT_MISSING; + } + + if (IS_EMPTY(m_path)) { + return TEMBOO_ERROR_CHOREO_MISSING; + } + + if (IS_EMPTY(m_appKeyName)) { + return TEMBOO_ERROR_APPKEY_NAME_MISSING; + } + + if (IS_EMPTY(m_appKeyValue)) { + return TEMBOO_ERROR_APPKEY_MISSING; + } + + m_packetStatus = TEMBOO_ERROR_OK; + + TembooMQTTSession session(m_client); + m_requestId = s_nextRequestId++; + + m_haveAckCode = false; + m_ackCode = TEMBOO_ERROR_NO_RESPONSE; + + m_haveHttpCode = false; + memset(m_httpCodeStr, '\0', sizeof(m_httpCodeStr)/sizeof(m_httpCodeStr[0])); + + m_haveData = false; + memset(m_respData, '\0', sizeof(m_respData)/sizeof(m_respData[0])); + + m_availableChars = 0; + m_nextState = START; + g_currentChoreo = this; + + TEMBOO_TRACE("RUN "); + TEMBOO_TRACE(m_requestId); + TEMBOO_TRACELN(" START"); + + ArduinoTimer timer(timeoutSecs * 1000L); + + int rc = session.executeChoreo( m_requestId, m_accountName, m_appKeyName, m_appKeyValue, m_path, m_inputs, m_outputs, m_preset); + if (TEMBOO_ERROR_OK != rc) { + goto ErrorExit; + } + + rc = waitForResponse(m_haveAckCode, session, timer); + if (TEMBOO_ERROR_OK != rc){ + goto ErrorExit; + } + + TEMBOO_TRACE("RUN "); + TEMBOO_TRACE(m_requestId); + TEMBOO_TRACE(" ACK: "); + TEMBOO_TRACELN(m_ackCode); + + if (TEMBOO_ERROR_OK != m_ackCode) { + rc = m_ackCode; + goto ErrorExit; + } + + rc = waitForResponse(m_haveHttpCode, session, timer); + if (TEMBOO_ERROR_OK != rc) { + goto ErrorExit; + } + + TEMBOO_TRACE("RUN "); + TEMBOO_TRACE(m_requestId); + TEMBOO_TRACE(" RESP: "); + TEMBOO_TRACELN(m_httpCodeStr); + + rc = waitForResponse(m_haveData, session, timer); + if (TEMBOO_ERROR_OK != rc) { + goto ErrorExit; + } + m_availableChars = strlen(m_respData) + strlen(m_httpCodeStr) + strlen_P(HTTP_CODE_PREFIX) + strlen_P(HTTP_CODE_SUFFIX); + + m_nextChar = HTTP_CODE_PREFIX; + + TEMBOO_TRACE("RUN "); + TEMBOO_TRACE(m_requestId); + TEMBOO_TRACE(" "); + TEMBOO_TRACE(m_availableChars); + TEMBOO_TRACELN(" CHARS"); + TEMBOO_TRACELN(m_respData); + + rc = m_packetStatus; + +ErrorExit: + if (TEMBOO_ERROR_OK != rc) { + TEMBOO_TRACE("RUN "); + TEMBOO_TRACE(m_requestId); + TEMBOO_TRACE(" ERROR:"); + TEMBOO_TRACELN(rc); + } + g_currentChoreo = NULL; + m_client.yield(YIELD_TIME_MILLIS); + return rc; +} + + +int TembooMQTTChoreo::available() { + return m_availableChars; +} + + +int TembooMQTTChoreo::peek() { + if (m_availableChars) { + if (m_nextState == HTTP_CODE_VALUE || m_nextState == RESP_DATA || m_nextState == END) { + return (int)*m_nextChar; + } else { + return (int)pgm_read_byte(m_nextChar); + } + } else { + return -1; + } +} + + +int TembooMQTTChoreo::read() { + + if (m_haveData) { + int c = 0; + switch(m_nextState) { + case START: + m_nextChar = HTTP_CODE_PREFIX; + c = (int)pgm_read_byte(m_nextChar++); + m_nextState = HTTP_CODE_PRE; + break; + + case HTTP_CODE_PRE: + c = (int)pgm_read_byte(m_nextChar++); + if (pgm_read_byte(m_nextChar) == '\0') { + m_nextState = HTTP_CODE_VALUE; + m_nextChar = m_httpCodeStr; + } + break; + + case HTTP_CODE_VALUE: + c = (int)(*m_nextChar++); + if (*m_nextChar == '\0') { + m_nextState = HTTP_CODE_SUF; + m_nextChar = HTTP_CODE_SUFFIX; + } + break; + + case HTTP_CODE_SUF: + c = (int)pgm_read_byte(m_nextChar++); + if (pgm_read_byte(m_nextChar) == '\0') { + m_nextState = RESP_DATA; + m_nextChar = m_respData; + } + break; + + case RESP_DATA: + c = (int)(*m_nextChar++); + if ((m_availableChars - 1) <= 0) { + m_nextState = END; + } + break; + + case END: + default: + c = -1; + } + if (m_availableChars > 0) { + m_availableChars--; + } + return c; + } else { + return -1; + } +} + + +size_t TembooMQTTChoreo::write(uint8_t data) { + return 0; +} + + +void TembooMQTTChoreo::flush() { + m_nextChar = NULL; + m_nextState = END; + m_availableChars = 0; +} + diff --git a/src/TembooMQTTEdgeDevice.h b/src/TembooMQTTEdgeDevice.h new file mode 100644 index 0000000..c667547 --- /dev/null +++ b/src/TembooMQTTEdgeDevice.h @@ -0,0 +1,238 @@ +/* +############################################################################### +# +# Temboo MQTT edge device library +# +# Copyright (C) 2015, Temboo Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +# +############################################################################### +*/ + +#ifndef TEMBOOMQTT_H_ +#define TEMBOOMQTT_H_ + +#include + + +/////////////////////////////////////////////////////// +// BEGIN ARDUINO NON-YUN SUPPORT +/////////////////////////////////////////////////////// + +#include "Arduino.h" +#include "utility/MQTTClient.h" +#include "utility/ArduinoTimer.h" +#include "utility/TembooTags.h" + +#include "utility/TembooMQTTIPStack.h" +#include "utility/ChoreoInputSet.h" +#include "utility/ChoreoOutputSet.h" +#include "utility/ChoreoPreset.h" + +#define IS_EMPTY(s) (NULL == s || '\0' == *s) + +#define TEMBOO_ERROR_OK (0) +#define TEMBOO_ERROR_FAILURE (1) +#define TEMBOO_ERROR_ACCOUNT_MISSING (201) +#define TEMBOO_ERROR_CHOREO_MISSING (203) +#define TEMBOO_ERROR_APPKEY_NAME_MISSING (205) +#define TEMBOO_ERROR_APPKEY_MISSING (207) +#define TEMBOO_ERROR_HTTP_ERROR (223) +#define TEMBOO_ERROR_TIMEOUT (225) +#define TEMBOO_ERROR_MEMORY (900) +#define TEMBOO_ERROR_TCPIP_CONNECT_FAIL (901) +#define TEMBOO_ERROR_MQTT_CONNECT_FAIL (902) +#define TEMBOO_ERROR_MQTT_SUBSCRIBE_FAIL (903) +#define TEMBOO_ERROR_MQTT_DISCONNECT (904) +#define TEMBOO_ERROR_DEVICE_ID_MISSING (905) +/* + * The data from the MQTT is too large to fit in the buffer. + * The MQTT read buffer read as much as it could and discarded + * the rest of the packet data. Increase MAX_MESSAGE_SIZE + * to read more data in MQTT's readbuf + */ +#define TEMBOO_ERROR_MQTT_BUFFER_OVERFLOW (906) +/* + * There was more data to be returned in the packet data than could + * fit in the return buffer. Incease MAX_RESPONSE_SIZE to read more + * of the packet response + */ +#define TEMBOO_ERROR_MQTT_DATA_TRUNCATED (907) +#define TEMBOO_ERROR_NO_RESPONSE (0xFFFF) + + + +static const int MAX_MESSAGE_SIZE = 512; +static const int MAX_RESPONSE_SIZE = 100; +static const int MAX_HANDLERS = 4; +static const int YIELD_TIME_MILLIS = 200; + +typedef MQTT::Client BaseClient; + +class TembooMQTTClient : public BaseClient { + public: + TembooMQTTClient(TembooMQTTIPStack& ipStack, unsigned int commandTimeoutMs = 30000 ); + virtual ~TembooMQTTClient(); + + using BaseClient::connect; + int connect(const char* hostname, int port=1883); + + using BaseClient::isConnected; + bool isConnected(); + + int sendChoreoRequest(const char* request, size_t len); + int setDeviceId(char* id); + int setDeviceIdFromMac(byte (&mac)[6]); + + protected: + TembooMQTTIPStack& m_ipStack; + char* m_deviceId; + char* m_requestTopic; + char* m_ackTopic; + char* m_responseTopic; + char* m_dataTopic; + + char* strCatNew_P(const char*, const char*); + void makeTopics(); +}; + +class TembooMQTTSession; + +class TembooMQTTChoreo : public Stream { + public: + + // Constructor. + // client - an instance of a TembooMQTTClient. + // Used to communicate with a Temboo Gateway. + TembooMQTTChoreo(TembooMQTTClient& client); + ~TembooMQTTChoreo(); + + // Does nothing. Just for source compatibility with Yun code. + void begin() {}; + + // Sets the account name to use when communicating with Temboo. + // (required) + void setAccountName(const String& accountName); + void setAccountName(const char* accountName); + + // Sets the application key name to use with choreo execution requests. + // (required) + void setAppKeyName(const String& appKeyName); + void setAppKeyName(const char* appKeyName); + + // Sets the application key value to use with choreo execution requests + // (required) + void setAppKey(const String& appKey); + void setAppKey(const char* appKey); + + // sets the name of the choreo to be executed. + // (required) + void setChoreo(const String& choreoPath); + void setChoreo(const char* choreoPath); + + + // sets the name of the saved inputs to use when executing the choreo + // (optional) + void setSavedInputs(const String& savedInputsName); + void setSavedInputs(const char* savedInputsName); + + void setCredential(const String& credentialName); + void setCredential(const char* credentialName); + + void setProfile(const String& profileName); + void setProfile(const char* profileName); + + // sets an input to be used when executing a choreo. + // (optional or required, depending on the choreo being executed.) + void addInput(const String& inputName, const String& inputValue); + void addInput(const char* inputName, const char* inputValue); + void addInput(const char* inputName, const String& inputValue); + void addInput(const String& inputName, const char* inputValue); + + // sets an output filter to be used to process the choreo output + // (optional) + void addOutputFilter(const char* filterName, const char* filterPath, const char* variableName); + void addOutputFilter(const String& filterName, const char* filterPath, const char* variableName); + void addOutputFilter(const char* filterName, const String& filterPath, const char* variableName); + void addOutputFilter(const String& filterName, const String& filterPath, const char* variableName); + void addOutputFilter(const char* filterName, const char* filterPath, const String& variableName); + void addOutputFilter(const String& filterName, const char* filterPath, const String& variableName); + void addOutputFilter(const char* filterName, const String& filterPath, const String& variableName); + void addOutputFilter(const String& filterName, const String& filterPath, const String& variableName); + + // run the choreo using the current input info + int run(uint16_t timeoutSecs); + + char* getResponseData() {return m_respData;} + char* getHTTPResponseCode() {return m_httpCodeStr;} + + + // Stream interface - see the Arduino library documentation. + void close() {}; + int available(); + int read(); + int peek(); + void flush(); + + //Print interface - see the Arduino library documentation + size_t write(uint8_t data); + + + protected: + TembooMQTTClient& m_client; + const char* m_accountName; + const char* m_appKeyName; + const char* m_appKeyValue; + const char* m_path; + + ChoreoInputSet m_inputs; + ChoreoOutputSet m_outputs; + ChoreoPreset m_preset; + + char m_httpCodeStr[4]; + volatile bool m_haveHttpCode; + + uint16_t m_ackCode; + volatile bool m_haveAckCode; + + char m_respData[MAX_RESPONSE_SIZE+1]; + volatile bool m_haveData; + + uint16_t m_requestId; + static uint16_t s_nextRequestId; + + volatile uint16_t m_packetStatus; + + // variables for the stream interface + size_t m_availableChars; + const char* m_nextChar; + enum State {START, HTTP_CODE_PRE, HTTP_CODE_VALUE, HTTP_CODE_SUF, RESP_DATA, END}; + State m_nextState; + + protected: + uint16_t getRequestId() {return m_requestId;} + void setAckCode(uint16_t ackCode); + void setHTTPResponseCode(char* respCode); + void setResponseData(char* data, size_t len); + + int waitForResponse(volatile bool& var, TembooMQTTSession& session, ArduinoTimer& timer); + + friend void handleDataMessage(MQTT::MessageData& md); + friend void handleResponseMessage(MQTT::MessageData& md); + friend void handleAckMessage(MQTT::MessageData& md); + +}; + + +#endif //TEMBOO_H_ diff --git a/src/utility/ArduinoTimer.h b/src/utility/ArduinoTimer.h new file mode 100644 index 0000000..48f26e2 --- /dev/null +++ b/src/utility/ArduinoTimer.h @@ -0,0 +1,62 @@ +/* + ############################################################################### + # + # Temboo MQTT edge device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef ARDUINOTIMER_H_ +#define ARDUINOTIMER_H_ + +class ArduinoTimer { + public: + ArduinoTimer() { + this->startTimeMillis = 0; + this->durationMillis = 0; + } + + ArduinoTimer(unsigned long durationMillis) { + this->countdown_ms(durationMillis); + } + + bool expired() { + return left_ms() == 0; + } + + void countdown_ms(unsigned long durationMillis) { + this->startTimeMillis = millis(); + this->durationMillis = durationMillis; + } + + void countdown(int durationSeconds) { + countdown_ms((unsigned long)durationSeconds * 1000L); + } + + unsigned long left_ms() { + unsigned long elapsedMillis = millis() - this->startTimeMillis; + return elapsedMillis < this->durationMillis ? (this->durationMillis - elapsedMillis) : 0; + } + + private: + unsigned long startTimeMillis; + unsigned long durationMillis; + +}; + + +#endif /* ARDUINOIMER_H_ */ diff --git a/src/utility/CoapMessageLayer.cpp b/src/utility/CoapMessageLayer.cpp new file mode 100644 index 0000000..d3aacf4 --- /dev/null +++ b/src/utility/CoapMessageLayer.cpp @@ -0,0 +1,400 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#include "utility/CoapMessageLayer.h" + +CoapMessageLayer::CoapMessageLayer(uint8_t* rxBuffer, uint16_t rxLen, TembooCoAPIPStack& ipStack) : + m_rxBuffer(rxBuffer), + m_rxLen(rxLen), + m_ipStack(ipStack), + m_state(STATE_CLOSED), + m_prevState(STATE_CLOSED), + m_retransmitCount(0), + m_lastResult(NO_ERROR), + m_msgID(0) { +} + + +CoapMessageLayer::Result CoapMessageLayer::rejectMsg(CoapMsg& msg) { + return rejectMsg(msg, m_ipStack.getRemoteAddress(), m_ipStack.getRemotePort()); +} + +CoapMessageLayer::Result CoapMessageLayer::rejectMsg(CoapMsg& msg, IPAddress addr, uint16_t port) { + msg.convertToReset(); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Sending RST"); + if (m_ipStack.sendDatagram(addr, port, msg.getMsgBytes(), msg.getMsgLen())) { + m_lastResult = ERROR_SENDING_PACKET; + } else { + m_lastResult = NO_ERROR; + if (STATE_ACK_PENDING == m_state) { + // go back to previous state since the + // message recv'd wasn't what we're expecting + m_state = m_prevState; + } + } + return m_lastResult; +} + + +CoapMessageLayer::Result CoapMessageLayer::acceptMsg(CoapMsg& msg) { + return acceptMsg(msg, m_ipStack.getRemoteAddress(), m_ipStack.getRemotePort()); +} + + + +CoapMessageLayer::Result CoapMessageLayer::acceptMsg(CoapMsg& msg, IPAddress addr, uint16_t port) { + if (m_state != STATE_ACK_PENDING) { + m_lastResult = ERROR_IMPROPER_STATE; + } else { + msg.convertToEmptyAck(); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Sending ACK"); + if (m_ipStack.sendDatagram(addr, port, msg.getMsgBytes(), msg.getMsgLen())) { + m_lastResult = ERROR_SENDING_PACKET; + } else { + m_state = STATE_CLOSED; + m_lastResult = NO_ERROR; + } + } + return m_lastResult; +} + + + +CoapMessageLayer::Result CoapMessageLayer::reliableSend(CoapMsg& msg, IPAddress destAddr, uint16_t destPort) { + + if (m_state != STATE_CLOSED) { + m_lastResult = ERROR_IMPROPER_STATE; + return m_lastResult; + } + + m_rxByteCount = 0; + m_msgID = msg.getId(); + m_msgBytes = msg.getMsgBytes(); + m_msgLen = msg.getMsgLen(); + m_destAddr = destAddr; + m_destPort = destPort; + msg.setType(CoapMsg::COAP_CONFIRMABLE); + m_retransmitCount = 0; + m_retransmitTimeoutMillis = random(ACK_TIMEOUT, MAX_ACK_TIMEOUT); + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Sending message"); + if (m_ipStack.sendDatagram(m_destAddr, m_destPort, msg.getMsgBytes(), msg.getMsgLen())) { + m_state = STATE_CLOSED; + m_lastResult = ERROR_SENDING_PACKET; + } else { + m_txSpanTimer.start(MAX_TRANSMIT_WAIT); + m_retransmitTimer.start(m_retransmitTimeoutMillis); + m_state = STATE_RELIABLE_TX; + m_lastResult = NO_ERROR; + } + + return m_lastResult; +} + + +CoapMessageLayer::Result CoapMessageLayer::cancelReliableSend() { + if (m_state != STATE_RELIABLE_TX) { + m_lastResult = ERROR_IMPROPER_STATE; + return m_lastResult; + } + + m_state = STATE_CLOSED; + m_lastResult = NO_ERROR; + return m_lastResult; +} + + + +CoapMessageLayer::Result CoapMessageLayer::loop() { + + m_lastResult = NO_ERROR; + + switch(m_state) { + case STATE_RELIABLE_TX: + // We have sent a CON request. + // We're expecting an ACK or a response. + + // See if it's time to give up all hope of getting an ACK. + if (m_txSpanTimer.expired()) { + m_lastResult = ERROR_TX_SPAN_TIME_EXCEEDED; + m_state = STATE_CLOSED; + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("ACK not received within timeout"); + break; + } + + // See if any messages have come in. + if (m_ipStack.recvDatagram(m_rxBuffer, m_rxLen, m_rxByteCount)) { + m_lastResult = ERROR_RECEIVING_PACKET; + m_state = STATE_CLOSED; + break; + } + + // We've received something. See if it's relevant. + if (m_rxByteCount > 0) { + CoapMsg msg(m_rxBuffer, m_rxLen, m_rxByteCount); + + // Make sure the message is valid + if (!msg.isValid()) { + rejectMsg(msg); + + // We're only interested in messages coming from the host + // we sent the original request to. + } else if (m_ipStack.getRemoteAddress() == m_destAddr) { + switch (msg.getType()) { + case CoapMsg::COAP_ACK: + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("ACK Received"); + // Is it ACK'ing the last request we sent? + if (msg.getId() == m_msgID) { + m_state = STATE_CLOSED; + m_lastResult = ACK_RECEIVED; + } else { + // if not, just ignore it. + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("MID did not match"); + } + + break; + + case CoapMsg::COAP_RESET: + // Is it rejecting the last request we sent? + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("RST Received"); + if (msg.getId() == m_msgID) { + m_state = STATE_CLOSED; + m_lastResult = RESET_RECEIVED; + } else { + // if not, just ignore it. + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("MID did not match"); + } + break; + + case CoapMsg::COAP_CONFIRMABLE: + // It COULD be the response to our request, or + // just some unexpected message. + // We'll let the upper layers decide. + m_prevState = m_state; + m_state = STATE_ACK_PENDING; + m_lastResult = CON_RECEIVED; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("CON Received"); + break; + + case CoapMsg::COAP_NON_CONFIRMABLE: + // It COULD be the response to our request, or + // just some unexpected message. + // We'll let the upper layers decide. + + // That's what Kovatsch et al. show in their FSM. + m_state = STATE_CLOSED; + m_lastResult = NON_RECEIVED; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("NON Received"); + break; + } + if (msg.getPayloadLen() > 0) { + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Payload data:"); + uint8_t *payload = msg.getPayload(); + uint16_t len = msg.getPayloadLen(); + for (uint16_t i = 0; i < len; i++) { + TEMBOO_TRACE((char)payload[i]); + } + TEMBOO_TRACELN(); + } + } else if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + // It's not from the host we sent the request to, but + // the sending host expects a reply, so explicitly reject it. + rejectMsg(msg); + } + + break; + } + + // Nothing was received. See if it's time to retransmit. + if (m_retransmitTimer.expired()) { + if (m_retransmitCount >= MAX_RETRANSMIT) { + // We've retried enough. Give up. + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("Maximum retransmit reached"); + m_lastResult = ERROR_RETRANSMIT_COUNT_EXCEEDED; + m_state = STATE_CLOSED; + } else { + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Retransmit message"); + m_retransmitCount++; + m_retransmitTimeoutMillis *= 2; + if (m_ipStack.sendDatagram(m_destAddr, m_destPort, m_msgBytes, m_msgLen)) { + m_state = STATE_CLOSED; + m_lastResult = ERROR_SENDING_PACKET; + } else { + m_retransmitTimer.start(m_retransmitTimeoutMillis); + m_state = STATE_RELIABLE_TX; + m_lastResult = NO_ERROR; + } + } + } + break; + + case STATE_ACK_PENDING: + // Nothing to do here but wait for + // the higher layer to accept or reject. + break; + + case STATE_WAITING_FOR_CON: + // See if any messages have come in. + if (m_ipStack.recvDatagram(m_rxBuffer, m_rxLen, m_rxByteCount)) { + m_lastResult = ERROR_RECEIVING_PACKET; + m_state = STATE_CLOSED; + break; + } + + // We've received something. See if it's relevant. + if (m_rxByteCount > 0) { + CoapMsg msg(m_rxBuffer, m_rxLen, m_rxByteCount); + + // Make sure the message is valid + if (!msg.isValid()) { + rejectMsg(msg); + + // We're only interested in messages coming from the host + // we sent the original request to. + } else if (m_ipStack.getRemoteAddress() == m_destAddr) { + switch (msg.getType()) { + case CoapMsg::COAP_ACK: + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("ACK Received"); + // Is it ACK'ing the last request we sent? + if (msg.getId() == m_msgID) { + m_state = STATE_CLOSED; + m_lastResult = ACK_RECEIVED; + } else { + // if not, just ignore it. + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("MID did not match"); + } + break; + + case CoapMsg::COAP_RESET: + // Is it rejecting the last request we sent? + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("RST Received"); + if (msg.getId() == m_msgID) { + m_state = STATE_CLOSED; + m_lastResult = RESET_RECEIVED; + } else { + // if not, just ignore it. + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("MID did not match"); + } + break; + + case CoapMsg::COAP_CONFIRMABLE: + // It COULD be the response to our request, or + // just some unexpected message. + // We'll let the upper layers decide. + m_prevState = m_state; + m_state = STATE_ACK_PENDING; + m_lastResult = CON_RECEIVED; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("CON Received"); + break; + + case CoapMsg::COAP_NON_CONFIRMABLE: + // It COULD be the response to our request, or + // just some unexpected message. + // We'll let the upper layers decide. + + // That's what Kovatsch et al. show in their FSM. + m_state = STATE_CLOSED; + m_lastResult = NON_RECEIVED; + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("NON Received"); + break; + } + if (msg.getPayloadLen() > 0) { + TEMBOO_TRACE("DBG: "); + TEMBOO_TRACELN("Payload data:"); + uint8_t *payload = msg.getPayload(); + uint16_t len = msg.getPayloadLen(); + for (uint16_t i = 0; i < len; i++) { + TEMBOO_TRACE((char)payload[i]); + } + TEMBOO_TRACELN(); + } + } else if (msg.getType() == CoapMsg::COAP_CONFIRMABLE) { + // It's not from the host we sent the request to, but + // the sending host expects a reply, so explicitly reject it. + rejectMsg(msg); + } + + break; + } + break; + + + case STATE_CLOSED: + + // We haven't sent anything that we're expecting an answer to. + // We haven't received anything that requires an answer. + // Just pump the receiver. + if (m_ipStack.recvDatagram(m_rxBuffer, m_rxLen, m_rxByteCount)) { + m_lastResult = ERROR_RECEIVING_PACKET; + break; + } + + // If we received something, handle it. + if (m_rxByteCount > 0) { + CoapMsg msg(m_rxBuffer, m_rxLen, m_rxByteCount); + switch (msg.getType()) { + case CoapMsg::COAP_ACK: + // Not expecting any ACKS, ignore it. + break; + + case CoapMsg::COAP_RESET: + // Haven't sent any CONs and we don't send NONs, ignore it. + break; + + case CoapMsg::COAP_CONFIRMABLE: + // Let the higher layers deal with this. + m_prevState = m_state; + m_state = STATE_ACK_PENDING; + m_lastResult = CON_RECEIVED; + break; + + case CoapMsg::COAP_NON_CONFIRMABLE: + // Let the higher layers deal with this. + m_lastResult = NON_RECEIVED; + break; + } + } + + break; + + } + return m_lastResult; +} + diff --git a/src/utility/CoapMessageLayer.h b/src/utility/CoapMessageLayer.h new file mode 100644 index 0000000..d9b5b74 --- /dev/null +++ b/src/utility/CoapMessageLayer.h @@ -0,0 +1,128 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef COAPMESSAGELAYER_H_ +#define COAPMESSAGELAYER_H_ + +#include "TembooCoAPIPStack.h" +#include "TembooTimer.h" +#include "CoapMsg.h" + +/** + * CoapMessageLayer is the lowest layer of the CoAP stack. It is responsible for + * transmitting and receiving messages. Specifically, this implementation is + * responsible for handling confirmable (CON) messages and their ACKs. + * It is intended to implement the Message Layer FSM described by Kovatsch et al. in + * https://tools.ietf.org/html/draft-kovatsch-lwig-coap-01 + * + * It can send reliable (confirmable or CON) messages and will maintain the necessary + * state information to wait for an acknowledgement. It will handle any necessary + * retransmissions and timeouts until a CON message has been ACK'd (or rejected.) + * + * It can not send unreliable (non-confirmable or NON) messages as currently designed + * because our application does not use NON messages. + * + * It can receive CON and NON messages and will send ACKs or RESETs as required. + * + * Any non-rejected CON or NON messages received are passed up to the Request/Response + * layer (CoapRRLayer) for processing. + * + * Be sure to note the difference between ACK's and Responses. This layer handles + * ACK's, it does not handle Responses. (Responses are handled by the CoapRRLayer class.) + */ + + +class CoapMessageLayer { + public: + + enum State { + STATE_CLOSED, + STATE_RELIABLE_TX, + STATE_ACK_PENDING, + STATE_WAITING_FOR_CON + }; + + enum Result { + NO_ERROR = 0, + CON_RECEIVED, + NON_RECEIVED, + ACK_RECEIVED, + RESET_RECEIVED, + ERROR_IMPROPER_STATE, + ERROR_NULL_MESSAGE, + ERROR_SENDING_PACKET, + ERROR_RECEIVING_PACKET, + ERROR_RETRANSMIT_COUNT_EXCEEDED, + ERROR_TX_SPAN_TIME_EXCEEDED + }; + + static const uint32_t ACK_TIMEOUT = 2000; + + // MAX_ACK_TIMEOUT = ACK_TIMEOUT * ACK_RANDOM_FACTOR + // RFC7252 suggests ACK_RANDOM_FACTOR = 1.5 + static const uint32_t MAX_ACK_TIMEOUT = 3000; + static const uint8_t MAX_RETRANSMIT = 4; + + // MAX_TRANSMIT_SPAN = ACK_TIMEOUT * ((2^MAX_RETRANSMIT) - 1) * ACK_RANDOM_FACTOR + static const uint32_t MAX_TRANSMIT_SPAN = 45000; + + // MAX_TRANSMIT_WAIT = ACK_TIMEOUT * (2^(MAX_RETRANSMIT + 1) - 1) * ACK_RANDOM_FACTOR + static const uint32_t MAX_TRANSMIT_WAIT = 93000; + + + CoapMessageLayer(uint8_t* rxBuffer, uint16_t rxLen, TembooCoAPIPStack& ipStack); + Result reliableSend(CoapMsg& msg, IPAddress destAddr, uint16_t destPort); + Result cancelReliableSend(); + Result acceptMsg(CoapMsg& msg); + Result acceptMsg(CoapMsg& msg, IPAddress addr, uint16_t port); + Result rejectMsg(CoapMsg& msg); + Result rejectMsg(CoapMsg& msg, IPAddress addr, uint16_t port); + Result loop(); + Result getLastResult() {return m_lastResult;} + uint16_t getRXByteCount() {return m_rxByteCount;} + void setState(State state) {m_state = state;} + + + private: + uint8_t* m_rxBuffer; + uint16_t m_rxLen; + TembooCoAPIPStack& m_ipStack; + + State m_state; + State m_prevState; + int m_retransmitCount; + Result m_lastResult; + IPAddress m_destAddr; + uint16_t m_destPort; + + uint16_t m_msgID; + uint8_t* m_msgBytes; + uint16_t m_msgLen; + int32_t m_rxByteCount; + TembooTimer m_txSpanTimer; + TembooTimer m_retransmitTimer; + uint32_t m_retransmitTimeoutMillis; + + +}; + +#endif diff --git a/src/utility/CoapMsg.cpp b/src/utility/CoapMsg.cpp new file mode 100644 index 0000000..5743aac --- /dev/null +++ b/src/utility/CoapMsg.cpp @@ -0,0 +1,714 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#include +#include +#include "CoapMsg.h" +#include "TembooGlobal.h" + +const uint8_t COAP_VERSION = 1; +const uint8_t HEADER_LENGTH = 4; + + +CoapMsg::CoapMsg(uint8_t* buffer, uint16_t bufferLen) : m_buffer(buffer), m_bufferLen(bufferLen) { + + // Starting a new message. All messages have a header. + m_msgLen = HEADER_LENGTH; + memset(m_buffer, 0, m_bufferLen); + m_buffer[0] = COAP_VERSION << 6; + m_buildState = BUILD_BEGIN; + m_lastOptionCode = 0; +} + + + +CoapMsg::CoapMsg(uint8_t* buffer, uint16_t bufferLen, uint16_t packetLen) : m_buffer(buffer), m_bufferLen(bufferLen) { + m_msgLen = packetLen; + m_buildState = BUILD_HAVE_PAYLOAD; + m_lastOptionCode = 0; +} + + + +void CoapMsg::setType(CoapMsg::Type msgType) { + m_buffer[0] &= 0xCF; //11001111 + m_buffer[0] |= msgType << 4; +} + + + +CoapMsg::Type CoapMsg::getType() { + return (CoapMsg::Type)((m_buffer[0] & 0x30) >> 4); +} + + + +void CoapMsg::setId(uint16_t msgId) { + m_buffer[2] = msgId >> 8; + m_buffer[3] = msgId & 0x00FF; +} + + + +uint16_t CoapMsg::getId() { + return (m_buffer[2] << 8) + m_buffer[3]; +} + + + +CoapMsg::Result CoapMsg::setToken(const uint8_t* token, uint8_t tokenLen){ + + if (m_buildState >= BUILD_HAVE_TOKEN) { + return COAP_RESULT_BUILD_ORDER; + } + + if (tokenLen > 8){ + return COAP_RESULT_TOKEN_LENGTH; + } + + if ((tokenLen > 0) && (NULL == token)) { + return COAP_RESULT_TOKEN_NULL; + } + + if ((m_msgLen + tokenLen) > m_bufferLen) { + return COAP_RESULT_BUFFER_OVERRUN; + } + + memcpy(m_buffer + m_msgLen, token, tokenLen); + m_msgLen += tokenLen; + m_buffer[0] &= 0xF0; + m_buffer[0] |= tokenLen; + m_buildState = BUILD_HAVE_TOKEN; + + return COAP_RESULT_SUCCESS; +} + + + +uint8_t* CoapMsg::getToken() { + if (getTokenLen() > 0) { + return m_buffer + HEADER_LENGTH; + } + return NULL; +} + + + +uint8_t CoapMsg::getTokenLen() { + return m_buffer[0] & 0x0F; +} + + + +void CoapMsg::setCode(CoapMsg::Code code) { + m_buffer[1] = (uint8_t)code; +} + + + +CoapMsg::Code CoapMsg::getCode() { + return (CoapMsg::Code)(m_buffer[1]); +} + + + +uint16_t CoapMsg::getHTTPStatus() { + uint8_t code = getCode(); + // 3 MSbs are the most significant digit (0 - 6) + // 5 LSbs are the two least significant digits (0 - 31) + return (code >> 5) * 100 + (code & 0x1F); +} + + + +CoapMsg::Result CoapMsg::setPayload(const uint8_t* payload, uint16_t payloadLen) { + + if (m_buildState >= BUILD_HAVE_PAYLOAD) { + return COAP_RESULT_BUILD_ORDER; + } + + if (payloadLen > 0 && payload == NULL) { + return COAP_RESULT_PAYLOAD_NULL; + } + + if ((m_msgLen + payloadLen + 1) > m_bufferLen) { + return COAP_RESULT_BUFFER_OVERRUN; + } + + // Add the special payload marker flag. + m_buffer[m_msgLen++] = 0xFF; + memcpy(m_buffer + m_msgLen, payload, payloadLen); + m_msgLen += payloadLen; + m_buildState = BUILD_HAVE_PAYLOAD; + + return COAP_RESULT_SUCCESS; +} + + + +uint8_t* CoapMsg::getPayload() { + if (m_buildState < BUILD_HAVE_PAYLOAD) { + return NULL; + } + + uint8_t* payload = NULL; + uint16_t optionDelta = 0; + uint16_t optionLen = 0; + uint8_t* i = m_buffer + HEADER_LENGTH + getTokenLen(); + + while (i < m_buffer + m_msgLen) { + i = decodeOption(i, &optionDelta, &optionLen); + if ((optionDelta == 15) || (optionLen == 15)) { + //Technically, if either optionDelta or optionLen is 15, then + //both MUST be 15, else it's a malformed message. However, + //we're going to be a little loose with the spec here. + payload = i; + break; + } + } + + return payload; +} + + + +uint16_t CoapMsg::getPayloadLen() { + if (m_buildState < BUILD_HAVE_PAYLOAD) { + return 0; + } + + uint8_t* payload = getPayload(); + if (NULL == payload) { + return 0; + } + return m_buffer + m_msgLen - payload; +} + + + +/* + 0 1 2 3 4 5 6 7 + +---------------+---------------+ + | | | + | Option Delta | Option Length | 1 byte + | | | + +---------------+---------------+ + \ \ 0 bytes if OptionDelta <= 12 + / Option Delta / 1 byte if OptionDelte == 13 + \ (extended) \ 2 bytes if OptionDelta == 14 + +-------------------------------+ + \ \ 0 bytes if OptionLength <= 12 + / Option Length / 1 byte if OptionLength == 13 + \ (extended) \ 2 bytes if OptionLength == 14 + +-------------------------------+ + \ \ + / / + \ \ + / Option Value / 0 or more bytes (i.e. OptionLength bytes) + \ \ + / / + \ \ + +-------------------------------+ + */ + +CoapMsg::Result CoapMsg::addOption(CoapMsg::Option optionCode, const uint8_t* optionValue, uint16_t optionLen) { + + if (m_buildState > BUILD_HAVE_OPTIONS) { + return COAP_RESULT_BUILD_ORDER; + } + + if (m_lastOptionCode > optionCode) { + return COAP_RESULT_BUILD_ORDER; + } + + if (optionLen > 0 && NULL == optionValue) { + return COAP_RESULT_OPTION_NULL; + } + + CoapMsg::Result rc = validateOption(optionCode, optionValue, optionLen); + if (COAP_RESULT_SUCCESS != rc) { + return rc; + } + + // See if there's enough room in the buffer to add this option. + uint16_t byteCount = 1; + uint16_t optionDelta = optionCode - m_lastOptionCode; + if (optionDelta >= 13) { + byteCount++; + } + if (optionDelta >= 269) { + byteCount++; + } + if (optionLen >= 13) { + byteCount++; + } + if (optionLen >= 269) { + byteCount++; + } + byteCount += optionLen; + + if (m_msgLen + byteCount > m_bufferLen) { + return COAP_RESULT_BUFFER_OVERRUN; + } + + // If we get this far, there's enough room. + byteCount = 0; + if (optionDelta >= 269) { + m_buffer[m_msgLen] = 14 << 4; + byteCount++; + m_buffer[m_msgLen + byteCount] = (optionDelta - 269) >> 8; + byteCount++; + m_buffer[m_msgLen + byteCount] = (optionDelta - 269) & 0x00FF; + } else if (optionDelta >= 13) { + m_buffer[m_msgLen] = 13 << 4; + byteCount++; + m_buffer[m_msgLen + byteCount] = (optionDelta - 13); + } else { + m_buffer[m_msgLen] = optionDelta << 4; + } + + if (optionLen >= 269) { + m_buffer[m_msgLen] = (m_buffer[m_msgLen] & 0XF0) | 14; + byteCount++; + m_buffer[m_msgLen + byteCount] = (optionLen - 269) >> 8; + byteCount++; + m_buffer[m_msgLen + byteCount] = (optionLen - 269) & 0xFF; + } else if (optionLen >= 13) { + m_buffer[m_msgLen] = (m_buffer[m_msgLen] & 0xF0) | 13; + byteCount++; + m_buffer[m_msgLen + byteCount] = (optionLen - 13); + } else { + m_buffer[m_msgLen] = (m_buffer[m_msgLen] & 0xF0) | optionLen; + byteCount++; + } + + m_msgLen += byteCount; + if (optionLen > 0) { + memcpy(m_buffer + m_msgLen, optionValue, optionLen); + } + m_msgLen += optionLen; + m_lastOptionCode = optionCode; + return COAP_RESULT_SUCCESS; +} + + + +CoapMsg::Result CoapMsg::validateOption(CoapMsg::Option optionCode, const uint8_t* optionValue, uint16_t optionLen) { + + CoapMsg::Result rc = COAP_RESULT_SUCCESS; + + switch(optionCode) { + case COAP_OPTION_IF_MATCH: + rc = validateOptionValue(0, 8, optionValue, optionLen); + break; + + case COAP_OPTION_URI_HOST: + rc = validateOptionValue(1, 255, optionValue, optionLen); + break; + + case COAP_OPTION_ETAG: + rc = validateOptionValue(1, 8, optionValue, optionLen); + break; + + case COAP_OPTION_IF_NONE_MATCH: + rc = validateOptionValue(0, 0, optionValue, optionLen); + break; + + //TODO: case COAP_OPTION_OBSERVE: + // rc = validateOptionValue(0, 0, optionValue, optionLen); + // break; + + case COAP_OPTION_URI_PORT: + rc = validateOptionValue(0, 2, optionValue, optionLen); + break; + + case COAP_OPTION_LOCATION_PATH: + rc = validateOptionValue(0, 255, optionValue, optionLen); + break; + + case COAP_OPTION_URI_PATH: + rc = validateOptionValue(0, 255, optionValue, optionLen); + break; + + case COAP_OPTION_CONTENT_FORMAT: + rc = validateOptionValue(0, 2, optionValue, optionLen); + break; + + case COAP_OPTION_MAX_AGE: + rc = validateOptionValue(0, 4, optionValue, optionLen); + break; + + case COAP_OPTION_URI_QUERY: + rc = validateOptionValue(0, 255, optionValue, optionLen); + break; + + case COAP_OPTION_ACCEPT: + rc = validateOptionValue(0, 2, optionValue, optionLen); + break; + + case COAP_OPTION_LOCATION_QUERY: + rc = validateOptionValue(0, 255, optionValue, optionLen); + break; + + case COAP_OPTION_BLOCK2: + rc = validateOptionValue(0, 3, optionValue, optionLen); + break; + + case COAP_OPTION_BLOCK1: + rc = validateOptionValue(0, 3, optionValue, optionLen); + break; + + case COAP_OPTION_SIZE2: + rc = validateOptionValue(0, 4, optionValue, optionLen); + break; + + case COAP_OPTION_PROXY_URI: + rc = validateOptionValue(0, 1034, optionValue, optionLen); + break; + + case COAP_OPTION_PROXY_SCHEME: + rc = validateOptionValue(1, 255, optionValue, optionLen); + break; + + case COAP_OPTION_SIZE1: + rc = validateOptionValue(0, 4, optionValue, optionLen); + break; + + default: + rc = COAP_RESULT_OPTION_UNKNOWN; + + } + return rc; +} + + + +CoapMsg::Result CoapMsg::validateOptionValue(uint16_t minLen, uint16_t maxLen, const uint8_t* optionValue, uint16_t optionLen) { + + if (optionLen < minLen || optionLen > maxLen) { + return COAP_RESULT_OPTION_LENGTH; + } + if (optionLen > 0 && NULL == optionValue) { + return COAP_RESULT_OPTION_NULL; + } + + //TODO: Maybe. Validate value format (uint vs string vs opaque) + + return COAP_RESULT_SUCCESS; +} + + + +uint8_t* CoapMsg::getMsgBytes() { + return m_buffer; +} + + + +uint16_t CoapMsg::getMsgLen() { + return m_msgLen; +} + + + +uint8_t* CoapMsg::decodeOption(uint8_t* buffer, uint16_t* optionDelta, uint16_t* optionLen) { + + *optionDelta = *buffer >> 4; + *optionLen = *buffer & 0x0F; + buffer++; + + if (13 == *optionDelta) { + *optionDelta = *buffer++ + 13; + } else if (14 == *optionDelta) { + *optionDelta = *buffer++ << 8; + *optionDelta += *buffer++; + *optionDelta += 269; + } + + if (13 == *optionLen) { + *optionLen = *buffer++ + 13; + } else if (14 == *optionLen) { + *optionLen = *buffer++ << 8; + *optionLen += *buffer++; + *optionLen += 269; + } + + if (*optionLen != 15) { + buffer += *optionLen; + } + + return buffer; +} + + + +uint16_t CoapMsg::getOptionCount(CoapMsg::Option optionCode) { + + if (m_buildState < BUILD_HAVE_OPTIONS) { + return 0; + } + + uint16_t count = 0; + uint16_t lastOption = 0; + uint16_t optionDelta = 0; + uint16_t optionLen = 0; + uint8_t* i = m_buffer + HEADER_LENGTH + getTokenLen(); + + while (i < (m_buffer + m_msgLen) && *i != 0xFF) { + i = decodeOption(i, &optionDelta, &optionLen); + lastOption += optionDelta; + if (lastOption == optionCode) { + count++; + } + } + return count; +} + + + +uint16_t CoapMsg::getOptionLen(CoapMsg::Option optionCode, uint16_t index) { + + uint16_t count = 0; + uint16_t lastOption = 0; + uint16_t optionDelta = 0; + uint16_t optionLen = 0; + uint8_t* i = m_buffer + HEADER_LENGTH + getTokenLen(); + + while (i < (m_buffer + m_msgLen) && *i != 0xFF) { + i = decodeOption(i, &optionDelta, &optionLen); + lastOption += optionDelta; + if (lastOption == optionCode) { + if (count == index) { + break; + } + count++; + } + optionLen = 0; + } + return optionLen; +} + + + +uint8_t* CoapMsg::getOptionValue(CoapMsg::Option optionCode, uint16_t index) { + uint16_t count = 0; + uint16_t lastOption = 0; + uint16_t optionDelta = 0; + uint16_t optionLen = 0; + uint8_t* i = m_buffer + HEADER_LENGTH + getTokenLen(); + + while (i < (m_buffer + m_msgLen) && *i != 0xFF) { + i = decodeOption(i, &optionDelta, &optionLen); + lastOption += optionDelta; + if (lastOption == optionCode) { + if (count == index) { + return i - optionLen; + } + count++; + } + } + return NULL; +} + + + +CoapMsg::Result CoapMsg::getOption(CoapMsg::Option optionCode, uint16_t index, uint8_t*& optionValue, uint16_t& optionLen) { + + uint16_t count = 0; + uint16_t lastOption = 0; + uint16_t optionDelta = 0; + uint16_t optLen = 0; + uint8_t* i = m_buffer + HEADER_LENGTH + getTokenLen(); + + while (i < (m_buffer + m_msgLen) && *i != 0xFF) { + i = decodeOption(i, &optionDelta, &optLen); + lastOption += optionDelta; + if (lastOption == optionCode) { + if (count == index) { + optionValue = i - optLen; + optionLen = optLen; + return COAP_RESULT_SUCCESS; + } + count++; + } + } + return COAP_RESULT_OPTION_NOT_FOUND; + +} + +uint16_t CoapMsg::getBlock1Size() { + return getBlockSize(COAP_OPTION_BLOCK1); +} + +uint16_t CoapMsg::getBlock2Size() { + return getBlockSize(COAP_OPTION_BLOCK2); +} + +uint16_t CoapMsg::getBlockSize(CoapMsg::Option optionCode) { + if (m_buildState < BUILD_HAVE_OPTIONS) { + return 0; + } + + uint16_t optionLen; + uint8_t* optionValue; + if (getOption(optionCode, 0, optionValue, optionLen) != COAP_RESULT_SUCCESS) { + return 0; + } + uint16_t blockLen = 16 << (optionValue[optionLen - 1] & 0x07); + if (blockLen > 1024) { + return 0; + } + return blockLen; +} + + + +uint32_t CoapMsg::getBlock1Num() { + return getBlockNum(COAP_OPTION_BLOCK1); +} + + + +uint32_t CoapMsg::getBlock2Num() { + return getBlockNum(COAP_OPTION_BLOCK2); +} + + + +uint32_t CoapMsg::getBlockNum(CoapMsg::Option optionCode) { + if (m_buildState < BUILD_HAVE_OPTIONS) { + return 0; + } + + uint16_t optionLen; + uint8_t* optionValue; + if (getOption(optionCode, 0, optionValue, optionLen) != COAP_RESULT_SUCCESS) { + return 0; + } + int32_t blockNum = 0; + for (;optionLen > 0; optionLen--) { + blockNum <<= 8; + blockNum += *optionValue; + optionValue++; + } + blockNum >>= 4; + + return blockNum; +} + + + +bool CoapMsg::getBlock1More() { + return getBlockMore(COAP_OPTION_BLOCK1); +} + + + +bool CoapMsg::getBlock2More() { + return getBlockMore(COAP_OPTION_BLOCK2); +} + + + +bool CoapMsg::getBlockMore(CoapMsg::Option optionCode) { + if (m_buildState < BUILD_HAVE_OPTIONS) { + return 0; + } + + uint16_t optionLen; + uint8_t* optionValue; + if (getOption(optionCode, 0, optionValue, optionLen) != COAP_RESULT_SUCCESS) { + return 0; + } + if (optionLen == 0) { + return false; + } + + return (optionValue[optionLen-1] & 0x08) > 0; +} + + +/** + * Convert this (existing received) message into a reset message. + */ +void CoapMsg::convertToReset() { + setType(COAP_RESET); + setCode(COAP_EMPTY); + + // Set token length to 0 + m_buffer[0] &= 0xF0; + + m_msgLen = HEADER_LENGTH; +} + +void CoapMsg::convertToEmptyAck() { + setType(COAP_ACK); + setCode(COAP_EMPTY); + m_buffer[0] &= 0xF0; + m_msgLen = HEADER_LENGTH; +} + +bool CoapMsg::isValid() { + // check packet size + if (m_msgLen < 4) { + TEMBOO_TRACE("Packet must be a minimum of 4 bytes\n"); + return false; + } + // check token length + if (getTokenLen() < 0 || getTokenLen() > 8) { + TEMBOO_TRACE("Invalid token length\n"); + return false; + } + + int16_t responseClass = m_buffer[1] >> 5; + // check HTTP Code is between 000-599 + if (responseClass < 0 || responseClass > 5) { + TEMBOO_TRACE("Invalid code\n"); + return false; + } + + if (HEADER_LENGTH + getTokenLen() == m_msgLen) { + // nothing else in the packet + return true; + } + + uint16_t count =0; + uint16_t lastOption = 0; + uint16_t optionDelta = 0; + uint16_t optionLen = 0; + uint8_t* i = m_buffer + HEADER_LENGTH + getTokenLen(); + // validate options + while (i < (m_buffer + m_msgLen) && *i != 0xFF) { + i = decodeOption(i, &optionDelta, &optionLen); + lastOption += optionDelta; + if (validateOption((Option)lastOption, i - optionLen, optionLen)) { + TEMBOO_TRACE("Invalid option\n"); + return false; + } + } + // if payload marker exists, make sure there is payload data + if (*i == 0xFF && (i - m_buffer) > m_msgLen) { + return false; + } + + return true; +} diff --git a/src/utility/CoapMsg.h b/src/utility/CoapMsg.h new file mode 100644 index 0000000..fd64b35 --- /dev/null +++ b/src/utility/CoapMsg.h @@ -0,0 +1,192 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef COAPMSG_H_ +#define COAPMSG_H_ + +/* + Byte 0 Byte 1 Byte 2 Byte 3 + MSB LSB MSB LSB MSB LSB MSB LSB + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + |Ver| T | TKL | | Code | | MsgID MSB | | MsgID LSB | + +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + | Token (if any, TKL bytes) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options (if any) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |1 1 1 1 1 1 1 1| Payload (if any) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + +#define RESPONSE_CODE(class, detail) ((class << 5) + detail) + +class CoapMsg { + + public: + enum Type { + COAP_CONFIRMABLE = 0, + COAP_NON_CONFIRMABLE = 1, + COAP_ACK = 2, + COAP_RESET = 3 + }; + + enum Code { + //REQUEST CODES + COAP_EMPTY = 0, + COAP_GET = 1, + COAP_POST = 2, + COAP_PUT = 3, + COAP_DELETE = 4, + + //RESPONSE CODES + COAP_CREATED = RESPONSE_CODE(2,1), + COAP_DELETED = RESPONSE_CODE(2,2), + COAP_VALID = RESPONSE_CODE(2,3), + COAP_CHANGED = RESPONSE_CODE(2,4), + COAP_CONTENT = RESPONSE_CODE(2,5), + COAP_CONTINUE = RESPONSE_CODE(2,31), + COAP_BAD_REQUEST = RESPONSE_CODE(4,0), + COAP_UNAUTHORIZED = RESPONSE_CODE(4,1), + COAP_BAD_OPTION = RESPONSE_CODE(4,2), + COAP_FORBIDDEN = RESPONSE_CODE(4,3), + COAP_NOT_FOUND = RESPONSE_CODE(4,4), + COAP_METHOD_NOT_ALLOWED = RESPONSE_CODE(4,5), + COAP_NOT_ACCEPTABLE = RESPONSE_CODE(4,6), + COAP_REQUEST_ENTITY_INCOMPLETE = RESPONSE_CODE(4,8), + COAP_PRECONDITION_FAILED = RESPONSE_CODE(4,12), + COAP_REQUEST_ENTITY_TOO_LARGE = RESPONSE_CODE(4,13), + COAP_UNSUPPORTED_CONTENT_FORMAT = RESPONSE_CODE(4,15), + COAP_INTERNAL_SERVER_ERROR = RESPONSE_CODE(5,0), + COAP_NOT_IMPLEMENTED = RESPONSE_CODE(5,1), + COAP_BAD_GATEWAY = RESPONSE_CODE(5,2), + COAP_SERVICE_UNAVAILABLE = RESPONSE_CODE(5,3), + COAP_GATEWAY_TIMEOUT = RESPONSE_CODE(5,4), + COAP_PROXYING_NOT_SUPPORTED = RESPONSE_CODE(5,5) + }; + + enum Option { + COAP_OPTION_IF_MATCH = 1, + COAP_OPTION_URI_HOST = 3, + COAP_OPTION_ETAG = 4, + COAP_OPTION_IF_NONE_MATCH = 5, + //TODO: COAP_OPTION_OBSERVE = 6, + COAP_OPTION_URI_PORT = 7, + COAP_OPTION_LOCATION_PATH = 8, + COAP_OPTION_URI_PATH = 11, + COAP_OPTION_CONTENT_FORMAT = 12, + COAP_OPTION_MAX_AGE = 14, + COAP_OPTION_URI_QUERY = 15, + COAP_OPTION_ACCEPT = 17, + COAP_OPTION_LOCATION_QUERY = 20, + COAP_OPTION_BLOCK2 = 23, + COAP_OPTION_BLOCK1 = 27, + COAP_OPTION_SIZE2 = 28, + COAP_OPTION_PROXY_URI = 35, + COAP_OPTION_PROXY_SCHEME = 39, + COAP_OPTION_SIZE1 = 60 + }; + + enum Result { + COAP_RESULT_SUCCESS = 0, // No error. + COAP_RESULT_TOKEN_NULL, // Token length > 0 but NULL pointer given for token value. + COAP_RESULT_TOKEN_LENGTH, // Illegal token length value (> 8). + COAP_RESULT_PAYLOAD_NULL, // Payload length > 0 but NULL pointer given for payload value. + COAP_RESULT_OPTION_UNKNOWN, // An unknown option code was specified. + COAP_RESULT_OPTION_NULL, // Option length > 0 but NULL pointer given for option value. + COAP_RESULT_OPTION_LENGTH, // Illegal length for option specified. + COAP_RESULT_OPTION_NOT_FOUND,// The requested option was not found in the message. + COAP_RESULT_BUFFER_OVERRUN, // Adding data would overrun the packet buffer + COAP_RESULT_BUILD_ORDER, // Message build order incorrect. + COAP_RESULT_INVALID_MSG, // Received message is malformed or invalid. + COAP_RESULT_FAILURE // Operation failed or unspecified error + }; + + CoapMsg(uint8_t* buffer, uint16_t bufferLen); + CoapMsg(uint8_t* buffer, uint16_t bufferLen, uint16_t packetLen); + + void setType(CoapMsg::Type msgType); + CoapMsg::Type getType(); + + void setId(uint16_t msgId); + uint16_t getId(); + + void setCode(CoapMsg::Code code); + CoapMsg::Code getCode(); + uint16_t getHTTPStatus(); + + CoapMsg::Result setToken(const uint8_t* token, uint8_t tokenLen); + uint8_t* getToken(); + uint8_t getTokenLen(); + + CoapMsg::Result addOption(CoapMsg::Option optionCode, const uint8_t* optionValue, uint16_t optionLen); + CoapMsg::Result getOption(CoapMsg::Option optionCode, uint16_t index, uint8_t*& optionValue, uint16_t& optionLen); + uint16_t getOptionCount(CoapMsg::Option optionCode); + uint16_t getOptionLen(CoapMsg::Option optionCode, uint16_t index); + uint8_t* getOptionValue(CoapMsg::Option optionCode, uint16_t index); + + CoapMsg::Result setPayload(const uint8_t* payload, uint16_t payloadLen); + uint8_t* getPayload(); + uint16_t getPayloadLen(); + + uint8_t* getMsgBytes(); + uint16_t getMsgLen(); + + bool isValid(); + + uint16_t getBlock1Size(); + uint32_t getBlock1Num(); + bool getBlock1More(); + + uint16_t getBlock2Size(); + uint32_t getBlock2Num(); + bool getBlock2More(); + + void convertToReset(); + void convertToEmptyAck(); + + protected: + uint8_t* m_buffer; + uint16_t m_bufferLen; + uint16_t m_msgLen; + + enum BuildState { + BUILD_BEGIN, + BUILD_HAVE_TOKEN, + BUILD_HAVE_OPTIONS, + BUILD_HAVE_PAYLOAD + }; + CoapMsg::BuildState m_buildState; + uint16_t m_lastOptionCode; + + protected: + CoapMsg::Result validateOption(CoapMsg::Option optionCode, const uint8_t* optionValue, uint16_t optionLen); + CoapMsg::Result validateOptionValue(uint16_t minLen, uint16_t maxLen, const uint8_t* optionValue, uint16_t optionLen); + uint8_t* decodeOption(uint8_t* buffer, uint16_t* optionDelta, uint16_t* optionLen); + uint16_t getBlockSize(CoapMsg::Option optionCode); + uint32_t getBlockNum(CoapMsg::Option optionCode); + bool getBlockMore(CoapMsg::Option optionCode); +}; + + +#endif //TEMBOOCOAP_H_ diff --git a/src/utility/CoapRRLayer.cpp b/src/utility/CoapRRLayer.cpp new file mode 100644 index 0000000..f146dd9 --- /dev/null +++ b/src/utility/CoapRRLayer.cpp @@ -0,0 +1,176 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#include "CoapRRLayer.h" + +CoapRRLayer::CoapRRLayer(CoapMessageLayer& messageLayer, uint8_t* rxBuffer, uint16_t rxBufferLen) : + m_messageLayer(messageLayer), + m_state(STATE_IDLE), + m_lastResult(NO_ERROR), + m_token(NULL), + m_rxBuffer(rxBuffer), + m_rxBufferLen(rxBufferLen) { +} + + + +CoapRRLayer::Result CoapRRLayer::reliableSend(CoapMsg& msg, char* token, IPAddress addr, uint16_t port) { + if (m_state != STATE_IDLE) { + return ERROR_IMPROPER_STATE; + } + + if (CoapMessageLayer::NO_ERROR != m_messageLayer.reliableSend(msg, addr, port)) { + return ERROR_SENDING_MSG; + } + + m_token = token; + m_state = STATE_WAITING; + return NO_ERROR; +} + + + +bool CoapRRLayer::rxTokenMatches(CoapMsg& msg) { + if (msg.getTokenLen() != strlen(m_token)) { + return false; + } + + if (0 != memcmp(msg.getToken(), m_token, strlen(m_token))) { + return false; + } + return true; +} + + + +CoapRRLayer::Result CoapRRLayer::loop() { + + m_lastResult = NO_ERROR; + + switch (m_state) { + + case STATE_IDLE: + + // Pump the receiver. + // We're not serving anything, so unless there's an outstanding + // request (which would mean we would be in STATE_WAITING, not STATE_IDLE), + // we're going to reject or ignore any incoming traffic. + switch(m_messageLayer.loop()) { + case CON_RECEIVED: + { + // Explicitly reject any CON messages so the sender will + // quit bugging us with retransmissions. + CoapMsg msg(m_rxBuffer, m_rxBufferLen, m_messageLayer.getRXByteCount()); + m_messageLayer.rejectMsg(msg); + break; + } + default: + // Just ignore anything else. + break; + } + + break; + + case STATE_WAITING: + // We're waiting for a response to an earlier request. + switch(m_messageLayer.loop()) { + + case CoapMessageLayer::NO_ERROR: + // Nothing happened. Nothing to do. + break; + + case CoapMessageLayer::ACK_RECEIVED: + { + CoapMsg msg(m_rxBuffer, m_rxBufferLen, m_messageLayer.getRXByteCount()); + + // If it wasn't an empty ack, it's a response. + // And if the token matches, then it's the response we're waiting for. + if (rxTokenMatches(msg)) { + m_lastResult = RESPONSE_RECEIVED; + m_state = STATE_IDLE; + } else { + // if ACK is not empty and tokens don't match, an error has occurred + if (msg.getTokenLen() != 0) { + m_lastResult = ERROR_RECEIVING_RESPONSE; + m_state = STATE_IDLE; + TEMBOO_TRACE("Error: "); + TEMBOO_TRACELN("Msg token did not match"); + } + } + break; + } + + case CoapMessageLayer::RESET_RECEIVED: + { + // If it was a reset, the message should be empty + // and there will be no token + m_lastResult = RST_RECEIVED; + m_state = STATE_IDLE; + break; + } + + case CoapMessageLayer::CON_RECEIVED: + { + // See if this is our response or just some random message. + // The message layer has already confirmed it's from the right host. + CoapMsg msg(m_rxBuffer, m_rxBufferLen, m_messageLayer.getRXByteCount()); + + // We only accept responses for the current request (i.e. the tokens must match) + if (rxTokenMatches(msg)) { + m_lastResult = RESPONSE_RECEIVED; + m_state = STATE_IDLE; + } else { + m_messageLayer.rejectMsg(msg); + TEMBOO_TRACE("Error: "); + TEMBOO_TRACELN("Msg token did not match"); + } + break; + } + + case CoapMessageLayer::NON_RECEIVED: + { + CoapMsg msg(m_rxBuffer, m_rxBufferLen, m_messageLayer.getRXByteCount()); + if (rxTokenMatches(msg)) { + m_lastResult = RESPONSE_RECEIVED; + m_state = STATE_IDLE; + } else { + // if the token does not match, then an error occurred + m_lastResult = ERROR_RECEIVING_RESPONSE; + m_state = STATE_IDLE; + TEMBOO_TRACE("Error: "); + TEMBOO_TRACELN("Msg token did not match"); + } + break; + } + + default: + // Anything else indicates a failure of some sort. Check + // the messageLayer lastResult for specifics. + m_lastResult = ERROR_RECEIVING_RESPONSE; + m_state = STATE_IDLE; + + } + break; + } + + return m_lastResult; +} diff --git a/src/utility/CoapRRLayer.h b/src/utility/CoapRRLayer.h new file mode 100644 index 0000000..e84c04b --- /dev/null +++ b/src/utility/CoapRRLayer.h @@ -0,0 +1,82 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef COAPRRLAYER_H_ +#define COAPRRLAYER_H_ + +#include "CoapMessageLayer.h" +#include "CoapMsg.h" + +/** + * CoapRRLayer (Request/Response layer) is a middle layer of the CoAP stack. It is responsible for + * sending requests and receiving responses to those requests. Note that it does not handle + * ACKs or RESETs. Those are handled at the CoapMessageLayer. + * + * This class is intended to implement the CoAP Client Request/Response Layer FSM as described + * by Kovatsch et al. in https://tools.ietf.org/html/draft-kovatsch-lwig-coap-01 + * + * Note that this design only implements the client functionality as our application does not + * serve anything. + */ + +class CoapRRLayer { + + public: + enum Result { + NO_ERROR = 0, + RESPONSE_RECEIVED, + ACK_RECEIVED, + CON_RECEIVED, + ERROR_IMPROPER_STATE, + ERROR_SENDING_MSG, + ERROR_RECEIVING_RESPONSE, + RST_RECEIVED + }; + + enum State { + STATE_IDLE, + STATE_WAITING + }; + + CoapRRLayer(CoapMessageLayer& messageLayer, uint8_t* rxBuffer, uint16_t rxBufferLen); + + Result reliableSend(CoapMsg& msg, char* token, IPAddress addr, uint16_t port); + Result send(CoapMsg& msg, char* token, IPAddress addr, uint16_t port); + Result loop(); + Result getLastResult() {return m_lastResult;} + void setState(State state) {m_state = state;} + int16_t getRxByteCount() {return m_rxByteCount;} + + protected: + + CoapMessageLayer& m_messageLayer; + State m_state; + Result m_lastResult; + char* m_token; + uint8_t* m_rxBuffer; + int16_t m_rxByteCount; + uint16_t m_rxBufferLen; + + bool rxTokenMatches(CoapMsg& msg); +}; + +#endif diff --git a/src/utility/FP.h b/src/utility/FP.h new file mode 100644 index 0000000..de5221d --- /dev/null +++ b/src/utility/FP.h @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2013, 2014 + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Sam Grove - initial API and implementation and/or initial documentation + * Ian Craggs - added attached and detached member functions + * Sam Grove - removed need for FP.cpp + *******************************************************************************/ + +#ifndef FP_H +#define FP_H + +/** Example using the FP Class with global functions + * @code + * #include "mbed.h" + * #include "FP.h" + * + * FPfp; + * DigitalOut myled(LED1); + * + * void handler(bool value) + * { + * myled = value; + * return; + * } + * + * int main() + * { + * fp.attach(&handler); + * + * while(1) + * { + * fp(1); + * wait(0.2); + * fp(0); + * wait(0.2); + * } + * } + * @endcode + */ + +/** Example using the FP Class with different class member functions + * @code + * #include "mbed.h" + * #include "FP.h" + * + * FPfp; + * DigitalOut myled(LED4); + * + * class Wrapper + * { + * public: + * Wrapper(){} + * + * void handler(bool value) + * { + * myled = value; + * return; + * } + * }; + * + * int main() + * { + * Wrapper wrapped; + * fp.attach(&wrapped, &Wrapper::handler); + * + * while(1) + * { + * fp(1); + * wait(0.2); + * fp(0); + * wait(0.2); + * } + * } + * @endcode + */ + +/** Example using the FP Class with member FP and member function +* @code +* #include "mbed.h" +* #include "FP.h" +* +* DigitalOut myled(LED2); +* +* class Wrapper +* { +* public: +* Wrapper() +* { +* fp.attach(this, &Wrapper::handler); +* } +* +* void handler(bool value) +* { +* myled = value; +* return; +* } +* +* FPfp; +* }; +* +* int main() +* { +* Wrapper wrapped; +* +* while(1) +* { +* wrapped.fp(1); +* wait(0.2); +* wrapped.fp(0); +* wait(0.2); +* } +* } +* @endcode +*/ + +/** + * @class FP + * @brief API for managing Function Pointers + */ +template +class FP +{ +public: + /** Create the FP object - only one callback can be attached to the object, that is + * a member function or a global function, not both at the same time + */ + FP() + { + obj_callback = 0; + c_callback = 0; + } + + /** Add a callback function to the object + * @param item - Address of the initialized object + * @param member - Address of the member function (dont forget the scope that the function is defined in) + */ + template + void attach(T *item, retT (T::*method)(argT)) + { + obj_callback = (FPtrDummy *)(item); + method_callback = (retT (FPtrDummy::*)(argT))(method); + return; + } + + /** Add a callback function to the object + * @param function - The address of a globally defined function + */ + void attach(retT (*function)(argT)) + { + c_callback = function; + } + + /** Invoke the function attached to the class + * @param arg - An argument that is passed into the function handler that is called + * @return The return from the function hanlder called by this class + */ + retT operator()(argT arg) const + { + if( 0 != c_callback ) { + return obj_callback ? (obj_callback->*method_callback)(arg) : (*c_callback)(arg); + } + return (retT)0; + } + + /** Determine if an callback is currently hooked + * @return 1 if a method is hooked, 0 otherwise + */ + bool attached() + { + return obj_callback || c_callback; + } + + /** Release a function from the callback hook + */ + void detach() + { + obj_callback = 0; + c_callback = 0; + } + +private: + + // empty type used for casting + class FPtrDummy; + + FPtrDummy *obj_callback; + + /** + * @union Funciton + * @brief Member or global callback function + */ + union { + retT (*c_callback)(argT); /*!< Footprint for a global function */ + retT (FPtrDummy::*method_callback)(argT); /*!< Footprint for a member function */ + }; +}; + +#endif diff --git a/src/utility/MQTTClient.h b/src/utility/MQTTClient.h new file mode 100644 index 0000000..31b9a79 --- /dev/null +++ b/src/utility/MQTTClient.h @@ -0,0 +1,954 @@ +/******************************************************************************* + * Copyright (c) 2014, 2015 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Ian Craggs - fix for bug 458512 - QoS 2 messages + * Ian Craggs - fix for bug 460389 - send loop uses wrong length + * Ian Craggs - fix for bug 464169 - clearing subscriptions + * Ian Craggs - fix for bug 464551 - enums and ints can be different size + *******************************************************************************/ + +#if !defined(MQTTCLIENT_H) +#define MQTTCLIENT_H + +#include "FP.h" +#include "MQTTPacket.h" +#include "stdio.h" +#include "MQTTLogging.h" + +#if !defined(MQTTCLIENT_QOS1) + #define MQTTCLIENT_QOS1 1 +#endif +#if !defined(MQTTCLIENT_QOS2) + #define MQTTCLIENT_QOS2 0 +#endif + +namespace MQTT +{ + + +enum QoS { QOS0, QOS1, QOS2 }; + +// all failure return codes must be negative +enum returnCode { BUFFER_OVERFLOW = -2, FAILURE = -1, SUCCESS = 0 }; + + +struct Message +{ + enum QoS qos; + bool retained; + bool dup; + unsigned short id; + void *payload; + size_t payloadlen; +}; + + +struct MessageData +{ + MessageData(MQTTString &aTopicName, struct Message &aMessage) : message(aMessage), topicName(aTopicName) + { } + + struct Message &message; + MQTTString &topicName; +}; + + +class PacketId +{ +public: + PacketId() + { + next = 0; + } + + int getNext() + { + next = (next == MAX_PACKET_ID) ? 1 : next + 1; + return next; + } + +private: + static const int MAX_PACKET_ID = 65535; + int next; +}; + + +/** + * @class Client + * @brief blocking, non-threaded MQTT client API + * + * This version of the API blocks on all method calls, until they are complete. This means that only one + * MQTT request can be in process at any one time. + * @param Network a network class which supports send, receive + * @param Timer a timer class with the methods: + */ +template +class Client +{ + +public: + + typedef void (*messageHandler)(MessageData&); + + /** Construct the client + * @param network - pointer to an instance of the Network class - must be connected to the endpoint + * before calling MQTT connect + * @param limits an instance of the Limit class - to alter limits as required + */ + Client(Network& network, unsigned int command_timeout_ms = 30000); + + /** Set the default message handling callback - used for any message which does not match a subscription message handler + * @param mh - pointer to the callback function + */ + void setDefaultMessageHandler(messageHandler mh) + { + defaultMessageHandler.attach(mh); + } + + /** MQTT Connect - send an MQTT connect packet down the network and wait for a Connack + * The nework object must be connected to the network endpoint before calling this + * Default connect options are used + * @return success code - + */ + int connect(); + + /** MQTT Connect - send an MQTT connect packet down the network and wait for a Connack + * The nework object must be connected to the network endpoint before calling this + * @param options - connect options + * @return success code - + */ + int connect(MQTTPacket_connectData& options); + + /** MQTT Publish - send an MQTT publish packet and wait for all acks to complete for all QoSs + * @param topic - the topic to publish to + * @param message - the message to send + * @return success code - + */ + int publish(const char* topicName, Message& message); + + /** MQTT Publish - send an MQTT publish packet and wait for all acks to complete for all QoSs + * @param topic - the topic to publish to + * @param payload - the data to send + * @param payloadlen - the length of the data + * @param qos - the QoS to send the publish at + * @param retained - whether the message should be retained + * @return success code - + */ + int publish(const char* topicName, void* payload, size_t payloadlen, enum QoS qos = QOS0, bool retained = false); + + /** MQTT Publish - send an MQTT publish packet and wait for all acks to complete for all QoSs + * @param topic - the topic to publish to + * @param payload - the data to send + * @param payloadlen - the length of the data + * @param id - the packet id used - returned + * @param qos - the QoS to send the publish at + * @param retained - whether the message should be retained + * @return success code - + */ + int publish(const char* topicName, void* payload, size_t payloadlen, unsigned short& id, enum QoS qos = QOS1, bool retained = false); + + /** MQTT Subscribe - send an MQTT subscribe packet and wait for the suback + * @param topicFilter - a topic pattern which can include wildcards + * @param qos - the MQTT QoS to subscribe at + * @param mh - the callback function to be invoked when a message is received for this subscription + * @return success code - + */ + int subscribe(const char* topicFilter, enum QoS qos, messageHandler mh); + + /** MQTT Unsubscribe - send an MQTT unsubscribe packet and wait for the unsuback + * @param topicFilter - a topic pattern which can include wildcards + * @return success code - + */ + int unsubscribe(const char* topicFilter); + + /** MQTT Disconnect - send an MQTT disconnect packet, and clean up any state + * @return success code - + */ + int disconnect(); + + /** A call to this API must be made within the keepAlive interval to keep the MQTT connection alive + * yield can be called if no other MQTT operation is needed. This will also allow messages to be + * received. + * @param timeout_ms the time to wait, in milliseconds + * @return success code - on failure, this means the client has disconnected + */ + int yield(unsigned long timeout_ms = 1000L); + + /** Is the client connected? + * @return flag - is the client connected or not? + */ + bool isConnected() + { + return isconnected; + } + +private: + + void cleanSession(); + int cycle(Timer& timer); + int waitfor(int packet_type, Timer& timer); + int keepalive(); + int publish(int len, Timer& timer, enum QoS qos); + + int decodePacket(int* value, int timeout); + int readPacket(Timer& timer); + int sendPacket(int length, Timer& timer); + int deliverMessage(MQTTString& topicName, Message& message); + bool isTopicMatched(char* topicFilter, MQTTString& topicName); + + Network& ipstack; + unsigned long command_timeout_ms; + + unsigned char sendbuf[MAX_MQTT_PACKET_SIZE]; + unsigned char readbuf[MAX_MQTT_PACKET_SIZE]; + + Timer last_sent, last_received; + unsigned int keepAliveInterval; + bool ping_outstanding; + bool cleansession; + + PacketId packetid; + + struct MessageHandlers + { + const char* topicFilter; + FP fp; + } messageHandlers[MAX_MESSAGE_HANDLERS]; // Message handlers are indexed by subscription topic + + FP defaultMessageHandler; + + bool isconnected; + +#if MQTTCLIENT_QOS1 || MQTTCLIENT_QOS2 + unsigned char pubbuf[MAX_MQTT_PACKET_SIZE]; // store the last publish for sending on reconnect + int inflightLen; + unsigned short inflightMsgid; + enum QoS inflightQoS; +#endif + +#if MQTTCLIENT_QOS2 + bool pubrel; + #if !defined(MAX_INCOMING_QOS2_MESSAGES) + #define MAX_INCOMING_QOS2_MESSAGES 10 + #endif + unsigned short incomingQoS2messages[MAX_INCOMING_QOS2_MESSAGES]; + bool isQoS2msgidFree(unsigned short id); + bool useQoS2msgid(unsigned short id); + void freeQoS2msgid(unsigned short id); +#endif + +}; + +} + + +template +void MQTT::Client::cleanSession() +{ + ping_outstanding = false; + for (int i = 0; i < MAX_MESSAGE_HANDLERS; ++i) + messageHandlers[i].topicFilter = 0; + isconnected = false; + +#if MQTTCLIENT_QOS1 || MQTTCLIENT_QOS2 + inflightMsgid = 0; + inflightQoS = QOS0; +#endif + +#if MQTTCLIENT_QOS2 + pubrel = false; + for (int i = 0; i < MAX_INCOMING_QOS2_MESSAGES; ++i) + incomingQoS2messages[i] = 0; +#endif +} + + +template +MQTT::Client::Client(Network& network, unsigned int command_timeout_ms) : ipstack(network), packetid() +{ + last_sent = Timer(); + last_received = Timer(); + this->command_timeout_ms = command_timeout_ms; + cleanSession(); +} + + +#if MQTTCLIENT_QOS2 +template +bool MQTT::Client::isQoS2msgidFree(unsigned short id) +{ + for (int i = 0; i < MAX_INCOMING_QOS2_MESSAGES; ++i) + { + if (incomingQoS2messages[i] == id) + return false; + } + return true; +} + + +template +bool MQTT::Client::useQoS2msgid(unsigned short id) +{ + for (int i = 0; i < MAX_INCOMING_QOS2_MESSAGES; ++i) + { + if (incomingQoS2messages[i] == 0) + { + incomingQoS2messages[i] = id; + return true; + } + } + return false; +} + + +template +void MQTT::Client::freeQoS2msgid(unsigned short id) +{ + for (int i = 0; i < MAX_INCOMING_QOS2_MESSAGES; ++i) + { + if (incomingQoS2messages[i] == id) + { + incomingQoS2messages[i] = 0; + return; + } + } +} +#endif + + +template +int MQTT::Client::sendPacket(int length, Timer& timer) +{ + int rc = FAILURE, + sent = 0; + + while (sent < length && !timer.expired()) + { + rc = ipstack.write(&sendbuf[sent], length - sent, timer.left_ms()); + if (rc < 0) // there was an error writing the data + break; + sent += rc; + } + if (sent == length) + { + if (this->keepAliveInterval > 0) + last_sent.countdown(this->keepAliveInterval); // record the fact that we have successfully sent the packet + rc = SUCCESS; + } + else + rc = FAILURE; + +#if defined(MQTT_DEBUG) + char printbuf[150]; + DEBUG("Rc %d from sending packet %s\n", rc, MQTTFormat_toServerString(printbuf, sizeof(printbuf), sendbuf, length)); +#endif + return rc; +} + + +template +int MQTT::Client::decodePacket(int* value, int timeout) +{ + unsigned char c; + int multiplier = 1; + int len = 0; + const int MAX_NO_OF_REMAINING_LENGTH_BYTES = 4; + + *value = 0; + do + { + int rc = MQTTPACKET_READ_ERROR; + + if (++len > MAX_NO_OF_REMAINING_LENGTH_BYTES) + { + rc = MQTTPACKET_READ_ERROR; /* bad data */ + goto exit; + } + rc = ipstack.read(&c, 1, timeout); + if (rc != 1) + goto exit; + *value += (c & 127) * multiplier; + multiplier *= 128; + } while ((c & 128) != 0); +exit: + return len; +} + + +/** + * If any read fails in this method, then we should disconnect from the network, as on reconnect + * the packets can be retried. + * @param timeout the max time to wait for the packet read to complete, in milliseconds + * @return the MQTT packet type, or -1 if none + */ +template +int MQTT::Client::readPacket(Timer& timer) +{ + int rc = FAILURE; + MQTTHeader header = {0}; + int len = 0; + int rem_len = 0; + + /* 1. read the header byte. This has the packet type in it */ + if (ipstack.read(readbuf, 1, timer.left_ms()) != 1) + goto exit; + + len = 1; + /* 2. read the remaining length. This is variable in itself */ + decodePacket(&rem_len, timer.left_ms()); + len += MQTTPacket_encode(readbuf + 1, rem_len); /* put the original remaining length into the buffer */ + + if (rem_len > (MAX_MQTT_PACKET_SIZE - len)) + { + rc = BUFFER_OVERFLOW; + } + + /* 3. read the rest of the buffer using a callback to supply the rest of the data */ + if (rc == BUFFER_OVERFLOW) { + if (rem_len > 0 && (ipstack.read(readbuf + len, MAX_MQTT_PACKET_SIZE - len, timer.left_ms()) != (MAX_MQTT_PACKET_SIZE - len))) + goto exit; + readbuf[MAX_MQTT_PACKET_SIZE-1] = '\0'; + rem_len = rem_len - MAX_MQTT_PACKET_SIZE - len; + while(rem_len--) { + unsigned char c; + ipstack.read(&c, 1, timer.left_ms() ); + } + } + else { + if (rem_len > 0 && (ipstack.read(readbuf + len, rem_len, timer.left_ms()) != rem_len)) + goto exit; + header.byte = readbuf[0]; + rc = header.bits.type; + } + + if (this->keepAliveInterval > 0) + last_received.countdown(this->keepAliveInterval); // record the fact that we have successfully received a packet +exit: +#if defined(MQTT_DEBUG) + if (rc >= 0) + { + char printbuf[50]; + DEBUG("Rc %d from receiving packet %s\n", rc, MQTTFormat_toClientString(printbuf, sizeof(printbuf), readbuf, len)); + } +#endif + return rc; +} + + +// assume topic filter and name is in correct format +// # can only be at end +// + and # can only be next to separator +template +bool MQTT::Client::isTopicMatched(char* topicFilter, MQTTString& topicName) +{ + char* curf = topicFilter; + char* curn = topicName.lenstring.data; + char* curn_end = curn + topicName.lenstring.len; + + while (*curf && curn < curn_end) + { + if (*curn == '/' && *curf != '/') + break; + if (*curf != '+' && *curf != '#' && *curf != *curn) + break; + if (*curf == '+') + { // skip until we meet the next separator, or end of string + char* nextpos = curn + 1; + while (nextpos < curn_end && *nextpos != '/') + nextpos = ++curn + 1; + } + else if (*curf == '#') + curn = curn_end - 1; // skip until end of string + curf++; + curn++; + }; + + return (curn == curn_end) && (*curf == '\0'); +} + + + +template +int MQTT::Client::deliverMessage(MQTTString& topicName, Message& message) +{ + int rc = FAILURE; + + // we have to find the right message handler - indexed by topic + for (int i = 0; i < MAX_MESSAGE_HANDLERS; ++i) + { + if (messageHandlers[i].topicFilter != 0 && (MQTTPacket_equals(&topicName, (char*)messageHandlers[i].topicFilter) || + isTopicMatched((char*)messageHandlers[i].topicFilter, topicName))) + { + if (messageHandlers[i].fp.attached()) + { + MessageData md(topicName, message); + messageHandlers[i].fp(md); + rc = SUCCESS; + } + } + } + + if (rc == FAILURE && defaultMessageHandler.attached()) + { + MessageData md(topicName, message); + defaultMessageHandler(md); + rc = SUCCESS; + } + + return rc; +} + + + +template +int MQTT::Client::yield(unsigned long timeout_ms) +{ + int rc = SUCCESS; + Timer timer = Timer(); + + timer.countdown_ms(timeout_ms); + while (!timer.expired()) + { + if (cycle(timer) < 0) + { + rc = FAILURE; + break; + } + } + + return rc; +} + + +template +int MQTT::Client::cycle(Timer& timer) +{ + /* get one piece of work off the wire and one pass through */ + + // read the socket, see what work is due + int packet_type = readPacket(timer); + int rc = SUCCESS; + if (packet_type == BUFFER_OVERFLOW) { + MQTTHeader header = {0}; + header.byte = readbuf[0]; + packet_type = header.bits.type; + } + + int len = 0; + switch (packet_type) + { + case FAILURE: + case BUFFER_OVERFLOW: + rc = packet_type; + break; + case CONNACK: + case PUBACK: + case SUBACK: + break; + case PUBLISH: + { + MQTTString topicName = MQTTString_initializer; + Message msg; + int intQoS; + if (MQTTDeserialize_publish((unsigned char*)&msg.dup, &intQoS, (unsigned char*)&msg.retained, (unsigned short*)&msg.id, &topicName, + (unsigned char**)&msg.payload, (int*)&msg.payloadlen, readbuf, MAX_MQTT_PACKET_SIZE) != 1) + goto exit; + msg.qos = (enum QoS)intQoS; +#if MQTTCLIENT_QOS2 + if (msg.qos != QOS2) +#endif + deliverMessage(topicName, msg); +#if MQTTCLIENT_QOS2 + else if (isQoS2msgidFree(msg.id)) + { + if (useQoS2msgid(msg.id)) + deliverMessage(topicName, msg); + else + WARN("Maximum number of incoming QoS2 messages exceeded"); + } +#endif +#if MQTTCLIENT_QOS1 || MQTTCLIENT_QOS2 + if (msg.qos != QOS0) + { + if (msg.qos == QOS1) + len = MQTTSerialize_ack(sendbuf, MAX_MQTT_PACKET_SIZE, PUBACK, 0, msg.id); + else if (msg.qos == QOS2) + len = MQTTSerialize_ack(sendbuf, MAX_MQTT_PACKET_SIZE, PUBREC, 0, msg.id); + if (len <= 0) + rc = FAILURE; + else + rc = sendPacket(len, timer); + if (rc == FAILURE) + goto exit; // there was a problem + } + break; +#endif + } +#if MQTTCLIENT_QOS2 + case PUBREC: + case PUBREL: + unsigned short mypacketid; + unsigned char dup, type; + if (MQTTDeserialize_ack(&type, &dup, &mypacketid, readbuf, MAX_MQTT_PACKET_SIZE) != 1) + rc = FAILURE; + else if ((len = MQTTSerialize_ack(sendbuf, MAX_MQTT_PACKET_SIZE, + (packet_type == PUBREC) ? PUBREL : PUBCOMP, 0, mypacketid)) <= 0) + rc = FAILURE; + else if ((rc = sendPacket(len, timer)) != SUCCESS) // send the PUBREL packet + rc = FAILURE; // there was a problem + if (rc == FAILURE) + goto exit; // there was a problem + if (packet_type == PUBREL) + freeQoS2msgid(mypacketid); + break; + + case PUBCOMP: + break; +#endif + case PINGRESP: + ping_outstanding = false; + break; + } + keepalive(); +exit: + if (rc == SUCCESS) + rc = packet_type; + return rc; +} + + +template +int MQTT::Client::keepalive() +{ + int rc = FAILURE; + + if (keepAliveInterval == 0) + { + rc = SUCCESS; + goto exit; + } + + if (last_sent.expired() || last_received.expired()) + { + if (!ping_outstanding) + { + Timer timer = Timer(1000); + int len = MQTTSerialize_pingreq(sendbuf, MAX_MQTT_PACKET_SIZE); + if (len > 0 && (rc = sendPacket(len, timer)) == SUCCESS) // send the ping packet + ping_outstanding = true; + } + } + +exit: + return rc; +} + + +// only used in single-threaded mode where one command at a time is in process +template +int MQTT::Client::waitfor(int packet_type, Timer& timer) +{ + int rc = FAILURE; + + do + { + if (timer.expired()) + break; // we timed out + } + while ((rc = cycle(timer)) != packet_type); + + return rc; +} + + +template +int MQTT::Client::connect(MQTTPacket_connectData& options) +{ + Timer connect_timer = Timer(command_timeout_ms); + int rc = FAILURE; + int len = 0; + + if (isconnected) // don't send connect packet again if we are already connected + goto exit; + + this->keepAliveInterval = options.keepAliveInterval; + this->cleansession = options.cleansession; + if ((len = MQTTSerialize_connect(sendbuf, MAX_MQTT_PACKET_SIZE, &options)) <= 0) + goto exit; + if ((rc = sendPacket(len, connect_timer)) != SUCCESS) // send the connect packet + goto exit; // there was a problem + + if (this->keepAliveInterval > 0) + last_received.countdown(this->keepAliveInterval); + // this will be a blocking call, wait for the connack + if (waitfor(CONNACK, connect_timer) == CONNACK) + { + unsigned char connack_rc = 255; + bool sessionPresent = false; + if (MQTTDeserialize_connack((unsigned char*)&sessionPresent, &connack_rc, readbuf, MAX_MQTT_PACKET_SIZE) == 1) + rc = connack_rc; + else + rc = FAILURE; + } + else + rc = FAILURE; + +#if MQTTCLIENT_QOS2 + // resend any inflight publish + if (inflightMsgid > 0 && inflightQoS == QOS2 && pubrel) + { + if ((len = MQTTSerialize_ack(sendbuf, MAX_MQTT_PACKET_SIZE, PUBREL, 0, inflightMsgid)) <= 0) + rc = FAILURE; + else + rc = publish(len, connect_timer, inflightQoS); + } + else +#endif +#if MQTTCLIENT_QOS1 || MQTTCLIENT_QOS2 + if (inflightMsgid > 0) + { + memcpy(sendbuf, pubbuf, MAX_MQTT_PACKET_SIZE); + rc = publish(inflightLen, connect_timer, inflightQoS); + } +#endif + +exit: + if (rc == SUCCESS) + isconnected = true; + return rc; +} + + +template +int MQTT::Client::connect() +{ + MQTTPacket_connectData default_options = MQTTPacket_connectData_initializer; + return connect(default_options); +} + + +template +int MQTT::Client::subscribe(const char* topicFilter, enum QoS qos, messageHandler messageHandler) +{ + int rc = FAILURE; + Timer timer = Timer(command_timeout_ms); + int len = 0; + MQTTString topic = {(char*)topicFilter, {0, 0}}; + + if (!isconnected) + goto exit; + + len = MQTTSerialize_subscribe(sendbuf, MAX_MQTT_PACKET_SIZE, 0, packetid.getNext(), 1, &topic, (int*)&qos); + if (len <= 0) + goto exit; + if ((rc = sendPacket(len, timer)) != SUCCESS) // send the subscribe packet + goto exit; // there was a problem + + if (waitfor(SUBACK, timer) == SUBACK) // wait for suback + { + int count = 0, grantedQoS = -1; + unsigned short mypacketid; + if (MQTTDeserialize_suback(&mypacketid, 1, &count, &grantedQoS, readbuf, MAX_MQTT_PACKET_SIZE) == 1) + rc = grantedQoS; // 0, 1, 2 or 0x80 + if (rc != 0x80) + { + for (int i = 0; i < MAX_MESSAGE_HANDLERS; ++i) + { + if (messageHandlers[i].topicFilter == 0) + { + messageHandlers[i].topicFilter = topicFilter; + messageHandlers[i].fp.attach(messageHandler); + rc = 0; + break; + } + } + } + } + else + rc = FAILURE; + +exit: + if (rc != SUCCESS) + cleanSession(); + return rc; +} + + +template +int MQTT::Client::unsubscribe(const char* topicFilter) +{ + int rc = FAILURE; + Timer timer = Timer(command_timeout_ms); + MQTTString topic = {(char*)topicFilter, {0, 0}}; + int len = 0; + + if (!isconnected) + goto exit; + + if ((len = MQTTSerialize_unsubscribe(sendbuf, MAX_MQTT_PACKET_SIZE, 0, packetid.getNext(), 1, &topic)) <= 0) + goto exit; + if ((rc = sendPacket(len, timer)) != SUCCESS) // send the unsubscribe packet + goto exit; // there was a problem + + if (waitfor(UNSUBACK, timer) == UNSUBACK) + { + unsigned short mypacketid; // should be the same as the packetid above + if (MQTTDeserialize_unsuback(&mypacketid, readbuf, MAX_MQTT_PACKET_SIZE) == 1) + { + rc = 0; + + // remove the subscription message handler associated with this topic, if there is one + for (int i = 0; i < MAX_MESSAGE_HANDLERS; ++i) + { + if (strcmp(messageHandlers[i].topicFilter, topicFilter) == 0) + { + messageHandlers[i].topicFilter = 0; + break; + } + } + } + } + else + rc = FAILURE; + +exit: + if (rc != SUCCESS) + cleanSession(); + return rc; +} + + +template +int MQTT::Client::publish(int len, Timer& timer, enum QoS qos) +{ + int rc; + + if ((rc = sendPacket(len, timer)) != SUCCESS) // send the publish packet + goto exit; // there was a problem + +#if MQTTCLIENT_QOS1 + if (qos == QOS1) + { + if (waitfor(PUBACK, timer) == PUBACK) + { + unsigned short mypacketid; + unsigned char dup, type; + if (MQTTDeserialize_ack(&type, &dup, &mypacketid, readbuf, MAX_MQTT_PACKET_SIZE) != 1) + rc = FAILURE; + else if (inflightMsgid == mypacketid) + inflightMsgid = 0; + } + else + rc = FAILURE; + } +#elif MQTTCLIENT_QOS2 + else if (qos == QOS2) + { + if (waitfor(PUBCOMP, timer) == PUBCOMP) + { + unsigned short mypacketid; + unsigned char dup, type; + if (MQTTDeserialize_ack(&type, &dup, &mypacketid, readbuf, MAX_MQTT_PACKET_SIZE) != 1) + rc = FAILURE; + else if (inflightMsgid == mypacketid) + inflightMsgid = 0; + } + else + rc = FAILURE; + } +#endif + +exit: + if (rc != SUCCESS) + cleanSession(); + return rc; +} + + + +template +int MQTT::Client::publish(const char* topicName, void* payload, size_t payloadlen, unsigned short& id, enum QoS qos, bool retained) +{ + int rc = FAILURE; + Timer timer = Timer(command_timeout_ms); + MQTTString topicString = MQTTString_initializer; + int len = 0; + + if (!isconnected) + goto exit; + + topicString.cstring = (char*)topicName; + +#if MQTTCLIENT_QOS1 || MQTTCLIENT_QOS2 + if (qos == QOS1 || qos == QOS2) + id = packetid.getNext(); +#endif + + len = MQTTSerialize_publish(sendbuf, MAX_MQTT_PACKET_SIZE, 0, qos, retained, id, + topicString, (unsigned char*)payload, payloadlen); + if (len <= 0) + goto exit; + +#if MQTTCLIENT_QOS1 || MQTTCLIENT_QOS2 + if (!cleansession) + { + memcpy(pubbuf, sendbuf, len); + inflightMsgid = id; + inflightLen = len; + inflightQoS = qos; +#if MQTTCLIENT_QOS2 + pubrel = false; +#endif + } +#endif + + rc = publish(len, timer, qos); +exit: + return rc; +} + + +template +int MQTT::Client::publish(const char* topicName, void* payload, size_t payloadlen, enum QoS qos, bool retained) +{ + unsigned short id = 0; // dummy - not used for anything + return publish(topicName, payload, payloadlen, id, qos, retained); +} + + +template +int MQTT::Client::publish(const char* topicName, Message& message) +{ + return publish(topicName, message.payload, message.payloadlen, message.qos, message.retained); +} + + +template +int MQTT::Client::disconnect() +{ + int rc = FAILURE; + Timer timer = Timer(command_timeout_ms); // we might wait for incomplete incoming publishes to complete + int len = MQTTSerialize_disconnect(sendbuf, MAX_MQTT_PACKET_SIZE); + if (len > 0) + rc = sendPacket(len, timer); // send the disconnect packet + + if (cleansession) + cleanSession(); + else + isconnected = false; + return rc; +} + + +#endif diff --git a/src/utility/MQTTConnect.h b/src/utility/MQTTConnect.h new file mode 100644 index 0000000..db90251 --- /dev/null +++ b/src/utility/MQTTConnect.h @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Xiang Rong - 442039 Add makefile to Embedded C client + *******************************************************************************/ + +#ifndef MQTTCONNECT_H_ +#define MQTTCONNECT_H_ + +#if !defined(DLLImport) + #define DLLImport +#endif +#if !defined(DLLExport) + #define DLLExport +#endif + + +typedef union +{ + unsigned char all; /**< all connect flags */ +#if defined(REVERSED) + struct + { + unsigned int username : 1; /**< 3.1 user name */ + unsigned int password : 1; /**< 3.1 password */ + unsigned int willRetain : 1; /**< will retain setting */ + unsigned int willQoS : 2; /**< will QoS value */ + unsigned int will : 1; /**< will flag */ + unsigned int cleansession : 1; /**< clean session flag */ + unsigned int : 1; /**< unused */ + } bits; +#else + struct + { + unsigned int : 1; /**< unused */ + unsigned int cleansession : 1; /**< cleansession flag */ + unsigned int will : 1; /**< will flag */ + unsigned int willQoS : 2; /**< will QoS value */ + unsigned int willRetain : 1; /**< will retain setting */ + unsigned int password : 1; /**< 3.1 password */ + unsigned int username : 1; /**< 3.1 user name */ + } bits; +#endif +} MQTTConnectFlags; /**< connect flags byte */ + + + +/** + * Defines the MQTT "Last Will and Testament" (LWT) settings for + * the connect packet. + */ +typedef struct +{ + /** The eyecatcher for this structure. must be MQTW. */ + char struct_id[4]; + /** The version number of this structure. Must be 0 */ + int struct_version; + /** The LWT topic to which the LWT message will be published. */ + MQTTString topicName; + /** The LWT payload. */ + MQTTString message; + /** + * The retained flag for the LWT message (see MQTTAsync_message.retained). + */ + unsigned char retained; + /** + * The quality of service setting for the LWT message (see + * MQTTAsync_message.qos and @ref qos). + */ + char qos; +} MQTTPacket_willOptions; + + +#define MQTTPacket_willOptions_initializer { {'M', 'Q', 'T', 'W'}, 0, {NULL, {0, NULL}}, {NULL, {0, NULL}}, 0, 0 } + + +typedef struct +{ + /** The eyecatcher for this structure. must be MQTC. */ + char struct_id[4]; + /** The version number of this structure. Must be 0 */ + int struct_version; + /** Version of MQTT to be used. 3 = 3.1 4 = 3.1.1 + */ + unsigned char MQTTVersion; + MQTTString clientID; + unsigned short keepAliveInterval; + unsigned char cleansession; + unsigned char willFlag; + MQTTPacket_willOptions will; + MQTTString username; + MQTTString password; +} MQTTPacket_connectData; + +typedef union +{ + unsigned char all; /**< all connack flags */ +#if defined(REVERSED) + struct + { + unsigned int sessionpresent : 1; /**< session present flag */ + unsigned int : 7; /**< unused */ + } bits; +#else + struct + { + unsigned int : 7; /**< unused */ + unsigned int sessionpresent : 1; /**< session present flag */ + } bits; +#endif +} MQTTConnackFlags; /**< connack flags byte */ + +#define MQTTPacket_connectData_initializer { {'M', 'Q', 'T', 'C'}, 0, 4, {NULL, {0, NULL}}, 60, 1, 0, \ + MQTTPacket_willOptions_initializer, {NULL, {0, NULL}}, {NULL, {0, NULL}} } + +DLLExport int MQTTSerialize_connect(unsigned char* buf, int buflen, MQTTPacket_connectData* options); +DLLExport int MQTTDeserialize_connect(MQTTPacket_connectData* data, unsigned char* buf, int len); + +DLLExport int MQTTSerialize_connack(unsigned char* buf, int buflen, unsigned char connack_rc, unsigned char sessionPresent); +DLLExport int MQTTDeserialize_connack(unsigned char* sessionPresent, unsigned char* connack_rc, unsigned char* buf, int buflen); + +DLLExport int MQTTSerialize_disconnect(unsigned char* buf, int buflen); +DLLExport int MQTTSerialize_pingreq(unsigned char* buf, int buflen); + +#endif /* MQTTCONNECT_H_ */ diff --git a/src/utility/MQTTConnectClient.c b/src/utility/MQTTConnectClient.c new file mode 100644 index 0000000..5f3cc29 --- /dev/null +++ b/src/utility/MQTTConnectClient.c @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "MQTTPacket.h" +#include "StackTrace.h" + +#include + +/** + * Determines the length of the MQTT connect packet that would be produced using the supplied connect options. + * @param options the options to be used to build the connect packet + * @return the length of buffer needed to contain the serialized version of the packet + */ +int MQTTSerialize_connectLength(MQTTPacket_connectData* options) +{ + int len = 0; + + FUNC_ENTRY; + + if (options->MQTTVersion == 3) + len = 12; /* variable depending on MQTT or MQIsdp */ + else if (options->MQTTVersion == 4) + len = 10; + + len += MQTTstrlen(options->clientID)+2; + if (options->willFlag) + len += MQTTstrlen(options->will.topicName)+2 + MQTTstrlen(options->will.message)+2; + if (options->username.cstring || options->username.lenstring.data) + len += MQTTstrlen(options->username)+2; + if (options->password.cstring || options->password.lenstring.data) + len += MQTTstrlen(options->password)+2; + + FUNC_EXIT_RC(len); + return len; +} + + +/** + * Serializes the connect options into the buffer. + * @param buf the buffer into which the packet will be serialized + * @param len the length in bytes of the supplied buffer + * @param options the options to be used to build the connect packet + * @return serialized length, or error if 0 + */ +int MQTTSerialize_connect(unsigned char* buf, int buflen, MQTTPacket_connectData* options) +{ + unsigned char *ptr = buf; + MQTTHeader header = {0}; + MQTTConnectFlags flags = {0}; + int len = 0; + int rc = -1; + + FUNC_ENTRY; + if (MQTTPacket_len(len = MQTTSerialize_connectLength(options)) > buflen) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + + header.byte = 0; + header.bits.type = CONNECT; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, len); /* write remaining length */ + + if (options->MQTTVersion == 4) + { + writeCString(&ptr, "MQTT"); + writeChar(&ptr, (char) 4); + } + else + { + writeCString(&ptr, "MQIsdp"); + writeChar(&ptr, (char) 3); + } + + flags.all = 0; + flags.bits.cleansession = options->cleansession; + flags.bits.will = (options->willFlag) ? 1 : 0; + if (flags.bits.will) + { + flags.bits.willQoS = options->will.qos; + flags.bits.willRetain = options->will.retained; + } + + if (options->username.cstring || options->username.lenstring.data) + flags.bits.username = 1; + if (options->password.cstring || options->password.lenstring.data) + flags.bits.password = 1; + + writeChar(&ptr, flags.all); + writeInt(&ptr, options->keepAliveInterval); + writeMQTTString(&ptr, options->clientID); + if (options->willFlag) + { + writeMQTTString(&ptr, options->will.topicName); + writeMQTTString(&ptr, options->will.message); + } + if (flags.bits.username) + writeMQTTString(&ptr, options->username); + if (flags.bits.password) + writeMQTTString(&ptr, options->password); + + rc = ptr - buf; + + exit: FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Deserializes the supplied (wire) buffer into connack data - return code + * @param sessionPresent the session present flag returned (only for MQTT 3.1.1) + * @param connack_rc returned integer value of the connack return code + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param len the length in bytes of the data in the supplied buffer + * @return error code. 1 is success, 0 is failure + */ +int MQTTDeserialize_connack(unsigned char* sessionPresent, unsigned char* connack_rc, unsigned char* buf, int buflen) +{ + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = 0; + int mylen; + MQTTConnackFlags flags = {0}; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + if (header.bits.type != CONNACK) + goto exit; + + curdata += (rc = MQTTPacket_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + if (enddata - curdata < 2) + goto exit; + + flags.all = readChar(&curdata); + *sessionPresent = flags.bits.sessionpresent; + *connack_rc = readChar(&curdata); + + rc = 1; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Serializes a 0-length packet into the supplied buffer, ready for writing to a socket + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer, to avoid overruns + * @param packettype the message type + * @return serialized length, or error if 0 + */ +int MQTTSerialize_zero(unsigned char* buf, int buflen, unsigned char packettype) +{ + MQTTHeader header = {0}; + int rc = -1; + unsigned char *ptr = buf; + + FUNC_ENTRY; + if (buflen < 2) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + header.byte = 0; + header.bits.type = packettype; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, 0); /* write remaining length */ + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Serializes a disconnect packet into the supplied buffer, ready for writing to a socket + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer, to avoid overruns + * @return serialized length, or error if 0 + */ +int MQTTSerialize_disconnect(unsigned char* buf, int buflen) +{ + return MQTTSerialize_zero(buf, buflen, DISCONNECT); +} + + +/** + * Serializes a disconnect packet into the supplied buffer, ready for writing to a socket + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer, to avoid overruns + * @return serialized length, or error if 0 + */ +int MQTTSerialize_pingreq(unsigned char* buf, int buflen) +{ + return MQTTSerialize_zero(buf, buflen, PINGREQ); +} diff --git a/src/utility/MQTTConnectServer.c b/src/utility/MQTTConnectServer.c new file mode 100644 index 0000000..07c7cb5 --- /dev/null +++ b/src/utility/MQTTConnectServer.c @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "StackTrace.h" +#include "MQTTPacket.h" +#include + +#define min(a, b) ((a < b) ? a : b) + + +/** + * Validates MQTT protocol name and version combinations + * @param protocol the MQTT protocol name as an MQTTString + * @param version the MQTT protocol version number, as in the connect packet + * @return correct MQTT combination? 1 is true, 0 is false + */ +int MQTTPacket_checkVersion(MQTTString* protocol, int version) +{ + int rc = 0; + + if (version == 3 && memcmp(protocol->lenstring.data, "MQIsdp", + min(6, protocol->lenstring.len)) == 0) + rc = 1; + else if (version == 4 && memcmp(protocol->lenstring.data, "MQTT", + min(4, protocol->lenstring.len)) == 0) + rc = 1; + return rc; +} + + +/** + * Deserializes the supplied (wire) buffer into connect data structure + * @param data the connect data structure to be filled out + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param len the length in bytes of the data in the supplied buffer + * @return error code. 1 is success, 0 is failure + */ +int MQTTDeserialize_connect(MQTTPacket_connectData* data, unsigned char* buf, int len) +{ + MQTTHeader header = {0}; + MQTTConnectFlags flags = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = &buf[len]; + int rc = 0; + MQTTString Protocol; + int version; + int mylen = 0; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + if (header.bits.type != CONNECT) + goto exit; + + curdata += MQTTPacket_decodeBuf(curdata, &mylen); /* read remaining length */ + + if (!readMQTTLenString(&Protocol, &curdata, enddata) || + enddata - curdata < 0) /* do we have enough data to read the protocol version byte? */ + goto exit; + + version = (int)readChar(&curdata); /* Protocol version */ + /* If we don't recognize the protocol version, we don't parse the connect packet on the + * basis that we don't know what the format will be. + */ + if (MQTTPacket_checkVersion(&Protocol, version)) + { + flags.all = readChar(&curdata); + data->cleansession = flags.bits.cleansession; + data->keepAliveInterval = readInt(&curdata); + if (!readMQTTLenString(&data->clientID, &curdata, enddata)) + goto exit; + data->willFlag = flags.bits.will; + if (flags.bits.will) + { + data->will.qos = flags.bits.willQoS; + data->will.retained = flags.bits.willRetain; + if (!readMQTTLenString(&data->will.topicName, &curdata, enddata) || + !readMQTTLenString(&data->will.message, &curdata, enddata)) + goto exit; + } + if (flags.bits.username) + { + if (enddata - curdata < 3 || !readMQTTLenString(&data->username, &curdata, enddata)) + goto exit; /* username flag set, but no username supplied - invalid */ + if (flags.bits.password && + (enddata - curdata < 3 || !readMQTTLenString(&data->password, &curdata, enddata))) + goto exit; /* password flag set, but no password supplied - invalid */ + } + else if (flags.bits.password) + goto exit; /* password flag set without username - invalid */ + rc = 1; + } +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Serializes the connack packet into the supplied buffer. + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param connack_rc the integer connack return code to be used + * @param sessionPresent the MQTT 3.1.1 sessionPresent flag + * @return serialized length, or error if 0 + */ +int MQTTSerialize_connack(unsigned char* buf, int buflen, unsigned char connack_rc, unsigned char sessionPresent) +{ + MQTTHeader header = {0}; + int rc = 0; + unsigned char *ptr = buf; + MQTTConnackFlags flags = {0}; + + FUNC_ENTRY; + if (buflen < 2) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + header.byte = 0; + header.bits.type = CONNACK; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, 2); /* write remaining length */ + + flags.all = 0; + flags.bits.sessionpresent = sessionPresent; + writeChar(&ptr, flags.all); + writeChar(&ptr, connack_rc); + + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + diff --git a/src/utility/MQTTDeserializePublish.c b/src/utility/MQTTDeserializePublish.c new file mode 100644 index 0000000..dafb6a3 --- /dev/null +++ b/src/utility/MQTTDeserializePublish.c @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "StackTrace.h" +#include "MQTTPacket.h" +#include + +#define min(a, b) ((a < b) ? 1 : 0) + +/** + * Deserializes the supplied (wire) buffer into publish data + * @param dup returned integer - the MQTT dup flag + * @param qos returned integer - the MQTT QoS value + * @param retained returned integer - the MQTT retained flag + * @param packetid returned integer - the MQTT packet identifier + * @param topicName returned MQTTString - the MQTT topic in the publish + * @param payload returned byte buffer - the MQTT publish payload + * @param payloadlen returned integer - the length of the MQTT payload + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @return error code. 1 is success + */ +int MQTTDeserialize_publish(unsigned char* dup, int* qos, unsigned char* retained, unsigned short* packetid, MQTTString* topicName, + unsigned char** payload, int* payloadlen, unsigned char* buf, int buflen) +{ + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = 0; + int mylen = 0; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + if (header.bits.type != PUBLISH) + goto exit; + *dup = header.bits.dup; + *qos = header.bits.qos; + *retained = header.bits.retain; + + curdata += (rc = MQTTPacket_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + + if (!readMQTTLenString(topicName, &curdata, enddata) || + enddata - curdata < 0) /* do we have enough data to read the protocol version byte? */ + goto exit; + + if (*qos > 0) + *packetid = readInt(&curdata); + + *payloadlen = enddata - curdata; + *payload = curdata; + rc = 1; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + + +/** + * Deserializes the supplied (wire) buffer into an ack + * @param packettype returned integer - the MQTT packet type + * @param dup returned integer - the MQTT dup flag + * @param packetid returned integer - the MQTT packet identifier + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @return error code. 1 is success, 0 is failure + */ +int MQTTDeserialize_ack(unsigned char* packettype, unsigned char* dup, unsigned short* packetid, unsigned char* buf, int buflen) +{ + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = 0; + int mylen; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + *dup = header.bits.dup; + *packettype = header.bits.type; + + curdata += (rc = MQTTPacket_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + + if (enddata - curdata < 2) + goto exit; + *packetid = readInt(&curdata); + + rc = 1; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + diff --git a/src/utility/MQTTFormat.c b/src/utility/MQTTFormat.c new file mode 100644 index 0000000..01d55c3 --- /dev/null +++ b/src/utility/MQTTFormat.c @@ -0,0 +1,256 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "StackTrace.h" +#include "MQTTPacket.h" + +#include + + +const char* MQTTPacket_names[] = +{ + "RESERVED", "CONNECT", "CONNACK", "PUBLISH", "PUBACK", "PUBREC", "PUBREL", + "PUBCOMP", "SUBSCRIBE", "SUBACK", "UNSUBSCRIBE", "UNSUBACK", + "PINGREQ", "PINGRESP", "DISCONNECT" +}; + + +const char* MQTTPacket_getName(unsigned short packetid) +{ + return MQTTPacket_names[packetid]; +} + + +int MQTTStringFormat_connect(char* strbuf, int strbuflen, MQTTPacket_connectData* data) +{ + int strindex = 0; + + strindex = snprintf(strbuf, strbuflen, + "CONNECT MQTT version %d, client id %.*s, clean session %d, keep alive %d", + (int)data->MQTTVersion, data->clientID.lenstring.len, data->clientID.lenstring.data, + (int)data->cleansession, data->keepAliveInterval); + if (data->willFlag) + strindex += snprintf(&strbuf[strindex], strbuflen - strindex, + ", will QoS %d, will retain %d, will topic %.*s, will message %.*s", + data->will.qos, data->will.retained, + data->will.topicName.lenstring.len, data->will.topicName.lenstring.data, + data->will.message.lenstring.len, data->will.message.lenstring.data); + if (data->username.lenstring.data && data->username.lenstring.len > 0) + strindex += snprintf(&strbuf[strindex], strbuflen - strindex, + ", user name %.*s", data->username.lenstring.len, data->username.lenstring.data); + if (data->password.lenstring.data && data->password.lenstring.len > 0) + strindex += snprintf(&strbuf[strindex], strbuflen - strindex, + ", password %.*s", data->password.lenstring.len, data->password.lenstring.data); + return strindex; +} + + +int MQTTStringFormat_connack(char* strbuf, int strbuflen, unsigned char connack_rc, unsigned char sessionPresent) +{ + int strindex = snprintf(strbuf, strbuflen, "CONNACK session present %d, rc %d", sessionPresent, connack_rc); + return strindex; +} + + +int MQTTStringFormat_publish(char* strbuf, int strbuflen, unsigned char dup, int qos, unsigned char retained, + unsigned short packetid, MQTTString topicName, unsigned char* payload, int payloadlen) +{ + int strindex = snprintf(strbuf, strbuflen, + "PUBLISH dup %d, QoS %d, retained %d, packet id %d, topic %.*s, payload length %d, payload %.*s", + dup, qos, retained, packetid, + (topicName.lenstring.len < 20) ? topicName.lenstring.len : 20, topicName.lenstring.data, + payloadlen, (payloadlen < 20) ? payloadlen : 20, payload); + return strindex; +} + + +int MQTTStringFormat_ack(char* strbuf, int strbuflen, unsigned char packettype, unsigned char dup, unsigned short packetid) +{ + int strindex = snprintf(strbuf, strbuflen, "%s, packet id %d", MQTTPacket_names[packettype], packetid); + if (dup) + strindex += snprintf(strbuf + strindex, strbuflen - strindex, ", dup %d", dup); + return strindex; +} + + +int MQTTStringFormat_subscribe(char* strbuf, int strbuflen, unsigned char dup, unsigned short packetid, int count, + MQTTString topicFilters[], int requestedQoSs[]) +{ + return snprintf(strbuf, strbuflen, + "SUBSCRIBE dup %d, packet id %d count %d topic %.*s qos %d", + dup, packetid, count, + topicFilters[0].lenstring.len, topicFilters[0].lenstring.data, + requestedQoSs[0]); +} + + +int MQTTStringFormat_suback(char* strbuf, int strbuflen, unsigned short packetid, int count, int* grantedQoSs) +{ + return snprintf(strbuf, strbuflen, + "SUBACK packet id %d count %d granted qos %d", packetid, count, grantedQoSs[0]); +} + + +int MQTTStringFormat_unsubscribe(char* strbuf, int strbuflen, unsigned char dup, unsigned short packetid, + int count, MQTTString topicFilters[]) +{ + return snprintf(strbuf, strbuflen, + "UNSUBSCRIBE dup %d, packet id %d count %d topic %.*s", + dup, packetid, count, + topicFilters[0].lenstring.len, topicFilters[0].lenstring.data); +} + + +char* MQTTFormat_toClientString(char* strbuf, int strbuflen, unsigned char* buf, int buflen) +{ + int index = 0; + int rem_length = 0; + MQTTHeader header = {0}; + + header.byte = buf[index++]; + index += MQTTPacket_decodeBuf(&buf[index], &rem_length); + + switch (header.bits.type) + { + case CONNACK: + { + unsigned char sessionPresent, connack_rc; + if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, buf, buflen) == 1) + MQTTStringFormat_connack(strbuf, strbuflen, connack_rc, sessionPresent); + } + break; + case PUBLISH: + { + unsigned char dup, retained, *payload; + unsigned short packetid; + int qos, payloadlen; + MQTTString topicName = MQTTString_initializer; + if (MQTTDeserialize_publish(&dup, &qos, &retained, &packetid, &topicName, + &payload, &payloadlen, buf, buflen) == 1) + MQTTStringFormat_publish(strbuf, strbuflen, dup, qos, retained, packetid, + topicName, payload, payloadlen); + } + break; + case PUBACK: + case PUBREC: + case PUBREL: + case PUBCOMP: + { + unsigned char packettype, dup; + unsigned short packetid; + if (MQTTDeserialize_ack(&packettype, &dup, &packetid, buf, buflen) == 1) + MQTTStringFormat_ack(strbuf, strbuflen, packettype, dup, packetid); + } + break; + case SUBACK: + { + unsigned short packetid; + int maxcount = 1, count = 0; + int grantedQoSs[1]; + if (MQTTDeserialize_suback(&packetid, maxcount, &count, grantedQoSs, buf, buflen) == 1) + MQTTStringFormat_suback(strbuf, strbuflen, packetid, count, grantedQoSs); + } + break; + case UNSUBACK: + { + unsigned short packetid; + if (MQTTDeserialize_unsuback(&packetid, buf, buflen) == 1) + MQTTStringFormat_ack(strbuf, strbuflen, UNSUBACK, 0, packetid); + } + break; + case PINGREQ: + case PINGRESP: + case DISCONNECT: + snprintf(strbuf, strbuflen, "%s", MQTTPacket_names[header.bits.type]); + break; + } + return strbuf; +} + + +char* MQTTFormat_toServerString(char* strbuf, int strbuflen, unsigned char* buf, int buflen) +{ + int index = 0; + int rem_length = 0; + MQTTHeader header = {0}; + + header.byte = buf[index++]; + index += MQTTPacket_decodeBuf(&buf[index], &rem_length); + + switch (header.bits.type) + { + case CONNECT: + { + MQTTPacket_connectData data; + int rc; + if ((rc = MQTTDeserialize_connect(&data, buf, buflen)) == 1) + MQTTStringFormat_connect(strbuf, strbuflen, &data); + } + break; + case PUBLISH: + { + unsigned char dup, retained, *payload; + unsigned short packetid; + int qos, payloadlen; + MQTTString topicName = MQTTString_initializer; + if (MQTTDeserialize_publish(&dup, &qos, &retained, &packetid, &topicName, + &payload, &payloadlen, buf, buflen) == 1) + MQTTStringFormat_publish(strbuf, strbuflen, dup, qos, retained, packetid, + topicName, payload, payloadlen); + } + break; + case PUBACK: + case PUBREC: + case PUBREL: + case PUBCOMP: + { + unsigned char packettype, dup; + unsigned short packetid; + if (MQTTDeserialize_ack(&packettype, &dup, &packetid, buf, buflen) == 1) + MQTTStringFormat_ack(strbuf, strbuflen, packettype, dup, packetid); + } + break; + case SUBSCRIBE: + { + unsigned char dup; + unsigned short packetid; + int maxcount = 1, count = 0; + MQTTString topicFilters[1]; + int requestedQoSs[1]; + if (MQTTDeserialize_subscribe(&dup, &packetid, maxcount, &count, + topicFilters, requestedQoSs, buf, buflen) == 1) + MQTTStringFormat_subscribe(strbuf, strbuflen, dup, packetid, count, topicFilters, requestedQoSs);; + } + break; + case UNSUBSCRIBE: + { + unsigned char dup; + unsigned short packetid; + int maxcount = 1, count = 0; + MQTTString topicFilters[1]; + if (MQTTDeserialize_unsubscribe(&dup, &packetid, maxcount, &count, topicFilters, buf, buflen) == 1) + MQTTStringFormat_unsubscribe(strbuf, strbuflen, dup, packetid, count, topicFilters); + } + break; + case PINGREQ: + case PINGRESP: + case DISCONNECT: + snprintf(strbuf, strbuflen, "%s", MQTTPacket_names[header.bits.type]); + break; + } + strbuf[strbuflen] = '\0'; + return strbuf; +} diff --git a/src/utility/MQTTFormat.h b/src/utility/MQTTFormat.h new file mode 100644 index 0000000..47b0c41 --- /dev/null +++ b/src/utility/MQTTFormat.h @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#if !defined(MQTTFORMAT_H) +#define MQTTFORMAT_H + +#include "StackTrace.h" +#include "MQTTPacket.h" + +const char* MQTTPacket_getName(unsigned short packetid); +int MQTTStringFormat_connect(char* strbuf, int strbuflen, MQTTPacket_connectData* data); +int MQTTStringFormat_connack(char* strbuf, int strbuflen, unsigned char connack_rc, unsigned char sessionPresent); +int MQTTStringFormat_publish(char* strbuf, int strbuflen, unsigned char dup, int qos, unsigned char retained, + unsigned short packetid, MQTTString topicName, unsigned char* payload, int payloadlen); +int MQTTStringFormat_ack(char* strbuf, int strbuflen, unsigned char packettype, unsigned char dup, unsigned short packetid); +int MQTTStringFormat_subscribe(char* strbuf, int strbuflen, unsigned char dup, unsigned short packetid, int count, + MQTTString topicFilters[], int requestedQoSs[]); +int MQTTStringFormat_suback(char* strbuf, int strbuflen, unsigned short packetid, int count, int* grantedQoSs); +int MQTTStringFormat_unsubscribe(char* strbuf, int strbuflen, unsigned char dup, unsigned short packetid, + int count, MQTTString topicFilters[]); +char* MQTTFormat_toClientString(char* strbuf, int strbuflen, unsigned char* buf, int buflen); +char* MQTTFormat_toServerString(char* strbuf, int strbuflen, unsigned char* buf, int buflen); + +#endif diff --git a/src/utility/MQTTLogging.h b/src/utility/MQTTLogging.h new file mode 100644 index 0000000..ebe246c --- /dev/null +++ b/src/utility/MQTTLogging.h @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +/******************************************************************************* + * Modified for Arduino environment by Temboo + * Joe Planisky - replaced fancy fprintf stuff with Serial.print(ln) + *******************************************************************************/ + +#if !defined(MQTT_LOGGING_H) +#define MQTT_LOGGING_H + +#if !defined(DEBUG) +#define DEBUG(msg) {Serial.print("DEBUG: "); Serial.println(msg);} +#endif + +#if !defined(LOG) +#define LOG(msg) {Serial.print("LOG: "); Serial.println(msg);} +#endif + +#if !defined(WARN) +#define WARN(msg) {Serial.print("WARN: "); Serial.println(msg);} +#endif + +#if !defined(ERROR) +#define ERROR() {Serial.print("ERROR: "); Serial.println(msg);} +#endif + +#endif diff --git a/src/utility/MQTTPacket.c b/src/utility/MQTTPacket.c new file mode 100644 index 0000000..bd5f90a --- /dev/null +++ b/src/utility/MQTTPacket.c @@ -0,0 +1,410 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Sergio R. Caprile - non-blocking packet read functions for stream transport + *******************************************************************************/ + +#include "StackTrace.h" +#include "MQTTPacket.h" + +#include + +/** + * Encodes the message length according to the MQTT algorithm + * @param buf the buffer into which the encoded data is written + * @param length the length to be encoded + * @return the number of bytes written to buffer + */ +int MQTTPacket_encode(unsigned char* buf, int length) +{ + int rc = 0; + + FUNC_ENTRY; + do + { + char d = length % 128; + length /= 128; + /* if there are more digits to encode, set the top bit of this digit */ + if (length > 0) + d |= 0x80; + buf[rc++] = d; + } while (length > 0); + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Decodes the message length according to the MQTT algorithm + * @param getcharfn pointer to function to read the next character from the data source + * @param value the decoded length returned + * @return the number of bytes read from the socket + */ +int MQTTPacket_decode(int (*getcharfn)(unsigned char*, int), int* value) +{ + unsigned char c; + int multiplier = 1; + int len = 0; +#define MAX_NO_OF_REMAINING_LENGTH_BYTES 4 + + FUNC_ENTRY; + *value = 0; + do + { + int rc = MQTTPACKET_READ_ERROR; + + if (++len > MAX_NO_OF_REMAINING_LENGTH_BYTES) + { + rc = MQTTPACKET_READ_ERROR; /* bad data */ + goto exit; + } + rc = (*getcharfn)(&c, 1); + if (rc != 1) + goto exit; + *value += (c & 127) * multiplier; + multiplier *= 128; + } while ((c & 128) != 0); +exit: + FUNC_EXIT_RC(len); + return len; +} + + +int MQTTPacket_len(int rem_len) +{ + rem_len += 1; /* header byte */ + + /* now remaining_length field */ + if (rem_len < 128) + rem_len += 1; + else if (rem_len < 16384) + rem_len += 2; + else if (rem_len < 2097151) + rem_len += 3; + else + rem_len += 4; + return rem_len; +} + + +static unsigned char* bufptr; + +int bufchar(unsigned char* c, int count) +{ + int i; + + for (i = 0; i < count; ++i) + *c = *bufptr++; + return count; +} + + +int MQTTPacket_decodeBuf(unsigned char* buf, int* value) +{ + bufptr = buf; + return MQTTPacket_decode(bufchar, value); +} + + +/** + * Calculates an integer from two bytes read from the input buffer + * @param pptr pointer to the input buffer - incremented by the number of bytes used & returned + * @return the integer value calculated + */ +int readInt(unsigned char** pptr) +{ + unsigned char* ptr = *pptr; + int len = 256*(*ptr) + (*(ptr+1)); + *pptr += 2; + return len; +} + + +/** + * Reads one character from the input buffer. + * @param pptr pointer to the input buffer - incremented by the number of bytes used & returned + * @return the character read + */ +char readChar(unsigned char** pptr) +{ + char c = **pptr; + (*pptr)++; + return c; +} + + +/** + * Writes one character to an output buffer. + * @param pptr pointer to the output buffer - incremented by the number of bytes used & returned + * @param c the character to write + */ +void writeChar(unsigned char** pptr, char c) +{ + **pptr = c; + (*pptr)++; +} + + +/** + * Writes an integer as 2 bytes to an output buffer. + * @param pptr pointer to the output buffer - incremented by the number of bytes used & returned + * @param anInt the integer to write + */ +void writeInt(unsigned char** pptr, int anInt) +{ + **pptr = (unsigned char)(anInt / 256); + (*pptr)++; + **pptr = (unsigned char)(anInt % 256); + (*pptr)++; +} + + +/** + * Writes a "UTF" string to an output buffer. Converts C string to length-delimited. + * @param pptr pointer to the output buffer - incremented by the number of bytes used & returned + * @param string the C string to write + */ +void writeCString(unsigned char** pptr, const char* string) +{ + int len = strlen(string); + writeInt(pptr, len); + memcpy(*pptr, string, len); + *pptr += len; +} + + +int getLenStringLen(char* ptr) +{ + int len = 256*((unsigned char)(*ptr)) + (unsigned char)(*(ptr+1)); + return len; +} + + +void writeMQTTString(unsigned char** pptr, MQTTString mqttstring) +{ + if (mqttstring.lenstring.len > 0) + { + writeInt(pptr, mqttstring.lenstring.len); + memcpy(*pptr, mqttstring.lenstring.data, mqttstring.lenstring.len); + *pptr += mqttstring.lenstring.len; + } + else if (mqttstring.cstring) + writeCString(pptr, mqttstring.cstring); + else + writeInt(pptr, 0); +} + + +/** + * @param mqttstring the MQTTString structure into which the data is to be read + * @param pptr pointer to the output buffer - incremented by the number of bytes used & returned + * @param enddata pointer to the end of the data: do not read beyond + * @return 1 if successful, 0 if not + */ +int readMQTTLenString(MQTTString* mqttstring, unsigned char** pptr, unsigned char* enddata) +{ + int rc = 0; + + FUNC_ENTRY; + /* the first two bytes are the length of the string */ + if (enddata - (*pptr) > 1) /* enough length to read the integer? */ + { + mqttstring->lenstring.len = readInt(pptr); /* increments pptr to point past length */ + if (&(*pptr)[mqttstring->lenstring.len] <= enddata) + { + mqttstring->lenstring.data = (char*)*pptr; + *pptr += mqttstring->lenstring.len; + rc = 1; + } + } + mqttstring->cstring = NULL; + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Return the length of the MQTTstring - C string if there is one, otherwise the length delimited string + * @param mqttstring the string to return the length of + * @return the length of the string + */ +int MQTTstrlen(MQTTString mqttstring) +{ + int rc = 0; + + if (mqttstring.cstring) + rc = strlen(mqttstring.cstring); + else + rc = mqttstring.lenstring.len; + return rc; +} + + +/** + * Compares an MQTTString to a C string + * @param a the MQTTString to compare + * @param bptr the C string to compare + * @return boolean - equal or not + */ +int MQTTPacket_equals(MQTTString* a, char* bptr) +{ + int alen = 0, + blen = 0; + char *aptr; + + if (a->cstring) + { + aptr = a->cstring; + alen = strlen(a->cstring); + } + else + { + aptr = a->lenstring.data; + alen = a->lenstring.len; + } + blen = strlen(bptr); + + return (alen == blen) && (strncmp(aptr, bptr, alen) == 0); +} + + +/** + * Helper function to read packet data from some source into a buffer + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param getfn pointer to a function which will read any number of bytes from the needed source + * @return integer MQTT packet type, or -1 on error + * @note the whole message must fit into the caller's buffer + */ +int MQTTPacket_read(unsigned char* buf, int buflen, int (*getfn)(unsigned char*, int)) +{ + int rc = -1; + MQTTHeader header = {0}; + int len = 0; + int rem_len = 0; + + /* 1. read the header byte. This has the packet type in it */ + if ((*getfn)(buf, 1) != 1) + goto exit; + + len = 1; + /* 2. read the remaining length. This is variable in itself */ + MQTTPacket_decode(getfn, &rem_len); + len += MQTTPacket_encode(buf + 1, rem_len); /* put the original remaining length back into the buffer */ + + /* 3. read the rest of the buffer using a callback to supply the rest of the data */ + if((rem_len + len) > buflen) + goto exit; + if ((*getfn)(buf + len, rem_len) != rem_len) + goto exit; + + header.byte = buf[0]; + rc = header.bits.type; +exit: + return rc; +} + +/** + * Decodes the message length according to the MQTT algorithm, non-blocking + * @param trp pointer to a transport structure holding what is needed to solve getting data from it + * @param value the decoded length returned + * @return integer the number of bytes read from the socket, 0 for call again, or -1 on error + */ +static int MQTTPacket_decodenb(MQTTTransport *trp) +{ + unsigned char c; + int rc = MQTTPACKET_READ_ERROR; + + FUNC_ENTRY; + if(trp->len == 0){ /* initialize on first call */ + trp->multiplier = 1; + trp->rem_len = 0; + } + do { + int frc; + if (++(trp->len) > MAX_NO_OF_REMAINING_LENGTH_BYTES) + goto exit; + if ((frc=(*trp->getfn)(trp->sck, &c, 1)) == -1) + goto exit; + if (frc == 0){ + rc = 0; + goto exit; + } + trp->rem_len += (c & 127) * trp->multiplier; + trp->multiplier *= 128; + } while ((c & 128) != 0); + rc = trp->len; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + +/** + * Helper function to read packet data from some source into a buffer, non-blocking + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param trp pointer to a transport structure holding what is needed to solve getting data from it + * @return integer MQTT packet type, 0 for call again, or -1 on error + * @note the whole message must fit into the caller's buffer + */ +int MQTTPacket_readnb(unsigned char* buf, int buflen, MQTTTransport *trp) +{ + int rc = -1, frc; + MQTTHeader header = {0}; + + switch(trp->state){ + default: + trp->state = 0; + /*FALLTHROUGH*/ + case 0: + /* read the header byte. This has the packet type in it */ + if ((frc=(*trp->getfn)(trp->sck, buf, 1)) == -1) + goto exit; + if (frc == 0) + return 0; + trp->len = 0; + ++trp->state; + /*FALLTHROUGH*/ + /* read the remaining length. This is variable in itself */ + case 1: + if((frc=MQTTPacket_decodenb(trp)) == MQTTPACKET_READ_ERROR) + goto exit; + if(frc == 0) + return 0; + trp->len = 1 + MQTTPacket_encode(buf + 1, trp->rem_len); /* put the original remaining length back into the buffer */ + if((trp->rem_len + trp->len) > buflen) + goto exit; + ++trp->state; + /*FALLTHROUGH*/ + case 2: + /* read the rest of the buffer using a callback to supply the rest of the data */ + if ((frc=(*trp->getfn)(trp->sck, buf + trp->len, trp->rem_len)) == -1) + goto exit; + if (frc == 0) + return 0; + trp->rem_len -= frc; + trp->len += frc; + if(trp->rem_len) + return 0; + + header.byte = buf[0]; + rc = header.bits.type; + break; + } + +exit: + trp->state = 0; + return rc; +} + diff --git a/src/utility/MQTTPacket.h b/src/utility/MQTTPacket.h new file mode 100644 index 0000000..588966d --- /dev/null +++ b/src/utility/MQTTPacket.h @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Xiang Rong - 442039 Add makefile to Embedded C client + *******************************************************************************/ + +#ifndef MQTTPACKET_H_ +#define MQTTPACKET_H_ + +#if defined(__cplusplus) /* If this is a C++ compiler, use C linkage */ +extern "C" { +#endif + +#if defined(WIN32_DLL) || defined(WIN64_DLL) + #define DLLImport __declspec(dllimport) + #define DLLExport __declspec(dllexport) +#elif defined(LINUX_SO) + #define DLLImport extern + #define DLLExport __attribute__ ((visibility ("default"))) +#else + #define DLLImport + #define DLLExport +#endif + +enum errors +{ + MQTTPACKET_BUFFER_TOO_SHORT = -2, + MQTTPACKET_READ_ERROR = -1, + MQTTPACKET_READ_COMPLETE +}; + +enum msgTypes +{ + CONNECT = 1, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, + PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, + PINGREQ, PINGRESP, DISCONNECT +}; + +/** + * Bitfields for the MQTT header byte. + */ +typedef union +{ + unsigned char byte; /**< the whole byte */ +#if defined(REVERSED) + struct + { + unsigned int type : 4; /**< message type nibble */ + unsigned int dup : 1; /**< DUP flag bit */ + unsigned int qos : 2; /**< QoS value, 0, 1 or 2 */ + unsigned int retain : 1; /**< retained flag bit */ + } bits; +#else + struct + { + unsigned int retain : 1; /**< retained flag bit */ + unsigned int qos : 2; /**< QoS value, 0, 1 or 2 */ + unsigned int dup : 1; /**< DUP flag bit */ + unsigned int type : 4; /**< message type nibble */ + } bits; +#endif +} MQTTHeader; + +typedef struct +{ + int len; + char* data; +} MQTTLenString; + +typedef struct +{ + char* cstring; + MQTTLenString lenstring; +} MQTTString; + +#define MQTTString_initializer {NULL, {0, NULL}} + +int MQTTstrlen(MQTTString mqttstring); + +#include "MQTTConnect.h" +#include "MQTTPublish.h" +#include "MQTTSubscribe.h" +#include "MQTTUnsubscribe.h" +#include "MQTTFormat.h" + +int MQTTSerialize_ack(unsigned char* buf, int buflen, unsigned char type, unsigned char dup, unsigned short packetid); +int MQTTDeserialize_ack(unsigned char* packettype, unsigned char* dup, unsigned short* packetid, unsigned char* buf, int buflen); + +int MQTTPacket_len(int rem_len); +int MQTTPacket_equals(MQTTString* a, char* b); + +int MQTTPacket_encode(unsigned char* buf, int length); +int MQTTPacket_decode(int (*getcharfn)(unsigned char*, int), int* value); +int MQTTPacket_decodeBuf(unsigned char* buf, int* value); + +int readInt(unsigned char** pptr); +char readChar(unsigned char** pptr); +void writeChar(unsigned char** pptr, char c); +void writeInt(unsigned char** pptr, int anInt); +int readMQTTLenString(MQTTString* mqttstring, unsigned char** pptr, unsigned char* enddata); +void writeCString(unsigned char** pptr, const char* string); +void writeMQTTString(unsigned char** pptr, MQTTString mqttstring); + +DLLExport int MQTTPacket_read(unsigned char* buf, int buflen, int (*getfn)(unsigned char*, int)); + +typedef struct { + int (*getfn)(void *, unsigned char*, int); /* must return -1 for error, 0 for call again, or the number of bytes read */ + void *sck; /* pointer to whatever the system may use to identify the transport */ + int multiplier; + int rem_len; + int len; + char state; +}MQTTTransport; + +int MQTTPacket_readnb(unsigned char* buf, int buflen, MQTTTransport *trp); + +#ifdef __cplusplus /* If this is a C++ compiler, use C linkage */ +} +#endif + + +#endif /* MQTTPACKET_H_ */ diff --git a/src/utility/MQTTPublish.h b/src/utility/MQTTPublish.h new file mode 100644 index 0000000..ebe479d --- /dev/null +++ b/src/utility/MQTTPublish.h @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Xiang Rong - 442039 Add makefile to Embedded C client + *******************************************************************************/ + +#ifndef MQTTPUBLISH_H_ +#define MQTTPUBLISH_H_ + +#if !defined(DLLImport) + #define DLLImport +#endif +#if !defined(DLLExport) + #define DLLExport +#endif + +DLLExport int MQTTSerialize_publish(unsigned char* buf, int buflen, unsigned char dup, int qos, unsigned char retained, unsigned short packetid, + MQTTString topicName, unsigned char* payload, int payloadlen); + +DLLExport int MQTTDeserialize_publish(unsigned char* dup, int* qos, unsigned char* retained, unsigned short* packetid, MQTTString* topicName, + unsigned char** payload, int* payloadlen, unsigned char* buf, int len); + +DLLExport int MQTTSerialize_puback(unsigned char* buf, int buflen, unsigned short packetid); +DLLExport int MQTTSerialize_pubrel(unsigned char* buf, int buflen, unsigned char dup, unsigned short packetid); +DLLExport int MQTTSerialize_pubcomp(unsigned char* buf, int buflen, unsigned short packetid); + +#endif /* MQTTPUBLISH_H_ */ diff --git a/src/utility/MQTTSerializePublish.c b/src/utility/MQTTSerializePublish.c new file mode 100644 index 0000000..2818a2d --- /dev/null +++ b/src/utility/MQTTSerializePublish.c @@ -0,0 +1,168 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "MQTTPacket.h" +#include "StackTrace.h" + +#include + + +/** + * Determines the length of the MQTT publish packet that would be produced using the supplied parameters + * @param qos the MQTT QoS of the publish (packetid is omitted for QoS 0) + * @param topicName the topic name to be used in the publish + * @param payloadlen the length of the payload to be sent + * @return the length of buffer needed to contain the serialized version of the packet + */ +int MQTTSerialize_publishLength(int qos, MQTTString topicName, int payloadlen) +{ + int len = 0; + + len += 2 + MQTTstrlen(topicName) + payloadlen; + if (qos > 0) + len += 2; /* packetid */ + return len; +} + + +/** + * Serializes the supplied publish data into the supplied buffer, ready for sending + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param dup integer - the MQTT dup flag + * @param qos integer - the MQTT QoS value + * @param retained integer - the MQTT retained flag + * @param packetid integer - the MQTT packet identifier + * @param topicName MQTTString - the MQTT topic in the publish + * @param payload byte buffer - the MQTT publish payload + * @param payloadlen integer - the length of the MQTT payload + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTSerialize_publish(unsigned char* buf, int buflen, unsigned char dup, int qos, unsigned char retained, unsigned short packetid, + MQTTString topicName, unsigned char* payload, int payloadlen) +{ + unsigned char *ptr = buf; + MQTTHeader header = {0}; + int rem_len = 0; + int rc = 0; + + FUNC_ENTRY; + if (MQTTPacket_len(rem_len = MQTTSerialize_publishLength(qos, topicName, payloadlen)) > buflen) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + + header.bits.type = PUBLISH; + header.bits.dup = dup; + header.bits.qos = qos; + header.bits.retain = retained; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, rem_len); /* write remaining length */; + + writeMQTTString(&ptr, topicName); + + if (qos > 0) + writeInt(&ptr, packetid); + + memcpy(ptr, payload, payloadlen); + ptr += payloadlen; + + rc = ptr - buf; + +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + + +/** + * Serializes the ack packet into the supplied buffer. + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param type the MQTT packet type + * @param dup the MQTT dup flag + * @param packetid the MQTT packet identifier + * @return serialized length, or error if 0 + */ +int MQTTSerialize_ack(unsigned char* buf, int buflen, unsigned char packettype, unsigned char dup, unsigned short packetid) +{ + MQTTHeader header = {0}; + int rc = 0; + unsigned char *ptr = buf; + + FUNC_ENTRY; + if (buflen < 4) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + header.bits.type = packettype; + header.bits.dup = dup; + header.bits.qos = 0; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, 2); /* write remaining length */ + writeInt(&ptr, packetid); + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Serializes a puback packet into the supplied buffer. + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param packetid integer - the MQTT packet identifier + * @return serialized length, or error if 0 + */ +int MQTTSerialize_puback(unsigned char* buf, int buflen, unsigned short packetid) +{ + return MQTTSerialize_ack(buf, buflen, PUBACK, packetid, 0); +} + + +/** + * Serializes a pubrel packet into the supplied buffer. + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param dup integer - the MQTT dup flag + * @param packetid integer - the MQTT packet identifier + * @return serialized length, or error if 0 + */ +int MQTTSerialize_pubrel(unsigned char* buf, int buflen, unsigned char dup, unsigned short packetid) +{ + return MQTTSerialize_ack(buf, buflen, PUBREL, packetid, dup); +} + + +/** + * Serializes a pubrel packet into the supplied buffer. + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param packetid integer - the MQTT packet identifier + * @return serialized length, or error if 0 + */ +int MQTTSerialize_pubcomp(unsigned char* buf, int buflen, unsigned short packetid) +{ + return MQTTSerialize_ack(buf, buflen, PUBCOMP, packetid, 0); +} + + diff --git a/src/utility/MQTTSubscribe.h b/src/utility/MQTTSubscribe.h new file mode 100644 index 0000000..aa91826 --- /dev/null +++ b/src/utility/MQTTSubscribe.h @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Xiang Rong - 442039 Add makefile to Embedded C client + *******************************************************************************/ + +#ifndef MQTTSUBSCRIBE_H_ +#define MQTTSUBSCRIBE_H_ + +#if !defined(DLLImport) + #define DLLImport +#endif +#if !defined(DLLExport) + #define DLLExport +#endif + +DLLExport int MQTTSerialize_subscribe(unsigned char* buf, int buflen, unsigned char dup, unsigned short packetid, + int count, MQTTString topicFilters[], int requestedQoSs[]); + +DLLExport int MQTTDeserialize_subscribe(unsigned char* dup, unsigned short* packetid, + int maxcount, int* count, MQTTString topicFilters[], int requestedQoSs[], unsigned char* buf, int len); + +DLLExport int MQTTSerialize_suback(unsigned char* buf, int buflen, unsigned short packetid, int count, int* grantedQoSs); + +DLLExport int MQTTDeserialize_suback(unsigned short* packetid, int maxcount, int* count, int grantedQoSs[], unsigned char* buf, int len); + + +#endif /* MQTTSUBSCRIBE_H_ */ diff --git a/src/utility/MQTTSubscribeClient.c b/src/utility/MQTTSubscribeClient.c new file mode 100644 index 0000000..57a0613 --- /dev/null +++ b/src/utility/MQTTSubscribeClient.c @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "MQTTPacket.h" +#include "StackTrace.h" + +#include + +/** + * Determines the length of the MQTT subscribe packet that would be produced using the supplied parameters + * @param count the number of topic filter strings in topicFilters + * @param topicFilters the array of topic filter strings to be used in the publish + * @return the length of buffer needed to contain the serialized version of the packet + */ +int MQTTSerialize_subscribeLength(int count, MQTTString topicFilters[]) +{ + int i; + int len = 2; /* packetid */ + + for (i = 0; i < count; ++i) + len += 2 + MQTTstrlen(topicFilters[i]) + 1; /* length + topic + req_qos */ + return len; +} + + +/** + * Serializes the supplied subscribe data into the supplied buffer, ready for sending + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied bufferr + * @param dup integer - the MQTT dup flag + * @param packetid integer - the MQTT packet identifier + * @param count - number of members in the topicFilters and reqQos arrays + * @param topicFilters - array of topic filter names + * @param requestedQoSs - array of requested QoS + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTSerialize_subscribe(unsigned char* buf, int buflen, unsigned char dup, unsigned short packetid, int count, + MQTTString topicFilters[], int requestedQoSs[]) +{ + unsigned char *ptr = buf; + MQTTHeader header = {0}; + int rem_len = 0; + int rc = 0; + int i = 0; + + FUNC_ENTRY; + if (MQTTPacket_len(rem_len = MQTTSerialize_subscribeLength(count, topicFilters)) > buflen) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + + header.byte = 0; + header.bits.type = SUBSCRIBE; + header.bits.dup = dup; + header.bits.qos = 1; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, rem_len); /* write remaining length */; + + writeInt(&ptr, packetid); + + for (i = 0; i < count; ++i) + { + writeMQTTString(&ptr, topicFilters[i]); + writeChar(&ptr, requestedQoSs[i]); + } + + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + + +/** + * Deserializes the supplied (wire) buffer into suback data + * @param packetid returned integer - the MQTT packet identifier + * @param maxcount - the maximum number of members allowed in the grantedQoSs array + * @param count returned integer - number of members in the grantedQoSs array + * @param grantedQoSs returned array of integers - the granted qualities of service + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @return error code. 1 is success, 0 is failure + */ +int MQTTDeserialize_suback(unsigned short* packetid, int maxcount, int* count, int grantedQoSs[], unsigned char* buf, int buflen) +{ + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = 0; + int mylen; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + if (header.bits.type != SUBACK) + goto exit; + + curdata += (rc = MQTTPacket_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + if (enddata - curdata < 2) + goto exit; + + *packetid = readInt(&curdata); + + *count = 0; + while (curdata < enddata) + { + if (*count > maxcount) + { + rc = -1; + goto exit; + } + grantedQoSs[(*count)++] = readChar(&curdata); + } + + rc = 1; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + diff --git a/src/utility/MQTTSubscribeServer.c b/src/utility/MQTTSubscribeServer.c new file mode 100644 index 0000000..5579645 --- /dev/null +++ b/src/utility/MQTTSubscribeServer.c @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "MQTTPacket.h" +#include "StackTrace.h" + +#include + + +/** + * Deserializes the supplied (wire) buffer into subscribe data + * @param dup integer returned - the MQTT dup flag + * @param packetid integer returned - the MQTT packet identifier + * @param maxcount - the maximum number of members allowed in the topicFilters and requestedQoSs arrays + * @param count - number of members in the topicFilters and requestedQoSs arrays + * @param topicFilters - array of topic filter names + * @param requestedQoSs - array of requested QoS + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTDeserialize_subscribe(unsigned char* dup, unsigned short* packetid, int maxcount, int* count, MQTTString topicFilters[], + int requestedQoSs[], unsigned char* buf, int buflen) +{ + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = -1; + int mylen = 0; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + if (header.bits.type != SUBSCRIBE) + goto exit; + *dup = header.bits.dup; + + curdata += (rc = MQTTPacket_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + + *packetid = readInt(&curdata); + + *count = 0; + while (curdata < enddata) + { + if (!readMQTTLenString(&topicFilters[*count], &curdata, enddata)) + goto exit; + if (curdata >= enddata) /* do we have enough data to read the req_qos version byte? */ + goto exit; + requestedQoSs[*count] = readChar(&curdata); + (*count)++; + } + + rc = 1; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Serializes the supplied suback data into the supplied buffer, ready for sending + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param packetid integer - the MQTT packet identifier + * @param count - number of members in the grantedQoSs array + * @param grantedQoSs - array of granted QoS + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTSerialize_suback(unsigned char* buf, int buflen, unsigned short packetid, int count, int* grantedQoSs) +{ + MQTTHeader header = {0}; + int rc = -1; + unsigned char *ptr = buf; + int i; + + FUNC_ENTRY; + if (buflen < 2 + count) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + header.byte = 0; + header.bits.type = SUBACK; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, 2 + count); /* write remaining length */ + + writeInt(&ptr, packetid); + + for (i = 0; i < count; ++i) + writeChar(&ptr, grantedQoSs[i]); + + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + diff --git a/src/utility/MQTTUnsubscribe.h b/src/utility/MQTTUnsubscribe.h new file mode 100644 index 0000000..355ca9a --- /dev/null +++ b/src/utility/MQTTUnsubscribe.h @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Xiang Rong - 442039 Add makefile to Embedded C client + *******************************************************************************/ + +#ifndef MQTTUNSUBSCRIBE_H_ +#define MQTTUNSUBSCRIBE_H_ + +#if !defined(DLLImport) + #define DLLImport +#endif +#if !defined(DLLExport) + #define DLLExport +#endif + +DLLExport int MQTTSerialize_unsubscribe(unsigned char* buf, int buflen, unsigned char dup, unsigned short packetid, + int count, MQTTString topicFilters[]); + +DLLExport int MQTTDeserialize_unsubscribe(unsigned char* dup, unsigned short* packetid, int max_count, int* count, MQTTString topicFilters[], + unsigned char* buf, int len); + +DLLExport int MQTTSerialize_unsuback(unsigned char* buf, int buflen, unsigned short packetid); + +DLLExport int MQTTDeserialize_unsuback(unsigned short* packetid, unsigned char* buf, int len); + +#endif /* MQTTUNSUBSCRIBE_H_ */ diff --git a/src/utility/MQTTUnsubscribeClient.c b/src/utility/MQTTUnsubscribeClient.c new file mode 100644 index 0000000..e7ec530 --- /dev/null +++ b/src/utility/MQTTUnsubscribeClient.c @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "MQTTPacket.h" +#include "StackTrace.h" + +#include + +/** + * Determines the length of the MQTT unsubscribe packet that would be produced using the supplied parameters + * @param count the number of topic filter strings in topicFilters + * @param topicFilters the array of topic filter strings to be used in the publish + * @return the length of buffer needed to contain the serialized version of the packet + */ +int MQTTSerialize_unsubscribeLength(int count, MQTTString topicFilters[]) +{ + int i; + int len = 2; /* packetid */ + + for (i = 0; i < count; ++i) + len += 2 + MQTTstrlen(topicFilters[i]); /* length + topic*/ + return len; +} + + +/** + * Serializes the supplied unsubscribe data into the supplied buffer, ready for sending + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @param dup integer - the MQTT dup flag + * @param packetid integer - the MQTT packet identifier + * @param count - number of members in the topicFilters array + * @param topicFilters - array of topic filter names + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTSerialize_unsubscribe(unsigned char* buf, int buflen, unsigned char dup, unsigned short packetid, + int count, MQTTString topicFilters[]) +{ + unsigned char *ptr = buf; + MQTTHeader header = {0}; + int rem_len = 0; + int rc = -1; + int i = 0; + + FUNC_ENTRY; + if (MQTTPacket_len(rem_len = MQTTSerialize_unsubscribeLength(count, topicFilters)) > buflen) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + + header.byte = 0; + header.bits.type = UNSUBSCRIBE; + header.bits.dup = dup; + header.bits.qos = 1; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, rem_len); /* write remaining length */; + + writeInt(&ptr, packetid); + + for (i = 0; i < count; ++i) + writeMQTTString(&ptr, topicFilters[i]); + + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Deserializes the supplied (wire) buffer into unsuback data + * @param packetid returned integer - the MQTT packet identifier + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @return error code. 1 is success, 0 is failure + */ +int MQTTDeserialize_unsuback(unsigned short* packetid, unsigned char* buf, int buflen) +{ + unsigned char type = 0; + unsigned char dup = 0; + int rc = 0; + + FUNC_ENTRY; + rc = MQTTDeserialize_ack(&type, &dup, packetid, buf, buflen); + if (type == UNSUBACK) + rc = 1; + FUNC_EXIT_RC(rc); + return rc; +} + + diff --git a/src/utility/MQTTUnsubscribeServer.c b/src/utility/MQTTUnsubscribeServer.c new file mode 100644 index 0000000..42b6102 --- /dev/null +++ b/src/utility/MQTTUnsubscribeServer.c @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + *******************************************************************************/ + +#include "MQTTPacket.h" +#include "StackTrace.h" + +#include + + +/** + * Deserializes the supplied (wire) buffer into unsubscribe data + * @param dup integer returned - the MQTT dup flag + * @param packetid integer returned - the MQTT packet identifier + * @param maxcount - the maximum number of members allowed in the topicFilters and requestedQoSs arrays + * @param count - number of members in the topicFilters and requestedQoSs arrays + * @param topicFilters - array of topic filter names + * @param buf the raw buffer data, of the correct length determined by the remaining length field + * @param buflen the length in bytes of the data in the supplied buffer + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTDeserialize_unsubscribe(unsigned char* dup, unsigned short* packetid, int maxcount, int* count, MQTTString topicFilters[], + unsigned char* buf, int len) +{ + MQTTHeader header = {0}; + unsigned char* curdata = buf; + unsigned char* enddata = NULL; + int rc = 0; + int mylen = 0; + + FUNC_ENTRY; + header.byte = readChar(&curdata); + if (header.bits.type != UNSUBSCRIBE) + goto exit; + *dup = header.bits.dup; + + curdata += (rc = MQTTPacket_decodeBuf(curdata, &mylen)); /* read remaining length */ + enddata = curdata + mylen; + + *packetid = readInt(&curdata); + + *count = 0; + while (curdata < enddata) + { + if (!readMQTTLenString(&topicFilters[*count], &curdata, enddata)) + goto exit; + (*count)++; + } + + rc = 1; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + +/** + * Serializes the supplied unsuback data into the supplied buffer, ready for sending + * @param buf the buffer into which the packet will be serialized + * @param buflen the length in bytes of the supplied buffer + * @param packetid integer - the MQTT packet identifier + * @return the length of the serialized data. <= 0 indicates error + */ +int MQTTSerialize_unsuback(unsigned char* buf, int buflen, unsigned short packetid) +{ + MQTTHeader header = {0}; + int rc = 0; + unsigned char *ptr = buf; + + FUNC_ENTRY; + if (buflen < 2) + { + rc = MQTTPACKET_BUFFER_TOO_SHORT; + goto exit; + } + header.byte = 0; + header.bits.type = UNSUBACK; + writeChar(&ptr, header.byte); /* write header */ + + ptr += MQTTPacket_encode(ptr, 2); /* write remaining length */ + + writeInt(&ptr, packetid); + + rc = ptr - buf; +exit: + FUNC_EXIT_RC(rc); + return rc; +} + + diff --git a/src/utility/StackTrace.h b/src/utility/StackTrace.h new file mode 100644 index 0000000..2808a0d --- /dev/null +++ b/src/utility/StackTrace.h @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + * Ian Craggs - fix for bug #434081 + *******************************************************************************/ + +#ifndef STACKTRACE_H_ +#define STACKTRACE_H_ + +#include +#define NOSTACKTRACE 1 + +#if defined(NOSTACKTRACE) +#define FUNC_ENTRY +#define FUNC_ENTRY_NOLOG +#define FUNC_ENTRY_MED +#define FUNC_ENTRY_MAX +#define FUNC_EXIT +#define FUNC_EXIT_NOLOG +#define FUNC_EXIT_MED +#define FUNC_EXIT_MAX +#define FUNC_EXIT_RC(x) +#define FUNC_EXIT_MED_RC(x) +#define FUNC_EXIT_MAX_RC(x) + +#else + +#if defined(WIN32) +#define inline __inline +#define FUNC_ENTRY StackTrace_entry(__FUNCTION__, __LINE__, TRACE_MINIMUM) +#define FUNC_ENTRY_NOLOG StackTrace_entry(__FUNCTION__, __LINE__, -1) +#define FUNC_ENTRY_MED StackTrace_entry(__FUNCTION__, __LINE__, TRACE_MEDIUM) +#define FUNC_ENTRY_MAX StackTrace_entry(__FUNCTION__, __LINE__, TRACE_MAXIMUM) +#define FUNC_EXIT StackTrace_exit(__FUNCTION__, __LINE__, NULL, TRACE_MINIMUM) +#define FUNC_EXIT_NOLOG StackTrace_exit(__FUNCTION__, __LINE__, -1) +#define FUNC_EXIT_MED StackTrace_exit(__FUNCTION__, __LINE__, NULL, TRACE_MEDIUM) +#define FUNC_EXIT_MAX StackTrace_exit(__FUNCTION__, __LINE__, NULL, TRACE_MAXIMUM) +#define FUNC_EXIT_RC(x) StackTrace_exit(__FUNCTION__, __LINE__, &x, TRACE_MINIMUM) +#define FUNC_EXIT_MED_RC(x) StackTrace_exit(__FUNCTION__, __LINE__, &x, TRACE_MEDIUM) +#define FUNC_EXIT_MAX_RC(x) StackTrace_exit(__FUNCTION__, __LINE__, &x, TRACE_MAXIMUM) +#else +#define FUNC_ENTRY StackTrace_entry(__func__, __LINE__, TRACE_MINIMUM) +#define FUNC_ENTRY_NOLOG StackTrace_entry(__func__, __LINE__, -1) +#define FUNC_ENTRY_MED StackTrace_entry(__func__, __LINE__, TRACE_MEDIUM) +#define FUNC_ENTRY_MAX StackTrace_entry(__func__, __LINE__, TRACE_MAXIMUM) +#define FUNC_EXIT StackTrace_exit(__func__, __LINE__, NULL, TRACE_MINIMUM) +#define FUNC_EXIT_NOLOG StackTrace_exit(__func__, __LINE__, NULL, -1) +#define FUNC_EXIT_MED StackTrace_exit(__func__, __LINE__, NULL, TRACE_MEDIUM) +#define FUNC_EXIT_MAX StackTrace_exit(__func__, __LINE__, NULL, TRACE_MAXIMUM) +#define FUNC_EXIT_RC(x) StackTrace_exit(__func__, __LINE__, &x, TRACE_MINIMUM) +#define FUNC_EXIT_MED_RC(x) StackTrace_exit(__func__, __LINE__, &x, TRACE_MEDIUM) +#define FUNC_EXIT_MAX_RC(x) StackTrace_exit(__func__, __LINE__, &x, TRACE_MAXIMUM) + +void StackTrace_entry(const char* name, int line, int trace); +void StackTrace_exit(const char* name, int line, void* return_value, int trace); + +void StackTrace_printStack(FILE* dest); +char* StackTrace_get(unsigned long); + +#endif + +#endif + + + + +#endif /* STACKTRACE_H_ */ diff --git a/src/utility/TembooCoAPIPStack.h b/src/utility/TembooCoAPIPStack.h new file mode 100644 index 0000000..de412b0 --- /dev/null +++ b/src/utility/TembooCoAPIPStack.h @@ -0,0 +1,101 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef TEMBOOCOAPIPSTACK_H_ +#define TEMBOOCOAPIPSTACK_H_ + +#include +#include +#include "TembooGlobal.h" + +class TembooCoAPIPStack +{ +public: + enum Error { + SUCCESS = 0, + ERROR_RESOLVING, + ERROR_WRITING, + ERROR_SENDING, + ERROR_RECEIVING + + }; + + + TembooCoAPIPStack(UDP& udp) : m_udp(udp) {} + + + int sendDatagram(IPAddress address, uint16_t port, uint8_t* data, size_t len) { + if (0 == m_udp.beginPacket(address, port)) { + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("UDP begin"); + return ERROR_RESOLVING; + } + + uint16_t c = m_udp.write(data, len); + if (len != c) { + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("UDP write"); + Serial.println(len); + Serial.println(c); + Serial.println((char*)data); + + return ERROR_WRITING; + } + + if (0 == m_udp.endPacket()) { + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("UDP send"); + return ERROR_SENDING; + } + + return SUCCESS; + } + + int recvDatagram(uint8_t* buffer, size_t maxLen, int32_t& count) { + memset(buffer, 0, maxLen); + count = 0; + if (m_udp.parsePacket() > 0) { + count = m_udp.read(buffer, maxLen); + if (count < 0) { + TEMBOO_TRACE("ERROR: "); + TEMBOO_TRACELN("UDP read"); + return ERROR_RECEIVING; + } + } + return SUCCESS; + } + + IPAddress getRemoteAddress() { + return m_udp.remoteIP(); + } + + uint16_t getRemotePort() { + return m_udp.remotePort(); + } + + +protected: + UDP& m_udp; +}; + +#endif + diff --git a/src/utility/TembooCoAPSession.cpp b/src/utility/TembooCoAPSession.cpp new file mode 100644 index 0000000..efdcc22 --- /dev/null +++ b/src/utility/TembooCoAPSession.cpp @@ -0,0 +1,199 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#include +#include +#include +#include "TembooCoAPSession.h" +#include "tmbhmac.h" +#include "DataFormatter.h" +#include "TembooTags.h" + +#define QSEND(x) if(!qsend((x))) { goto SendFailed;} + +const size_t MAX_HOST_LEN = 64; +const size_t MAX_URL_PATH_LEN = 100; + +const char EOL[] = "\r\n"; +const char POST[] = "POST "; +const char POSTAMBLE[] = " HTTP/1.0"; // Prevent host from using chunked encoding in response. +const char HEADER_HOST[] = "Host: "; +const char HEADER_ACCEPT[] = "Accept: application/xml"; +const char HEADER_ORG[] = "x-temboo-domain: /"; +const char HEADER_DOM[] = "/master"; +const char HEADER_CONTENT_LENGTH[] = "Content-Length: "; +const char HEADER_TIME[] = "x-temboo-time: "; +const char BASE_CHOREO_URI[] = "/arcturus-web/api-1.0/ar"; +const char HEADER_AUTH[] = "x-temboo-authentication: "; +const char HEADER_CONTENT_TYPE[] = "Content-Type: text/plain"; +const char TEMBOO_DOMAIN[] = ".temboolive.com"; +const char SDK_ID[] = "?source_id=CoAPGateway_1"; + + +unsigned long TembooCoAPSession::s_timeOffset = 0; + +TembooCoAPSession::TembooCoAPSession(TembooCoAPClient& client) : m_client(client) { +} + + +void TembooCoAPSession::setTime(unsigned long currentTime) { + s_timeOffset = currentTime - (millis()/1000); +} + + +unsigned long TembooCoAPSession::getTime() { + return s_timeOffset + (millis()/1000); +} + + + +int TembooCoAPSession::executeChoreo( + uint16_t requestId, + const char* accountName, + const char* appKeyName, + const char* appKeyValue, + const char* path, + const ChoreoInputSet& inputSet, + const ChoreoOutputSet& outputSet, + const ChoreoPreset& preset) { + + DataFormatter fmt(&inputSet, &outputSet, &preset); + char auth[HMAC_HEX_SIZE_BYTES + 1]; + char timeStr[11]; + char requestIdStr[5]; + uint16toa(requestId, requestIdStr); + + // We use the current time-of-day as salt on the app key. + // We keep track of time-of-day by getting the current time + // from the server and applying an offset (the length of time + // we've been running.) + uint32toa((uint32_t)TembooCoAPSession::getTime(), timeStr); + + getAuth(fmt, appKeyValue, timeStr, auth); + + m_client.clearData(); + + QSEND(TAG_REQUEST_ID); + QSEND(requestIdStr); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_ACCOUNT_NAME); + QSEND(accountName); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_APP_KEY_NAME); + QSEND(appKeyName); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_TIME); + QSEND(timeStr); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_AUTH); + QSEND(auth); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_CHOREO_ID); + QSEND(path); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_DATA); + fmt.reset(); + while(fmt.hasNext()) { + QSEND(fmt.next()); + } + + QSEND(TAG_END_REQUEST); + + return m_client.sendChoreoRequest(); + +SendFailed: + TEMBOO_TRACELN("FAIL"); + return FAILURE; + +} + +uint16_t TembooCoAPSession::getAuth(DataFormatter& fmt, const char* appKeyValue, const char* salt, char* result) const { + + // We need the length of the data for other things, and + // this method is a convenient place to calculate it. + uint16_t len = 0; + + HMAC hmac; + + //combine the salt and the key and give it to the HMAC calculator + size_t keyLength = strlen(appKeyValue) + strlen(salt); + char key[keyLength + 1]; + strcpy(key, salt); + strcat(key, appKeyValue); + hmac.init((uint8_t*)key, keyLength); + + // process the data a block at a time. + uint8_t buffer[HMAC_BLOCK_SIZE_BYTES]; + int blockCount = 0; + fmt.reset(); + while(fmt.hasNext()) { + uint8_t c = fmt.next(); + len++; + buffer[blockCount++] = c; + if (blockCount == HMAC_BLOCK_SIZE_BYTES) { + hmac.process(buffer, blockCount); + blockCount = 0; + } + } + hmac.process(buffer, blockCount); + + // Finalize the HMAC calculation and store the (ASCII HEX) value in *result. + hmac.finishHex(result); + + // Return the number of characters processed. + return len; +} + + +bool TembooCoAPSession::qsend(const char* s) { + char c = *s++; + bool rc = true; + while(c != '\0' && rc) { + rc = qsend(c); + c = *s++; + } + return rc; +} + + +bool TembooCoAPSession::qsend(char c) { + // Never send a nul character. + if ('\0' != c) { + if (TembooCoAPClient::NO_ERROR == m_client.write(c)) { + return true; + } + } + return false; +} + diff --git a/src/utility/TembooCoAPSession.h b/src/utility/TembooCoAPSession.h new file mode 100644 index 0000000..f34ca6c --- /dev/null +++ b/src/utility/TembooCoAPSession.h @@ -0,0 +1,111 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef TEMBOOCOAPSESSIONCLASS_H_ +#define TEMBOOCOAPSESSIONCLASS_H_ + +#include +#include +#include "TembooCoAPEdgeDevice.h" +#include "TembooGlobal.h" + +#ifndef TEMBOO_SEND_QUEUE_SIZE +#define TEMBOO_SEND_QUEUE_SIZE (1000) +#endif + +class ChoreoInputSet; +class ChoreoOutputSet; +class ChoreoPreset; +class DataFormatter; + +class TembooCoAPSession { + public: + + //TembooSession constructor + //client: REQUIRED TembooCoAPClient client object. + TembooCoAPSession(TembooCoAPClient& client); + + //executeChoreo sends a choreo execution request to the Temboo system. + // Does not wait for a response (that's a job for whoever owns the Client.) + //accountName: the name of the user's account at Temboo. + //appKeyName: the name of an application key in the user's account to use + // for this execution (analogous to a user name). + //appKeyValue: the value of the application key named in appKeyName. + // Used to authenticate the user (analogous to a password) + //path: The full path to the choreo to be executed (relative to the root of the + // user's account.) + //inputSet: the set of inputs needed by the choreo. + // May be an empty ChoreoInputSet. + //outputSet: the set of output filters to be applied to the choreo results. + // May be an empty ChoreoOutputSet + //preset: the ChoreoPreset to be used with the choreo execution. + // May be an empty ChoreoPreset. + int executeChoreo( + uint16_t requestId, + const char* accountName, + const char* appKeyName, + const char* appKeyValue, + const char* path, + const ChoreoInputSet& inputSet, + const ChoreoOutputSet& outputSet, + const ChoreoPreset& preset); + + // setTime sets the current time in Unix timestamp format. Needed for execution request authentication. + // NOTE: This method is usually called by TembooChoreo.run() with the current time returned by + // an error response from the Temboo system, thus automatically setting the time. However, it + // MAY be called from user code if the particular board has a way of determining the current + // time in the proper format. + // currentTime: the number of seconds since 1970-01-01 00:00:00 UTC. + static void setTime(unsigned long currentTime); + + //getTime returns the current time in Unix timestamp format (seconds since 1970-01-01 00:00:00 UTC). + // Only valid after setTime has been called. + static unsigned long getTime(); + + enum Error { + SUCCESS = 0, + FAILURE + }; + + private: + TembooCoAPClient& m_client; + static unsigned long s_timeOffset; + + // calculate the authentication code value of the formatted request body + // using the salted application key value as the key. + // Returns the number of characters processed (i.e. the length of the request body) + uint16_t getAuth(DataFormatter& fmt, const char* appKeyValue, const char* salt, char* hexAuth) const; + + + // queue an entire nul-terminated char array + // from RAM one byte at a time. + // returns true on success, false on failure + bool qsend(const char*); + + // queue a single character to be sent. + // returns true on success, false on failure + bool qsend(char); + +}; + +#endif + diff --git a/src/utility/TembooMQTTIPStack.h b/src/utility/TembooMQTTIPStack.h new file mode 100644 index 0000000..8282341 --- /dev/null +++ b/src/utility/TembooMQTTIPStack.h @@ -0,0 +1,96 @@ +/* + ############################################################################### + # + # Temboo MQTT edge device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef TEMBOOBASEIPSTACK_H_ +#define TEMBOOBASEIPSTACK_H_ + +#include + +static const int WRITE_CHUNK_SIZE = 64; + +class TembooMQTTIPStack +{ +public: + TembooMQTTIPStack(Client& client) : m_client(client) { + } + + int connect(const char* hostname, int port) { + return m_client.connect(hostname, port); + } + + bool isConnected() { + return m_client.connected(); + } + + int disconnect() { + m_client.stop(); + return 0; + } + + int read(unsigned char* buffer, int len, int timeoutMillis) { + + m_client.setTimeout(timeoutMillis); + size_t count = m_client.readBytes((char*)buffer, len); + + // Not sure this is a totally good idea. + // (Stream defaults to 1000 mS timeouts.) + m_client.setTimeout(1000); + return count; + } + + int write(unsigned char* buffer, int len, int timeout) { + + // It's possible for write to get called with a 0 timeout, + // in which case we want to make at least one attempt to send + // the data. + timeout = timeout == 0 ? 1 : timeout; + m_client.setTimeout(timeout); + + // Arduino's WiFiClient::write can accept only a limited + // number of characters on each call, so we must make sure + // we pass less than this limit. Note that this isn't necessary + // for Arduino's EthernetClient. However, instead of trying to + // differentiate between clients, we do it for both. + // It SHOULDN'T impact performance in any significant way, but + // if you're using a client that doesn't have this limitation and + // the idea bothers you, feel free to eliminate the min() call or + // else set WRITE_CHUNK_SIZE to a large number. + + // Note that the caller (in this case, MQTT::Client::sendPacket) + // is responsible for calling this method repeatedly until all + // bytes have been written. + + size_t count = m_client.write(buffer, min(len, WRITE_CHUNK_SIZE)); + + // Not sure this is a totally good idea. + // (Stream defaults to 1000 mS timeouts.) + m_client.setTimeout(1000); + return count; + } + + +protected: + Client& m_client; +}; + +#endif + diff --git a/src/utility/TembooMQTTSession.cpp b/src/utility/TembooMQTTSession.cpp new file mode 100644 index 0000000..50e59f9 --- /dev/null +++ b/src/utility/TembooMQTTSession.cpp @@ -0,0 +1,189 @@ +/* + ############################################################################### + # + # Temboo MQTT edge device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#include +#include +#include +#include "TembooMQTTSession.h" +#include "tmbhmac.h" +#include "DataFormatter.h" +#include "TembooTags.h" + +#define QSEND(x) if(!qsend((x))) { goto SendFailed;} + +unsigned long TembooMQTTSession::s_timeOffset = 0; + +TembooMQTTSession::TembooMQTTSession(TembooMQTTClient& client) : m_client(client) { +} + + +void TembooMQTTSession::setTime(unsigned long currentTime) { + s_timeOffset = currentTime - (millis()/1000); +} + + +unsigned long TembooMQTTSession::getTime() { + return s_timeOffset + (millis()/1000); +} + + + +int TembooMQTTSession::executeChoreo( + uint16_t requestId, + const char* accountName, + const char* appKeyName, + const char* appKeyValue, + const char* path, + const ChoreoInputSet& inputSet, + const ChoreoOutputSet& outputSet, + const ChoreoPreset& preset) { + + DataFormatter fmt(&inputSet, &outputSet, &preset); + char auth[HMAC_HEX_SIZE_BYTES + 1]; + char timeStr[11]; + char requestIdStr[5]; + uint16toa(requestId, requestIdStr); + + // We use the current time-of-day as salt on the app key. + // We keep track of time-of-day by getting the current time + // from the server and applying an offset (the length of time + // we've been running.) + uint32toa((uint32_t)TembooMQTTSession::getTime(), timeStr); + + getAuth(fmt, appKeyValue, timeStr, auth); + + if (m_client.isConnected()) { + + m_sendQueueDepth = 0; + + QSEND(TAG_REQUEST_ID); + QSEND(requestIdStr); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_ACCOUNT_NAME); + QSEND(accountName); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_APP_KEY_NAME); + QSEND(appKeyName); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_TIME); + QSEND(timeStr); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_AUTH); + QSEND(auth); + + QSEND(TAG_ELEMENT_SEPARATOR); + + QSEND(TAG_CHOREO_ID); + QSEND(path); + + QSEND(TAG_ELEMENT_SEPARATOR); + + // Format and send the body of the request + QSEND(TAG_DATA); + fmt.reset(); + while(fmt.hasNext()) { + QSEND(fmt.next()); + } + + QSEND(TAG_END_REQUEST); + + return m_client.sendChoreoRequest(this->m_sendQueue, this->m_sendQueueDepth); + +SendFailed: + TEMBOO_TRACELN("FAIL"); + return TEMBOO_ERROR_FAILURE; + + } else { + TEMBOO_TRACELN("FAIL"); + return TEMBOO_ERROR_FAILURE; + } +} + +uint16_t TembooMQTTSession::getAuth(DataFormatter& fmt, const char* appKeyValue, const char* salt, char* result) const { + + // We need the length of the data for other things, and + // this method is a convenient place to calculate it. + uint16_t len = 0; + + HMAC hmac; + + //combine the salt and the key and give it to the HMAC calculator + size_t keyLength = strlen(appKeyValue) + strlen(salt); + char key[keyLength + 1]; + strcpy(key, salt); + strcat(key, appKeyValue); + hmac.init((uint8_t*)key, keyLength); + + // process the data a block at a time. + uint8_t buffer[HMAC_BLOCK_SIZE_BYTES]; + int blockCount = 0; + fmt.reset(); + while(fmt.hasNext()) { + uint8_t c = fmt.next(); + len++; + buffer[blockCount++] = c; + if (blockCount == HMAC_BLOCK_SIZE_BYTES) { + hmac.process(buffer, blockCount); + blockCount = 0; + } + } + hmac.process(buffer, blockCount); + + // Finalize the HMAC calculation and store the (ASCII HEX) value in *result. + hmac.finishHex(result); + + // Return the number of characters processed. + return len; +} + + +bool TembooMQTTSession::qsend(const char* s) { + char c = *s++; + bool rc = true; + while(c != '\0' && rc) { + rc = qsend(c); + c = *s++; + } + return rc; +} + + +bool TembooMQTTSession::qsend(char c) { + if (m_sendQueueDepth >= TEMBOO_SEND_QUEUE_SIZE) { + return false; + } + + // Never send a nul character. + if ('\0' != c) { + m_sendQueue[m_sendQueueDepth++] = c; + } + return true; +} + diff --git a/src/utility/TembooMQTTSession.h b/src/utility/TembooMQTTSession.h new file mode 100644 index 0000000..87b2fb7 --- /dev/null +++ b/src/utility/TembooMQTTSession.h @@ -0,0 +1,108 @@ +/* + ############################################################################### + # + # Temboo MQTT edge device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef TEMBOOMQTTSESSIONCLASS_H_ +#define TEMBOOMQTTSESSIONCLASS_H_ + +#include +#include +#include "TembooMQTTEdgeDevice.h" +#include "TembooGlobal.h" + +#ifndef TEMBOO_SEND_QUEUE_SIZE +#define TEMBOO_SEND_QUEUE_SIZE (1000) +#endif + +class ChoreoInputSet; +class ChoreoOutputSet; +class ChoreoPreset; +class DataFormatter; + +class TembooMQTTSession { + public: + + //TembooMQTTSession constructor + //client: REQUIRED TembooMQTTClient client object. + TembooMQTTSession(TembooMQTTClient& client); + + //executeChoreo sends a choreo execution request to the Temboo system. + // Does not wait for a response (that's a job for whoever owns the Client.) + //accountName: the name of the user's account at Temboo. + //appKeyName: the name of an application key in the user's account to use + // for this execution (analogous to a user name). + //appKeyValue: the value of the application key named in appKeyName. + // Used to authenticate the user (analogous to a password) + //path: The full path to the choreo to be executed (relative to the root of the + // user's account.) + //inputSet: the set of inputs needed by the choreo. + // May be an empty ChoreoInputSet. + //outputSet: the set of output filters to be applied to the choreo results. + // May be an empty ChoreoOutputSet + //preset: the ChoreoPreset to be used with the choreo execution. + // May be an empty ChoreoPreset. + int executeChoreo( + uint16_t requestId, + const char* accountName, + const char* appKeyName, + const char* appKeyValue, + const char* path, + const ChoreoInputSet& inputSet, + const ChoreoOutputSet& outputSet, + const ChoreoPreset& preset); + + // setTime sets the current time in Unix timestamp format. Needed for execution request authentication. + // NOTE: This method is usually called by TembooChoreo.run() with the current time returned by + // an error response from the Temboo system, thus automatically setting the time. However, it + // MAY be called from user code if the particular board has a way of determining the current + // time in the proper format. + // currentTime: the number of seconds since 1970-01-01 00:00:00 UTC. + static void setTime(unsigned long currentTime); + + //getTime returns the current time in Unix timestamp format (seconds since 1970-01-01 00:00:00 UTC). + // Only valid after setTime has been called. + static unsigned long getTime(); + + private: + TembooMQTTClient& m_client; + size_t m_sendQueueDepth; + char m_sendQueue[TEMBOO_SEND_QUEUE_SIZE]; + static unsigned long s_timeOffset; + + // calculate the authentication code value of the formatted request body + // using the salted application key value as the key. + // Returns the number of characters processed (i.e. the length of the request body) + uint16_t getAuth(DataFormatter& fmt, const char* appKeyValue, const char* salt, char* hexAuth) const; + + + // queue an entire nul-terminated char array + // from RAM one byte at a time. + // returns true on success, false on failure + bool qsend(const char*); + + // queue a single character to be sent. + // returns true on success, false on failure + bool qsend(char); + +}; + +#endif + diff --git a/src/utility/TembooSession.cpp b/src/utility/TembooSession.cpp index 70dbb9e..544f417 100644 --- a/src/utility/TembooSession.cpp +++ b/src/utility/TembooSession.cpp @@ -41,6 +41,7 @@ static const char HEADER_AUTH[] PROGMEM = "x-temboo-authentication: " static const char HEADER_CONTENT_TYPE[] PROGMEM = "Content-Type: text/plain"; static const char TEMBOO_DOMAIN[] PROGMEM = ".temboolive.com"; static const char SDK_ID[] PROGMEM = "?source_id=arduinoSDK1"; +static const char HTTP[] PROGMEM = "http://"; unsigned long TembooSession::s_timeOffset = 0; @@ -94,40 +95,35 @@ int TembooSession::executeChoreo( // reserve space for the "host" string sufficient to hold either the // (dotted-quad) IP address + port, or the default .temboolive.com // host string. - int hostLen = (m_addr == INADDR_NONE ? (strlen_P(TEMBOO_DOMAIN) + strlen(accountName) + 1):21); + int hostLen = strlen_P(TEMBOO_DOMAIN) + strlen(accountName) + 1; char host[hostLen]; + + // Construct the "host" string from the account name and the temboo domain name. - // If no explicit IP address was specified (the normal case), construct - // the "host" string from the account name and the temboo domain name. + strcpy(host, accountName); + strcat_P(host, TEMBOO_DOMAIN); + + bool useProxy = false; + if (m_addr == INADDR_NONE) { - strcpy(host, accountName); - strcat_P(host, TEMBOO_DOMAIN); TEMBOO_TRACELN(host); connected = m_client.connect(host, m_port); } else { - - // If an IP address was explicitly specified (presumably for testing purposes), - // convert it to a dotted-quad text string. - host[0] = '\0'; - for(int i = 0; i < 4; i++) { - uint16toa(m_addr[i], &host[strlen(host)]); - strcat(host, "."); - } - - // replace the last '.' with ':' - host[strlen(host)-1] = ':'; - - // append the port number - uint16toa(m_port, &host[strlen(host)]); TEMBOO_TRACELN(host); connected = m_client.connect(m_addr, m_port); + + useProxy = true; } if (connected) { TEMBOO_TRACELN("OK. req:"); qsendProgmem(POST); + if(useProxy) { + qsendProgmem(HTTP); + qsend(host); + } qsendProgmem(BASE_CHOREO_URI); qsend(path); qsendProgmem(SDK_ID); diff --git a/src/utility/TembooTags.h b/src/utility/TembooTags.h new file mode 100644 index 0000000..22cf2f5 --- /dev/null +++ b/src/utility/TembooTags.h @@ -0,0 +1,37 @@ +/* +############################################################################### +# +# Temboo CoAP Edge Device library for Arduino +# +# Copyright (C) 2014, 2015 Temboo Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +# +############################################################################### +*/ + +#ifndef TEMBOOTAGS_H_ +#define TEMBOOTAGS_H_ + +#define TAG_REQUEST_ID ('R') +#define TAG_ACCOUNT_NAME ('N') +#define TAG_APP_KEY_NAME ('K') +#define TAG_TIME ('T') +#define TAG_AUTH ('A') +#define TAG_CHOREO_ID ('C') +#define TAG_DATA ('X') +#define TAG_END_REQUEST ('!') +#define TAG_ELEMENT_SEPARATOR ('|') +#define TAG_VALUE_SEPARATOR (':') + +#endif //TEMBOOTAGS_H_ diff --git a/src/utility/TembooTimer.h b/src/utility/TembooTimer.h new file mode 100644 index 0000000..f2b444f --- /dev/null +++ b/src/utility/TembooTimer.h @@ -0,0 +1,62 @@ +/* + ############################################################################### + # + # Temboo CoAP Edge Device library + # + # Copyright (C) 2015, Temboo Inc. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + # either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + # + ############################################################################### + */ + +#ifndef TEMBOOTIMER_H_ +#define TEMBOOTIMER_H_ + +class TembooTimer { + public: + TembooTimer() { + this->m_startTimeMillis = 0; + this->m_durationMillis = 0; + } + + TembooTimer(uint32_t m_durationMillis) { + this->start(m_durationMillis); + } + + bool expired() { + return leftMillis() == 0; + } + + void start(uint32_t m_durationMillis) { + this->m_startTimeMillis = getMillis(); + this->m_durationMillis = m_durationMillis; + } + + uint32_t leftMillis() { + uint32_t elapsedMillis = getMillis() - this->m_startTimeMillis; + return elapsedMillis < this->m_durationMillis ? (this->m_durationMillis - elapsedMillis) : 0; + } + + protected: + uint32_t getMillis() { + return millis(); + } + + uint32_t m_startTimeMillis; + uint32_t m_durationMillis; + +}; + + +#endif /* TEMBOOTIMER_H_ */