mirror of
https://github.com/espressif/esp-idf.git
synced 2025-08-02 12:14:32 +02:00
feat(restful_server): upgrade the example to use vue3+vuetify3
also cleaned up the backend firmware to use littlefs filesystem.
This commit is contained in:
@@ -106,7 +106,7 @@ Asynchronous Handlers
|
||||
RESTful API
|
||||
-----------
|
||||
|
||||
:example:`protocols/http_server/restful_server` demonstrates how to implement a RESTful API server and HTTP server, with a frontend browser UI, and designs several APIs to fetch resources, using mDNS to parse the domain name, and deploying the webpage to host PC via semihost technology or to SPI flash or SD Card.
|
||||
:example:`protocols/http_server/restful_server` demonstrates how to implement a RESTful API server and web server, with a modern frontend UI, and designs several APIs to fetch resources, using mDNS to parse the domain name, and deploying the webpage to SPI flash.
|
||||
|
||||
URI Handlers
|
||||
------------
|
||||
|
@@ -106,7 +106,7 @@ ESP HTTP 服务器有各种事件,当特定事件发生时,:doc:`事件循
|
||||
RESTful API
|
||||
-----------
|
||||
|
||||
:example:`protocols/http_server/restful_server` 演示了如何实现 RESTful API 服务器和 HTTP 服务器,并结合前端浏览器 UI,设计了多个 API 来获取资源,使用 mDNS 解析域名,并通过半主机技术将网页部署到主机 PC、SPI flash 或 SD 卡上。
|
||||
:example:`protocols/http_server/restful_server` 演示了如何实现 RESTful API 服务器和网页服务器,设计了多个 API 服务端点,使用 mDNS 解析域名,以及将网页部署到 SPI flash 中。
|
||||
|
||||
URI 处理程序
|
||||
------------
|
||||
|
@@ -1,6 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
|
@@ -7,135 +7,102 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This example mainly introduces how to implement a RESTful API server and HTTP server on ESP32, with a frontend browser UI.
|
||||
This example demonstrates on the implementation of both the RESTful API server and web server on ESP32, with a modern UI made by Vue.js and Vuetify frameworks. Please note, using the Vue is not a must, we're just using it as an example to show how to build a modern web UI with the latest web technologies in an IoT application.
|
||||
|
||||
This example designs several APIs to fetch resources as follows:
|
||||
This example exposes several APIs for the clients 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.
|
||||
| API | Method | Resource Example | Description |
|
||||
| -------------------------- | ------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `/api/v1/system/info` | `GET` | {<br />version:"v6.0-dev",<br />cores:2<br />} | Return system information like IDF version, CPU cores, etc |
|
||||
| `/api/v1/temp/raw` | `GET` | {<br />raw:22<br />} | Return temperature data (note, this API returns a random number just for illustration) |
|
||||
| `/api/v1/light/brightness` | `POST` | {<br />red:160,<br />green:160,<br />blue:160<br />} | Set the RGB value of the LED light |
|
||||
|
||||
### 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 always 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.
|
||||
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 `dashboard.local`, so that we can always 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-protocols/mdns/docs/latest/en/index.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.
|
||||
Many famous frontend frameworks (e.g. Vue, React, Svelte) 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 component library.
|
||||
|
||||
## How to use example
|
||||
### About developing frontend and backend independently
|
||||
|
||||
In this example, the webpage files (html, js, css, images, etc) are stored in the filesystem on the ESP chip (we use the littlefs as an example). You can, however, develop the frontend without flashing the filesystem to the ESP every time:
|
||||
|
||||
1. First, disable the `EXAMPLE_DEPLOY_WEB_PAGES` from the menuconfig, implement the endpoints in the backend (the application running on the ESP) and flash it to the device.
|
||||
2. Start developing the frontend on the PC, using Vite dev mode: `pnpm dev`. In dev mode, you can edit the source code of the frontend and see the changes in the web browser immediately. The frontend will be served from your PC, while the Vite proxy will automatically forward the HTTP requests to the `/api` endpoints to the ESP chip.
|
||||
3. Once the frontend development and debugging is done, build the web pages by running `pnpm build`, which will generate the static files in the `dist` directory.
|
||||
4. Finally, enable the `EXAMPLE_DEPLOY_WEB_PAGES` option in the menuconfig, and flash the webpages with the backend firmware together to the ESP chip again.
|
||||
|
||||
This way, you can develop the frontend and backend independently, which is very convenient for web developers.
|
||||
|
||||
## How to use the 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 |
|
||||
|
||||
To run this example, you need an ESP32 dev board (e.g. ESP32-WROVER Kit, ESP32-Ethernet-Kit) with Wi-Fi or Ethernet connection.
|
||||
|
||||
### Configure the project
|
||||
|
||||
Open the project configuration menu (`idf.py menuconfig`).
|
||||
|
||||
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`.
|
||||
* Enable the `Deploy web pages to device's filesystem` option to deploy the web pages to the device's filesystem. This will flash the files from `front/web-demo/dist` to the device's filesystem.
|
||||
|
||||
### Build and Flash
|
||||
### Build the web project
|
||||
|
||||
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
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
> **_NOTE:_** This example needs `nodejs` version `v10.19.0`
|
||||
|
||||
After a while, you will see a `dist` directory which contains all the website files (e.g. html, js, css, images).
|
||||
|
||||
Run `idf.py -p PORT flash monitor` to build and flash the project..
|
||||
Refer to [front/web-demo/README.md](front/web-demo/README.md) for more information about the front-end development.
|
||||
|
||||
### Build and Flash to ESP32 device
|
||||
|
||||
Then, you can Run `idf.py -p PORT flash monitor` to build and flash the project (including the webpage bundle) to ESP32;
|
||||
|
||||
(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 board/esp32-wrover-kit-3.3v.cfg
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
### Render webpage in browser
|
||||
### Check the 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.
|
||||
In your browser, enter the URL where the website located (e.g. `http://dashboard.local`). You can also enter the IP address that ESP32 obtained if your operating system currently don't have support for mDNS service.
|
||||
|
||||
Besides that, this example also enables the NetBIOS feature with the domain name `esp-home`. If your OS supports NetBIOS and has enabled it (e.g. Windows has native support for NetBIOS), then the URL `http://esp-home` should also work.
|
||||
|
||||

|
||||
Besides that, this example also enables the NetBIOS feature with the domain name `dashboard`. If your OS supports NetBIOS and has enabled it (e.g. Windows has native support for NetBIOS), then the URL `http://dashboard` should also work.
|
||||
|
||||
### 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.
|
||||
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 the RGB value will be printed in the ESP32's console.
|
||||
|
||||
```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
|
||||
I (422) main_task: Calling app_main()
|
||||
I (422) mdns_mem: mDNS task will be created from internal RAM
|
||||
I (422) example_connect: Start example_connect.
|
||||
I (612) example_connect: Connecting to TP-LINK_CB59...
|
||||
I (622) example_connect: Waiting for IP(s)
|
||||
I (4792) esp_netif_handlers: example_netif_sta ip: 192.168.0.112, mask: 255.255.255.0, gw: 192.168.0.1
|
||||
I (4792) example_connect: Got IPv4 event: Interface "example_netif_sta" address: 192.168.0.112
|
||||
I (4792) example_common: Connected to example_netif_sta
|
||||
I (4802) example_common: - IPv4 address: 192.168.0.112,
|
||||
I (4832) example: Partition size: total: 2097152, used: 770048
|
||||
I (4832) esp-rest: Starting HTTP Server
|
||||
I (4832) main_task: Returned from app_main()
|
||||
I (49052) esp-rest: File sending complete
|
||||
I (67352) esp-rest: Light control: red = 160, green = 160, blue = 48
|
||||
```
|
||||
|
||||
## 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.)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
||||
not dead
|
||||
not ie 11
|
||||
|
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"Slot": true,
|
||||
"Slots": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"defineStore": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"storeToRefs": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useId": true,
|
||||
"useModel": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"useTemplateRef": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
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'
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
@@ -20,7 +21,5 @@ yarn-error.log*
|
||||
*.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
|
||||
# Ignore pnpm lock file
|
||||
pnpm-lock.yaml
|
||||
|
@@ -0,0 +1,44 @@
|
||||
# Dashboard
|
||||
|
||||
This is a modernized version of the dashboard web interface.
|
||||
|
||||
## Features
|
||||
|
||||
- **Home**: Display ESP system information (IDF version, CPU name)
|
||||
- **Chart**: Real-time temperature data visualization with spark-line chart
|
||||
- **Light**: RGB light control with color sliders
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Vue 3**: Modern Vue.js framework with Composition API
|
||||
- **Vuetify 3**: Material Design component library
|
||||
- **Alova**: Modern HTTP client with better performance and features
|
||||
- **Pinia**: State management
|
||||
- **Vite**: Fast build tool and development server
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
- **Tree-shaking for icons**: Only imports specific MDI icons used in the app (this is important for deploying web pages to embedded devices like ESP32)
|
||||
- **Modern bundle size**: Smaller bundle with Vite and modern dependencies
|
||||
- **Composition API**: Better performance and code organization
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The application expects the following endpoints from ESP32 device:
|
||||
|
||||
- `GET /api/v1/system/info` - System information
|
||||
- `GET /api/v1/temp/raw` - Temperature sensor data
|
||||
- `POST /api/v1/light/brightness` - Set RGB LED colors
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start development server
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
```
|
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
import vuetify from 'eslint-config-vuetify'
|
||||
|
||||
export default vuetify()
|
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
@@ -1,32 +1,35 @@
|
||||
{
|
||||
"name": "web-demo",
|
||||
"version": "0.1.0",
|
||||
"name": "dashboard",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^2.6.5",
|
||||
"vue": "^2.6.10",
|
||||
"vue-router": "^3.0.3",
|
||||
"vuetify": "^1.5.14",
|
||||
"vuex": "^3.0.1"
|
||||
"@fontsource/roboto": "5.2.6",
|
||||
"@mdi/js": "7.4.47",
|
||||
"alova": "3.3.4",
|
||||
"vue": "3.5.17",
|
||||
"vuetify": "3.9.2"
|
||||
},
|
||||
"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"
|
||||
"@vitejs/plugin-vue": "6.0.0",
|
||||
"eslint": "9.31.0",
|
||||
"eslint-config-vuetify": "4.0.0",
|
||||
"globals": "16.3.0",
|
||||
"pinia": "3.0.3",
|
||||
"sass-embedded": "1.89.2",
|
||||
"unplugin-auto-import": "19.3.0",
|
||||
"unplugin-fonts": "1.3.1",
|
||||
"unplugin-vue-components": "28.8.0",
|
||||
"unplugin-vue-router": "0.14.0",
|
||||
"vite": "7.0.5",
|
||||
"vite-plugin-vue-layouts-next": "1.0.0",
|
||||
"vite-plugin-vuetify": "2.1.1",
|
||||
"vue-router": "4.5.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,19 +0,0 @@
|
||||
<!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>
|
@@ -1,55 +1,9 @@
|
||||
<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>
|
||||
<router-view />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "App",
|
||||
data() {
|
||||
return {
|
||||
drawer: null
|
||||
};
|
||||
}
|
||||
};
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 223 KiB |
@@ -0,0 +1,35 @@
|
||||
# Components
|
||||
|
||||
Vue template files in this folder are automatically imported.
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
|
||||
|
||||
The following example assumes a component located at `src/components/MyComponent.vue`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
```
|
||||
|
||||
When your template is rendered, the component's import will automatically be inlined, which renders to this:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MyComponent from '@/components/MyComponent.vue'
|
||||
</script>
|
||||
```
|
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Composables for dashboard application
|
||||
*/
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { lightApi, systemApi, tempApi } from '@/services/api'
|
||||
|
||||
/**
|
||||
* Composable for system information
|
||||
*/
|
||||
export function useSystemInfo () {
|
||||
const systemInfo = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const fetchSystemInfo = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await systemApi.getInfo().send()
|
||||
systemInfo.value = response
|
||||
} catch (error_) {
|
||||
error.value = error_
|
||||
console.error('Failed to fetch system info:', error_)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
systemInfo,
|
||||
loading,
|
||||
error,
|
||||
fetchSystemInfo,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for temperature data polling
|
||||
*/
|
||||
export function useTemperaturePolling (interval = 1000) {
|
||||
const isPolling = ref(false)
|
||||
const error = ref(null)
|
||||
let timer = null
|
||||
|
||||
const startPolling = callback => {
|
||||
if (isPolling.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isPolling.value = true
|
||||
timer = setInterval(async () => {
|
||||
try {
|
||||
// Force fresh request by bypassing cache
|
||||
const response = await tempApi.getRaw().send({ force: true })
|
||||
callback(response.raw)
|
||||
error.value = null
|
||||
} catch (error_) {
|
||||
error.value = error_
|
||||
console.error('Failed to fetch temperature data:', error_)
|
||||
}
|
||||
}, interval)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
isPolling.value = false
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
return {
|
||||
isPolling,
|
||||
error,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for light control
|
||||
*/
|
||||
export function useLightControl () {
|
||||
// Load saved RGB values from localStorage or use defaults
|
||||
const savedRed = localStorage.getItem('lightControl.red')
|
||||
const savedGreen = localStorage.getItem('lightControl.green')
|
||||
const savedBlue = localStorage.getItem('lightControl.blue')
|
||||
|
||||
const red = ref(savedRed ? Number.parseInt(savedRed) : 160)
|
||||
const green = ref(savedGreen ? Number.parseInt(savedGreen) : 160)
|
||||
const blue = ref(savedBlue ? Number.parseInt(savedBlue) : 160)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Watch for changes and save to localStorage
|
||||
watch(red, newValue => {
|
||||
localStorage.setItem('lightControl.red', newValue.toString())
|
||||
})
|
||||
|
||||
watch(green, newValue => {
|
||||
localStorage.setItem('lightControl.green', newValue.toString())
|
||||
})
|
||||
|
||||
watch(blue, newValue => {
|
||||
localStorage.setItem('lightControl.blue', newValue.toString())
|
||||
})
|
||||
|
||||
const setColor = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
// Ensure RGB values are integers (0-255)
|
||||
const colorData = {
|
||||
red: Math.round(Math.max(0, Math.min(255, red.value || 0))),
|
||||
green: Math.round(Math.max(0, Math.min(255, green.value || 0))),
|
||||
blue: Math.round(Math.max(0, Math.min(255, blue.value || 0))),
|
||||
}
|
||||
console.log('Setting color:', colorData)
|
||||
const response = await lightApi.setBrightness(colorData).send()
|
||||
console.log('Light control response:', response)
|
||||
} catch (error_) {
|
||||
error.value = error_
|
||||
console.error('Failed to set color:', error_)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
loading,
|
||||
error,
|
||||
setColor,
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
# Layouts
|
||||
|
||||
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
|
||||
|
||||
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts-next](https://github.com/loicduong/vite-plugin-vue-layouts-next) repository.
|
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-navigation-drawer v-model="drawer" app clipped>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
prepend-icon="$home"
|
||||
title="Home"
|
||||
to="/"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="$chart-line"
|
||||
title="Chart"
|
||||
to="/chart"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="$lightbulb"
|
||||
title="Light"
|
||||
to="/light"
|
||||
/>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app clipped-left color="blue-grey-darken-3">
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer" />
|
||||
<v-app-bar-title>Dashboard</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<v-container fill-height fluid>
|
||||
<router-view />
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-footer app class="px-4 py-3" color="blue-grey-darken-4">
|
||||
<v-row align="center" justify="space-between" no-gutters>
|
||||
<v-col cols="auto">
|
||||
<span class="text-grey-lighten-3 text-body-2">
|
||||
Copyright © {{ new Date().getFullYear() }} Espressif Systems. All rights reserved.
|
||||
</span>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
class="mr-2"
|
||||
color="grey-lighten-3"
|
||||
href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/index.html"
|
||||
:icon="mdiBookOpenPageVariant"
|
||||
rel="noopener noreferrer"
|
||||
size="small"
|
||||
target="_blank"
|
||||
variant="text"
|
||||
/>
|
||||
<v-btn
|
||||
color="grey-lighten-3"
|
||||
href="https://github.com/espressif/esp-idf"
|
||||
:icon="mdiGithub"
|
||||
rel="noopener noreferrer"
|
||||
size="small"
|
||||
target="_blank"
|
||||
variant="text"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { mdiBookOpenPageVariant, mdiGithub } from '@mdi/js'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const drawer = ref(null)
|
||||
</script>
|
@@ -1,16 +1,23 @@
|
||||
import Vue from 'vue'
|
||||
import './plugins/vuetify'
|
||||
/**
|
||||
* main.js
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
import store from './store'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
// Styles
|
||||
import 'unfonts.css'
|
||||
|
||||
Vue.prototype.$ajax = axios
|
||||
const app = createApp(App)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
|
@@ -0,0 +1,5 @@
|
||||
# Pages
|
||||
|
||||
Vue components created in this folder will automatically be converted to navigatable routes.
|
||||
|
||||
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.
|
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" lg="8">
|
||||
<v-card class="pa-2">
|
||||
<v-card-title class="d-flex align-center py-2">
|
||||
<span class="text-h6">Temperature Chart</span>
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
:color="isPolling ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ isPolling ? 'Live' : 'Offline' }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text class="py-2">
|
||||
<v-sparkline
|
||||
auto-draw
|
||||
:gradient="['#f72047', '#ffd200', '#1feaea']"
|
||||
gradient-direction="top"
|
||||
height="150"
|
||||
:line-width="2"
|
||||
:model-value="chartStore.chartValue"
|
||||
:padding="4"
|
||||
:smooth="10"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mt-2"
|
||||
density="compact"
|
||||
dismissible
|
||||
type="error"
|
||||
@click:close="error = null"
|
||||
>
|
||||
Failed to fetch temperature data
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions class="py-2">
|
||||
<v-btn
|
||||
v-if="!isPolling"
|
||||
color="primary"
|
||||
size="default"
|
||||
@click="startDataPolling"
|
||||
>
|
||||
<v-icon :icon="mdiPlay" start />
|
||||
Start Monitoring
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
color="error"
|
||||
size="default"
|
||||
@click="stopPolling"
|
||||
>
|
||||
<v-icon :icon="mdiStop" start />
|
||||
Stop Monitoring
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { mdiPlay, mdiStop } from '@mdi/js'
|
||||
import { onMounted } from 'vue'
|
||||
import { useTemperaturePolling } from '@/composables/useApi'
|
||||
import { useChartStore } from '@/stores/chart'
|
||||
|
||||
const chartStore = useChartStore()
|
||||
const { isPolling, error, startPolling, stopPolling } = useTemperaturePolling(1000)
|
||||
|
||||
const startDataPolling = () => {
|
||||
startPolling(newValue => {
|
||||
chartStore.chartValue.push(newValue)
|
||||
chartStore.chartValue.shift()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startDataPolling()
|
||||
})
|
||||
</script>
|
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-card :loading="loading">
|
||||
<v-img
|
||||
contain
|
||||
height="200"
|
||||
:src="logoSrc"
|
||||
/>
|
||||
<v-card-title class="justify-center">
|
||||
<div class="text-center">
|
||||
<div class="text-grey">
|
||||
Chip: {{ systemInfo?.chip || 'Loading...' }}
|
||||
</div>
|
||||
<div class="text-grey">
|
||||
CPU cores: {{ systemInfo?.cores || 'Loading...' }}
|
||||
</div>
|
||||
<div class="text-grey">
|
||||
IDF version: {{ systemInfo?.idf_version || 'Loading...' }}
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="mt-4"
|
||||
dismissible
|
||||
type="error"
|
||||
@click:close="error = null"
|
||||
>
|
||||
Failed to load system information
|
||||
</v-alert>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="fetchSystemInfo"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import logoImage from '@/assets/logo.png'
|
||||
import { useSystemInfo } from '@/composables/useApi'
|
||||
|
||||
const { systemInfo, loading, error, fetchSystemInfo } = useSystemInfo()
|
||||
const logoSrc = logoImage
|
||||
|
||||
onMounted(() => {
|
||||
fetchSystemInfo()
|
||||
})
|
||||
</script>
|
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-card>
|
||||
<v-responsive
|
||||
class="d-flex align-center justify-center"
|
||||
height="300"
|
||||
:style="{ background: `rgb(${red}, ${green}, ${blue})` }"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3 class="text-white text-shadow">
|
||||
RGB({{ red }}, {{ green }}, {{ blue }})
|
||||
</h3>
|
||||
</div>
|
||||
</v-responsive>
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<div class="mb-4">
|
||||
<v-row align="center" no-gutters>
|
||||
<v-col class="text-body-1 font-weight-medium" cols="2">
|
||||
Red
|
||||
</v-col>
|
||||
<v-col class="px-3" cols="7">
|
||||
<v-slider
|
||||
v-model="red"
|
||||
color="red"
|
||||
hide-details
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
thumb-label
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="red"
|
||||
density="compact"
|
||||
hide-details
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
type="number"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<v-row align="center" no-gutters>
|
||||
<v-col class="text-body-1 font-weight-medium" cols="2">
|
||||
Green
|
||||
</v-col>
|
||||
<v-col class="px-3" cols="7">
|
||||
<v-slider
|
||||
v-model="green"
|
||||
color="green"
|
||||
hide-details
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
thumb-label
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="green"
|
||||
density="compact"
|
||||
hide-details
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
type="number"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<v-row align="center" no-gutters>
|
||||
<v-col class="text-body-1 font-weight-medium" cols="2">
|
||||
Blue
|
||||
</v-col>
|
||||
<v-col class="px-3" cols="7">
|
||||
<v-slider
|
||||
v-model="blue"
|
||||
color="blue"
|
||||
hide-details
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
thumb-label
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="blue"
|
||||
density="compact"
|
||||
hide-details
|
||||
:max="255"
|
||||
:min="0"
|
||||
:step="1"
|
||||
type="number"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
color="red-accent-4"
|
||||
:loading="loading"
|
||||
prepend-icon="$check"
|
||||
size="large"
|
||||
@click="setColor"
|
||||
>
|
||||
Apply Color
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
class="ma-4"
|
||||
dismissible
|
||||
type="error"
|
||||
@click:close="error = null"
|
||||
>
|
||||
Failed to set light color
|
||||
</v-alert>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useLightControl } from '@/composables/useApi'
|
||||
|
||||
const { red, green, blue, loading, error, setColor } = useLightControl()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-shadow {
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,3 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* plugins/alova.js
|
||||
*
|
||||
* Alova HTTP client configuration
|
||||
*/
|
||||
import { alova } from '@/services/api'
|
||||
|
||||
export default function alovaPlugin (app) {
|
||||
app.config.globalProperties.$alova = alova
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* plugins/index.js
|
||||
*
|
||||
* Automatically included in `./src/main.js`
|
||||
*/
|
||||
|
||||
import router from '@/router'
|
||||
import pinia from '@/stores'
|
||||
import alova from './alova'
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
|
||||
export function registerPlugins (app) {
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.use(alova)
|
||||
}
|
@@ -1,7 +1,45 @@
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib'
|
||||
import 'vuetify/src/stylus/app.styl'
|
||||
/**
|
||||
* plugins/vuetify.js
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
Vue.use(Vuetify, {
|
||||
iconfont: 'md',
|
||||
import {
|
||||
mdiChartLine,
|
||||
mdiCheck,
|
||||
mdiHome,
|
||||
mdiLightbulb,
|
||||
mdiMenu,
|
||||
} from '@mdi/js'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// Icons
|
||||
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
|
||||
// Styles
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Custom icon aliases for on-demand loading
|
||||
const customAliases = {
|
||||
...aliases,
|
||||
'home': mdiHome,
|
||||
'chart-line': mdiChartLine,
|
||||
'lightbulb': mdiLightbulb,
|
||||
'check': mdiCheck,
|
||||
'menu': mdiMenu,
|
||||
}
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
icons: {
|
||||
defaultSet: 'mdi',
|
||||
aliases: customAliases,
|
||||
sets: {
|
||||
mdi,
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
defaultTheme: 'light',
|
||||
},
|
||||
})
|
||||
|
@@ -1,29 +0,0 @@
|
||||
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,36 @@
|
||||
/**
|
||||
* router/index.ts
|
||||
*
|
||||
* Automatic routes for `./src/pages/*.vue`
|
||||
*/
|
||||
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: setupLayouts(routes),
|
||||
})
|
||||
|
||||
// Workaround for https://github.com/vitejs/vite/issues/11804
|
||||
router.onError((err, to) => {
|
||||
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
|
||||
if (localStorage.getItem('vuetify:dynamic-reload')) {
|
||||
console.error('Dynamic import error, reloading page did not fix it', err)
|
||||
} else {
|
||||
console.log('Reloading page to fix dynamic import error')
|
||||
localStorage.setItem('vuetify:dynamic-reload', 'true')
|
||||
location.assign(to.fullPath)
|
||||
}
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.isReady().then(() => {
|
||||
localStorage.removeItem('vuetify:dynamic-reload')
|
||||
})
|
||||
|
||||
export default router
|
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* API service using Alova
|
||||
*
|
||||
* CORS Solution: Use empty baseURL to leverage Vite proxy (no CORS issues)
|
||||
*
|
||||
* The Vite proxy in vite.config.mjs handles '/api' routes by forwarding them
|
||||
* to the ESP32, avoiding CORS issues during development.
|
||||
*/
|
||||
import { createAlova } from 'alova'
|
||||
import fetch from 'alova/fetch'
|
||||
import VueHook from 'alova/vue'
|
||||
|
||||
export const alova = createAlova({
|
||||
statesHook: VueHook,
|
||||
requestAdapter: fetch(),
|
||||
baseURL: '',
|
||||
beforeRequest (method) {
|
||||
// Minimize headers to avoid ESP32 431 error
|
||||
const essentialHeaders = {}
|
||||
|
||||
// Only add Content-Type for POST requests
|
||||
if (method.type === 'POST') {
|
||||
essentialHeaders['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
// Clear any existing headers and set only essential ones
|
||||
method.config.headers = essentialHeaders
|
||||
},
|
||||
responded: {
|
||||
onSuccess: async response => {
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`HTTP Error: ${response.status}`)
|
||||
}
|
||||
|
||||
// Check if response has content and is JSON
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
} else {
|
||||
// For non-JSON responses (like light control), return text or empty object
|
||||
const text = await response.text()
|
||||
console.log('Non-JSON response:', text)
|
||||
return text || { success: true }
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
console.error('API Error:', error)
|
||||
throw error
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// API endpoints
|
||||
export const systemApi = {
|
||||
getInfo: () => alova.Get('/api/v1/system/info'),
|
||||
}
|
||||
|
||||
export const tempApi = {
|
||||
getRaw: () => {
|
||||
// Create a fresh request each time with timestamp to prevent caching
|
||||
return alova.Get(`/api/v1/temp/raw?_t=${Date.now()}`, {
|
||||
localCache: 0, // Disable local cache
|
||||
hitSource: 'network', // Always fetch from network
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const lightApi = {
|
||||
setBrightness: data => alova.Post('/api/v1/light/brightness', data),
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
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,5 @@
|
||||
# Store
|
||||
|
||||
Pinia stores are used to store reactive state and expose actions to mutate it.
|
||||
|
||||
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.
|
@@ -0,0 +1,8 @@
|
||||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
//
|
||||
}),
|
||||
})
|
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Chart data store using Pinia
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { tempApi } from '@/services/api'
|
||||
|
||||
export const useChartStore = defineStore('chart', () => {
|
||||
const chartValue = ref([8, 2, 5, 9, 5, 11, 3, 5, 10, 0, 1, 8, 2, 9, 0, 13, 10, 7, 16])
|
||||
|
||||
const updateChartValue = async () => {
|
||||
try {
|
||||
const response = await tempApi.getRaw().send()
|
||||
chartValue.value.push(response.raw)
|
||||
chartValue.value.shift()
|
||||
} catch (error) {
|
||||
console.error('Failed to update chart value:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chartValue,
|
||||
updateChartValue,
|
||||
}
|
||||
})
|
@@ -0,0 +1,4 @@
|
||||
// Utilities
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export default createPinia()
|
@@ -0,0 +1,3 @@
|
||||
# Styles
|
||||
|
||||
This directory is for configuring the styles of the application.
|
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* src/styles/settings.scss
|
||||
*
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
// @use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
// );
|
@@ -1,41 +0,0 @@
|
||||
<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>
|
@@ -1,40 +0,0 @@
|
||||
<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>
|
@@ -1,62 +0,0 @@
|
||||
<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,145 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
// Plugins
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Fonts from 'unplugin-fonts/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
// Utilities
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import Layouts from 'vite-plugin-vue-layouts-next'
|
||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
VueRouter(),
|
||||
Layouts(),
|
||||
Vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify({
|
||||
autoImport: true,
|
||||
styles: {
|
||||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
Components(),
|
||||
Fonts({
|
||||
google: {
|
||||
families: [{
|
||||
name: 'Roboto',
|
||||
styles: 'wght@100;300;400;500;700;900',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
VueRouterAutoImports,
|
||||
{
|
||||
pinia: ['defineStore', 'storeToRefs'],
|
||||
},
|
||||
],
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
vueTemplate: true,
|
||||
}),
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'vuetify',
|
||||
'vue-router',
|
||||
'unplugin-vue-router/runtime',
|
||||
'unplugin-vue-router/data-loaders',
|
||||
'unplugin-vue-router/data-loaders/basic',
|
||||
],
|
||||
},
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('src', import.meta.url)),
|
||||
},
|
||||
extensions: [
|
||||
'.js',
|
||||
'.json',
|
||||
'.jsx',
|
||||
'.mjs',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
],
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
// Proxy configuration to avoid CORS issues during development
|
||||
// All requests to /api/* will be forwarded to the ESP32
|
||||
// IMPORTANT: Also strips large headers (especially cookies) to avoid HTTP 431 errors
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://dashboard.local', // Replace with your ESP32's actual IP
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
// Configure proxy to minimize headers for ESP32 compatibility
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
// CRITICAL: Remove cookies - this is often the largest header!
|
||||
proxyReq.removeHeader('cookie')
|
||||
|
||||
// Remove other large/unnecessary headers that might cause 431 error
|
||||
proxyReq.removeHeader('user-agent')
|
||||
proxyReq.removeHeader('accept-encoding')
|
||||
proxyReq.removeHeader('accept-language')
|
||||
proxyReq.removeHeader('cache-control')
|
||||
proxyReq.removeHeader('pragma')
|
||||
proxyReq.removeHeader('referer')
|
||||
proxyReq.removeHeader('origin')
|
||||
proxyReq.removeHeader('sec-fetch-dest')
|
||||
proxyReq.removeHeader('sec-fetch-mode')
|
||||
proxyReq.removeHeader('sec-fetch-site')
|
||||
proxyReq.removeHeader('sec-fetch-user')
|
||||
proxyReq.removeHeader('sec-ch-ua')
|
||||
proxyReq.removeHeader('sec-ch-ua-mobile')
|
||||
proxyReq.removeHeader('sec-ch-ua-platform')
|
||||
proxyReq.removeHeader('sec-ch-ua-full-version')
|
||||
proxyReq.removeHeader('sec-ch-ua-full-version-list')
|
||||
proxyReq.removeHeader('upgrade-insecure-requests')
|
||||
proxyReq.removeHeader('x-requested-with')
|
||||
proxyReq.removeHeader('dnt')
|
||||
proxyReq.removeHeader('te')
|
||||
|
||||
// Keep only absolutely essential headers
|
||||
proxyReq.setHeader('host', 'dashboard.local')
|
||||
proxyReq.setHeader('accept', 'application/json')
|
||||
|
||||
// For POST requests, keep content-type if it exists
|
||||
if (req.method === 'POST' && req.headers['content-type']) {
|
||||
proxyReq.setHeader('content-type', req.headers['content-type'])
|
||||
}
|
||||
|
||||
const finalHeaders = proxyReq.getHeaders()
|
||||
|
||||
// Log header info for monitoring ESP32 compatibility
|
||||
const headerSize = Object.entries(finalHeaders)
|
||||
.reduce((size, [key, value]) => size + key.length + value.length + 4, 0)
|
||||
console.log(`ESP32 Request: ${req.method} ${req.url} (${Object.keys(finalHeaders).length} headers, ~${headerSize} bytes)`)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
api: 'modern-compiler',
|
||||
},
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://esp-home.local:80',
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
idf_component_register(SRCS "esp_rest_main.c"
|
||||
"rest_server.c"
|
||||
PRIV_REQUIRES esp_http_server esp_driver_gpio fatfs json spiffs nvs_flash
|
||||
INCLUDE_DIRS ".")
|
||||
PRIV_REQUIRES esp_http_server json nvs_flash
|
||||
INCLUDE_DIRS ".")
|
||||
|
||||
if(CONFIG_EXAMPLE_WEB_DEPLOY_SF)
|
||||
if(CONFIG_EXAMPLE_DEPLOY_WEB_PAGES)
|
||||
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)
|
||||
littlefs_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}")
|
||||
message(FATAL_ERROR "'${WEB_SRC_DIR}/dist' doesn't exist. Please run 'pnpm build' under '${WEB_SRC_DIR}'")
|
||||
endif()
|
||||
endif()
|
||||
|
@@ -2,50 +2,21 @@ menu "Example Configuration"
|
||||
|
||||
config EXAMPLE_MDNS_HOST_NAME
|
||||
string "mDNS Host Name"
|
||||
default "esp-home"
|
||||
default "dashboard"
|
||||
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 EXAMPLE_WEB_DEPLOY_MODE
|
||||
prompt "Website deploy mode"
|
||||
default EXAMPLE_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 EXAMPLE_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 EXAMPLE_WEB_DEPLOY_SD
|
||||
depends on IDF_TARGET_ESP32
|
||||
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 EXAMPLE_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 EXAMPLE_WEB_DEPLOY_SEMIHOST
|
||||
config EXAMPLE_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 EXAMPLE_WEB_MOUNT_POINT
|
||||
string "Website mount point in VFS"
|
||||
default "/www"
|
||||
help
|
||||
Specify the mount point in VFS.
|
||||
|
||||
config EXAMPLE_DEPLOY_WEB_PAGES
|
||||
bool "Deploy web pages to device's filesystem"
|
||||
default n
|
||||
help
|
||||
If enabled, the example will deploy web pages to the device's filesystem.
|
||||
Ensure that the necessary files (html, css, js, etc.) are available in the `front/web-demo/dist` directory.
|
||||
endmenu
|
||||
|
@@ -1,17 +1,11 @@
|
||||
/* HTTP Restful API Server Example
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2010-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
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 "sdkconfig.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_vfs_semihost.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_event.h"
|
||||
@@ -19,24 +13,23 @@
|
||||
#include "mdns.h"
|
||||
#include "lwip/apps/netbiosns.h"
|
||||
#include "protocol_examples_common.h"
|
||||
#if CONFIG_EXAMPLE_WEB_DEPLOY_SD
|
||||
#include "driver/sdmmc_host.h"
|
||||
#endif
|
||||
|
||||
#define MDNS_INSTANCE "esp home web server"
|
||||
#define MDNS_INSTANCE "dashboard web server"
|
||||
#define MDNS_HOST_NAME CONFIG_EXAMPLE_MDNS_HOST_NAME
|
||||
#define WEB_PAGE_MOUNT_POINT_IN_FS CONFIG_EXAMPLE_WEB_MOUNT_POINT
|
||||
|
||||
static const char *TAG = "example";
|
||||
[[maybe_unused]] static const char *TAG = "example";
|
||||
|
||||
esp_err_t start_rest_server(const char *base_path);
|
||||
extern esp_err_t start_rest_server(const char *base_path);
|
||||
|
||||
static void initialise_mdns(void)
|
||||
{
|
||||
mdns_init();
|
||||
mdns_hostname_set(CONFIG_EXAMPLE_MDNS_HOST_NAME);
|
||||
mdns_hostname_set(MDNS_HOST_NAME);
|
||||
mdns_instance_name_set(MDNS_INSTANCE);
|
||||
|
||||
mdns_txt_item_t serviceTxtData[] = {
|
||||
{"board", "esp32"},
|
||||
{"chip", CONFIG_IDF_TARGET},
|
||||
{"path", "/"}
|
||||
};
|
||||
|
||||
@@ -44,84 +37,38 @@ static void initialise_mdns(void)
|
||||
sizeof(serviceTxtData) / sizeof(serviceTxtData[0])));
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_WEB_DEPLOY_SEMIHOST
|
||||
esp_err_t init_fs(void)
|
||||
{
|
||||
esp_err_t ret = esp_vfs_semihost_register(CONFIG_EXAMPLE_WEB_MOUNT_POINT);
|
||||
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_EXAMPLE_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 = {
|
||||
#if CONFIG_EXAMPLE_DEPLOY_WEB_PAGES
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = WEB_PAGE_MOUNT_POINT_IN_FS,
|
||||
.partition_label = "www",
|
||||
.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_EXAMPLE_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_EXAMPLE_WEB_DEPLOY_SF
|
||||
esp_err_t init_fs(void)
|
||||
{
|
||||
esp_vfs_spiffs_conf_t conf = {
|
||||
.base_path = CONFIG_EXAMPLE_WEB_MOUNT_POINT,
|
||||
.partition_label = NULL,
|
||||
.max_files = 5,
|
||||
.format_if_mount_failed = false
|
||||
};
|
||||
esp_err_t ret = esp_vfs_spiffs_register(&conf);
|
||||
esp_err_t ret = esp_vfs_littlefs_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");
|
||||
ESP_LOGE(TAG, "Failed to find LittleFS partition");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
|
||||
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_spiffs_info(NULL, &total, &used);
|
||||
ret = esp_littlefs_info(conf.partition_label, &total, &used);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
|
||||
ESP_LOGE(TAG, "Failed to get LittleFS partition information (%s)", esp_err_to_name(ret));
|
||||
esp_littlefs_format(conf.partition_label);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
|
||||
}
|
||||
#endif // CONFIG_EXAMPLE_DEPLOY_WEB_PAGES
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
@@ -130,9 +77,9 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
initialise_mdns();
|
||||
netbiosns_init();
|
||||
netbiosns_set_name(CONFIG_EXAMPLE_MDNS_HOST_NAME);
|
||||
netbiosns_set_name(MDNS_HOST_NAME);
|
||||
|
||||
ESP_ERROR_CHECK(example_connect());
|
||||
ESP_ERROR_CHECK(init_fs());
|
||||
ESP_ERROR_CHECK(start_rest_server(CONFIG_EXAMPLE_WEB_MOUNT_POINT));
|
||||
ESP_ERROR_CHECK(start_rest_server(WEB_PAGE_MOUNT_POINT_IN_FS));
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/mdns: "^1.0.3"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=5.0"
|
||||
espressif/mdns: "^1.8.0"
|
||||
joltwallet/littlefs: "^1.20.0"
|
||||
protocol_examples_common:
|
||||
path: ${IDF_PATH}/examples/common_components/protocol_examples_common
|
||||
|
@@ -1,30 +1,20 @@
|
||||
/* HTTP Restful API Server
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2010-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
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_chip_info.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_check.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)
|
||||
static const char *TAG = "esp-rest";
|
||||
|
||||
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
|
||||
#define SCRATCH_BUFSIZE (10240)
|
||||
@@ -36,6 +26,7 @@ typedef struct rest_server_context {
|
||||
|
||||
#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0)
|
||||
|
||||
#if CONFIG_EXAMPLE_DEPLOY_WEB_PAGES
|
||||
/* 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)
|
||||
{
|
||||
@@ -70,7 +61,7 @@ static esp_err_t rest_common_get_handler(httpd_req_t *req)
|
||||
}
|
||||
int fd = open(filepath, O_RDONLY, 0);
|
||||
if (fd == -1) {
|
||||
ESP_LOGE(REST_TAG, "Failed to open file : %s", filepath);
|
||||
ESP_LOGE(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;
|
||||
@@ -84,12 +75,12 @@ static esp_err_t rest_common_get_handler(httpd_req_t *req)
|
||||
/* 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);
|
||||
ESP_LOGE(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!");
|
||||
ESP_LOGE(TAG, "File sending failed!");
|
||||
/* Abort sending file */
|
||||
httpd_resp_sendstr_chunk(req, NULL);
|
||||
/* Respond with 500 Internal Server Error */
|
||||
@@ -100,11 +91,12 @@ static esp_err_t rest_common_get_handler(httpd_req_t *req)
|
||||
} while (read_bytes > 0);
|
||||
/* Close file after sending complete */
|
||||
close(fd);
|
||||
ESP_LOGI(REST_TAG, "File sending complete");
|
||||
ESP_LOGI(TAG, "File sending complete");
|
||||
/* Respond with an empty chunk to signal HTTP response completion */
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif // CONFIG_EXAMPLE_DEPLOY_WEB_PAGES
|
||||
|
||||
/* Simple handler for light brightness control */
|
||||
static esp_err_t light_brightness_post_handler(httpd_req_t *req)
|
||||
@@ -133,7 +125,7 @@ static esp_err_t light_brightness_post_handler(httpd_req_t *req)
|
||||
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);
|
||||
ESP_LOGI(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;
|
||||
@@ -146,7 +138,8 @@ static esp_err_t system_info_get_handler(httpd_req_t *req)
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
cJSON_AddStringToObject(root, "version", IDF_VER);
|
||||
cJSON_AddStringToObject(root, "chip", CONFIG_IDF_TARGET);
|
||||
cJSON_AddStringToObject(root, "idf_version", IDF_VER);
|
||||
cJSON_AddNumberToObject(root, "cores", chip_info.cores);
|
||||
const char *sys_info = cJSON_Print(root);
|
||||
httpd_resp_sendstr(req, sys_info);
|
||||
@@ -160,6 +153,7 @@ static esp_err_t temperature_data_get_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
// Note: we're simulating temperature data with a random number for demonstration purposes
|
||||
cJSON_AddNumberToObject(root, "raw", esp_random() % 20);
|
||||
const char *sys_info = cJSON_Print(root);
|
||||
httpd_resp_sendstr(req, sys_info);
|
||||
@@ -170,17 +164,18 @@ static esp_err_t temperature_data_get_handler(httpd_req_t *req)
|
||||
|
||||
esp_err_t start_rest_server(const char *base_path)
|
||||
{
|
||||
REST_CHECK(base_path, "wrong base path", err);
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_RETURN_ON_FALSE(base_path && strlen(base_path) < ESP_VFS_PATH_MAX, ESP_ERR_INVALID_ARG, TAG, "Invalid base path");
|
||||
rest_server_context_t *rest_context = calloc(1, sizeof(rest_server_context_t));
|
||||
REST_CHECK(rest_context, "No memory for rest context", err);
|
||||
ESP_RETURN_ON_FALSE(rest_context, ESP_ERR_NO_MEM, TAG, "No memory for rest context");
|
||||
strlcpy(rest_context->base_path, base_path, sizeof(rest_context->base_path));
|
||||
|
||||
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);
|
||||
ESP_LOGI(TAG, "Starting HTTP Server");
|
||||
ESP_GOTO_ON_ERROR(httpd_start(&server, &config), err, TAG, "Failed to start http server");
|
||||
|
||||
/* URI handler for fetching system info */
|
||||
httpd_uri_t system_info_get_uri = {
|
||||
@@ -209,6 +204,7 @@ esp_err_t start_rest_server(const char *base_path)
|
||||
};
|
||||
httpd_register_uri_handler(server, &light_brightness_post_uri);
|
||||
|
||||
#if CONFIG_EXAMPLE_DEPLOY_WEB_PAGES
|
||||
/* URI handler for getting web server files */
|
||||
httpd_uri_t common_get_uri = {
|
||||
.uri = "/*",
|
||||
@@ -217,10 +213,12 @@ esp_err_t start_rest_server(const char *base_path)
|
||||
.user_ctx = rest_context
|
||||
};
|
||||
httpd_register_uri_handler(server, &common_get_uri);
|
||||
#endif // CONFIG_EXAMPLE_DEPLOY_WEB_PAGES
|
||||
|
||||
return ESP_OK;
|
||||
err_start:
|
||||
free(rest_context);
|
||||
err:
|
||||
return ESP_FAIL;
|
||||
if (rest_context) {
|
||||
free(rest_context);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
phy_init, data, phy, 0xf000, 0x1000,
|
||||
factory, app, factory, 0x10000, 1M,
|
||||
www, data, spiffs, , 2M,
|
||||
www, data, littlefs, , 2M,
|
||||
|
|
@@ -1,5 +1,3 @@
|
||||
CONFIG_SPIFFS_OBJ_NAME_LEN=64
|
||||
CONFIG_FATFS_LFN_HEAP=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv"
|
||||
|
@@ -870,8 +870,6 @@ examples/protocols/http_server/advanced_tests/main/tests.c
|
||||
examples/protocols/http_server/async_handlers/main/main.c
|
||||
examples/protocols/http_server/captive_portal/main/main.c
|
||||
examples/protocols/http_server/persistent_sockets/main/main.c
|
||||
examples/protocols/http_server/restful_server/main/esp_rest_main.c
|
||||
examples/protocols/http_server/restful_server/main/rest_server.c
|
||||
examples/protocols/http_server/simple/main/main.c
|
||||
examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c
|
||||
examples/protocols/https_server/simple/main/main.c
|
||||
|
Reference in New Issue
Block a user