The Cayenne MQTT mbed Library provides functions to easily connect to the Cayenne IoT project builder.
Fork of Cayenne-MQTT-mbed by
Diff: src/CayenneUtils/CayenneUtils.c
- Revision:
- 0:09ef59d2d0f7
- Child:
- 6:82e142a864ad
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/CayenneUtils/CayenneUtils.c Fri Oct 07 17:21:45 2016 +0000 @@ -0,0 +1,537 @@ +/* +The MIT License(MIT) + +Cayenne MQTT Client Library +Copyright (c) 2016 myDevices + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files(the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions : +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include <stdlib.h> +#include <string.h> +#include "CayenneUtils.h" + +#ifdef DIGITAL_AND_ANALOG_SUPPORT + #ifdef PARSE_INFO_PAYLOADS + #define PARSE_TOPICS_COUNT 13 + #else + #define PARSE_TOPICS_COUNT 6 + #endif +#else + #ifdef PARSE_INFO_PAYLOADS + #define PARSE_TOPICS_COUNT 7 + #else + #define PARSE_TOPICS_COUNT 2 + #endif +#endif + +#define THINGS_STRING CAYENNE_PSTR("/things/") + +typedef struct TopicChannel +{ + CayenneTopic topic; + unsigned int channel; +} TopicChannel; + +/** +* Build a specified topic string. +* @param[out] topic Returned topic string +* @param[in] length CayenneTopic buffer length +* @param[in] username Cayenne username +* @param[in] clientID Cayennne client ID +* @param[in] suffix The topic suffix +* @return CAYENNE_SUCCESS if topic string was created, error code otherwise +*/ +int buildTopic(char* topic, size_t length, const char* username, const char* clientID, const char* suffix) { + size_t topicLength = 0; + if (!topic || !username || !clientID || !suffix) + return CAYENNE_FAILURE; + topicLength = strlen(username) + strlen(clientID) + strlen(suffix) + 11; + if (topicLength > length) + return CAYENNE_BUFFER_OVERFLOW; + + topic[0] = '\0'; + strcat(topic, CAYENNE_VERSION); + strcat(topic, "/"); + strcat(topic, username); + CAYENNE_STRCAT(topic, THINGS_STRING); + strcat(topic, clientID); + strcat(topic, "/"); + strcat(topic, suffix); + return CAYENNE_SUCCESS; +} + +/** +* Build a specified topic suffix string. +* @param[out] suffix Returned suffix string +* @param[in] length Suffix buffer length +* @param[in] topic Cayenne topic +* @param[in] channel The topic channel, CAYENNE_NO_CHANNEL for none, CAYENNE_ALL_CHANNELS for all +* @return CAYENNE_SUCCESS if suffix string was created, error code otherwise +*/ +int buildSuffix(char* suffix, size_t length, const CayenneTopic topic, unsigned int channel) { + char* topicString = NULL; + if (!suffix) + return CAYENNE_FAILURE; + switch (topic) + { + case COMMAND_TOPIC: + topicString = COMMAND_STRING; + break; + case CONFIG_TOPIC: + topicString = CONFIG_STRING; + break; + case DATA_TOPIC: + topicString = DATA_STRING; + break; + case RESPONSE_TOPIC: + topicString = RESPONSE_STRING; + break; + case SYS_MODEL_TOPIC: + topicString = SYS_MODEL_STRING; + break; + case SYS_VERSION_TOPIC: + topicString = SYS_VERSION_STRING; + break; + case SYS_CPU_MODEL_TOPIC: + topicString = SYS_CPU_MODEL_STRING; + break; + case SYS_CPU_SPEED_TOPIC: + topicString = SYS_CPU_SPEED_STRING; + break; +#ifdef DIGITAL_AND_ANALOG_SUPPORT + case DIGITAL_TOPIC: + topicString = DIGITAL_STRING; + break; + case DIGITAL_COMMAND_TOPIC: + topicString = DIGITAL_COMMAND_STRING; + break; + case DIGITAL_CONFIG_TOPIC: + topicString = DIGITAL_CONFIG_STRING; + break; + case ANALOG_TOPIC: + topicString = ANALOG_STRING; + break; + case ANALOG_COMMAND_TOPIC: + topicString = ANALOG_COMMAND_STRING; + break; + case ANALOG_CONFIG_TOPIC: + topicString = ANALOG_CONFIG_STRING; + break; +#endif + default: + return CAYENNE_FAILURE; + } + + if (!topicString) + return CAYENNE_FAILURE; + if (CAYENNE_STRLEN(topicString) >= length) + return CAYENNE_BUFFER_OVERFLOW; + + suffix[0] = '\0'; + CAYENNE_STRCAT(suffix, topicString); + if (channel != CAYENNE_NO_CHANNEL) { + strcat(suffix, "/"); + if (channel == CAYENNE_ALL_CHANNELS) { + strcat(suffix, "+"); + } + else { +#if defined(__AVR__) || defined (ARDUINO_ARCH_ARC32) + itoa(channel, &suffix[strlen(suffix)], 10); +#else + snprintf(&suffix[strlen(suffix)], length - strlen(suffix), "%u", channel); +#endif + } + } + return CAYENNE_SUCCESS; +} + +/** +* Check if topic matches. +* @param[in] filter Filter to check topic against +* @param[in] topicName CayenneTopic name +* @param[in] topicNameLen CayenneTopic name length +* return true if topic matches, false otherwise +*/ +int topicMatches(char* filter, char* topicName, unsigned int topicNameLen) +{ + char* curf = filter; + char* curn = topicName; + char* curn_end = topicName + topicNameLen; + + 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')); +} + +/** +* Get the count of values in a message. +* @param[out] count Returned number of values found in message +* @param[in] payload Payload string, must be null terminated +* @param[in] token Character token for splitting "unit=value" payloads, 0 to just parse first comma delimited value +* @return CAYENNE_SUCCESS if value count succeeded, error code otherwise +*/ +int getValueCount(size_t* count, char* payload, char token) { + char* index = payload; + size_t unitCount = 0; + size_t valueCount = 0; + int countingValues = 0; + + if (token == 0) { + //Currently there can only be one value in payload if this isn't a "unit=value" payload. + *count = 1; + return CAYENNE_SUCCESS; + } + + *count = 0; + while (*index && index != '\0') { + if ((*index == ',') || (*index == token)) { + if (*index == ',') { + if (countingValues) { + valueCount++; + } + else { + unitCount++; + } + } + else if (*index == token) { + countingValues = 1; + valueCount++; + } + } + index++; + } + + if (countingValues) { + if ((valueCount != unitCount) && !(unitCount == 0 && valueCount == 1)) { + return CAYENNE_FAILURE; + } + } + else { + valueCount = 1; + } + *count = valueCount; + return CAYENNE_SUCCESS; +} + +/** +* Parse a null terminated payload string in place. This may modify the payload string. +* @param[out] values Returned payload data unit & value array +* @param[in,out] valuesSize Size of values array, returns the count of values in the array +* @param[out] type Returned type, NULL if there is none +* @param[in] payload Payload string, must be null terminated +* @param[in] token Character token for splitting "unit=value" payloads, 0 to just parse first comma delimited value +* @return CAYENNE_SUCCESS if value and id were parsed, error code otherwise +*/ +int parsePayload(CayenneValuePair* values, size_t* valuesSize, const char** type, char* payload, char token) { + char* index = payload; + size_t count = 0; + int parsingValues = 0; + size_t valueIndex = 0; +#ifdef PARSE_INFO_PAYLOADS + int result = getValueCount(&count, payload, token); + if (result != CAYENNE_SUCCESS) { + *valuesSize = 0; + return result; + } +#else + count = 1; +#endif + + if(token == 0) + parsingValues = 1; //Don't need to parse units if there is no unit/value separator + + values[0].value = NULL; + values[0].unit = NULL; + *type = NULL; + while (*index && index != '\0') { + if ((*index == ',') || (*index == token)) { + if (*index == ',') { + *type = payload; + if (valueIndex < *valuesSize) { + if (parsingValues) { + values[valueIndex].value = index + 1; + } + else { + values[valueIndex].unit = index + 1; + } + } + *index = '\0'; + valueIndex++; + if (token == 0) + break; + } + else if (*index == token && !parsingValues) { + parsingValues = 1; + valueIndex = 0; + *type = payload; + values[valueIndex].value = index + 1; + *index = '\0'; + valueIndex++; + if (count == valueIndex) + break; + } + } + index++; + }; + *valuesSize = count; + return CAYENNE_SUCCESS; +} + +/** +* Build a specified topic string. +* @param[out] topicName Returned topic string +* @param[in] length CayenneTopic buffer length +* @param[in] username Cayenne username +* @param[in] clientID Cayennne client ID +* @param[in] topic Cayenne topic +* @param[in] channel The topic channel, CAYENNE_NO_CHANNEL for none, CAYENNE_ALL_CHANNELS for all +* @return CAYENNE_SUCCESS if topic string was created, error code otherwise +*/ +int CayenneBuildTopic(char* topicName, size_t length, const char* username, const char* clientID, CayenneTopic topic, unsigned int channel) { + char channelSuffix[20] = {0}; + int result = buildSuffix(channelSuffix, sizeof(channelSuffix), topic, channel); + if (result != CAYENNE_SUCCESS) + return result; + return buildTopic(topicName, length, username, clientID, channelSuffix); +} + +/** +* Build a specified data payload. +* @param[out] payload Returned payload +* @param[in,out] length Payload buffer length +* @param[in] type Optional type to use for type,unit=value payload, can be NULL +* @param[in] values Unit/value array +* @param[in] valueCount Number of values +* @return CAYENNE_SUCCESS if topic string was created, error code otherwise +*/ +int CayenneBuildDataPayload(char* payload, size_t* length, const char* type, const CayenneValuePair* values, size_t valueCount) { + int i; + size_t payloadLength = 0; + for (i = 0; i < valueCount; ++i) { + payloadLength += values[i].unit ? strlen(values[i].unit) + 1 : 0; + payloadLength += values[i].value ? strlen(values[i].value) + 1 : 0; + } + payloadLength += type ? strlen(type) + 1 : 0; + //If payload can't fit the payload plus a terminating null byte return. + if (payloadLength > *length) { + return CAYENNE_BUFFER_OVERFLOW; + } + + payload[0] = '\0'; + if (type) { + strcat(payload, type); + } + for (i = 0; i < valueCount && values[i].unit; ++i) { + if(payload[0] != '\0') + strcat(payload, ","); + strcat(payload, values[i].unit); + } + if (payload[0] != '\0' && valueCount > 0 && values[0].value) + strcat(payload, "="); + for (i = 0; i < valueCount && values[i].value; ++i) { + strcat(payload, values[i].value); + if(i + 1 < valueCount) + strcat(payload, ","); + } + *length = --payloadLength; //Subtract terminating null + return CAYENNE_SUCCESS; +} + +/** +* Build a specified response payload. +* @param[out] payload Returned payload +* @param[in,out] length Payload buffer length +* @param[in] id ID of message the response is for +* @param[in] error Optional error message, NULL for success +* @return CAYENNE_SUCCESS if payload string was created, error code otherwise +*/ +int CayenneBuildResponsePayload(char* payload, size_t* length, const char* id, const char* error) { + CayenneValuePair values[1]; + values[0].unit = id; + values[0].value = error; + if (error) { + return CayenneBuildDataPayload(payload, length, "error", values, 1); + } + return CayenneBuildDataPayload(payload, length, "ok", values, 1); +} + +/** +* Parse a topic string in place. This may modify the topic string. +* @param[out] topic Returned Cayenne topic +* @param[out] channel Returned channel, CAYENNE_NO_CHANNEL if there is none +* @param[out] clientID Returned client ID +* @param[in] username Cayenne username +* @param[in] topicName Topic name string +* @param[in] length Topic name string length +* @return CAYENNE_SUCCESS if topic was parsed, error code otherwise +*/ +int CayenneParseTopic(CayenneTopic* topic, unsigned int* channel, const char** clientID, const char* username, char* topicName, unsigned int length) { + char* index = NULL; + int i = 0; + TopicChannel parseTopics[PARSE_TOPICS_COUNT] = { { COMMAND_TOPIC, CAYENNE_ALL_CHANNELS },{ CONFIG_TOPIC, CAYENNE_ALL_CHANNELS }, +#ifdef DIGITAL_AND_ANALOG_SUPPORT + { ANALOG_COMMAND_TOPIC, CAYENNE_ALL_CHANNELS },{ ANALOG_CONFIG_TOPIC, CAYENNE_ALL_CHANNELS },{ DIGITAL_COMMAND_TOPIC, CAYENNE_ALL_CHANNELS },{ DIGITAL_CONFIG_TOPIC, CAYENNE_ALL_CHANNELS }, +#ifdef PARSE_INFO_PAYLOADS + { DIGITAL_TOPIC, CAYENNE_ALL_CHANNELS },{ ANALOG_TOPIC, CAYENNE_ALL_CHANNELS }, +#endif +#endif +#ifdef PARSE_INFO_PAYLOADS + { DATA_TOPIC, CAYENNE_ALL_CHANNELS },{ SYS_MODEL_TOPIC, CAYENNE_NO_CHANNEL },{ SYS_VERSION_TOPIC, CAYENNE_NO_CHANNEL },{ SYS_CPU_MODEL_TOPIC, CAYENNE_NO_CHANNEL },{ SYS_CPU_SPEED_TOPIC, CAYENNE_NO_CHANNEL } +#endif + }; + + if (!topic || !channel || !topicName) + { + return CAYENNE_FAILURE; + } + if (length > CAYENNE_MAX_MESSAGE_SIZE) + { + return CAYENNE_BUFFER_OVERFLOW; + } + if (strncmp(CAYENNE_VERSION, topicName, strlen(CAYENNE_VERSION)) != 0) + return CAYENNE_FAILURE; + index = topicName + strlen(CAYENNE_VERSION) + 1; + if (strncmp(username, index, strlen(username)) != 0) + return CAYENNE_FAILURE; + index += strlen(username); + if (CAYENNE_STRNCMP(index, THINGS_STRING, CAYENNE_STRLEN(THINGS_STRING)) != 0) + return CAYENNE_FAILURE; + index += CAYENNE_STRLEN(THINGS_STRING); + char* deviceIDEnd = strchr(index, '/'); + if (!deviceIDEnd) + return CAYENNE_FAILURE; + *clientID = index; + *deviceIDEnd = '\0'; + + index = deviceIDEnd + 1; + *topic = UNDEFINED_TOPIC; + *channel = CAYENNE_NO_CHANNEL; + length -= (index - topicName); + for (i = 0; i < PARSE_TOPICS_COUNT; ++i) + { + char channelSuffix[32] = { 0 }; + if (buildSuffix(channelSuffix, sizeof(channelSuffix), parseTopics[i].topic, parseTopics[i].channel) == CAYENNE_SUCCESS && topicMatches(channelSuffix, index, length)) { + *topic = parseTopics[i].topic; + break; + } + } + + if (*topic == UNDEFINED_TOPIC) + return CAYENNE_FAILURE; + + if (parseTopics[i].channel != CAYENNE_NO_CHANNEL) { + if (length == 0 || length > 31) + return CAYENNE_FAILURE; + char* channelIndex = NULL; + char buffer[32] = { 0 }; + memcpy(buffer, index, length); + buffer[length] = '\0'; + channelIndex = strrchr(buffer, '/'); + if (channelIndex && ++channelIndex) { + char* indexEnd = NULL; + unsigned int channelNumber = strtoul(channelIndex, &indexEnd, 10); + if (indexEnd && *indexEnd == '\0') { + if (((channelNumber != 0) && (*channelIndex != '0')) || ((channelNumber == 0) && (*channelIndex == '0') && (channelIndex + 1 == indexEnd))) { + *channel = channelNumber; + } + } + } + } + + return CAYENNE_SUCCESS; +} + +/** +* Parse a null terminated payload in place. This may modify the payload string. +* @param[out] values Returned payload data unit & value array +* @param[in,out] valuesSize Size of values array, returns the count of values in the array +* @param[out] type Returned type, NULL if there is none +* @param[out] id Returned message id, empty string if there is none +* @param[in] topic Cayenne topic +* @param[in] payload Payload string, must be null terminated. +* @return CAYENNE_SUCCESS if topic string was created, error code otherwise +*/ +int CayenneParsePayload(CayenneValuePair* values, size_t* valuesSize, const char** type, const char** id, CayenneTopic topic, char* payload) { + int i; + if (!payload || !valuesSize || *valuesSize == 0) + return CAYENNE_FAILURE; + + *type = NULL; + *id = NULL; + for(i = 0; i < *valuesSize; i++) { + values[i].unit = NULL; + values[i].value = NULL; + } + switch (topic) + { +#ifdef PARSE_INFO_PAYLOADS + case DATA_TOPIC: + parsePayload(values, valuesSize, type, payload, '='); + if (!values[0].value) + return CAYENNE_FAILURE; + break; +#endif +#ifdef DIGITAL_AND_ANALOG_SUPPORT +#ifdef PARSE_INFO_PAYLOADS + case ANALOG_TOPIC: + parsePayload(values, valuesSize, type, payload, 0); + values[0].unit = values[0].value; //Use unit to store resolution + values[0].value = *type; + *type = NULL; + if (!values[0].value) + return CAYENNE_FAILURE; + break; +#endif + case DIGITAL_COMMAND_TOPIC: + case ANALOG_COMMAND_TOPIC: +#endif + case COMMAND_TOPIC: + parsePayload(values, valuesSize, type, payload, 0); + *id = *type; + *type = NULL; + if (!values[0].value) + return CAYENNE_FAILURE; + break; + default: + break; + } + + if (!values[0].value) { + values[0].value = payload; + values[0].unit = NULL; + *type = NULL; + *id = NULL; + *valuesSize = 1; + } + + return CAYENNE_SUCCESS; +} + + +