From 0654c1d0382b1dca4a7c8328c2caaacf6d543cb1 Mon Sep 17 00:00:00 2001 From: beegee-tokyo Date: Tue, 27 Sep 2016 16:56:42 +0800 Subject: [PATCH] Synced master branch with upstream from jeelabs Signed-off-by: beegee-tokyo --- .gitignore | 1 - .travis.yml | 31 +++ Dockerfile | 32 +++ Makefile | 122 +++++----- WEB-SERVER.md | 44 ++++ cmd/cmd.h | 6 +- cmd/handlers.c | 2 + createEspFs.pl | 114 +++++++++ esp-link/cgi.c | 11 +- esp-link/cgiservices.c | 2 + esp-link/cgiwebserversetup.c | 158 ++++++++++++ esp-link/cgiwebserversetup.h | 8 + esp-link/config.c | 51 +++- esp-link/config.h | 6 + esp-link/main.c | 34 ++- espfs/espfs.c | 202 +++++++++++----- espfs/espfs.h | 27 ++- espfs/mkespfsimage/Makefile | 5 +- html/console.html | 26 +- html/flash.html | 43 ++++ html/flash.js | 33 +++ html/home.html | 6 + html/ui.js | 4 +- html/userpage.js | 242 +++++++++++++++++++ html/web-server.html | 29 +++ httpd/httpd.c | 50 +++- httpd/httpd.h | 3 + httpd/httpdespfs.c | 19 +- httpd/multipart.c | 301 +++++++++++++++++++++++ httpd/multipart.h | 32 +++ rest/rest.c | 1 - serial/console.c | 32 +++ serial/console.h | 1 + serial/serbridge.c | 15 +- serial/serbridge.h | 2 + serial/slip.c | 2 - serial/uart.c | 28 +-- serial/uart.h | 3 +- syslog/syslog.h | 1 + web-server/web-server.c | 454 +++++++++++++++++++++++++++++++++++ web-server/web-server.h | 37 +++ 41 files changed, 2047 insertions(+), 173 deletions(-) create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 WEB-SERVER.md create mode 100644 createEspFs.pl create mode 100644 esp-link/cgiwebserversetup.c create mode 100644 esp-link/cgiwebserversetup.h create mode 100644 html/flash.html create mode 100644 html/flash.js create mode 100644 html/userpage.js create mode 100644 html/web-server.html create mode 100644 httpd/multipart.c create mode 100644 httpd/multipart.h create mode 100644 web-server/web-server.c create mode 100644 web-server/web-server.h diff --git a/.gitignore b/.gitignore index a18e820..119586c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,5 @@ esp-link.opensdf esp-link.sdf espfs/mkespfsimage/mman-win32/libmman.a .localhistory/ -#tools/ local.conf *.tgz diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..008251c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +# Travis-CI file for Esp-Link + +language: c + +before_install: + - curl -Ls http://s3.voneicken.com/xtensa-lx106-elf-20160330.tgx | tar Jxf - + - curl -Ls http://s3.voneicken.com/esp_iot_sdk_v2.0.0.p1.tgx | tar -C .. -Jxf - + +after_script: + # upload to an S3 bucket, requires S3_BUCKET, AWS_ACCESS_KEY_ID and AWS_SECRET_KEY to be set + # in environment using travis' repository settings + - "if [[ -n \"$S3_BUCKET\" && -n \"$AWS_ACCESS_KEY_ID\" ]]; then + echo Uploading *.tgz to $S3_BUCKET; + curl -Ls https://github.com/rlmcpherson/s3gof3r/releases/download/v0.5.0/gof3r_0.5.0_linux_amd64.tar.gz | tar zxf - gof3r_0.5.0_linux_amd64/gof3r; + mv gof3r*/gof3r .; + ls *.tgz | xargs -I {} ./gof3r put -b $S3_BUCKET -k esp-link/{} --acl public-read -p {}; + ls *.tgz | xargs -I {} echo \"URL: http://$S3_BUCKET/esp-link/{}\"; + fi" + +compiler: gcc + +env: + +script: + - export XTENSA_TOOLS_ROOT=$PWD/xtensa-lx106-elf/bin/ + - export BRANCH=$TRAVIS_BRANCH + #- export SDK_BASE=$PWD/esp_iot_sdk_v2.0.0.p1 + - make release + +notifications: + email: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..572c102 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Dockerfile for github.com/jeelabs/esp-link +# +# This dockerfile is intended to be used to compile esp-link as it's checked out on +# your desktop/laptop. You can git clone esp-link, and then compile it using +# a commandline of `docker run -v $PWD:/esp-link jeelabs/esp-link`. The -v mounts +# your esp-link source directory onto /esp-link in the container and the default command is +# to run make. +# If you would like to create your own container image, use `docker build -t esp-link .` +FROM ubuntu:16.04 + +RUN apt-get update \ + && apt-get install -y software-properties-common build-essential python curl git + +RUN curl -Ls http://s3.voneicken.com/xtensa-lx106-elf-20160330.tgx | tar Jxf - +RUN curl -Ls http://s3.voneicken.com/esp_iot_sdk_v2.0.0.p1.tgx | tar -Jxf - + +RUN apt-get install zlib1g-dev openjdk-8-jre-headless + +ENV XTENSA_TOOLS_ROOT /xtensa-lx106-elf/bin/ + +# This could be used to create an image with esp-link in it from github: +#RUN git clone https://github.com/jeelabs/esp-link + +# This could be used to create an image with esp-link in it from the local dir: +#COPY . esp-link/ + +# Expect the esp-link source/home dir to be mounted here: +VOLUME /esp-link +WORKDIR /esp-link + +# Default command is to run a build, can be overridden on the docker commandline: +CMD make diff --git a/Makefile b/Makefile index bf869b8..df6806d 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ # Makefile heavily adapted to esp-link and wireless flashing by Thorsten von Eicken # Lots of work, in particular to support windows, by brunnels # Original from esphttpd and others... -# VERBOSE=1 # # Start by setting the directories for the toolchain a few lines down # the default target will build the firmware images @@ -52,36 +51,37 @@ ESP_HOSTNAME ?= esp-link # Base directory for the compiler. Needs a / at the end. # Typically you'll install https://github.com/pfalcon/esp-open-sdk +# IMPORTANT: use esp-open-sdk `make STANDALONE=n`: the SDK bundled with esp-open-sdk will *not* work! XTENSA_TOOLS_ROOT ?= $(abspath ../esp-open-sdk/xtensa-lx106-elf/bin)/ # Firmware version # WARNING: if you change this expect to make code adjustments elsewhere, don't expect # that esp-link will magically work with a different version of the SDK!!! -SDK_VERS ?= esp_iot_sdk_v1.5.4 -#SDK_VERS ?= esp_iot_sdk_v2.0.0 +SDK_VERS ?= esp_iot_sdk_v2.0.0.p1 # Try to find the firmware manually extracted, e.g. after downloading from Espressif's BBS, # http://bbs.espressif.com/viewforum.php?f=46 SDK_BASE ?= $(wildcard ../$(SDK_VERS)) # If the firmware isn't there, see whether it got downloaded as part of esp-open-sdk -ifeq ($(SDK_BASE),) -SDK_BASE := $(wildcard $(XTENSA_TOOLS_ROOT)/../../$(SDK_VERS)) -endif +# This used to work at some point, but is not supported, uncomment if you feel lucky ;-) +#ifeq ($(SDK_BASE),) +#SDK_BASE := $(wildcard $(XTENSA_TOOLS_ROOT)/../../$(SDK_VERS)) +#endif # Clean up SDK path SDK_BASE := $(abspath $(SDK_BASE)) $(warning Using SDK from $(SDK_BASE)) # Path to bootloader file -BOOTFILE ?= $(SDK_BASE/bin/boot_v1.5.bin) +BOOTFILE ?= $(SDK_BASE/bin/boot_v1.6.bin) # Esptool.py path and port, only used for 1-time serial flashing # Typically you'll use https://github.com/themadinventor/esptool # Windows users use the com port i.e: ESPPORT ?= com3 ESPTOOL ?= $(abspath ../esp-open-sdk/esptool/esptool.py) ESPPORT ?= /dev/ttyUSB0 -ESPBAUD ?= 460800 +ESPBAUD ?= 230400 # --------------- chipset configuration --------------- @@ -101,8 +101,8 @@ LED_SERIAL_PIN ?= 14 # --------------- esp-link modules config options --------------- -# Optional Modules mqtt -MODULES ?= mqtt rest socket syslog +# Optional Modules mqtt rest socket webserver syslog +MODULES ?= mqtt rest socket webserver syslog # --------------- esphttpd config options --------------- @@ -118,8 +118,6 @@ MODULES ?= mqtt rest socket syslog # # Adding JPG or PNG files (and any other compressed formats) is not recommended, because GZIP # compression does not work effectively on compressed files. - -#Static gzipping is disabled by default. GZIP_COMPRESSION ?= yes # If COMPRESS_W_HTMLCOMPRESSOR is set to "yes" then the static css and js files will be compressed with @@ -217,18 +215,22 @@ ifneq (,$(findstring rest,$(MODULES))) CFLAGS += -DREST endif -ifneq (,$(findstring socket,$(MODULES))) - CFLAGS += -DSOCKET -endif - ifneq (,$(findstring syslog,$(MODULES))) CFLAGS += -DSYSLOG endif +ifneq (,$(findstring web-server,$(MODULES))) + CFLAGS += -DWEBSERVER +endif + +ifneq (,$(findstring socket,$(MODULES))) + CFLAGS += -DSOCKET +endif + # which modules (subdirectories) of the project to include in compiling LIBRARIES_DIR = libraries -MODULES += espfs httpd user serial cmd esp-link -MODULES += $(foreach sdir,$(LIBRARIES_DIR),$(wildcard $(sdir)/*)) +MODULES += espfs httpd user serial cmd esp-link +MODULES += $(foreach sdir,$(LIBRARIES_DIR),$(wildcard $(sdir)/*)) EXTRA_INCDIR = include . # libraries used in this project, mainly provided by the SDK @@ -236,11 +238,11 @@ LIBS = c gcc hal phy pp net80211 wpa main lwip crypto # compiler flags using during compilation of source files CFLAGS += -Os -ggdb -std=c99 -Werror -Wpointer-arith -Wundef -Wall -Wl,-EL -fno-inline-functions \ - -nostdlib -mlongcalls -mtext-section-literals -ffunction-sections -fdata-sections \ - -D__ets__ -DICACHE_FLASH -Wno-address -DFIRMWARE_SIZE=$(ESP_FLASH_MAX) \ - -DMCU_RESET_PIN=$(MCU_RESET_PIN) -DMCU_ISP_PIN=$(MCU_ISP_PIN) \ - -DLED_CONN_PIN=$(LED_CONN_PIN) -DLED_SERIAL_PIN=$(LED_SERIAL_PIN) \ - -DVERSION="$(VERSION)" + -nostdlib -mlongcalls -mtext-section-literals -ffunction-sections -fdata-sections \ + -D__ets__ -DICACHE_FLASH -Wno-address -DFIRMWARE_SIZE=$(ESP_FLASH_MAX) \ + -DMCU_RESET_PIN=$(MCU_RESET_PIN) -DMCU_ISP_PIN=$(MCU_ISP_PIN) \ + -DLED_CONN_PIN=$(LED_CONN_PIN) -DLED_SERIAL_PIN=$(LED_SERIAL_PIN) \ + -DVERSION="$(VERSION)" # linker flags used to generate the main object file LDFLAGS = -nostdlib -Wl,--no-check-sections -u call_user_start -Wl,-static -Wl,--gc-sections @@ -251,18 +253,17 @@ LD_SCRIPT1 := build/eagle.esphttpd1.v6.ld LD_SCRIPT2 := build/eagle.esphttpd2.v6.ld # various paths from the SDK used in this project -SDK_LIBDIR = lib -SDK_LDDIR = ld -SDK_INCDIR = include include/json +SDK_LIBDIR = lib +SDK_LDDIR = ld +SDK_INCDIR = include include/json SDK_TOOLSDIR = tools # select which tools to use as compiler, librarian and linker CC := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-gcc AR := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-ar LD := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-gcc -OBJCP := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-objcopy -OBJDP := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-objdump - +OBJCP := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-objcopy +OBJDP := $(XTENSA_TOOLS_ROOT)xtensa-lx106-elf-objdump #### SRC_DIR := $(MODULES) @@ -274,14 +275,14 @@ SDK_INCDIR := $(addprefix -I$(SDK_BASE)/,$(SDK_INCDIR)) SDK_TOOLS := $(addprefix $(SDK_BASE)/,$(SDK_TOOLSDIR)) APPGEN_TOOL := $(addprefix $(SDK_TOOLS)/,$(APPGEN_TOOL)) -SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.c)) -OBJ := $(patsubst %.c,$(BUILD_BASE)/%.o,$(SRC)) $(BUILD_BASE)/espfs_img.o +SRC := $(foreach sdir,$(SRC_DIR),$(wildcard $(sdir)/*.c)) +OBJ := $(patsubst %.c,$(BUILD_BASE)/%.o,$(SRC)) $(BUILD_BASE)/espfs_img.o LIBS := $(addprefix -l,$(LIBS)) APP_AR := $(addprefix $(BUILD_BASE)/,$(TARGET)_app.a) USER1_OUT := $(addprefix $(BUILD_BASE)/,$(TARGET).user1.out) USER2_OUT := $(addprefix $(BUILD_BASE)/,$(TARGET).user2.out) -INCDIR := $(addprefix -I,$(SRC_DIR)) +INCDIR := $(addprefix -I,$(SRC_DIR)) EXTRA_INCDIR := $(addprefix -I,$(EXTRA_INCDIR)) MODULE_INCDIR := $(addsuffix /include,$(INCDIR)) @@ -371,7 +372,7 @@ $(FW_BASE)/user1.bin: $(USER1_OUT) $(FW_BASE) $(Q) $(OBJCP) --only-section .rodata -O binary $(USER1_OUT) eagle.app.v6.rodata.bin $(Q) $(OBJCP) --only-section .irom0.text -O binary $(USER1_OUT) eagle.app.v6.irom0text.bin ls -ls eagle*bin - $(Q) COMPILE=gcc PATH=$(XTENSA_TOOLS_ROOT):$(PATH) python $(APPGEN_TOOL) $(USER1_OUT) 2 $(ESP_FLASH_MODE) $(ESP_FLASH_FREQ_DIV) $(ESP_SPI_SIZE) 0 + $(Q) COMPILE=gcc PATH=$(XTENSA_TOOLS_ROOT):$(PATH) python $(APPGEN_TOOL) $(USER1_OUT) 2 $(ESP_FLASH_MODE) $(ESP_FLASH_FREQ_DIV) $(ESP_SPI_SIZE) 0 >/dev/null $(Q) rm -f eagle.app.v6.*.bin $(Q) mv eagle.app.flash.bin $@ @echo "** user1.bin uses $$(stat -c '%s' $@) bytes of" $(ESP_FLASH_MAX) "available" @@ -382,7 +383,7 @@ $(FW_BASE)/user2.bin: $(USER2_OUT) $(FW_BASE) $(Q) $(OBJCP) --only-section .data -O binary $(USER2_OUT) eagle.app.v6.data.bin $(Q) $(OBJCP) --only-section .rodata -O binary $(USER2_OUT) eagle.app.v6.rodata.bin $(Q) $(OBJCP) --only-section .irom0.text -O binary $(USER2_OUT) eagle.app.v6.irom0text.bin - $(Q) COMPILE=gcc PATH=$(XTENSA_TOOLS_ROOT):$(PATH) python $(APPGEN_TOOL) $(USER2_OUT) 2 $(ESP_FLASH_MODE) $(ESP_FLASH_FREQ_DIV) $(ESP_SPI_SIZE) 0 + $(Q) COMPILE=gcc PATH=$(XTENSA_TOOLS_ROOT):$(PATH) python $(APPGEN_TOOL) $(USER2_OUT) 2 $(ESP_FLASH_MODE) $(ESP_FLASH_FREQ_DIV) $(ESP_SPI_SIZE) 1 >/dev/null $(Q) rm -f eagle.app.v6.*.bin $(Q) mv eagle.app.flash.bin $@ $(Q) if [ $$(stat -c '%s' $@) -gt $$(( $(ESP_FLASH_MAX) )) ]; then echo "$@ too big!"; false; fi @@ -431,23 +432,23 @@ $(BUILD_BASE)/espfs_img.o: html/ html/wifi/ espfs/mkespfsimage/mkespfsimage $(Q) cp -r html/wifi/*.png html_compressed/wifi; $(Q) cp -r html/wifi/*.js html_compressed/wifi; ifeq ("$(COMPRESS_W_HTMLCOMPRESSOR)","yes") - $(Q) echo "Compression assets with htmlcompressor. This may take a while..." - $(Q) java -jar tools/$(HTML_COMPRESSOR) \ - -t html --remove-surrounding-spaces max --remove-quotes --remove-intertag-spaces \ - -o $(abspath ./html_compressed)/ \ - $(HTML_PATH)head- \ - $(HTML_PATH)*.html + $(Q) echo "Compressing assets with htmlcompressor. This may take a while..." + $(Q) java -jar tools/$(HTML_COMPRESSOR) \ + -t html --remove-surrounding-spaces max --remove-quotes --remove-intertag-spaces \ + -o $(abspath ./html_compressed)/ \ + $(HTML_PATH)head- \ + $(HTML_PATH)*.html $(Q) java -jar tools/$(HTML_COMPRESSOR) \ - -t html --remove-surrounding-spaces max --remove-quotes --remove-intertag-spaces \ - -o $(abspath ./html_compressed)/wifi/ \ - $(WIFI_PATH)*.html - $(Q) echo "Compression assets with yui-compressor. This may take a while..." + -t html --remove-surrounding-spaces max --remove-quotes --remove-intertag-spaces \ + -o $(abspath ./html_compressed)/wifi/ \ + $(WIFI_PATH)*.html + $(Q) echo "Compressing assets with yui-compressor. This may take a while..." $(Q) for file in `find html_compressed -type f -name "*.js"`; do \ - java -jar tools/$(YUI_COMPRESSOR) $$file --line-break 0 -o $$file; \ - done + java -jar tools/$(YUI_COMPRESSOR) $$file --line-break 0 -o $$file; \ + done $(Q) for file in `find html_compressed -type f -name "*.css"`; do \ - java -jar tools/$(YUI_COMPRESSOR) $$file -o $$file; \ - done + java -jar tools/$(YUI_COMPRESSOR) $$file -o $$file; \ + done else $(Q) cp -r html/head- html_compressed; $(Q) cp -r html/*.html html_compressed; @@ -458,25 +459,25 @@ ifeq (,$(findstring mqtt,$(MODULES))) $(Q) rm -rf html_compressed/mqtt.js endif $(Q) for file in `find html_compressed -type f -name "*.htm*"`; do \ - cat html_compressed/head- $$file >$${file}-; \ - mv $$file- $$file; \ - done + cat html_compressed/head- $$file >$${file}-; \ + mv $$file- $$file; \ + done $(Q) rm html_compressed/head- $(Q) cd html_compressed; find . \! -name \*- | ../espfs/mkespfsimage/mkespfsimage > ../build/espfs.img; cd ..; $(Q) ls -sl build/espfs.img $(Q) cd build; $(OBJCP) -I binary -O elf32-xtensa-le -B xtensa --rename-section .data=.espfs \ - espfs.img espfs_img.o; cd .. + espfs.img espfs_img.o; cd .. # edit the loader script to add the espfs section to the end of irom with a 4 byte alignment. # we also adjust the sizes of the segments 'cause we need more irom0 build/eagle.esphttpd1.v6.ld: $(SDK_LDDIR)/eagle.app.v6.new.1024.app1.ld $(Q) sed -e '/\.irom\.text/{' -e 'a . = ALIGN (4);' -e 'a *(.espfs)' -e '}' \ - -e '/^ irom0_0_seg/ s/6B000/7C000/' \ - $(SDK_LDDIR)/eagle.app.v6.new.1024.app1.ld >$@ + -e '/^ irom0_0_seg/ s/6B000/7C000/' \ + $(SDK_LDDIR)/eagle.app.v6.new.1024.app1.ld >$@ build/eagle.esphttpd2.v6.ld: $(SDK_LDDIR)/eagle.app.v6.new.1024.app2.ld $(Q) sed -e '/\.irom\.text/{' -e 'a . = ALIGN (4);' -e 'a *(.espfs)' -e '}' \ - -e '/^ irom0_0_seg/ s/6B000/7C000/' \ - $(SDK_LDDIR)/eagle.app.v6.new.1024.app2.ld >$@ + -e '/^ irom0_0_seg/ s/6B000/7C000/' \ + $(SDK_LDDIR)/eagle.app.v6.new.1024.app2.ld >$@ espfs/mkespfsimage/mkespfsimage: espfs/mkespfsimage/ $(Q) $(MAKE) -C espfs/mkespfsimage GZIP_COMPRESSION="$(GZIP_COMPRESSION)" @@ -486,11 +487,14 @@ release: all $(Q) egrep -a 'esp-link [a-z0-9.]+ - 201' $(FW_BASE)/user1.bin | cut -b 1-80 $(Q) egrep -a 'esp-link [a-z0-9.]+ - 201' $(FW_BASE)/user2.bin | cut -b 1-80 $(Q) cp $(FW_BASE)/user1.bin $(FW_BASE)/user2.bin $(SDK_BASE)/bin/blank.bin \ - "$(SDK_BASE)/bin/boot_v1.5.bin" wiflash avrflash release/esp-link-$(BRANCH) - $(Q) tar zcf esp-link-$(BRANCH).tgz -C release esp-link-$(BRANCH) - $(Q) echo "Release file: esp-link-$(BRANCH).tgz" + "$(SDK_BASE)/bin/boot_v1.6.bin" "$(SDK_BASE)/bin/esp_init_data_default.bin" \ + wiflash avrflash release/esp-link-$(BRANCH) + $(Q) tar zcf esp-link-$(BRANCH)-$(SHA).tgz -C release esp-link-$(BRANCH) + $(Q) echo "Release file: esp-link-$(BRANCH)-$(SHA).tgz" $(Q) rm -rf release +docker: + $(Q) docker build -t jeelabs/esp-link . clean: $(Q) rm -f $(APP_AR) $(Q) rm -f $(TARGET_OUT) 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/cmd/cmd.h b/cmd/cmd.h index 6580236..39f0667 100644 --- a/cmd/cmd.h +++ b/cmd/cmd.h @@ -49,8 +49,12 @@ typedef enum { CMD_REST_REQUEST, // do REST request CMD_REST_SETHEADER, // define header - CMD_SOCKET_SETUP = 30, // set-up callbacks + CMD_WEB_DATA = 30, // MCU pushes data using this command + CMD_WEB_REQ_CB, // esp-link WEB callback + + CMD_SOCKET_SETUP = 40, // set-up callbacks CMD_SOCKET_SEND, // send data over UDP socket + } CmdName; typedef void (*cmdfunc_t)(CmdPacket *cmd); diff --git a/cmd/handlers.c b/cmd/handlers.c index 9bb21c5..d551804 100644 --- a/cmd/handlers.c +++ b/cmd/handlers.c @@ -12,6 +12,7 @@ #ifdef REST #include #endif +#include #ifdef SOCKET #include #endif @@ -50,6 +51,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}, #ifdef SOCKET {CMD_SOCKET_SETUP, "SOCKET_SETUP", SOCKET_Setup}, {CMD_SOCKET_SEND, "SOCKET_SEND", SOCKET_Send}, diff --git a/createEspFs.pl b/createEspFs.pl new file mode 100644 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..d667be3 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,15 @@ int ICACHE_FLASH_ATTR cgiMenu(HttpdConnData *connData) { #ifdef MQTT "\"REST/MQTT\", \"/mqtt.html\", " #endif - "\"Debug log\", \"/log.html\"" + "\"Debug log\", \"/log.html\"," + "\"Upgrade Firmware\", \"/flash.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..d12f7a7 100644 --- a/esp-link/config.c +++ b/esp-link/config.c @@ -31,7 +31,10 @@ FlashConfig flashDefault = { .rx_pullup = 1, .sntp_server = "us.pool.ntp.org\0", .syslog_host = "\0", .syslog_minheap = 8192, .syslog_filter = 7, .syslog_showtick = 1, .syslog_showdate = 0, - .mdns_enable = 1, .mdns_servername = "http\0", .timezone_offset = 0 + .mdns_enable = 1, .mdns_servername = "http\0", .timezone_offset = 0, + .data_bits = EIGHT_BITS, + .parity = NONE_BITS, + .stop_bits = ONE_STOP_BIT, }; typedef union { @@ -152,6 +155,12 @@ bool ICACHE_FLASH_ATTR configRestore(void) { os_memset(flashConfig.mqtt_old_host, 0, 32); } else os_printf("mqtt_host is '%s'\n", flashConfig.mqtt_host); + if (flashConfig.data_bits == 0) { + // restore to default 8N1 + flashConfig.data_bits = flashDefault.data_bits; + flashConfig.parity = flashDefault.parity; + flashConfig.stop_bits = flashDefault.stop_bits; + } return true; } @@ -195,3 +204,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..65195d2 100644 --- a/esp-link/config.h +++ b/esp-link/config.h @@ -38,6 +38,9 @@ typedef struct { char mdns_servername[32]; int8_t timezone_offset; char mqtt_host[64]; // MQTT host we connect to, was 32-char mqtt_old_host + int8_t data_bits; + int8_t parity; + int8_t stop_bits; } FlashConfig; extern FlashConfig flashConfig; @@ -46,4 +49,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..e775de9 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" @@ -74,6 +76,7 @@ HttpdBuiltInUrl builtInUrls[] = { { "/log/reset", cgiReset, NULL }, { "/console/reset", ajaxConsoleReset, NULL }, { "/console/baud", ajaxConsoleBaud, NULL }, + { "/console/fmt", ajaxConsoleFormat, NULL }, { "/console/text", ajaxConsole, NULL }, { "/console/send", ajaxConsoleSend, NULL }, //Enable the line below to protect the WiFi configuration with an username/password combo. @@ -96,6 +99,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 } }; @@ -117,13 +122,30 @@ extern uint32_t _binary_espfs_img_start; extern void app_init(void); extern void mqtt_client_init(void); -void user_rf_pre_init(void) { +void ICACHE_FLASH_ATTR +user_rf_pre_init(void) { //default is enabled system_set_os_print(DEBUG_SDK); } +/* user_rf_cal_sector_set is a required function that is called by the SDK to get a flash + * sector number where it can store RF calibration data. This was introduced with SDK 1.5.4.1 + * and is necessary because Espressif ran out of pre-reserved flash sectors. Ooops... */ +uint32 ICACHE_FLASH_ATTR +user_rf_cal_sector_set(void) { + uint32_t sect = 0; + switch (system_get_flash_size_map()) { + case FLASH_SIZE_4M_MAP_256_256: // 512KB + sect = 128 - 10; // 0x76000 + default: + sect = 128; // 0x80000 + } + return sect; +} + // Main routine to initialize esp-link. -void user_init(void) { +void ICACHE_FLASH_ATTR +user_init(void) { // uncomment the following three lines to see flash config messages for troubleshooting //uart_init(115200, 115200); //logInit(); @@ -135,7 +157,8 @@ void user_init(void) { gpio_init(); gpio_output_set(0, 0, 0, (1<<15)); // some people tie it to GND, gotta ensure it's disabled // init UART - uart_init(flashConfig.baud_rate, 115200); + uart_init(CALC_UARTMODE(flashConfig.data_bits, flashConfig.parity, flashConfig.stop_bits), + flashConfig.baud_rate, 115200); logInit(); // must come after init of uart // Say hello (leave some time to cause break in TX after boot loader's msg os_delay_us(10000L); @@ -147,11 +170,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/espfs/mkespfsimage/Makefile b/espfs/mkespfsimage/Makefile index 2980331..9b910ff 100644 --- a/espfs/mkespfsimage/Makefile +++ b/espfs/mkespfsimage/Makefile @@ -34,9 +34,10 @@ clean: else +CC=gcc CFLAGS=-I.. -std=gnu99 ifeq ("$(GZIP_COMPRESSION)","yes") -CFLAGS += -DESPFS_GZIP +CFLAGS+= -DESPFS_GZIP endif OBJS=main.o @@ -52,4 +53,4 @@ endif clean: rm -f $(TARGET) $(OBJS) -endif \ No newline at end of file +endif diff --git a/html/console.html b/html/console.html index d84da72..b2e6597 100644 --- a/html/console.html +++ b/html/console.html @@ -18,7 +18,17 @@ -   Fmt: 8N1 +   Fmt: +

