From 11c17ab5b622db6c7c0014db63e028f4a220b14f Mon Sep 17 00:00:00 2001 From: suda-morris <362953310@qq.com> Date: Fri, 10 May 2019 13:21:14 +0800 Subject: [PATCH] add RESTful API server example --- .../http_server/restful_server/CMakeLists.txt | 7 + .../http_server/restful_server/Makefile | 14 ++ .../http_server/restful_server/README.md | 135 +++++++++++ .../front/web-demo/.browserslistrc | 3 + .../front/web-demo/.editorconfig | 5 + .../front/web-demo/.eslintrc.js | 17 ++ .../restful_server/front/web-demo/.gitignore | 26 ++ .../front/web-demo/babel.config.js | 5 + .../front/web-demo/package.json | 32 +++ .../front/web-demo/postcss.config.js | 5 + .../front/web-demo/public/favicon.ico | Bin 0 -> 6429 bytes .../front/web-demo/public/index.html | 19 ++ .../restful_server/front/web-demo/src/App.vue | 55 +++++ .../front/web-demo/src/assets/logo.png | Bin 0 -> 37327 bytes .../restful_server/front/web-demo/src/main.js | 16 ++ .../front/web-demo/src/plugins/vuetify.js | 7 + .../front/web-demo/src/router.js | 29 +++ .../front/web-demo/src/store.js | 28 +++ .../front/web-demo/src/views/Chart.vue | 41 ++++ .../front/web-demo/src/views/Home.vue | 40 ++++ .../front/web-demo/src/views/Light.vue | 63 +++++ .../front/web-demo/vue.config.js | 11 + .../restful_server/main/CMakeLists.txt | 13 + .../restful_server/main/Kconfig.projbuild | 50 ++++ .../restful_server/main/component.mk | 0 .../restful_server/main/esp_rest_main.c | 134 +++++++++++ .../restful_server/main/rest_server.c | 225 ++++++++++++++++++ .../restful_server/partitions_example.csv | 6 + .../restful_server/sdkconfig.defaults | 9 + 29 files changed, 995 insertions(+) create mode 100644 examples/protocols/http_server/restful_server/CMakeLists.txt create mode 100644 examples/protocols/http_server/restful_server/Makefile create mode 100644 examples/protocols/http_server/restful_server/README.md create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/.browserslistrc create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/.editorconfig create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/.eslintrc.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/.gitignore create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/babel.config.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/package.json create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/postcss.config.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/public/favicon.ico create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/public/index.html create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/App.vue create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/assets/logo.png create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/main.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/plugins/vuetify.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/router.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/store.js create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/views/Chart.vue create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/views/Home.vue create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/src/views/Light.vue create mode 100644 examples/protocols/http_server/restful_server/front/web-demo/vue.config.js create mode 100644 examples/protocols/http_server/restful_server/main/CMakeLists.txt create mode 100644 examples/protocols/http_server/restful_server/main/Kconfig.projbuild create mode 100644 examples/protocols/http_server/restful_server/main/component.mk create mode 100644 examples/protocols/http_server/restful_server/main/esp_rest_main.c create mode 100644 examples/protocols/http_server/restful_server/main/rest_server.c create mode 100644 examples/protocols/http_server/restful_server/partitions_example.csv create mode 100644 examples/protocols/http_server/restful_server/sdkconfig.defaults diff --git a/examples/protocols/http_server/restful_server/CMakeLists.txt b/examples/protocols/http_server/restful_server/CMakeLists.txt new file mode 100644 index 0000000000..cb143ac640 --- /dev/null +++ b/examples/protocols/http_server/restful_server/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.5) + +# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection. +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(restful_server) diff --git a/examples/protocols/http_server/restful_server/Makefile b/examples/protocols/http_server/restful_server/Makefile new file mode 100644 index 0000000000..6870cd55f6 --- /dev/null +++ b/examples/protocols/http_server/restful_server/Makefile @@ -0,0 +1,14 @@ +PROJECT_NAME := restful_server + +EXTRA_COMPONENT_DIRS = $(IDF_PATH)/examples/common_components/protocol_examples_common + +include $(IDF_PATH)/make/project.mk + +ifdef CONFIG_WEB_DEPLOY_SF +WEB_SRC_DIR = $(shell pwd)/front/web-demo +ifneq ($(wildcard $(WEB_SRC_DIR)/dist/.*),) +$(eval $(call spiffs_create_partition_image,www,$(WEB_SRC_DIR)/dist,FLASH_IN_PROJECT)) +else +$(error $(WEB_SRC_DIR)/dist doesn't exist. Please run 'npm run build' in $(WEB_SRC_DIR)) +endif +endif diff --git a/examples/protocols/http_server/restful_server/README.md b/examples/protocols/http_server/restful_server/README.md new file mode 100644 index 0000000000..0a2ad7d871 --- /dev/null +++ b/examples/protocols/http_server/restful_server/README.md @@ -0,0 +1,135 @@ +# HTTP Restful API Server Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +## Overview + +This example mainly introduces how to implement a RESTful API server and HTTP server on ESP32, with a frontend browser UI. + +This example designs several APIs to fetch resources as follows: + +| API | Method | Resource Example | Description | Page URL | +| -------------------------- | ------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | +| `/api/v1/system/info` | `GET` | {
version:"v4.0-dev",
cores:2
} | Used for clients to get system information like IDF version, ESP32 cores, etc | `/` | +| `/api/v1/temp/raw` | `GET` | {
raw:22
} | Used for clients to get raw temperature data read from sensor | `/chart` | +| `/api/v1/light/brightness` | `POST` | {
red:160,
green:160,
blue:160
} | Used for clients to upload control values to ESP32 in order to control LED’s brightness | `/light` | + +**Page URL** is the URL of the webpage which will send a request to the API. + +### About mDNS + +The IP address of an IoT device may vary from time to time, so it’s impracticable to hard code the IP address in the webpage. In this example, we use the `mDNS` to parse the domain name `esp-home.local`, so that we can alway get access to the web server by this URL no matter what the real IP address behind it. See [here](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/protocols/mdns.html) for more information about mDNS. + +**Notes: mDNS is installed by default on most operating systems or is available as separate package.** + +### About deploy mode + +In development mode, it would be awful to flash the whole webpages every time we update the html, js or css files. So it is highly recommended to deploy the webpage to host PC via `semihost` technology. Whenever the browser fetch the webpage, ESP32 can forward the required files located on host PC. By this mean, it will save a lot of time when designing new pages. + +After developing, the pages should be deployed to one of the following destinations: + +* SPI Flash - which is recommended when the website after built is small (e.g. less than 2MB). +* SD Card - which would be an option when the website after built is very large that the SPI Flash have not enough space to hold (e.g. larger than 2MB). + +### About frontend framework + +Many famous frontend frameworks (e.g. Vue, React, Angular) can be used in this example. Here we just take [Vue](https://vuejs.org/) as example and adopt the [vuetify](https://vuetifyjs.com/) as the UI library. + +## How to use example + +### Hardware Required + +To run this example, you need an ESP32 dev board (e.g. ESP32-WROVER Kit, ESP32-Ethernet-Kit) or ESP32 core board (e.g. ESP32-DevKitC). An extra JTAG adapter might also needed if you choose to deploy the website by semihosting. For more information about supported JTAG adapter, please refer to [select JTAG adapter](https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/jtag-debugging/index.html#jtag-debugging-selecting-jtag-adapter). Or if you choose to deploy the website to SD card, an extra SD slot board is needed. + +#### Pin Assignment: + +Only if you deploy the website to SD card, then the following pin connection is used in this example. + +| ESP32 | SD Card | +| ------ | ------- | +| GPIO2 | D0 | +| GPIO4 | D1 | +| GPIO12 | D2 | +| GPIO13 | D3 | +| GPIO14 | CLK | +| GPIO15 | CMD | + + +### Configure the project + +Enter `make menuconfig` if you are using GNU Make based build system or enter `idf.py menuconfig` if you are using CMake based build system. + +In the `Example Connection Configuration` menu: + +* Choose the network interface in `Connect using` option based on your board. Currently we support both Wi-Fi and Ethernet. +* If you select the Wi-Fi interface, you also have to set: + * Wi-Fi SSID and Wi-Fi password that your esp32 will connect to. +* If you select the Ethernet interface, you also have to set: + * PHY model in `Ethernet PHY` option, e.g. IP101. + * PHY address in `PHY Address` option, which should be determined by your board schematic. + * EMAC Clock mode, GPIO used by SMI. + +In the `Example Configuration` menu: + +* Set the domain name in `mDNS Host Name` option. +* Choose the deploy mode in `Website deploy mode`, currently we support deploy website to host PC, SD card and SPI Nor flash. + * If we choose to `Deploy website to host (JTAG is needed)`, then we also need to specify the full path of the website in `Host path to mount (e.g. absolute path to web dist directory)`. +* Set the mount point of the website in `Website mount point in VFS` option, the default value is `/www`. + +### Build and Flash + +After the webpage design work has been finished, you should compile them by running following commands: + +```bash +cd path_to_this_example/front/web-demo +npm install +npm run build +``` + +After a while, you will see a `dist` directory which contains all the website files (e.g. html, js, css, images). + +Enter `make -j4 flash monitor` if you are using GNU Make based build system or enter `idf.py build flash monitor` if you are using CMake based build system. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. + +### Extra steps to do for deploying website by semihost + +We need to run the latest version of OpenOCD which should support semihost feature when we test this deploy mode: + +```bash +openocd-esp32/bin/openocd -s openocd-esp32/share/openocd/scripts -f interface/ftdi/esp32_devkitj_v1.cfg -f board/esp-wroom-32.cfg +``` + +## Example Output + +### Render webpage in browser + +In your browser, enter the URL where the website located (e.g. `http://esp-home.local`). You can also enter the IP address that ESP32 obtained if your operating system currently don't have support for mDNS service. + +![esp_home_local](https://dl.espressif.com/dl/esp-idf/docs/_static/esp_home_local.gif) + +### ESP monitor output + +In the *Light* page, after we set up the light color and click on the check button, the browser will send a post request to ESP32, and in the console, we just print the color value. + +```bash +I (6115) example_connect: Connected to Ethernet +I (6115) example_connect: IPv4 address: 192.168.2.151 +I (6325) esp-home: Partition size: total: 1920401, used: 1587575 +I (6325) esp-rest: Starting HTTP Server +I (128305) esp-rest: File sending complete +I (128565) esp-rest: File sending complete +I (128855) esp-rest: File sending complete +I (129525) esp-rest: File sending complete +I (129855) esp-rest: File sending complete +I (137485) esp-rest: Light control: red = 50, green = 85, blue = 28 +``` + +## Troubleshooting + +1. Error occurred when building example: `...front/web-demo/dist doesn't exit. Please run 'npm run build' in ...front/web-demo`. + * When you choose to deploy website to SPI flash, make sure the `dist` directory has been generated before you building this example. + +(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.) diff --git a/examples/protocols/http_server/restful_server/front/web-demo/.browserslistrc b/examples/protocols/http_server/restful_server/front/web-demo/.browserslistrc new file mode 100644 index 0000000000..9dee646463 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not ie <= 8 diff --git a/examples/protocols/http_server/restful_server/front/web-demo/.editorconfig b/examples/protocols/http_server/restful_server/front/web-demo/.editorconfig new file mode 100644 index 0000000000..7053c49a04 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/.editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/examples/protocols/http_server/restful_server/front/web-demo/.eslintrc.js b/examples/protocols/http_server/restful_server/front/web-demo/.eslintrc.js new file mode 100644 index 0000000000..98d043169d --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { + node: true + }, + 'extends': [ + 'plugin:vue/essential', + '@vue/standard' + ], + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' + }, + parserOptions: { + parser: 'babel-eslint' + } +} diff --git a/examples/protocols/http_server/restful_server/front/web-demo/.gitignore b/examples/protocols/http_server/restful_server/front/web-demo/.gitignore new file mode 100644 index 0000000000..d0d94890c3 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/.gitignore @@ -0,0 +1,26 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# APIs used in this example is simple and stable enough. +# There shouldn't be risk of compatibility unless the major version of some library changed. +# To compress the package size, just exclude the package-lock.json file. +package-lock.json diff --git a/examples/protocols/http_server/restful_server/front/web-demo/babel.config.js b/examples/protocols/http_server/restful_server/front/web-demo/babel.config.js new file mode 100644 index 0000000000..ba179669a1 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/examples/protocols/http_server/restful_server/front/web-demo/package.json b/examples/protocols/http_server/restful_server/front/web-demo/package.json new file mode 100644 index 0000000000..b2c4bc3ad7 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/package.json @@ -0,0 +1,32 @@ +{ + "name": "web-demo", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "axios": "^0.18.0", + "core-js": "^2.6.5", + "vue": "^2.6.10", + "vue-router": "^3.0.3", + "vuetify": "^1.5.14", + "vuex": "^3.0.1" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^3.7.0", + "@vue/cli-plugin-eslint": "^3.7.0", + "@vue/cli-service": "^3.7.0", + "@vue/eslint-config-standard": "^4.0.0", + "babel-eslint": "^10.0.1", + "eslint": "^5.16.0", + "eslint-plugin-vue": "^5.0.0", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "vue-cli-plugin-vuetify": "^0.5.0", + "vue-template-compiler": "^2.5.21", + "vuetify-loader": "^1.0.5" + } +} diff --git a/examples/protocols/http_server/restful_server/front/web-demo/postcss.config.js b/examples/protocols/http_server/restful_server/front/web-demo/postcss.config.js new file mode 100644 index 0000000000..961986e2b1 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/examples/protocols/http_server/restful_server/front/web-demo/public/favicon.ico b/examples/protocols/http_server/restful_server/front/web-demo/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..17b8ff3cb6c3c920c732f563c2507c8472dfb95c GIT binary patch literal 6429 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 O1_n=8KbLh*2~7ZoN?8Q} literal 0 HcmV?d00001 diff --git a/examples/protocols/http_server/restful_server/front/web-demo/public/index.html b/examples/protocols/http_server/restful_server/front/web-demo/public/index.html new file mode 100644 index 0000000000..26a416ee4f --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/public/index.html @@ -0,0 +1,19 @@ + + + + + + + + ESP-HOME + + + + + +
+ + + diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/App.vue b/examples/protocols/http_server/restful_server/front/web-demo/src/App.vue new file mode 100644 index 0000000000..0d79bb1819 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/App.vue @@ -0,0 +1,55 @@ + + + diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/assets/logo.png b/examples/protocols/http_server/restful_server/front/web-demo/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c19919e5cafb7933aa0815c044461ddde4f765ac GIT binary patch literal 37327 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 literal 0 HcmV?d00001 diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/main.js b/examples/protocols/http_server/restful_server/front/web-demo/src/main.js new file mode 100644 index 0000000000..ef8f1c5cba --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/main.js @@ -0,0 +1,16 @@ +import Vue from 'vue' +import './plugins/vuetify' +import App from './App.vue' +import router from './router' +import axios from 'axios' +import store from './store' + +Vue.config.productionTip = false + +Vue.prototype.$ajax = axios + +new Vue({ + router, + store, + render: h => h(App) +}).$mount('#app') diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/plugins/vuetify.js b/examples/protocols/http_server/restful_server/front/web-demo/src/plugins/vuetify.js new file mode 100644 index 0000000000..975696e792 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/plugins/vuetify.js @@ -0,0 +1,7 @@ +import Vue from 'vue' +import Vuetify from 'vuetify/lib' +import 'vuetify/src/stylus/app.styl' + +Vue.use(Vuetify, { + iconfont: 'md', +}) diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/router.js b/examples/protocols/http_server/restful_server/front/web-demo/src/router.js new file mode 100644 index 0000000000..2e6ce9440b --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/router.js @@ -0,0 +1,29 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Home from './views/Home.vue' +import Chart from './views/Chart.vue' +import Light from './views/Light.vue' + +Vue.use(Router) + +export default new Router({ + mode: 'history', + base: process.env.BASE_URL, + routes: [ + { + path: '/', + name: 'home', + component: Home + }, + { + path: '/chart', + name: 'chart', + component: Chart + }, + { + path: '/light', + name: 'light', + component: Light + } + ] +}) diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/store.js b/examples/protocols/http_server/restful_server/front/web-demo/src/store.js new file mode 100644 index 0000000000..62f44f6515 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/store.js @@ -0,0 +1,28 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import axios from 'axios' + +Vue.use(Vuex) + +export default new Vuex.Store({ + state: { + chart_value: [8, 2, 5, 9, 5, 11, 3, 5, 10, 0, 1, 8, 2, 9, 0, 13, 10, 7, 16], + }, + mutations: { + update_chart_value(state, new_value) { + state.chart_value.push(new_value); + state.chart_value.shift(); + } + }, + actions: { + update_chart_value({ commit }) { + axios.get("/api/v1/temp/raw") + .then(data => { + commit("update_chart_value", data.data.raw); + }) + .catch(error => { + console.log(error); + }); + } + } +}) diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/views/Chart.vue b/examples/protocols/http_server/restful_server/front/web-demo/src/views/Chart.vue new file mode 100644 index 0000000000..7d75477cfd --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/views/Chart.vue @@ -0,0 +1,41 @@ + + + diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/views/Home.vue b/examples/protocols/http_server/restful_server/front/web-demo/src/views/Home.vue new file mode 100644 index 0000000000..f3ab672878 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/views/Home.vue @@ -0,0 +1,40 @@ + + + diff --git a/examples/protocols/http_server/restful_server/front/web-demo/src/views/Light.vue b/examples/protocols/http_server/restful_server/front/web-demo/src/views/Light.vue new file mode 100644 index 0000000000..bcfbe56e4e --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/src/views/Light.vue @@ -0,0 +1,63 @@ + + + + diff --git a/examples/protocols/http_server/restful_server/front/web-demo/vue.config.js b/examples/protocols/http_server/restful_server/front/web-demo/vue.config.js new file mode 100644 index 0000000000..0322551512 --- /dev/null +++ b/examples/protocols/http_server/restful_server/front/web-demo/vue.config.js @@ -0,0 +1,11 @@ +module.exports = { + devServer: { + proxy: { + '/api': { + target: 'http://esp-home.local:80', + changeOrigin: true, + ws: true + } + } + } +} diff --git a/examples/protocols/http_server/restful_server/main/CMakeLists.txt b/examples/protocols/http_server/restful_server/main/CMakeLists.txt new file mode 100644 index 0000000000..e0a89216f0 --- /dev/null +++ b/examples/protocols/http_server/restful_server/main/CMakeLists.txt @@ -0,0 +1,13 @@ +set(COMPONENT_SRCS "esp_rest_main.c" "rest_server.c") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +register_component() + +if(CONFIG_WEB_DEPLOY_SF) + set(WEB_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../front/web-demo") + if(EXISTS ${WEB_SRC_DIR}/dist) + spiffs_create_partition_image(www ${WEB_SRC_DIR}/dist FLASH_IN_PROJECT) + else() + message(FATAL_ERROR "${WEB_SRC_DIR}/dist doesn't exit. Please run 'npm run build' in ${WEB_SRC_DIR}") + endif() +endif() diff --git a/examples/protocols/http_server/restful_server/main/Kconfig.projbuild b/examples/protocols/http_server/restful_server/main/Kconfig.projbuild new file mode 100644 index 0000000000..5ae5a36e6f --- /dev/null +++ b/examples/protocols/http_server/restful_server/main/Kconfig.projbuild @@ -0,0 +1,50 @@ +menu "Example Configuration" + + config MDNS_HOST_NAME + string "mDNS Host Name" + default "esp-home" + help + Specify the domain name used in the mDNS service. + Note that webpage also take it as a part of URL where it will send GET/POST requests to. + + choice WEB_DEPLOY_MODE + prompt "Website deploy mode" + default WEB_DEPLOY_SEMIHOST + help + Select website deploy mode. + You can deploy website to host, and ESP32 will retrieve them in a semihost way (JTAG is needed). + You can deploy website to SD card or SPI flash, and ESP32 will retrieve them via SDIO/SPI interface. + Detailed operation steps are listed in the example README file. + config WEB_DEPLOY_SEMIHOST + bool "Deploy website to host (JTAG is needed)" + help + Deploy website to host. + It is recommended to choose this mode during developing. + config WEB_DEPLOY_SD + bool "Deploy website to SD card" + help + Deploy website to SD card. + Choose this production mode if the size of website is too large (bigger than 2MB). + config WEB_DEPLOY_SF + bool "Deploy website to SPI Nor Flash" + help + Deploy website to SPI Nor Flash. + Choose this production mode if the size of website is small (less than 2MB). + endchoice + + if WEB_DEPLOY_SEMIHOST + config HOST_PATH_TO_MOUNT + string "Host path to mount (e.g. absolute path to web dist directory)" + default "PATH-TO-WEB-DIST_DIR" + help + When using semihost in ESP32, you should specify the host path which will be mounted to VFS. + Note that only absolute path is acceptable. + endif + + config WEB_MOUNT_POINT + string "Website mount point in VFS" + default "/www" + help + Specify the mount point in VFS. + +endmenu diff --git a/examples/protocols/http_server/restful_server/main/component.mk b/examples/protocols/http_server/restful_server/main/component.mk new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/protocols/http_server/restful_server/main/esp_rest_main.c b/examples/protocols/http_server/restful_server/main/esp_rest_main.c new file mode 100644 index 0000000000..7785a0a41c --- /dev/null +++ b/examples/protocols/http_server/restful_server/main/esp_rest_main.c @@ -0,0 +1,134 @@ +/* HTTP Restful API Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include "esp_event_loop.h" +#include "driver/sdmmc_host.h" +#include "driver/gpio.h" +#include "esp_vfs_semihost.h" +#include "esp_vfs_fat.h" +#include "esp_spiffs.h" +#include "sdmmc_cmd.h" +#include "nvs_flash.h" +#include "tcpip_adapter.h" +#include "esp_event.h" +#include "esp_log.h" +#include "mdns.h" +#include "protocol_examples_common.h" +#include "sdkconfig.h" + +#define MDNS_INSTANCE "esp home web server" + +static const char *TAG = "example"; + +esp_err_t start_rest_server(const char *base_path); + +static void initialise_mdns(void) +{ + mdns_init(); + mdns_hostname_set(CONFIG_MDNS_HOST_NAME); + mdns_instance_name_set(MDNS_INSTANCE); + + mdns_txt_item_t serviceTxtData[] = { + {"board", "esp32"}, + {"path", "/"} + }; + + ESP_ERROR_CHECK(mdns_service_add("ESP32-WebServer", "_http", "_tcp", 80, serviceTxtData, + sizeof(serviceTxtData) / sizeof(serviceTxtData[0]))); +} + +#if CONFIG_WEB_DEPLOY_SEMIHOST +esp_err_t init_fs(void) +{ + esp_err_t ret = esp_vfs_semihost_register(CONFIG_WEB_MOUNT_POINT, CONFIG_HOST_PATH_TO_MOUNT); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to register semihost driver (%s)!", esp_err_to_name(ret)); + return ESP_FAIL; + } + return ESP_OK; +} +#endif + +#if CONFIG_WEB_DEPLOY_SD +esp_err_t init_fs(void) +{ + sdmmc_host_t host = SDMMC_HOST_DEFAULT(); + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + + gpio_set_pull_mode(15, GPIO_PULLUP_ONLY); // CMD + gpio_set_pull_mode(2, GPIO_PULLUP_ONLY); // D0 + gpio_set_pull_mode(4, GPIO_PULLUP_ONLY); // D1 + gpio_set_pull_mode(12, GPIO_PULLUP_ONLY); // D2 + gpio_set_pull_mode(13, GPIO_PULLUP_ONLY); // D3 + + esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = true, + .max_files = 4, + .allocation_unit_size = 16 * 1024 + }; + + sdmmc_card_t *card; + esp_err_t ret = esp_vfs_fat_sdmmc_mount(CONFIG_WEB_MOUNT_POINT, &host, &slot_config, &mount_config, &card); + if (ret != ESP_OK) { + if (ret == ESP_FAIL) { + ESP_LOGE(TAG, "Failed to mount filesystem."); + } else { + ESP_LOGE(TAG, "Failed to initialize the card (%s)", esp_err_to_name(ret)); + } + return ESP_FAIL; + } + /* print card info if mount successfully */ + sdmmc_card_print_info(stdout, card); + return ESP_OK; +} +#endif + +#if CONFIG_WEB_DEPLOY_SF +esp_err_t init_fs(void) +{ + esp_vfs_spiffs_conf_t conf = { + .base_path = CONFIG_WEB_MOUNT_POINT, + .partition_label = NULL, + .max_files = 5, + .format_if_mount_failed = false + }; + esp_err_t ret = esp_vfs_spiffs_register(&conf); + + if (ret != ESP_OK) { + if (ret == ESP_FAIL) { + ESP_LOGE(TAG, "Failed to mount or format filesystem"); + } else if (ret == ESP_ERR_NOT_FOUND) { + ESP_LOGE(TAG, "Failed to find SPIFFS partition"); + } else { + ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); + } + return ESP_FAIL; + } + + size_t total = 0, used = 0; + ret = esp_spiffs_info(NULL, &total, &used); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret)); + } else { + ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used); + } + return ESP_OK; +} +#endif + +void app_main() +{ + ESP_ERROR_CHECK(nvs_flash_init()); + tcpip_adapter_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + initialise_mdns(); + + ESP_ERROR_CHECK(example_connect()); + ESP_ERROR_CHECK(init_fs()); + ESP_ERROR_CHECK(start_rest_server(CONFIG_WEB_MOUNT_POINT)); +} diff --git a/examples/protocols/http_server/restful_server/main/rest_server.c b/examples/protocols/http_server/restful_server/main/rest_server.c new file mode 100644 index 0000000000..8c74ccec52 --- /dev/null +++ b/examples/protocols/http_server/restful_server/main/rest_server.c @@ -0,0 +1,225 @@ +/* HTTP Restful API Server + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#include "esp_http_server.h" +#include "esp_system.h" +#include "esp_log.h" +#include "esp_vfs.h" +#include "cJSON.h" + +static const char *REST_TAG = "esp-rest"; +#define REST_CHECK(a, str, goto_tag, ...) \ + do \ + { \ + if (!(a)) \ + { \ + ESP_LOGE(REST_TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + goto goto_tag; \ + } \ + } while (0) + +#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128) +#define SCRATCH_BUFSIZE (10240) + +typedef struct rest_server_context { + char base_path[ESP_VFS_PATH_MAX + 1]; + char scratch[SCRATCH_BUFSIZE]; +} rest_server_context_t; + +#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0) + +/* Set HTTP response content type according to file extension */ +static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filepath) +{ + const char *type = "text/plain"; + if (CHECK_FILE_EXTENSION(filepath, ".html")) { + type = "text/html"; + } else if (CHECK_FILE_EXTENSION(filepath, ".js")) { + type = "application/javascript"; + } else if (CHECK_FILE_EXTENSION(filepath, ".css")) { + type = "text/css"; + } else if (CHECK_FILE_EXTENSION(filepath, ".png")) { + type = "image/png"; + } else if (CHECK_FILE_EXTENSION(filepath, ".ico")) { + type = "image/x-icon"; + } else if (CHECK_FILE_EXTENSION(filepath, ".svg")) { + type = "text/xml"; + } + return httpd_resp_set_type(req, type); +} + +/* Send HTTP response with the contents of the requested file */ +static esp_err_t rest_common_get_handler(httpd_req_t *req) +{ + char filepath[FILE_PATH_MAX]; + + rest_server_context_t *rest_context = (rest_server_context_t *)req->user_ctx; + strlcpy(filepath, rest_context->base_path, sizeof(filepath)); + if (req->uri[strlen(req->uri) - 1] == '/') { + strlcat(filepath, "/index.html", sizeof(filepath)); + } else { + strlcat(filepath, req->uri, sizeof(filepath)); + } + int fd = open(filepath, O_RDONLY, 0); + if (fd == -1) { + ESP_LOGE(REST_TAG, "Failed to open file : %s", filepath); + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file"); + return ESP_FAIL; + } + + set_content_type_from_file(req, filepath); + + char *chunk = rest_context->scratch; + ssize_t read_bytes; + do { + /* Read file in chunks into the scratch buffer */ + read_bytes = read(fd, chunk, SCRATCH_BUFSIZE); + if (read_bytes == -1) { + ESP_LOGE(REST_TAG, "Failed to read file : %s", filepath); + } else if (read_bytes > 0) { + /* Send the buffer contents as HTTP response chunk */ + if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) { + close(fd); + ESP_LOGE(REST_TAG, "File sending failed!"); + /* Abort sending file */ + httpd_resp_sendstr_chunk(req, NULL); + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + return ESP_FAIL; + } + } + } while (read_bytes > 0); + /* Close file after sending complete */ + close(fd); + ESP_LOGI(REST_TAG, "File sending complete"); + /* Respond with an empty chunk to signal HTTP response completion */ + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; +} + +/* Simple handler for light brightness control */ +static esp_err_t light_brightness_post_handler(httpd_req_t *req) +{ + int total_len = req->content_len; + int cur_len = 0; + char *buf = ((rest_server_context_t *)(req->user_ctx))->scratch; + int received = 0; + if (total_len >= SCRATCH_BUFSIZE) { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long"); + return ESP_FAIL; + } + while (cur_len < total_len) { + received = httpd_req_recv(req, buf + cur_len, total_len); + if (received <= 0) { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value"); + return ESP_FAIL; + } + cur_len += received; + } + buf[total_len] = '\0'; + + cJSON *root = cJSON_Parse(buf); + int red = cJSON_GetObjectItem(root, "red")->valueint; + int green = cJSON_GetObjectItem(root, "green")->valueint; + int blue = cJSON_GetObjectItem(root, "blue")->valueint; + ESP_LOGI(REST_TAG, "Light control: red = %d, green = %d, blue = %d", red, green, blue); + cJSON_Delete(root); + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +/* Simple handler for getting system handler */ +static esp_err_t system_info_get_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + cJSON *root = cJSON_CreateObject(); + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + cJSON_AddStringToObject(root, "version", IDF_VER); + cJSON_AddNumberToObject(root, "cores", chip_info.cores); + const char *sys_info = cJSON_Print(root); + httpd_resp_sendstr(req, sys_info); + free((void *)sys_info); + cJSON_Delete(root); + return ESP_OK; +} + +/* Simple handler for getting temperature data */ +static esp_err_t temperature_data_get_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + cJSON *root = cJSON_CreateObject(); + cJSON_AddNumberToObject(root, "raw", esp_random() % 20); + const char *sys_info = cJSON_Print(root); + httpd_resp_sendstr(req, sys_info); + free((void *)sys_info); + cJSON_Delete(root); + return ESP_OK; +} + +esp_err_t start_rest_server(const char *base_path) +{ + REST_CHECK(base_path, "wrong base path", err); + rest_server_context_t *rest_context = calloc(1, sizeof(rest_server_context_t)); + REST_CHECK(rest_context, "No memory for rest context", err); + strncpy(rest_context->base_path, base_path, ESP_VFS_PATH_MAX); + + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.uri_match_fn = httpd_uri_match_wildcard; + + ESP_LOGI(REST_TAG, "Starting HTTP Server"); + REST_CHECK(httpd_start(&server, &config) == ESP_OK, "Start server failed", err_start); + + /* URI handler for fetching system info */ + httpd_uri_t system_info_get_uri = { + .uri = "/api/v1/system/info", + .method = HTTP_GET, + .handler = system_info_get_handler, + .user_ctx = rest_context + }; + httpd_register_uri_handler(server, &system_info_get_uri); + + /* URI handler for fetching temperature data */ + httpd_uri_t temperature_data_get_uri = { + .uri = "/api/v1/temp/raw", + .method = HTTP_GET, + .handler = temperature_data_get_handler, + .user_ctx = rest_context + }; + httpd_register_uri_handler(server, &temperature_data_get_uri); + + /* URI handler for light brightness control */ + httpd_uri_t light_brightness_post_uri = { + .uri = "/api/v1/light/brightness", + .method = HTTP_POST, + .handler = light_brightness_post_handler, + .user_ctx = rest_context + }; + httpd_register_uri_handler(server, &light_brightness_post_uri); + + /* URI handler for getting web server files */ + httpd_uri_t common_get_uri = { + .uri = "/*", + .method = HTTP_GET, + .handler = rest_common_get_handler, + .user_ctx = rest_context + }; + httpd_register_uri_handler(server, &common_get_uri); + + return ESP_OK; +err_start: + free(rest_context); +err: + return ESP_FAIL; +} diff --git a/examples/protocols/http_server/restful_server/partitions_example.csv b/examples/protocols/http_server/restful_server/partitions_example.csv new file mode 100644 index 0000000000..ca4898e596 --- /dev/null +++ b/examples/protocols/http_server/restful_server/partitions_example.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +www, data, spiffs, , 2M, diff --git a/examples/protocols/http_server/restful_server/sdkconfig.defaults b/examples/protocols/http_server/restful_server/sdkconfig.defaults new file mode 100644 index 0000000000..599472d848 --- /dev/null +++ b/examples/protocols/http_server/restful_server/sdkconfig.defaults @@ -0,0 +1,9 @@ +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_SPIFFS_OBJ_NAME_LEN=64 +CONFIG_FATFS_LONG_FILENAME=y +CONFIG_FATFS_LFN_HEAP=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv" +CONFIG_PARTITION_TABLE_CUSTOM_APP_BIN_OFFSET=0x10000 +CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv"