diff --git a/Makefile b/Makefile index 4138886..ccf379f 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ LED_SERIAL_PIN ?= 14 # --------------- esp-link modules config options --------------- # Optional Modules mqtt -MODULES ?= mqtt rest syslog +MODULES ?= mqtt rest syslog web-server # --------------- esphttpd config options --------------- @@ -220,6 +220,10 @@ ifneq (,$(findstring syslog,$(MODULES))) CFLAGS += -DSYSLOG endif +ifneq (,$(findstring web-server,$(MODULES))) + CFLAGS += -DWEBSERVER +endif + # which modules (subdirectories) of the project to include in compiling LIBRARIES_DIR = libraries MODULES += espfs httpd user serial cmd esp-link diff --git a/WEB-SERVER.md b/WEB-SERVER.md new file mode 100644 index 0000000..6bd2a3c --- /dev/null +++ b/WEB-SERVER.md @@ -0,0 +1,44 @@ +ESP-LINK web-server tutorial +============================ + +LED flashing sample +-------------------- + +Circuit: + + - 1: connect a Nodemcu (ESP8266) board and an Arduino Nano / UNO: + (RX - levelshifter - TX, TX - levelshifter - RX) + - 2: optionally connect RESET-s with a level shifter + + +Installation steps: + + - 1: install the latest Arduino on the PC + - 2: install EspLink library from arduino/libraries path + - 3: open EspLinkWebSimpleLedControl sample from Arduino + - 4: upload the code onto an Arduino Nano/Uno + - 5: install esp-link + - 6: jump to the Web Server page on esp-link UI + - 7: upload SimpleLED.html ( arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/SimpleLED.html ) + - 8: jump to SimpleLED page on esp-link UI + - 9: turn on/off the LED + +Complex application sample +-------------------------- + +Circuit: + + - 1: connect a Nodemcu (ESP8266) board and an Arduino Nano / UNO: + (RX - levelshifter - TX, TX - levelshifter - RX) + - 2: optionally connect RESET-s with a level shifter + - 3: add a trimmer to A0 for Voltage measurement + +Installation steps: + + - 1: open EspLinkWebApp sample from Arduino + - 2: upload the code onto an Arduino Nano/Uno + - 3: jump to the Web Server page on esp-link UI + - 4: upload web-page.espfs.img ( arduino/libraries/EspLink/examples/EspLinkWebApp/web-page.espfs.img ) + - 5: jump to LED/User/Voltage pages + - 6: try out different settings + diff --git a/arduino/libraries/EspLink/EspLink.cpp b/arduino/libraries/EspLink/EspLink.cpp new file mode 100644 index 0000000..0e2972a --- /dev/null +++ b/arduino/libraries/EspLink/EspLink.cpp @@ -0,0 +1,207 @@ +#include "EspLink.h" + +#define READ_BUF_DFLT_SIZE 64 + +// Standard SLIP escape chars from RFC +#define SLIP_END 0300 // indicates end of packet +#define SLIP_ESC 0333 // indicates byte stuffing +#define SLIP_ESC_END 0334 // ESC ESC_END means END data byte +#define SLIP_ESC_ESC 0335 // ESC ESC_ESC means ESC data byte + +EspLink::EspLink(Stream &streamIn, CmdRequestCB callback):stream(streamIn),requestCb(callback) +{ + readBuf = NULL; + readLastChar = 0; +} + +EspLink::~EspLink() +{ + if( readBuf != NULL ) + free( readBuf ); + readBuf = NULL; +} + +void EspLink::writeChar(uint8_t data) +{ + switch(data) + { + case SLIP_END: + stream.write(SLIP_ESC); + stream.write(SLIP_ESC_END); + break; + case SLIP_ESC: + stream.write(SLIP_ESC); + stream.write(SLIP_ESC_ESC); + break; + default: + stream.write(data); + } + + crc16_add(data, &crc16_out); +} + +/* CITT CRC16 polynomial ^16 + ^12 + ^5 + 1 */ +/*---------------------------------------------------------------------------*/ +void EspLink::crc16_add(uint8_t b, uint16_t *crc) +{ + *crc ^= b; + *crc = (*crc >> 8) | (*crc << 8); + *crc ^= (*crc & 0xff00) << 4; + *crc ^= (*crc >> 8) >> 4; + *crc ^= (*crc & 0xff00) >> 5; +} + +void EspLink::writeBuf(uint8_t * buf, uint16_t len) +{ + while(len-- > 0) + writeChar(*buf++); +} + +void EspLink::sendPacketStart(uint16_t cmd, uint32_t value, uint16_t argc) +{ + crc16_out = 0; + stream.write( SLIP_END ); + writeBuf((uint8_t*)&cmd, 2); + writeBuf((uint8_t*)&argc, 2); + writeBuf((uint8_t*)&value, 4); +} + +void EspLink::sendPacketArg(uint16_t len, uint8_t * data) +{ + writeBuf((uint8_t*)&len, 2); + writeBuf(data, len); + + uint16_t pad = ((len+3)&~3) - len; // get to multiple of 4 + if (pad > 0) { + uint32_t temp = 0; + writeBuf((uint8_t*)&temp, pad); + } +} + +void EspLink::sendPacketEnd() { + uint16_t crc = crc16_out; + writeBuf((uint8_t*)&crc, 2); + stream.write(SLIP_END); +} + +void EspLink::parseSlipPacket() +{ + CmdRequest req; + req.cmd = (CmdPacket *)readBuf; + req.arg_num = 0; + req.arg_ptr = readBuf + sizeof(CmdPacket); + + requestCb(&req); + + free(readBuf); + readBuf = NULL; +} + +void EspLink::checkPacket() +{ + if( readBufPtr <= 3 ) + return; + uint16_t crc = 0; + for(uint16_t i=0; i < readBufPtr - 2; i++) + crc16_add(readBuf[i], &crc); + + uint16_t crcpacket = *(uint16_t*)(readBuf + readBufPtr - 2); + + if( crc == crcpacket ) + { + readBufPtr -= 2; + parseSlipPacket(); + } +} + +void EspLink::readLoop() +{ + if( stream.available() > 0 ) + { + int byt = stream.read(); + + switch(readState) + { + case WAIT_FOR_SLIP_START: + if( byt == SLIP_END ) + { + if(readBuf != NULL) + free(readBuf); + readBufPtr = 0; + readBufMax = READ_BUF_DFLT_SIZE; + readBuf = (uint8_t *)malloc(readBufMax); + readState = READ_SLIP_PACKAGE; + } + break; + case READ_SLIP_PACKAGE: + if( byt == SLIP_END ) + { + readState = WAIT_FOR_SLIP_START; + checkPacket(); + break; + } + if( byt == SLIP_ESC ) + break; + if( readLastChar == SLIP_ESC && byt == SLIP_ESC_END ) + byt = SLIP_END; + else if( readLastChar == SLIP_ESC && byt == SLIP_ESC_ESC ) + byt = SLIP_ESC; + + if( readBufPtr >= readBufMax ) + { + readBufMax = readBufMax + READ_BUF_DFLT_SIZE; + readBuf = (uint8_t *)realloc(readBuf, readBufMax); + if( readBuf == NULL ) + { + readState = WAIT_FOR_SLIP_START; // TODO + break; + } + } + readBuf[readBufPtr++] = byt; + break; + } + + readLastChar = byt; + } +} + +// Return the number of arguments given a command struct +uint32_t EspLink::cmdGetArgc(CmdRequest *req) { + return req->cmd->argc; +} + +// Copy the next argument from a command structure into the data pointer, returns 0 on success +// -1 on error +int32_t EspLink::cmdPopArg(CmdRequest *req, void *data, uint16_t len) { + uint16_t length; + + if (req->arg_num >= req->cmd->argc) + return -1; + + length = *(uint16_t*)req->arg_ptr; + if (length != len) return -1; // safety check + + memcpy(data, req->arg_ptr + 2, length); + req->arg_ptr += (length+5)&~3; // round up to multiple of 4 + + req->arg_num ++; + return 0; +} + +// Skip the next argument +void EspLink::cmdSkipArg(CmdRequest *req) { + uint16_t length; + + if (req->arg_num >= req->cmd->argc) return; + + length = *(uint16_t*)req->arg_ptr; + + req->arg_ptr += (length+5)&~3; + req->arg_num ++; +} + +// Return the length of the next argument +uint16_t EspLink::cmdArgLen(CmdRequest *req) { + return *(uint16_t*)req->arg_ptr; +} + diff --git a/arduino/libraries/EspLink/EspLink.h b/arduino/libraries/EspLink/EspLink.h new file mode 100644 index 0000000..aff43f9 --- /dev/null +++ b/arduino/libraries/EspLink/EspLink.h @@ -0,0 +1,91 @@ +#ifndef ESP_LINK_H +#define ESP_LINK_H + +#include +#include + +typedef struct __attribute__((__packed__)) { + uint16_t len; // length of data + uint8_t data[0]; // really data[len] +} CmdArg; + +typedef struct __attribute__((__packed__)) { + uint16_t cmd; // command to perform, from CmdName enum + uint16_t argc; // number of arguments to command + uint32_t value; // callback pointer for response or first argument + CmdArg args[0]; // really args[argc] +} CmdPacket; + +typedef struct { + CmdPacket *cmd; // command packet header + uint32_t arg_num; // number of args parsed + uint8_t *arg_ptr; // pointer to ?? +} CmdRequest; + +typedef void (* CmdRequestCB)(CmdRequest *); + +typedef enum { + CMD_NULL = 0, + CMD_SYNC, // synchronize and clear + CMD_RESP_V, // response with a value + CMD_RESP_CB, // response with a callback + CMD_WIFI_STATUS, // get the current wifi status + CMD_CB_ADD, + CMD_CB_EVENTS, + CMD_GET_TIME, // get current time in seconds since the unix epoch + + CMD_MQTT_SETUP = 10, // set-up callbacks + CMD_MQTT_PUBLISH, // publish a message + CMD_MQTT_SUBSCRIBE, // subscribe to a topic + CMD_MQTT_LWT, // set the last-will-topic and messge + + CMD_REST_SETUP = 20, + CMD_REST_REQUEST, + CMD_REST_SETHEADER, + + CMD_WEB_DATA = 30, + CMD_WEB_REQ_CB, +} CmdName; + +typedef enum +{ + WAIT_FOR_SLIP_START, + READ_SLIP_PACKAGE, +} ReadState; + +class EspLink +{ + private: + uint16_t crc16_out; + Stream &stream; + ReadState readState; + uint8_t * readBuf; + uint16_t readBufPtr; + uint16_t readBufMax; + int readLastChar; + CmdRequestCB requestCb; + + void crc16_add(uint8_t b, uint16_t *crc); + void writeChar(uint8_t chr); + void writeBuf(uint8_t * buf, uint16_t len); + void checkPacket(); + void parseSlipPacket(); + + public: + EspLink(Stream &stream, CmdRequestCB callback); + ~EspLink(); + + void sendPacketStart(uint16_t cmd, uint32_t value, uint16_t argc); + void sendPacketArg(uint16_t len, uint8_t * data); + void sendPacketEnd(); + + void readLoop(); + + uint32_t cmdGetArgc(CmdRequest *req); + int32_t cmdPopArg(CmdRequest *req, void *data, uint16_t len); + void cmdSkipArg(CmdRequest *req); + uint16_t cmdArgLen(CmdRequest *req); +}; + +#endif /* ESP_LINK_H */ + diff --git a/arduino/libraries/EspLink/WebServer.cpp b/arduino/libraries/EspLink/WebServer.cpp new file mode 100644 index 0000000..6a5c270 --- /dev/null +++ b/arduino/libraries/EspLink/WebServer.cpp @@ -0,0 +1,214 @@ +#include "WebServer.h" +#include "Arduino.h" + +#define RESUBSCRIBE_LIMIT 1000 + +WebServer * WebServer::instance = NULL; + +void webServerCallback(CmdRequest *req) +{ + WebServer::getInstance()->handleRequest(req); +} + +WebServer::WebServer(Stream &streamIn, const WebMethod * PROGMEM methodsIn):espLink(streamIn, webServerCallback),methods(methodsIn),stream(streamIn),esplink_cb(NULL) +{ + instance = this; +} + +void WebServer::init() +{ + registerCallback(); +} + +void WebServer::loop() +{ + // resubscribe periodically + uint32_t elapsed = millis() - last_connect_ts; + if( elapsed > RESUBSCRIBE_LIMIT ) + registerCallback(); + espLink.readLoop(); +} + +void WebServer::registerCallback() +{ + espLink.sendPacketStart(CMD_CB_ADD, 100, 1); + espLink.sendPacketArg(5, (uint8_t *)"webCb"); + espLink.sendPacketEnd(); + last_connect_ts = millis(); +} + +void WebServer::invokeMethod(RequestReason reason, WebMethod * method, CmdRequest *req) +{ + switch(reason) + { + case WS_BUTTON: + { + uint16_t len = espLink.cmdArgLen(req); + char bf[len+1]; + bf[len] = 0; + espLink.cmdPopArg(req, bf, len); + + method->callback(BUTTON_PRESS, bf, len); + } + break; + case WS_SUBMIT: + { + int arg_len = espLink.cmdGetArgc( req ); + int cnt = 4; + + while( cnt < arg_len ) + { + uint16_t len = espLink.cmdArgLen(req); + char bf[len+1]; + bf[len] = 0; + espLink.cmdPopArg(req, bf, len); + + value_ptr = bf + 2 + strlen(bf+1); + method->callback(SET_FIELD, bf+1, strlen(bf+1)); + + cnt++; + } + } + return; + case WS_LOAD: + case WS_REFRESH: + break; + default: + return; + } + + espLink.sendPacketStart(CMD_WEB_DATA, 100, 255); + espLink.sendPacketArg(4, remote_ip); + espLink.sendPacketArg(2, (uint8_t *)&remote_port); + + method->callback( reason == WS_LOAD ? LOAD : REFRESH, NULL, 0); + + espLink.sendPacketArg(0, NULL); + espLink.sendPacketEnd(); +} + +void WebServer::handleRequest(CmdRequest *req) +{ + if( req->cmd->cmd != CMD_WEB_REQ_CB ) + { + if( esplink_cb != NULL ) + esplink_cb(req); + return; + } + + uint16_t shrt; + espLink.cmdPopArg(req, &shrt, 2); + RequestReason reason = (RequestReason)shrt; + + espLink.cmdPopArg(req, &remote_ip, 4); + espLink.cmdPopArg(req, &remote_port, 2); + + { + uint16_t len = espLink.cmdArgLen(req); + char bf[len+1]; + bf[len] = 0; + espLink.cmdPopArg(req, bf, len); + + const WebMethod * meth = methods; + do + { + WebMethod m; + memcpy_P(&m, meth, sizeof(WebMethod)); + if( m.url == NULL || m.callback == NULL ) + break; + + if( strcmp_P(bf, m.url) == 0 ) + { + invokeMethod(reason, &m, req); + return; + } + meth++; + }while(1); + } + + if( reason == WS_SUBMIT ) + return; + + // empty response + espLink.sendPacketStart(CMD_WEB_DATA, 100, 2); + espLink.sendPacketArg(4, remote_ip); + espLink.sendPacketArg(2, (uint8_t *)&remote_port); + espLink.sendPacketEnd(); +} + +void WebServer::setArgString(const char * name, const char * value) +{ + uint8_t nlen = strlen(name); + uint8_t vlen = strlen(value); + char buf[nlen + vlen + 3]; + buf[0] = WEB_STRING; + strcpy(buf+1, name); + strcpy(buf+2+nlen, value); + espLink.sendPacketArg(nlen+vlen+2, (uint8_t *)buf); +} + +void WebServer::setArgStringP(const char * name, const char * value) +{ + uint8_t nlen = strlen(name); + uint8_t vlen = strlen_P(value); + char buf[nlen + vlen + 3]; + buf[0] = WEB_STRING; + strcpy(buf+1, name); + strcpy_P(buf+2+nlen, value); + espLink.sendPacketArg(nlen+vlen+2, (uint8_t *)buf); +} + +void WebServer::setArgBoolean(const char * name, uint8_t value) +{ + uint8_t nlen = strlen(name); + char buf[nlen + 4]; + buf[0] = WEB_BOOLEAN; + strcpy(buf+1, name); + buf[2 + nlen] = value; + espLink.sendPacketArg(nlen+3, (uint8_t *)buf); +} + +void WebServer::setArgJson(const char * name, const char * value) +{ + uint8_t nlen = strlen(name); + uint8_t vlen = strlen(value); + char buf[nlen + vlen + 3]; + buf[0] = WEB_JSON; + strcpy(buf+1, name); + strcpy(buf+2+nlen, value); + espLink.sendPacketArg(nlen+vlen+2, (uint8_t *)buf); +} + +void WebServer::setArgInt(const char * name, int32_t value) +{ + uint8_t nlen = strlen(name); + char buf[nlen + 7]; + buf[0] = WEB_INTEGER; + strcpy(buf+1, name); + memcpy(buf+2+nlen, &value, 4); + espLink.sendPacketArg(nlen+6, (uint8_t *)buf); +} + +int32_t WebServer::getArgInt() +{ + return (int32_t)atol(value_ptr); +} + +char * WebServer::getArgString() +{ + return value_ptr; +} + +uint8_t WebServer::getArgBoolean() +{ + if( strcmp_P(value_ptr, PSTR("on")) == 0 ) + return 1; + if( strcmp_P(value_ptr, PSTR("true")) == 0 ) + return 1; + if( strcmp_P(value_ptr, PSTR("yes")) == 0 ) + return 1; + if( strcmp_P(value_ptr, PSTR("1")) == 0 ) + return 1; + return 0; +} + diff --git a/arduino/libraries/EspLink/WebServer.h b/arduino/libraries/EspLink/WebServer.h new file mode 100644 index 0000000..59f1cb2 --- /dev/null +++ b/arduino/libraries/EspLink/WebServer.h @@ -0,0 +1,91 @@ +#ifndef WEB_SERVER_H +#define WEB_SERVER_H + +#include "EspLink.h" + +typedef enum +{ + BUTTON_PRESS, + SET_FIELD, + REFRESH, + LOAD, +} WebServerCommand; + +typedef void (*WebServerCallback)(WebServerCommand command, char * data, int dataLen); + +typedef struct +{ + const char * PROGMEM url; + WebServerCallback callback; +} WebMethod; + + +typedef enum { + WS_LOAD=0, + WS_REFRESH, + WS_BUTTON, + WS_SUBMIT, +} RequestReason; + +typedef enum +{ + WEB_STRING=0, + WEB_NULL, + WEB_INTEGER, + WEB_BOOLEAN, + WEB_FLOAT, + WEB_JSON +} WebValueType; + +class WebServer +{ + friend void webServerCallback(CmdRequest *req); + + private: + const WebMethod * PROGMEM methods; + Stream &stream; + static WebServer * instance; + + void invokeMethod(RequestReason reason, WebMethod * method, CmdRequest *req); + void handleRequest(CmdRequest *req); + + uint8_t remote_ip[4]; + uint16_t remote_port; + + char * value_ptr; + + uint32_t last_connect_ts; + + CmdRequestCB esplink_cb; + + protected: + EspLink espLink; + + public: + WebServer(Stream &stream, const WebMethod * PROGMEM methods); + + void init(); + void loop(); + + void registerCallback(); + + void setEspLinkCallback(CmdRequestCB cb) { esplink_cb = cb; } + + static WebServer * getInstance() { return instance; } + uint8_t * getRemoteIp() { return remote_ip; } + uint16_t getRemotePort() { return remote_port; } + + void setArgInt(const char * name, int32_t value); + void setArgJson(const char * name, const char * value); + void setArgString(const char * name, const char * value); + void setArgStringP(const char * name, const char * value); + void setArgBoolean(const char * name, uint8_t value); + + int32_t getArgInt(); + char * getArgString(); + uint8_t getArgBoolean(); + + EspLink * getEspLink() { return &espLink; } +}; + +#endif /* WEB_SERVER_H */ diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/EspLinkWebApp.ino b/arduino/libraries/EspLink/examples/EspLinkWebApp/EspLinkWebApp.ino new file mode 100644 index 0000000..3f6dbaf --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/EspLinkWebApp.ino @@ -0,0 +1,35 @@ +#include "EspLink.h" +#include "WebServer.h" +#include "Pages.h" + +const char ledURL[] PROGMEM = "/LED.html.json"; +const char userURL[] PROGMEM = "/User.html.json"; +const char voltageURL[] PROGMEM = "/Voltage.html.json"; + +const WebMethod PROGMEM methods[] = { + { ledURL, ledHtmlCallback }, + { userURL, userHtmlCallback }, + { voltageURL, voltageHtmlCallback }, + { NULL, NULL }, +}; + +WebServer webServer(Serial, methods); + +void setup() +{ + Serial.begin(57600); + webServer.init(); + + ledInit(); + userInit(); + voltageInit(); +} + +void loop() +{ + webServer.loop(); + + ledLoop(); + voltageLoop(); +} + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/LedPage.ino b/arduino/libraries/EspLink/examples/EspLinkWebApp/LedPage.ino new file mode 100644 index 0000000..494df83 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/LedPage.ino @@ -0,0 +1,188 @@ +#include "WebServer.h" + +#define LED_PIN 13 + +int8_t blinking = 0; +int8_t frequency = 10; +uint8_t pattern = 2; +uint16_t elapse = 100; +uint16_t elapse_delta = 200; +uint32_t next_ts = 0; + +#define MAX_LOGS 5 +uint32_t log_ts[MAX_LOGS]; +uint8_t log_msg[MAX_LOGS]; +uint8_t log_ptr = 0; + +void ledInit() +{ + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, false); +} + +void ledLoop() +{ + if( blinking ) + { + if( next_ts <= millis() ) + { + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); + next_ts += elapse; + elapse = elapse_delta - elapse; + } + } +} + +void ledAddLog(uint8_t msg) +{ + if( log_ptr >= MAX_LOGS ) + log_ptr = MAX_LOGS - 1; + + for(int8_t i=log_ptr-1; i >= 0; i--) + { + log_ts[i+1] = log_ts[i]; + log_msg[i+1] = log_msg[i]; + } + log_msg[0] = msg; + log_ts[0] = millis(); + log_ptr++; +} + +void ledHistoryToLog(char * buf) +{ + buf[0] = 0; + strcat(buf, "["); + for(uint8_t i=0; i < log_ptr; i++) + { + if( i != 0 ) + strcat(buf, ","); + + char bf[20]; + sprintf(bf, "\"%lds: ", log_ts[i] / 1000); + strcat(buf, bf); + + uint8_t msg = log_msg[i]; + if( msg == 0xE1 ) + { + strcat_P(buf, PSTR("set pattern to 25%-75%")); + } + else if( msg == 0xE2 ) + { + strcat_P(buf, PSTR("set pattern to 50%-50%")); + } + else if( msg == 0xE3 ) + { + strcat_P(buf, PSTR("set pattern to 75%-25%")); + } + else if( msg == 0xF0 ) + { + strcat_P(buf, PSTR("set led on")); + } + else if( msg == 0xF1 ) + { + strcat_P(buf, PSTR("set led blinking")); + } + else if( msg == 0xF2 ) + { + strcat_P(buf, PSTR("set led off")); + } + else + { + strcat_P(buf, PSTR("set frequency to ")); + sprintf(bf, "%d Hz", msg); + strcat(buf, bf); + } + strcat(buf, "\""); + } + strcat(buf, "]"); +} + + +void ledHtmlCallback(WebServerCommand command, char * data, int dataLen) +{ + switch(command) + { + case BUTTON_PRESS: + if( strcmp_P(data, PSTR("btn_on") ) == 0 ) + { + if( blinking || digitalRead(LED_PIN) == false ) + ledAddLog(0xF0); + blinking = 0; + digitalWrite(LED_PIN, true); + } else if( strcmp_P(data, PSTR("btn_off") ) == 0 ) + { + if( blinking || digitalRead(LED_PIN) == true ) + ledAddLog(0xF2); + blinking = 0; + digitalWrite(LED_PIN, false); + } else if( strcmp_P(data, PSTR("btn_blink") ) == 0 ) + { + if( !blinking ) + ledAddLog(0xF1); + blinking = 1; + next_ts = millis() + elapse; + } + break; + case SET_FIELD: + if( strcmp_P(data, PSTR("frequency") ) == 0 ) + { + int8_t oldf = frequency; + frequency = webServer.getArgInt(); + digitalWrite(LED_PIN, false); + elapse_delta = 2000 / frequency; + elapse = pattern * elapse_delta / 4; + if( oldf != frequency ) + ledAddLog(frequency); + } + else if( strcmp_P(data, PSTR("pattern") ) == 0 ) + { + int8_t oldp = pattern; + char * arg = webServer.getArgString(); + + if( strcmp_P(arg, PSTR("25_75")) == 0 ) + pattern = 1; + else if( strcmp_P(arg, PSTR("50_50")) == 0 ) + pattern = 2; + else if( strcmp_P(arg, PSTR("75_25")) == 0 ) + pattern = 3; + + digitalWrite(LED_PIN, false); + elapse = pattern * elapse_delta / 4; + + if( oldp != pattern ) + ledAddLog(0xE0 + pattern); + } + break; + case LOAD: + webServer.setArgInt("frequency", frequency); + + switch(pattern) + { + case 1: + webServer.setArgStringP("pattern", PSTR("25_75")); + break; + case 2: + webServer.setArgStringP("pattern", PSTR("50_50")); + break; + case 3: + webServer.setArgStringP("pattern", PSTR("75_25")); + break; + } + case REFRESH: + { + if( blinking ) + webServer.setArgStringP("text", PSTR("LED is blinking")); + else + webServer.setArgStringP("text", digitalRead(LED_PIN) ? PSTR("LED is turned on") : PSTR("LED is turned off")); + + char buf[255]; + ledHistoryToLog(buf); + webServer.setArgJson("led_history", buf); + } + break; + default: + break; + } +} + + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/Makefile.webpage b/arduino/libraries/EspLink/examples/EspLinkWebApp/Makefile.webpage new file mode 100644 index 0000000..d0183b0 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/Makefile.webpage @@ -0,0 +1,8 @@ +all: user_img + +clean: + rm -rf web-page.espfs.img + +user_img: + ../../../../../createEspFs.pl web-page web-page.espfs.img + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/Pages.h b/arduino/libraries/EspLink/examples/EspLinkWebApp/Pages.h new file mode 100644 index 0000000..7043b9d --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/Pages.h @@ -0,0 +1,17 @@ +#ifndef PAGES_H +#define PAGES_H + +void ledHtmlCallback(WebServerCommand command, char * data, int dataLen); +void ledLoop(); +void ledInit(); + +void userHtmlCallback(WebServerCommand command, char * data, int dataLen); +void userInit(); + +void voltageHtmlCallback(WebServerCommand command, char * data, int dataLen); +void voltageLoop(); +void voltageInit(); + +#endif /* PAGES_H */ + + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/UserPage.ino b/arduino/libraries/EspLink/examples/EspLinkWebApp/UserPage.ino new file mode 100644 index 0000000..e6581a5 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/UserPage.ino @@ -0,0 +1,84 @@ +#include +#include "WebServer.h" + +#define MAGIC 0xABEF + +#define MAX_STR_LEN 32 + +#define POS_MAGIC 0 +#define POS_FIRST_NAME (POS_MAGIC + 2) +#define POS_LAST_NAME (POS_FIRST_NAME + MAX_STR_LEN) +#define POS_AGE (POS_LAST_NAME + MAX_STR_LEN) +#define POS_GENDER (POS_AGE+1) +#define POS_NOTIFICATIONS (POS_GENDER+1) + +void userInit() +{ + uint16_t magic; + EEPROM.get(POS_MAGIC, magic); + + if( magic != MAGIC ) + { + magic = MAGIC; + EEPROM.put(POS_MAGIC, magic); + EEPROM.update(POS_FIRST_NAME, 0); + EEPROM.update(POS_LAST_NAME, 0); + EEPROM.update(POS_AGE, 0); + EEPROM.update(POS_GENDER, 'f'); + EEPROM.update(POS_NOTIFICATIONS, 0); + } +} + +void userWriteStr(char * str, int ndx) +{ + for(uint8_t i=0; i < MAX_STR_LEN-1; i++) + { + EEPROM.update(ndx + i, str[i]); + if( str[i] == 0 ) + break; + } + EEPROM.update(ndx + MAX_STR_LEN - 1, 0); +} + +void userReadStr(char * str, int ndx) +{ + for(uint8_t i=0; i < MAX_STR_LEN; i++) + { + str[i] = EEPROM[ndx + i]; + } +} + +void userHtmlCallback(WebServerCommand command, char * data, int dataLen) +{ + switch(command) + { + case SET_FIELD: + if( strcmp_P(data, PSTR("first_name")) == 0 ) + userWriteStr(webServer.getArgString(), POS_FIRST_NAME); + if( strcmp_P(data, PSTR("last_name")) == 0 ) + userWriteStr(webServer.getArgString(), POS_LAST_NAME); + if( strcmp_P(data, PSTR("age")) == 0 ) + EEPROM.update(POS_AGE, (uint8_t)webServer.getArgInt()); + if( strcmp_P(data, PSTR("gender")) == 0 ) + EEPROM.update(POS_GENDER, (strcmp_P(webServer.getArgString(), PSTR("male")) == 0 ? 'm' : 'f')); + if( strcmp_P(data, PSTR("notifications")) == 0 ) + EEPROM.update(POS_NOTIFICATIONS, (uint8_t)webServer.getArgBoolean()); + break; + case LOAD: + { + char buf[MAX_STR_LEN]; + userReadStr( buf, POS_FIRST_NAME ); + webServer.setArgString("first_name", buf); + userReadStr( buf, POS_LAST_NAME ); + webServer.setArgString("last_name", buf); + webServer.setArgInt("age", (uint8_t)EEPROM[POS_AGE]); + webServer.setArgStringP("gender", (EEPROM[POS_GENDER] == 'm') ? PSTR("male") : PSTR("female")); + webServer.setArgBoolean("notifications", EEPROM[POS_NOTIFICATIONS] != 0); + } + break; + case REFRESH: + // do nothing + break; + } +} + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/VoltagePage.ino b/arduino/libraries/EspLink/examples/EspLinkWebApp/VoltagePage.ino new file mode 100644 index 0000000..4080f75 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/VoltagePage.ino @@ -0,0 +1,125 @@ +#include "WebServer.h" + +#include + +#define SAMPLE_COUNT 100 +#define PERIOD_COUNT (135 * SAMPLE_COUNT) + +uint16_t smin = 0xFFFF; +uint16_t smax = 0; +uint32_t savg = 0; + +uint16_t count; +uint32_t voltage = 0; +uint16_t measured_voltage = 0; + +#define MAX_HISTORY 3 + +uint8_t history_cnt = 0; +uint32_t h_ts[MAX_HISTORY]; +uint16_t h_min[MAX_HISTORY]; +uint16_t h_max[MAX_HISTORY]; +uint16_t h_avg[MAX_HISTORY]; + +uint16_t calibrate = 0x128; // calibrate this manually + +void voltageInit() +{ + analogReference(DEFAULT); + + count = 0; +} + +void voltageLoop() +{ + uint16_t adc = analogRead(A0); + + if( adc < smin ) + smin = adc; + if( adc > smax ) + smax = adc; + savg += adc; + + voltage += adc; + count++; + + if( (count % SAMPLE_COUNT) == 0 ) + { + voltage /= SAMPLE_COUNT; + measured_voltage = voltage * calibrate / 256; + voltage = 0; + } + if( count == PERIOD_COUNT ) + { + for(int8_t i=MAX_HISTORY-2; i >=0; i-- ) + { + h_ts[i+1] = h_ts[i]; + h_min[i+1] = h_min[i]; + h_max[i+1] = h_max[i]; + h_avg[i+1] = h_avg[i]; + } + + h_ts[0] = millis(); + h_min[0] = (uint32_t)smin * calibrate / 256; + h_max[0] = (uint32_t)smax * calibrate / 256; + h_avg[0] = (savg / PERIOD_COUNT) * calibrate / 256; + + smin = 0xFFFF; + smax = 0; + savg = 0; + + if( history_cnt < MAX_HISTORY ) + history_cnt++; + count = 0; + } +} + +void voltageHtmlCallback(WebServerCommand command, char * data, int dataLen) +{ + switch(command) + { + case BUTTON_PRESS: + // no buttons + break; + case SET_FIELD: + /* TODO */ + break; + case LOAD: + case REFRESH: + { + char buf[20]; + uint8_t int_part = measured_voltage / 256; + uint8_t float_part = ((measured_voltage & 255) * 100) / 256; + sprintf(buf, "%d.%02d V", int_part, float_part); + webServer.setArgString("voltage", buf); + + char tab[256]; + tab[0] = 0; + strcat_P(tab, PSTR("[[\"Time\",\"Min\",\"AVG\",\"Max\"]")); + + for(uint8_t i=0; i < history_cnt; i++ ) + { + uint8_t min_i = h_min[i] / 256; + uint8_t min_f = ((h_min[i] & 255) * 100) / 256; + uint8_t max_i = h_max[i] / 256; + uint8_t max_f = ((h_max[i] & 255) * 100) / 256; + uint8_t avg_i = h_avg[i] / 256; + uint8_t avg_f = ((h_avg[i] & 255) * 100) / 256; + + sprintf(buf, ",[\"%d s\",", h_ts[i] / 1000); + strcat(tab, buf); + sprintf(buf, "\"%d.%02d V\",", min_i, min_f); + strcat(tab, buf); + sprintf(buf, "\"%d.%02d V\",", avg_i, avg_f); + strcat(tab, buf); + sprintf(buf, "\"%d.%02d V\"]", max_i, max_f); + strcat(tab, buf); + } + + strcat_P(tab, PSTR("]")); + webServer.setArgJson("table", tab); + } + break; + } +} + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page.espfs.img b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page.espfs.img new file mode 100644 index 0000000..e7a760f Binary files /dev/null and b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page.espfs.img differ diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/LED.html b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/LED.html new file mode 100644 index 0000000..a87f348 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/LED.html @@ -0,0 +1,36 @@ + + +
+

