diff --git a/firmware/HttpClient.cpp b/firmware/HttpClient.cpp index 3f4bb94..082a754 100644 --- a/firmware/HttpClient.cpp +++ b/firmware/HttpClient.cpp @@ -1,4 +1,5 @@ #include "HttpClient.h" +#include "IChunkStream.h" static const uint16_t DEFAULT_TIMEOUT = 5000; // Allow maximum 5s between data packets. @@ -284,3 +285,231 @@ void HttpClient::request(http_request_t &aRequest, http_response_t &aResponse, h // Return the entire message body from bodyPos+4 till end. aResponse.body = buffer; } + +/** +* Method to send an HTTP Request. Allocate variables in your application code +* in the aResponse struct and set the headers and the options in the aRequest +* struct. +*/ +void HttpClient::request(http_request_t &aRequest, http_response_t &aResponse, http_header_t headers[], const char* aHttpMethod, IChunkStream* stream) +{ + // If a proper response code isn't received it will be set to -1. + aResponse.status = -1; + + // NOTE: The default port tertiary statement is unpredictable if the request structure is not initialised + // http_request_t request = {0} or memset(&request, 0, sizeof(http_request_t)) should be used + // to ensure all fields are zero + bool connected = false; + if(aRequest.hostname!=NULL) { + connected = client.connect(aRequest.hostname.c_str(), (aRequest.port) ? aRequest.port : 80 ); + } else { + connected = client.connect(aRequest.ip, aRequest.port); + } + + #ifdef LOGGING + if (connected) { + if(aRequest.hostname!=NULL) { + Serial.print("HttpClient>\tConnecting to: "); + Serial.print(aRequest.hostname); + } else { + Serial.print("HttpClient>\tConnecting to IP: "); + Serial.print(aRequest.ip); + } + Serial.print(":"); + Serial.println(aRequest.port); + } else { + Serial.println("HttpClient>\tConnection failed."); + } + #endif + + if (!connected) { + client.stop(); + // If TCP Client can't connect to host, exit here. + return; + } + + // + // Send HTTP Headers + // + + // Send initial headers (only HTTP 1.0 is supported for now). + client.print(aHttpMethod); + client.print(" "); + client.print(aRequest.path); + client.print(" HTTP/1.0\r\n"); + + #ifdef LOGGING + Serial.println("HttpClient>\tStart of HTTP Request."); + Serial.print(aHttpMethod); + Serial.print(" "); + Serial.print(aRequest.path); + Serial.print(" HTTP/1.0\r\n"); + #endif + + // Send General and Request Headers. + sendHeader("Connection", "close"); // Not supporting keep-alive for now. + if(aRequest.hostname!=NULL) { + sendHeader("HOST", aRequest.hostname.c_str()); + } + + //Send Entity Headers + // TODO: Check the standard, currently sending Content-Length : 0 for empty + // POST requests, and no content-length for other types. + + sendHeader("Content-Length", stream->getSize()); + + if (headers != NULL) + { + int i = 0; + while (headers[i].header != NULL) + { + if (headers[i].value != NULL) { + sendHeader(headers[i].header, headers[i].value); + } else { + sendHeader(headers[i].header); + } + i++; + } + } + + // Empty line to finish headers + client.println(); + client.flush(); + + + //Send all chunks + while (!stream->Eof()){ + client.print(stream->getNextChunk()); + } + + #ifdef LOGGING + Serial.println("HttpClient>\tEnd of HTTP Request."); + #endif + + // clear response buffer + memset(&buffer[0], 0, sizeof(buffer)); + + + // + // Receive HTTP Response + // + // The first value of client.available() might not represent the + // whole response, so after the first chunk of data is received instead + // of terminating the connection there is a delay and another attempt + // to read data. + // The loop exits when the connection is closed, or if there is a + // timeout or an error. + + unsigned int bufferPosition = 0; + unsigned long lastRead = millis(); + unsigned long firstRead = millis(); + bool error = false; + bool timeout = false; + uint16_t actualTimeout = aRequest.timeout == 0 ? DEFAULT_TIMEOUT : aRequest.timeout; + char lastChar = 0; + bool inHeaders = true; + + do { + #ifdef LOGGING + int bytes = client.available(); + if(bytes) { + Serial.print("\r\nHttpClient>\tReceiving TCP transaction of "); + Serial.print(bytes); + Serial.println(" bytes."); + } + #endif + + while (client.available()) { + char c = client.read(); + #ifdef LOGGING + Serial.print(c); + #endif + lastRead = millis(); + + if (c == -1) { + error = true; + + #ifdef LOGGING + Serial.println("HttpClient>\tError: No data available."); + #endif + + break; + } + + if (inHeaders) { + if ((c == '\n') && (lastChar == '\n')) { + // End of headers. Grab the status code and reset the buffer. + aResponse.status = atoi(&buffer[9]); + + memset(&buffer[0], 0, sizeof(buffer)); + bufferPosition = 0; + inHeaders = false; + #ifdef LOGGING + Serial.print("\r\nHttpClient>\tEnd of HTTP Headers ("); + Serial.print(aResponse.status); + Serial.println(")"); + #endif + continue; + } else if (c != '\r') { + lastChar = c; + } + } + + // Check that received character fits in buffer before storing. + if (bufferPosition < sizeof(buffer)-1) { + buffer[bufferPosition] = c; + } else if ((bufferPosition == sizeof(buffer)-1)) { + buffer[bufferPosition] = '\0'; // Null-terminate buffer + client.stop(); + error = true; + + #ifdef LOGGING + Serial.println("\r\nHttpClient>\tError: Response body larger than buffer."); + #endif + break; + } + bufferPosition++; + } + // We don't need to null terminate the buffer since it was zeroed to start with, or null terminated when it reached capacity. + + #ifdef LOGGING + if (bytes) { + Serial.print("\r\nHttpClient>\tEnd of TCP transaction."); + } + #endif + + // Check that there hasn't been more than 5s since last read. + timeout = millis() - lastRead > actualTimeout; + + // Unless there has been an error or timeout wait 200ms to allow server + // to respond or close connection. + if (!error && !timeout) { + delay(200); + } + } while (client.connected() && !timeout && !error); + + #ifdef LOGGING + if (timeout) { + Serial.println("\r\nHttpClient>\tError: Timeout while reading response."); + } + Serial.print("\r\nHttpClient>\tEnd of HTTP Response ("); + Serial.print(millis() - firstRead); + Serial.println("ms)."); + #endif + client.stop(); + + #ifdef LOGGING + Serial.print("HttpClient>\tStatus Code: "); + Serial.println(aResponse.status); + #endif + + if (inHeaders) { + #ifdef LOGGING + Serial.println("HttpClient>\tError: Can't find HTTP response body."); + #endif + + return; + } + // Return the entire message body from bodyPos+4 till end. + aResponse.body = buffer; +} diff --git a/firmware/HttpClient.h b/firmware/HttpClient.h index b143547..1767db5 100644 --- a/firmware/HttpClient.h +++ b/firmware/HttpClient.h @@ -5,6 +5,7 @@ #include "spark_wiring_string.h" #include "spark_wiring_tcpclient.h" #include "spark_wiring_usbserial.h" +#include "IChunkStream.h" /** * Defines for the HTTP methods. @@ -101,7 +102,12 @@ class HttpClient { { request(aRequest, aResponse, headers, HTTP_METHOD_POST); } - + + void post(http_request_t &aRequest, http_response_t &aResponse, http_header_t headers[], IChunkStream* stream) + { + request(aRequest, aResponse, headers, HTTP_METHOD_POST, stream); + } + void put(http_request_t &aRequest, http_response_t &aResponse, http_header_t headers[]) { request(aRequest, aResponse, headers, HTTP_METHOD_PUT); @@ -122,6 +128,7 @@ class HttpClient { * Underlying HTTP methods. */ void request(http_request_t &aRequest, http_response_t &aResponse, http_header_t headers[], const char* aHttpMethod); + void request(http_request_t &aRequest, http_response_t &aResponse, http_header_t headers[], const char* aHttpMethod, IChunkStream* stream); void sendHeader(const char* aHeaderName, const char* aHeaderValue); void sendHeader(const char* aHeaderName, const int aHeaderValue); void sendHeader(const char* aHeaderName); diff --git a/firmware/IChunkStream.h b/firmware/IChunkStream.h new file mode 100644 index 0000000..d76dc37 --- /dev/null +++ b/firmware/IChunkStream.h @@ -0,0 +1,12 @@ +#ifndef __I_CHUNK_STREAM_H_ +#define __I_CHUNK_STREAM_H_ + +class IChunkStream { + public: + virtual ~IChunkStream() {} + virtual int getSize() = 0; + virtual String getNextChunk() = 0; + virtual int Eof() = 0; +}; + +#endif /* __I_CHUNK_STREAM_H_ */ \ No newline at end of file