mirror of https://github.com/jeelabs/esp-link.git
Merge 687898e8c1
into 4dcd61714b
commit
06c384badd
@ -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 |
||||||
|
|
@ -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; |
||||||
|
} |
||||||
|
|
@ -0,0 +1,91 @@ |
|||||||
|
#ifndef ESP_LINK_H |
||||||
|
#define ESP_LINK_H |
||||||
|
|
||||||
|
#include <inttypes.h> |
||||||
|
#include <Stream.h> |
||||||
|
|
||||||
|
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 */ |
||||||
|
|
@ -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; |
||||||
|
} |
||||||
|
|
@ -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 */ |
@ -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(); |
||||||
|
} |
||||||
|
|
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@ |
|||||||
|
all: user_img |
||||||
|
|
||||||
|
clean: |
||||||
|
rm -rf web-page.espfs.img
|
||||||
|
|
||||||
|
user_img: |
||||||
|
../../../../../createEspFs.pl web-page web-page.espfs.img
|
||||||
|
|
@ -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 */ |
||||||
|
|
||||||
|
|
@ -0,0 +1,84 @@ |
|||||||
|
#include <EEPROM.h> |
||||||
|
#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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,125 @@ |
|||||||
|
#include "WebServer.h" |
||||||
|
|
||||||
|
#include <avr/io.h> |
||||||
|
|
||||||
|
#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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
Binary file not shown.
@ -0,0 +1,36 @@ |
|||||||
|
<!-- EspLink will add header here --> |
||||||
|
|
||||||
|
<div class="header"> |
||||||
|
<h1>LED configuration</h1> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="content"> |
||||||
|
<div class="pure-g"><div class="pure-u-1 pure-u-md-1-2"> |
||||||
|
<div class="card"> |
||||||
|
<h1>Control</h1> |
||||||
|
<button id="btn_on" type="button">Turn on</button> |
||||||
|
<button id="btn_blink" type="button">Start blinking</button> |
||||||
|
<button id="btn_off" type="button">Turn off</button> |
||||||
|
<p id="text"/> |
||||||
|
</div> |
||||||
|
<div class="card"> |
||||||
|
<h1>Frequency and pattern</h1> |
||||||
|
<form> |
||||||
|
<b>Pattern:</b><br/> |
||||||
|
<input type="radio" name="pattern" value="25_75">25% on 75% off</input><br/> |
||||||
|
<input type="radio" name="pattern" value="50_50">50% on 50% off</input><br/> |
||||||
|
<input type="radio" name="pattern" value="75_25">75% on 25% off</input><br/> |
||||||
|
|
||||||
|
<b>Frequency:</b><br/> |
||||||
|
<input type="range" name="frequency" min="1" max="25"><br/> |
||||||
|
<input type="submit"> |
||||||
|
</form> |
||||||
|
</div></div> |
||||||
|
<div class="pure-u-1 pure-u-md-1-2"> |
||||||
|
<div class="card" style="min-height: 400px"> |
||||||
|
<h1>Logs</h1> |
||||||
|
<ul id="led_history"/> |
||||||
|
</div> |
||||||
|
</div></div> |
||||||
|
</div> |
||||||
|
</body></html> |
@ -0,0 +1,24 @@ |
|||||||
|
<!-- EspLink will add header here --> |
||||||
|
|
||||||
|
<div class="header"> |
||||||
|
<h1>User setup</h1> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="content"> |
||||||
|
<form> |
||||||
|
First name:<br/><input name="first_name" type="text"/> |
||||||
|
Last name:<br/><input name="last_name" type="text"/> |
||||||
|
Age: |
||||||
|
<input name="age" type="number"/> |
||||||
|
Gender: |
||||||
|
<select name="gender"> |
||||||
|
<option value="female">Female</option> |
||||||
|
<option value="male">Male</option> |
||||||
|
</select> |
||||||
|
<br> |
||||||
|
Notifications<input name="notifications" type="checkbox"/> |
||||||
|
<br> |
||||||
|
<input type="submit"> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</body></html> |
@ -0,0 +1,15 @@ |
|||||||
|
<!-- EspLink will add header here --> |
||||||
|
|
||||||
|
<meta name="refresh-rate" content="500" /> |
||||||
|
|
||||||
|
<div class="header"> |
||||||
|
<h1>Voltage measurement</h1> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="content"> |
||||||
|
<p id="voltage" align="center" style="font-size: 800%"/> |
||||||
|
|
||||||
|
<table class="pure-table pure-table-bordered" align="center" id="table"/> |
||||||
|
</div> |
||||||
|
|
||||||
|
</body></html> |
@ -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(); |
||||||
|
} |
||||||
|
|
@ -0,0 +1,7 @@ |
|||||||
|
<!-- EspLink will add header here --> |
||||||
|
|
||||||
|
<h1 class="header">Simple LED control</h1> |
||||||
|
<h2><div id="text"/></h2> |
||||||
|
<button id="btn_on" type="button">Turn on</button> |
||||||
|
<button id="btn_off" type="button">Turn off</button> |
||||||
|
</body></html> |
@ -0,0 +1 @@ |
|||||||
|
For information on installing libraries, see: http://www.arduino.cc/en/Guide/Libraries |
@ -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 = '<!doctype html><html><head><title>esp-link</title><link rel=stylesheet href="/pure.css"><link rel=stylesheet href="/style.css"><meta name=viewport content="width=device-width, initial-scale=1"><script src="/ui.js"></script><script src="/userpage.js"></script></head><body><div id=layout>'; |
||||||
|
|
||||||
|
open IF, "<", "$dir/$file" or die "Can't read file: $!"; |
||||||
|
my @fc = <IF>; |
||||||
|
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; |
||||||
|
} |
@ -0,0 +1,158 @@ |
|||||||
|
// Copyright (c) 2015 by Thorsten von Eicken, see LICENSE.txt in the esp-link repo
|
||||||
|
|
||||||
|
#include <esp8266.h> |
||||||
|
#include <osapi.h> |
||||||
|
#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 = "<!doctype html><html><head><title>esp-link</title>" |
||||||
|
"<link rel=stylesheet href=\"/pure.css\"><link rel=stylesheet href=\"/style.css\">" |
||||||
|
"<meta name=viewport content=\"width=device-width, initial-scale=1\"><script src=\"/ui.js\">" |
||||||
|
"</script><script src=\"/userpage.js\"></script></head><body><div id=layout> "; |
||||||
|
|
||||||
|
// 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); |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
#ifndef CGIWEBSERVER_H |
||||||
|
#define CGIWEBSERVER_H |
||||||
|
|
||||||
|
#include <httpd.h> |
||||||
|
|
||||||
|
int ICACHE_FLASH_ATTR cgiWebServerSetupUpload(HttpdConnData *connData); |
||||||
|
|
||||||
|
#endif /* CGIWEBSERVER_H */ |
@ -1,19 +1,42 @@ |
|||||||
#ifndef ESPFS_H |
#ifndef ESPFS_H |
||||||
#define ESPFS_H |
#define ESPFS_H |
||||||
|
|
||||||
|
#include "espfsformat.h" |
||||||
|
|
||||||
typedef enum { |
typedef enum { |
||||||
ESPFS_INIT_RESULT_OK, |
ESPFS_INIT_RESULT_OK, |
||||||
ESPFS_INIT_RESULT_NO_IMAGE, |
ESPFS_INIT_RESULT_NO_IMAGE, |
||||||
ESPFS_INIT_RESULT_BAD_ALIGN, |
ESPFS_INIT_RESULT_BAD_ALIGN, |
||||||
} EspFsInitResult; |
} 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 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); |
EspFsInitResult espFsInit(EspFsContext *ctx, void *flashAddress, EspFsSource source); |
||||||
EspFsFile *espFsOpen(char *fileName); |
EspFsFile *espFsOpen(EspFsContext *ctx, char *fileName); |
||||||
|
int espFsIsValid(EspFsContext *ctx); |
||||||
int espFsFlags(EspFsFile *fh); |
int espFsFlags(EspFsFile *fh); |
||||||
int espFsRead(EspFsFile *fh, char *buff, int len); |
int espFsRead(EspFsFile *fh, char *buff, int len); |
||||||
void espFsClose(EspFsFile *fh); |
void espFsClose(EspFsFile *fh); |
||||||
|
|
||||||
|
void espFsIteratorInit(EspFsContext *ctx, EspFsIterator *iterator); |
||||||
|
int espFsIteratorNext(EspFsIterator *iterator); |
||||||
|
|
||||||
#endif |
#endif |
@ -0,0 +1,242 @@ |
|||||||
|
//===== Java script for user pages
|
||||||
|
|
||||||
|
var loadCounter = 0; |
||||||
|
var refreshRate = 0; |
||||||
|
var refreshTimer; |
||||||
|
var hiddenInputs = []; |
||||||
|
|
||||||
|
function notifyResponse( data ) |
||||||
|
{ |
||||||
|
Object.keys(data).forEach(function(v) { |
||||||
|
var elems = document.getElementsByName(v); |
||||||
|
var ndx; |
||||||
|
for(ndx = 0; ndx < elems.length; ndx++ ) |
||||||
|
{ |
||||||
|
var el = elems[ndx]; |
||||||
|
if(el.tagName == "INPUT") |
||||||
|
{ |
||||||
|
if( el.type == "radio" ) |
||||||
|
{ |
||||||
|
el.checked = data[v] == el.value; |
||||||
|
} |
||||||
|
else if( el.type == "checkbox" ) |
||||||
|
{ |
||||||
|
if( data[v] == "on" ) |
||||||
|
el.checked = true; |
||||||
|
else if( data[v] == "off" ) |
||||||
|
el.checked = false; |
||||||
|
else if( data[v] == true ) |
||||||
|
el.checked = true; |
||||||
|
else |
||||||
|
el.checked = false; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
el.value = data[v]; |
||||||
|
} |
||||||
|
} |
||||||
|
if(el.tagName == "SELECT") |
||||||
|
{ |
||||||
|
el.value = data[v]; |
||||||
|
} |
||||||
|
} |
||||||
|
var elem = document.getElementById(v); |
||||||
|
if( elem != null ) |
||||||
|
{ |
||||||
|
if(elem.tagName == "P" || elem.tagName == "DIV" || elem.tagName == "SPAN" || elem.tagName == "TR" || elem.tagName == "TH" || elem.tagName == "TD" || |
||||||
|
elem.tagName == "TEXTAREA" ) |
||||||
|
{ |
||||||
|
elem.innerHTML = data[v]; |
||||||
|
} |
||||||
|
if(elem.tagName == "UL" || elem.tagName == "OL") |
||||||
|
{ |
||||||
|
var list = data[v]; |
||||||
|
var html = ""; |
||||||
|
|
||||||
|
for (var i=0; i<list.length; i++) { |
||||||
|
html = html.concat("<li>" + list[i] + "</li>"); |
||||||
|
} |
||||||
|
|
||||||
|
elem.innerHTML = html; |
||||||
|
} |
||||||
|
if(elem.tagName == "TABLE") |
||||||
|
{ |
||||||
|
var list = data[v]; |
||||||
|
var html = ""; |
||||||
|
|
||||||
|
if( list.length > 0 ) |
||||||
|
{ |
||||||
|
var ths = list[0]; |
||||||
|
html = html.concat("<tr>"); |
||||||
|
|
||||||
|
for (var i=0; i<ths.length; i++) { |
||||||
|
html = html.concat("<th>" + ths[i] + "</th>"); |
||||||
|
} |
||||||
|
|
||||||
|
html = html.concat("</tr>"); |
||||||
|
} |
||||||
|
|
||||||
|
for (var i=1; i<list.length; i++) { |
||||||
|
var tds = list[i]; |
||||||
|
html = html.concat("<tr>"); |
||||||
|
|
||||||
|
for (var j=0; j<tds.length; j++) { |
||||||
|
html = html.concat("<td>" + tds[j] + "</td>"); |
||||||
|
} |
||||||
|
|
||||||
|
html = html.concat("</tr>"); |
||||||
|
} |
||||||
|
|
||||||
|
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(); |
||||||
|
}); |
@ -0,0 +1,29 @@ |
|||||||
|
<div id="main"> |
||||||
|
<div class="header"> |
||||||
|
<h1>Web Server</h1> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="content"> |
||||||
|
<p>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.</p> |
||||||
|
|
||||||
|
<form method="post" action="web-server/upload" name="submit" enctype="multipart/form-data" onSubmit="return onSubmit()"> |
||||||
|
The custom web page to upload: <input type="file" name="webpage"> |
||||||
|
<input type="submit" name="submit" value="Submit"> |
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
|
||||||
|
<script> |
||||||
|
var allowSubmit = true; |
||||||
|
|
||||||
|
function onSubmit() { |
||||||
|
setTimeout(function() { |
||||||
|
window.location.reload(); |
||||||
|
}, 1000); |
||||||
|
return true; |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body></html> |
@ -0,0 +1,301 @@ |
|||||||
|
#include <esp8266.h> |
||||||
|
#include <osapi.h> |
||||||
|
|
||||||
|
#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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
#ifndef MULTIPART_H |
||||||
|
#define MULTIPART_H |
||||||
|
|
||||||
|
#include <httpd.h> |
||||||
|
|
||||||
|
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 */ |
@ -0,0 +1,454 @@ |
|||||||
|
#include "web-server.h" |
||||||
|
|
||||||
|
#include <espconn.h> |
||||||
|
|
||||||
|
#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"); |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
#ifndef WEB_SERVER_H |
||||||
|
#define WEB_SERVER_H |
||||||
|
|
||||||
|
#include <esp8266.h> |
||||||
|
|
||||||
|
#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 */ |
||||||
|
|
Loading…
Reference in new issue