add RESTful API server example

This commit is contained in:
suda-morris 2019-05-10 13:21:14 +08:00
parent ce45e7806b
commit 11c17ab5b6
29 changed files with 995 additions and 0 deletions

View file

@ -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)

View file

@ -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

View file

@ -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` | {<br />version:"v4.0-dev",<br />cores:2<br />} | Used for clients to get system information like IDF version, ESP32 cores, etc | `/` |
| `/api/v1/temp/raw` | `GET` | {<br />raw:22<br />} | Used for clients to get raw temperature data read from sensor | `/chart` |
| `/api/v1/light/brightness` | `POST` | { <br />red:160,<br />green:160,<br />blue:160<br />} | Used for clients to upload control values to ESP32 in order to control LEDs 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 its 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.)

View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8

View file

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View file

@ -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'
}
}

View file

@ -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

View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

View file

@ -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"
}
}

View file

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>ESP-HOME</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
</head>
<body>
<noscript>
<strong>We're sorry but web-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View file

@ -0,0 +1,55 @@
<template>
<v-app id="inspire">
<v-navigation-drawer v-model="drawer" fixed app clipped>
<v-list dense>
<v-list-tile to="/">
<v-list-tile-action>
<v-icon>home</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Home</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/chart">
<v-list-tile-action>
<v-icon>show_chart</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Chart</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/light">
<v-list-tile-action>
<v-icon>highlight</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Light</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar color="red accent-4" dark fixed app clipped-left>
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
<v-toolbar-title>ESP Home</v-toolbar-title>
</v-toolbar>
<v-content>
<v-container fluid fill-height>
<router-view></router-view>
</v-container>
</v-content>
<v-footer color="red accent-4" app fixed>
<span class="white--text">&copy; ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD. All rights reserved.</span>
</v-footer>
</v-app>
</template>
<script>
export default {
name: "App",
data() {
return {
drawer: null
};
}
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -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')

View file

@ -0,0 +1,7 @@
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'
Vue.use(Vuetify, {
iconfont: 'md',
})

View file

@ -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
}
]
})

View file

@ -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);
});
}
}
})

View file

@ -0,0 +1,41 @@
<template>
<v-container fluid>
<v-sparkline
:value="get_chart_value"
:gradient="['#f72047', '#ffd200', '#1feaea']"
:smooth="10"
:padding="8"
:line-width="2"
stroke-linecap="round"
gradient-direction="top"
auto-draw
></v-sparkline>
</v-container>
</template>
<script>
export default {
data() {
return {
timer: null
};
},
computed: {
get_chart_value() {
return this.$store.state.chart_value;
}
},
methods: {
updateData: function() {
this.$store.dispatch("update_chart_value");
}
},
mounted() {
clearInterval(this.timer);
this.timer = setInterval(this.updateData, 1000);
},
destroyed: function() {
clearInterval(this.timer);
}
};
</script>

View file

@ -0,0 +1,40 @@
<template>
<v-container>
<v-layout text-xs-center wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-img :src="require('../assets/logo.png')" contain height="200"></v-img>
<v-card-title primary-title>
<div class="ma-auto">
<span class="grey--text">IDF version: {{version}}</span>
<br>
<span class="grey--text">ESP cores: {{cores}}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return {
version: null,
cores: null
};
},
mounted() {
this.$ajax
.get("/api/v1/system/info")
.then(data => {
this.version = data.data.version;
this.cores = data.data.cores;
})
.catch(error => {
console.log(error);
});
}
};
</script>

View file

@ -0,0 +1,63 @@
<template>
<v-container>
<v-layout text-xs-center wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-responsive :style="{ background: `rgb(${red}, ${green}, ${blue})` }" height="300px"></v-responsive>
<v-card-text>
<v-container fluid grid-list-lg>
<v-layout row wrap>
<v-flex xs9>
<v-slider v-model="red" :max="255" label="R"></v-slider>
</v-flex>
<v-flex xs3>
<v-text-field v-model="red" class="mt-0" type="number"></v-text-field>
</v-flex>
<v-flex xs9>
<v-slider v-model="green" :max="255" label="G"></v-slider>
</v-flex>
<v-flex xs3>
<v-text-field v-model="green" class="mt-0" type="number"></v-text-field>
</v-flex>
<v-flex xs9>
<v-slider v-model="blue" :max="255" label="B"></v-slider>
</v-flex>
<v-flex xs3>
<v-text-field v-model="blue" class="mt-0" type="number"></v-text-field>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
<v-btn fab dark large color="red accent-4" @click="set_color">
<v-icon dark>check_box</v-icon>
</v-btn>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data() {
return { red: 160, green: 160, blue: 160 };
},
methods: {
set_color: function() {
this.$ajax
.post("/api/v1/light/brightness", {
red: this.red,
green: this.green,
blue: this.blue
})
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
}
}
};
</script>

View file

@ -0,0 +1,11 @@
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://esp-home.local:80',
changeOrigin: true,
ws: true
}
}
}
}

View file

@ -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()

View file

@ -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

View file

@ -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));
}

View file

@ -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 <string.h>
#include <fcntl.h>
#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;
}

View file

@ -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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 1M,
6 www, data, spiffs, , 2M,

View file

@ -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"