kopia lustrzana https://github.com/espressif/esp-idf
add RESTful API server example
rodzic
ce45e7806b
commit
11c17ab5b6
|
@ -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)
|
|
@ -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
|
|
@ -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 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.)
|
|
@ -0,0 +1,3 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
|
@ -0,0 +1,5 @@
|
|||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 6.3 KiB |
|
@ -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>
|
|
@ -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">© ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD. All rights reserved.</span>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "App",
|
||||
data() {
|
||||
return {
|
||||
drawer: null
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 36 KiB |
|
@ -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')
|
|
@ -0,0 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib'
|
||||
import 'vuetify/src/stylus/app.styl'
|
||||
|
||||
Vue.use(Vuetify, {
|
||||
iconfont: 'md',
|
||||
})
|
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://esp-home.local:80',
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
|
|
@ -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"
|
Ładowanie…
Reference in New Issue