diff --git a/Release/DotNet/WoopsaDemoServer.exe b/Release/DotNet/WoopsaDemoServer.exe index 5f51c52..515d2c3 100644 Binary files a/Release/DotNet/WoopsaDemoServer.exe and b/Release/DotNet/WoopsaDemoServer.exe differ diff --git a/Release/Embedded/woopsa-config.h b/Release/Embedded/woopsa-config.h new file mode 100644 index 0000000..2105643 --- /dev/null +++ b/Release/Embedded/woopsa-config.h @@ -0,0 +1,47 @@ +#ifndef __WOOPSA_CONFIG_H_ +#define __WOOPSA_CONFIG_H_ + +#include +#include +#include +#include + + +// Woopsa uses these internally, allowing you to use Woopsa in a +// thread-safe manner, or disabling interrupts. Just fill this in +// if needed with whatever locking mechanism your environment has. +#define WOOPSA_LOCK // disable interrupts +#define WOOPSA_UNLOCK // enable interrupts + +// If you are on a system with very low memory, you can reduce the +// buffer size that the Woopsa server uses internally. +// This value changes the maximum length of URLs you can parse. +// You should not go under 128 bytes to be 100% safe. +#define WOOPSA_BUFFER_SIZE 256 + +// Thanks microsoft for not supporting snprintf! +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define snprintf _snprintf +#endif + +#define WOOPSA_ENABLE_STRINGS +#define WOOPSA_ENABLE_METHODS + +// 99% of systems will have the standard C library but in case +// you end up in the 1%, you can always re-define these functions +// to work for you. +#define WOOPSA_INTEGER_TO_PADDED_STRING(value, string, padding) sprintf(string, "%" #padding "d", value) +#define WOOPSA_INTEGER_TO_STRING(value, string, max_length) snprintf(string, max_length, "%d", value) +#define WOOPSA_REAL_TO_STRING(value, string, max_length) snprintf(string, max_length, "%f", value) + +#define WOOPSA_STRING_TO_INTEGER(value, string) (value = atoi(string)) +#define WOOPSA_STRING_TO_FLOAT(value, string) (value = atof(string)) + +#define WOOPSA_STRING_POSITION(haystack, needle) strstr(haystack, needle) +#define WOOPSA_STRING_EQUAL(string1, string2) (strcmp(string1, string2) == 0) +#define WOOPSA_STRING_LENGTH(string) strlen(string) +#define WOOPSA_CHAR_TO_LOWER(character) tolower(character) +#define WOOPSA_STRING_COPY(destination, source) strcpy(destination, source) +#define WOOPSA_STRING_N_COPY(destination, source, n) strncpy(destination, source, n) + +#endif \ No newline at end of file diff --git a/Release/Embedded/woopsa-server.c b/Release/Embedded/woopsa-server.c new file mode 100644 index 0000000..7310560 --- /dev/null +++ b/Release/Embedded/woopsa-server.c @@ -0,0 +1,659 @@ +#include "woopsa-server.h" +#include +#include + + + +// Woopsa constants +#define VERB_META "meta" +#define VERB_READ "read" +#define VERB_WRITE "write" +#define VERB_INVOKE "invoke" + +#define TYPE_STRING_NULL "Null" +#define TYPE_STRING_LOGICAL "Logical" +#define TYPE_STRING_INTEGER "Integer" +#define TYPE_STRING_REAL "Real" +#define TYPE_STRING_DATE_TIME "DateTime" +#define TYPE_STRING_TIME_SPAN "TimeSpan" +#define TYPE_STRING_TEXT "Text" +#define TYPE_STRING_LINK "Link" +#define TYPE_STRING_RESOURCE_URL "ResourceUrl" + +typedef struct { + WoopsaType type; + WoopsaChar8* string; +} TypesDictionaryEntry; + +TypesDictionaryEntry Types[] = { + { WOOPSA_TYPE_NULL, TYPE_STRING_NULL }, + { WOOPSA_TYPE_LOGICAL, TYPE_STRING_LOGICAL }, + { WOOPSA_TYPE_INTEGER, TYPE_STRING_INTEGER }, + { WOOPSA_TYPE_REAL, TYPE_STRING_REAL }, + { WOOPSA_TYPE_TIME_SPAN, TYPE_STRING_TIME_SPAN }, +#ifdef WOOPSA_ENABLE_STRINGS + { WOOPSA_TYPE_DATE_TIME, TYPE_STRING_DATE_TIME }, + { WOOPSA_TYPE_TEXT, TYPE_STRING_TEXT }, + { WOOPSA_TYPE_LINK, TYPE_STRING_LINK }, + { WOOPSA_TYPE_RESOURCE_URL, TYPE_STRING_RESOURCE_URL } +#endif +}; + +// HTTP constants +#define HTTP_METHOD_GET "GET" +#define HTTP_METHOD_POST "POST" +#define HTTP_VERSION_STRING "HTTP/1.1" + +#define HTTP_CODE_BAD_REQUEST "400" +#define HTTP_TEXT_BAD_REQUEST "Bad request" + +#define HTTP_CODE_NOT_FOUND "404" +#define HTTP_TEXT_NOT_FOUND "Not found" + +#define HTTP_CODE_INTERNAL_ERROR "500" +#define HTTP_TEXT_INTERNAL_ERROR "Internal server error" + +#define HTTP_CODE_NOT_IMPLEMENTED "501" +#define HTTP_TEXT_METHOD_NOT_IMPLEMENTED "%s method not implemented" + +#define HTTP_CODE_OK "200" +#define HTTP_TEXT_OK "OK" + +#define HEADER_SEPARATOR "\r\n" +#define HEADER_VALUE_SEPARATOR ":" +#define HEADER_CONTENT_LENGTH "content-length" +#define HEADER_CONTENT_LENGTH_SPACE " " +#define HEADER_CONTENT_LENGTH_PADDING 8 +#define HEADER_CONTENT_TYPE "Content-Type: " +#define EXTRA_HEADERS "Access-Control-Allow-Origin: *" HEADER_SEPARATOR "Connection: close" HEADER_SEPARATOR +#define CONTENT_TYPE_JSON "application/json" +#define CONTENT_TYPE_HTML "text/html" + +// POST constants +#define POST_VALUE_KEY "value" +#define URLENCODE_KEY_SEPARATOR '&' +#define URLENCODE_VALUE_SEPARATOR '=' +#define URLENCODE_VALUE_ENCODER '%' + +// Serialization constants +#define JSON_VALUE_VALUE "{\"Value\":" +#define JSON_VALUE_TYPE ",\"Type\":\"" +#define JSON_VALUE_END "\"}" +#define JSON_META_PROPERTIES "{\"Name\":\"Root\",\"Properties\":" +#define JSON_META_METHODS ",\"Methods\":" +#define JSON_META_END ",\"Items\":[]}" +#define JSON_PROPERTY_NAME "{\"Name\":\"" +#define JSON_PROPERTY_TYPE "\",\"Type\":\"" +#define JSON_PROPERTY_READONLY "\",\"ReadOnly\":" +#define JSON_PROPERTY_END "}" +#define JSON_METHOD_NAME "{\"Name\":\"" +#define JSON_METHOD_RETURN_TYPE "\",\"ReturnType\":\"" +#define JSON_METHOD_END "\",\"ArgumentInfos\":[]}" +#define JSON_ARRAY_START "[" +#define JSON_ARRAY_END "]" +#define JSON_ARRAY_DELIMITER "," +#define JSON_TRUE "true" +#define JSON_FALSE "false" +#define JSON_STRING_DELIMITER "\"" +#define JSON_STRING_DELIMITER_CHAR '"' +#define JSON_ESCAPE_CHAR '\\' + +// Memory-specific constants +#define MAX_NUMERICAL_VALUE_LENGTH 10 + +// Gets a Woopsa Property by name +// Returns a pointer to the WoopsaEntry, or null if not found +WoopsaEntry* GetPropertyByNameOrNull(WoopsaEntry entries[], WoopsaChar8 name[]) { + WoopsaUInt16 i = 0; + while (entries[i].name != NULL) { + if (WOOPSA_STRING_EQUAL(entries[i].name, name) && entries[i].isMethod == 0) + return &entries[i]; + i++; + } + return NULL; +} + +#ifdef WOOPSA_ENABLE_METHODS +// Gets a Woopsa Method by name +// Returns a pointer to the WoopsaEntry, or null if not found +WoopsaEntry* GetMethodByNameOrNull(WoopsaEntry entries[], WoopsaChar8 name[]) { + WoopsaUInt16 i = 0; + while (entries[i].name != NULL) { + if (WOOPSA_STRING_EQUAL(entries[i].name, name) && entries[i].isMethod == 1) + return &entries[i]; + i++; + } + return NULL; +} +#endif + +// Gets the type string for a given type +// Returns a pointer to a string or NULL if not found (why ??) +TypesDictionaryEntry * GetTypeEntry(WoopsaType type) { + WoopsaUInt8 i; + for (i = 0; i < sizeof(Types) / sizeof(TypesDictionaryEntry); i++) + if (Types[i].type == type) + return &Types[i]; + return NULL; +} + +// Finds the string length of the first HTTP header found in searchString +// Also sets nextHeader to be the start of the next header +// Returns the length of the *current* header, or -1 if this header is empty +// (which means this is the double new-line at the end of HTTP request) +WoopsaInt16 NextHTTPHeader(const WoopsaChar8* searchString, const WoopsaChar8** nextHeader) { + WoopsaUInt16 i = 0, carriageFound = 0; + while (searchString[i] != '\0') { + if (carriageFound == 1 && searchString[i] == '\n') { + *nextHeader = searchString + i + 1; + return i - 1; + } else if (carriageFound == 1 && searchString[i] != '\n') + carriageFound = 0; + else if (searchString[i] == '\r') + carriageFound = 1; + i++; + } + return -1; +} + +// Finds the next key/value pair in a URLEncoded string +// and decodes the actual value. Note: keys are lowercased +// Returns the length of the *current* key/value pair, or -1 if none found +WoopsaInt16 NextURLDecodedValue(const WoopsaChar8* searchString, WoopsaChar8* key, WoopsaChar8* value) { + WoopsaUInt16 i = 0, inKey = 1, keyAt = 0, valueAt = 0; + WoopsaUInt8 specialCharAt = 0, inSpecialChar = 0; + WoopsaChar8 charBuff[2] = { 0, 0 }; + WoopsaChar8 specialChar = '\0', curChar = '\0'; + for (i = 0; searchString[i] != '\0' ; i++) { + if (searchString[i] == URLENCODE_VALUE_ENCODER){ + // We're entering a %-encoded value + inSpecialChar = 1; + continue; + } else if (inSpecialChar && specialCharAt < 2 ) { + // We're inside a %-encoded value, add to the buffer + curChar = WOOPSA_CHAR_TO_LOWER(searchString[i]); + if ((curChar >= '0' && curChar <= '9') || (curChar >= 'a' && curChar <= 'f')) + charBuff[specialCharAt++] = curChar; + else + charBuff[specialCharAt++] = '4'; // must be something.. just not null! + continue; + } else if (inSpecialChar && specialCharAt == 2) { + // We're out of the %-encoded value - convert it to a char + if (charBuff[0] >= 'a') + specialChar = (charBuff[0] - 'a' + 0xa) * 0x10; + else + specialChar = (charBuff[0] - '0') * 0x10; + if (charBuff[1] >= 'a') + specialChar += charBuff[1] - 'a' + 0xa; + else + specialChar += charBuff[1] - '0'; + if (inKey) + key[keyAt++] = specialChar; + else + value[valueAt++] = specialChar; + inSpecialChar = 0; + specialCharAt = 0; + } + curChar = searchString[i]; + if (curChar == URLENCODE_VALUE_SEPARATOR) { + inKey = 0; + continue; + } + if (curChar == URLENCODE_KEY_SEPARATOR) { + key[keyAt] = '\0'; + value[valueAt] = '\0'; + return i; + } + if (curChar == '+') + curChar = ' '; + if (inKey == 1) + key[keyAt++] = WOOPSA_CHAR_TO_LOWER(curChar); + else + value[valueAt++] = curChar; + } + if (keyAt > 0 || valueAt > 0) { + key[keyAt] = '\0'; + value[valueAt] = '\0'; + return i; + } else { + return -1; + } +} + + +// Appends source to destination, while keeping destination under num length +// Returns amount of characters actually appended +WoopsaUInt16 Append(WoopsaChar8 destination[], const WoopsaChar8 source[], WoopsaUInt16 num) { + WoopsaUInt16 i = 0, cnt = 0; + // go to end of destination + while (destination[i] != '\0') + i++; + //append characters one-by-one + while (source[cnt] != '\0' && i < num - 1) + destination[i++] = source[cnt++]; + destination[i] = '\0'; + return cnt; +} + +// Appends source to destination, while keeping destination under num length +// and escaping specified character +// Returns amount of characters actually appended +WoopsaUInt16 AppendEscape(WoopsaChar8 destination[], const WoopsaChar8 source[], WoopsaChar8 special, WoopsaChar8 escape, WoopsaUInt16 num) { + WoopsaUInt16 i = 0, cnt = 0, extras = 0; + // go to end of destination + while (destination[i] != '\0') + i++; + //append characters one-by-one + while (source[cnt] != '\0' && i < num - 1) { + if (source[cnt] == special) { + // TODO: Watch out for buffer overflow! + destination[i++] = escape; + destination[i++] = source[cnt++]; + extras++; + } else { + destination[i++] = source[cnt++]; + } + } + destination[i] = '\0'; + return cnt + extras; +} + +// Prepares an HTTP response in the specified outputBuffer +// Will send the specified HTTP status code and a status string +// Will also add the content +// This method will basically prepare the entire string that can be +// send out to the client, including all HTTP headers. +// Returns the length of the prepared string +WoopsaUInt16 PrepareResponse( + WoopsaChar8* outputBuffer, + const WoopsaUInt16 outputBufferLength, + const WoopsaChar8* httpStatusCode, + const WoopsaChar8* httpStatusStr, + WoopsaUInt16* contentLengthPosition, + const WoopsaChar8* contentType + ) { + WoopsaUInt16 size = 0; + outputBuffer[0] = '\0'; + // HTTP/1.1 + size = Append(outputBuffer, HTTP_VERSION_STRING " ", outputBufferLength); + // 200 OK + size += Append(outputBuffer, httpStatusCode, outputBufferLength); + size += Append(outputBuffer, " ", outputBufferLength); + size += Append(outputBuffer, httpStatusStr, outputBufferLength); + size += Append(outputBuffer, HEADER_SEPARATOR, outputBufferLength); + // Content type + size += Append(outputBuffer, HEADER_CONTENT_TYPE, outputBufferLength); + size += Append(outputBuffer, contentType, outputBufferLength); + size += Append(outputBuffer, HEADER_SEPARATOR, outputBufferLength); + // Extra headers + size += Append(outputBuffer, EXTRA_HEADERS, outputBufferLength); + // Content-Length: + size += Append(outputBuffer, HEADER_CONTENT_LENGTH HEADER_VALUE_SEPARATOR, outputBufferLength); + *contentLengthPosition = size; + // We leave a few spaces for the Content-Length + size += Append(outputBuffer, HEADER_CONTENT_LENGTH_SPACE, outputBufferLength); + // Final double new lines + size += Append(outputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR, outputBufferLength); + return size; +} + +void SetContentLength(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaUInt16 contentLengthPosition, WoopsaUInt16 contentLength) { + WOOPSA_INTEGER_TO_PADDED_STRING(contentLength, outputBuffer + contentLengthPosition, 8); + // snprintf usually adds a null byte, remove it and put a \r instead + outputBuffer[contentLengthPosition + WOOPSA_STRING_LENGTH(HEADER_CONTENT_LENGTH_SPACE)] = '\r'; +} + +WoopsaUInt16 PrepareResponseWithContent( + WoopsaChar8* outputBuffer, + const WoopsaUInt16 outputBufferLength, + const WoopsaChar8* httpStatusCode, + const WoopsaChar8* httpStatusStr, + const WoopsaChar8* content) { + WoopsaUInt16 len, pos, contentLength; + len = PrepareResponse(outputBuffer, outputBufferLength, httpStatusCode, httpStatusStr, &pos, CONTENT_TYPE_JSON); + contentLength = WOOPSA_STRING_LENGTH(content); + SetContentLength(outputBuffer, outputBufferLength, pos, contentLength); + len += Append(outputBuffer, content, outputBufferLength); + return len; +} + +// Shortcut to generate an HTTP error. The contents +// of the response is the error string itself. +WoopsaUInt16 PrepareError(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaChar8 errorCode[], const WoopsaChar8 errorStr[]) { + return PrepareResponseWithContent(outputBuffer, outputBufferLength, errorCode, errorStr, errorStr); +} + +void StringToLower(WoopsaChar8 str[]) { + int i = 0; + for (i = 0; str[i]; i++) { + str[i] = WOOPSA_CHAR_TO_LOWER(str[i]); + } +} + +WoopsaUInt16 OutputSerializedValue(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaChar8 stringValue[], WoopsaChar8 typeString[], WoopsaChar8 isStringValue) { + WoopsaUInt16 contentLength = 0; + contentLength += Append(outputBuffer, JSON_VALUE_VALUE, outputBufferLength); +#ifdef WOOPSA_ENABLE_STRINGS + if (isStringValue) { + contentLength += Append(outputBuffer, JSON_STRING_DELIMITER, outputBufferLength); + contentLength += AppendEscape(outputBuffer, stringValue, JSON_STRING_DELIMITER_CHAR, JSON_ESCAPE_CHAR, outputBufferLength); + contentLength += Append(outputBuffer, JSON_STRING_DELIMITER, outputBufferLength); + } else { +#endif + contentLength += Append(outputBuffer, stringValue, outputBufferLength); +#ifdef WOOPSA_ENABLE_STRINGS + } +#endif + contentLength += Append(outputBuffer, JSON_VALUE_TYPE, outputBufferLength); + contentLength += Append(outputBuffer, typeString, outputBufferLength); + contentLength += Append(outputBuffer, JSON_VALUE_END, outputBufferLength); + return contentLength; +} + +WoopsaUInt16 OutputProperty(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaEntry* woopsaEntry, TypesDictionaryEntry* typeEntry, WoopsaChar8 numericValueBuffer[]) { + WoopsaUInt16 contentLength = 0; +#ifdef WOOPSA_ENABLE_STRINGS + if (woopsaEntry->type == WOOPSA_TYPE_TEXT + || woopsaEntry->type == WOOPSA_TYPE_LINK + || woopsaEntry->type == WOOPSA_TYPE_RESOURCE_URL + || woopsaEntry->type == WOOPSA_TYPE_DATE_TIME) { + WOOPSA_LOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, (WoopsaChar8*)woopsaEntry->address, typeEntry->string, 1); + WOOPSA_UNLOCK + } else if ( woopsaEntry->type == WOOPSA_TYPE_LOGICAL ){ + WOOPSA_LOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, *(WoopsaChar8*)(woopsaEntry->address)?JSON_TRUE:JSON_FALSE, typeEntry->string, 0); + WOOPSA_UNLOCK + } else { +#endif + WOOPSA_LOCK + if (woopsaEntry->type == WOOPSA_TYPE_INTEGER) + WOOPSA_INTEGER_TO_STRING(*(int*)woopsaEntry->address, numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + else + WOOPSA_REAL_TO_STRING(*(float*)(woopsaEntry->address), numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + WOOPSA_UNLOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, numericValueBuffer, typeEntry->string, 0); +#ifdef WOOPSA_ENABLE_STRINGS + } +#endif + return contentLength; +} + +/////////////////////////////////////////////////////////////////////////////// +// BEGIN PUBLIC WOOPSA IMPLEMENTATION // +/////////////////////////////////////////////////////////////////////////////// + +void WoopsaServerInit(WoopsaServer* server, const WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)) { + server->pathPrefix = prefix; + server->entries = entries; + server->handleRequest = handleRequest; +} + +WoopsaUInt8 WoopsaCheckRequestComplete(WoopsaServer* server, WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength) { + WoopsaUInt8 contentLengthLength = 0; + WoopsaChar8 *buffer = server->buffer, *header = NULL, *contentLengthPosition = NULL, *contentStart = NULL, *contentPosition = NULL; + WoopsaInt16 headerSize = 0, contentLength = 0, i = 0; + // Zero-out the buffer + memset(buffer, 0, sizeof(WoopsaBuffer)); + header = inputBuffer; + while ((headerSize = NextHTTPHeader(header, &header)) != -1) { + WOOPSA_STRING_N_COPY(buffer, header, headerSize); + StringToLower(buffer); + // Is there a "Content-Length" header? + if (WOOPSA_STRING_POSITION(buffer, HEADER_CONTENT_LENGTH) == buffer) { + // Yes, find the content-length + for (i = 0; buffer[i] != '\r'; i++) { + if (contentLengthPosition == NULL && buffer[i] == ':') { + contentLengthPosition = buffer + i; + } else if (contentLengthPosition != NULL && buffer[i] == ' ') { + contentLengthPosition++; + } else if (contentLengthPosition != NULL && buffer[i] != ' ') { + contentLengthLength++; + } + } + contentLengthPosition[contentLengthLength+1] = '\0'; + WOOPSA_STRING_TO_INTEGER(contentLength, contentLengthPosition); + break; + } + } + if (contentLength == 0) { + // If there is no content and we have the terminator, all done. + if (WOOPSA_STRING_POSITION(inputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR)) + return WOOPSA_REQUEST_COMLETE; + else + return WOOPSA_REQUEST_MORE_DATA_NEEDED; + } else { + // Otherwise it's a bit more technical, we have to make sure all + // the content is there + contentPosition = WOOPSA_STRING_POSITION(inputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR) + WOOPSA_STRING_LENGTH(HEADER_SEPARATOR HEADER_SEPARATOR); + if (WOOPSA_STRING_LENGTH(contentPosition) == contentLength) + return WOOPSA_REQUEST_COMLETE; + else + return WOOPSA_REQUEST_MORE_DATA_NEEDED; + } +} + +WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength, WoopsaChar8* outputBuffer, WoopsaUInt16 outputBufferLength, WoopsaUInt16* responseLength) { + WoopsaChar8* header = NULL; + WoopsaChar8* woopsaPath = NULL; + WoopsaChar8* requestContent = NULL; + WoopsaChar8 numericValueBuffer[MAX_NUMERICAL_VALUE_LENGTH]; + WoopsaChar8 oldChar = '\0'; + WoopsaUInt16 i = 0, pos = 0, contentLengthPosition = 0, contentLength = 0; + WoopsaUInt8 isPost = 0, valueFound = 0, entryAt = 0; + WoopsaInt16 headerSize = 0, keypairSize = 0; + WoopsaEntry* woopsaEntry = NULL; + TypesDictionaryEntry* typeEntry = NULL; + WoopsaChar8* buffer = server->buffer; + // Zero-out the buffers + memset(buffer, 0, sizeof(WoopsaBuffer)); + memset(numericValueBuffer, 0, MAX_NUMERICAL_VALUE_LENGTH); + // Parse the first header (GET/POST) + headerSize = NextHTTPHeader(inputBuffer, &header); + if (headerSize == -1) + return WOOPSA_CLIENT_REQUEST_ERROR; + // Copy the HTTP method in the buffer and check if POST + for (i = 0; i < headerSize && i < sizeof(WoopsaBuffer); i++) { + if (inputBuffer[i] == ' ') { + pos = i + 1; + break; + } + buffer[i] = inputBuffer[i]; + } + if (WOOPSA_STRING_EQUAL(buffer, "POST")) + isPost = 1; + // Extract the requested path into the buffer + for (i = 0; (i < headerSize - pos) && i < sizeof(WoopsaBuffer); i++) { + if (inputBuffer[pos + i] == ' ') + break; + buffer[i] = inputBuffer[pos + i]; + } + // Extract all headers (but do nothing with them) + while ((headerSize = NextHTTPHeader(header, &header)) != -1) { + // Do some work on the headers. We don't need to really do anything now actually + } + // Check if the path is a Woopsa path + if ((WOOPSA_STRING_POSITION(buffer, server->pathPrefix)) != buffer) { + // It's not, so we try to handle it with the handleRequest func pointer + if (server->handleRequest != NULL) { + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_HTML); + contentLength = server->handleRequest(buffer, isPost, outputBuffer, outputBufferLength); + if (contentLength == 0) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } else { + SetContentLength(outputBuffer, outputBufferLength, contentLengthPosition, contentLength); + *responseLength += contentLength; + return WOOPSA_OTHER_RESPONSE; + } + } else { + // This request does not start with the prefix, return 404 + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + } + // Remove the Woopsa prefix and handle each Woopsa verb + woopsaPath = &(buffer[WOOPSA_STRING_LENGTH(server->pathPrefix)]); + if (WOOPSA_STRING_POSITION(woopsaPath, VERB_META) == woopsaPath && isPost == 0) { + // Meta request, start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Output the serialized response + contentLength += Append(outputBuffer, JSON_META_PROPERTIES JSON_ARRAY_START, outputBufferLength); + for(i = 0; server->entries[i].name != NULL; i++) { + woopsaEntry = &server->entries[i]; + if (woopsaEntry->isMethod) + continue; + if ( entryAt != 0 ) + contentLength += Append(outputBuffer, JSON_ARRAY_DELIMITER, outputBufferLength); + entryAt++; + typeEntry = GetTypeEntry(woopsaEntry->type); + contentLength += Append(outputBuffer, JSON_PROPERTY_NAME, outputBufferLength); + contentLength += AppendEscape(outputBuffer, woopsaEntry->name, JSON_STRING_DELIMITER_CHAR, JSON_ESCAPE_CHAR, outputBufferLength); + contentLength += Append(outputBuffer, JSON_PROPERTY_TYPE, outputBufferLength); + contentLength += Append(outputBuffer, typeEntry->string, outputBufferLength); + contentLength += Append(outputBuffer, JSON_PROPERTY_READONLY, outputBufferLength); + contentLength += Append(outputBuffer, (woopsaEntry->readOnly == 1) ? JSON_TRUE : JSON_FALSE, outputBufferLength); + contentLength += Append(outputBuffer, JSON_PROPERTY_END, outputBufferLength); + } + contentLength += Append(outputBuffer, JSON_ARRAY_END JSON_META_METHODS JSON_ARRAY_START, outputBufferLength); +#ifdef WOOPSA_ENABLE_METHODS + entryAt = 0; + for (i = 0; server->entries[i].name != NULL; i++) { + woopsaEntry = &server->entries[i]; + if (!woopsaEntry->isMethod) + continue; + if (entryAt != 0) + contentLength += Append(outputBuffer, JSON_ARRAY_DELIMITER, outputBufferLength); + entryAt++; + typeEntry = GetTypeEntry(woopsaEntry->type); + contentLength += Append(outputBuffer, JSON_METHOD_NAME, outputBufferLength); + contentLength += AppendEscape(outputBuffer, woopsaEntry->name, JSON_STRING_DELIMITER_CHAR, JSON_ESCAPE_CHAR, outputBufferLength); + contentLength += Append(outputBuffer, JSON_METHOD_RETURN_TYPE, outputBufferLength); + contentLength += Append(outputBuffer, typeEntry->string, outputBufferLength); + contentLength += Append(outputBuffer, JSON_METHOD_END, outputBufferLength); + } +#endif + contentLength += Append(outputBuffer, JSON_ARRAY_END JSON_META_END, outputBufferLength); + } else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_READ) == woopsaPath && isPost == 0) { + // Read request - Get the property for this read + buffer[0] = '\0'; + woopsaPath = &(woopsaPath[sizeof(VERB_READ)]); + if ((woopsaEntry = GetPropertyByNameOrNull(server->entries, woopsaPath)) == NULL) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + typeEntry = GetTypeEntry(woopsaEntry->type); + // Start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Output the serialized response + contentLength += OutputProperty(outputBuffer, outputBufferLength, woopsaEntry, typeEntry, numericValueBuffer); + } else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_WRITE) == woopsaPath && isPost == 1) { + // Write request - Get the property for this write + buffer[0] = '\0'; + woopsaPath = &(woopsaPath[sizeof(VERB_WRITE)]); + if ((woopsaEntry = GetPropertyByNameOrNull(server->entries, woopsaPath)) == NULL) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + typeEntry = GetTypeEntry(woopsaEntry->type); + // Decode POST data + requestContent = WOOPSA_STRING_POSITION(inputBuffer, "\r\n\r\n"); + requestContent = &requestContent[4]; + // Cheating here - using the numericValueBuffer to store the key (10 chars should be enough) + while ((keypairSize = NextURLDecodedValue(&requestContent[keypairSize], numericValueBuffer, buffer)) != -1) { + if (WOOPSA_STRING_EQUAL(numericValueBuffer, POST_VALUE_KEY)) { + valueFound = 1; + } + } + if (!valueFound) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_BAD_REQUEST, HTTP_TEXT_BAD_REQUEST); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + // Write the value + if (woopsaEntry->type == WOOPSA_TYPE_INTEGER) { + WOOPSA_LOCK + WOOPSA_STRING_TO_INTEGER(*(int*)woopsaEntry->address, buffer); + WOOPSA_UNLOCK + } else if (woopsaEntry->type == WOOPSA_TYPE_REAL || woopsaEntry->type == WOOPSA_TYPE_TIME_SPAN) { + WOOPSA_LOCK + WOOPSA_STRING_TO_FLOAT(*(float*)woopsaEntry->address, buffer); + WOOPSA_UNLOCK + } + else if (woopsaEntry->type == WOOPSA_TYPE_LOGICAL) { + StringToLower(buffer); + WOOPSA_LOCK + if (WOOPSA_STRING_EQUAL(buffer, JSON_TRUE)) + *(char*)woopsaEntry->address = 1; + else + *(char*)woopsaEntry->address = 0; + WOOPSA_UNLOCK + } +#ifdef WOOPSA_ENABLE_STRINGS + else + { + if (woopsaEntry->size > WOOPSA_STRING_LENGTH(buffer)) { + WOOPSA_LOCK + WOOPSA_STRING_COPY((char*)woopsaEntry->address, buffer); + WOOPSA_UNLOCK + } else { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_BAD_REQUEST, HTTP_TEXT_BAD_REQUEST); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + } +#endif + // Start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Output the serialized response + contentLength += OutputProperty(outputBuffer, outputBufferLength, woopsaEntry, typeEntry, numericValueBuffer); + } +#ifdef WOOPSA_ENABLE_METHODS + else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_INVOKE) == woopsaPath && isPost == 1) + { + // Write request - Get the property for this write + buffer[0] = '\0'; + woopsaPath = &(woopsaPath[sizeof(VERB_INVOKE)]); + if ((woopsaEntry = GetMethodByNameOrNull(server->entries, woopsaPath)) == NULL) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + typeEntry = GetTypeEntry(woopsaEntry->type); + // Start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Invoke the method + if (woopsaEntry->type == WOOPSA_TYPE_NULL) { + (*(ptrMethodVoid)woopsaEntry->address)(); + contentLength = 0; + } else { + if (woopsaEntry->type == WOOPSA_TYPE_TEXT + || woopsaEntry->type == WOOPSA_TYPE_LINK + || woopsaEntry->type == WOOPSA_TYPE_RESOURCE_URL + || woopsaEntry->type == WOOPSA_TYPE_DATE_TIME) { + WOOPSA_LOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, (*(ptrMethodRetString)woopsaEntry->address)(), typeEntry->string, 1); + WOOPSA_UNLOCK + } else { + if (woopsaEntry->type == WOOPSA_TYPE_INTEGER) { + WOOPSA_LOCK + WOOPSA_INTEGER_TO_STRING((*(ptrMethodRetInteger)woopsaEntry->address)(), numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + WOOPSA_UNLOCK + } else { + WOOPSA_LOCK + WOOPSA_REAL_TO_STRING((*(ptrMethodRetReal)woopsaEntry->address)(), numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + WOOPSA_UNLOCK + } + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, numericValueBuffer, typeEntry->string, 0); + } + } + } +#endif + else + { + // Invalid request + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + // Re-inject the content-length into the HTTP headers + SetContentLength(outputBuffer, outputBufferLength, contentLengthPosition, contentLength); + *responseLength += contentLength; + return WOOPSA_SUCCESS; +} \ No newline at end of file diff --git a/Release/Embedded/woopsa-server.h b/Release/Embedded/woopsa-server.h new file mode 100644 index 0000000..ca0f976 --- /dev/null +++ b/Release/Embedded/woopsa-server.h @@ -0,0 +1,121 @@ +#ifndef __WOOPSA_H_ +#define __WOOPSA_H_ + +#include "woopsa-config.h" + +typedef short WoopsaInt16; +typedef unsigned short WoopsaUInt16; +typedef char WoopsaChar8; +typedef unsigned char WoopsaUInt8; +typedef void * WoopsaVoidPtr; + +typedef WoopsaChar8 WoopsaBuffer[WOOPSA_BUFFER_SIZE]; + +#ifdef WOOPSA_ENABLE_METHODS + typedef void(*ptrMethodVoid)(void); + typedef char*(*ptrMethodRetString)(void); + typedef int*(*ptrMethodRetInteger)(void); + typedef float*(*ptrMethodRetReal)(void); +#endif + +// JsonData is not supported in Woopsa-C +typedef enum { + WOOPSA_TYPE_NULL, + WOOPSA_TYPE_LOGICAL, + WOOPSA_TYPE_INTEGER, + WOOPSA_TYPE_REAL, + WOOPSA_TYPE_TIME_SPAN, +#ifdef WOOPSA_ENABLE_STRINGS + WOOPSA_TYPE_DATE_TIME, + WOOPSA_TYPE_TEXT, + WOOPSA_TYPE_LINK, + WOOPSA_TYPE_RESOURCE_URL +#endif +} WoopsaType; + +typedef struct { + WoopsaChar8 * name; + void* address; + WoopsaUInt8 type; + WoopsaChar8 readOnly; + WoopsaChar8 isMethod; + WoopsaUInt8 size; +} WoopsaEntry; + +typedef struct { + // The prefix for all Woopsa routes. Any client request + // made without this prefix will pass the request to + // the handleRequest function, if it exists + const WoopsaChar8 * pathPrefix; + // A function pointer to a function that accepts client + // requests with a specified path and POST. + // The function is in charge of copying content to the + // content string, and should return an amount of bytes. + // If the amount of bytes is 0, then the server will send + // a 404 Not Found error. + WoopsaUInt16 (*handleRequest)(WoopsaChar8* requestedPath, WoopsaUInt8 isPost, WoopsaChar8* outputBuffer, WoopsaUInt16 outputBufferSize); + // A list of entries to be published by Woopsa + WoopsaEntry* entries; + // A small buffer string used in various places in the server + WoopsaBuffer buffer; +} WoopsaServer; + +#define WOOPSA_BEGIN(woopsaDictionaryName) \ + WoopsaEntry woopsaDictionaryName[] = \ + { + +#define WOOPSA_END \ + { NULL, NULL, NULL, NULL, NULL, NULL }}; + +#define WOOPSA_PROPERTY_CUSTOM(variable, type, readonly) \ + { #variable, &variable, type, readonly, 0, sizeof variable }, + +#define WOOPSA_PROPERTY_READONLY(variable, type) \ + WOOPSA_PROPERTY_CUSTOM(variable, type, 1) + +#define WOOPSA_PROPERTY(variable, type) \ + WOOPSA_PROPERTY_CUSTOM(variable, type, 0) + +#ifdef WOOPSA_ENABLE_METHODS + #define WOOPSA_METHOD(method, returnType) \ + { #method, (void*)method, returnType, 0, 1, 0 }, + + #define WOOPSA_METHOD_VOID(method) \ + WOOPSA_METHOD(method, WOOPSA_TYPE_NULL) +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +// Creates a new Woopsa server using the specified prefix +// and a list of entries to publish +void WoopsaServerInit(WoopsaServer* server, const WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)); + +// Checks if the request contained in inputBuffer +// is finished. This is useful in the case where +// data is received in fragments for some reason. +// You should always call this method on your buffer +// before passing it to WoopsaHandleRequest +#define WOOPSA_REQUEST_MORE_DATA_NEEDED 0 +#define WOOPSA_REQUEST_COMLETE 1 +WoopsaUInt8 WoopsaCheckRequestComplete(WoopsaServer* server, WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength); + +// Parses a request and prepares the reply as well +// Returns: +// WOOPSA_SUCCESS (0) = success +// WOOPSA_CLIENT_REQUEST_ERROR (1) = the client made a bad request +// WOPOSA_OTHER_ERROR (2) = something wrong happened inside Woopsa (it's our fault) +#define WOOPSA_SUCCESS 0 +#define WOOPSA_CLIENT_REQUEST_ERROR 1 +#define WOOPSA_OTHER_ERROR 2 +#define WOOPSA_OTHER_RESPONSE 3 + +WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength, WoopsaChar8* outputBuffer, WoopsaUInt16 outputBufferLength, WoopsaUInt16* responseLength); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/Sources/Embedded/ArduinoDemoServer/ArduinoDemoServer.ino b/Sources/Embedded/ArduinoDemoServer/ArduinoDemoServer.ino new file mode 100644 index 0000000..07ca745 --- /dev/null +++ b/Sources/Embedded/ArduinoDemoServer/ArduinoDemoServer.ino @@ -0,0 +1,271 @@ +/* +================================================================== + + WOOPSA ARDUINO DEMO SERVER + +================================================================== + + THIS SAMPLE REQUIRES THE ETHERNET2 LIBRARY BUNDLED ONLY WITH THE + ARDUINO 1.7+ IDE AVAILABLE FROM ARDUINO.ORG (AND NOT ARDUINO.CC). + + This little piece of software creates a Woopsa server on + an Arduino board that allows you to view Analog input values + and manage digital I/Os (read/write and set pin mode). + + Since Woopsa uses HTTP (over TCP/IP), you will obviously need an + Arduino Ethernet shield. + + If you are using version 1 of the Ethernet Shield, you might have + to change "Ethernet2.h" to "Ethernet.h" in the include directives. + + Instructions for use: + - Choose an IP address below that matches your local network + - Upload the sketch to the arduino + - Point your browser to http://{ip-address}/ + - Have fun playing with the Arduino's I/Os! + + NOTES: This code uses about 4k of RAM, which means it won't fit + in the Arduino Uno without a lot of optimizations. Sorry. + +================================================================== +*/ +#include +#include +#include + +#include "woopsa-server.h" +#include "html.h" + +// Enter a MAC address and IP address for your controller below. +// The IP address will be dependent on your local network: +byte mac[] = { + 0x90, 0xA2, 0xDA, 0x10, 0x32, 0x0B +}; +IPAddress ip(192, 168, 42, 3); + +// Initialize the Ethernet server library +// with the IP address and port you want to use +// (port 80 is default for HTTP): +EthernetServer server(80); + +// The struct for holding all woopsa information +WoopsaServer woopsaServer; + +// The properties that will be published over Woopsa +int AnalogIn0, AnalogIn1, AnalogIn2, AnalogIn3, AnalogIn4, AnalogIn5; +int Digital0, Digital1, Digital2, Digital3, Digital4, Digital5, Digital6, Digital7, Digital8, Digital9, Digital10, Digital11, Digital12, Digital13; +int PinMode0, PinMode1, PinMode2, PinMode3, PinMode4, PinMode5, PinMode6, PinMode7, PinMode8, PinMode9, PinMode10, PinMode11, PinMode12, PinMode13; + +// These macros publish properties over the Woopsa protocol +WOOPSA_BEGIN(woopsaEntries) + WOOPSA_PROPERTY_READONLY(AnalogIn0, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY_READONLY(AnalogIn1, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY_READONLY(AnalogIn2, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY_READONLY(AnalogIn3, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY_READONLY(AnalogIn4, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY_READONLY(AnalogIn5, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital0, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital1, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital2, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital3, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital4, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital5, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital6, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital7, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital8, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital9, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital10, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital11, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital12, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(Digital13, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode0, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode1, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode2, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode3, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode4, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode5, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode6, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode7, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode8, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode9, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode10, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode11, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode12, WOOPSA_TYPE_INTEGER) + WOOPSA_PROPERTY(PinMode13, WOOPSA_TYPE_INTEGER) +WOOPSA_END; + +void IoLoop() { + // Read values from Analog inputs + AnalogIn0 = analogRead(A0); + AnalogIn1 = analogRead(A1); + AnalogIn2 = analogRead(A2); + AnalogIn3 = analogRead(A3); + AnalogIn4 = analogRead(A4); + AnalogIn5 = analogRead(A5); + + // Auto-generated code, copies I/Os + pinMode(0,PinMode0); + if (PinMode0 == OUTPUT) + digitalWrite(0, Digital0); + else + Digital0 = digitalRead(0); + + pinMode(1,PinMode1); + if (PinMode1 == OUTPUT) + digitalWrite(1, Digital1); + else + Digital1 = digitalRead(1); + + pinMode(2,PinMode2); + if (PinMode2 == OUTPUT) + digitalWrite(2, Digital2); + else + Digital2 = digitalRead(2); + + pinMode(3,PinMode3); + if (PinMode3 == OUTPUT) + digitalWrite(3, Digital3); + else + Digital3 = digitalRead(3); + + pinMode(4,PinMode4); + if (PinMode4 == OUTPUT) + digitalWrite(4, Digital4); + else + Digital4 = digitalRead(4); + + pinMode(5,PinMode5); + if (PinMode5 == OUTPUT) + digitalWrite(5, Digital5); + else + Digital5 = digitalRead(5); + + pinMode(6,PinMode6); + if (PinMode6 == OUTPUT) + digitalWrite(6, Digital6); + else + Digital6 = digitalRead(6); + + pinMode(7,PinMode7); + if (PinMode7 == OUTPUT) + digitalWrite(7, Digital7); + else + Digital7 = digitalRead(7); + + pinMode(8,PinMode8); + if (PinMode8 == OUTPUT) + digitalWrite(8, Digital8); + else + Digital8 = digitalRead(8); + + pinMode(9,PinMode9); + if (PinMode9 == OUTPUT) + digitalWrite(9, Digital9); + else + Digital9 = digitalRead(9); + + // Pins reserved for ethernet shield + // pinMode(10,PinMode10); + if (PinMode10 == OUTPUT) + digitalWrite(10, Digital10); + else + Digital10 = digitalRead(10); + + // Pins reserved for ethernet shield + // pinMode(11,PinMode11); + if (PinMode11 == OUTPUT) + digitalWrite(11, Digital11); + else + Digital11 = digitalRead(11); + + // Pins reserved for ethernet shield + // pinMode(12,PinMode12); + if (PinMode12 == OUTPUT) + digitalWrite(12, Digital12); + else + Digital12 = digitalRead(12); + + // Pins reserved for ethernet shield + // pinMode(13,PinMode13); + if (PinMode13 == OUTPUT) + digitalWrite(13, Digital13); + else + Digital13 = digitalRead(13); +} + + +WoopsaUInt16 ServeHTML(WoopsaChar8 path[], WoopsaUInt8 isPost, WoopsaChar8 dataBuffer[], WoopsaUInt16 dataBufferSize) { + // Normally, we would write inside the dataBuffer + // However, due to the Arduino's extremely limited + // RAM (a few kB), we can't write a "huge" 14kB HTML + // file. So we just return the length of the HTML + // and we will directly write the HTML stored in flash + // memory to the TCP stream in the woopsaLoop. See below. + return sizeof(HTML)-1; +} + +void WoopsaLoop() { + char dataBuffer[2048]; + int bufferAt = 0; + short unsigned int responseLength; + memset(dataBuffer, 0, sizeof(dataBuffer)); + // Listen for incoming clients + EthernetClient client = server.available(); + if (client) { + while (client.connected()) { + if (client.available()) { + // We _append_ data to our buffer when it is received + bufferAt += client.read((unsigned char*)(dataBuffer + bufferAt), sizeof(dataBuffer)); + + // When we get a request from a client, we need + // to make sure it's complete before we pass it + // to the Woopsa server. This allows us to handle + // cases where packets are fragmented. + if (WoopsaCheckRequestComplete(&woopsaServer, dataBuffer, sizeof(dataBuffer)) != WOOPSA_REQUEST_COMLETE) { + continue; + } + + if ( WoopsaHandleRequest(&woopsaServer, dataBuffer, sizeof(dataBuffer), dataBuffer, sizeof(dataBuffer), &responseLength) == WOOPSA_OTHER_RESPONSE ) { + // This was a non-Woopsa request, serve the HTML page + client.print(dataBuffer); + // The F() macro specifies the const string is stored + // in flash memory + client.print(F(HTML)); + } else { + client.print(dataBuffer); + } + break; + } + } + } + client.stop(); +} + +void setup() { + // Open serial communications and wait for port to open: + Serial.begin(9600); + while (!Serial); // wait for serial port to connect. Needed for native USB port only + + // start the Ethernet connection and the server: + Ethernet.begin(mac, ip); + server.begin(); + + // Initialize the woopsa server with a path prefix of + // /woopsa/ and the specified woopsa entries. + // The ServeHTML function allows us to serve an HTML + // page which you can access + WoopsaServerInit(&woopsaServer, "/woopsa/", woopsaEntries, ServeHTML); + + Serial.print(F("Woopsa server listening on http://")); + Serial.print(Ethernet.localIP()); + Serial.println(F("/woopsa/")); + Serial.print(F("You can check out a demo on http://")); + Serial.print(Ethernet.localIP()); + Serial.println(F("/ which allows you to play with the Arduino's IOs.")); +} + +void loop() { + IoLoop(); + WoopsaLoop(); +} + diff --git a/Sources/Embedded/ArduinoDemoServer/html.h b/Sources/Embedded/ArduinoDemoServer/html.h new file mode 100644 index 0000000..d584676 --- /dev/null +++ b/Sources/Embedded/ArduinoDemoServer/html.h @@ -0,0 +1,3 @@ +#ifndef HTML +#define HTML "Woopsa Arduino demo

