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 0000000000..17b8ff3cb6 Binary files /dev/null and b/examples/protocols/http_server/restful_server/front/web-demo/public/favicon.ico differ 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 0000000000..c19919e5ca Binary files /dev/null and b/examples/protocols/http_server/restful_server/front/web-demo/src/assets/logo.png differ 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"