LED configuration

+
+ +
+
+
+

Control

+ + + +

+

+
+

Frequency and pattern

+
+ Pattern:
+ 25% on 75% off
+ 50% on 50% off
+ 75% on 25% off
+ + Frequency:
+
+ +
+
+
+
+

Logs

+
    +
+
+
+ diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/User.html b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/User.html new file mode 100644 index 0000000..8dc4286 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/User.html @@ -0,0 +1,24 @@ + + +
+

User setup

+
+ +
+
+ First name:
+ Last name:
+ Age: + + Gender: + +
+ Notifications +
+ +
+
+ diff --git a/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/Voltage.html b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/Voltage.html new file mode 100644 index 0000000..b52098e --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebApp/web-page/Voltage.html @@ -0,0 +1,15 @@ + + + + +
+

Voltage measurement

+
+ +
+

+ + + + + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/EspLinkWebSimpleLedControl.ino b/arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/EspLinkWebSimpleLedControl.ino new file mode 100644 index 0000000..516a2ae --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/EspLinkWebSimpleLedControl.ino @@ -0,0 +1,49 @@ +#include "EspLink.h" +#include "WebServer.h" + +#define LED_PIN 13 + +void simpleLedHtmlCallback(WebServerCommand command, char * data, int dataLen); +const char simpleLedURL[] PROGMEM = "/SimpleLED.html.json"; + +const WebMethod PROGMEM methods[] = { + { simpleLedURL, simpleLedHtmlCallback }, + { NULL, NULL }, +}; + +WebServer webServer(Serial, methods); + +void simpleLedHtmlCallback(WebServerCommand command, char * data, int dataLen) +{ + switch(command) + { + case BUTTON_PRESS: + if( strcmp_P(data, PSTR("btn_on") ) == 0 ) + digitalWrite(LED_PIN, true); + else if( strcmp_P(data, PSTR("btn_off") ) == 0 ) + digitalWrite(LED_PIN, false); + break; + case SET_FIELD: + // no fields to set + break; + case LOAD: + case REFRESH: + if( digitalRead(LED_PIN) ) + webServer.setArgString("text", "LED is on"); + else + webServer.setArgString("text", "LED is off"); + break; + } +} + +void setup() +{ + Serial.begin(57600); + webServer.init(); +} + +void loop() +{ + webServer.loop(); +} + diff --git a/arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/SimpleLED.html b/arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/SimpleLED.html new file mode 100644 index 0000000..27dc359 --- /dev/null +++ b/arduino/libraries/EspLink/examples/EspLinkWebSimpleLedControl/SimpleLED.html @@ -0,0 +1,7 @@ + + +

