mirror of https://github.com/jeelabs/esp-link.git
commit
acc7c90575
@ -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 |
@ -0,0 +1,112 @@ |
||||
Building esp-link |
||||
================= |
||||
|
||||
Before you build esp-link, consider that you can download ready-made firmware images! |
||||
Just head over to the [release section](https://github.com/jeelabs/esp-link/releases) |
||||
and download the tgz archive. |
||||
|
||||
If you decide to build your own, there are a number of options: |
||||
- On linux x86 download the ready-built toolchain and patched SDK like the automated build does |
||||
and compile the firmware |
||||
- On linux use a docker image with the toolchain and the SDK to compile the firmware |
||||
- On linux download and build the toolchain, download and patch the SDK, then compile the firmware |
||||
- On windows use a docker image with the toolchain and the SDK to compile the firmware |
||||
- On windows install mingw, python, java, and a slew of other tools and then build the |
||||
firmware |
||||
|
||||
Once you have built the firmware you will want to flash it to your esp8266 module. |
||||
Assuming you already have esp-link running you can either go back to the initial flashing |
||||
via the serial port or you can use the over-the-air (i.e. Wifi) update method, which is faster |
||||
and more reliable (unless you have a non-booting version of esp-link). |
||||
The OTA flashing is described at the end of this page, |
||||
the serial flashing is described in [FLASHING.md](FLASHING.md). |
||||
|
||||
### Automated builds |
||||
|
||||
For every commit on github an automated build is made. This means that every branch, including |
||||
master, and every pull request always has an up-to-date build. These builds are made by Travis |
||||
using the instructions in `.travis.yml`, which basically consist of cloning the esp-link repo, |
||||
downloading the compiler toolchain, downloading the Espressif SDK, and running `make`. |
||||
If you're looking for how to build esp-link the travis instructions will always give you |
||||
accurate pointers to what to download. |
||||
|
||||
### Docker (linux or windows) |
||||
|
||||
The [esp-link docker image](https://hub.docker.com/r/jeelabs/esp-link/) contains all the |
||||
tools to build esp-link as well as the appropriate Espressif SDK. *It does not contain the |
||||
esp-link source code!*. You use the docker image just to build the firmware, you don't have |
||||
to do your editing in there. The steps are: |
||||
- clone the esp-link github repo |
||||
- checkout the branch or tag you want (for example the tag `v2.2.3` for that release) |
||||
- cd into the esp-link top directory |
||||
- run `make` in docker while mounting your esp-link directory into the container: |
||||
- linux: `docker run -v $PWD:/esp-link jeelabs/esp-link:latest` |
||||
- windows: `docker run -v c:\somepath\esp-link:/esp-link jeelabs/esp-link:latest`, |
||||
where `somepath` is the path to where you cloned esp-link, you probably end up with |
||||
something like `-v c:\Users\tve\source\esp-link:/esp-link` |
||||
- if you are not building esp-link `master` then read the release notes to see which version of |
||||
the Espressif SDK you need and use that as tag for the container image, such as |
||||
`jeelabs/esp-link:SDK2.0.0.p1`; you can see the list of available SDKs on |
||||
[dockerhub](https://hub.docker.com/r/jeelabs/esp-link/tags/)o |
||||
|
||||
Sample steps to build esp-link v2.2.3 on a Win7 Pro x64 (these use the docker terminal, there |
||||
are multiple way to skin the proverbial cat...): |
||||
1) Install Docker Toolbox ( http://www.docker.com/products/docker-toolbox ) |
||||
2) Install Git Desktop ( https://desktop.github.com/ ) |
||||
3) Clone esp-link from Github master to local repository ( https://github.com/jeelabs/esp-link ) |
||||
4) Open Docker Quickstart Terminal |
||||
5) cd to local esp-link git repository ( C:\Users\xxxxx\Documents\GitHub\esp-link ) |
||||
6) Run "docker run -v $PWD:/esp-link jeelabs/esp-link" command in Docker Quickstart Terminal window |
||||
|
||||
Note: there has been one report of messed-up timestamps on windows, the symptom is that `make` |
||||
complains about file modification times being in the future. This may be due to the different |
||||
way Windows and Linux handle time zones and daylight savings time. PLease report if you |
||||
encounter this or know a solution. |
||||
|
||||
### Linux |
||||
|
||||
The firmware has been built using the https://github.com/pfalcon/esp-open-sdk[esp-open-sdk] |
||||
on a Linux system. Create an esp8266 directory, install the esp-open-sdk into a sub-directory |
||||
using the *non-standalone* install (i.e., there should not be an sdk directory in the esp-open-sdk |
||||
dir when done installing, *if you use the standalone install you will get compilation errors* |
||||
with std types, such as `uint32_t`). |
||||
|
||||
Download the Espressif "NONOS" SDK (use the version mentioned in the release notes) from their |
||||
http://bbs.espressif.com/viewforum.php?f=5[download forum] and also expand it into a |
||||
sub-directory. Often there are patches to apply, in that case you need to download the patches |
||||
from the same source and apply them. |
||||
|
||||
You can simplify your life (and avoid the hour-long build time for esp-open-sdk) if you are |
||||
on an x86 box by downloading the packaged and built esp-open-sdk and the fully patches SDKfrom the |
||||
links used in the `.travis.yaml`. |
||||
|
||||
Clone the esp-link repository into a third sub-directory and check out the tag you would like, |
||||
such as `git checkout v2.2.3`. |
||||
This way the relative paths in the Makefile will work. |
||||
If you choose a different directory structure look at the top of the Makefile for the |
||||
appropriate environment variables to define. |
||||
Do not use the source tarballs from the release page on github, |
||||
these will give you trouble compiling because the Makefile uses git to determine the esp-link |
||||
version being built. |
||||
|
||||
In order to OTA-update the esp8266 you should `export ESP_HOSTNAME=...` with the hostname or |
||||
IP address of your module. |
||||
|
||||
Now, build the code: `make` in the top-level of esp-link. If you want to se the commands being |
||||
issued, use `VERBOSE=1 make`. |
||||
|
||||
A few notes from others (I can't fully verify these): |
||||
|
||||
- You may need to install `zlib1g-dev` and `python-serial` |
||||
- Make sure you have the correct version of the SDK |
||||
- Make sure the paths at the beginning of the makefile are correct |
||||
- Make sure `esp-open-sdk/xtensa-lx106-elf/bin` is in the PATH set in the Makefile |
||||
|
||||
### Windows |
||||
|
||||
Please consider installing docker and using the docker image to save yourself grief getting all |
||||
the tools installed and working. |
||||
If you do want to compile "natively" on Windows it certainly is possible. |
||||
|
||||
It is possible to build esp-link on Windows, but it requires a |
||||
[gaggle of software to be installed](WINDOWS.md) |
@ -0,0 +1,29 @@ |
||||
Contributing to Esp-Link |
||||
======================== |
||||
|
||||
Esp-link is not the work of a single individual, rather many people have contributed directly or indirectly. |
||||
Your contribution is very much appreciated, but please follow these guidelines to make the task easier on |
||||
everyone. |
||||
|
||||
- Contributions do not have to be in the form of code: often documentation, how-tos are very valuable and answering questions |
||||
in github issues as well as gitter is also very valuable and welcome! |
||||
- Before you make a change or submit a change via a pull request, **open an issue and discuss your proposed change**. Gitter |
||||
is a good alternative to a github issue. This ensures that you don't spend time doing work that ultimately won't be accepted. |
||||
There's nothing more frustrating than receiving a pull-request that has lots of goodies but doesn't fit because it wasn't |
||||
discussed and agreed upon up-front. |
||||
- Keep your pull request as small as practical, if you have 3 things you want to change, please create 3 pull requests, |
||||
or at the very least, make sure your 3 changes are in different commits. This makes the review and testing easier |
||||
and ensures that if one feature is good to go it can be merged even if another feature needs more tweaking. |
||||
- The esp-link codebase is not uniform, it comes from a variety of sources, in particular esphttpd. A result of this is |
||||
that there is more than one coding style in use. If you make changes to existing files, please respect the file's |
||||
coding style (yes, sometimes that's not even totally uniform). Your overall goal should be for your code or changes to |
||||
look as if the original author had made them, not how you would like them to look. |
||||
- Changes that reformat or reorganize code will generally not be accepted, please do not mix them with other functionality |
||||
changes you are making and certainly discuss them first. Accept the fact that some people prefer bastards over pure-breads ;-). |
||||
- Esp-link has a mission stated in the readme.md, changes that deviate from that mission will generally be rejected. The reason |
||||
is that at the end of the day focusing on doing one thing well has a higher chance of succeeding than doing many things. |
||||
In that sense, esp-link is not a swiss-army knife firmware. (This being said, many people have used esp-link as a basis to add |
||||
their own functionality, which is very cool.) |
||||
|
||||
I believe the above guidelines are pretty standard across a very large number of open source projects and not unique to esp-link, |
||||
so please do not get discouraged. Thank you for taking a look at esp-link! |
@ -0,0 +1,31 @@ |
||||
# 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 \ |
||||
zlib1g-dev openjdk-8-jre-headless |
||||
|
||||
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 - |
||||
|
||||
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 |
@ -0,0 +1,204 @@ |
||||
Flashing esp-link |
||||
================= |
||||
|
||||
### Hardware configuration for normal operation |
||||
|
||||
This firmware is designed for any esp8266 module. |
||||
The recommended connections for an esp-01 module are: |
||||
|
||||
- URXD: connect to TX of microcontroller |
||||
- UTXD: connect to RX of microcontroller |
||||
- GPIO0: connect to RESET of microcontroller |
||||
- GPIO2: optionally connect green LED to 3.3V (indicates wifi status) |
||||
|
||||
The recommended connections for an esp-12 module are: |
||||
|
||||
- URXD: connect to TX of microcontroller |
||||
- UTXD: connect to RX of microcontroller |
||||
- GPIO12: connect to RESET of microcontroller |
||||
- GPIO13: connect to ISP of LPC/ARM microcontroller (not used with Arduino/AVR) |
||||
- GPIO0: either a 1k-10k pull-up resistor to 3.3v or a green "conn" LED via a 1k-2.2k |
||||
resistor to 3.3V (indicates wifi status) |
||||
- GPIO2: either a 1k-10k pull-up resistor to 3.3v or a yellow "ser" LED via a 1k-2.2k |
||||
resistor to 3.3V (indicates serial activity) |
||||
|
||||
At boot time the esp8266 ROM outputs a boot message on UTXD, this can cause problems to the attached |
||||
microcontroller. If you need to avoid this, you can configure esp-link to swap the uart pins. |
||||
You should then connect the esp-12 module as follows and choose the "swap_uart" pin assignment |
||||
in the esp-link web interface: |
||||
|
||||
- GPIO13: connect to TX of microcontroller |
||||
- GPIO15: connect to RX of microcontroller and use a pull-down to ensure proper booting |
||||
- GPIO12: connect to RESET of microcontroller |
||||
- GPIO14: connect to ISP of LPC/ARM microcontroller (not used with Arduino/AVR) |
||||
- GPIO0: either a 1k-10k pull-up resistor to 3.3v or a green "conn" LED via a 1k-2.2k |
||||
resistor to 3.3V (indicates wifi status) |
||||
- GPIO2: either a 1k-10k pull-up resistor to 3.3v or a yellow "ser" LED via a 1k-2.2k |
||||
resistor to 3.3V (indicates serial activity) |
||||
|
||||
The GPIO pin assignments can be changed dynamically in the web UI and are saved in flash. |
||||
|
||||
### Hardware configuration for flashing |
||||
|
||||
To flash firmware onto the esp8266 via the serial port the following must be observed: |
||||
- GPIO0 must be low when reset ends to put the esp8266 into flash programming mode, it must be high |
||||
to enter normal run mode |
||||
- GPIO2 must be high (pull-up resistor) |
||||
- GPIO15 must be low (pull-down resistor) |
||||
|
||||
### Initial serial flashing |
||||
|
||||
Download the latest [release](https://github.com/jeelabs/esp-link/releases) or use the |
||||
`user1.bin` file that is produced by the build process. |
||||
You will need to flash the bootloader, the `user1.bin` firmware, blank wifi settings, and init data |
||||
as described below. |
||||
|
||||
_Important_: the firmware adapts to the size of the flash chip using information |
||||
stored in the boot sector (address 0). This is the standard way that the esp8266 SDK detects |
||||
the flash size. What this means is that you need to set this properly when you flash the bootloader. |
||||
If you use esptool.py you can do it using the -ff and -fs options. See the end of this page for |
||||
instructions on installing esptool.py. |
||||
|
||||
The short version for the serial flashing is: |
||||
- flash `boot_v1.X.bin` from the official SDK or from the release tgz to `0x00000` |
||||
- flash `blank.bin` from the official SDK or from the tgz to `0x3FE000` |
||||
- flash `esp_init_data_default.bin` from the official SDK or from the tgz to `0x3FC000` |
||||
- flash `user1.bin` to `0x01000` |
||||
- be sure to use the commandline flags to set the correct flash size when flashing the bootloader |
||||
- some of the addresses vary with flash chip size |
||||
|
||||
After the initial flashing if you want to update the firmware it is recommended to use the |
||||
over-the-air update described further down. If you want to update serially you only need to |
||||
reflash `user1.bin`. |
||||
|
||||
### 32Mbit / 4Mbyte module |
||||
On Linux using esptool.py this turns into the following for a 32mbit=4MByte flash chip, |
||||
such as an esp-12 module typically has (_substitute the appropriate release number and bootloader |
||||
version number_): |
||||
``` |
||||
curl -L https://github.com/jeelabs/esp-link/releases/download/v2.2.3/esp-link-v2.2.3.tgz | \ |
||||
tar xzf - |
||||
cd esp-link-v2.2.3 |
||||
esptool.py --port /dev/ttyUSB0 --baud 230400 write_flash -fs 32m -ff 80m \ |
||||
0x00000 boot_v1.5.bin 0x1000 user1.bin \ |
||||
0x3FC000 esp_init_data_default.bin 0x3FE000 blank.bin |
||||
``` |
||||
I use a high baud rate as shown above because I'm impatient, but that's not required. |
||||
|
||||
### 4Mbit / 512Kbyte module |
||||
``` |
||||
curl -L https://github.com/jeelabs/esp-link/releases/download/v2.2.3/esp-link-v2.2.3.tgz | \ |
||||
tar xzf - |
||||
cd esp-link-v2.2.3 |
||||
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash -fs 4m -ff 40m \ |
||||
0x00000 boot_v1.5.bin 0x1000 user1.bin \ |
||||
0x7C000 esp_init_data_default.bin 0x7E000 blank.bin |
||||
``` |
||||
The `-fs 4m -ff40m` options say 4Mbits and 40Mhz as opposed to 32Mbits at 80Mhz for the 4MByte |
||||
flash modules. Note the different address for esp_init_data_default.bin and blank.bin |
||||
(the SDK stores its wifi settings near the end of flash, so it changes with flash size). |
||||
|
||||
For __8Mbit / 1MByte__ modules the addresses are 0xFC000 and 0xFE000. |
||||
|
||||
__Warning__: there is a bug in boot_v1.5.bin which causes it to only boot into user1 once. |
||||
If that fails it gets stuck trying to boot into user2. If this happens (can be seen in the |
||||
boot output on uart2 at 76600 baud) reflash just blank.bin at 0x7E000 (4Mbit module). (Sigh) |
||||
|
||||
## Updating the firmware over-the-air |
||||
|
||||
This firmware supports over-the-air (OTA) flashing for modules with 1MByte or more flash, |
||||
so you do not have to deal with serial flashing again after the initial one! |
||||
The recommended way to flash is to use `make wiflash` |
||||
if you are also building the firmware and `./wiflash` if you are downloading firmware binaries. |
||||
|
||||
The resulting commandlines are: |
||||
``` |
||||
ESP_HOSTNAME=192.168.1.5 make wiflash |
||||
``` |
||||
or assuming mDNS is working: |
||||
``` |
||||
ESP_HOSTNAME=esp-link.local make wiflash |
||||
``` |
||||
or using wiflash.sh: |
||||
``` |
||||
./wiflash.sh <esp-hostname> user1.bin user2.bin |
||||
``` |
||||
|
||||
The flashing, restart, and re-associating with your wireless network takes about 15 seconds |
||||
and is fully automatic. The first 1MB of flash are divided into two 512KB partitions allowing for new |
||||
code to be uploaded into one partition while running from the other. This is the official |
||||
OTA upgrade method supported by the SDK, except that the firmware is POSTed to the module |
||||
using curl as opposed to having the module download it from a cloud server. On a module with |
||||
512KB flash there is only space for one partition and thus no way to do an OTA update. |
||||
|
||||
If you need to clear the wifi settings you need to reflash the `blank.bin` |
||||
using the serial method. |
||||
|
||||
The flash configuration and the OTA upgrade process is described in more detail |
||||
in [FLASH.md](FLASH.md). |
||||
|
||||
## Installing esptool.py on Linux |
||||
|
||||
On Linux use [esptool.py](https://github.com/themadinventor/esptool) to flash the esp8266. |
||||
If you're a little python challenged then the following install instructions might help: |
||||
- Install ez_setup with the following two commands (I believe this will do something |
||||
reasonable if you already have it): |
||||
|
||||
wget https://bootstrap.pypa.io/ez_setup.py |
||||
python ez_setup.py |
||||
|
||||
- Install esptool.py: |
||||
|
||||
git clone https://github.com/themadinventor/esptool.git |
||||
cd esptool |
||||
python setup.py install |
||||
cd .. |
||||
esptool.py -h |
||||
|
||||
## Installing esptool.py on Windows |
||||
|
||||
Esptool is a pythin pgm that works just fine on windows. These instructions assume that git and |
||||
python are available from the commandline. |
||||
|
||||
Start a command line, clone esptool, and run `python setup.py install` in esptool's |
||||
directory (this step needs to be done only once): |
||||
``` |
||||
> git clone https://github.com/themadinventor/esptool.git |
||||
Cloning into 'esptool'... |
||||
remote: Counting objects: 268, done. |
||||
emote: Total 268 (delta 0), reused 0 (delta 0), pack-reused 268 |
||||
Receiving objects: 100% (268/268), 99.66 KiB | 0 bytes/s, done. |
||||
Resolving deltas: 100% (142/142), done. |
||||
Checking connectivity... done. |
||||
|
||||
> cd esptool |
||||
|
||||
> python setup.py install |
||||
running install |
||||
... |
||||
... |
||||
... |
||||
Finished processing dependencies for esptool==0.1.0 |
||||
``` |
||||
|
||||
Download and unzip the latest esp-link release package, and start a commandline |
||||
in that directory. The command to run is pretty much the same as for linux. |
||||
Adjust the path to esptool and the COM port if you don't have the ESP on COM12. 460800 |
||||
baud worked just fine for me, writing at ~260kbit/s instead of ~80kbit/s. |
||||
``` |
||||
>python "../esptool/esptool.py" --port COM12 --baud 115200 write_flash \ |
||||
--flash_freq 80m --flash_mode qio --flash_size 32m \ |
||||
0x0000 boot_v1.6.bin 0x1000 user1.bin \ |
||||
0x3FC000 esp_init_data_default.bin 0x3FE000 blank.bin |
||||
Connecting... |
||||
Erasing flash... |
||||
Wrote 3072 bytes at 0x00000000 in 0.3 seconds (79.8 kbit/s)... |
||||
Erasing flash... |
||||
Wrote 438272 bytes at 0x00001000 in 43.4 seconds (80.7 kbit/s)... |
||||
Erasing flash... |
||||
Wrote 1024 bytes at 0x003fc000 in 0.1 seconds (83.6 kbit/s)... |
||||
Erasing flash... |
||||
Wrote 4096 bytes at 0x003fe000 in 0.4 seconds (83.4 kbit/s)... |
||||
|
||||
Leaving... |
||||
``` |
@ -1,535 +0,0 @@ |
||||
ESP-LINK: Wifi-Serial Bridge w/REST&MQTT |
||||
======================================== |
||||
Thorsten von Eicken |
||||
:toc: |
||||
:toc-title!: |
||||
:toc-placement!: |
||||
|
||||
This firmware connects an attached micro-controller to the internet using a ESP8266 Wifi module. |
||||
It implements a number of features: |
||||
|
||||
[options="compact"] |
||||
- transparent bridge between Wifi and serial, useful for debugging or inputting into a uC |
||||
- flash-programming attached Arduino/AVR microcontrollers, esp8266 modules, as well as |
||||
LPC800-series and other ARM microcontrollers via Wifi |
||||
- built-in stk500v1 programmer for AVR uC's: program using HTTP upload of hex file |
||||
- outbound REST HTTP requests from the attached micro-controller to the internet |
||||
- MQTT client pub/sub from the attached micro-controller to the internet |
||||
|
||||
The firmware includes a tiny HTTP server based on |
||||
http://www.esp8266.com/viewforum.php?f=34[esphttpd] |
||||
with a simple web interface, many thanks to Jeroen Domburg for making it available! |
||||
The REST and MQTT functionality are loosely based on https://github.com/tuanpmt/espduino |
||||
but significantly rewritten and no longer protocol compatible, thanks to tuanpmt for the |
||||
inspiration! |
||||
|
||||
Many thanks to https://github.com/brunnels for contributions in particular around the espduino |
||||
functionality. Thank you also to https://github.com/susisstrolch, https://github.com/bc547, |
||||
and https://github.com/katast for additional contributions! |
||||
|
||||
[float] |
||||
Table of Contents |
||||
----------------- |
||||
|
||||
toc::[] |
||||
|
||||
Releases & Downloads |
||||
-------------------- |
||||
|
||||
- https://github.com/jeelabs/esp-link/releases/tag/v2.1.7[V2.1.7] is the most recent release. |
||||
It has the new built-in stk500v1 programmer and works on all modules (esp-01 through esp-12). |
||||
- https://github.com/jeelabs/esp-link/releases/tag/v2.2.beta2[V2.2.beta2] will be coming |
||||
up shortly with mDNS, sNTP, and syslog support, stay tuned... |
||||
- See https://github.com/jeelabs/esp-link/releases[all releases]. |
||||
|
||||
For quick support and questions chat at |
||||
image:https://badges.gitter.im/Join%20Chat.svg[link="https://gitter.im/jeelabs/esp-link"] |
||||
|
||||
Intro |
||||
----- |
||||
|
||||
### Esp-link goals |
||||
|
||||
The goal of the esp-link project is to create an advanced Wifi co-processor. Esp-link assumes that |
||||
there is a "main processor" (also referred to as "attached uController") and that esp-link's role |
||||
is to facilitate communication over Wifi. Where esp-link is a bit unusual is that it's not really |
||||
just a Wifi interface or a slave co-processor. In some sense it's the master, because the main |
||||
processor can be reset, controlled and reprogrammed through esp-link. The three main areas of |
||||
functionality in esp-link are: |
||||
|
||||
- reprogramming and debugging the attached uC |
||||
- letting the attached uC make outbound communication and offloading the protocol processing |
||||
- forwarding inbound communication and offloading the protocol processing (this part is the |
||||
least developed) |
||||
|
||||
The goal of the project is also to remain focused on the above mission. In particular, esp-link |
||||
is not a platform for stand-alone applications and it does not support connecting sensors or |
||||
actuators directly to it. A few users have taken esp-link as a starting point for doing these |
||||
things and that's great, but there's also value in keeping the mainline esp-link project |
||||
focused on a clear mission. |
||||
|
||||
### Esp-link uses |
||||
|
||||
The simplest use of esp-link is as a transparent serial to wifi bridge. You can flash an attached |
||||
uC over wifi and you can watch the uC's serial debug output by connecting to port 23 or looking |
||||
at the uC Console web page. |
||||
|
||||
The next level is to use the outbound connectivity of esp-link in the uC code. For example, the |
||||
uC can use REST requests to services like thingspeak.com to send sensor values that then get |
||||
stored and plotted by the external service. |
||||
The uC can also use REST requests to retrieve simple configuration |
||||
information or push other forms of notifications. (MQTT functionality is forthcoming.) |
||||
|
||||
An additional option is to add code to esp-link to customize it and put all the communication |
||||
code into esp-link and only keep simple sensor/actuator control in the attached uC. In this |
||||
mode the attached uC sends custom commands to esp-link with sensor/acturator info and |
||||
registers a set of callbacks with esp-link that control sensors/actuators. This way, custom |
||||
commands in esp-link can receive MQTT messages, make simple callbacks into the uC to get sensor |
||||
values or change actuators, and then respond back with MQTT. The way this is architected is that |
||||
the attached uC registers callbacks at start-up such that the code in the esp doesn't need to |
||||
know which exact sensors/actuators the attached uC has, it learns that through the initial |
||||
callback registration. |
||||
|
||||
### Eye Candy |
||||
|
||||
These screen shots show the Home page, the Wifi configuration page, the console for the |
||||
attached microcontroller, and the pin assignments card: |
||||
|
||||
image:https://cloud.githubusercontent.com/assets/39480/8261425/6ca395a6-167f-11e5-8e92-77150371135a.png[width="45%"] |
||||
image:https://cloud.githubusercontent.com/assets/39480/8261427/6caf7326-167f-11e5-8085-bc8b20159b2b.png[width="45%"] |
||||
image:https://cloud.githubusercontent.com/assets/39480/8261426/6ca7f75e-167f-11e5-827d-9a1c582ad05d.png[width="45%"] |
||||
image:https://cloud.githubusercontent.com/assets/39480/8261658/11e6c64a-1681-11e5-82d0-ea5ec90a6ddb.png[width="45%"] |
||||
|
||||
Getting Started |
||||
--------------- |
||||
|
||||
### Hardware configuration |
||||
|
||||
This firmware is designed for any esp8266 module. |
||||
The recommended connections for an esp-01 module are: |
||||
|
||||
- URXD: connect to TX of microcontroller |
||||
- UTXD: connect to RX of microcontroller |
||||
- GPIO0: connect to RESET of microcontroller |
||||
- GPIO2: optionally connect green LED to 3.3V (indicates wifi status) |
||||
|
||||
The recommended connections for an esp-12 module are: |
||||
|
||||
- URXD: connect to TX of microcontroller |
||||
- UTXD: connect to RX of microcontroller |
||||
- GPIO12: connect to RESET of microcontroller |
||||
- GPIO13: connect to ISP of LPC/ARM microcontroller or to GPIO0 of esp8266 being programmed |
||||
(not used with Arduino/AVR) |
||||
- GPIO0: optionally connect green "conn" LED to 3.3V (indicates wifi status) |
||||
- GPIO2: optionally connect yellow "ser" LED to 3.3V (indicates serial activity) |
||||
|
||||
If your application has problems with the boot message that is output at ~74600 baud by the ROM |
||||
at boot time you can connect an esp-12 module as follows and choose the "swap_uart" pin assignment |
||||
in the esp-link web interface: |
||||
|
||||
- GPIO13: connect to TX of microcontroller |
||||
- GPIO15: connect to RX of microcontroller |
||||
- GPIO1/UTXD: connect to RESET of microcontroller |
||||
- GPIO3/URXD: connect to ISP of LPC/ARM microcontroller or to GPIO0 of esp8266 being programmed |
||||
(not used with Arduino/AVR) |
||||
- GPIO0: optionally connect green "conn" LED to 3.3V (indicates wifi status) |
||||
- GPIO2: optionally connect yellow "ser" LED to 3.3V (indicates serial activity) |
||||
|
||||
If you are using an FTDI connector, GPIO12 goes to DTR and GPIO13 goes to CTS (or vice-versa, I've |
||||
seen both used, sigh). |
||||
|
||||
The GPIO pin assignments can be changed dynamically in the web UI and are saved in flash. |
||||
|
||||
### Initial flashing |
||||
|
||||
If you want to simply flash a pre-built firmware binary, you can download the latest |
||||
https://github.com/jeelabs/esp-link/releases[release] and use your favorite |
||||
ESP8266 flashing tool to flash the bootloader, the firmware, and blank settings. |
||||
Detailed instructions are provided in the release notes. |
||||
|
||||
_Important_: the firmware adapts automatically to the size of the flash chip using information |
||||
stored in the boot sector (address 0). This is the standard way that the esp8266 SDK detects |
||||
the flash size. What this means is that you need to set this properly when you flash the bootloader. |
||||
If you use esptool.py you can do it using the -ff and -fs options. |
||||
|
||||
### Wifi configuration overview |
||||
|
||||
For proper operation the end state that esp-link needs to arrive at is to have it |
||||
join your pre-existing wifi network as a pure station. |
||||
However, in order to get there esp-link will start out as an access point and you'll have |
||||
to join its network to configure it. The short version is: |
||||
|
||||
1. esp-link creates a wifi access point with an SSID of the form `ESP_012ABC` (some modules |
||||
use a different SSID form, such as `ai-thinker-012ABC`) |
||||
2. you join your laptop or phone to esp-link's network as a station and you configure |
||||
esp-link wifi with your network info by pointing your browser at `http://192.168.4.1/` |
||||
3. you set a hostname for esp-link on the "home" page, or leave the default ("esp-link") |
||||
4. esp-link starts to connect to your network while continuing to also be an access point |
||||
("AP+STA"), the esp-link may show up with a `${hostname}.local` hostname |
||||
(depends on your DHCP/DNS config) |
||||
4. esp-link succeeds in connecting and shuts down its own access point after 15 seconds, |
||||
you reconnect your laptop/phone to your normal network and access esp-link via its hostname |
||||
or IP address |
||||
|
||||
### LED indicators |
||||
|
||||
Assuming appropriate hardware attached to GPIO pins, the green "conn" LED will show the wifi |
||||
status as follows: |
||||
|
||||
- Very short flash once a second: not connected to a network and running as AP+STA, i.e. |
||||
trying to connect to the configured network |
||||
- Very short flash once every two seconds: not connected to a network and running as AP-only |
||||
- Even on/off at 1HZ: connected to the configured network but no IP address (waiting on DHCP) |
||||
- Steady on with very short off every 3 seconds: connected to the configured network with an |
||||
IP address (esp-link shuts down its AP after 60 seconds) |
||||
|
||||
The yellow "ser" LED will blink briefly every time serial data is sent or received by the esp-link. |
||||
|
||||
### Troubleshooting |
||||
|
||||
- verify that you have sufficient power, borderline power can cause the esp module to seemingly |
||||
function until it tries to transmit and the power rail collapses |
||||
- if you just cannot flash your esp8266 module (some people call it the zombie mode) make sure you |
||||
have gpio0 and gpio15 pulled to gnd with a 1K resistor, gpio2 tied to 3.3V with 1K resistor, and |
||||
RX/TX connected without anything in series. If you need to level shift the signal going into the |
||||
esp8266's RX use a 1K resistor. Use 115200 baud in the flasher. |
||||
(For a permanent set-up I would use higher resistor values but |
||||
when nothing seems to work these are the ones I try.) |
||||
- if the flashing succeeded, check the "conn" LED to see which mode esp-link is in (see LED info above) |
||||
- reset or power-cycle the esp-link to force it to become an access-point if it can't |
||||
connect to your network within 15-20 seconds |
||||
- if the LED says that esp-link is on your network but you can't get to it, make sure your |
||||
laptop is on the same network (and no longer on the esp's network) |
||||
- if you do not know the esp-link's IP address on your network, try `esp-link.local`, try to find |
||||
the lease in your DHCP server; if all fails, you may have to turn off your access point (or walk |
||||
far enough away) and reset/power-cycle esp-link, it will then fail to connect and start its |
||||
own AP after 15-20 seconds |
||||
|
||||
Configuration details |
||||
--------------------- |
||||
|
||||
### Wifi |
||||
|
||||
After you have serially flashed the module it will create a wifi access point (AP) with an |
||||
SSID of the form `ESP_012ABC` where 012ABC is a piece of the module's MAC address. |
||||
Using a laptop, phone, or tablet connect to this SSID and then open a browser pointed at |
||||
http://192.168.4.1/, you should then see the esp-link web site. |
||||
|
||||
Now configure the wifi. The desired configuration is for the esp-link to be a |
||||
station on your local wifi network so you can communicate with it from all your computers. |
||||
|
||||
To make this happen, navigate to the wifi page and you should see the esp-link scan |
||||
for available networks. You should then see a list of detected networks on the web page and you |
||||
can select yours. |
||||
Enter a password if your network is secure (highly recommended...) and hit the connect button. |
||||
|
||||
You should now see that the esp-link has connected to your network and it should show you |
||||
its IP address. _Write it down_. You will then have to switch your laptop, phone, or tablet |
||||
back to your network and then you can connect to the esp-link's IP address or, depending on your |
||||
network's DHCP/DNS config you may be able to go to http://esp-link.local |
||||
|
||||
At this point the esp-link will have switched to STA mode and be just a station on your |
||||
wifi network. These settings are stored in flash and thereby remembered through resets and |
||||
power cycles. They are also remembered when you flash new firmware. Only flashing `blank.bin` |
||||
via the serial port as indicated above will reset the wifi settings. |
||||
|
||||
There is a fail-safe, which is that after a reset or a configuration change, if the esp-link |
||||
cannot connect to your network it will revert back to AP+STA mode after 15 seconds and thus |
||||
both present its `ESP_012ABC`-style network and continue trying to reconnect to the requested network. |
||||
You can then connect to the esp-link's AP and reconfigure the station part. |
||||
|
||||
One open issue (#28) is that esp-link cannot always display the IP address it is getting to the browser |
||||
used to configure the ssid/password info. The problem is that the initial STA+AP mode may use |
||||
channel 1 and you configure it to connect to an AP on channel 6. This requires the ESP8266's AP |
||||
to also switch to channel 6 disconnecting you in the meantime. |
||||
|
||||
### Hostname, description, DHCP, mDNS |
||||
|
||||
You can set a hostname on the "home" page, this should be just the hostname and not a domain |
||||
name, i.e., something like "test-module-1" and not "test-module-1.mydomain.com". |
||||
This has a number of effects: |
||||
|
||||
- you will see the first 12 chars of the hostname in the menu bar (top left of the page) so |
||||
if you have multiple modules you can distinguish them visually |
||||
- esp-link will use the hostname in its DHCP request, which allows you to identify the module's |
||||
MAC and IP addresses in your DHCP server (typ. your wifi router). In addition, some DHCP |
||||
servers will inject these names into the local DNS cache so you can use URLs like |
||||
`hostname.local`. |
||||
- someday, esp-link will inject the hostname into mDNS (multicast DNS, bonjour, etc...) so |
||||
URLs of the form `hostname.local` work for everyone (as of v2.1.beta5 mDNS is disabled due |
||||
to reliability issues with it) |
||||
|
||||
You can also enter a description of up to 128 characters on the home page (bottom right). This |
||||
allows you to leave a memo for yourself, such as "installed in basement to control the heating |
||||
system". This descritpion is not used anywhere else. |
||||
|
||||
Building the firmware |
||||
--------------------- |
||||
|
||||
### Linux |
||||
|
||||
The firmware has been built using the https://github.com/pfalcon/esp-open-sdk[esp-open-sdk] |
||||
on a Linux system. Create an esp8266 directory, install the esp-open-sdk into a sub-directory |
||||
using the *non-standalone* install (i.e., there should not be an sdk directory in the esp-open-sdk |
||||
dir when done installing, if you use the standalone install you will get compilation errors |
||||
with std types, such as `uint32_t`). |
||||
|
||||
Download the Espressif SDK (use the version mentioned in the release notes) from their |
||||
http://bbs.espressif.com/viewforum.php?f=5[download forum] and also expand it into a |
||||
sub-directory. |
||||
|
||||
Clone the esp-link repository into a third sub-directory and check out the tag you would like, |
||||
such as `git checkout v2.1.7`. |
||||
This way the relative paths in the Makefile will work. |
||||
If you choose a different directory structure look at the Makefile for the appropriate environment |
||||
variables to define. |
||||
Do not use the source tarballs from the release page on github, |
||||
these will give you trouble compiling because the Makefile uses git to determine the esp-link |
||||
version being built. |
||||
|
||||
In order to OTA-update the esp8266 you should `export ESP_HOSTNAME=...` with the hostname or |
||||
IP address of your module. |
||||
|
||||
Now, build the code: `make` in the top-level of esp-link. If you want to se the commands being |
||||
issued, use `VERBOSE=1 make`. |
||||
|
||||
A few notes from others (I can't fully verify these): |
||||
|
||||
- You may need to install `zlib1g-dev` and `python-serial` |
||||
- Make sure you have the correct version of the esp_iot_sdk |
||||
- Make sure the paths at the beginning of the makefile are correct |
||||
- Make sure `esp-open-sdk/xtensa-lx106-elf/bin` is in the PATH set in the Makefile |
||||
|
||||
### Windows |
||||
|
||||
It is possible to build esp-link on Windows, but it requires a gaggle of software to be installed: |
||||
|
||||
- Install the unofficial sdk, mingw, SourceTree (gui git client), python 2.7, git cli, Java |
||||
- Use SourceTree to checkout under C:\espressif or wherever you installed the unofficial sdk, |
||||
(see this thread for the unofficial sdk http://www.esp8266.com/viewtopic.php?t=820) |
||||
- Create a symbolic link under c:/espressif for the git bin directory under program files and |
||||
the java bin directory under program files. |
||||
- ... |
||||
|
||||
### Updating the firmware over-the-air |
||||
|
||||
This firmware supports over-the-air (OTA) flashing, so you do not have to deal with serial |
||||
flashing again after the initial one! The recommended way to flash is to use `make wiflash` |
||||
if you are also building the firmware. |
||||
If you are downloading firmware binaries use `./wiflash`. |
||||
`make wiflash` assumes that you set `ESP_HOSTNAME` to the hostname or IP address of your esp-link. |
||||
You can easily do that using something like `ESP_HOSTNAME=192.168.1.5 make wiflash`. |
||||
|
||||
The flashing, restart, and re-associating with your wireless network takes about 15 seconds |
||||
and is fully automatic. The first 1MB of flash are divided into two 512KB partitions allowing for new |
||||
code to be uploaded into one partition while running from the other. This is the official |
||||
OTA upgrade method supported by the SDK, except that the firmware is POSTed to the module |
||||
using curl as opposed to having the module download it from a cloud server. On a module with |
||||
512KB flash there is only space for one partition and thus no way to do an OTA update. |
||||
|
||||
If you are downloading the binary versions of the firmware (links forthcoming) you need to have |
||||
both `user1.bin` and `user2.bin` handy and run `wiflash.sh <esp-hostname> user1.bin user2.bin`. |
||||
This will query the esp-link for which file it needs, upload the file, and then reconnect to |
||||
ensure all is well. |
||||
|
||||
Note that when you flash the firmware the wifi settings are all preserved so the esp-link should |
||||
reconnect to your network within a few seconds and the whole flashing process should take 15-30 |
||||
from beginning to end. If you need to clear the wifi settings you need to reflash the `blank.bin` |
||||
using the serial port. |
||||
|
||||
The flash configuration and the OTA upgrade process is described in more detail in [FLASH.md](FLASH.md) |
||||
|
||||
Serial bridge and connections to Arduino, AVR, ARM, LPC microcontrollers |
||||
------------------------------------------------------------------------ |
||||
|
||||
In order to connect through the esp-link to a microcontroller use port 23. For example, |
||||
on linux you can use `nc esp-hostname 23` or `telnet esp-hostname 23`. |
||||
|
||||
Note that multiple connections to port 23 and 2323 can be made simultaneously. Esp-link will |
||||
intermix characters received on all these connections onto the serial TX and it will |
||||
broadcast incoming characters from the serial RX to all connections. Use with caution! |
||||
|
||||
### Flashing an attached AVR/Arduino |
||||
|
||||
There are three options for reprogramming an attached AVR/Arduino microcontroller: |
||||
|
||||
- Use avrdude and point it at port 23 of esp-link. Esp-link automatically detects the programming |
||||
sequence and issues a reset to the AVR. |
||||
- Use avrdude and point it at port 2323 of esp-link. This is the same as port 23 except that the |
||||
autodectection is not used and the reset happens because port 2323 is used |
||||
- Use curl or a similar tool to HTTP POST the firmware to esp-link. This uses the built-in |
||||
programmer, which only works for AVRs/Arduinos with the optiboot bootloader (which is std). |
||||
|
||||
To reprogram an Arduino / AVR microcontroller by pointing avrdude at port 23 or 2323 you |
||||
specify a serial port of the form `net:esp-link:23` in avrdude's -P option, where |
||||
`esp-link` is either the hostname of your esp-link or its IP address). |
||||
This is instead of specifying a serial port of the form /dev/ttyUSB0. |
||||
Esp-link detects that avrdude starts its connection with a flash synchronization sequence |
||||
and sends a reset to the AVR microcontroller so it can switch into flash programming mode. |
||||
|
||||
To reprogram using the HTTP POST method you need to first issue a POST to put optiboot into |
||||
programming mode: POST to `http://esp-link/pgm/sync`, this starts the process. Then check that |
||||
synchronization with optiboot has been achieved by issuing a GET to the same URL |
||||
(`http://esp-link/pgm/sync`). Repeat until you have sync (takes <500ms normally). Finally |
||||
issue a POST request to `http://esp-link/pgm/upload` with your hex file as POST data (raw, |
||||
not url-encoded or multipart-mime. Please look into the avrflash script for the curl command-line |
||||
details or use that script directly (`./avrflash esp-link.local my_sketch.hex`). |
||||
_Important_: after the initial sync request that resets the AVR you have 10 seconds to get to the |
||||
upload post or esp-link will time-out. So if you're manually entering curl commands have them |
||||
prepared so you can copy&paste! |
||||
|
||||
Beware of the baud rate, which you can set on the uC Console page. Sometimes you may be using |
||||
115200 baud in sketches but the bootloader may use 57600 baud. When you use port 23 or 2323 you |
||||
need to set the baud rate correctly. If you use the built-in programmer (HTTP POST method) then |
||||
esp-link will try the configured baud rate and also 9600, 57600, and 115200 baud, so it should |
||||
work even if you have the wrong baud rate configured... |
||||
|
||||
When to use which method? If port 23 works then go with that. If you have trouble getting sync |
||||
or it craps out in the middle too often then try the built-in programmer with the HTTP POST. |
||||
If your AVR doesn't use optiboot then use port 2323 since esp-link may not recognize the programming |
||||
sequence and not issue a reset if you use port 23. |
||||
|
||||
If you are having trouble with the built-in programmer and see something like this: |
||||
|
||||
-------------------- |
||||
# ./avrflash 192.168.3.104 blink.hex |
||||
Error checking sync: FAILED to SYNC: abandoned after timeout, got: |
||||
:\xF/\x00\xCj\xCz\xCJ\xCZ\xC\xAÜ\xC\xAä\xC\xAÜ\xC\xAä\xC\xBì\xC\xBô\xC\xBì\xC\xBô\xC\xAÜ\xC\xAä\xC |
||||
-------------------- |
||||
|
||||
the most likely cause is a baud rate mismatch and/or a bad connection from the esp8266 to the |
||||
AVRs reset line. |
||||
The baud rate used by esp-link is set on the uC Console web page and, as mentioned above, it will |
||||
automatically try 9600, 57600, and 115200 as well. |
||||
The above garbage characters are most likely due to optiboot timing out and starting the sketch |
||||
and then the sketch sending data at a different baud rate than configured into esp-link. |
||||
Note that sketches don't necessarily use the same baud rate as optiboot, so you may have the |
||||
correct baud rate configured but reset isn't functioning, or reset may be functioning but the |
||||
baud rate may be incorrect. |
||||
|
||||
The output of a successful flash using the built-in programmer looks like this: |
||||
|
||||
-------------------- |
||||
Success. 3098 bytes at 57600 baud in 0.8s, 3674B/s 63% efficient |
||||
-------------------- |
||||
|
||||
This says that the sketch comprises 3098 bytes of flash, was written in 0.8 seconds |
||||
(excludes the initial sync time) at 57600 baud, |
||||
and the 3098 bytes were flashed at a rate of 3674 bytes per second. |
||||
The efficiency measure is the ratio of the actual rate to the serial baud rate, |
||||
thus 3674/5760 = 0.63 (there are 10 baud per character). |
||||
The efficiency is not 100% because there is protocol overhead (such as sync, record type, and |
||||
length characters) |
||||
and there is dead time waiting for an ack or preparing the next record to be sent. |
||||
|
||||
### Details of built-in AVR flash algorithm |
||||
|
||||
The built-in flashing algorithm differs a bit from what avrdude does. The programming protocol |
||||
states that STK_GET_SYNC+CRC_EOP (0x30 0x20) should be sent to synchronize, but that works poorly |
||||
because the AVR's UART only buffers one character. This means that if STK_GET_SYNC+CRC_EOP is |
||||
sent twice there is a high chance that only the last character (CRC_EOP) is actually |
||||
received. If that is followed by another STK_GET_SYNC+CRC_EOP sequence then optiboot receives |
||||
CRC_EOP+STK_GET_SYNC+CRC_EOP which causes it to abort and run the old sketch. Ending up in that |
||||
situation is quite likely because optiboot initializes the UART as one of the first things, but |
||||
then goes off an flashes an LED for ~300ms during which it doesn't empty the UART. |
||||
|
||||
Looking at the optiboot code, the good news is that CRC_EOP+CRC_EOP can be used to get an initial |
||||
response without the overrun danger of the normal sync sequence and this is what esp-link does. |
||||
The programming sequence runs as follows: |
||||
|
||||
- esp-link sends a brief reset pulse (1ms) |
||||
- esp-link sends CRC_EOP+CRC_EOP ~50ms later |
||||
- esp-link sends CRC_EOP+CRC_EOP every ~70-80ms |
||||
- eventually optiboot responds with STK_INSYNC+STK_OK (0x14;0x10) |
||||
- esp-link sends one CRC_EOP to sort out the even/odd issue |
||||
- either optiboot responds with STK_INSYNC+STK_OK or nothing happens for 70-80ms, in which case |
||||
esp-link sends another CRC_EOP |
||||
- esp-link sends STK_GET_SYNC+CRC_EOP and optiboot responds with STK_INSYNC+STK_OK and we're in |
||||
sync now |
||||
- esp-link sends the next command (starts with 'u') and programming starts... |
||||
|
||||
If no sync is achieved, esp-link changes baud rate and the whole thing starts over with a reset |
||||
pulse about 600ms, esp-link gives up after about 5 seconds and reports an error. |
||||
|
||||
### Flashing an attached ARM processor |
||||
|
||||
You can reprogram NXP's LPC800-series and many other ARM processors as well by pointing your |
||||
programmer similarly at the esp-link's port 23. For example, if you are using |
||||
https://github.com/jeelabs/embello/tree/master/tools/uploader a command line like |
||||
`uploader -t -s -w esp-link:23 build/firmware.bin` does the trick. |
||||
The way it works is that the uploader uses telnet protocol escape sequences in order to |
||||
make esp-link issue the appropriate "ISP" and reset sequence to the microcontroller to start the |
||||
flash programming. If you use a different ARM programming tool it will work as well as long as |
||||
it starts the connection with the `?\r\n` synchronization sequence. |
||||
|
||||
### Flashing an attached esp8266 |
||||
|
||||
Yes, you can use esp-link running on one esp8266 module to flash another esp8266 module, |
||||
however it is rather tricky! The problem is not electric, it is wifi interference. |
||||
The basic idea is to use some method to direct the esp8266 flash program to port 2323 of |
||||
esp-link. Using port 2323 with the appropriate wiring will cause the esp8266's reset and |
||||
gpio0 pins to be toggled such that the chip enters the flash programming mode. |
||||
|
||||
One option for connecting the programmer with esp-link is to use my version of esptool.py |
||||
at http://github.com/tve/esptool, which supports specifying a URL instead of a port. Thus |
||||
instead of specifying something like `--port /dev/ttyUSB0` or `--port COM1` you specify |
||||
`--port socket://esp-link.local:2323`. Important: the baud rate specified on the esptool.py |
||||
command-line is irrelevant as the baud rate used by esp-link will be the one set in the |
||||
uC console page. Fortunately the esp8266 bootloader does auto-baud detection. (Setting the |
||||
baud rate to 115200 is recommended.) |
||||
|
||||
Another option is to use a serial-to-tcp port forwarding driver and point that to port 2323 |
||||
of esp-link. On windows users have reported success with |
||||
http://www.hw-group.com/products/hw_vsp/hw_vsp2_en.html[HW Virtual Serial Port] |
||||
|
||||
Now to the interference problem: once the attached esp8266 is reset it |
||||
starts outputting its 26Mhz clock on gpio0, which needs to be attached to |
||||
the esp8266 running esp-link (since it needs to drive gpio0 low during |
||||
the reset to enter flash mode). This 26Mhz signal on gpio0 causes a |
||||
significant amount of radio interference with the result that the esp8266 |
||||
running esp-link has trouble receiving Wifi packets. You can observe this |
||||
by running a ping to esp-link in another window: as soon as the target |
||||
esp8266 is reset, the pings become very slow or stop altogetehr. As soon |
||||
as you remove power to the attached esp8266 the pings resume beautifully. |
||||
|
||||
To try and get the interference under control, try some of the following: |
||||
add a series 100ohm resistor and 100pf capacitor to ground as close to |
||||
the gpio0 pin as possible (basically a low pass filter); and/or pass |
||||
the cable connecting the two esp8266's through a ferrite bead. |
||||
|
||||
### Debug log |
||||
|
||||
The esp-link web UI can display the esp-link debug log (os_printf statements in the code). This |
||||
is handy but sometimes not sufficient. Esp-link also prints the debug info to the UART where |
||||
it is sometimes more convenient and sometimes less... For this reason three UART debug log |
||||
modes are supported that can be set in the web UI (and the mode is saved in flash): |
||||
|
||||
- auto: the UART log starts enabled at boot using uart0 and disables itself when esp-link |
||||
associates with an AP. It re-enables itself if the association is lost. |
||||
- off: the UART log is always off |
||||
- on0: the UART log is always on using uart0 |
||||
- on1: the UART log is always on using uart1 (gpio2 pin) |
||||
|
||||
Note that even if the UART log is always off the ROM prints to uart0 whenever the |
||||
esp8266 comes out of reset. This cannot be disabled. |
||||
|
||||
Outbound HTTP REST requests and MQTT client |
||||
------------------------------------------- |
||||
|
||||
The V2 versions of esp-link use the SLIP protocol over the serial link to support simple outbound |
||||
HTTP REST requests as well as an MQTT client. The SLIP protocol consists of commands with |
||||
binary arguments sent from the |
||||
attached microcontroller to the esp8266, which then performs the command and responds back. |
||||
The responses back use a callback address in the attached microcontroller code, i.e., the |
||||
command sent by the uC contains a callback address and the response from the esp8266 starts |
||||
with that callback address. This enables asynchronous communication where esp-link can notify the |
||||
uC when requests complete or when other actions happen, such as wifi connectivity status changes. |
||||
|
||||
You can find REST and MQTT libraries as well as demo sketches in the |
||||
https://github.com/jeelabs/el-client[el-client] repository. |
||||
|
||||
Contact |
||||
------- |
||||
|
||||
If you find problems with esp-link, please create a github issue. If you have a question, please |
||||
use the gitter chat link at the top of this page. |
@ -0,0 +1,175 @@ |
||||
ESP-LINK: Wifi-Serial Bridge w/REST&MQTT |
||||
======================================== |
||||
|
||||
<img src="https://cloud.githubusercontent.com/assets/39480/19333951/73fcdcbe-90ad-11e6-8572-5e654377275a.png"> |
||||
|
||||
The esp-link firmware connects a micro-controller to the internet using an ESP8266 Wifi module. |
||||
It implements a number of features: |
||||
|
||||
- transparent bridge between Wifi and serial, useful for debugging or inputting into a uC |
||||
- flash-programming attached Arduino/AVR microcontrollers and |
||||
LPC800-series and other ARM microcontrollers via Wifi |
||||
- built-in stk500v1 programmer for AVR uC's: program using HTTP upload of hex file |
||||
- outbound REST HTTP requests from the attached micro-controller to the internet |
||||
- MQTT client pub/sub from the attached micro-controller to the internet |
||||
- serve custom web pages containing data that is dynamically pulled from the attached uC and |
||||
that contain buttons and fields that are transmitted to the attached uC (feature not |
||||
fully ready yet) |
||||
|
||||
The firmware includes a tiny HTTP server based on |
||||
[esphttpd](http://www.esp8266.com/viewforum.php?f=34) |
||||
with a simple web interface, many thanks to Jeroen Domburg for making it available! |
||||
The REST and MQTT functionality are loosely based on [espduino](https://github.com/tuanpmt/espduino) |
||||
but significantly rewritten and no longer protocol compatible, thanks to tuanpmt for the |
||||
inspiration! |
||||
|
||||
The following people contributed significant functionality to esp-link: |
||||
[brunnels](https://github.com/brunnels) (espduino integration), |
||||
[cskarai](https://github.com/cskarai) (custom dynamic web pages), |
||||
[beegee-tokyo](https://github.com/beegee-tokyo) (lots of code documentation), |
||||
[susisstrolch](https://github.com/susisstrolch) (syslog feature), |
||||
[bc547](https://github.com/bc547) and [katast](https://github.com/katast) (misc contributions). |
||||
Esp-link is the work of many contributors! |
||||
|
||||
Note that http://github.com/jeelabs/esp-link is the original esp-link software which has |
||||
notably been forked by arduino.org as [Esp-Link](https://github.com/arduino-org/Esp-Link) and shipped |
||||
with the initial Arduino Uno Wifi. The JeeLabs esp-link has evolved significantly since the |
||||
fork and added cool new features as well as bug fixes. |
||||
|
||||
### Quick links |
||||
|
||||
In this document: [goals](#esp-link-goals), [uses](#esp-link-uses), [eye candy](#eye-candy), |
||||
[getting-started](#getting-started), [serial-bridge](#serial-bridge), [contact](#contact). |
||||
|
||||
Separate documents: |
||||
- [hardware configuration](FLASHING.md), [serial flashing](FLASHING.md#initial-serial-flashing) |
||||
- [wifi configuration](WIFI-CONFIG.md) |
||||
- [troubleshooting](TROUBLESHOOTING.md), [LED indicators](TROUBLESHOOTING.md#led-indicators) |
||||
- [flashing an attached uC](UC-FLASHING.md) |
||||
- [MQTT and outbound REST requests](RESTMQTT.md) |
||||
- [service web pages](WEB-SERVER.md) |
||||
- [building esp-link](BUILDING.md), [over-the-air flashing](BUILDING.md#updating-the-firmware-over-the-air) |
||||
- [flash layout](FLASH.md) |
||||
|
||||
For quick support and questions chat at |
||||
[![Chat at https://gitter.im/jeelabs/esp-link](https://badges.gitter.im/esp-link.svg)](https://gitter.im/jeelabs/esp-link) |
||||
or (a little slower) open a github issue. |
||||
|
||||
Releases & Downloads |
||||
-------------------- |
||||
Esp-link uses semantic versioning. The main change between versions 1.x and 2.x was the |
||||
addition of MQTT and outbound REST requests from the attached uC. The main change between 2.x |
||||
and 3.x will be the addition of custom web pages (this is not ready yet). |
||||
|
||||
- The master branch is currently unstable as we integrate a number of new features to get |
||||
to version 3.0. Please use v2.2.3 unless you want to hack up the latest code! |
||||
This being said, the older functionality seems to work fine on master, YMMV... |
||||
- [V2.2.3](https://github.com/jeelabs/esp-link/releases/tag/v2.2.3) is the most recent release. |
||||
It has a built-in stk500v1 programmer (for AVRs), work on all modules, and supports mDNS, |
||||
sNTP, and syslog. It is built using the Espressif SDK 1.5.4. |
||||
- [V2.1.7](https://github.com/jeelabs/esp-link/releases/tag/v2.1.7) is the previous release. |
||||
- See [all releases](https://github.com/jeelabs/esp-link/releases). |
||||
|
||||
## Esp-link goals |
||||
|
||||
The goal of the esp-link project is to create an advanced Wifi co-processor. Esp-link assumes that |
||||
there is a "main processor" (also referred to as "attached uController") and that esp-link's role |
||||
is to facilitate communication over Wifi. This means that esp-link does not just connect TCP/UDP |
||||
sockets through to the attached uC, rather it implements mostly higher-level functionality to |
||||
offload the attached uC, which often has much less flash and memory than esp-link. |
||||
|
||||
Where esp-link is a bit unusual is that it's not really |
||||
just a Wifi interface or a slave co-processor. In some sense it's the master, because the main |
||||
processor can be reset, controlled and reprogrammed through esp-link. The three main areas of |
||||
functionality in esp-link are: |
||||
|
||||
- reprogramming and debugging the attached uC |
||||
- letting the attached uC make outbound communication and offloading the protocol processing |
||||
- forwarding inbound communication and offloading the protocol processing |
||||
|
||||
The goal of the project is also to remain focused on the above mission. In particular, esp-link |
||||
is not a platform for stand-alone applications and it does not support connecting sensors or |
||||
actuators directly to it. A few users have taken esp-link as a starting point for doing these |
||||
things and that's great, but there's also value in keeping the mainline esp-link project |
||||
focused on a clear mission. |
||||
|
||||
## Esp-link uses |
||||
|
||||
The simplest use of esp-link is as a transparent serial to wifi bridge. You can flash an attached |
||||
uC over wifi and you can watch the uC's serial debug output by connecting to port 23 or looking |
||||
at the uC Console web page. |
||||
|
||||
The next level is to use the outbound connectivity of esp-link in the uC code. For example, the |
||||
uC can use REST requests to services like thingspeak.com to send sensor values that then get |
||||
stored and plotted by the external service. |
||||
The uC can also use REST requests to retrieve simple configuration |
||||
information or push other forms of notifications. (MQTT functionality is forthcoming.) |
||||
|
||||
An additional option is to add code to esp-link to customize it and put all the communication |
||||
code into esp-link and only keep simple sensor/actuator control in the attached uC. In this |
||||
mode the attached uC sends custom commands to esp-link with sensor/acturator info and |
||||
registers a set of callbacks with esp-link that control sensors/actuators. This way, custom |
||||
commands in esp-link can receive MQTT messages, make simple callbacks into the uC to get sensor |
||||
values or change actuators, and then respond back with MQTT. The way this is architected is that |
||||
the attached uC registers callbacks at start-up such that the code in the esp doesn't need to |
||||
know which exact sensors/actuators the attached uC has, it learns that through the initial |
||||
callback registration. |
||||
|
||||
## Eye Candy |
||||
|
||||
These screen shots show the Home page, the Wifi configuration page, the console for the |
||||
attached microcontroller, and the pin assignments card: |
||||
|
||||
<img width="45%" src="https://cloud.githubusercontent.com/assets/39480/19334029/f8128c92-90ad-11e6-804e-9a4796035e9a.png"> |
||||
<img width="45%" src="https://cloud.githubusercontent.com/assets/39480/8261427/6caf7326-167f-11e5-8085-bc8b20159b2b.png"> |
||||
<img width="45%" src="https://cloud.githubusercontent.com/assets/39480/8261426/6ca7f75e-167f-11e5-827d-9a1c582ad05d.png"> |
||||
<img width="30%" src="https://cloud.githubusercontent.com/assets/39480/8261658/11e6c64a-1681-11e5-82d0-ea5ec90a6ddb.png"> |
||||
<img width="45%" src="https://cloud.githubusercontent.com/assets/39480/19334011/e0c3fe40-90ad-11e6-9893-847e805e7b89.png"> |
||||
<img width="45%" src="https://cloud.githubusercontent.com/assets/39480/19333988/c1858cec-90ad-11e6-8b1c-ffed516e1b7f.png"> |
||||
|
||||
Getting Started |
||||
--------------- |
||||
|
||||
To get started you need to: |
||||
1. prepare your esp8266 module for serial flashing |
||||
2. download the latest esp-link release image (you can build your own later) |
||||
3. flash the firmware |
||||
4. configure the Wifi in esp-link for your network |
||||
|
||||
You can then attach a uC and upload a sketch: |
||||
1. attach a uC (e.g. arduino) to your esp8266 module |
||||
2. connect via the serial port to see a pre-loaded sketch running |
||||
3. upload a fresh version of the sketch |
||||
|
||||
From there, more advanced steps are: |
||||
- write a sketch that uses MQTT to communicate, or that makes outbound REST requests |
||||
- create some web pages and write a sketch that populates data in them or reacts to buttons |
||||
and forms |
||||
- make changes or enhancements to esp-link and build your own firmware |
||||
|
||||
### Serial bridge |
||||
|
||||
In order to connect through the esp-link to a microcontroller use port 23. For example, |
||||
on linux you can use `nc esp-hostname 23` or `telnet esp-hostname 23`. |
||||
|
||||
The connections on port 23 and 2323 have a 5 minute inactivity timeout. This is standard with |
||||
Espressif's SDK and esp-link does not change it. The reason is that due to memory limitations only a |
||||
few connections can be open (4 per port) and it's easy for connections to get "lost" staying open |
||||
forever, for example, due to wifi disconnects. That could easily make it impossible to connect to |
||||
esp-link due to connection exhaustion. Something smarter is most likely possible... |
||||
|
||||
Note that multiple connections to port 23 and 2323 can be made simultaneously. Esp-link will |
||||
intermix characters received on all these connections onto the serial TX and it will |
||||
broadcast incoming characters from the serial RX to all connections. Use with caution! |
||||
|
||||
If you are using esp-link to connect to the console of a linux system, such as an rPi, you |
||||
will most likely see what you typed being echoed twice. If you are on a linux system use |
||||
telnet and issue a `mode char` command (in telnet, hit the escape char `^]` and type `mode |
||||
char` at the prompt). If you are using putty on Windows, open the connection settings and |
||||
in the terminal settings set both `local echo` and `local line editing` to `off`. |
||||
|
||||
Contact |
||||
------- |
||||
|
||||
If you find problems with esp-link, please create a github issue. If you have a question, please |
||||
use the gitter chat link at the top of this page. |
@ -0,0 +1,14 @@ |
||||
Esp-link: Outbound HTTP REST requests and MQTT client |
||||
------------------------------------------- |
||||
|
||||
The V2 versions of esp-link use the SLIP protocol over the serial link to support simple outbound |
||||
HTTP REST requests as well as an MQTT client. The SLIP protocol consists of commands with |
||||
binary arguments sent from the |
||||
attached microcontroller to the esp8266, which then performs the command and responds back. |
||||
The responses back use a callback address in the attached microcontroller code, i.e., the |
||||
command sent by the uC contains a callback address and the response from the esp8266 starts |
||||
with that callback address. This enables asynchronous communication where esp-link can notify the |
||||
uC when requests complete or when other actions happen, such as wifi connectivity status changes. |
||||
|
||||
You can find REST and MQTT libraries as well as demo sketches in the |
||||
[el-client](https://github.com/jeelabs/el-client) repository. |
@ -0,0 +1,36 @@ |
||||
Esp-Link troubleshooting |
||||
======================== |
||||
|
||||
### Troubleshooting |
||||
|
||||
- verify that you have sufficient power, borderline power can cause the esp module to seemingly |
||||
function until it tries to transmit and the power rail collapses |
||||
- if you just cannot flash your esp8266 module (some people call it the zombie mode) make sure you |
||||
have gpio0 and gpio15 pulled to gnd with a 1K resistor, gpio2 tied to 3.3V with 1K resistor, and |
||||
RX/TX connected without anything in series. If you need to level shift the signal going into the |
||||
esp8266's RX use a 1K resistor. Use 115200 baud in the flasher. |
||||
(For a permanent set-up I would use higher resistor values but |
||||
when nothing seems to work these are the ones I try.) |
||||
- if the flashing succeeded, check the "conn" LED to see which mode esp-link is in (see LED info above) |
||||
- reset or power-cycle the esp-link to force it to become an access-point if it can't |
||||
connect to your network within 15-20 seconds |
||||
- if the LED says that esp-link is on your network but you can't get to it, make sure your |
||||
laptop is on the same network (and no longer on the esp's network) |
||||
- if you do not know the esp-link's IP address on your network, try `esp-link.local`, try to find |
||||
the lease in your DHCP server; if all fails, you may have to turn off your access point (or walk |
||||
far enough away) and reset/power-cycle esp-link, it will then fail to connect and start its |
||||
own AP after 15-20 seconds |
||||
|
||||
### LED indicators |
||||
|
||||
Assuming appropriate hardware attached to GPIO pins, the green "conn" LED will show the wifi |
||||
status as follows: |
||||
|
||||
- Very short flash once a second: not connected to a network and running as AP+STA, i.e. |
||||
trying to connect to the configured network |
||||
- Very short flash once every two seconds: not connected to a network and running as AP-only |
||||
- Even on/off at 1HZ: connected to the configured network but no IP address (waiting on DHCP) |
||||
- Steady on with very short off every 3 seconds: connected to the configured network with an |
||||
IP address (esp-link shuts down its AP after 60 seconds) |
||||
|
||||
The yellow "ser" LED will blink briefly every time serial data is sent or received by the esp-link. |
@ -0,0 +1,183 @@ |
||||
Flashing an attached Microcontroller |
||||
==================================== |
||||
|
||||
In order to connect through the esp-link to a microcontroller use port 23. For example, |
||||
on linux you can use `nc esp-hostname 23` or `telnet esp-hostname 23`. |
||||
|
||||
Note that multiple connections to port 23 and 2323 can be made simultaneously. Esp-link will |
||||
intermix characters received on all these connections onto the serial TX and it will |
||||
broadcast incoming characters from the serial RX to all connections. Use with caution! |
||||
|
||||
### Flashing an attached AVR/Arduino |
||||
|
||||
There are multiple options for reprogramming an attached AVR/Arduino microcontroller: |
||||
|
||||
- Use avrdude and point it at port 23 of esp-link. Esp-link automatically detects the programming |
||||
sequence and issues a reset to the AVR. |
||||
- Use avrdude and point it at port 2323 of esp-link. This is the same as port 23 except that the |
||||
autodectection is not used and the reset happens because port 2323 is used |
||||
- Use curl or a similar tool to HTTP POST the firmware to esp-link. This uses the built-in |
||||
programmer, which only works for AVRs/Arduinos with the optiboot bootloader (which is std). |
||||
- Use some serial port forwarding software, such as com2com, or hwvsp (you have to uncheck |
||||
nvt in the settings when using the latter). |
||||
|
||||
To reprogram an Arduino / AVR microcontroller by pointing avrdude at port 23 or 2323 you |
||||
specify a serial port of the form `net:esp-link:23` in avrdude's -P option, where |
||||
`esp-link` is either the hostname of your esp-link or its IP address). |
||||
This is instead of specifying a serial port of the form /dev/ttyUSB0. |
||||
Esp-link detects that avrdude starts its connection with a flash synchronization sequence |
||||
and sends a reset to the AVR microcontroller so it can switch into flash programming mode. |
||||
|
||||
Note for Windows users: very recent avrdude versions on Windows support the -P option, while older |
||||
ones don't. See the second-to-last bullet in the |
||||
[avrdude 6.3 release notes]http://savannah.nongnu.org/forum/forum.php?forum_id=8461). |
||||
|
||||
To reprogram using the HTTP POST method you need to first issue a POST to put optiboot into |
||||
programming mode: POST to `http://esp-link/pgm/sync`, this starts the process. Then check that |
||||
synchronization with optiboot has been achieved by issuing a GET to the same URL |
||||
(`http://esp-link/pgm/sync`). Repeat until you have sync (takes <500ms normally). Finally |
||||
issue a POST request to `http://esp-link/pgm/upload` with your hex file as POST data (raw, |
||||
not url-encoded or multipart-mime. Please look into the avrflash script for the curl command-line |
||||
details or use that script directly (`./avrflash esp-link.local my_sketch.hex`). |
||||
_Important_: after the initial sync request that resets the AVR you have 10 seconds to get to the |
||||
upload post or esp-link will time-out. So if you're manually entering curl commands have them |
||||
prepared so you can copy&paste! |
||||
|
||||
Beware of the baud rate, which you can set on the uC Console page. Sometimes you may be using |
||||
115200 baud in sketches but the bootloader may use 57600 baud. When you use port 23 or 2323 you |
||||
need to set the baud rate correctly. If you use the built-in programmer (HTTP POST method) then |
||||
esp-link will try the configured baud rate and also 9600, 57600, and 115200 baud, so it should |
||||
work even if you have the wrong baud rate configured... |
||||
|
||||
When to use which method? If port 23 works then go with that. If you have trouble getting sync |
||||
or it craps out in the middle too often then try the built-in programmer with the HTTP POST. |
||||
If your AVR doesn't use optiboot then use port 2323 since esp-link may not recognize the programming |
||||
sequence and not issue a reset if you use port 23. |
||||
|
||||
If you are having trouble with the built-in programmer and see something like this: |
||||
|
||||
``` |
||||
# ./avrflash 192.168.3.104 blink.hex |
||||
Error checking sync: FAILED to SYNC: abandoned after timeout, got: |
||||
:\xF/\x00\xCj\xCz\xCJ\xCZ\xC\xAÜ\xC\xAä\xC\xAÜ\xC\xAä\xC\xBì\xC\xBô\xC\xBì\xC\xBô\xC\xAÜ\xC\xAä\xC |
||||
``` |
||||
|
||||
the most likely cause is a baud rate mismatch and/or a bad connection from the esp8266 to the |
||||
AVRs reset line. |
||||
The baud rate used by esp-link is set on the uC Console web page and, as mentioned above, it will |
||||
automatically try 9600, 57600, and 115200 as well. |
||||
The above garbage characters are most likely due to optiboot timing out and starting the sketch |
||||
and then the sketch sending data at a different baud rate than configured into esp-link. |
||||
Note that sketches don't necessarily use the same baud rate as optiboot, so you may have the |
||||
correct baud rate configured but reset isn't functioning, or reset may be functioning but the |
||||
baud rate may be incorrect. |
||||
|
||||
The output of a successful flash using the built-in programmer looks like this: |
||||
|
||||
``` |
||||
Success. 3098 bytes at 57600 baud in 0.8s, 3674B/s 63% efficient |
||||
``` |
||||
|
||||
This says that the sketch comprises 3098 bytes of flash, was written in 0.8 seconds |
||||
(excludes the initial sync time) at 57600 baud, |
||||
and the 3098 bytes were flashed at a rate of 3674 bytes per second. |
||||
The efficiency measure is the ratio of the actual rate to the serial baud rate, |
||||
thus 3674/5760 = 0.63 (there are 10 baud per character). |
||||
The efficiency is not 100% because there is protocol overhead (such as sync, record type, and |
||||
length characters) |
||||
and there is dead time waiting for an ack or preparing the next record to be sent. |
||||
|
||||
### Details of built-in AVR flash algorithm |
||||
|
||||
The built-in flashing algorithm differs a bit from what avrdude does. The programming protocol |
||||
states that STK_GET_SYNC+CRC_EOP (0x30 0x20) should be sent to synchronize, but that works poorly |
||||
because the AVR's UART only buffers one character. This means that if STK_GET_SYNC+CRC_EOP is |
||||
sent twice there is a high chance that only the last character (CRC_EOP) is actually |
||||
received. If that is followed by another STK_GET_SYNC+CRC_EOP sequence then optiboot receives |
||||
CRC_EOP+STK_GET_SYNC+CRC_EOP which causes it to abort and run the old sketch. Ending up in that |
||||
situation is quite likely because optiboot initializes the UART as one of the first things, but |
||||
then goes off an flashes an LED for ~300ms during which it doesn't empty the UART. |
||||
|
||||
Looking at the optiboot code, the good news is that CRC_EOP+CRC_EOP can be used to get an initial |
||||
response without the overrun danger of the normal sync sequence and this is what esp-link does. |
||||
The programming sequence runs as follows: |
||||
|
||||
- esp-link sends a brief reset pulse (1ms) |
||||
- esp-link sends CRC_EOP+CRC_EOP ~50ms later |
||||
- esp-link sends CRC_EOP+CRC_EOP every ~70-80ms |
||||
- eventually optiboot responds with STK_INSYNC+STK_OK (0x14;0x10) |
||||
- esp-link sends one CRC_EOP to sort out the even/odd issue |
||||
- either optiboot responds with STK_INSYNC+STK_OK or nothing happens for 70-80ms, in which case |
||||
esp-link sends another CRC_EOP |
||||
- esp-link sends STK_GET_SYNC+CRC_EOP and optiboot responds with STK_INSYNC+STK_OK and we're in |
||||
sync now |
||||
- esp-link sends the next command (starts with 'u') and programming starts... |
||||
|
||||
If no sync is achieved, esp-link changes baud rate and the whole thing starts over with a reset |
||||
pulse about 600ms, esp-link gives up after about 5 seconds and reports an error. |
||||
|
||||
### Flashing an attached ARM processor |
||||
|
||||
You can reprogram NXP's LPC800-series and many other ARM processors as well by pointing your |
||||
programmer similarly at the esp-link's port 23. For example, if you are using |
||||
https://github.com/jeelabs/embello/tree/master/tools/uploader a command line like |
||||
`uploader -t -s -w esp-link:23 build/firmware.bin` does the trick. |
||||
The way it works is that the uploader uses telnet protocol escape sequences in order to |
||||
make esp-link issue the appropriate "ISP" and reset sequence to the microcontroller to start the |
||||
flash programming. If you use a different ARM programming tool it will work as well as long as |
||||
it starts the connection with the `?\r\n` synchronization sequence. |
||||
|
||||
### Flashing an attached esp8266 |
||||
|
||||
__Flashing another esp8266 module is possible in theory but real-world attempts have so far been |
||||
rather unsuccessful due to Wifi interference. This section is left here in case someone else |
||||
wants to dig in and find a solution.__ |
||||
|
||||
You can use esp-link running on one esp8266 module to flash another esp8266 module, |
||||
however it is rather tricky! The problem is not electric, it is wifi interference. |
||||
The basic idea is to use some method to direct the esp8266 flash program to port 2323 of |
||||
esp-link. Using port 2323 with the appropriate wiring will cause the esp8266's reset and |
||||
gpio0 pins to be toggled such that the chip enters the flash programming mode. |
||||
|
||||
One option for connecting the programmer with esp-link is to use my version of esptool.py |
||||
at http://github.com/tve/esptool, which supports specifying a URL instead of a port. Thus |
||||
instead of specifying something like `--port /dev/ttyUSB0` or `--port COM1` you specify |
||||
`--port socket://esp-link.local:2323`. Important: the baud rate specified on the esptool.py |
||||
command-line is irrelevant as the baud rate used by esp-link will be the one set in the |
||||
uC console page. Fortunately the esp8266 bootloader does auto-baud detection. (Setting the |
||||
baud rate to 115200 is recommended.) |
||||
|
||||
Another option is to use a serial-to-tcp port forwarding driver and point that to port 2323 |
||||
of esp-link. On windows users have reported success with |
||||
http://www.hw-group.com/products/hw_vsp/hw_vsp2_en.html[HW Virtual Serial Port] |
||||
|
||||
Now to the interference problem: once the attached esp8266 is reset it |
||||
starts outputting its 26Mhz clock on gpio0, which needs to be attached to |
||||
the esp8266 running esp-link (since it needs to drive gpio0 low during |
||||
the reset to enter flash mode). This 26Mhz signal on gpio0 causes a |
||||
significant amount of radio interference with the result that the esp8266 |
||||
running esp-link has trouble receiving Wifi packets. You can observe this |
||||
by running a ping to esp-link in another window: as soon as the target |
||||
esp8266 is reset, the pings become very slow or stop altogetehr. As soon |
||||
as you remove power to the attached esp8266 the pings resume beautifully. |
||||
|
||||
To try and get the interference under control, try some of the following: |
||||
add a series 100ohm resistor and 100pf capacitor to ground as close to |
||||
the gpio0 pin as possible (basically a low pass filter); and/or pass |
||||
the cable connecting the two esp8266's through a ferrite bead. |
||||
|
||||
### Debug log |
||||
|
||||
The esp-link web UI can display the esp-link debug log (os_printf statements in the code). This |
||||
is handy but sometimes not sufficient. Esp-link also prints the debug info to the UART where |
||||
it is sometimes more convenient and sometimes less... For this reason three UART debug log |
||||
modes are supported that can be set in the web UI (and the mode is saved in flash): |
||||
|
||||
- auto: the UART log starts enabled at boot using uart0 and disables itself when esp-link |
||||
associates with an AP. It re-enables itself if the association is lost. |
||||
- off: the UART log is always off |
||||
- on0: the UART log is always on using uart0 |
||||
- on1: the UART log is always on using uart1 (gpio2 pin) |
||||
|
||||
Note that even if the UART log is always off the ROM prints to uart0 whenever the |
||||
esp8266 comes out of reset. This cannot be disabled. |
@ -0,0 +1,69 @@ |
||||
ESP-LINK web-server tutorial |
||||
============================ |
||||
|
||||
Video |
||||
-------------------- |
||||
|
||||
https://www.youtube.com/watch?v=vBESCO0UhYI |
||||
|
||||
|
||||
Installing el-client Arduino library |
||||
-------------------- |
||||
|
||||
Download and install ELClient library. |
||||
|
||||
https://github.com/jeelabs/el-client |
||||
|
||||
|
||||
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: open webserver_led ELClient sample file in Arduino |
||||
- 2: upload the code onto an Arduino Nano/Uno |
||||
- 3: open the Web Server page on esp-link UI |
||||
- 4: upload LED.html from webserver_led ( ELCient/examples/webserver_led/LED.html ) |
||||
- 5: choose LED page on esp-link UI |
||||
- 6: turn on/off the LED |
||||
|
||||
|
||||
HTML controls 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 webserver_controls ELClient sample file in Arduino |
||||
- 2: upload the code onto an Arduino Nano/Uno |
||||
- 3: open the Web Server page on esp-link UI |
||||
- 4: upload the 3 HTML files from webserver_controls ( select multiple htmls from ELCient/examples/webserver_controls/ ) |
||||
- 5: jump to LED/User/Voltage pages |
||||
- 6: try out different settings |
||||
|
||||
|
||||
Supported HTML controls |
||||
-------------------- |
||||
|
||||
HTML control | Value Type | Description | Form Submission | |
||||
------------------------ | ----- | ------------ | ------- | |
||||
<p id="id"/> <br/> <div id="id"/> <br/> <tr id="id"/> <br/> <th id="id"/> <br/> <td id="id"/> <br/> <textarea id="id"/> | String (HTML) | MCU can replace the inner HTML part of the control at LOAD/REFRESH queries. The string (sent by MCU) is handled as HTML, so <img...> will be displayed as an image on the page | NO | |
||||
<button id="id"/> | String | When button is pressed, a message is transmitted to MCU containing the id (BUTTON_PRESS) | NO | |
||||
<input name="id"/> | String <br/> Integer <br/> Float <br/> Boolean | MCU can replace the value or checked properties of the HTML control in the form (LOAD/REFRESH). At form submission, the content of value will be transmitted to MCU (SET_FIELD). | YES | |
||||
<select name="id"/> | String | MCU can choose a value from the drop down (LOAD/REFRESH). At form submission the currently selected value will be transmitted to MCU (SET_FIELD) | YES | |
||||
<ul id="id"/> <br/> <ol id="id"/> | JSON list <br/> ["1","2","3"] | MCU can send a JSON list which is transformed to an HTML list ( <li/> ) (LOAD/REFRESH) | NO | |
||||
<table id="id"/> | JSON table <br/> [["1","2"], <br/> ["3","4"]] | MCU sends a JSON table which is transformed to an HTML table (LOAD/REFRESH) | NO | |
||||
|
@ -0,0 +1,87 @@ |
||||
Esp-link Wifi configuration |
||||
=========================== |
||||
|
||||
For proper operation the end state that esp-link needs to arrive at is to have it |
||||
join your pre-existing wifi network as a pure station. |
||||
However, in order to get there esp-link will start out as an access point and you'll have |
||||
to join its network to configure it. The short version is: |
||||
|
||||
1. esp-link creates a wifi access point with an SSID of the form `ESP_012ABC` (some modules |
||||
use a different SSID form, such as `ai-thinker-012ABC`) |
||||
2. you join your laptop or phone to esp-link's network as a station and you configure |
||||
esp-link wifi with your network info by pointing your browser at `http://192.168.4.1/` |
||||
3. you set a hostname for esp-link on the "home" page, or leave the default ("esp-link") |
||||
4. esp-link starts to connect to your network while continuing to also be an access point |
||||
("AP+STA"), the esp-link may show up with a `${hostname}.local` hostname |
||||
(depends on your DHCP/DNS config) |
||||
4. esp-link succeeds in connecting and shuts down its own access point after 15 seconds, |
||||
you reconnect your laptop/phone to your normal network and access esp-link via its hostname |
||||
or IP address |
||||
|
||||
### Notes on using AP (access point) mode |
||||
|
||||
Esp-link does not support STA+AP mode, however it does support STA mode and AP mode. What happens |
||||
is that STA+AP mode is used at boot and when making STA changes to allow for recovery: the AP |
||||
mode stays on for a while so you can connect to it and fix the STA mode. Once STA has connected, |
||||
esp-link switches to STA-only mode. There is no setting to stay in STA+AP mode. So... if you want |
||||
to use AP ensure you set esp-link to AP-only mode. If you want STA+AP mode you're gonna have to |
||||
modify the source for yourself. (This stuff is painful to test and rather tricky, so don't expect |
||||
the way it works to change.) |
||||
|
||||
Configuration details |
||||
--------------------- |
||||
|
||||
### Wifi |
||||
|
||||
After you have serially flashed the module it will create a wifi access point (AP) with an |
||||
SSID of the form `ESP_012ABC` where 012ABC is a piece of the module's MAC address. |
||||
Using a laptop, phone, or tablet connect to this SSID and then open a browser pointed at |
||||
http://192.168.4.1/, you should then see the esp-link web site. |
||||
|
||||
Now configure the wifi. The desired configuration is for the esp-link to be a |
||||
station on your local wifi network so you can communicate with it from all your computers. |
||||
|
||||
To make this happen, navigate to the wifi page and you should see the esp-link scan |
||||
for available networks. You should then see a list of detected networks on the web page and you |
||||
can select yours. |
||||
Enter a password if your network is secure (highly recommended...) and hit the connect button. |
||||
|
||||
You should now see that the esp-link has connected to your network and it should show you |
||||
its IP address. _Write it down_. You will then have to switch your laptop, phone, or tablet |
||||
back to your network and then you can connect to the esp-link's IP address or, depending on your |
||||
network's DHCP/DNS config you may be able to go to http://esp-link.local |
||||
|
||||
At this point the esp-link will have switched to STA mode and be just a station on your |
||||
wifi network. These settings are stored in flash and thereby remembered through resets and |
||||
power cycles. They are also remembered when you flash new firmware. Only flashing `blank.bin` |
||||
via the serial port as indicated above will reset the wifi settings. |
||||
|
||||
There is a fail-safe, which is that after a reset or a configuration change, if the esp-link |
||||
cannot connect to your network it will revert back to AP+STA mode after 15 seconds and thus |
||||
both present its `ESP_012ABC`-style network and continue trying to reconnect to the requested network. |
||||
You can then connect to the esp-link's AP and reconfigure the station part. |
||||
|
||||
One open issue (#28) is that esp-link cannot always display the IP address it is getting to the browser |
||||
used to configure the ssid/password info. The problem is that the initial STA+AP mode may use |
||||
channel 1 and you configure it to connect to an AP on channel 6. This requires the ESP8266's AP |
||||
to also switch to channel 6 disconnecting you in the meantime. |
||||
|
||||
### Hostname, description, DHCP, mDNS |
||||
|
||||
You can set a hostname on the "home" page, this should be just the hostname and not a domain |
||||
name, i.e., something like "test-module-1" and not "test-module-1.mydomain.com". |
||||
This has a number of effects: |
||||
|
||||
- you will see the first 12 chars of the hostname in the menu bar (top left of the page) so |
||||
if you have multiple modules you can distinguish them visually |
||||
- esp-link will use the hostname in its DHCP request, which allows you to identify the module's |
||||
MAC and IP addresses in your DHCP server (typ. your wifi router). In addition, some DHCP |
||||
servers will inject these names into the local DNS cache so you can use URLs like |
||||
`hostname.local`. |
||||
- someday, esp-link will inject the hostname into mDNS (multicast DNS, bonjour, etc...) so |
||||
URLs of the form `hostname.local` work for everyone (as of v2.1.beta5 mDNS is disabled due |
||||
to reliability issues with it) |
||||
|
||||
You can also enter a description of up to 128 characters on the home page (bottom right). This |
||||
allows you to leave a memo for yourself, such as "installed in basement to control the heating |
||||
system". This descritpion is not used anywhere else. |
@ -0,0 +1,189 @@ |
||||
// 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 header_position = 0; // flash offset of the file header
|
||||
int upload_position = 0; // flash offset where to store page upload
|
||||
int html_header_len = 0; // length of the HTML header added to the file
|
||||
|
||||
// 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> "; |
||||
|
||||
// this method is for flash writing and erasing the page
|
||||
// write is incremental, so whenever a page border is reached, the next page will be erased
|
||||
int ICACHE_FLASH_ATTR webServerSetupWriteFlash( int addr, void * data, int length ) |
||||
{ |
||||
int end_addr = addr + length; |
||||
if( end_addr >= getUserPageSectionEnd() ) |
||||
{ |
||||
os_printf("No more space in the flash!\n"); |
||||
return 1; |
||||
} |
||||
|
||||
void * free_ptr = 0; |
||||
if(( length & 3 ) != 0 ) // ESP8266 always writes 4 bytes, so the remaining ones should be oxFF-ed out
|
||||
{ |
||||
free_ptr = os_malloc(length + 4); |
||||
os_memset(free_ptr, 0xFF, length + 4); |
||||
os_memcpy(free_ptr, data, length); |
||||
data = free_ptr; |
||||
} |
||||
|
||||
int ptr = 0; |
||||
while( addr < end_addr ) |
||||
{ |
||||
if (addr % SPI_FLASH_SEC_SIZE == 0){ |
||||
spi_flash_erase_sector(addr/SPI_FLASH_SEC_SIZE); |
||||
} |
||||
|
||||
int max = (addr | (SPI_FLASH_SEC_SIZE - 1)) + 1; |
||||
int len = end_addr - addr; |
||||
if( end_addr > max ) |
||||
len = max - addr; |
||||
|
||||
spi_flash_write( addr, (uint32_t *)((char *)data + ptr), len ); |
||||
ptr += len; |
||||
addr += len; |
||||
} |
||||
if( free_ptr != 0 ) |
||||
os_free(free_ptr); |
||||
return 0; |
||||
} |
||||
|
||||
// debug code
|
||||
void ICACHE_FLASH_ATTR dumpFlash( int end ) |
||||
{ |
||||
int dump = getUserPageSectionStart(); |
||||
while( dump < end ) |
||||
{ |
||||
char buffer[16]; |
||||
spi_flash_read(dump, (uint32_t *)buffer, sizeof(buffer)); |
||||
char dmpstr[sizeof(buffer)*3]; |
||||
os_sprintf(dmpstr, "%06X: ", dump); |
||||
for(int i=0; i < sizeof(buffer); i++ ) |
||||
os_sprintf(dmpstr + os_strlen(dmpstr), "%02X ", buffer[i]); |
||||
os_printf("%s\n", dmpstr); |
||||
dump += sizeof(buffer); |
||||
} |
||||
} |
||||
|
||||
// multipart callback for uploading user defined pages
|
||||
int ICACHE_FLASH_ATTR webServerSetupMultipartCallback(MultipartCmd cmd, char *data, int dataLen, int position) |
||||
{ |
||||
switch(cmd) |
||||
{ |
||||
case FILE_UPLOAD_START: |
||||
upload_position = getUserPageSectionStart(); |
||||
header_position = upload_position; |
||||
break; |
||||
case FILE_START: |
||||
{ |
||||
html_header_len = 0; |
||||
|
||||
// write the starting block on esp-fs
|
||||
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; |
||||
|
||||
header_position = upload_position; |
||||
if( webServerSetupWriteFlash( upload_position, (uint32_t *)(&hdr), sizeof(EspFsHeader) ) ) |
||||
return 1; |
||||
upload_position += sizeof(EspFsHeader); |
||||
|
||||
char nameBuf[len]; |
||||
os_memset(nameBuf, 0, len); |
||||
os_memcpy(nameBuf, data, dataLen); |
||||
|
||||
if( webServerSetupWriteFlash( upload_position, (uint32_t *)(nameBuf), len ) ) |
||||
return 1; |
||||
upload_position += len; |
||||
|
||||
// add header to HTML files
|
||||
if( ( dataLen > 5 ) && ( os_strcmp(data + dataLen - 5, ".html") == 0 ) ) // if the file ends with .html, wrap into an espfs image
|
||||
{ |
||||
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); |
||||
if( webServerSetupWriteFlash( upload_position, (uint32_t *)(buf), html_header_len ) ) |
||||
return 1; |
||||
upload_position += html_header_len; |
||||
} |
||||
} |
||||
break; |
||||
case FILE_DATA: |
||||
if( webServerSetupWriteFlash( upload_position, data, dataLen ) ) |
||||
return 1; |
||||
upload_position += dataLen; |
||||
break; |
||||
case FILE_DONE: |
||||
{ |
||||
// write padding after the file
|
||||
uint8_t pad_cnt = (4 - position) & 3; |
||||
if( pad_cnt ) { |
||||
uint32_t pad = 0; |
||||
if( webServerSetupWriteFlash( upload_position, &pad, pad_cnt ) ) |
||||
return 1; |
||||
upload_position += pad_cnt; |
||||
} |
||||
|
||||
EspFsHeader hdr; |
||||
hdr.magic = ESPFS_MAGIC;
|
||||
hdr.fileLenComp = hdr.fileLenDecomp = position + html_header_len; |
||||
|
||||
// restore ESPFS magic
|
||||
spi_flash_write( header_position + ((char *)&hdr.magic - (char*)&hdr), (uint32_t *)&hdr.magic, sizeof(uint32_t) ); |
||||
// set file size
|
||||
spi_flash_write( header_position + ((char *)&hdr.fileLenComp - (char*)&hdr), (uint32_t *)&hdr.fileLenComp, sizeof(uint32_t) ); |
||||
spi_flash_write( header_position + ((char *)&hdr.fileLenDecomp - (char*)&hdr), (uint32_t *)&hdr.fileLenDecomp, sizeof(uint32_t) ); |
||||
} |
||||
break; |
||||
case FILE_UPLOAD_DONE: |
||||
{ |
||||
// write the termination block
|
||||
|
||||
EspFsHeader hdr; |
||||
hdr.magic = ESPFS_MAGIC;
|
||||
hdr.flags = 1; |
||||
hdr.compression = 0; |
||||
hdr.nameLen = 0; |
||||
hdr.fileLenComp = hdr.fileLenDecomp = 0; |
||||
|
||||
if( webServerSetupWriteFlash( upload_position, (uint32_t *)(&hdr), sizeof(EspFsHeader) ) ) |
||||
return 1; |
||||
upload_position += sizeof(EspFsHeader); |
||||
|
||||
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 |
||||
#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 |
@ -0,0 +1,43 @@ |
||||
<div id="main"> |
||||
<div class="header"> |
||||
<h1>Upgrade Firmware</h1> |
||||
</div> |
||||
|
||||
<div class="content"> |
||||
<div class="pure-g"> |
||||
<div class="pure-u-1 pure-u-md-1-2"> |
||||
<div class="card"> |
||||
<h1>Upgrade Firmware |
||||
<div id="fw-spinner" class="spinner spinner-small"></div> |
||||
</h1> |
||||
<form action="#" id="fw-form" class="pure-form" hidden> |
||||
<legend>Firmware Info</legend> |
||||
<p> |
||||
Current firmware: <span style="font-weight: bold;" id="current-fw"></span> |
||||
</p> |
||||
<div class="pure-form-stacked"> |
||||
<p> |
||||
Make sure you upload the file called: <span style="font-weight: bold;" id="fw-slot"></span> |
||||
</p> |
||||
<label>Firmware File</label> |
||||
<input type="file" name="fw-file" id="fw-file"/> |
||||
</div> |
||||
<button id="fw-button" type="submit" class="pure-button button-primary"> |
||||
Update the firmware |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script src="flash.js"></script> |
||||
<script type="text/javascript"> |
||||
onLoad(function() { |
||||
fetchFlash(); |
||||
bnd($("#fw-form"), "submit", flashFirmware); |
||||
}); |
||||
</script> |
||||
</body></html> |
@ -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; } |
||||
} |
||||
); |
||||
} |
@ -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" multiple> |
||||
<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,307 @@ |
||||
#include <esp8266.h> |
||||
#include <osapi.h> |
||||
|
||||
#include "multipart.h" |
||||
#include "cgi.h" |
||||
|
||||
#define BOUNDARY_SIZE 128 |
||||
|
||||
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->callBack( FILE_UPLOAD_START, NULL, 0, context->position ) ) // start uploading files
|
||||
context->state = STATE_ERROR; |
||||
} |
||||
|
||||
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; |
||||
else if( context->callBack( FILE_UPLOAD_DONE, NULL, 0, context->position ) ) // done with files
|
||||
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,34 @@ |
||||
#ifndef MULTIPART_H |
||||
#define MULTIPART_H |
||||
|
||||
#include <httpd.h> |
||||
|
||||
typedef enum { |
||||
FILE_UPLOAD_START, // multipart: uploading files started
|
||||
FILE_START, // multipart: the start of a new file (can be more)
|
||||
FILE_DATA, // multipart: file data
|
||||
FILE_DONE, // multipart: file end
|
||||
FILE_UPLOAD_DONE, // multipart: finished for all files
|
||||
} 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,432 @@ |
||||
// Copyright 2016 by BeeGee, see LICENSE.txt
|
||||
//
|
||||
// Adapted from: github.com/tuanpmt/esp_bridge, Created on: Mar 4, 2015, Author: Minh
|
||||
// Adapted from: rest.c, Author: Thorsten von Eicken
|
||||
|
||||
#include "esp8266.h" |
||||
#include "c_types.h" |
||||
#include "ip_addr.h" |
||||
#include "socket.h" |
||||
#include "cmd.h" |
||||
|
||||
#define SOCK_DBG |
||||
|
||||
#ifdef SOCK_DBG |
||||
#define DBG_SOCK(format, ...) os_printf(format, ## __VA_ARGS__) |
||||
#else |
||||
#define DBG_SOCK(format, ...) do { } while(0) |
||||
#endif |
||||
|
||||
typedef struct { |
||||
char *host; |
||||
uint32_t port; |
||||
ip_addr_t ip; |
||||
struct espconn *pCon; |
||||
char *data; |
||||
uint16_t data_len; |
||||
uint16_t data_sent; |
||||
uint32_t resp_cb; |
||||
uint8_t conn_num; |
||||
uint8_t sock_mode; |
||||
} SocketClient; |
||||
|
||||
|
||||
// Connection pool for TCP/UDP socket clients/servers. Attached MCU's just call SOCKET_setup and this allocates
|
||||
// a connection, They never call any 'free' and given that the attached MCU could restart at
|
||||
// any time, we cannot really rely on the attached MCU to call 'free' ever, so better do without.
|
||||
// Instead, we allocate a fixed pool of connections an round-robin. What this means is that the
|
||||
// attached MCU should really use at most as many SOCKET connections as there are slots in the pool.
|
||||
#define MAX_SOCKET 4 |
||||
static SocketClient socketClient[MAX_SOCKET]; |
||||
static uint8_t socketNum = 0xff; // index into socketClient for next slot to allocate
|
||||
|
||||
// Any incoming data?
|
||||
static void ICACHE_FLASH_ATTR |
||||
socketclient_recv_cb(void *arg, char *pusrdata, unsigned short length) { |
||||
struct espconn *pCon = (struct espconn *)arg; |
||||
SocketClient* client = (SocketClient *)pCon->reverse; |
||||
|
||||
uint8_t clientNum = client->conn_num; |
||||
uint8_t cb_type = USERCB_RECV; |
||||
DBG_SOCK("SOCKET #%d: Received %d bytes: %s\n", client-socketClient, length, pusrdata); |
||||
cmdResponseStart(CMD_RESP_CB, client->resp_cb, 4); |
||||
cmdResponseBody(&cb_type, 1);
|
||||
cmdResponseBody(&clientNum, 1); |
||||
cmdResponseBody(&length, 2); |
||||
cmdResponseBody(pusrdata, length); |
||||
cmdResponseEnd(); |
||||
|
||||
if (client->sock_mode != SOCKET_TCP_SERVER) { // We don't wait for a response
|
||||
DBG_SOCK("SOCKET #%d: disconnect after receiving\n", client-socketClient); |
||||
espconn_disconnect(client->pCon); // disconnect from the server
|
||||
} |
||||
} |
||||
|
||||
// Data is sent
|
||||
static void ICACHE_FLASH_ATTR |
||||
socketclient_sent_cb(void *arg) { |
||||
struct espconn *pCon = (struct espconn *)arg; |
||||
SocketClient* client = (SocketClient *)pCon->reverse; |
||||
|
||||
uint8_t clientNum = client->conn_num; |
||||
uint8_t cb_type = USERCB_SENT; |
||||
DBG_SOCK("SOCKET #%d: Sent\n", client-socketClient); |
||||
sint16 sentDataLen = client->data_sent; |
||||
if (client->data_sent != client->data_len)
|
||||
{ |
||||
// we only sent part of the buffer, send the rest
|
||||
uint16_t data_left = client->data_len - client->data_sent; |
||||
if (data_left > 1400) // we have more than 1400 bytes left
|
||||
{
|
||||
data_left = 1400; |
||||
espconn_sent(client->pCon, (uint8_t*)(client->data+client->data_sent), 1400 ); |
||||
} |
||||
espconn_sent(client->pCon, (uint8_t*)(client->data+client->data_sent), data_left ); |
||||
client->data_sent += data_left; |
||||
} |
||||
else |
||||
{ |
||||
// we're done sending, free the memory
|
||||
if (client->data) os_free(client->data); |
||||
client->data = 0; |
||||
|
||||
if (client->sock_mode == SOCKET_TCP_CLIENT) { // We don't wait for a response
|
||||
DBG_SOCK("SOCKET #%d: disconnect after sending\n", clientNum); |
||||
espconn_disconnect(client->pCon); |
||||
} |
||||
|
||||
cmdResponseStart(CMD_RESP_CB, client->resp_cb, 3); |
||||
cmdResponseBody(&cb_type, 1);
|
||||
cmdResponseBody(&clientNum, 1); |
||||
cmdResponseBody(&sentDataLen, 2); |
||||
cmdResponseEnd(); |
||||
} |
||||
} |
||||
|
||||
// Connection is disconnected
|
||||
static void ICACHE_FLASH_ATTR |
||||
socketclient_discon_cb(void *arg) { |
||||
struct espconn *pespconn = (struct espconn *)arg; |
||||
SocketClient* client = (SocketClient *)pespconn->reverse; |
||||
|
||||
uint8_t clientNum = client->conn_num; |
||||
uint8_t cb_type = USERCB_CONN; |
||||
sint16 _status = CONNSTAT_DIS; |
||||
DBG_SOCK("SOCKET #%d: Disconnect\n", clientNum); |
||||
// free the data buffer, if we have one
|
||||
if (client->data) os_free(client->data); |
||||
client->data = 0; |
||||
cmdResponseStart(CMD_RESP_CB, client->resp_cb, 3); |
||||
cmdResponseBody(&cb_type, 1);
|
||||
cmdResponseBody(&clientNum, 1); |
||||
cmdResponseBody(&_status, 2); |
||||
cmdResponseEnd(); |
||||
} |
||||
|
||||
// Connection was reset
|
||||
static void ICACHE_FLASH_ATTR |
||||
socketclient_recon_cb(void *arg, sint8 errType) { |
||||
struct espconn *pCon = (struct espconn *)arg; |
||||
SocketClient* client = (SocketClient *)pCon->reverse; |
||||
|
||||
uint8_t clientNum = client->conn_num; |
||||
uint8_t cb_type = USERCB_RECO; |
||||
sint16 _errType = errType; |
||||
os_printf("SOCKET #%d: conn reset, err=%d\n", clientNum, _errType); |
||||
cmdResponseStart(CMD_RESP_CB, client->resp_cb, 3); |
||||
cmdResponseBody(&cb_type, 1);
|
||||
cmdResponseBody(&clientNum, 1); |
||||
cmdResponseBody(&_errType, 2); |
||||
cmdResponseEnd(); |
||||
// free the data buffer, if we have one
|
||||
if (client->data) os_free(client->data); |
||||
client->data = 0; |
||||
} |
||||
|
||||
// Connection is done
|
||||
static void ICACHE_FLASH_ATTR |
||||
socketclient_connect_cb(void *arg) { |
||||
struct espconn *pCon = (struct espconn *)arg; |
||||
SocketClient* client = (SocketClient *)pCon->reverse; |
||||
|
||||
uint8_t clientNum = client->conn_num; |
||||
uint8_t cb_type = USERCB_CONN; |
||||
sint16 _status = CONNSTAT_CON; |
||||
DBG_SOCK("SOCKET #%d: connected socket mode = %d\n", clientNum, client->sock_mode); |
||||
espconn_regist_disconcb(client->pCon, socketclient_discon_cb); |
||||
espconn_regist_recvcb(client->pCon, socketclient_recv_cb); |
||||
espconn_regist_sentcb(client->pCon, socketclient_sent_cb); |
||||
|
||||
DBG_SOCK("SOCKET #%d: sending %d\n", clientNum, client->data_sent); |
||||
if (client->sock_mode != SOCKET_TCP_SERVER) { // Send data after established connection only in client mode
|
||||
client->data_sent = client->data_len <= 1400 ? client->data_len : 1400; |
||||
DBG_SOCK("SOCKET #%d: sending %d\n", clientNum, client->data_sent); |
||||
espconn_send(client->pCon, (uint8_t*)client->data, client->data_sent); |
||||
} |
||||
|
||||
cmdResponseStart(CMD_RESP_CB, client->resp_cb, 3); |
||||
cmdResponseBody(&cb_type, 1);
|
||||
cmdResponseBody(&clientNum, 1); |
||||
cmdResponseBody(&_status, 2); |
||||
cmdResponseEnd(); |
||||
} |
||||
|
||||
static void ICACHE_FLASH_ATTR |
||||
socket_dns_found(const char *name, ip_addr_t *ipaddr, void *arg) { |
||||
struct espconn *pConn = (struct espconn *)arg; |
||||
SocketClient* client = (SocketClient *)pConn->reverse; |
||||
uint8_t clientNum = client->conn_num; |
||||
|
||||
if(ipaddr == NULL) { |
||||
sint16 _errType = ESPCONN_RTE; //-4;
|
||||
uint8_t cb_type = USERCB_RECO; // use Routing problem or define a new one
|
||||
os_printf("SOCKET #%d DNS: Got no ip, report error\n", clientNum); |
||||
cmdResponseStart(CMD_RESP_CB, client->resp_cb, 3);
|
||||
cmdResponseBody(&cb_type, 2); // Same as connection reset?? or define a new one
|
||||
cmdResponseBody(&clientNum, 1);
|
||||
cmdResponseBody(&_errType, 2);
|
||||
cmdResponseEnd();
|
||||
return; |
||||
} |
||||
DBG_SOCK("SOCKET #%d DNS: found ip %d.%d.%d.%d\n", |
||||
clientNum, |
||||
*((uint8 *) &ipaddr->addr), |
||||
*((uint8 *) &ipaddr->addr + 1), |
||||
*((uint8 *) &ipaddr->addr + 2), |
||||
*((uint8 *) &ipaddr->addr + 3)); |
||||
if(client->ip.addr == 0 && ipaddr->addr != 0) { |
||||
os_memcpy(client->pCon->proto.tcp->remote_ip, &ipaddr->addr, 4); |
||||
espconn_connect(client->pCon); |
||||
DBG_SOCK("SOCKET #%d: connecting...\n", clientNum); |
||||
} |
||||
} |
||||
|
||||
void ICACHE_FLASH_ATTR |
||||
SOCKET_Setup(CmdPacket *cmd) { |
||||
CmdRequest req; |
||||
uint16_t port; |
||||
uint8_t sock_mode; |
||||
int32_t err = -1; // error code in case of failure
|
||||
|
||||
// start parsing the command
|
||||
cmdRequest(&req, cmd); |
||||
if(cmdGetArgc(&req) != 3) { |
||||
DBG_SOCK("SOCKET Setup parse command failure: (cmdGetArgc(&req) != 3)\n"); |
||||
goto fail; |
||||
} |
||||
err--; |
||||
|
||||
// get the hostname (IP address)
|
||||
uint16_t len = cmdArgLen(&req); |
||||
if (len > 128) { |
||||
DBG_SOCK("SOCKET Setup parse command failure: hostname longer than 128 characters\n"); |
||||
goto fail; // safety check
|
||||
} |
||||
err--; |
||||
uint8_t *socket_host = (uint8_t*)os_zalloc(len + 1); |
||||
if (socket_host == NULL) { |
||||
DBG_SOCK("SOCKET Setup failed to alloc memory for socket_host\n"); |
||||
goto fail; |
||||
} |
||||
if (cmdPopArg(&req, socket_host, len)) { |
||||
DBG_SOCK("SOCKET Setup parse command failure: (cmdPopArg(&req, socket_host, len))\n"); |
||||
goto fail; |
||||
} |
||||
err--; |
||||
socket_host[len] = 0; |
||||
|
||||
// get the port
|
||||
if (cmdPopArg(&req, (uint8_t*)&port, 2)) { |
||||
DBG_SOCK("SOCKET Setup parse command failure: cannot get port\n"); |
||||
os_free(socket_host); |
||||
goto fail; |
||||
} |
||||
err--; |
||||
|
||||
// get the socket mode
|
||||
if (cmdPopArg(&req, (uint8_t*)&sock_mode, 1)) { |
||||
DBG_SOCK("SOCKET Setup parse command failure: cannot get mode\n"); |
||||
os_free(socket_host); |
||||
goto fail; |
||||
} |
||||
err--; |
||||
DBG_SOCK("SOCKET Setup listener flag\n"); |
||||
|
||||
// clear connection structures the first time
|
||||
if (socketNum == 0xff) { |
||||
os_memset(socketClient, 0, MAX_SOCKET * sizeof(SocketClient)); |
||||
socketNum = 0; |
||||
} |
||||
|
||||
// allocate a connection structure
|
||||
SocketClient *client = socketClient + socketNum; |
||||
uint8_t clientNum = socketNum; |
||||
socketNum = (socketNum+1)%MAX_SOCKET; |
||||
|
||||
// free any data structure that may be left from a previous connection
|
||||
if (client->data) os_free(client->data); |
||||
if (client->pCon) { |
||||
if (sock_mode != SOCKET_UDP) { |
||||
if (client->pCon->proto.tcp) os_free(client->pCon->proto.tcp); |
||||
} else { |
||||
if (client->pCon->proto.udp) os_free(client->pCon->proto.udp); |
||||
} |
||||
os_free(client->pCon); |
||||
} |
||||
os_memset(client, 0, sizeof(SocketClient)); |
||||
DBG_SOCK("SOCKET #%d: Setup host=%s port=%d \n", clientNum, socket_host, port); |
||||
|
||||
client->sock_mode = sock_mode; |
||||
client->resp_cb = cmd->value; |
||||
client->conn_num = clientNum; |
||||
|
||||
client->host = (char *)socket_host; |
||||
client->port = port; |
||||
|
||||
if (sock_mode == SOCKET_UDP) { |
||||
wifi_set_broadcast_if(STATIONAP_MODE); |
||||
} |
||||
|
||||
client->pCon = (struct espconn *)os_zalloc(sizeof(struct espconn)); |
||||
if (client->pCon == NULL) { |
||||
DBG_SOCK("SOCKET #%d: Setup failed to alloc memory for client_pCon\n", clientNum); |
||||
goto fail; |
||||
} |
||||
|
||||
if (sock_mode != SOCKET_UDP) { |
||||
client->pCon->type = ESPCONN_TCP; |
||||
client->pCon->proto.tcp = (esp_tcp *)os_zalloc(sizeof(esp_tcp)); |
||||
if (client->pCon->proto.tcp == NULL) { |
||||
DBG_SOCK("SOCKET #%d: Setup failed to alloc memory for client->pCon->proto.tcp\n", clientNum); |
||||
goto fail; |
||||
} |
||||
} else { |
||||
client->pCon->type = ESPCONN_UDP; |
||||
client->pCon->proto.udp = (esp_udp *)os_zalloc(sizeof(esp_udp)); |
||||
if (client->pCon->proto.udp == NULL) { |
||||
DBG_SOCK("SOCKET #%d: Setup failed to alloc memory for client->pCon->proto.udp\n", clientNum); |
||||
goto fail; |
||||
} |
||||
} |
||||
client->pCon->state = ESPCONN_NONE; |
||||
|
||||
os_memcpy(client->host, socket_host, 4); |
||||
if (sock_mode != SOCKET_UDP) { |
||||
client->pCon->proto.tcp->remote_port = client->port; |
||||
client->pCon->proto.tcp->local_port = client->port; // espconn_port();
|
||||
} else { |
||||
client->pCon->proto.udp->remote_port = client->port; |
||||
client->pCon->proto.udp->local_port = client->port; |
||||
} |
||||
|
||||
client->pCon->reverse = client; |
||||
|
||||
espconn_regist_sentcb(client->pCon, socketclient_sent_cb); |
||||
espconn_regist_recvcb(client->pCon, socketclient_recv_cb); |
||||
if (sock_mode == SOCKET_UDP) { |
||||
DBG_SOCK("SOCKET #%d: Create connection to ip %s:%d\n", clientNum, client->host, client->port); |
||||
|
||||
if(UTILS_StrToIP((char *)client->host, &client->pCon->proto.udp->remote_ip)) { |
||||
espconn_create(client->pCon); |
||||
} else { |
||||
DBG_SOCK("SOCKET #%d: failed to copy remote_ip to &client->pCon->proto.udp->remote_ip\n", clientNum); |
||||
goto fail; |
||||
} |
||||
} else { |
||||
espconn_regist_reconcb(client->pCon, socketclient_recon_cb); |
||||
if (client->sock_mode == SOCKET_TCP_SERVER) { // Server mode?
|
||||
DBG_SOCK("SOCKET #%d: Enable server mode on port%d\n", clientNum, client->port); |
||||
espconn_accept(client->pCon); |
||||
espconn_regist_connectcb(client->pCon, socketclient_connect_cb); |
||||
} |
||||
} |
||||
|
||||
cmdResponseStart(CMD_RESP_V, clientNum, 0); |
||||
cmdResponseEnd(); |
||||
DBG_SOCK("SOCKET #%d: setup finished\n", clientNum); |
||||
return; |
||||
|
||||
fail: |
||||
cmdResponseStart(CMD_RESP_V, err, 0); |
||||
cmdResponseEnd(); |
||||
return; |
||||
} |
||||
|
||||
void ICACHE_FLASH_ATTR |
||||
SOCKET_Send(CmdPacket *cmd) { |
||||
CmdRequest req; |
||||
cmdRequest(&req, cmd); |
||||
|
||||
// Get client
|
||||
uint32_t clientNum = cmd->value; |
||||
SocketClient *client = socketClient + (clientNum % MAX_SOCKET); |
||||
DBG_SOCK("SOCKET #%d: send", clientNum); |
||||
|
||||
if (cmd->argc != 1 && cmd->argc != 2) { |
||||
DBG_SOCK("\nSOCKET #%d: send - wrong number of arguments\n", clientNum); |
||||
return; |
||||
} |
||||
|
||||
// Get data to sent
|
||||
client->data_len = cmdArgLen(&req); |
||||
DBG_SOCK(" dataLen=%d", client->data_len); |
||||
|
||||
if (client->data) os_free(client->data); |
||||
client->data = (char*)os_zalloc(client->data_len); |
||||
if (client->data == NULL) { |
||||
DBG_SOCK("\nSOCKET #%d failed to alloc memory for client->data\n", clientNum); |
||||
goto fail; |
||||
} |
||||
cmdPopArg(&req, client->data, client->data_len); |
||||
DBG_SOCK(" socketData=%s", client->data); |
||||
|
||||
// client->data_len = os_sprintf((char*)client->data, socketDataSet, socketData);
|
||||
|
||||
DBG_SOCK("\n"); |
||||
|
||||
DBG_SOCK("SOCKET #%d: Create connection to ip %s:%d\n", clientNum, client->host, client->port); |
||||
|
||||
if (client->sock_mode == SOCKET_TCP_SERVER) { // In TCP server mode we should be connected already and send the data immediately
|
||||
remot_info *premot = NULL; |
||||
if (espconn_get_connection_info(client->pCon,&premot,0) == ESPCONN_OK){ |
||||
for (uint8 count = 0; count < client->pCon->link_cnt; count ++){ |
||||
client->pCon->proto.tcp->remote_port = premot[count].remote_port; |
||||
|
||||
client->pCon->proto.tcp->remote_ip[0] = premot[count].remote_ip[0]; |
||||
client->pCon->proto.tcp->remote_ip[1] = premot[count].remote_ip[1]; |
||||
client->pCon->proto.tcp->remote_ip[2] = premot[count].remote_ip[2]; |
||||
client->pCon->proto.tcp->remote_ip[3] = premot[count].remote_ip[3]; |
||||
DBG_SOCK("SOCKET #%d: connected to %d.%d.%d.%d:%d\n",
|
||||
clientNum, |
||||
client->pCon->proto.tcp->remote_ip[0], |
||||
client->pCon->proto.tcp->remote_ip[1], |
||||
client->pCon->proto.tcp->remote_ip[2], |
||||
client->pCon->proto.tcp->remote_ip[3], |
||||
client->pCon->proto.tcp->remote_port |
||||
); |
||||
} |
||||
client->data_sent = client->data_len <= 1400 ? client->data_len : 1400; |
||||
DBG_SOCK("SOCKET #%d: Server sending %d\n", clientNum, client->data_sent); |
||||
espconn_send(client->pCon, (uint8_t*)client->data, client->data_sent); |
||||
} |
||||
} else if (client->sock_mode != SOCKET_UDP) { // In TCP client mode we connect and send the data from the connected callback
|
||||
espconn_regist_connectcb(client->pCon, socketclient_connect_cb); |
||||
|
||||
if(UTILS_StrToIP((char *)client->host, &client->pCon->proto.tcp->remote_ip)) { |
||||
DBG_SOCK("SOCKET #%d: Connect to ip %s:%d\n", clientNum, client->host, client->port); |
||||
espconn_connect(client->pCon); |
||||
} else { |
||||
DBG_SOCK("SOCKET #%d: Connect to host %s:%d\n", clientNum, client->host, client->port); |
||||
espconn_gethostbyname(client->pCon, (char *)client->host, &client->ip, socket_dns_found); |
||||
}
|
||||
} else { // in UDP socket mode we send the data immediately
|
||||
client->data_sent = client->data_len <= 1400 ? client->data_len : 1400; |
||||
DBG_SOCK("SOCKET #%d: sending %d bytes: %s\n", clientNum, client->data_sent, client->data); |
||||
espconn_sent(client->pCon, (uint8_t*)client->data, client->data_sent); |
||||
} |
||||
|
||||
return; |
||||
|
||||
fail: |
||||
DBG_SOCK("\n"); |
||||
} |
@ -0,0 +1,38 @@ |
||||
/*
|
||||
* socket.h |
||||
* |
||||
* Created on: Sep 16th 2016 |
||||
* Author: BeeGee |
||||
*/ |
||||
|
||||
#ifndef MODULES_SOCKET_H_ |
||||
#define MODULES_SOCKET_H_ |
||||
|
||||
#include "cmd.h" |
||||
|
||||
void SOCKET_Setup(CmdPacket *cmd); |
||||
void SOCKET_Send(CmdPacket *cmd); |
||||
|
||||
// Socket mode
|
||||
typedef enum { |
||||
SOCKET_TCP_CLIENT = 0, /**< TCP socket client for sending only, doesn't wait for response from server */ |
||||
SOCKET_TCP_CLIENT_LISTEN, /**< TCP socket client, waits for response from server after sending */ |
||||
SOCKET_TCP_SERVER, /**< TCP socket server */ |
||||
SOCKET_UDP, /**< UDP socket for sending and receiving UDP packets */ |
||||
} socketMode; |
||||
|
||||
// Callback type
|
||||
typedef enum { |
||||
USERCB_SENT = 0, /**< Data send finished */ |
||||
USERCB_RECV, /**< Data received */ |
||||
USERCB_RECO, /**< Connection error */ |
||||
USERCB_CONN, /**< Connection event */ |
||||
} cbType; |
||||
|
||||
// Connection status
|
||||
typedef enum { |
||||
CONNSTAT_DIS = 0, // Disconnected
|
||||
CONNSTAT_CON, // Connected
|
||||
} connStat; |
||||
|
||||
#endif /* MODULES_SOCKET_H_ */ |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,489 @@ |
||||
#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 MAX_ARGUMENT_BUFFER_SIZE 128 |
||||
#define HEADER_SIZE 32 |
||||
|
||||
uint32_t web_server_cb = 0; |
||||
|
||||
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 ICACHE_FLASH_ATTR WEB_argInit(struct ArgumentBuffer * argBuffer) |
||||
{ |
||||
argBuffer->numberOfArgs = 0; |
||||
argBuffer->argBufferPtr = 0; |
||||
} |
||||
|
||||
// adds an argument to the argument buffer (returns 0 if successful)
|
||||
static int ICACHE_FLASH_ATTR 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 ICACHE_FLASH_ATTR WEB_sendArgBuffer(struct ArgumentBuffer * argBuffer, HttpdConnData *connData, RequestReason reason) |
||||
{ |
||||
cmdResponseStart(CMD_RESP_CB, web_server_cb, 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; |
||||
} |
||||
|
||||
if( web_server_cb == 0 ) |
||||
{ |
||||
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; |
||||
int sent_args = 0; |
||||
int max_buf_size = MAX_ARGUMENT_BUFFER_SIZE - HEADER_SIZE - os_strlen(connData->url); |
||||
|
||||
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( sent_args != 0 ) |
||||
{ |
||||
if( argBuffer.argBufferPtr + argPtr >= max_buf_size ) |
||||
{ |
||||
WEB_addArg(&argBuffer, NULL, 0); // there's enough room in the buffer for termination block
|
||||
WEB_sendArgBuffer(&argBuffer, connData, reason ); |
||||
WEB_argInit( &argBuffer ); |
||||
sent_args = 0; |
||||
} |
||||
} |
||||
|
||||
if( WEB_addArg(&argBuffer, arg, argPtr) ) |
||||
{ |
||||
errorResponse(connData, 400, "Post too large!"); |
||||
return HTTPD_CGI_DONE; |
||||
} |
||||
sent_args++; |
||||
} |
||||
} |
||||
} |
||||
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, 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); |
||||
|
||||
// os_sprintf doesn't support %f
|
||||
int intPart = f; |
||||
int fracPart = (f - intPart) * 1000; // use 3 digit precision
|
||||
if( fracPart < 0 ) // for negative numbers
|
||||
fracPart = -fracPart; |
||||
|
||||
char floatBuf[20]; |
||||
os_sprintf(floatBuf, "%d.%03d", intPart, fracPart); |
||||
os_strcpy(jsonBuf + jsonPtr, floatBuf); |
||||
jsonPtr += os_strlen(floatBuf); |
||||
} |
||||
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; |
||||
} |
||||
|
||||
// configuring the callback
|
||||
void ICACHE_FLASH_ATTR WEB_Setup(CmdPacket *cmd) |
||||
{ |
||||
CmdRequest req; |
||||
cmdRequest(&req, cmd); |
||||
|
||||
if (cmdGetArgc(&req) < 1) return; |
||||
|
||||
cmdPopArg(&req, &web_server_cb, 4); // pop the callback
|
||||
|
||||
os_printf("Web-server connected, cb=0x%x\n", web_server_cb); |
||||
} |
||||
|
||||
// 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,38 @@ |
||||
#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_Setup(CmdPacket *cmd); |
||||
void WEB_Data(CmdPacket *cmd); |
||||
|
||||
#endif /* WEB_SERVER_H */ |
||||
|
Loading…
Reference in new issue