From 1c1456b12000a1282c9046be8be9aa4625f312a2 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 24 May 2018 14:40:24 +0300 Subject: [PATCH 1/2] Added support for static content like HTML pages as well as XML return for example --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50358df --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +*.suo From f3c5d79e67184d9afb3bb9990aa1e91637a9fec0 Mon Sep 17 00:00:00 2001 From: Laurent Ellerbach Date: Thu, 24 May 2018 14:40:57 +0300 Subject: [PATCH 2/2] Added support for static content like HTML page or returning XML documents --- .gitignore | 6 + README.md | 6 + aREST.h | 164 +++++++++++++++++++------ examples/ESP8266_page/ESP8266_page.ino | 101 +++++++++++++++ 4 files changed, 239 insertions(+), 38 deletions(-) create mode 100644 examples/ESP8266_page/ESP8266_page.ino diff --git a/.gitignore b/.gitignore index 50358df..1452900 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,9 @@ ################################################################################ *.suo +*.db +*.opendb +/.vs +/.vscode +/examples/ESP8266_page/.vs +/examples/ESP8266_page/.vscode diff --git a/README.md b/README.md index 1bac5cb..ac0a296 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,12 @@ You can also define your own functions in your sketch that can be called using t * `rest.function("led",ledControl);` declares the function in the Arduino sketch * `/led?params=0` executes the function +### Pages + +Like for the functions, you can also define your own String content in your sketch that can be called using the REST API. This can be useful is you want to return an XML file or a simple HTML page. To access a page defined in your sketch, you have to declare it first, and then call it from with a REST call. Note that all functions needs to take a String as the unique argument (for parameters to be passed to the function) and return a String. Note that during the declaration, the page need to specify the content type that will be returned. For example, if your aREST instance is called "rest" and the function "html": + * `rest.function("thml","text/hml",ledControl);` declares the page in the Arduino sketch + * `/html?params=0` executes the page + ### Get data about the board You can also access a description of all the variables that were declared on the board with a single command. This is useful to automatically build graphical interfaces based on the variables exposed to the API. This can be done via the following calls: diff --git a/aREST.h b/aREST.h index 0f1e33b..3bc47bf 100644 --- a/aREST.h +++ b/aREST.h @@ -141,6 +141,10 @@ #define NUMBER_FUNCTIONS AREST_NUMBER_FUNCTIONS #endif +#ifdef AREST_NUMBER_PAGES +#define NUMBER_PAGES AREST_NUMBER_PAGES +#endif + // Default number of max. exposed variables #ifndef NUMBER_VARIABLES #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(CORE_WILDFIRE) || defined(ESP8266)|| defined(ESP32) || !defined(ADAFRUIT_CC3000_H) @@ -159,6 +163,14 @@ #endif #endif +// Default number of max. exposed pages +#ifndef NUMBER_PAGES + #if defined(__AVR_ATmega1280__) || defined(ESP32) || defined(__AVR_ATmega2560__) || defined(CORE_WILDFIRE) || defined(ESP8266) + #define NUMBER_PAGES 10 + #else + #define NUMBER_PAGES 5 + #endif +#endif #ifdef AREST_BUFFER_SIZE #define OUTPUT_BUFFER_SIZE AREST_BUFFER_SIZE @@ -375,10 +387,46 @@ void addToBufferF(const __FlashStringHelper *toAdd){ } } +void addStringToBuffer(const char * toAdd, bool quotable){ + + if (DEBUG_MODE) { + #if defined(ESP8266)|| defined (ESP32) + Serial.print("Memory loss:"); + Serial.println(freeMemory - ESP.getFreeHeap(),DEC); + freeMemory = ESP.getFreeHeap(); + #endif + Serial.print(F("Added to buffer as char: ")); + Serial.println(toAdd); + } + + if(quotable) { + addQuote(); + } + + for (int i = 0; i < strlen(toAdd) && index < OUTPUT_BUFFER_SIZE; i++, index++) { + // Handle quoting quotes and backslashes + if(quotable && (toAdd[i] == '"' || toAdd[i] == '\\')) { + if(index == OUTPUT_BUFFER_SIZE - 1) // No room! + return; + buffer[index] = '\\'; + index++; + } + + buffer[index] = toAdd[i]; + } + + if(quotable) { + addQuote(); + } +} + + // Send HTTP headers for Ethernet & WiFi void send_http_headers(){ - addToBufferF(F("HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, PUT, OPTIONS\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n")); + addToBufferF(F("HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, PUT, OPTIONS\r\nContent-Type: ")); + addStringToBuffer(content_type.c_str(), false); + addToBufferF(F("\r\nConnection: close\r\n\r\n")); } @@ -1137,6 +1185,47 @@ void process(char c) { } } + // Check if page name is in array + for (uint8_t i = 0; i < pages_index; i++) { + if (answer.startsWith(pages_names[i])) { + + // End here + pin_selected = true; + state = 'x'; + + // Set state + command = 'p'; + value = i; + + content_type = pages_content_type[i]; + + answer.trim(); + + // We're expecting a string of the form ?xxxxx=, where xxxxx can be almost anything as long as it's followed by an '=' + // Get command -- Anything following the first '=' in answer will be put in the arguments string. + arguments = ""; + uint16_t header_length = strlen(pages_names[i]); + if (answer.substring(header_length, header_length + 1) == "?") { + uint16_t footer_start = answer.length(); + if (answer.endsWith(" HTTP/")) + footer_start -= 6; // length of " HTTP/" + + // Standard operation --> strip off anything preceeding the first "=", pass the rest to the function + if(AREST_PARAMS_MODE == 0) { + uint16_t eq_position = answer.indexOf('=', header_length); // Replacing 'magic number' 8 for fixed location of '=' + if (eq_position != -1) + arguments = answer.substring(eq_position + 1, footer_start); + } + // All params mode --> pass all parameters, if any, to the function. Function will be resonsible for parsing + else if(AREST_PARAMS_MODE == 1) { + arguments = answer.substring(header_length + 1, footer_start); + } + } + + break; // We found what we're looking for + } + } + // If the command is "id", return device id, name and status if ((answer[0] == 'i' && answer[1] == 'd')) { @@ -1157,7 +1246,12 @@ void process(char c) { pin_selected = true; state = 'x'; } - + + // Check if we return content type as json + if (command != 'p') + { + content_type = "application/json"; + } // Check the type of HTTP request // if (answer.startsWith("GET")) {method = "GET";} // if (answer.startsWith("POST")) {method = "POST";} @@ -1427,6 +1521,17 @@ bool send_command(bool headers, bool decodeArgs) { } } + // page selected + if (command == 'p') { + + // Execute function + if (decodeArgs) + urldecode(arguments); // Modifies arguments + + String result = pages[value](arguments); + addStringToBuffer(result.c_str(), false); + } + if (command == 'r' || command == 'u') { root_answer(); } @@ -1444,7 +1549,7 @@ bool send_command(bool headers, bool decodeArgs) { addToBufferF(F("\r\n")); } - else { + else if (command != 'p') { if (command != 'r' && command != 'u') { addHardwareToBuffer(); addToBufferF(F("\r\n")); @@ -1511,6 +1616,17 @@ void function(char * function_name, int (*f)(String)){ functions_index++; } +// To add static content page +// page_content_type can be any type like application/json or text/html +// it will be added automatically to the headers +void page(char * page_name, char * page_content_type, String (*f)(String)){ + + pages_names[pages_index] = page_name; + pages_content_type[pages_index] = page_content_type; + pages[pages_index] = f; + pages_index++; +} + // Set device ID void set_id(const String& device_id) { @@ -1629,41 +1745,6 @@ void addQuote() { } } - -void addStringToBuffer(const char * toAdd, bool quotable){ - - if (DEBUG_MODE) { - #if defined(ESP8266)|| defined (ESP32) - Serial.print("Memory loss:"); - Serial.println(freeMemory - ESP.getFreeHeap(),DEC); - freeMemory = ESP.getFreeHeap(); - #endif - Serial.print(F("Added to buffer as char: ")); - Serial.println(toAdd); - } - - if(quotable) { - addQuote(); - } - - for (int i = 0; i < strlen(toAdd) && index < OUTPUT_BUFFER_SIZE; i++, index++) { - // Handle quoting quotes and backslashes - if(quotable && (toAdd[i] == '"' || toAdd[i] == '\\')) { - if(index == OUTPUT_BUFFER_SIZE - 1) // No room! - return; - buffer[index] = '\\'; - index++; - } - - buffer[index] = toAdd[i]; - } - - if(quotable) { - addQuote(); - } -} - - // Add to output buffer template @@ -1892,6 +1973,7 @@ void setMQTTServer(char* new_mqtt_server){ char state; uint16_t value; boolean pin_selected; + String content_type; char* remote_server; int port; @@ -1937,6 +2019,12 @@ void setMQTTServer(char* new_mqtt_server){ int (*functions[NUMBER_FUNCTIONS])(String); char * functions_names[NUMBER_FUNCTIONS]; + // Pages array + uint8_t pages_index; + String (*pages[NUMBER_PAGES])(String); + char * pages_names[NUMBER_PAGES]; + char * pages_content_type[NUMBER_PAGES]; + // Memory debug #if defined(ESP8266) || defined(ESP32) int freeMemory; diff --git a/examples/ESP8266_page/ESP8266_page.ino b/examples/ESP8266_page/ESP8266_page.ino new file mode 100644 index 0000000..d00d1fa --- /dev/null +++ b/examples/ESP8266_page/ESP8266_page.ino @@ -0,0 +1,101 @@ +/* + This a simple example of the aREST Library for the ESP8266 WiFi chip. + See the README file for more details. + + Written in 2015 by Marco Schwartz under a GPL license. +*/ + +// Import required libraries +#include +#include + +// Create aREST instance +aREST rest = aREST(); + +// WiFi parameters +const char* ssid = "your_wifi_network_name"; +const char* password = "your_wifi_network_password"; + +// The port to listen for incoming TCP connections +#define LISTEN_PORT 80 + +// Create an instance of the server +WiFiServer server(LISTEN_PORT); + +// Variables to be exposed to the API +int temperature; +int humidity; + +// Declare functions to be exposed to the API +int ledControl(String command); + +void setup(void) +{ + // Start Serial + Serial.begin(115200); + + // Init variables and expose them to REST API + temperature = 24; + humidity = 40; + rest.variable("temperature",&temperature); + rest.variable("humidity",&humidity); + + // Function to be exposed + rest.function("led",ledControl); + + // page to be returned + rest.page("html","text/html",Html); + + // Give name & ID to the device (ID should be 6 characters long) + rest.set_id("1"); + rest.set_name("esp8266"); + + // Connect to WiFi + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(""); + Serial.println("WiFi connected"); + + // Start the server + server.begin(); + Serial.println("Server started"); + + // Print the IP address + Serial.println(WiFi.localIP()); +} + +void loop() { + + // Handle REST calls + WiFiClient client = server.available(); + if (!client) { + return; + } + while(!client.available()){ + delay(1); + } + rest.handle(client); + +} + +String Html(String command) { + String ret = "This is a web page

Great page!

And this is text with Base64 encoded image
"; + ret += "Temperature: " + String(temperature); + ret += "
Humidity: " + String(humidity); + ret += "
Command passed: " + command; + ret += ""; + return ret; +} + +// Custom function accessible by the API +int ledControl(String command) { + + // Get state from command + int state = command.toInt(); + + digitalWrite(6,state); + return 1; +}