Woopsa Arduino demo

Analog inputs

 

Digital I/Os

 

" +#endif diff --git a/Sources/Embedded/ArduinoDemoServer/index.html b/Sources/Embedded/ArduinoDemoServer/index.html new file mode 100644 index 0000000..cb7fb8f --- /dev/null +++ b/Sources/Embedded/ArduinoDemoServer/index.html @@ -0,0 +1,117 @@ + + + Woopsa Arduino demo + + + + +
+

Woopsa Arduino demo

+
+
+

Analog inputs

+
+
+
+
+
+

 

+
+
+
+
+
+
+
+

Digital I/Os

+
+
+
+
+
+
+
+
+
+

 

+
+
+
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/Sources/Embedded/ArduinoDemoServer/woopsa-config.h b/Sources/Embedded/ArduinoDemoServer/woopsa-config.h new file mode 100644 index 0000000..2105643 --- /dev/null +++ b/Sources/Embedded/ArduinoDemoServer/woopsa-config.h @@ -0,0 +1,47 @@ +#ifndef __WOOPSA_CONFIG_H_ +#define __WOOPSA_CONFIG_H_ + +#include +#include +#include +#include + + +// Woopsa uses these internally, allowing you to use Woopsa in a +// thread-safe manner, or disabling interrupts. Just fill this in +// if needed with whatever locking mechanism your environment has. +#define WOOPSA_LOCK // disable interrupts +#define WOOPSA_UNLOCK // enable interrupts + +// If you are on a system with very low memory, you can reduce the +// buffer size that the Woopsa server uses internally. +// This value changes the maximum length of URLs you can parse. +// You should not go under 128 bytes to be 100% safe. +#define WOOPSA_BUFFER_SIZE 256 + +// Thanks microsoft for not supporting snprintf! +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define snprintf _snprintf +#endif + +#define WOOPSA_ENABLE_STRINGS +#define WOOPSA_ENABLE_METHODS + +// 99% of systems will have the standard C library but in case +// you end up in the 1%, you can always re-define these functions +// to work for you. +#define WOOPSA_INTEGER_TO_PADDED_STRING(value, string, padding) sprintf(string, "%" #padding "d", value) +#define WOOPSA_INTEGER_TO_STRING(value, string, max_length) snprintf(string, max_length, "%d", value) +#define WOOPSA_REAL_TO_STRING(value, string, max_length) snprintf(string, max_length, "%f", value) + +#define WOOPSA_STRING_TO_INTEGER(value, string) (value = atoi(string)) +#define WOOPSA_STRING_TO_FLOAT(value, string) (value = atof(string)) + +#define WOOPSA_STRING_POSITION(haystack, needle) strstr(haystack, needle) +#define WOOPSA_STRING_EQUAL(string1, string2) (strcmp(string1, string2) == 0) +#define WOOPSA_STRING_LENGTH(string) strlen(string) +#define WOOPSA_CHAR_TO_LOWER(character) tolower(character) +#define WOOPSA_STRING_COPY(destination, source) strcpy(destination, source) +#define WOOPSA_STRING_N_COPY(destination, source, n) strncpy(destination, source, n) + +#endif \ No newline at end of file diff --git a/Sources/Embedded/ArduinoDemoServer/woopsa-server.c b/Sources/Embedded/ArduinoDemoServer/woopsa-server.c new file mode 100644 index 0000000..7310560 --- /dev/null +++ b/Sources/Embedded/ArduinoDemoServer/woopsa-server.c @@ -0,0 +1,659 @@ +#include "woopsa-server.h" +#include +#include + + + +// Woopsa constants +#define VERB_META "meta" +#define VERB_READ "read" +#define VERB_WRITE "write" +#define VERB_INVOKE "invoke" + +#define TYPE_STRING_NULL "Null" +#define TYPE_STRING_LOGICAL "Logical" +#define TYPE_STRING_INTEGER "Integer" +#define TYPE_STRING_REAL "Real" +#define TYPE_STRING_DATE_TIME "DateTime" +#define TYPE_STRING_TIME_SPAN "TimeSpan" +#define TYPE_STRING_TEXT "Text" +#define TYPE_STRING_LINK "Link" +#define TYPE_STRING_RESOURCE_URL "ResourceUrl" + +typedef struct { + WoopsaType type; + WoopsaChar8* string; +} TypesDictionaryEntry; + +TypesDictionaryEntry Types[] = { + { WOOPSA_TYPE_NULL, TYPE_STRING_NULL }, + { WOOPSA_TYPE_LOGICAL, TYPE_STRING_LOGICAL }, + { WOOPSA_TYPE_INTEGER, TYPE_STRING_INTEGER }, + { WOOPSA_TYPE_REAL, TYPE_STRING_REAL }, + { WOOPSA_TYPE_TIME_SPAN, TYPE_STRING_TIME_SPAN }, +#ifdef WOOPSA_ENABLE_STRINGS + { WOOPSA_TYPE_DATE_TIME, TYPE_STRING_DATE_TIME }, + { WOOPSA_TYPE_TEXT, TYPE_STRING_TEXT }, + { WOOPSA_TYPE_LINK, TYPE_STRING_LINK }, + { WOOPSA_TYPE_RESOURCE_URL, TYPE_STRING_RESOURCE_URL } +#endif +}; + +// HTTP constants +#define HTTP_METHOD_GET "GET" +#define HTTP_METHOD_POST "POST" +#define HTTP_VERSION_STRING "HTTP/1.1" + +#define HTTP_CODE_BAD_REQUEST "400" +#define HTTP_TEXT_BAD_REQUEST "Bad request" + +#define HTTP_CODE_NOT_FOUND "404" +#define HTTP_TEXT_NOT_FOUND "Not found" + +#define HTTP_CODE_INTERNAL_ERROR "500" +#define HTTP_TEXT_INTERNAL_ERROR "Internal server error" + +#define HTTP_CODE_NOT_IMPLEMENTED "501" +#define HTTP_TEXT_METHOD_NOT_IMPLEMENTED "%s method not implemented" + +#define HTTP_CODE_OK "200" +#define HTTP_TEXT_OK "OK" + +#define HEADER_SEPARATOR "\r\n" +#define HEADER_VALUE_SEPARATOR ":" +#define HEADER_CONTENT_LENGTH "content-length" +#define HEADER_CONTENT_LENGTH_SPACE " " +#define HEADER_CONTENT_LENGTH_PADDING 8 +#define HEADER_CONTENT_TYPE "Content-Type: " +#define EXTRA_HEADERS "Access-Control-Allow-Origin: *" HEADER_SEPARATOR "Connection: close" HEADER_SEPARATOR +#define CONTENT_TYPE_JSON "application/json" +#define CONTENT_TYPE_HTML "text/html" + +// POST constants +#define POST_VALUE_KEY "value" +#define URLENCODE_KEY_SEPARATOR '&' +#define URLENCODE_VALUE_SEPARATOR '=' +#define URLENCODE_VALUE_ENCODER '%' + +// Serialization constants +#define JSON_VALUE_VALUE "{\"Value\":" +#define JSON_VALUE_TYPE ",\"Type\":\"" +#define JSON_VALUE_END "\"}" +#define JSON_META_PROPERTIES "{\"Name\":\"Root\",\"Properties\":" +#define JSON_META_METHODS ",\"Methods\":" +#define JSON_META_END ",\"Items\":[]}" +#define JSON_PROPERTY_NAME "{\"Name\":\"" +#define JSON_PROPERTY_TYPE "\",\"Type\":\"" +#define JSON_PROPERTY_READONLY "\",\"ReadOnly\":" +#define JSON_PROPERTY_END "}" +#define JSON_METHOD_NAME "{\"Name\":\"" +#define JSON_METHOD_RETURN_TYPE "\",\"ReturnType\":\"" +#define JSON_METHOD_END "\",\"ArgumentInfos\":[]}" +#define JSON_ARRAY_START "[" +#define JSON_ARRAY_END "]" +#define JSON_ARRAY_DELIMITER "," +#define JSON_TRUE "true" +#define JSON_FALSE "false" +#define JSON_STRING_DELIMITER "\"" +#define JSON_STRING_DELIMITER_CHAR '"' +#define JSON_ESCAPE_CHAR '\\' + +// Memory-specific constants +#define MAX_NUMERICAL_VALUE_LENGTH 10 + +// Gets a Woopsa Property by name +// Returns a pointer to the WoopsaEntry, or null if not found +WoopsaEntry* GetPropertyByNameOrNull(WoopsaEntry entries[], WoopsaChar8 name[]) { + WoopsaUInt16 i = 0; + while (entries[i].name != NULL) { + if (WOOPSA_STRING_EQUAL(entries[i].name, name) && entries[i].isMethod == 0) + return &entries[i]; + i++; + } + return NULL; +} + +#ifdef WOOPSA_ENABLE_METHODS +// Gets a Woopsa Method by name +// Returns a pointer to the WoopsaEntry, or null if not found +WoopsaEntry* GetMethodByNameOrNull(WoopsaEntry entries[], WoopsaChar8 name[]) { + WoopsaUInt16 i = 0; + while (entries[i].name != NULL) { + if (WOOPSA_STRING_EQUAL(entries[i].name, name) && entries[i].isMethod == 1) + return &entries[i]; + i++; + } + return NULL; +} +#endif + +// Gets the type string for a given type +// Returns a pointer to a string or NULL if not found (why ??) +TypesDictionaryEntry * GetTypeEntry(WoopsaType type) { + WoopsaUInt8 i; + for (i = 0; i < sizeof(Types) / sizeof(TypesDictionaryEntry); i++) + if (Types[i].type == type) + return &Types[i]; + return NULL; +} + +// Finds the string length of the first HTTP header found in searchString +// Also sets nextHeader to be the start of the next header +// Returns the length of the *current* header, or -1 if this header is empty +// (which means this is the double new-line at the end of HTTP request) +WoopsaInt16 NextHTTPHeader(const WoopsaChar8* searchString, const WoopsaChar8** nextHeader) { + WoopsaUInt16 i = 0, carriageFound = 0; + while (searchString[i] != '\0') { + if (carriageFound == 1 && searchString[i] == '\n') { + *nextHeader = searchString + i + 1; + return i - 1; + } else if (carriageFound == 1 && searchString[i] != '\n') + carriageFound = 0; + else if (searchString[i] == '\r') + carriageFound = 1; + i++; + } + return -1; +} + +// Finds the next key/value pair in a URLEncoded string +// and decodes the actual value. Note: keys are lowercased +// Returns the length of the *current* key/value pair, or -1 if none found +WoopsaInt16 NextURLDecodedValue(const WoopsaChar8* searchString, WoopsaChar8* key, WoopsaChar8* value) { + WoopsaUInt16 i = 0, inKey = 1, keyAt = 0, valueAt = 0; + WoopsaUInt8 specialCharAt = 0, inSpecialChar = 0; + WoopsaChar8 charBuff[2] = { 0, 0 }; + WoopsaChar8 specialChar = '\0', curChar = '\0'; + for (i = 0; searchString[i] != '\0' ; i++) { + if (searchString[i] == URLENCODE_VALUE_ENCODER){ + // We're entering a %-encoded value + inSpecialChar = 1; + continue; + } else if (inSpecialChar && specialCharAt < 2 ) { + // We're inside a %-encoded value, add to the buffer + curChar = WOOPSA_CHAR_TO_LOWER(searchString[i]); + if ((curChar >= '0' && curChar <= '9') || (curChar >= 'a' && curChar <= 'f')) + charBuff[specialCharAt++] = curChar; + else + charBuff[specialCharAt++] = '4'; // must be something.. just not null! + continue; + } else if (inSpecialChar && specialCharAt == 2) { + // We're out of the %-encoded value - convert it to a char + if (charBuff[0] >= 'a') + specialChar = (charBuff[0] - 'a' + 0xa) * 0x10; + else + specialChar = (charBuff[0] - '0') * 0x10; + if (charBuff[1] >= 'a') + specialChar += charBuff[1] - 'a' + 0xa; + else + specialChar += charBuff[1] - '0'; + if (inKey) + key[keyAt++] = specialChar; + else + value[valueAt++] = specialChar; + inSpecialChar = 0; + specialCharAt = 0; + } + curChar = searchString[i]; + if (curChar == URLENCODE_VALUE_SEPARATOR) { + inKey = 0; + continue; + } + if (curChar == URLENCODE_KEY_SEPARATOR) { + key[keyAt] = '\0'; + value[valueAt] = '\0'; + return i; + } + if (curChar == '+') + curChar = ' '; + if (inKey == 1) + key[keyAt++] = WOOPSA_CHAR_TO_LOWER(curChar); + else + value[valueAt++] = curChar; + } + if (keyAt > 0 || valueAt > 0) { + key[keyAt] = '\0'; + value[valueAt] = '\0'; + return i; + } else { + return -1; + } +} + + +// Appends source to destination, while keeping destination under num length +// Returns amount of characters actually appended +WoopsaUInt16 Append(WoopsaChar8 destination[], const WoopsaChar8 source[], WoopsaUInt16 num) { + WoopsaUInt16 i = 0, cnt = 0; + // go to end of destination + while (destination[i] != '\0') + i++; + //append characters one-by-one + while (source[cnt] != '\0' && i < num - 1) + destination[i++] = source[cnt++]; + destination[i] = '\0'; + return cnt; +} + +// Appends source to destination, while keeping destination under num length +// and escaping specified character +// Returns amount of characters actually appended +WoopsaUInt16 AppendEscape(WoopsaChar8 destination[], const WoopsaChar8 source[], WoopsaChar8 special, WoopsaChar8 escape, WoopsaUInt16 num) { + WoopsaUInt16 i = 0, cnt = 0, extras = 0; + // go to end of destination + while (destination[i] != '\0') + i++; + //append characters one-by-one + while (source[cnt] != '\0' && i < num - 1) { + if (source[cnt] == special) { + // TODO: Watch out for buffer overflow! + destination[i++] = escape; + destination[i++] = source[cnt++]; + extras++; + } else { + destination[i++] = source[cnt++]; + } + } + destination[i] = '\0'; + return cnt + extras; +} + +// Prepares an HTTP response in the specified outputBuffer +// Will send the specified HTTP status code and a status string +// Will also add the content +// This method will basically prepare the entire string that can be +// send out to the client, including all HTTP headers. +// Returns the length of the prepared string +WoopsaUInt16 PrepareResponse( + WoopsaChar8* outputBuffer, + const WoopsaUInt16 outputBufferLength, + const WoopsaChar8* httpStatusCode, + const WoopsaChar8* httpStatusStr, + WoopsaUInt16* contentLengthPosition, + const WoopsaChar8* contentType + ) { + WoopsaUInt16 size = 0; + outputBuffer[0] = '\0'; + // HTTP/1.1 + size = Append(outputBuffer, HTTP_VERSION_STRING " ", outputBufferLength); + // 200 OK + size += Append(outputBuffer, httpStatusCode, outputBufferLength); + size += Append(outputBuffer, " ", outputBufferLength); + size += Append(outputBuffer, httpStatusStr, outputBufferLength); + size += Append(outputBuffer, HEADER_SEPARATOR, outputBufferLength); + // Content type + size += Append(outputBuffer, HEADER_CONTENT_TYPE, outputBufferLength); + size += Append(outputBuffer, contentType, outputBufferLength); + size += Append(outputBuffer, HEADER_SEPARATOR, outputBufferLength); + // Extra headers + size += Append(outputBuffer, EXTRA_HEADERS, outputBufferLength); + // Content-Length: + size += Append(outputBuffer, HEADER_CONTENT_LENGTH HEADER_VALUE_SEPARATOR, outputBufferLength); + *contentLengthPosition = size; + // We leave a few spaces for the Content-Length + size += Append(outputBuffer, HEADER_CONTENT_LENGTH_SPACE, outputBufferLength); + // Final double new lines + size += Append(outputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR, outputBufferLength); + return size; +} + +void SetContentLength(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaUInt16 contentLengthPosition, WoopsaUInt16 contentLength) { + WOOPSA_INTEGER_TO_PADDED_STRING(contentLength, outputBuffer + contentLengthPosition, 8); + // snprintf usually adds a null byte, remove it and put a \r instead + outputBuffer[contentLengthPosition + WOOPSA_STRING_LENGTH(HEADER_CONTENT_LENGTH_SPACE)] = '\r'; +} + +WoopsaUInt16 PrepareResponseWithContent( + WoopsaChar8* outputBuffer, + const WoopsaUInt16 outputBufferLength, + const WoopsaChar8* httpStatusCode, + const WoopsaChar8* httpStatusStr, + const WoopsaChar8* content) { + WoopsaUInt16 len, pos, contentLength; + len = PrepareResponse(outputBuffer, outputBufferLength, httpStatusCode, httpStatusStr, &pos, CONTENT_TYPE_JSON); + contentLength = WOOPSA_STRING_LENGTH(content); + SetContentLength(outputBuffer, outputBufferLength, pos, contentLength); + len += Append(outputBuffer, content, outputBufferLength); + return len; +} + +// Shortcut to generate an HTTP error. The contents +// of the response is the error string itself. +WoopsaUInt16 PrepareError(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaChar8 errorCode[], const WoopsaChar8 errorStr[]) { + return PrepareResponseWithContent(outputBuffer, outputBufferLength, errorCode, errorStr, errorStr); +} + +void StringToLower(WoopsaChar8 str[]) { + int i = 0; + for (i = 0; str[i]; i++) { + str[i] = WOOPSA_CHAR_TO_LOWER(str[i]); + } +} + +WoopsaUInt16 OutputSerializedValue(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaChar8 stringValue[], WoopsaChar8 typeString[], WoopsaChar8 isStringValue) { + WoopsaUInt16 contentLength = 0; + contentLength += Append(outputBuffer, JSON_VALUE_VALUE, outputBufferLength); +#ifdef WOOPSA_ENABLE_STRINGS + if (isStringValue) { + contentLength += Append(outputBuffer, JSON_STRING_DELIMITER, outputBufferLength); + contentLength += AppendEscape(outputBuffer, stringValue, JSON_STRING_DELIMITER_CHAR, JSON_ESCAPE_CHAR, outputBufferLength); + contentLength += Append(outputBuffer, JSON_STRING_DELIMITER, outputBufferLength); + } else { +#endif + contentLength += Append(outputBuffer, stringValue, outputBufferLength); +#ifdef WOOPSA_ENABLE_STRINGS + } +#endif + contentLength += Append(outputBuffer, JSON_VALUE_TYPE, outputBufferLength); + contentLength += Append(outputBuffer, typeString, outputBufferLength); + contentLength += Append(outputBuffer, JSON_VALUE_END, outputBufferLength); + return contentLength; +} + +WoopsaUInt16 OutputProperty(WoopsaChar8* outputBuffer, const WoopsaUInt16 outputBufferLength, WoopsaEntry* woopsaEntry, TypesDictionaryEntry* typeEntry, WoopsaChar8 numericValueBuffer[]) { + WoopsaUInt16 contentLength = 0; +#ifdef WOOPSA_ENABLE_STRINGS + if (woopsaEntry->type == WOOPSA_TYPE_TEXT + || woopsaEntry->type == WOOPSA_TYPE_LINK + || woopsaEntry->type == WOOPSA_TYPE_RESOURCE_URL + || woopsaEntry->type == WOOPSA_TYPE_DATE_TIME) { + WOOPSA_LOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, (WoopsaChar8*)woopsaEntry->address, typeEntry->string, 1); + WOOPSA_UNLOCK + } else if ( woopsaEntry->type == WOOPSA_TYPE_LOGICAL ){ + WOOPSA_LOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, *(WoopsaChar8*)(woopsaEntry->address)?JSON_TRUE:JSON_FALSE, typeEntry->string, 0); + WOOPSA_UNLOCK + } else { +#endif + WOOPSA_LOCK + if (woopsaEntry->type == WOOPSA_TYPE_INTEGER) + WOOPSA_INTEGER_TO_STRING(*(int*)woopsaEntry->address, numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + else + WOOPSA_REAL_TO_STRING(*(float*)(woopsaEntry->address), numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + WOOPSA_UNLOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, numericValueBuffer, typeEntry->string, 0); +#ifdef WOOPSA_ENABLE_STRINGS + } +#endif + return contentLength; +} + +/////////////////////////////////////////////////////////////////////////////// +// BEGIN PUBLIC WOOPSA IMPLEMENTATION // +/////////////////////////////////////////////////////////////////////////////// + +void WoopsaServerInit(WoopsaServer* server, const WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)) { + server->pathPrefix = prefix; + server->entries = entries; + server->handleRequest = handleRequest; +} + +WoopsaUInt8 WoopsaCheckRequestComplete(WoopsaServer* server, WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength) { + WoopsaUInt8 contentLengthLength = 0; + WoopsaChar8 *buffer = server->buffer, *header = NULL, *contentLengthPosition = NULL, *contentStart = NULL, *contentPosition = NULL; + WoopsaInt16 headerSize = 0, contentLength = 0, i = 0; + // Zero-out the buffer + memset(buffer, 0, sizeof(WoopsaBuffer)); + header = inputBuffer; + while ((headerSize = NextHTTPHeader(header, &header)) != -1) { + WOOPSA_STRING_N_COPY(buffer, header, headerSize); + StringToLower(buffer); + // Is there a "Content-Length" header? + if (WOOPSA_STRING_POSITION(buffer, HEADER_CONTENT_LENGTH) == buffer) { + // Yes, find the content-length + for (i = 0; buffer[i] != '\r'; i++) { + if (contentLengthPosition == NULL && buffer[i] == ':') { + contentLengthPosition = buffer + i; + } else if (contentLengthPosition != NULL && buffer[i] == ' ') { + contentLengthPosition++; + } else if (contentLengthPosition != NULL && buffer[i] != ' ') { + contentLengthLength++; + } + } + contentLengthPosition[contentLengthLength+1] = '\0'; + WOOPSA_STRING_TO_INTEGER(contentLength, contentLengthPosition); + break; + } + } + if (contentLength == 0) { + // If there is no content and we have the terminator, all done. + if (WOOPSA_STRING_POSITION(inputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR)) + return WOOPSA_REQUEST_COMLETE; + else + return WOOPSA_REQUEST_MORE_DATA_NEEDED; + } else { + // Otherwise it's a bit more technical, we have to make sure all + // the content is there + contentPosition = WOOPSA_STRING_POSITION(inputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR) + WOOPSA_STRING_LENGTH(HEADER_SEPARATOR HEADER_SEPARATOR); + if (WOOPSA_STRING_LENGTH(contentPosition) == contentLength) + return WOOPSA_REQUEST_COMLETE; + else + return WOOPSA_REQUEST_MORE_DATA_NEEDED; + } +} + +WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength, WoopsaChar8* outputBuffer, WoopsaUInt16 outputBufferLength, WoopsaUInt16* responseLength) { + WoopsaChar8* header = NULL; + WoopsaChar8* woopsaPath = NULL; + WoopsaChar8* requestContent = NULL; + WoopsaChar8 numericValueBuffer[MAX_NUMERICAL_VALUE_LENGTH]; + WoopsaChar8 oldChar = '\0'; + WoopsaUInt16 i = 0, pos = 0, contentLengthPosition = 0, contentLength = 0; + WoopsaUInt8 isPost = 0, valueFound = 0, entryAt = 0; + WoopsaInt16 headerSize = 0, keypairSize = 0; + WoopsaEntry* woopsaEntry = NULL; + TypesDictionaryEntry* typeEntry = NULL; + WoopsaChar8* buffer = server->buffer; + // Zero-out the buffers + memset(buffer, 0, sizeof(WoopsaBuffer)); + memset(numericValueBuffer, 0, MAX_NUMERICAL_VALUE_LENGTH); + // Parse the first header (GET/POST) + headerSize = NextHTTPHeader(inputBuffer, &header); + if (headerSize == -1) + return WOOPSA_CLIENT_REQUEST_ERROR; + // Copy the HTTP method in the buffer and check if POST + for (i = 0; i < headerSize && i < sizeof(WoopsaBuffer); i++) { + if (inputBuffer[i] == ' ') { + pos = i + 1; + break; + } + buffer[i] = inputBuffer[i]; + } + if (WOOPSA_STRING_EQUAL(buffer, "POST")) + isPost = 1; + // Extract the requested path into the buffer + for (i = 0; (i < headerSize - pos) && i < sizeof(WoopsaBuffer); i++) { + if (inputBuffer[pos + i] == ' ') + break; + buffer[i] = inputBuffer[pos + i]; + } + // Extract all headers (but do nothing with them) + while ((headerSize = NextHTTPHeader(header, &header)) != -1) { + // Do some work on the headers. We don't need to really do anything now actually + } + // Check if the path is a Woopsa path + if ((WOOPSA_STRING_POSITION(buffer, server->pathPrefix)) != buffer) { + // It's not, so we try to handle it with the handleRequest func pointer + if (server->handleRequest != NULL) { + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_HTML); + contentLength = server->handleRequest(buffer, isPost, outputBuffer, outputBufferLength); + if (contentLength == 0) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } else { + SetContentLength(outputBuffer, outputBufferLength, contentLengthPosition, contentLength); + *responseLength += contentLength; + return WOOPSA_OTHER_RESPONSE; + } + } else { + // This request does not start with the prefix, return 404 + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + } + // Remove the Woopsa prefix and handle each Woopsa verb + woopsaPath = &(buffer[WOOPSA_STRING_LENGTH(server->pathPrefix)]); + if (WOOPSA_STRING_POSITION(woopsaPath, VERB_META) == woopsaPath && isPost == 0) { + // Meta request, start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Output the serialized response + contentLength += Append(outputBuffer, JSON_META_PROPERTIES JSON_ARRAY_START, outputBufferLength); + for(i = 0; server->entries[i].name != NULL; i++) { + woopsaEntry = &server->entries[i]; + if (woopsaEntry->isMethod) + continue; + if ( entryAt != 0 ) + contentLength += Append(outputBuffer, JSON_ARRAY_DELIMITER, outputBufferLength); + entryAt++; + typeEntry = GetTypeEntry(woopsaEntry->type); + contentLength += Append(outputBuffer, JSON_PROPERTY_NAME, outputBufferLength); + contentLength += AppendEscape(outputBuffer, woopsaEntry->name, JSON_STRING_DELIMITER_CHAR, JSON_ESCAPE_CHAR, outputBufferLength); + contentLength += Append(outputBuffer, JSON_PROPERTY_TYPE, outputBufferLength); + contentLength += Append(outputBuffer, typeEntry->string, outputBufferLength); + contentLength += Append(outputBuffer, JSON_PROPERTY_READONLY, outputBufferLength); + contentLength += Append(outputBuffer, (woopsaEntry->readOnly == 1) ? JSON_TRUE : JSON_FALSE, outputBufferLength); + contentLength += Append(outputBuffer, JSON_PROPERTY_END, outputBufferLength); + } + contentLength += Append(outputBuffer, JSON_ARRAY_END JSON_META_METHODS JSON_ARRAY_START, outputBufferLength); +#ifdef WOOPSA_ENABLE_METHODS + entryAt = 0; + for (i = 0; server->entries[i].name != NULL; i++) { + woopsaEntry = &server->entries[i]; + if (!woopsaEntry->isMethod) + continue; + if (entryAt != 0) + contentLength += Append(outputBuffer, JSON_ARRAY_DELIMITER, outputBufferLength); + entryAt++; + typeEntry = GetTypeEntry(woopsaEntry->type); + contentLength += Append(outputBuffer, JSON_METHOD_NAME, outputBufferLength); + contentLength += AppendEscape(outputBuffer, woopsaEntry->name, JSON_STRING_DELIMITER_CHAR, JSON_ESCAPE_CHAR, outputBufferLength); + contentLength += Append(outputBuffer, JSON_METHOD_RETURN_TYPE, outputBufferLength); + contentLength += Append(outputBuffer, typeEntry->string, outputBufferLength); + contentLength += Append(outputBuffer, JSON_METHOD_END, outputBufferLength); + } +#endif + contentLength += Append(outputBuffer, JSON_ARRAY_END JSON_META_END, outputBufferLength); + } else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_READ) == woopsaPath && isPost == 0) { + // Read request - Get the property for this read + buffer[0] = '\0'; + woopsaPath = &(woopsaPath[sizeof(VERB_READ)]); + if ((woopsaEntry = GetPropertyByNameOrNull(server->entries, woopsaPath)) == NULL) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + typeEntry = GetTypeEntry(woopsaEntry->type); + // Start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Output the serialized response + contentLength += OutputProperty(outputBuffer, outputBufferLength, woopsaEntry, typeEntry, numericValueBuffer); + } else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_WRITE) == woopsaPath && isPost == 1) { + // Write request - Get the property for this write + buffer[0] = '\0'; + woopsaPath = &(woopsaPath[sizeof(VERB_WRITE)]); + if ((woopsaEntry = GetPropertyByNameOrNull(server->entries, woopsaPath)) == NULL) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + typeEntry = GetTypeEntry(woopsaEntry->type); + // Decode POST data + requestContent = WOOPSA_STRING_POSITION(inputBuffer, "\r\n\r\n"); + requestContent = &requestContent[4]; + // Cheating here - using the numericValueBuffer to store the key (10 chars should be enough) + while ((keypairSize = NextURLDecodedValue(&requestContent[keypairSize], numericValueBuffer, buffer)) != -1) { + if (WOOPSA_STRING_EQUAL(numericValueBuffer, POST_VALUE_KEY)) { + valueFound = 1; + } + } + if (!valueFound) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_BAD_REQUEST, HTTP_TEXT_BAD_REQUEST); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + // Write the value + if (woopsaEntry->type == WOOPSA_TYPE_INTEGER) { + WOOPSA_LOCK + WOOPSA_STRING_TO_INTEGER(*(int*)woopsaEntry->address, buffer); + WOOPSA_UNLOCK + } else if (woopsaEntry->type == WOOPSA_TYPE_REAL || woopsaEntry->type == WOOPSA_TYPE_TIME_SPAN) { + WOOPSA_LOCK + WOOPSA_STRING_TO_FLOAT(*(float*)woopsaEntry->address, buffer); + WOOPSA_UNLOCK + } + else if (woopsaEntry->type == WOOPSA_TYPE_LOGICAL) { + StringToLower(buffer); + WOOPSA_LOCK + if (WOOPSA_STRING_EQUAL(buffer, JSON_TRUE)) + *(char*)woopsaEntry->address = 1; + else + *(char*)woopsaEntry->address = 0; + WOOPSA_UNLOCK + } +#ifdef WOOPSA_ENABLE_STRINGS + else + { + if (woopsaEntry->size > WOOPSA_STRING_LENGTH(buffer)) { + WOOPSA_LOCK + WOOPSA_STRING_COPY((char*)woopsaEntry->address, buffer); + WOOPSA_UNLOCK + } else { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_BAD_REQUEST, HTTP_TEXT_BAD_REQUEST); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + } +#endif + // Start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Output the serialized response + contentLength += OutputProperty(outputBuffer, outputBufferLength, woopsaEntry, typeEntry, numericValueBuffer); + } +#ifdef WOOPSA_ENABLE_METHODS + else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_INVOKE) == woopsaPath && isPost == 1) + { + // Write request - Get the property for this write + buffer[0] = '\0'; + woopsaPath = &(woopsaPath[sizeof(VERB_INVOKE)]); + if ((woopsaEntry = GetMethodByNameOrNull(server->entries, woopsaPath)) == NULL) { + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + typeEntry = GetTypeEntry(woopsaEntry->type); + // Start the HTTP response + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); + // Invoke the method + if (woopsaEntry->type == WOOPSA_TYPE_NULL) { + (*(ptrMethodVoid)woopsaEntry->address)(); + contentLength = 0; + } else { + if (woopsaEntry->type == WOOPSA_TYPE_TEXT + || woopsaEntry->type == WOOPSA_TYPE_LINK + || woopsaEntry->type == WOOPSA_TYPE_RESOURCE_URL + || woopsaEntry->type == WOOPSA_TYPE_DATE_TIME) { + WOOPSA_LOCK + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, (*(ptrMethodRetString)woopsaEntry->address)(), typeEntry->string, 1); + WOOPSA_UNLOCK + } else { + if (woopsaEntry->type == WOOPSA_TYPE_INTEGER) { + WOOPSA_LOCK + WOOPSA_INTEGER_TO_STRING((*(ptrMethodRetInteger)woopsaEntry->address)(), numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + WOOPSA_UNLOCK + } else { + WOOPSA_LOCK + WOOPSA_REAL_TO_STRING((*(ptrMethodRetReal)woopsaEntry->address)(), numericValueBuffer, MAX_NUMERICAL_VALUE_LENGTH); + WOOPSA_UNLOCK + } + contentLength += OutputSerializedValue(outputBuffer, outputBufferLength, numericValueBuffer, typeEntry->string, 0); + } + } + } +#endif + else + { + // Invalid request + *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); + return WOOPSA_CLIENT_REQUEST_ERROR; + } + // Re-inject the content-length into the HTTP headers + SetContentLength(outputBuffer, outputBufferLength, contentLengthPosition, contentLength); + *responseLength += contentLength; + return WOOPSA_SUCCESS; +} \ No newline at end of file diff --git a/Sources/Embedded/ArduinoDemoServer/woopsa-server.h b/Sources/Embedded/ArduinoDemoServer/woopsa-server.h new file mode 100644 index 0000000..ca0f976 --- /dev/null +++ b/Sources/Embedded/ArduinoDemoServer/woopsa-server.h @@ -0,0 +1,121 @@ +#ifndef __WOOPSA_H_ +#define __WOOPSA_H_ + +#include "woopsa-config.h" + +typedef short WoopsaInt16; +typedef unsigned short WoopsaUInt16; +typedef char WoopsaChar8; +typedef unsigned char WoopsaUInt8; +typedef void * WoopsaVoidPtr; + +typedef WoopsaChar8 WoopsaBuffer[WOOPSA_BUFFER_SIZE]; + +#ifdef WOOPSA_ENABLE_METHODS + typedef void(*ptrMethodVoid)(void); + typedef char*(*ptrMethodRetString)(void); + typedef int*(*ptrMethodRetInteger)(void); + typedef float*(*ptrMethodRetReal)(void); +#endif + +// JsonData is not supported in Woopsa-C +typedef enum { + WOOPSA_TYPE_NULL, + WOOPSA_TYPE_LOGICAL, + WOOPSA_TYPE_INTEGER, + WOOPSA_TYPE_REAL, + WOOPSA_TYPE_TIME_SPAN, +#ifdef WOOPSA_ENABLE_STRINGS + WOOPSA_TYPE_DATE_TIME, + WOOPSA_TYPE_TEXT, + WOOPSA_TYPE_LINK, + WOOPSA_TYPE_RESOURCE_URL +#endif +} WoopsaType; + +typedef struct { + WoopsaChar8 * name; + void* address; + WoopsaUInt8 type; + WoopsaChar8 readOnly; + WoopsaChar8 isMethod; + WoopsaUInt8 size; +} WoopsaEntry; + +typedef struct { + // The prefix for all Woopsa routes. Any client request + // made without this prefix will pass the request to + // the handleRequest function, if it exists + const WoopsaChar8 * pathPrefix; + // A function pointer to a function that accepts client + // requests with a specified path and POST. + // The function is in charge of copying content to the + // content string, and should return an amount of bytes. + // If the amount of bytes is 0, then the server will send + // a 404 Not Found error. + WoopsaUInt16 (*handleRequest)(WoopsaChar8* requestedPath, WoopsaUInt8 isPost, WoopsaChar8* outputBuffer, WoopsaUInt16 outputBufferSize); + // A list of entries to be published by Woopsa + WoopsaEntry* entries; + // A small buffer string used in various places in the server + WoopsaBuffer buffer; +} WoopsaServer; + +#define WOOPSA_BEGIN(woopsaDictionaryName) \ + WoopsaEntry woopsaDictionaryName[] = \ + { + +#define WOOPSA_END \ + { NULL, NULL, NULL, NULL, NULL, NULL }}; + +#define WOOPSA_PROPERTY_CUSTOM(variable, type, readonly) \ + { #variable, &variable, type, readonly, 0, sizeof variable }, + +#define WOOPSA_PROPERTY_READONLY(variable, type) \ + WOOPSA_PROPERTY_CUSTOM(variable, type, 1) + +#define WOOPSA_PROPERTY(variable, type) \ + WOOPSA_PROPERTY_CUSTOM(variable, type, 0) + +#ifdef WOOPSA_ENABLE_METHODS + #define WOOPSA_METHOD(method, returnType) \ + { #method, (void*)method, returnType, 0, 1, 0 }, + + #define WOOPSA_METHOD_VOID(method) \ + WOOPSA_METHOD(method, WOOPSA_TYPE_NULL) +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +// Creates a new Woopsa server using the specified prefix +// and a list of entries to publish +void WoopsaServerInit(WoopsaServer* server, const WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)); + +// Checks if the request contained in inputBuffer +// is finished. This is useful in the case where +// data is received in fragments for some reason. +// You should always call this method on your buffer +// before passing it to WoopsaHandleRequest +#define WOOPSA_REQUEST_MORE_DATA_NEEDED 0 +#define WOOPSA_REQUEST_COMLETE 1 +WoopsaUInt8 WoopsaCheckRequestComplete(WoopsaServer* server, WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength); + +// Parses a request and prepares the reply as well +// Returns: +// WOOPSA_SUCCESS (0) = success +// WOOPSA_CLIENT_REQUEST_ERROR (1) = the client made a bad request +// WOPOSA_OTHER_ERROR (2) = something wrong happened inside Woopsa (it's our fault) +#define WOOPSA_SUCCESS 0 +#define WOOPSA_CLIENT_REQUEST_ERROR 1 +#define WOOPSA_OTHER_ERROR 2 +#define WOOPSA_OTHER_RESPONSE 3 + +WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength, WoopsaChar8* outputBuffer, WoopsaUInt16 outputBufferLength, WoopsaUInt16* responseLength); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/Sources/Embedded/DemoServer/DemoServer.c b/Sources/Embedded/DemoServer/DemoServer.c index a8e7999..3e1fc76 100644 --- a/Sources/Embedded/DemoServer/DemoServer.c +++ b/Sources/Embedded/DemoServer/DemoServer.c @@ -84,6 +84,12 @@ WOOPSA_END; #define WOOPSA_PORT 8000 #define BUFFER_SIZE 1024 + +WoopsaUInt16 ServeHTML(WoopsaChar8 path[], WoopsaUInt8 isPost, WoopsaChar8 dataBuffer[], WoopsaUInt16 dataBufferSize) { + strcpy(dataBuffer, "Hello world!"); + return strlen("Hellow world!"); +} + int main(int argc, char argv[]) { SOCKET sock, clientSock; struct sockaddr_in addr, clientAddr; @@ -141,7 +147,7 @@ int main(int argc, char argv[]) { break; } - if (WoopsaCheckRequestComplete(&server, buffer, sizeof(buffer)) != 1) { + if (WoopsaCheckRequestComplete(&server, buffer, sizeof(buffer)) != WOOPSA_REQUEST_COMLETE) { // If the request is not complete, it means more data needs // to be -added- to the buffer continue; diff --git a/Sources/Embedded/Server/Server.vcxproj b/Sources/Embedded/Server/Server.vcxproj index 7c36855..30c96ae 100644 --- a/Sources/Embedded/Server/Server.vcxproj +++ b/Sources/Embedded/Server/Server.vcxproj @@ -40,6 +40,8 @@ false + xcopy "$(ProjectDir)*.h" "$(ProjectDir)..\ArduinoDemoServer\" +xcopy "$(ProjectDir)*.c" "$(ProjectDir)..\ArduinoDemoServer\" diff --git a/Sources/Embedded/Server/woopsa-server.c b/Sources/Embedded/Server/woopsa-server.c index 95f2c23..7310560 100644 --- a/Sources/Embedded/Server/woopsa-server.c +++ b/Sources/Embedded/Server/woopsa-server.c @@ -64,7 +64,10 @@ TypesDictionaryEntry Types[] = { #define HEADER_CONTENT_LENGTH "content-length" #define HEADER_CONTENT_LENGTH_SPACE " " #define HEADER_CONTENT_LENGTH_PADDING 8 -#define EXTRA_HEADERS "Content-Type: application/json" HEADER_SEPARATOR "Access-Control-Allow-Origin: *" HEADER_SEPARATOR "Connection: close" HEADER_SEPARATOR +#define HEADER_CONTENT_TYPE "Content-Type: " +#define EXTRA_HEADERS "Access-Control-Allow-Origin: *" HEADER_SEPARATOR "Connection: close" HEADER_SEPARATOR +#define CONTENT_TYPE_JSON "application/json" +#define CONTENT_TYPE_HTML "text/html" // POST constants #define POST_VALUE_KEY "value" @@ -266,7 +269,9 @@ WoopsaUInt16 PrepareResponse( const WoopsaUInt16 outputBufferLength, const WoopsaChar8* httpStatusCode, const WoopsaChar8* httpStatusStr, - WoopsaUInt16* contentLengthPosition) { + WoopsaUInt16* contentLengthPosition, + const WoopsaChar8* contentType + ) { WoopsaUInt16 size = 0; outputBuffer[0] = '\0'; // HTTP/1.1 @@ -276,6 +281,10 @@ WoopsaUInt16 PrepareResponse( size += Append(outputBuffer, " ", outputBufferLength); size += Append(outputBuffer, httpStatusStr, outputBufferLength); size += Append(outputBuffer, HEADER_SEPARATOR, outputBufferLength); + // Content type + size += Append(outputBuffer, HEADER_CONTENT_TYPE, outputBufferLength); + size += Append(outputBuffer, contentType, outputBufferLength); + size += Append(outputBuffer, HEADER_SEPARATOR, outputBufferLength); // Extra headers size += Append(outputBuffer, EXTRA_HEADERS, outputBufferLength); // Content-Length: @@ -301,7 +310,7 @@ WoopsaUInt16 PrepareResponseWithContent( const WoopsaChar8* httpStatusStr, const WoopsaChar8* content) { WoopsaUInt16 len, pos, contentLength; - len = PrepareResponse(outputBuffer, outputBufferLength, httpStatusCode, httpStatusStr, &pos); + len = PrepareResponse(outputBuffer, outputBufferLength, httpStatusCode, httpStatusStr, &pos, CONTENT_TYPE_JSON); contentLength = WOOPSA_STRING_LENGTH(content); SetContentLength(outputBuffer, outputBufferLength, pos, contentLength); len += Append(outputBuffer, content, outputBufferLength); @@ -374,7 +383,7 @@ WoopsaUInt16 OutputProperty(WoopsaChar8* outputBuffer, const WoopsaUInt16 output // BEGIN PUBLIC WOOPSA IMPLEMENTATION // /////////////////////////////////////////////////////////////////////////////// -void WoopsaServerInit(WoopsaServer* server, WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)) { +void WoopsaServerInit(WoopsaServer* server, const WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)) { server->pathPrefix = prefix; server->entries = entries; server->handleRequest = handleRequest; @@ -410,17 +419,17 @@ WoopsaUInt8 WoopsaCheckRequestComplete(WoopsaServer* server, WoopsaChar8* inputB if (contentLength == 0) { // If there is no content and we have the terminator, all done. if (WOOPSA_STRING_POSITION(inputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR)) - return 1; + return WOOPSA_REQUEST_COMLETE; else - return 0; + return WOOPSA_REQUEST_MORE_DATA_NEEDED; } else { // Otherwise it's a bit more technical, we have to make sure all // the content is there contentPosition = WOOPSA_STRING_POSITION(inputBuffer, HEADER_SEPARATOR HEADER_SEPARATOR) + WOOPSA_STRING_LENGTH(HEADER_SEPARATOR HEADER_SEPARATOR); if (WOOPSA_STRING_LENGTH(contentPosition) == contentLength) - return 1; + return WOOPSA_REQUEST_COMLETE; else - return 0; + return WOOPSA_REQUEST_MORE_DATA_NEEDED; } } @@ -467,7 +476,7 @@ WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBu if ((WOOPSA_STRING_POSITION(buffer, server->pathPrefix)) != buffer) { // It's not, so we try to handle it with the handleRequest func pointer if (server->handleRequest != NULL) { - *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition); + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_HTML); contentLength = server->handleRequest(buffer, isPost, outputBuffer, outputBufferLength); if (contentLength == 0) { *responseLength = PrepareError(outputBuffer, outputBufferLength, HTTP_CODE_NOT_FOUND, HTTP_TEXT_NOT_FOUND); @@ -487,7 +496,7 @@ WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBu woopsaPath = &(buffer[WOOPSA_STRING_LENGTH(server->pathPrefix)]); if (WOOPSA_STRING_POSITION(woopsaPath, VERB_META) == woopsaPath && isPost == 0) { // Meta request, start the HTTP response - *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition); + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); // Output the serialized response contentLength += Append(outputBuffer, JSON_META_PROPERTIES JSON_ARRAY_START, outputBufferLength); for(i = 0; server->entries[i].name != NULL; i++) { @@ -535,7 +544,7 @@ WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBu } typeEntry = GetTypeEntry(woopsaEntry->type); // Start the HTTP response - *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition); + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); // Output the serialized response contentLength += OutputProperty(outputBuffer, outputBufferLength, woopsaEntry, typeEntry, numericValueBuffer); } else if (WOOPSA_STRING_POSITION(woopsaPath, VERB_WRITE) == woopsaPath && isPost == 1) { @@ -593,7 +602,7 @@ WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBu } #endif // Start the HTTP response - *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition); + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); // Output the serialized response contentLength += OutputProperty(outputBuffer, outputBufferLength, woopsaEntry, typeEntry, numericValueBuffer); } @@ -609,7 +618,7 @@ WoopsaUInt8 WoopsaHandleRequest(WoopsaServer* server, const WoopsaChar8* inputBu } typeEntry = GetTypeEntry(woopsaEntry->type); // Start the HTTP response - *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition); + *responseLength = PrepareResponse(outputBuffer, outputBufferLength, HTTP_CODE_OK, HTTP_TEXT_OK, &contentLengthPosition, CONTENT_TYPE_JSON); // Invoke the method if (woopsaEntry->type == WOOPSA_TYPE_NULL) { (*(ptrMethodVoid)woopsaEntry->address)(); diff --git a/Sources/Embedded/Server/woopsa-server.h b/Sources/Embedded/Server/woopsa-server.h index b5e4ce3..ca0f976 100644 --- a/Sources/Embedded/Server/woopsa-server.h +++ b/Sources/Embedded/Server/woopsa-server.h @@ -34,7 +34,7 @@ typedef enum { } WoopsaType; typedef struct { - const WoopsaChar8 * name; + WoopsaChar8 * name; void* address; WoopsaUInt8 type; WoopsaChar8 readOnly; @@ -46,7 +46,7 @@ typedef struct { // The prefix for all Woopsa routes. Any client request // made without this prefix will pass the request to // the handleRequest function, if it exists - WoopsaChar8 * pathPrefix; + const WoopsaChar8 * pathPrefix; // A function pointer to a function that accepts client // requests with a specified path and POST. // The function is in charge of copying content to the @@ -91,13 +91,15 @@ extern "C" // Creates a new Woopsa server using the specified prefix // and a list of entries to publish -void WoopsaServerInit(WoopsaServer* server, WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)); +void WoopsaServerInit(WoopsaServer* server, const WoopsaChar8* prefix, WoopsaEntry entries[], WoopsaUInt16(*handleRequest)(WoopsaChar8*, WoopsaUInt8, WoopsaChar8*, WoopsaUInt16)); // Checks if the request contained in inputBuffer // is finished. This is useful in the case where // data is received in fragments for some reason. // You should always call this method on your buffer // before passing it to WoopsaHandleRequest +#define WOOPSA_REQUEST_MORE_DATA_NEEDED 0 +#define WOOPSA_REQUEST_COMLETE 1 WoopsaUInt8 WoopsaCheckRequestComplete(WoopsaServer* server, WoopsaChar8* inputBuffer, WoopsaUInt16 inputBufferLength); // Parses a request and prepares the reply as well diff --git a/Sources/Embedded/WoopsaC.sln b/Sources/Embedded/WoopsaEmbedded.sln similarity index 100% rename from Sources/Embedded/WoopsaC.sln rename to Sources/Embedded/WoopsaEmbedded.sln diff --git a/Sources/Embedded/build-windows.bat b/Sources/Embedded/build-windows.bat index ab7d9fa..5571b9f 100644 --- a/Sources/Embedded/build-windows.bat +++ b/Sources/Embedded/build-windows.bat @@ -1 +1,2 @@ -devenv WoopsaC.sln /build Release /project DemoServer \ No newline at end of file +devenv WoopsaEmbedded.sln /build Release /project Server +devenv WoopsaEmbedded.sln /build Release /project DemoServer \ No newline at end of file diff --git a/Sources/JavaScript/demo/demo.js b/Sources/JavaScript/demo/demo.js index a816bce..74375bf 100644 --- a/Sources/JavaScript/demo/demo.js +++ b/Sources/JavaScript/demo/demo.js @@ -13,8 +13,6 @@ $("#urlForm").submit(function (){ woopsa = new WoopsaClient($("#serverUrl").val(), jQuery); - woopsa.username = "admin"; - woopsa.password = "password"; //The very easy way of making a subscription woopsa.onChange($("#subscribePath").val(), function (value){ diff --git a/make-release-windows.bat b/make-release-windows.bat index 7fc885c..1835463 100644 --- a/make-release-windows.bat +++ b/make-release-windows.bat @@ -13,8 +13,7 @@ set DOT_NET_CLIENT_EXAMPLE_DIR=WoopsaDemoClient set JAVASCRIPT_RELEASE_DIR=JavaScript set JAVASCRIPT_PROJECT_DIR=Sources\JavaScript set EMBEDDED_RELEASE_DIR=Embedded -set EMBEDDED_SOURCES_DIR=Sources\Embedded -set EMBEDDED_PROJECT_DIR=Server +set EMBEDDED_PROJECT_DIR=Sources\Embedded set EMBEDDED_EXAMPLE_DIR=Release REM .NET Library @@ -42,4 +41,12 @@ if not exist %RELEASE_DIR%\%JAVASCRIPT_RELEASE_DIR% mkdir %RELEASE_DIR%\%JAVASCR cd %JAVASCRIPT_PROJECT_DIR% call build-windows.bat cd ..\.. -copy %JAVASCRIPT_PROJECT_DIR%\dist\* %RELEASE_DIR%\%JAVASCRIPT_RELEASE_DIR%\ \ No newline at end of file +copy %JAVASCRIPT_PROJECT_DIR%\dist\* %RELEASE_DIR%\%JAVASCRIPT_RELEASE_DIR%\ + +REM Embedded library +if not exist %RELEASE_DIR%\%EMBEDDED_RELEASE_DIR% mkdir %RELEASE_DIR%\%EMBEDDED_RELEASE_DIR% +cd %EMBEDDED_PROJECT_DIR% +call build-windows.bat +cd ..\.. +copy %EMBEDDED_PROJECT_DIR%\Server\*.h %RELEASE_DIR%\%EMBEDDED_RELEASE_DIR%\ +copy %EMBEDDED_PROJECT_DIR%\Server\*.c %RELEASE_DIR%\%EMBEDDED_RELEASE_DIR%\ \ No newline at end of file