Simple LED control

+

+ + + diff --git a/arduino/libraries/readme.txt b/arduino/libraries/readme.txt new file mode 100644 index 0000000..96ce674 --- /dev/null +++ b/arduino/libraries/readme.txt @@ -0,0 +1 @@ +For information on installing libraries, see: http://www.arduino.cc/en/Guide/Libraries diff --git a/cmd/cmd.h b/cmd/cmd.h index 6b4b018..be1e684 100644 --- a/cmd/cmd.h +++ b/cmd/cmd.h @@ -48,6 +48,9 @@ typedef enum { CMD_REST_SETUP = 20, CMD_REST_REQUEST, CMD_REST_SETHEADER, + + CMD_WEB_DATA = 30, // MCU pushes data using this command + CMD_WEB_REQ_CB, // esp-link WEB callback } CmdName; typedef void (*cmdfunc_t)(CmdPacket *cmd); diff --git a/cmd/handlers.c b/cmd/handlers.c index 9e719a7..af328e7 100644 --- a/cmd/handlers.c +++ b/cmd/handlers.c @@ -12,6 +12,7 @@ #ifdef REST #include #endif +#include #ifdef CMD_DBG #define DBG(format, ...) do { os_printf(format, ## __VA_ARGS__); } while(0) @@ -47,6 +48,7 @@ const CmdList commands[] = { {CMD_REST_REQUEST, "REST_REQ", REST_Request}, {CMD_REST_SETHEADER, "REST_SETHDR", REST_SetHeader}, #endif + {CMD_WEB_DATA, "WEB_DATA", WEB_Data}, }; //===== List of registered callbacks (to uC) diff --git a/createEspFs.pl b/createEspFs.pl new file mode 100755 index 0000000..1612098 --- /dev/null +++ b/createEspFs.pl @@ -0,0 +1,114 @@ +#!/usr/bin/perl + +use strict; +use Data::Dumper; + +my $dir = shift @ARGV; +my $out = shift @ARGV; + +my $espfs = ''; + +my @structured = read_dir_structure($dir, ""); + +for my $file (@structured) +{ + my $flags = 0; + my $name = $file; + my $compression = 0; + + if( $name =~ /\.gz$/ ) + { + $flags |= 2; + $name =~ s/\.gz$//; + } + + my $head = 'esp-link
'; + + open IF, "<", "$dir/$file" or die "Can't read file: $!"; + my @fc = ; + close(IF); + my $cnt = join("", @fc); + + if( $name =~ /\.html$/ ) + { + if( ! ( $flags & 2 ) ) + { + $cnt = "$head$cnt"; + } + else + { + printf("TODO: prepend headers to GZipped HTML content!\n"); + } + } + + $name .= chr(0); + $name .= chr(0) while( (length($name) & 3) != 0 ); + + my $size = length($cnt); + + $espfs .= "ESfs"; + $espfs .= chr($flags); + $espfs .= chr($compression); + $espfs .= chr( length($name) & 255 ); + $espfs .= chr( length($name) / 256 ); + $espfs .= chr( $size & 255 ); + $espfs .= chr( ( $size / 0x100 ) & 255 ); + $espfs .= chr( ( $size / 0x10000 ) & 255 ); + $espfs .= chr( ( $size / 0x1000000 ) & 255 ); + $espfs .= chr( $size & 255 ); + $espfs .= chr( ( $size / 0x100 ) & 255 ); + $espfs .= chr( ( $size / 0x10000 ) & 255 ); + $espfs .= chr( ( $size / 0x1000000 ) & 255 ); + + $espfs .= $name; + + + + $cnt .= chr(0) while( (length($cnt) & 3) != 0 ); + $espfs .= $cnt; +} + +$espfs .= "ESfs"; +$espfs .= chr(1); +for(my $i=0; $i < 11; $i++) +{ + $espfs .= chr(0); +} + +open FH, ">", $out or die "Can't open file for write, $!"; +print FH $espfs; +close(FH); + + +exit(0); + +sub read_dir_structure +{ + my ($dir, $base) = @_; + + my @files; + + opendir my $dh, $dir or die "Could not open '$dir' for reading: $!\n"; + + while (my $file = readdir $dh) { + if ($file eq '.' or $file eq '..') { + next; + } + + my $path = "$dir/$file"; + if( -d "$path" ) + { + my @sd = read_dir_structure($path, "$base/$file"); + push @files, @sd ; + } + else + { + push @files, "$base/$file"; + } + } + + close( $dh ); + + $_ =~ s/^\/// for(@files); + return @files; +} diff --git a/esp-link/cgi.c b/esp-link/cgi.c index 96c03e9..76b1539 100644 --- a/esp-link/cgi.c +++ b/esp-link/cgi.c @@ -16,6 +16,7 @@ Some random cgi routines. #include #include "cgi.h" #include "config.h" +#include "web-server.h" #ifdef CGI_DBG #define DBG(format, ...) do { os_printf(format, ## __VA_ARGS__); } while(0) @@ -193,8 +194,7 @@ int ICACHE_FLASH_ATTR cgiMenu(HttpdConnData *connData) { if (connData->conn==NULL) return HTTPD_CGI_DONE; // Connection aborted. Clean up. char buff[1024]; // don't use jsonHeader so the response does get cached - httpdStartResponse(connData, 200); - httpdHeader(connData, "Cache-Control", "max-age=3600, must-revalidate"); + noCacheHeaders(connData, 200); httpdHeader(connData, "Content-Type", "application/json"); httpdEndHeaders(connData); // limit hostname to 12 chars @@ -213,12 +213,14 @@ int ICACHE_FLASH_ATTR cgiMenu(HttpdConnData *connData) { #ifdef MQTT "\"REST/MQTT\", \"/mqtt.html\", " #endif - "\"Debug log\", \"/log.html\"" + "\"Debug log\", \"/log.html\", " + "\"Web Server\", \"/web-server.html\"" + "%s" " ], " "\"version\": \"%s\", " "\"name\": \"%s\"" " }", - esp_link_version, name); + WEB_UserPages(), esp_link_version, name); httpdSend(connData, buff, -1); return HTTPD_CGI_DONE; diff --git a/esp-link/cgiservices.c b/esp-link/cgiservices.c index 200039d..ef76287 100644 --- a/esp-link/cgiservices.c +++ b/esp-link/cgiservices.c @@ -68,6 +68,7 @@ int ICACHE_FLASH_ATTR cgiSystemInfo(HttpdConnData *connData) { "\"name\": \"%s\", " "\"reset cause\": \"%d=%s\", " "\"size\": \"%s\", " + "\"upload-size\": \"%d\", " "\"id\": \"0x%02X 0x%04X\", " "\"partition\": \"%s\", " "\"slip\": \"%s\", " @@ -79,6 +80,7 @@ int ICACHE_FLASH_ATTR cgiSystemInfo(HttpdConnData *connData) { rst_info->reason, rst_codes[rst_info->reason], flash_maps[system_get_flash_size_map()], + getUserPageSectionEnd()-getUserPageSectionStart(), fid & 0xff, (fid & 0xff00) | ((fid >> 16) & 0xff), part_id ? "user2.bin" : "user1.bin", flashConfig.slip_enable ? "enabled" : "disabled", diff --git a/esp-link/cgiwebserversetup.c b/esp-link/cgiwebserversetup.c new file mode 100644 index 0000000..2a25950 --- /dev/null +++ b/esp-link/cgiwebserversetup.c @@ -0,0 +1,158 @@ +// Copyright (c) 2015 by Thorsten von Eicken, see LICENSE.txt in the esp-link repo + +#include +#include +#include "cgi.h" +#include "cgioptiboot.h" +#include "multipart.h" +#include "espfsformat.h" +#include "config.h" +#include "web-server.h" + +int upload_offset = 0; // flash offset where to store page upload +int html_header_len = 0; // HTML header length (for uploading HTML files) + +// this is the header to add if user uploads HTML file +const char * HTML_HEADER = "esp-link" + "" + "
"; + +// multipart callback for uploading user defined pages +int ICACHE_FLASH_ATTR webServerSetupMultipartCallback(MultipartCmd cmd, char *data, int dataLen, int position) +{ + switch(cmd) + { + case FILE_START: + upload_offset = 0; + html_header_len = 0; + // simple HTML file + if( ( dataLen > 5 ) && ( os_strcmp(data + dataLen - 5, ".html") == 0 ) ) // if the file ends with .html, wrap into an espfs image + { + // write the start block on esp-fs + int spi_flash_addr = getUserPageSectionStart(); + spi_flash_erase_sector(spi_flash_addr/SPI_FLASH_SEC_SIZE); + EspFsHeader hdr; + hdr.magic = 0xFFFFFFFF; // espfs magic is invalid during upload + hdr.flags = 0; + hdr.compression = 0; + + int len = dataLen + 1; + while(( len & 3 ) != 0 ) + len++; + + hdr.nameLen = len; + hdr.fileLenComp = hdr.fileLenDecomp = 0xFFFFFFFF; + + spi_flash_write( spi_flash_addr + upload_offset, (uint32_t *)(&hdr), sizeof(EspFsHeader) ); + upload_offset += sizeof(EspFsHeader); + + char nameBuf[len]; + os_memset(nameBuf, 0, len); + os_memcpy(nameBuf, data, dataLen); + + spi_flash_write( spi_flash_addr + upload_offset, (uint32_t *)(nameBuf), len ); + upload_offset += len; + + html_header_len = os_strlen(HTML_HEADER) & ~3; // upload only 4 byte aligned part + char buf[html_header_len]; + os_memcpy(buf, HTML_HEADER, html_header_len); + spi_flash_write( spi_flash_addr + upload_offset, (uint32_t *)(buf), html_header_len ); + upload_offset += html_header_len; + } + break; + case FILE_DATA: + if(( position < 4 ) && (upload_offset == 0)) // for espfs images check the magic number + { + for(int p = position; p < 4; p++ ) + { + if( data[p - position] != ((ESPFS_MAGIC >> (p * 8) ) & 255 ) ) + { + os_printf("Not an espfs image!\n"); + return 1; + } + data[p - position] = 0xFF; // espfs magic is invalid during upload + } + } + + int spi_flash_addr = getUserPageSectionStart() + upload_offset + position; + int spi_flash_end_addr = spi_flash_addr + dataLen; + if( spi_flash_end_addr + dataLen >= getUserPageSectionEnd() ) + { + os_printf("No more space in the flash!\n"); + return 1; + } + + int ptr = 0; + while( spi_flash_addr < spi_flash_end_addr ) + { + if (spi_flash_addr % SPI_FLASH_SEC_SIZE == 0){ + spi_flash_erase_sector(spi_flash_addr/SPI_FLASH_SEC_SIZE); + } + + int max = (spi_flash_addr | (SPI_FLASH_SEC_SIZE - 1)) + 1; + int len = spi_flash_end_addr - spi_flash_addr; + if( spi_flash_end_addr > max ) + len = max - spi_flash_addr; + + spi_flash_write( spi_flash_addr, (uint32_t *)(data + ptr), len ); + ptr += len; + spi_flash_addr += len; + } + + break; + case FILE_DONE: + { + if( html_header_len != 0 ) + { + // write the terminating block on esp-fs + int spi_flash_addr = getUserPageSectionStart() + upload_offset + position; + + uint32_t pad = 0; + uint8_t pad_cnt = (4 - position) & 3; + if( pad_cnt ) + spi_flash_write( spi_flash_addr, &pad, pad_cnt ); + + spi_flash_addr += pad_cnt; + + // create ESPFS image + EspFsHeader hdr; + hdr.magic = ESPFS_MAGIC; + hdr.flags = 1; + hdr.compression = 0; + hdr.nameLen = 0; + hdr.fileLenComp = hdr.fileLenDecomp = 0; + + spi_flash_write( spi_flash_addr, (uint32_t *)(&hdr), sizeof(EspFsHeader) ); + + uint32_t totallen = html_header_len + position; + + // restore ESPFS magic + spi_flash_write( (int)getUserPageSectionStart(), (uint32_t *)&hdr.magic, sizeof(uint32_t) ); + // set file size + spi_flash_write( (int)getUserPageSectionStart() + 8, &totallen, sizeof(uint32_t) ); + spi_flash_write( (int)getUserPageSectionStart() + 12, &totallen, sizeof(uint32_t) ); + } + else + { + // set espfs magic (set it valid) + uint32_t magic = ESPFS_MAGIC; + spi_flash_write( (int)getUserPageSectionStart(), (uint32_t *)&magic, sizeof(uint32_t) ); + } + WEB_Init(); // reload the content + } + break; + } + return 0; +} + +MultipartCtx * webServerContext = NULL; // multipart upload context for web server + +// this callback is called when user uploads the web-page +int ICACHE_FLASH_ATTR cgiWebServerSetupUpload(HttpdConnData *connData) +{ + if( webServerContext == NULL ) + webServerContext = multipartCreateContext( webServerSetupMultipartCallback ); + + return multipartProcess(webServerContext, connData); +} diff --git a/esp-link/cgiwebserversetup.h b/esp-link/cgiwebserversetup.h new file mode 100644 index 0000000..ffecb82 --- /dev/null +++ b/esp-link/cgiwebserversetup.h @@ -0,0 +1,8 @@ +#ifndef CGIWEBSERVER_H +#define CGIWEBSERVER_H + +#include + +int ICACHE_FLASH_ATTR cgiWebServerSetupUpload(HttpdConnData *connData); + +#endif /* CGIWEBSERVER_H */ diff --git a/esp-link/config.c b/esp-link/config.c index 1af160a..10adc55 100644 --- a/esp-link/config.c +++ b/esp-link/config.c @@ -195,3 +195,43 @@ getFlashSize() { return 0; return 1 << size_id; } + +const uint32_t getUserPageSectionStart() +{ + enum flash_size_map map = system_get_flash_size_map(); + switch(map) + { + case FLASH_SIZE_4M_MAP_256_256: + return FLASH_SECT + FIRMWARE_SIZE - 3*FLASH_SECT;// bootloader + firmware - 12KB (highly risky...) + case FLASH_SIZE_8M_MAP_512_512: + return FLASH_SECT + FIRMWARE_SIZE; + case FLASH_SIZE_16M_MAP_512_512: + case FLASH_SIZE_16M_MAP_1024_1024: + case FLASH_SIZE_32M_MAP_512_512: + case FLASH_SIZE_32M_MAP_1024_1024: + return 0x100000; + default: + return 0xFFFFFFFF; + } +} + +const uint32_t getUserPageSectionEnd() +{ + enum flash_size_map map = system_get_flash_size_map(); + switch(map) + { + case FLASH_SIZE_4M_MAP_256_256: + return FLASH_SECT + FIRMWARE_SIZE - 2*FLASH_SECT; + case FLASH_SIZE_8M_MAP_512_512: + return FLASH_SECT + FIRMWARE_SIZE + 2*FLASH_SECT; + case FLASH_SIZE_16M_MAP_512_512: + case FLASH_SIZE_16M_MAP_1024_1024: + return 0x1FC000; + case FLASH_SIZE_32M_MAP_512_512: + case FLASH_SIZE_32M_MAP_1024_1024: + return 0x3FC000; + default: + return 0xFFFFFFFF; + } +} + diff --git a/esp-link/config.h b/esp-link/config.h index 341d7e1..bf08df4 100644 --- a/esp-link/config.h +++ b/esp-link/config.h @@ -46,4 +46,7 @@ bool configRestore(void); void configWipe(void); const size_t getFlashSize(); +const uint32_t getUserPageSectionStart(); +const uint32_t getUserPageSectionEnd(); + #endif diff --git a/esp-link/main.c b/esp-link/main.c index 49a228a..596cfbd 100644 --- a/esp-link/main.c +++ b/esp-link/main.c @@ -19,6 +19,7 @@ #include "cgimqtt.h" #include "cgiflash.h" #include "cgioptiboot.h" +#include "cgiwebserversetup.h" #include "auth.h" #include "espfs.h" #include "uart.h" @@ -30,6 +31,7 @@ #include "log.h" #include "gpio.h" #include "cgiservices.h" +#include "web-server.h" #ifdef SYSLOG #include "syslog.h" @@ -96,6 +98,8 @@ HttpdBuiltInUrl builtInUrls[] = { #ifdef MQTT { "/mqtt", cgiMqtt, NULL }, #endif + { "/web-server/upload", cgiWebServerSetupUpload, NULL }, + { "*.json", WEB_CgiJsonHook, NULL }, //Catch-all cgi JSON queries { "*", cgiEspFsHook, NULL }, //Catch-all cgi function for the filesystem { NULL, NULL, NULL } }; @@ -147,11 +151,14 @@ void user_init(void) { // Wifi wifiInit(); // init the flash filesystem with the html stuff - espFsInit(&_binary_espfs_img_start); + espFsInit(espLinkCtx, &_binary_espfs_img_start, ESPFS_MEMORY); + //EspFsInitResult res = espFsInit(&_binary_espfs_img_start); //os_printf("espFsInit %s\n", res?"ERR":"ok"); // mount the http handlers httpdInit(builtInUrls, 80); + WEB_Init(); + // init the wifi-serial transparent bridge (port 23) serbridgeInit(23, 2323); uart_add_recv_cb(&serbridgeUartCb); diff --git a/espfs/espfs.c b/espfs/espfs.c index f9942f3..111bfd7 100644 --- a/espfs/espfs.c +++ b/espfs/espfs.c @@ -30,6 +30,7 @@ It's written for use with httpd, but doesn't need to be used as such. #define os_malloc malloc #define os_free free #define os_memcpy memcpy +#define os_memset memset #define os_strncmp strncmp #define os_strcmp strcmp #define os_strcpy strcpy @@ -40,9 +41,21 @@ It's written for use with httpd, but doesn't need to be used as such. #include "espfsformat.h" #include "espfs.h" -static char* espFsData = NULL; +EspFsContext espLinkCtxDef; +EspFsContext userPageCtxDef; + +EspFsContext * espLinkCtx = &espLinkCtxDef; +EspFsContext * userPageCtx = &userPageCtxDef; + +struct EspFsContext +{ + char* data; + EspFsSource source; + uint8_t valid; +}; struct EspFsFile { + EspFsContext *ctx; EspFsHeader *header; char decompressor; int32_t posDecomp; @@ -67,29 +80,12 @@ Accessing the flash through the mem emulation at 0x40200000 is a bit hairy: All a memory exception, crashing the program. */ -EspFsInitResult ICACHE_FLASH_ATTR espFsInit(void *flashAddress) { - // base address must be aligned to 4 bytes - if (((int)flashAddress & 3) != 0) { - return ESPFS_INIT_RESULT_BAD_ALIGN; - } - - // check if there is valid header at address - EspFsHeader testHeader; - os_memcpy(&testHeader, flashAddress, sizeof(EspFsHeader)); - if (testHeader.magic != ESPFS_MAGIC) { - return ESPFS_INIT_RESULT_NO_IMAGE; - } - - espFsData = (char *)flashAddress; - return ESPFS_INIT_RESULT_OK; -} - //Copies len bytes over from dst to src, but does it using *only* //aligned 32-bit reads. Yes, it's no too optimized but it's short and sweet and it works. //ToDo: perhaps os_memcpy also does unaligned accesses? #ifdef __ets__ -void ICACHE_FLASH_ATTR memcpyAligned(char *dst, char *src, int len) { +void ICACHE_FLASH_ATTR memcpyAligned(char *dst, const char *src, int len) { int x; int w, b; for (x=0; xsource == ESPFS_MEMORY ) + os_memcpy( dest, src, count ); + else + memcpyFromFlash(dest, src, count); +} + +// aligned memcpy on MEMORY/FLASH file systems +void espfs_memcpyAligned( EspFsContext * ctx, void * dest, const void * src, int count ) +{ + if( ctx->source == ESPFS_MEMORY ) + memcpyAligned(dest, src, count); + else + memcpyFromFlash(dest, src, count); +} + +// initializes an EspFs context +EspFsInitResult ICACHE_FLASH_ATTR espFsInit(EspFsContext *ctx, void *flashAddress, EspFsSource source) { + ctx->valid = 0; + ctx->source = source; + // base address must be aligned to 4 bytes + if (((int)flashAddress & 3) != 0) { + return ESPFS_INIT_RESULT_BAD_ALIGN; + } + + // check if there is valid header at address + EspFsHeader testHeader; + espfs_memcpy(ctx, &testHeader, flashAddress, sizeof(EspFsHeader)); + if (testHeader.magic != ESPFS_MAGIC) { + return ESPFS_INIT_RESULT_NO_IMAGE; + } + + ctx->data = (char *)flashAddress; + ctx->valid = 1; + return ESPFS_INIT_RESULT_OK; +} + // Returns flags of opened file. int ICACHE_FLASH_ATTR espFsFlags(EspFsFile *fh) { if (fh == NULL) { @@ -116,57 +157,93 @@ int ICACHE_FLASH_ATTR espFsFlags(EspFsFile *fh) { } int8_t flags; - memcpyAligned((char*)&flags, (char*)&fh->header->flags, 1); + espfs_memcpyAligned(fh->ctx, (char*)&flags, (char*)&fh->header->flags, 1); return (int)flags; } +// creates and initializes an iterator over the espfs file system +void ICACHE_FLASH_ATTR espFsIteratorInit(EspFsContext *ctx, EspFsIterator *iterator) +{ + if( ctx->data == NULL ) + { + iterator->ctx = NULL; + return; + } + iterator->ctx = ctx; + iterator->position = NULL; +} + +// moves iterator to the next file on espfs +// returns 1 if iterator move was successful, otherwise 0 (last file) +// iterator->header and iterator->name will contain file information +int ICACHE_FLASH_ATTR espFsIteratorNext(EspFsIterator *iterator) +{ + if( iterator->ctx == NULL ) + return 0; + + char * position = iterator->position; + if( position == NULL ) + position = iterator->ctx->data; // first node + else + { + // jump the iterator to the next file + + position+=sizeof(EspFsHeader) + iterator->header.nameLen+iterator->header.fileLenComp; + if ((int)position&3) position+=4-((int)position&3); //align to next 32bit val + } + + iterator->position = position; + EspFsHeader * hdr = &iterator->header; + espfs_memcpy(iterator->ctx, hdr, position, sizeof(EspFsHeader)); + + if (hdr->magic!=ESPFS_MAGIC) { +#ifdef ESPFS_DBG + os_printf("Magic mismatch. EspFS image broken.\n"); +#endif + return 0; + } + if (hdr->flags&FLAG_LASTFILE) { + //os_printf("End of image.\n"); + iterator->ctx = NULL; // invalidate the iterator + return 0; + } + + position += sizeof(EspFsHeader); + + //Grab the name of the file. + espfs_memcpy(iterator->ctx, iterator->name, position, sizeof(iterator->name)); + + return 1; +} + //Open a file and return a pointer to the file desc struct. -EspFsFile ICACHE_FLASH_ATTR *espFsOpen(char *fileName) { - if (espFsData == NULL) { +EspFsFile ICACHE_FLASH_ATTR *espFsOpen(EspFsContext *ctx, char *fileName) { + EspFsIterator it; + espFsIteratorInit(ctx, &it); + if (it.ctx == NULL) { #ifdef ESPFS_DBG os_printf("Call espFsInit first!\n"); #endif return NULL; } - char *p=espFsData; - char *hpos; - char namebuf[256]; - EspFsHeader h; - EspFsFile *r; //Strip initial slashes while(fileName[0]=='/') fileName++; - //Go find that file! - while(1) { - hpos=p; - //Grab the next file header. - os_memcpy(&h, p, sizeof(EspFsHeader)); - if (h.magic!=ESPFS_MAGIC) { -#ifdef ESPFS_DBG - os_printf("Magic mismatch. EspFS image broken.\n"); -#endif - return NULL; - } - if (h.flags&FLAG_LASTFILE) { - //os_printf("End of image.\n"); - return NULL; - } - //Grab the name of the file. - p+=sizeof(EspFsHeader); - os_memcpy(namebuf, p, sizeof(namebuf)); -// os_printf("Found file '%s'. Namelen=%x fileLenComp=%x, compr=%d flags=%d\n", -// namebuf, (unsigned int)h.nameLen, (unsigned int)h.fileLenComp, h.compression, h.flags); - if (os_strcmp(namebuf, fileName)==0) { + + //Search the file + while( espFsIteratorNext(&it) ) + { + if (os_strcmp(it.name, fileName)==0) { //Yay, this is the file we need! - p+=h.nameLen; //Skip to content. - r=(EspFsFile *)os_malloc(sizeof(EspFsFile)); //Alloc file desc mem + EspFsFile * r=(EspFsFile *)os_malloc(sizeof(EspFsFile)); //Alloc file desc mem //os_printf("Alloc %p[%d]\n", r, sizeof(EspFsFile)); if (r==NULL) return NULL; - r->header=(EspFsHeader *)hpos; - r->decompressor=h.compression; - r->posComp=p; - r->posStart=p; + r->ctx = ctx; + r->header=(EspFsHeader *)it.position; + r->decompressor=it.header.compression; + r->posComp=it.position + it.header.nameLen + sizeof(EspFsHeader); + r->posStart=it.position + it.header.nameLen + sizeof(EspFsHeader); r->posDecomp=0; - if (h.compression==COMPRESS_NONE) { + if (it.header.compression==COMPRESS_NONE) { r->decompData=NULL; } else { #ifdef ESPFS_DBG @@ -176,10 +253,8 @@ EspFsFile ICACHE_FLASH_ATTR *espFsOpen(char *fileName) { } return r; } - //We don't need this file. Skip name and file - p+=h.nameLen+h.fileLenComp; - if ((int)p&3) p+=4-((int)p&3); //align to next 32bit val } + return NULL; } //Read len bytes from the given file into buff. Returns the actual amount of bytes read. @@ -187,15 +262,15 @@ int ICACHE_FLASH_ATTR espFsRead(EspFsFile *fh, char *buff, int len) { int flen, fdlen; if (fh==NULL) return 0; //Cache file length. - memcpyAligned((char*)&flen, (char*)&fh->header->fileLenComp, 4); - memcpyAligned((char*)&fdlen, (char*)&fh->header->fileLenDecomp, 4); + espfs_memcpyAligned(fh->ctx, (char*)&flen, (char*)&fh->header->fileLenComp, 4); + espfs_memcpyAligned(fh->ctx, (char*)&fdlen, (char*)&fh->header->fileLenDecomp, 4); //Do stuff depending on the way the file is compressed. if (fh->decompressor==COMPRESS_NONE) { int toRead; toRead=flen-(fh->posComp-fh->posStart); if (len>toRead) len=toRead; // os_printf("Reading %d bytes from %x\n", len, (unsigned int)fh->posComp); - memcpyAligned(buff, fh->posComp, len); + espfs_memcpyAligned(fh->ctx, buff, fh->posComp, len); fh->posDecomp+=len; fh->posComp+=len; // os_printf("Done reading %d bytes, pos=%x\n", len, fh->posComp); @@ -211,5 +286,8 @@ void ICACHE_FLASH_ATTR espFsClose(EspFsFile *fh) { os_free(fh); } - +// checks if the file system is valid (detect if the content is an espfs image or random data) +int ICACHE_FLASH_ATTR espFsIsValid(EspFsContext *ctx) { + return ctx->valid; +} diff --git a/espfs/espfs.h b/espfs/espfs.h index c8e13e7..654bc27 100644 --- a/espfs/espfs.h +++ b/espfs/espfs.h @@ -1,19 +1,42 @@ #ifndef ESPFS_H #define ESPFS_H +#include "espfsformat.h" + typedef enum { ESPFS_INIT_RESULT_OK, ESPFS_INIT_RESULT_NO_IMAGE, ESPFS_INIT_RESULT_BAD_ALIGN, } EspFsInitResult; +// Only 1 MByte of the flash can be directly accessed with ESP8266 +// If flash size is >1 Mbyte, SDK API is required to retrieve flash content +typedef enum { + ESPFS_MEMORY, // read data directly from memory (fast, max 1 MByte) + ESPFS_FLASH, // read data from flash using SDK API (no limit for the size) +} EspFsSource; + typedef struct EspFsFile EspFsFile; +typedef struct EspFsContext EspFsContext; + +typedef struct { + EspFsHeader header; // the header of the current file + EspFsContext *ctx; // pointer to espfs context + char name[256]; // the name of the current file + char *position; // position of the iterator (pointer on the file system) +} EspFsIterator; + +extern EspFsContext * espLinkCtx; +extern EspFsContext * userPageCtx; -EspFsInitResult espFsInit(void *flashAddress); -EspFsFile *espFsOpen(char *fileName); +EspFsInitResult espFsInit(EspFsContext *ctx, void *flashAddress, EspFsSource source); +EspFsFile *espFsOpen(EspFsContext *ctx, char *fileName); +int espFsIsValid(EspFsContext *ctx); int espFsFlags(EspFsFile *fh); int espFsRead(EspFsFile *fh, char *buff, int len); void espFsClose(EspFsFile *fh); +void espFsIteratorInit(EspFsContext *ctx, EspFsIterator *iterator); +int espFsIteratorNext(EspFsIterator *iterator); #endif \ No newline at end of file diff --git a/html/home.html b/html/home.html index 138222e..de9862f 100644 --- a/html/home.html +++ b/html/home.html @@ -123,6 +123,12 @@
+
"); + + for (var i=0; i" + ths[i] + ""); + } + + html = html.concat(""); + } + + for (var i=1; i"); + + for (var j=0; j" + tds[j] + ""); + } + + html = html.concat(""); + } + + elem.innerHTML = html; + } + } + }); + + if( refreshRate != 0 ) + { + clearTimeout(refreshTimer); + refreshTimer = setTimeout( function () { + ajaxJson("GET", window.location.pathname + ".json?reason=refresh", notifyResponse ); + }, refreshRate ); + } +} + +function notifyButtonPressed( btnId ) +{ + ajaxJson("POST", window.location.pathname + ".json?reason=button\&id=" + btnId, notifyResponse); +} + +function refreshFormData() +{ + setTimeout( function () { + ajaxJson("GET", window.location.pathname + ".json?reason=refresh", function (resp) { + notifyResponse(resp); + if( loadCounter > 0 ) + { + loadCounter--; + refreshFormData(); + } + } ); + } , 250); +} + +function recalculateHiddenInputs() +{ + for(var i=0; i < hiddenInputs.length; i++) + { + var hinput = hiddenInputs[i]; + var name = hinput.name; + + var elems = document.getElementsByName(name); + for(var j=0; j < elems.length; j++ ) + { + var chk = elems[j]; + var inptp = chk.type; + if( inptp == "checkbox" ) { + if( chk.checked ) + { + hinput.disabled = true; + hinput.value = "on"; + } + else + { + hinput.disabled = false; + hinput.value = "off"; + } + } + } + } +} + +document.addEventListener("DOMContentLoaded", function(){ + // collect buttons + var btns = document.getElementsByTagName("button"); + var ndx; + + for (ndx = 0; ndx < btns.length; ndx++) { + var btn = btns[ndx]; + var id = btn.getAttribute("id"); + var onclk = btn.getAttribute("onclick"); + var type = btn.getAttribute("type"); + + if( id != null && onclk == null && type == "button" ) + { + var fn; + eval( "fn = function() { notifyButtonPressed(\"" + id + "\") }" ); + btn.onclick = fn; + } + } + + // collect forms + var frms = document.getElementsByTagName("form"); + + for (ndx = 0; ndx < frms.length; ndx++) { + var frm = frms[ndx]; + + var method = frm.method; + var action = frm.action; + + frm.method = "POST"; + frm.action = window.location.pathname + ".json?reason=submit"; + loadCounter = 4; + + frm.onsubmit = function () { + recalculateHiddenInputs(); + refreshFormData(); + return true; + }; + } + + // collect metas + var metas = document.getElementsByTagName("meta"); + + for (ndx = 0; ndx < metas.length; ndx++) { + var meta = metas[ndx]; + if( meta.getAttribute("name") == "refresh-rate" ) + { + refreshRate = meta.getAttribute("content"); + } + } + + // collect checkboxes + var inputs = document.getElementsByTagName("input"); + + for (ndx = 0; ndx < inputs.length; ndx++) { + var inp = inputs[ndx]; + if( inp.getAttribute("type") == "checkbox" ) + { + var name = inp.getAttribute("name"); + var hasHidden = false; + if( name != null ) + { + var inpelems = document.getElementsByName(name); + for(var i=0; i < inpelems.length; i++ ) + { + var inptp = inpelems[i].type; + if( inptp == "hidden" ) + hasHidden = true; + } + } + + if( !hasHidden ) + { + var parent = inp.parentElement; + + var input = document.createElement("input"); + input.type = "hidden"; + input.name = inp.name; + + parent.appendChild(input); + hiddenInputs.push(input); + } + } + } + + // load variables at first time + var loadVariables = function() { + ajaxJson("GET", window.location.pathname + ".json?reason=load", notifyResponse, + function () { setTimeout(loadVariables, 1000); } + ); + }; + loadVariables(); +}); diff --git a/html/web-server.html b/html/web-server.html new file mode 100644 index 0000000..a5a1d41 --- /dev/null +++ b/html/web-server.html @@ -0,0 +1,29 @@ +
+
+

Web Server

+
+ +
+

User defined web pages can be uploaded to esp-link. This is useful if esp-link acts as a web server while MCU provides + the measurement data.

+ +
+ The custom web page to upload: + + + +
+ +
+ + + diff --git a/httpd/httpd.c b/httpd/httpd.c index f3c5594..bc16649 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -132,6 +132,7 @@ static void ICACHE_FLASH_ATTR httpdRetireConn(HttpdConnData *conn) { if (conn->post->buff != NULL) os_free(conn->post->buff); conn->cgi = NULL; conn->post->buff = NULL; + conn->post->multipartBoundary = NULL; } //Stupid li'l helper function that returns the value of a hex char. @@ -354,14 +355,18 @@ static void ICACHE_FLASH_ATTR httpdProcessRequest(HttpdConnData *conn) { if (conn->cgi == NULL) { while (builtInUrls[i].url != NULL) { int match = 0; + int urlLen = os_strlen(builtInUrls[i].url); //See if there's a literal match if (os_strcmp(builtInUrls[i].url, conn->url) == 0) match = 1; //See if there's a wildcard match - if (builtInUrls[i].url[os_strlen(builtInUrls[i].url) - 1] == '*' && - os_strncmp(builtInUrls[i].url, conn->url, os_strlen(builtInUrls[i].url) - 1) == 0) match = 1; + if (builtInUrls[i].url[urlLen - 1] == '*' && + os_strncmp(builtInUrls[i].url, conn->url, urlLen - 1) == 0) match = 1; + else if (builtInUrls[i].url[0] == '*' && ( strlen(conn->url) >= urlLen -1 ) && + os_strncmp(builtInUrls[i].url + 1, conn->url + strlen(conn->url) - urlLen + 1, urlLen - 1) == 0) match = 1; if (match) { //os_printf("Is url index %d\n", i); conn->cgiData = NULL; + conn->cgiResponse = NULL; conn->cgi = builtInUrls[i].cgiCb; conn->cgiArg = builtInUrls[i].cgiArg; break; @@ -509,6 +514,7 @@ static void ICACHE_FLASH_ATTR httpdRecvCb(void *arg, char *data, unsigned short if (data[x] == '\n' && (char *)os_strstr(conn->priv->head, "\r\n\r\n") != NULL) { //Indicate we're done with the headers. conn->post->len = 0; + conn->post->multipartBoundary = NULL; //Reset url data conn->url = NULL; //Iterate over all received headers and parse them. @@ -621,3 +627,43 @@ void ICACHE_FLASH_ATTR httpdInit(HttpdBuiltInUrl *fixedUrls, int port) { espconn_accept(&httpdConn); espconn_tcp_set_max_con_allow(&httpdConn, MAX_CONN); } + +// looks up connection handle based on ip / port +HttpdConnData * ICACHE_FLASH_ATTR httpdLookUpConn(uint8_t * ip, int port) { + int i; + + for (i = 0; iconn == NULL) + continue; + if (conn->cgi == NULL) + continue; + if (conn->conn->proto.tcp->remote_port != port ) + continue; + if (os_memcmp(conn->conn->proto.tcp->remote_ip, ip, 4) != 0) + continue; + + return conn; + } + return NULL; +} + +// this method is used for setting the response of a CGI handler outside of the HTTP callback +// this method useful at the following scenario: +// Browser -> CGI handler -> MCU request +// MCU response -> CGI handler -> browser +// when MCU response arrives, the handler looks up connection based on ip/port and call httpdSetCGIResponse with the data to transmit + +int ICACHE_FLASH_ATTR httpdSetCGIResponse(HttpdConnData * conn, void * response) { + char sendBuff[MAX_SENDBUFF_LEN]; + conn->priv->sendBuff = sendBuff; + conn->priv->sendBuffLen = 0; + + conn->cgiResponse = response; + httpdProcessRequest(conn); + conn->cgiResponse = NULL; + + return HTTPD_CGI_DONE; +} diff --git a/httpd/httpd.h b/httpd/httpd.h index 33a0700..32d9394 100644 --- a/httpd/httpd.h +++ b/httpd/httpd.h @@ -30,6 +30,7 @@ struct HttpdConnData { const void *cgiArg; void *cgiData; void *cgiPrivData; // Used for streaming handlers storing state between requests + void *cgiResponse; // used for forwarding response to the CGI handler HttpdPriv *priv; cgiSendCallback cgi; HttpdPostData *post; @@ -66,5 +67,7 @@ void ICACHE_FLASH_ATTR httpdEndHeaders(HttpdConnData *conn); int ICACHE_FLASH_ATTR httpdGetHeader(HttpdConnData *conn, char *header, char *ret, int retLen); int ICACHE_FLASH_ATTR httpdSend(HttpdConnData *conn, const char *data, int len); void ICACHE_FLASH_ATTR httpdFlush(HttpdConnData *conn); +HttpdConnData * ICACHE_FLASH_ATTR httpdLookUpConn(uint8_t * ip, int port); +int ICACHE_FLASH_ATTR httpdSetCGIResponse(HttpdConnData * conn, void *response); #endif diff --git a/httpd/httpdespfs.c b/httpd/httpdespfs.c index c6f2c0c..e080bdc 100644 --- a/httpd/httpdespfs.c +++ b/httpd/httpdespfs.c @@ -14,11 +14,12 @@ Connector to let httpd use the espfs filesystem to serve the files in it. */ #include "httpdespfs.h" +#define MAX_URL_LEN 255 + // The static files marked with FLAG_GZIP are compressed and will be served with GZIP compression. // If the client does not advertise that he accepts GZIP send following warning message (telnet users for e.g.) static const char *gzipNonSupportedMessage = "HTTP/1.0 501 Not implemented\r\nServer: esp8266-httpd/"HTTPDVER"\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: 52\r\n\r\nYour browser does not accept gzip-compressed data.\r\n"; - //This is a catch-all cgi function. It takes the url passed to it, looks up the corresponding //path in the filesystem and if it exists, passes the file through. This simulates what a normal //webserver would do with static files. @@ -40,9 +41,21 @@ cgiEspFsHook(HttpdConnData *connData) { if (file==NULL) { //First call to this cgi. Open the file so we can read it. - file=espFsOpen(connData->url); + file=espFsOpen(espLinkCtx, connData->url); if (file==NULL) { - return HTTPD_CGI_NOTFOUND; + if( espFsIsValid(userPageCtx) ) + { + int maxLen = strlen(connData->url) * 2 + 1; + if( maxLen > MAX_URL_LEN ) + maxLen = MAX_URL_LEN; + char decodedURL[maxLen]; + httpdUrlDecode(connData->url, strlen(connData->url), decodedURL, maxLen); + file = espFsOpen(userPageCtx, decodedURL ); + if( file == NULL ) + return HTTPD_CGI_NOTFOUND; + } + else + return HTTPD_CGI_NOTFOUND; } // The gzip checking code is intentionally without #ifdefs because checking diff --git a/httpd/multipart.c b/httpd/multipart.c new file mode 100644 index 0000000..d709798 --- /dev/null +++ b/httpd/multipart.c @@ -0,0 +1,301 @@ +#include +#include + +#include "multipart.h" +#include "cgi.h" + +#define BOUNDARY_SIZE 100 + +typedef enum { + STATE_SEARCH_BOUNDARY = 0, // state: searching multipart boundary + STATE_SEARCH_HEADER, // state: search multipart file header + STATE_SEARCH_HEADER_END, // state: search the end of the file header + STATE_UPLOAD_FILE, // state: read file content + STATE_ERROR, // state: error (stop processing) +} MultipartState; + +struct _MultipartCtx { + MultipartCallback callBack; // callback for multipart events + int position; // current file position + int startTime; // timestamp when connection was initiated + int recvPosition; // receive position (how many bytes was processed from the HTTP post) + char * boundaryBuffer; // buffer used for boundary detection + int boundaryBufferPtr; // pointer in the boundary buffer + MultipartState state; // multipart processing state +}; + +// this method is responsible for creating the multipart context +MultipartCtx * ICACHE_FLASH_ATTR multipartCreateContext(MultipartCallback callback) +{ + MultipartCtx * ctx = (MultipartCtx *)os_malloc(sizeof(MultipartCtx)); + ctx->callBack = callback; + ctx->position = ctx->startTime = ctx->recvPosition = ctx->boundaryBufferPtr = 0; + ctx->boundaryBuffer = NULL; + ctx->state = STATE_SEARCH_BOUNDARY; + return ctx; +} + +// for allocating buffer for multipart upload +void ICACHE_FLASH_ATTR multipartAllocBoundaryBuffer(MultipartCtx * context) +{ + if( context->boundaryBuffer == NULL ) + context->boundaryBuffer = (char *)os_malloc(3*BOUNDARY_SIZE + 1); + context->boundaryBufferPtr = 0; +} + +// for freeing multipart buffer +void ICACHE_FLASH_ATTR multipartFreeBoundaryBuffer(MultipartCtx * context) +{ + if( context->boundaryBuffer != NULL ) + { + os_free(context->boundaryBuffer); + context->boundaryBuffer = NULL; + } +} + +// for destroying the context +void ICACHE_FLASH_ATTR multipartDestroyContext(MultipartCtx * context) +{ + multipartFreeBoundaryBuffer(context); + os_free(context); +} + +// this is because of os_memmem is missing +void * mp_memmem(const void *l, size_t l_len, const void *s, size_t s_len) +{ + register char *cur, *last; + const char *cl = (const char *)l; + const char *cs = (const char *)s; + + /* we need something to compare */ + if (l_len == 0 || s_len == 0) + return NULL; + + /* "s" must be smaller or equal to "l" */ + if (l_len < s_len) + return NULL; + + /* special case where s_len == 1 */ + if (s_len == 1) + return memchr(l, (int)*cs, l_len); + + /* the last position where its possible to find "s" in "l" */ + last = (char *)cl + l_len - s_len; + + for (cur = (char *)cl; cur <= last; cur++) + if (cur[0] == cs[0] && memcmp(cur, cs, s_len) == 0) + return cur; + + return NULL; +} + + +// this method is for processing data coming from the HTTP post request +// context: the multipart context +// boundary: a string which indicates boundary +// data: the received data +// len: the received data length (can't be bigger than BOUNDARY_SIZE) +// last: last packet indicator +// +// Detecting a boundary is not easy. One has to take care of boundaries which are splitted in 2 packets +// [Packet 1, 5 bytes of the boundary][Packet 2, remaining 10 bytes of the boundary]; +// +// Algorythm: +// - create a buffer which size is 3*BOUNDARY_SIZE +// - put data into the buffer as long as the buffer size is smaller than 2*BOUNDARY_SIZE +// - search boundary in the received buffer, if found: boundary reached -> process data before boundary -> process boundary +// - if not found -> process the first BOUNDARY_SIZE amount of bytes from the buffer +// - remove processed data from the buffer +// this algorythm guarantees that no boundary loss will happen + +int ICACHE_FLASH_ATTR multipartProcessData(MultipartCtx * context, char * boundary, char * data, int len, int last) +{ + if( len != 0 ) // add data to the boundary buffer + { + os_memcpy(context->boundaryBuffer + context->boundaryBufferPtr, data, len); + + context->boundaryBufferPtr += len; + context->boundaryBuffer[context->boundaryBufferPtr] = 0; + } + + while( context->boundaryBufferPtr > 0 ) + { + if( ! last && context->boundaryBufferPtr <= 2 * BOUNDARY_SIZE ) // return if buffer is too small and not the last packet is processed + return 0; + + int dataSize = BOUNDARY_SIZE; + + char * boundaryLoc = mp_memmem( context->boundaryBuffer, context->boundaryBufferPtr, boundary, os_strlen(boundary) ); + if( boundaryLoc != NULL ) + { + int pos = boundaryLoc - context->boundaryBuffer; + if( pos > BOUNDARY_SIZE ) // process in the next call + boundaryLoc = NULL; + else + dataSize = pos; + } + + if( dataSize != 0 ) // data to process + { + switch( context->state ) + { + case STATE_SEARCH_HEADER: + case STATE_SEARCH_HEADER_END: + { + char * chr = os_strchr( context->boundaryBuffer, '\n' ); + if( chr != NULL ) + { + // chop datasize to contain only one line + int pos = chr - context->boundaryBuffer + 1; + if( pos < dataSize ) // if chop smaller than the dataSize, delete the boundary + { + dataSize = pos; + boundaryLoc = NULL; // process boundary next time + } + if( context->state == STATE_SEARCH_HEADER_END ) + { + if( pos == 1 || ( ( pos == 2 ) && ( context->boundaryBuffer[0] == '\r' ) ) ) // empty line? + { + context->state = STATE_UPLOAD_FILE; + context->position = 0; + } + } + else if( os_strncmp( context->boundaryBuffer, "Content-Disposition:", 20 ) == 0 ) + { + char * fnam = os_strstr( context->boundaryBuffer, "filename=" ); + if( fnam != NULL ) + { + int pos = fnam - context->boundaryBuffer + 9; + if( pos < dataSize ) + { + while(context->boundaryBuffer[pos] == ' ') pos++; // skip spaces + if( context->boundaryBuffer[pos] == '"' ) // quote start + { + pos++; + int start = pos; + while( pos < context->boundaryBufferPtr ) + { + if( context->boundaryBuffer[pos] == '"' ) // quote end + break; + pos++; + } + if( pos < context->boundaryBufferPtr ) + { + context->boundaryBuffer[pos] = 0; // terminating zero for the file name + os_printf("Uploading file: %s\n", context->boundaryBuffer + start); + if( context->callBack( FILE_START, context->boundaryBuffer + start, pos - start, 0 ) ) // FILE_START callback + return 1; // if an error happened + context->boundaryBuffer[pos] = '"'; // restore the original quote + context->state = STATE_SEARCH_HEADER_END; + } + } + } + } + } + } + } + break; + case STATE_UPLOAD_FILE: + { + char c = context->boundaryBuffer[dataSize]; + context->boundaryBuffer[dataSize] = 0; // add terminating zero (for easier handling) + if( context->callBack( FILE_DATA, context->boundaryBuffer, dataSize, context->position ) ) // FILE_DATA callback + return 1; + context->boundaryBuffer[dataSize] = c; + context->position += dataSize; + } + break; + default: + break; + } + } + + if( boundaryLoc != NULL ) // boundary found? + { + dataSize += os_strlen(boundary); // jump over the boundary + if( context->state == STATE_UPLOAD_FILE ) + { + if( context->callBack( FILE_DONE, NULL, 0, context->position ) ) // file done callback + return 1; // if an error happened + os_printf("File upload done\n"); + } + + context->state = STATE_SEARCH_HEADER; // search the next header + } + + // move the buffer back with dataSize + context->boundaryBufferPtr -= dataSize; + os_memcpy(context->boundaryBuffer, context->boundaryBuffer + dataSize, context->boundaryBufferPtr); + } + return 0; +} + +// for processing multipart requests +int ICACHE_FLASH_ATTR multipartProcess(MultipartCtx * context, HttpdConnData * connData ) +{ + if (connData->conn==NULL) return HTTPD_CGI_DONE; // Connection aborted. Clean up. + + if (connData->requestType == HTTPD_METHOD_POST) { + HttpdPostData *post = connData->post; + + if( post->multipartBoundary == NULL ) + { + errorResponse(connData, 404, "Only multipart POST is supported"); + return HTTPD_CGI_DONE; + } + + if( connData->startTime != context->startTime ) + { + // reinitialize, as this is a new request + context->position = 0; + context->recvPosition = 0; + context->startTime = connData->startTime; + context->state = STATE_SEARCH_BOUNDARY; + + multipartAllocBoundaryBuffer(context); + } + + if( context->state != STATE_ERROR ) + { + int feed = 0; + while( feed < post->buffLen ) + { + int len = post->buffLen - feed; + if( len > BOUNDARY_SIZE ) + len = BOUNDARY_SIZE; + if( multipartProcessData(context, post->multipartBoundary, post->buff + feed, len, 0) ) + { + context->state = STATE_ERROR; + break; + } + feed += len; + } + } + + context->recvPosition += post->buffLen; + if( context->recvPosition < post->len ) + return HTTPD_CGI_MORE; + + if( context->state != STATE_ERROR ) + { + // this is the last package, process the remaining data + if( multipartProcessData(context, post->multipartBoundary, NULL, 0, 1) ) + context->state = STATE_ERROR; + } + + multipartFreeBoundaryBuffer( context ); + + if( context->state == STATE_ERROR ) + errorResponse(connData, 400, "Invalid file upload!"); + else + { + httpdStartResponse(connData, 204); + httpdEndHeaders(connData); + } + return HTTPD_CGI_DONE; + } + else { + errorResponse(connData, 404, "Only multipart POST is supported"); + return HTTPD_CGI_DONE; + } +} diff --git a/httpd/multipart.h b/httpd/multipart.h new file mode 100644 index 0000000..87a4bb2 --- /dev/null +++ b/httpd/multipart.h @@ -0,0 +1,32 @@ +#ifndef MULTIPART_H +#define MULTIPART_H + +#include + +typedef enum { + FILE_START, // multipart: the start of a new file + FILE_DATA, // multipart: file data + FILE_DONE, // multipart: file end +} MultipartCmd; + +// multipart callback +// -> FILE_START : data+dataLen contains the filename, position is 0 +// -> FILE_DATA : data+dataLen contains file data, position is the file position +// -> FILE_DONE : data+dataLen is 0, position is the complete file size + +typedef int (* MultipartCallback)(MultipartCmd cmd, char *data, int dataLen, int position); + +struct _MultipartCtx; // the context for multipart listening + +typedef struct _MultipartCtx MultipartCtx; + +// use this for creating a multipart context +MultipartCtx * ICACHE_FLASH_ATTR multipartCreateContext(MultipartCallback callback); + +// for destroying multipart context +void ICACHE_FLASH_ATTR multipartDestroyContext(MultipartCtx * context); + +// use this function for processing HTML multipart updates +int ICACHE_FLASH_ATTR multipartProcess(MultipartCtx * context, HttpdConnData * post ); + +#endif /* MULTIPART_H */ diff --git a/serial/serbridge.c b/serial/serbridge.c index 66b180c..9f782bb 100644 --- a/serial/serbridge.c +++ b/serial/serbridge.c @@ -23,7 +23,7 @@ static struct espconn serbridgeConn2; // programming port static esp_tcp serbridgeTcp1, serbridgeTcp2; static int8_t mcu_reset_pin, mcu_isp_pin; -extern uint8_t slip_disabled; // disable slip to allow flashing of attached MCU +uint8_t in_mcu_flashing; // for disabling slip during MCU flashing void (*programmingCB)(char *buffer, short length) = NULL; @@ -124,14 +124,14 @@ telnetUnwrap(uint8_t *inBuf, int len, uint8_t state) #ifdef SERBR_DBG else { os_printf("MCU isp: no pin\n"); } #endif - slip_disabled++; + in_mcu_flashing++; break; case RTS_OFF: if (mcu_isp_pin >= 0) { GPIO_OUTPUT_SET(mcu_isp_pin, 1); os_delay_us(100L); } - if (slip_disabled > 0) slip_disabled--; + if (in_mcu_flashing > 0) in_mcu_flashing--; break; } state = TN_end; @@ -222,7 +222,7 @@ serbridgeRecvCb(void *arg, char *data, unsigned short len) //if (mcu_isp_pin >= 0) GPIO_OUTPUT_SET(mcu_isp_pin, 1); os_delay_us(1000L); // wait a millisecond before writing to the UART below conn->conn_mode = cmPGM; - slip_disabled++; // disable SLIP so it doesn't interfere with flashing + in_mcu_flashing++; // disable SLIP so it doesn't interfere with flashing #ifdef SKIP_AT_RESET serledFlash(50); // short blink on serial LED return; @@ -355,7 +355,7 @@ serbridgeUartCb(char *buf, short length) { if (programmingCB) { programmingCB(buf, length); - } else if (!flashConfig.slip_enable || slip_disabled > 0) { + } else if (!flashConfig.slip_enable || in_mcu_flashing > 0) { //os_printf("SLIP: disabled got %d\n", length); console_process(buf, length); } else { @@ -504,3 +504,8 @@ serbridgeInit(int port1, int port2) espconn_tcp_set_max_con_allow(&serbridgeConn2, MAX_CONN); espconn_regist_time(&serbridgeConn2, SER_BRIDGE_TIMEOUT, 0); } + +int ICACHE_FLASH_ATTR serbridgeInMCUFlashing() +{ + return in_mcu_flashing; +} diff --git a/serial/serbridge.h b/serial/serbridge.h index aad593e..ed661e1 100644 --- a/serial/serbridge.h +++ b/serial/serbridge.h @@ -36,6 +36,8 @@ void ICACHE_FLASH_ATTR serbridgeInitPins(void); void ICACHE_FLASH_ATTR serbridgeUartCb(char *buf, short len); void ICACHE_FLASH_ATTR serbridgeReset(); +int ICACHE_FLASH_ATTR serbridgeInMCUFlashing(); + // callback when receiving UART chars when in programming mode extern void (*programmingCB)(char *buffer, short length); diff --git a/serial/slip.c b/serial/slip.c index 76d8f97..2f15186 100644 --- a/serial/slip.c +++ b/serial/slip.c @@ -13,8 +13,6 @@ #define DBG(format, ...) do { } while(0) #endif -uint8_t slip_disabled; // temporarily disable slip to allow flashing of attached MCU - extern void ICACHE_FLASH_ATTR console_process(char *buf, short len); // This SLIP parser tries to conform to RFC 1055 https://tools.ietf.org/html/rfc1055. diff --git a/web-server/web-server.c b/web-server/web-server.c new file mode 100644 index 0000000..0d323a2 --- /dev/null +++ b/web-server/web-server.c @@ -0,0 +1,454 @@ +#include "web-server.h" + +#include + +#include "espfs.h" +#include "config.h" +#include "cgi.h" +#include "cmd.h" +#include "serbridge.h" + +// the file is responsible for handling user defined web-pages +// - collects HTML files from user image, shows them on the left frame +// - handles JSON data coming from the browser +// - handles SLIP messages coming from MCU + +#define WEB_CB "webCb" +#define MAX_ARGUMENT_BUFFER_SIZE 1024 + +struct ArgumentBuffer +{ + char argBuffer[MAX_ARGUMENT_BUFFER_SIZE]; + int argBufferPtr; + int numberOfArgs; +}; + +static char* web_server_reasons[] = { + "load", // readable name for RequestReason::LOAD + "refresh", // readable name for RequestReason::REFRESH + "button", // readable name for RequestReason::BUTTON + "submit" // readable name for RequestReason::SUBMIT +}; + +// this variable contains the names of the user defined pages +// this information appears at the left frame below of the built in URL-s +// format: ,"UserPage1", "/UserPage1.html", "UserPage2", "/UserPage2.html", +char * webServerPages = NULL; + +char * ICACHE_FLASH_ATTR WEB_UserPages() +{ + return webServerPages; +} + +// generates the content of webServerPages variable (called at booting/web page uploading) +void ICACHE_FLASH_ATTR WEB_BrowseFiles() +{ + char buffer[1024]; + buffer[0] = 0; + + if( espFsIsValid( userPageCtx ) ) + { + EspFsIterator it; + espFsIteratorInit(userPageCtx, &it); + while( espFsIteratorNext(&it) ) + { + int nameLen = os_strlen(it.name); + if( nameLen >= 6 ) + { + // fetch HTML files + if( os_strcmp( it.name + nameLen-5, ".html" ) == 0 ) + { + int slashPos = nameLen - 5; + + // chop path and .html from the name + while( slashPos > 0 && it.name[slashPos-1] != '/' ) + slashPos--; + + // here we check buffer overrun + int maxLen = 10 + os_strlen( it.name ) + (nameLen - slashPos -5); + if( maxLen >= sizeof(buffer) ) + break; + + os_strcat(buffer, ", \""); + + int writePos = os_strlen(buffer); + for( int i=slashPos; i < nameLen-5; i++ ) + buffer[writePos++] = it.name[i]; + buffer[writePos] = 0; // terminating zero + + os_strcat(buffer, "\", \"/"); + os_strcat(buffer, it.name); + os_strcat(buffer, "\""); + } + } + } + } + + if( webServerPages != NULL ) + os_free( webServerPages ); + + int len = os_strlen(buffer) + 1; + webServerPages = (char *)os_malloc( len ); + os_memcpy( webServerPages, buffer, len ); +} + +// initializer +void ICACHE_FLASH_ATTR WEB_Init() +{ + espFsInit(userPageCtx, (void *)getUserPageSectionStart(), ESPFS_FLASH); + if( espFsIsValid( userPageCtx ) ) + os_printf("Valid user file system found!\n"); + else + os_printf("No user file system found!\n"); + WEB_BrowseFiles(); // collect user defined HTML files +} + +// initializes the argument buffer +static void WEB_argInit(struct ArgumentBuffer * argBuffer) +{ + argBuffer->numberOfArgs = 0; + argBuffer->argBufferPtr = 0; +} + +// adds an argument to the argument buffer (returns 0 if successful) +static int WEB_addArg(struct ArgumentBuffer * argBuffer, char * arg, int argLen ) +{ + if( argBuffer->argBufferPtr + argLen + sizeof(int) >= MAX_ARGUMENT_BUFFER_SIZE ) + return -1; // buffer overflow + + os_memcpy(argBuffer->argBuffer + argBuffer->argBufferPtr, &argLen, sizeof(int)); + + if( argLen != 0 ) + { + os_memcpy( argBuffer->argBuffer + argBuffer->argBufferPtr + sizeof(int), arg, argLen ); + argBuffer->numberOfArgs++; + } + + argBuffer->argBufferPtr += argLen + sizeof(int); + return 0; +} + +// creates and sends a SLIP message from the argument buffer +static void WEB_sendArgBuffer(struct ArgumentBuffer * argBuffer, HttpdConnData *connData, int id, RequestReason reason) +{ + cmdResponseStart(CMD_WEB_REQ_CB, id, 4 + argBuffer->numberOfArgs); + uint16_t r = (uint16_t)reason; + cmdResponseBody(&r, sizeof(uint16_t)); // 1st argument: reason + cmdResponseBody(&connData->conn->proto.tcp->remote_ip, 4); // 2nd argument: IP + cmdResponseBody(&connData->conn->proto.tcp->remote_port, sizeof(uint16_t)); // 3rd argument: port + cmdResponseBody(connData->url, os_strlen(connData->url)); // 4th argument: URL + + int p = 0; + for( int j=0; j < argBuffer->numberOfArgs; j++ ) + { + int argLen; + os_memcpy( &argLen, argBuffer->argBuffer + p, sizeof(int) ); + + char * arg = argBuffer->argBuffer + p + sizeof(int); + cmdResponseBody(arg, argLen); + p += argLen + sizeof(int); + } + + cmdResponseEnd(); +} + +// this method processes SLIP data from MCU and converts to JSON +// this method receives JSON from the browser, sends SLIP data to MCU +static int ICACHE_FLASH_ATTR WEB_handleJSONRequest(HttpdConnData *connData) +{ + if( !flashConfig.slip_enable ) + { + errorResponse(connData, 400, "Slip processing is disabled!"); + return HTTPD_CGI_DONE; + } + CmdCallback* cb = cmdGetCbByName( WEB_CB ); + if( cb == NULL ) + { + errorResponse(connData, 500, "No MCU callback is registered!"); + return HTTPD_CGI_DONE; + } + if( serbridgeInMCUFlashing() ) + { + errorResponse(connData, 500, "Slip disabled at uploading program onto the MCU!"); + return HTTPD_CGI_DONE; + } + + char reasonBuf[16]; + int i; + int len = httpdFindArg(connData->getArgs, "reason", reasonBuf, sizeof(reasonBuf)); + if( len < 0 ) + { + errorResponse(connData, 400, "No reason specified!"); + return HTTPD_CGI_DONE; + } + + RequestReason reason = INVALID; + for(i=0; i < sizeof(web_server_reasons)/sizeof(char *); i++) + { + if( os_strcmp( web_server_reasons[i], reasonBuf ) == 0 ) + reason = (RequestReason)i; + } + + if( reason == INVALID ) + { + errorResponse(connData, 400, "Invalid reason!"); + return HTTPD_CGI_DONE; + } + + struct ArgumentBuffer argBuffer; + WEB_argInit( &argBuffer ); + + switch(reason) + { + case BUTTON: + { + char id_buf[40]; + + int id_len = httpdFindArg(connData->getArgs, "id", id_buf, sizeof(id_buf)); + if( id_len <= 0 ) + { + errorResponse(connData, 400, "No button ID specified!"); + return HTTPD_CGI_DONE; + } + if( WEB_addArg(&argBuffer, id_buf, id_len) ) + { + errorResponse(connData, 400, "Post too large!"); + return HTTPD_CGI_DONE; + } + } + break; + case SUBMIT: + { + if( connData->post->received < connData->post->len ) + { + errorResponse(connData, 400, "Post too large!"); + return HTTPD_CGI_DONE; + } + + int bptr = 0; + + while( bptr < connData->post->len ) + { + char * line = connData->post->buff + bptr; + + char * eo = os_strchr(line, '&' ); + if( eo != NULL ) + { + *eo = 0; + bptr = eo - connData->post->buff + 1; + } + else + { + eo = line + os_strlen( line ); + bptr = connData->post->len; + } + + int len = os_strlen(line); + while( len >= 1 && ( line[len-1] == '\r' || line[len-1] == '\n' )) + len--; + line[len] = 0; + + char * val = os_strchr(line, '='); + if( val != NULL ) + { + *val = 0; + char * name = line; + int vblen = os_strlen(val+1) * 2; + char value[vblen]; + httpdUrlDecode(val+1, strlen(val+1), value, vblen); + + int namLen = os_strlen(name); + int valLen = os_strlen(value); + + char arg[namLen + valLen + 3]; + int argPtr = 0; + arg[argPtr++] = (char)WEB_STRING; + os_strcpy( arg + argPtr, name ); + argPtr += namLen; + arg[argPtr++] = 0; + os_strcpy( arg + argPtr, value ); + argPtr += valLen; + + if( WEB_addArg(&argBuffer, arg, argPtr) ) + { + errorResponse(connData, 400, "Post too large!"); + return HTTPD_CGI_DONE; + } + } + } + } + break; + case LOAD: + case REFRESH: + default: + break; + } + + if( WEB_addArg(&argBuffer, NULL, 0) ) + { + errorResponse(connData, 400, "Post too large!"); + return HTTPD_CGI_DONE; + } + + os_printf("Web callback to MCU: %s\n", reasonBuf); + + WEB_sendArgBuffer(&argBuffer, connData, (uint32_t)cb->callback, reason ); + + if( reason == SUBMIT ) + { + httpdStartResponse(connData, 204); + httpdEndHeaders(connData); + return HTTPD_CGI_DONE; + } + + return HTTPD_CGI_MORE; +} + +// this method receives SLIP data from MCU sends JSON to the browser +static int ICACHE_FLASH_ATTR WEB_handleMCUResponse(HttpdConnData *connData, CmdRequest * response) +{ + char jsonBuf[1500]; + int jsonPtr = 0; + + + jsonBuf[jsonPtr++] = '{'; + + int c = 2; + while( c++ < cmdGetArgc(response) ) + { + int len = cmdArgLen(response); + char buf[len+1]; + buf[len] = 0; + + cmdPopArg(response, buf, len); + + if(len == 0) + break; // last argument + + if( c > 3 ) // skip the first argument + jsonBuf[jsonPtr++] = ','; + + if( jsonPtr + 20 + len > sizeof(jsonBuf) ) + { + errorResponse(connData, 500, "Response too large!"); + return HTTPD_CGI_DONE; + } + + WebValueType type = (WebValueType)buf[0]; + + int nameLen = os_strlen(buf+1); + jsonBuf[jsonPtr++] = '"'; + os_memcpy(jsonBuf + jsonPtr, buf + 1, nameLen); + jsonPtr += nameLen; + jsonBuf[jsonPtr++] = '"'; + jsonBuf[jsonPtr++] = ':'; + + char * value = buf + 2 + nameLen; + + switch(type) + { + case WEB_NULL: + os_memcpy(jsonBuf + jsonPtr, "null", 4); + jsonPtr += 4; + break; + case WEB_INTEGER: + { + int v; + os_memcpy( &v, value, 4); + + char intbuf[20]; + os_sprintf(intbuf, "%d", v); + os_strcpy(jsonBuf + jsonPtr, intbuf); + jsonPtr += os_strlen(intbuf); + } + break; + case WEB_BOOLEAN: + if( *value ) { + os_memcpy(jsonBuf + jsonPtr, "true", 4); + jsonPtr += 4; + } else { + os_memcpy(jsonBuf + jsonPtr, "false", 5); + jsonPtr += 5; + } + break; + case WEB_FLOAT: + { + float f; + os_memcpy( &f, value, 4); + + char intbuf[20]; + os_sprintf(intbuf, "%f", f); + os_strcpy(jsonBuf + jsonPtr, intbuf); + jsonPtr += os_strlen(intbuf); + } + break; + case WEB_STRING: + jsonBuf[jsonPtr++] = '"'; + while(*value) + { + if( *value == '\\' || *value == '"' ) + jsonBuf[jsonPtr++] = '\\'; + jsonBuf[jsonPtr++] = *(value++); + } + jsonBuf[jsonPtr++] = '"'; + break; + case WEB_JSON: + os_memcpy(jsonBuf + jsonPtr, value, len - 2 - nameLen); + jsonPtr += len - 2 - nameLen; + break; + } + } + + jsonBuf[jsonPtr++] = '}'; + + noCacheHeaders(connData, 200); + httpdHeader(connData, "Content-Type", "application/json"); + + char cl[16]; + os_sprintf(cl, "%d", jsonPtr); + httpdHeader(connData, "Content-Length", cl); + httpdEndHeaders(connData); + + httpdSend(connData, jsonBuf, jsonPtr); + return HTTPD_CGI_DONE; +} + +// this method is responsible for the MCU <==JSON==> Browser communication +int ICACHE_FLASH_ATTR WEB_CgiJsonHook(HttpdConnData *connData) +{ + if (connData->conn==NULL) return HTTPD_CGI_DONE; // Connection aborted. Clean up. + + void * cgiData = connData->cgiData; + + if( cgiData == NULL ) + { + connData->cgiData = (void *)1; // indicate, that request was processed + return WEB_handleJSONRequest(connData); + } + + if( connData->cgiResponse != NULL ) // data from MCU + return WEB_handleMCUResponse(connData, (CmdRequest *)(connData->cgiResponse)); + + return HTTPD_CGI_MORE; +} + +// this method is called when MCU transmits WEB_DATA command +void ICACHE_FLASH_ATTR WEB_Data(CmdPacket *cmd) +{ + CmdRequest req; + cmdRequest(&req, cmd); + + if (cmdGetArgc(&req) < 2) return; + + uint8_t ip[4]; + cmdPopArg(&req, ip, 4); // pop the IP address + + uint16_t port; + cmdPopArg(&req, &port, 2); // pop the HTTP port + + HttpdConnData * conn = httpdLookUpConn(ip, port); // look up connection based on IP/port + if( conn != NULL && conn->cgi == WEB_CgiJsonHook ) // make sure that the right CGI handler is configured + httpdSetCGIResponse( conn, &req ); + else + os_printf("WEB_DATA ignored as no valid HTTP connection found!\n"); +} diff --git a/web-server/web-server.h b/web-server/web-server.h new file mode 100644 index 0000000..0827229 --- /dev/null +++ b/web-server/web-server.h @@ -0,0 +1,37 @@ +#ifndef WEB_SERVER_H +#define WEB_SERVER_H + +#include + +#include "httpd.h" +#include "cmd.h" + +typedef enum +{ + LOAD=0, // loading web-page content at the first time + REFRESH, // loading web-page subsequently + BUTTON, // HTML button pressed + SUBMIT, // HTML form is submitted + + INVALID=-1, +} RequestReason; + +typedef enum +{ + WEB_STRING=0, // the value is string + WEB_NULL, // the value is NULL + WEB_INTEGER, // the value is integer + WEB_BOOLEAN, // the value is boolean + WEB_FLOAT, // the value is float + WEB_JSON // the value is JSON data +} WebValueType; + +void WEB_Init(); + +char * WEB_UserPages(); + +int WEB_CgiJsonHook(HttpdConnData *connData); +void WEB_Data(CmdPacket *cmd); + +#endif /* WEB_SERVER_H */ +
Webpage size +
+ + +
+
Current partition