Console
@@ -93,6 +103,20 @@ ); }); + ajaxJson('GET', "/console/fmt", + function(data) { $("#fmt-sel").value = data.fmt; }, + function(s, st) { showNotification(st); } + ); + + bnd($("#fmt-sel"), "change", function(ev) { + ev.preventDefault(); + var fmt = $("#fmt-sel").value; + ajaxSpin('POST', "/console/fmt?fmt="+fmt, + function(resp) { showNotification("" + fmt + " format set"); }, + function(s, st) { showWarning("Error setting format: " + st); } + ); + }); + consoleSendInit(); addClass($('html')[0], "height100"); diff --git a/html/flash.html b/html/flash.html new file mode 100644 index 0000000..2de096a --- /dev/null +++ b/html/flash.html @@ -0,0 +1,43 @@ +
+
+

Upgrade Firmware

+
+ +
+
+
+
+

Upgrade Firmware +
+

+ +
+
+
+
+
+
+ + + + diff --git a/html/flash.js b/html/flash.js new file mode 100644 index 0000000..ca3d6ef --- /dev/null +++ b/html/flash.js @@ -0,0 +1,33 @@ +//===== FLASH cards + +function flashFirmware(e) { + e.preventDefault(); + var fw_data = document.getElementById('fw-file').files[0]; + + $("#fw-form").setAttribute("hidden", ""); + $("#fw-spinner").removeAttribute("hidden"); + showNotification("Firmware is being updated ..."); + + ajaxReq("POST", "/flash/upload", function (resp) { + ajaxReq("GET", "/flash/reboot", function (resp) { + showNotification("Firmware has been successfully updated!"); + setTimeout(function(){ window.location.reload()}, 4000); + + $("#fw-spinner").setAttribute("hidden", ""); + $("#fw-form").removeAttribute("hidden"); + }); + }, null, fw_data) +} + +function fetchFlash() { + ajaxReq("GET", "/flash/next", function (resp) { + $("#fw-slot").innerHTML = resp; + $("#fw-spinner").setAttribute("hidden", ""); + $("#fw-form").removeAttribute("hidden"); + }); + ajaxJson("GET", "/menu", function(data) { + var v = $("#current-fw"); + if (v != null) { v.innerHTML = data.version; } + } + ); +} 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 @@
+ Webpage size +
+ + +
+ Current partition Description:
diff --git a/html/ui.js b/html/ui.js index aa66f69..67361c9 100644 --- a/html/ui.js +++ b/html/ui.js @@ -151,7 +151,7 @@ function toggleClass(el, cl) { //===== AJAX -function ajaxReq(method, url, ok_cb, err_cb) { +function ajaxReq(method, url, ok_cb, err_cb, data) { var xhr = j(); xhr.open(method, url, true); var timeout = setTimeout(function() { @@ -173,7 +173,7 @@ function ajaxReq(method, url, ok_cb, err_cb) { } // console.log("XHR send:", method, url); try { - xhr.send(); + xhr.send(data); } catch(err) { console.log("XHR EXC :", method, url, "->", err); err_cb(599, err); diff --git a/html/userpage.js b/html/userpage.js new file mode 100644 index 0000000..e9e7555 --- /dev/null +++ b/html/userpage.js @@ -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[i] + ""); + } + + elem.innerHTML = html; + } + if(elem.tagName == "TABLE") + { + var list = data[v]; + var html = ""; + + if( list.length > 0 ) + { + var ths = list[0]; + html = html.concat(""); + + 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/rest/rest.c b/rest/rest.c index e9736f5..aa40ccb 100644 --- a/rest/rest.c +++ b/rest/rest.c @@ -116,7 +116,6 @@ tcpclient_recv(void *arg, char *pdata, unsigned short len) { espconn_disconnect(client->pCon); } -// Data is sent static void ICACHE_FLASH_ATTR tcpclient_sent_cb(void *arg) { struct espconn *pCon = (struct espconn *)arg; diff --git a/serial/console.c b/serial/console.c index 44e860a..330c870 100644 --- a/serial/console.c +++ b/serial/console.c @@ -80,6 +80,38 @@ ajaxConsoleBaud(HttpdConnData *connData) { httpdSend(connData, buff, -1); return HTTPD_CGI_DONE; } +int ICACHE_FLASH_ATTR +ajaxConsoleFormat(HttpdConnData *connData) { + if (connData->conn==NULL) return HTTPD_CGI_DONE; // Connection aborted. Clean up. + char buff[16]; + int len, status = 400; + uint32 conf0; + + len = httpdFindArg(connData->getArgs, "fmt", buff, sizeof(buff)); + if (len >= 3) { + int c = buff[0]; + if (c >= '5' && c <= '8') + flashConfig.data_bits = c - '5' + FIVE_BITS; + if (buff[1] == 'N' || buff[1] == 'E') + flashConfig.parity = buff[1] == 'E' ? EVEN_BITS : NONE_BITS; + if (buff[2] == '1' || buff[2] == '2') + flashConfig.stop_bits = buff[2] == '2' ? TWO_STOP_BIT : ONE_STOP_BIT; + conf0 = CALC_UARTMODE(flashConfig.data_bits, flashConfig.parity, flashConfig.stop_bits); + uart_config(0, flashConfig.baud_rate, conf0); + status = configSave() ? 200 : 400; + } else if (connData->requestType == HTTPD_METHOD_GET) { + status = 200; + } + + jsonHeader(connData, status); + os_sprintf(buff, "{\"fmt\": \"%c%c%c\"}", + flashConfig.data_bits + '5', + flashConfig.parity ? 'E' : 'N', + flashConfig.stop_bits ? '2': '1'); + httpdSend(connData, buff, -1); + return HTTPD_CGI_DONE; +} + int ICACHE_FLASH_ATTR ajaxConsoleSend(HttpdConnData *connData) { diff --git a/serial/console.h b/serial/console.h index 6225c0a..3020903 100644 --- a/serial/console.h +++ b/serial/console.h @@ -8,6 +8,7 @@ void ICACHE_FLASH_ATTR console_write_char(char c); int ajaxConsole(HttpdConnData *connData); int ajaxConsoleReset(HttpdConnData *connData); int ajaxConsoleBaud(HttpdConnData *connData); +int ajaxConsoleFormat(HttpdConnData *connData); int ajaxConsoleSend(HttpdConnData *connData); int tplConsole(HttpdConnData *connData, char *token, void **arg); 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/serial/uart.c b/serial/uart.c index f7f07fa..92233a0 100644 --- a/serial/uart.c +++ b/serial/uart.c @@ -44,8 +44,8 @@ static void uart0_rx_intr_handler(void *para); * Parameters : uart_no, use UART0 or UART1 defined ahead * Returns : NONE *******************************************************************************/ -static void ICACHE_FLASH_ATTR -uart_config(uint8 uart_no) +void ICACHE_FLASH_ATTR +uart_config(uint8 uart_no, UartBautRate baudrate, uint32 conf0) { if (uart_no == UART1) { PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_U1TXD_BK); @@ -59,14 +59,11 @@ uart_config(uint8 uart_no) //PIN_PULLUP_DIS (PERIPHS_IO_MUX_U0RXD_U); } - uart_div_modify(uart_no, UART_CLK_FREQ / UartDev.baut_rate); + uart_div_modify(uart_no, UART_CLK_FREQ / baudrate); if (uart_no == UART1) //UART 1 always 8 N 1 - WRITE_PERI_REG(UART_CONF0(uart_no), - CALC_UARTMODE(EIGHT_BITS, NONE_BITS, ONE_STOP_BIT)); - else - WRITE_PERI_REG(UART_CONF0(uart_no), - CALC_UARTMODE(UartDev.data_bits, UartDev.parity, UartDev.stop_bits)); + conf0 = CALC_UARTMODE(EIGHT_BITS, NONE_BITS, ONE_STOP_BIT); + WRITE_PERI_REG(UART_CONF0(uart_no), conf0); //clear rx and tx fifo,not ready SET_PERI_REG_MASK(UART_CONF0(uart_no), UART_RXFIFO_RST | UART_TXFIFO_RST); @@ -267,13 +264,11 @@ uart0_baud(int rate) { * Returns : NONE *******************************************************************************/ void ICACHE_FLASH_ATTR -uart_init(UartBautRate uart0_br, UartBautRate uart1_br) +uart_init(uint32 conf0, UartBautRate uart0_br, UartBautRate uart1_br) { // rom use 74880 baut_rate, here reinitialize - UartDev.baut_rate = uart0_br; - uart_config(UART0); - UartDev.baut_rate = uart1_br; - uart_config(UART1); + uart_config(UART0, uart0_br, conf0); + uart_config(UART1, uart1_br, conf0); for (int i=0; i<4; i++) uart_tx_one_char(UART1, '\n'); for (int i=0; i<4; i++) uart_tx_one_char(UART0, '\n'); ETS_UART_INTR_ENABLE(); @@ -295,10 +290,3 @@ uart_add_recv_cb(UartRecv_cb cb) { os_printf("UART: max cb count exceeded\n"); } -void ICACHE_FLASH_ATTR -uart_reattach() -{ - uart_init(BIT_RATE_74880, BIT_RATE_74880); -// ETS_UART_INTR_ATTACH(uart_rx_intr_handler_ssc, &(UartDev.rcv_buff)); -// ETS_UART_INTR_ENABLE(); -} diff --git a/serial/uart.h b/serial/uart.h index 24fb2df..d1469c6 100644 --- a/serial/uart.h +++ b/serial/uart.h @@ -8,7 +8,7 @@ typedef void (*UartRecv_cb)(char *buf, short len); // Initialize UARTs to the provided baud rates (115200 recommended). This also makes the os_printf // calls use uart1 for output (for debugging purposes) -void uart_init(UartBautRate uart0_br, UartBautRate uart1_br); +void uart_init(uint32 conf0, UartBautRate uart0_br, UartBautRate uart1_br); // Transmit a buffer of characters on UART0 void uart0_tx_buffer(char *buf, uint16 len); @@ -27,5 +27,6 @@ void uart_add_recv_cb(UartRecv_cb cb); uint16_t uart0_rx_poll(char *buff, uint16_t nchars, uint32_t timeout_us); void uart0_baud(int rate); +void uart_config(uint8 uart_no, UartBautRate baudrate, uint32 conf0); #endif /* __UART_H__ */ diff --git a/syslog/syslog.h b/syslog/syslog.h index ed453ab..d556ee9 100644 --- a/syslog/syslog.h +++ b/syslog/syslog.h @@ -70,6 +70,7 @@ enum syslog_facility { #define REG_READ(_r) (*(volatile uint32 *)(_r)) #define WDEV_NOW() REG_READ(0x3ff20c00) +// This variable disappeared from lwip in SDK 2.0... extern uint32_t realtime_stamp; // 1sec NTP ticker typedef struct syslog_host_t syslog_host_t; 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 */ +