3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/.idea
|
||||||
|
sdkconfig.old
|
||||||
|
/managed_components
|
||||||
6
CMakeLists.txt
Normal file
6
CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# The following five lines of boilerplate have to be in your project's
|
||||||
|
# CMakeLists in this exact order for cmake to work correctly
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(odroid-remote-http)
|
||||||
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# ODROID Remote HTTP Power Manager (OWPM)
|
||||||
|
|
||||||
|
A web-based remote power and monitoring tool for ODROID Single Board Computers (SBCs), powered by an ESP32. This project provides a comprehensive web interface to control power, monitor real-time metrics, and access the serial console of your ODROID from any web browser on your local network.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Web-Based UI**: Modern, responsive interface built with Bootstrap and vanilla JavaScript.
|
||||||
|
- **Power Control**: Independently toggle Main (e.g., 12V) and USB (5V) power supplies.
|
||||||
|
- **Power Actions**: Remotely trigger Reset and Power On/Off actions.
|
||||||
|
- **Real-time Metrics**: Monitor voltage, current, and power consumption with a live-updating graph.
|
||||||
|
- **Interactive Serial Terminal**: Access your ODROID's serial console directly from the web UI via WebSockets.
|
||||||
|
- **Wi-Fi Management**:
|
||||||
|
- Scan and connect to Wi-Fi networks (STA mode).
|
||||||
|
- Enable Access Point mode (AP+STA) to connect directly to the device.
|
||||||
|
- Configure static IP settings.
|
||||||
|
- **System Info**: View device uptime and connection status.
|
||||||
|
- **Customizable Theme**: Switch between light and dark modes, with the preference saved locally.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have the following installed and configured on your system:
|
||||||
|
|
||||||
|
- **[ESP-IDF (Espressif IoT Development Framework)](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html)**: This project is developed and tested with ESP-IDF v5.x.
|
||||||
|
- **[Node.js and npm](https://nodejs.org/)**: Required to build the web application. Node.js LTS version (e.g., 18.x or later) is recommended.
|
||||||
|
|
||||||
|
## How to Build and Flash
|
||||||
|
|
||||||
|
1. **Clone the repository:**
|
||||||
|
```bash
|
||||||
|
git clone <your-repository-url>
|
||||||
|
cd odroid-remote-http
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build the Web Application:**
|
||||||
|
The web interface needs to be compiled before building the main firmware.
|
||||||
|
```bash
|
||||||
|
cd page
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set up the ESP-IDF environment:**
|
||||||
|
Open a terminal and source the ESP-IDF export script. The path may vary depending on your installation location.
|
||||||
|
```bash
|
||||||
|
. $HOME/esp/esp-idf/export.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set the target chip:**
|
||||||
|
Specify your ESP32 variant (e.g., `esp32`, `esp32s3`).
|
||||||
|
```bash
|
||||||
|
idf.py set-target esp32
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **(Optional) Configure the project:**
|
||||||
|
You can configure project-specific settings, such as default Wi-Fi credentials, by running `menuconfig`.
|
||||||
|
```bash
|
||||||
|
idf.py menuconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Build the project:**
|
||||||
|
This command compiles the application, bootloader, partition table, and embeds the compiled web page data.
|
||||||
|
```bash
|
||||||
|
idf.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Flash the firmware:**
|
||||||
|
Connect your ESP32 board to your computer and replace `/dev/ttyUSB0` with your device's serial port.
|
||||||
|
```bash
|
||||||
|
idf.py -p /dev/ttyUSB0 flash
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Monitor the output:**
|
||||||
|
To view the serial logs from the device, use the `monitor` command. This is useful for finding the device's IP address after it connects to your Wi-Fi.
|
||||||
|
```bash
|
||||||
|
idf.py -p /dev/ttyUSB0 monitor
|
||||||
|
```
|
||||||
|
To exit the monitor, press `Ctrl+]`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. After flashing, the ESP32 will either connect to the pre-configured Wi-Fi network or start an Access Point (AP).
|
||||||
|
2. Check the serial monitor logs to find the IP address assigned to the device in STA mode, or the default AP address (usually `192.168.4.1`).
|
||||||
|
3. Open a web browser and navigate to the device's IP address.
|
||||||
|
4. You should now see the ODROID Remote control panel.
|
||||||
59
dependencies.lock
Normal file
59
dependencies.lock
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
dependencies:
|
||||||
|
espressif/cmake_utilities:
|
||||||
|
component_hash: 05165f30922b422b4b90c08845e6d449329b97370fbd06309803d8cb539d79e3
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=4.1'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com
|
||||||
|
type: service
|
||||||
|
version: 1.1.1
|
||||||
|
espressif/led_indicator:
|
||||||
|
component_hash: 5b2531835a989825c0dc94465e3481086473e086dca109b99bea5605d8e70396
|
||||||
|
dependencies:
|
||||||
|
- name: espressif/cmake_utilities
|
||||||
|
registry_url: https://components.espressif.com
|
||||||
|
require: private
|
||||||
|
version: '*'
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=4.0'
|
||||||
|
- name: espressif/led_strip
|
||||||
|
registry_url: https://components.espressif.com
|
||||||
|
require: public
|
||||||
|
version: 2.5.5
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.1.1
|
||||||
|
espressif/led_strip:
|
||||||
|
component_hash: 28c6509a727ef74925b372ed404772aeedf11cce10b78c3f69b3c66799095e2d
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=4.4'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com
|
||||||
|
type: service
|
||||||
|
version: 2.5.5
|
||||||
|
idf:
|
||||||
|
source:
|
||||||
|
type: idf
|
||||||
|
version: 5.4.0
|
||||||
|
joltwallet/littlefs:
|
||||||
|
component_hash: 8e12955f47e27e6070b76715a96d6c75fc2b44f069e8c33679332d9bdd3120c4
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.20.1
|
||||||
|
direct_dependencies:
|
||||||
|
- espressif/led_indicator
|
||||||
|
- joltwallet/littlefs
|
||||||
|
manifest_hash: 445ef18c991ae952f2f16ffe06a905b6d1414a42286212d7b2459fa32945a09c
|
||||||
|
target: esp32c3
|
||||||
|
version: 2.0.0
|
||||||
209
docs/API.md
Normal file
209
docs/API.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# ODROID Remote API Documentation
|
||||||
|
|
||||||
|
This document outlines the HTTP REST and WebSocket APIs for communication between the web interface and the ESP32 device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WebSocket API
|
||||||
|
|
||||||
|
The WebSocket API provides a full-duplex communication channel for real-time data, such as sensor metrics and the interactive serial console.
|
||||||
|
|
||||||
|
**Endpoint**: `/ws`
|
||||||
|
|
||||||
|
### Server-to-Client Messages
|
||||||
|
|
||||||
|
The server pushes messages to the client, which can be either JSON objects or raw binary data. JSON messages always contain a `type` field to identify the payload.
|
||||||
|
|
||||||
|
#### JSON Messages
|
||||||
|
|
||||||
|
| Type | Description | Payload Example |
|
||||||
|
|---------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `sensor_data` | Pushed periodically (e.g., every second) with the latest power metrics. | `{"type":"sensor_data", "voltage":12.01, "current":1.52, "power":18.25, "uptime_sec":3600, "timestamp": 1672531200}` |
|
||||||
|
| `wifi_status` | Pushed periodically or on change to update the current Wi-Fi connection status. | `{"type":"wifi_status", "connected":true, "ssid":"MyHome_WiFi", "rssi":-65}` |
|
||||||
|
|
||||||
|
**Field Descriptions:**
|
||||||
|
- `sensor_data`:
|
||||||
|
- `voltage` (float): The measured voltage in Volts (V).
|
||||||
|
- `current` (float): The measured current in Amperes (A).
|
||||||
|
- `power` (float): The calculated power in Watts (W).
|
||||||
|
- `uptime_sec` (integer): The system uptime in seconds.
|
||||||
|
- `timestamp` (integer): A Unix timestamp (seconds) of when the measurement was taken.
|
||||||
|
- `wifi_status`:
|
||||||
|
- `connected` (boolean): `true` if connected to a Wi-Fi network, `false` otherwise.
|
||||||
|
- `ssid` (string): The SSID of the connected network. Null if not connected.
|
||||||
|
- `rssi` (integer): The Received Signal Strength Indicator in dBm. Null if not connected.
|
||||||
|
|
||||||
|
#### Raw Binary Data
|
||||||
|
|
||||||
|
- **Description**: Raw binary data from the ODROID's serial (UART) port is forwarded directly to the client. This is used to display the terminal output.
|
||||||
|
- **Payload**: `(binary data)`
|
||||||
|
|
||||||
|
### Client-to-Server Messages
|
||||||
|
|
||||||
|
The client primarily sends raw binary data, which is interpreted as terminal input.
|
||||||
|
|
||||||
|
- **Description**: Raw binary data representing user input from the web terminal. This data is forwarded directly to the ODROID's serial (UART) port.
|
||||||
|
- **Payload**: `(binary data)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP REST API
|
||||||
|
|
||||||
|
The REST API is used for configuration and to trigger specific actions. All request and response bodies are in `application/json` format.
|
||||||
|
|
||||||
|
### Endpoint: `/api/control`
|
||||||
|
|
||||||
|
Manages power relays and system actions.
|
||||||
|
|
||||||
|
#### `GET /api/control`
|
||||||
|
|
||||||
|
Retrieves the current status of the power relays.
|
||||||
|
|
||||||
|
- **Success Response (200 OK)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"load_12v_on": true,
|
||||||
|
"load_5v_on": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `load_12v_on` (boolean): The state of the main 12V power relay.
|
||||||
|
- `load_5v_on` (boolean): The state of the 5V USB power relay.
|
||||||
|
|
||||||
|
#### `POST /api/control`
|
||||||
|
|
||||||
|
Sets the state of power relays or triggers a power action. You can send one or more commands in a single request.
|
||||||
|
|
||||||
|
- **Request Body Examples**:
|
||||||
|
- To turn the main power on:
|
||||||
|
```json
|
||||||
|
{ "load_12v_on": true }
|
||||||
|
```
|
||||||
|
- To trigger a system reset:
|
||||||
|
```json
|
||||||
|
{ "reset_trigger": true }
|
||||||
|
```
|
||||||
|
- To toggle the power button:
|
||||||
|
```json
|
||||||
|
{ "power_trigger": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Request Fields**:
|
||||||
|
- `load_12v_on` (boolean, optional): Set the state of the 12V relay.
|
||||||
|
- `load_5v_on` (boolean, optional): Set the state of the 5V relay.
|
||||||
|
- `reset_trigger` (boolean, optional): If `true`, momentarily triggers the reset button.
|
||||||
|
- `power_trigger` (boolean, optional): If `true`, momentarily triggers the power button.
|
||||||
|
|
||||||
|
- **Success Response**: `204 No Content`
|
||||||
|
- **Error Response**: `400 Bad Request` if the request body is invalid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Endpoint: `/api/wifi`
|
||||||
|
|
||||||
|
Manages all Wi-Fi and network-related configurations.
|
||||||
|
|
||||||
|
#### `GET /api/wifi`
|
||||||
|
|
||||||
|
Retrieves the complete current network configuration.
|
||||||
|
|
||||||
|
- **Success Response (200 OK)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"connected": true,
|
||||||
|
"ssid": "MyHome_WiFi",
|
||||||
|
"mode": "apsta",
|
||||||
|
"net_type": "static",
|
||||||
|
"ip": {
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
"subnet": "255.255.255.0",
|
||||||
|
"dns1": "8.8.8.8",
|
||||||
|
"dns2": "8.8.4.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Response Fields**:
|
||||||
|
- `connected` (boolean): Current Wi-Fi connection state.
|
||||||
|
- `ssid` (string): The SSID of the connected network.
|
||||||
|
- `mode` (string): The current Wi-Fi mode (`"sta"` or `"apsta"`).
|
||||||
|
- `net_type` (string): The network type (`"dhcp"` or `"static"`).
|
||||||
|
- `ip` (object): Contains IP details. Present even if using DHCP (may show last-leased IP).
|
||||||
|
|
||||||
|
#### `POST /api/wifi`
|
||||||
|
|
||||||
|
This is a multi-purpose endpoint. The server determines the action based on the fields provided in the request body.
|
||||||
|
|
||||||
|
- **Action: Connect to a Wi-Fi Network**
|
||||||
|
- **Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ssid": "MyHome_WiFi",
|
||||||
|
"password": "my_secret_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Success Response (200 OK)**:
|
||||||
|
```json
|
||||||
|
{ "status": "connection_initiated" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Action: Configure Network Type (DHCP/Static)**
|
||||||
|
- **Request Body (for DHCP)**:
|
||||||
|
```json
|
||||||
|
{ "net_type": "dhcp" }
|
||||||
|
```
|
||||||
|
- **Request Body (for Static IP)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"net_type": "static",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
"subnet": "255.255.255.0",
|
||||||
|
"dns1": "8.8.8.8",
|
||||||
|
"dns2": "8.8.4.4"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Success Response**: `204 No Content`
|
||||||
|
|
||||||
|
- **Action: Configure Wi-Fi Mode (STA/APSTA)**
|
||||||
|
- **Request Body (for STA mode)**:
|
||||||
|
```json
|
||||||
|
{ "mode": "sta" }
|
||||||
|
```
|
||||||
|
- **Request Body (for AP+STA mode)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "apsta",
|
||||||
|
"ap_ssid": "ODROID-Remote-AP",
|
||||||
|
"ap_password": "hardkernel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Note: `ap_password` is optional. If omitted, the AP will be open.*
|
||||||
|
- **Success Response**: `204 No Content`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Endpoint: `/api/wifi/scan`
|
||||||
|
|
||||||
|
Scans for available Wi-Fi networks.
|
||||||
|
|
||||||
|
#### `GET /api/wifi/scan`
|
||||||
|
|
||||||
|
- **Success Response (200 OK)**: Returns a JSON array of found networks.
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ssid": "MyHome_WiFi",
|
||||||
|
"rssi": -55,
|
||||||
|
"authmode": "WPA2_PSK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ssid": "GuestNetwork",
|
||||||
|
"rssi": -78,
|
||||||
|
"authmode": "OPEN"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- **Response Fields**:
|
||||||
|
- `ssid` (string): The network's Service Set Identifier.
|
||||||
|
- `rssi` (integer): Signal strength in dBm.
|
||||||
|
- `authmode` (string): The authentication mode (e.g., `"OPEN"`, `"WPA_PSK"`, `"WPA2_PSK"`).
|
||||||
31
main/CMakeLists.txt
Normal file
31
main/CMakeLists.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Define the web application source directory and the final output file
|
||||||
|
set(WEB_APP_SOURCE_DIR ${CMAKE_SOURCE_DIR}/page)
|
||||||
|
set(GZ_OUTPUT_FILE ${WEB_APP_SOURCE_DIR}/dist/index.html.gz)
|
||||||
|
|
||||||
|
# Register the component. Now, CMake knows how GZ_OUTPUT_FILE is generated
|
||||||
|
# and can correctly handle the dependency for embedding.
|
||||||
|
idf_component_register(SRC_DIRS "app" "nconfig" "wifi" "indicator" "system" "service" "ina226"
|
||||||
|
INCLUDE_DIRS "include"
|
||||||
|
EMBED_FILES ${GZ_OUTPUT_FILE}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define a custom command to build the web app.
|
||||||
|
# This command explicitly tells CMake that it produces the GZ_OUTPUT_FILE.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${GZ_OUTPUT_FILE}
|
||||||
|
COMMAND npm install
|
||||||
|
COMMAND npm run build
|
||||||
|
WORKING_DIRECTORY ${WEB_APP_SOURCE_DIR}
|
||||||
|
# Re-run the build if package.json or vite.config.js changes
|
||||||
|
DEPENDS ${WEB_APP_SOURCE_DIR}/index.html ${WEB_APP_SOURCE_DIR}/src/main.js ${WEB_APP_SOURCE_DIR}/src/style.css
|
||||||
|
COMMENT "Building Node.js project to produce ${GZ_OUTPUT_FILE}"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a target that depends on the output file. When this target is built,
|
||||||
|
# it ensures the custom command above is executed first.
|
||||||
|
add_custom_target(build_web_app ALL
|
||||||
|
DEPENDS ${GZ_OUTPUT_FILE}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
82
main/Kconfig
Normal file
82
main/Kconfig
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
menu "ODROID-MONITOR"
|
||||||
|
menu "GPIO"
|
||||||
|
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
|
||||||
|
|
||||||
|
config GPIO_INA226_SCL
|
||||||
|
int "INA226 SCL GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 0
|
||||||
|
help
|
||||||
|
GPIO number for I2C Master data line.
|
||||||
|
|
||||||
|
config GPIO_INA226_SDA
|
||||||
|
int "INA226 SDA GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 1
|
||||||
|
help
|
||||||
|
GPIO number for I2C Master data line.
|
||||||
|
|
||||||
|
config GPIO_INA226_INT
|
||||||
|
int "INA226 ALERT GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 10
|
||||||
|
help
|
||||||
|
GPIO number for I2C Master data line.
|
||||||
|
|
||||||
|
config GPIO_UART_TX
|
||||||
|
int "UART TX GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 6
|
||||||
|
help
|
||||||
|
GPIO number for UART data line.
|
||||||
|
|
||||||
|
config GPIO_UART_RX
|
||||||
|
int "UART RX GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 7
|
||||||
|
help
|
||||||
|
GPIO number for UART data line.
|
||||||
|
|
||||||
|
config GPIO_LED_STATUS
|
||||||
|
int "Status LED GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 8
|
||||||
|
help
|
||||||
|
GPIO number for LED.
|
||||||
|
|
||||||
|
config GPIO_LED_WIFI
|
||||||
|
int "Wi-Fi LED GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 9
|
||||||
|
help
|
||||||
|
GPIO number for LED.
|
||||||
|
|
||||||
|
config GPIO_SW_12V
|
||||||
|
int "12v Load Switch GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 4
|
||||||
|
help
|
||||||
|
GPIO number for Load switch.
|
||||||
|
|
||||||
|
config GPIO_SW_5V
|
||||||
|
int "5v Load Switch GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 5
|
||||||
|
help
|
||||||
|
GPIO number for Load switch.
|
||||||
|
|
||||||
|
config GPIO_TRIGGER_POWER
|
||||||
|
int "Trigger power GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 2
|
||||||
|
help
|
||||||
|
GPIO number for Trigger.
|
||||||
|
|
||||||
|
config GPIO_TRIGGER_RESET
|
||||||
|
int "Trigger reset GPIO Num"
|
||||||
|
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||||
|
default 3
|
||||||
|
help
|
||||||
|
GPIO number for Trigger.
|
||||||
|
endmenu
|
||||||
|
endmenu
|
||||||
50
main/app/odroid-remote-http.c
Normal file
50
main/app/odroid-remote-http.c
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "indicator.h"
|
||||||
|
#include "nconfig.h"
|
||||||
|
#include "system.h"
|
||||||
|
#include "wifi.h"
|
||||||
|
|
||||||
|
#include "lwip/err.h"
|
||||||
|
#include "lwip/sockets.h"
|
||||||
|
#include "lwip/sys.h"
|
||||||
|
|
||||||
|
static const char *TAG = "odroid-remote";
|
||||||
|
|
||||||
|
void app_main(void) {
|
||||||
|
init_led();
|
||||||
|
led_set(LED_BLU, BLINK_TRIPLE);
|
||||||
|
led_off(LED_BLU);
|
||||||
|
|
||||||
|
// NVS 초기화
|
||||||
|
esp_err_t ret = nvs_flash_init();
|
||||||
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
|
ret = nvs_flash_init();
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(ret);
|
||||||
|
|
||||||
|
// 네트워크 초기화
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(init_nconfig());
|
||||||
|
|
||||||
|
// WiFi 연결
|
||||||
|
wifi_connect();
|
||||||
|
sync_time();
|
||||||
|
|
||||||
|
start_webserver();
|
||||||
|
}
|
||||||
3
main/idf_component.yml
Normal file
3
main/idf_component.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies:
|
||||||
|
espressif/led_indicator: ^1.1.1
|
||||||
|
joltwallet/littlefs: ==1.20.1
|
||||||
118
main/ina226/ina226.c
Normal file
118
main/ina226/ina226.c
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#include "ina226.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <esp_err.h>
|
||||||
|
|
||||||
|
#define INA226_REG_CONFIG (0x00)
|
||||||
|
#define INA226_REG_SHUNT_VOLTAGE (0x01)
|
||||||
|
#define INA226_REG_BUS_VOLTAGE (0x02)
|
||||||
|
#define INA226_REG_POWER (0x03)
|
||||||
|
#define INA226_REG_CURRENT (0x04)
|
||||||
|
#define INA226_REG_CALIBRATION (0x05)
|
||||||
|
#define INA226_REG_ALERT_MASK (0x06)
|
||||||
|
#define INA226_REG_ALERT_LIMIT (0x07)
|
||||||
|
#define INA226_REG_MANUFACTURER_ID (0xFE)
|
||||||
|
#define INA226_REG_DIE_ID (0xFF)
|
||||||
|
|
||||||
|
#define INA226_CFG_AVERAGING_OFFSET 9
|
||||||
|
#define INA226_CFG_BUS_VOLTAGE_OFFSET 6
|
||||||
|
#define INA226_CFG_SHUNT_VOLTAGE_OFFSET 3
|
||||||
|
|
||||||
|
static esp_err_t ina226_read_reg(ina226_t *handle, uint8_t reg_addr, uint16_t *data, size_t len)
|
||||||
|
{
|
||||||
|
return i2c_master_transmit_receive(handle->dev_handle, ®_addr, 1, (uint8_t *)data, len, handle->timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t ina226_write_reg(ina226_t *handle, uint8_t reg_addr, uint16_t value)
|
||||||
|
{
|
||||||
|
uint8_t write_buf[3] = {reg_addr, value >> 8, value & 0xFF};
|
||||||
|
return i2c_master_transmit(handle->dev_handle, write_buf, sizeof(write_buf), handle->timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_manufacturer_id(ina226_t *device, uint16_t *manufacturer_id)
|
||||||
|
{
|
||||||
|
return ina226_read_reg(device, INA226_REG_MANUFACTURER_ID, manufacturer_id, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_die_id(ina226_t *device, uint16_t *die_id)
|
||||||
|
{
|
||||||
|
return ina226_read_reg(device, INA226_REG_DIE_ID, die_id, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_shunt_voltage(ina226_t *device, float *voltage)
|
||||||
|
{
|
||||||
|
uint8_t data[2];
|
||||||
|
esp_err_t err = ina226_read_reg(device, INA226_REG_SHUNT_VOLTAGE, (uint16_t*)data, 2);
|
||||||
|
*voltage = (float) (data[0] << 8 | data[1]) * 2.5e-6f; /* fixed to 2.5 uV */
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_bus_voltage(ina226_t *device, float *voltage)
|
||||||
|
{
|
||||||
|
uint8_t data[2];
|
||||||
|
esp_err_t err = ina226_read_reg(device, INA226_REG_BUS_VOLTAGE, (uint16_t*)data, 2);
|
||||||
|
*voltage = (float) (data[0] << 8 | data[1]) * 0.00125f;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_current(ina226_t *device, float *current)
|
||||||
|
{
|
||||||
|
uint8_t data[2];
|
||||||
|
esp_err_t err = ina226_read_reg(device, INA226_REG_CURRENT, (uint16_t*)data, 2);
|
||||||
|
*current = ((float) (data[0] << 8 | data[1])) * device->current_lsb;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_power(ina226_t *device, float *power)
|
||||||
|
{
|
||||||
|
uint8_t data[2];
|
||||||
|
esp_err_t err = ina226_read_reg(device, INA226_REG_POWER, (uint16_t*)data, 2);
|
||||||
|
*power = (float) (data[0] << 8 | data[1]) * device->power_lsb;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_init(ina226_t *device, i2c_master_dev_handle_t dev_handle, const ina226_config_t *config)
|
||||||
|
{
|
||||||
|
esp_err_t err;
|
||||||
|
device->timeout_ms = config->timeout_ms;
|
||||||
|
device->dev_handle = dev_handle;
|
||||||
|
uint16_t bitmask = 0;
|
||||||
|
bitmask |= (config->averages << INA226_CFG_AVERAGING_OFFSET);
|
||||||
|
bitmask |= (config->bus_conv_time << INA226_CFG_BUS_VOLTAGE_OFFSET);
|
||||||
|
bitmask |= (config->shunt_conv_time << INA226_CFG_SHUNT_VOLTAGE_OFFSET);
|
||||||
|
bitmask |= config->mode;
|
||||||
|
err = ina226_write_reg(device, INA226_REG_CONFIG, bitmask);
|
||||||
|
if(err != ESP_OK) return err;
|
||||||
|
|
||||||
|
/* write calibration*/
|
||||||
|
float minimum_lsb = config->max_current / 32767;
|
||||||
|
float current_lsb = (uint16_t)(minimum_lsb * 100000000);
|
||||||
|
current_lsb /= 100000000;
|
||||||
|
current_lsb /= 0.0001;
|
||||||
|
current_lsb = ceil(current_lsb);
|
||||||
|
current_lsb *= 0.0001;
|
||||||
|
device->current_lsb = current_lsb;
|
||||||
|
device->power_lsb = current_lsb * 25;
|
||||||
|
uint16_t calibration_value = (uint16_t)((0.00512) / (current_lsb * config->r_shunt));
|
||||||
|
err = ina226_write_reg(device, INA226_REG_CALIBRATION, calibration_value);
|
||||||
|
if(err != ESP_OK) return err;
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_get_alert_mask(ina226_t *device, ina226_alert_t *alert_mask)
|
||||||
|
{
|
||||||
|
return ina226_read_reg(device, INA226_REG_ALERT_MASK, (uint16_t *)alert_mask, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_set_alert_mask(ina226_t *device, ina226_alert_t alert_mask)
|
||||||
|
{
|
||||||
|
return ina226_write_reg(device, INA226_REG_ALERT_MASK, (uint16_t)alert_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ina226_set_alert_limit(ina226_t *device, float voltage)
|
||||||
|
{
|
||||||
|
return ina226_write_reg(device, INA226_REG_ALERT_LIMIT, (uint16_t)(voltage));
|
||||||
|
}
|
||||||
105
main/include/ina226.h
Normal file
105
main/include/ina226.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#ifndef _INA226_H_
|
||||||
|
#define _INA226_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
INA226_AVERAGES_1 = 0b000,
|
||||||
|
INA226_AVERAGES_4 = 0b001,
|
||||||
|
INA226_AVERAGES_16 = 0b010,
|
||||||
|
INA226_AVERAGES_64 = 0b011,
|
||||||
|
INA226_AVERAGES_128 = 0b100,
|
||||||
|
INA226_AVERAGES_256 = 0b101,
|
||||||
|
INA226_AVERAGES_512 = 0b110,
|
||||||
|
INA226_AVERAGES_1024 = 0b111
|
||||||
|
} ina226_averages_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
INA226_BUS_CONV_TIME_140_US = 0b000,
|
||||||
|
INA226_BUS_CONV_TIME_204_US = 0b001,
|
||||||
|
INA226_BUS_CONV_TIME_332_US = 0b010,
|
||||||
|
INA226_BUS_CONV_TIME_588_US = 0b011,
|
||||||
|
INA226_BUS_CONV_TIME_1100_US = 0b100,
|
||||||
|
INA226_BUS_CONV_TIME_2116_US = 0b101,
|
||||||
|
INA226_BUS_CONV_TIME_4156_US = 0b110,
|
||||||
|
INA226_BUS_CONV_TIME_8244_US = 0b111
|
||||||
|
} ina226_bus_conv_time_t;
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
INA226_SHUNT_CONV_TIME_140_US = 0b000,
|
||||||
|
INA226_SHUNT_CONV_TIME_204_US = 0b001,
|
||||||
|
INA226_SHUNT_CONV_TIME_332_US = 0b010,
|
||||||
|
INA226_SHUNT_CONV_TIME_588_US = 0b011,
|
||||||
|
INA226_SHUNT_CONV_TIME_1100_US = 0b100,
|
||||||
|
INA226_SHUNT_CONV_TIME_2116_US = 0b101,
|
||||||
|
INA226_SHUNT_CONV_TIME_4156_US = 0b110,
|
||||||
|
INA226_SHUNT_CONV_TIME_8244_US = 0b111
|
||||||
|
} ina226_shunt_conv_time_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
|
||||||
|
INA226_MODE_POWER_DOWN = 0b000,
|
||||||
|
INA226_MODE_SHUNT_TRIG = 0b001,
|
||||||
|
INA226_MODE_BUS_TRIG = 0b010,
|
||||||
|
INA226_MODE_SHUNT_BUS_TRIG = 0b011,
|
||||||
|
INA226_MODE_ADC_OFF = 0b100,
|
||||||
|
INA226_MODE_SHUNT_CONT = 0b101,
|
||||||
|
INA226_MODE_BUS_CONT = 0b110,
|
||||||
|
INA226_MODE_SHUNT_BUS_CONT = 0b111,
|
||||||
|
} ina226_mode_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
INA226_ALERT_SHUNT_OVER_VOLTAGE = 0xf,
|
||||||
|
INA226_ALERT_SHUNT_UNDER_VOLTAGE = 0xe,
|
||||||
|
INA226_ALERT_BUS_OVER_VOLTAGE = 0xd,
|
||||||
|
INA226_ALERT_BUS_UNDER_VOLTAGE = 0xc,
|
||||||
|
INA226_ALERT_POWER_OVER_LIMIT = 0xb,
|
||||||
|
INA226_ALERT_CONVERSION_READY = 0xa,
|
||||||
|
INA226_ALERT_FUNCTION_FLAG = 0x4,
|
||||||
|
INA226_ALERT_CONVERSION_READY_FLAG = 0x3,
|
||||||
|
INA226_ALERT_MATH_OVERFLOW_FLAG = 0x2,
|
||||||
|
INA226_ALERT_POLARITY = 0x1,
|
||||||
|
INA226_ALERT_LATCH_ENABLE = 0x0
|
||||||
|
} ina226_alert_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
i2c_port_t i2c_port;
|
||||||
|
int i2c_addr;
|
||||||
|
int timeout_ms;
|
||||||
|
ina226_averages_t averages;
|
||||||
|
ina226_bus_conv_time_t bus_conv_time;
|
||||||
|
ina226_shunt_conv_time_t shunt_conv_time;
|
||||||
|
ina226_mode_t mode;
|
||||||
|
float r_shunt; /* ohm */
|
||||||
|
float max_current; /* amps */
|
||||||
|
} ina226_config_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
i2c_master_dev_handle_t dev_handle;
|
||||||
|
int timeout_ms;
|
||||||
|
float current_lsb;
|
||||||
|
float power_lsb;
|
||||||
|
} ina226_t;
|
||||||
|
|
||||||
|
esp_err_t ina226_get_manufacturer_id(ina226_t *device, uint16_t *manufacturer_id);
|
||||||
|
esp_err_t ina226_get_die_id(ina226_t *device, uint16_t *die_id);
|
||||||
|
esp_err_t ina226_get_shunt_voltage(ina226_t *device, float *voltage);
|
||||||
|
esp_err_t ina226_get_bus_voltage(ina226_t *device, float *voltage);
|
||||||
|
esp_err_t ina226_get_current(ina226_t *device, float *current);
|
||||||
|
esp_err_t ina226_get_power(ina226_t *device, float *power);
|
||||||
|
esp_err_t ina226_get_alert_mask(ina226_t *device, ina226_alert_t *alert_mask);
|
||||||
|
esp_err_t ina226_set_alert_mask(ina226_t *device, ina226_alert_t alert_mask);
|
||||||
|
esp_err_t ina226_set_alert_limit(ina226_t *device, float voltage);
|
||||||
|
esp_err_t ina226_init(ina226_t *device, i2c_master_dev_handle_t dev_handle, const ina226_config_t *config);
|
||||||
|
|
||||||
|
#endif
|
||||||
29
main/include/indicator.h
Normal file
29
main/include/indicator.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 7. 29.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef LED_H
|
||||||
|
#define LED_H
|
||||||
|
|
||||||
|
enum blink_type
|
||||||
|
{
|
||||||
|
BLINK_SLOW = 0,
|
||||||
|
BLINK_FAST,
|
||||||
|
BLINK_DOUBLE,
|
||||||
|
BLINK_TRIPLE,
|
||||||
|
BLINK_SOLID,
|
||||||
|
BLINK_MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum blink_led
|
||||||
|
{
|
||||||
|
LED_RED = 0,
|
||||||
|
LED_BLU = 1,
|
||||||
|
LED_MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
void init_led(void);
|
||||||
|
void led_set(enum blink_led led, enum blink_type type);
|
||||||
|
void led_off(enum blink_led led);
|
||||||
|
|
||||||
|
#endif //LED_H
|
||||||
46
main/include/nconfig.h
Normal file
46
main/include/nconfig.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 7. 10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef NCONFIG_H
|
||||||
|
#define NCONFIG_H
|
||||||
|
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#define NCONFIG_NVS_NAMESPACE "er"
|
||||||
|
#define NCONFIG_NOT_FOUND ESP_ERR_NVS_NOT_FOUND
|
||||||
|
|
||||||
|
esp_err_t init_nconfig();
|
||||||
|
|
||||||
|
enum nconfig_type
|
||||||
|
{
|
||||||
|
WIFI_SSID,
|
||||||
|
WIFI_PASSWORD,
|
||||||
|
WIFI_MODE,
|
||||||
|
AP_SSID,
|
||||||
|
AP_PASSWORD,
|
||||||
|
NETIF_HOSTNAME,
|
||||||
|
NETIF_IP,
|
||||||
|
NETIF_GATEWAY,
|
||||||
|
NETIF_SUBNET,
|
||||||
|
NETIF_DNS1,
|
||||||
|
NETIF_DNS2,
|
||||||
|
NETIF_TYPE,
|
||||||
|
UART_BAUD_RATE,
|
||||||
|
NCONFIG_TYPE_MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write config
|
||||||
|
esp_err_t nconfig_write(enum nconfig_type type, const char* data);
|
||||||
|
|
||||||
|
// Check config is set and get config value length
|
||||||
|
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t *len);
|
||||||
|
|
||||||
|
// Read config
|
||||||
|
esp_err_t nconfig_read(enum nconfig_type type, char* data, size_t len);
|
||||||
|
|
||||||
|
// Remove key
|
||||||
|
esp_err_t nconfig_delete(enum nconfig_type type);
|
||||||
|
|
||||||
|
#endif //NCONFIG_H
|
||||||
12
main/include/system.h
Normal file
12
main/include/system.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 8. 5.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef SYSTEM_H
|
||||||
|
#define SYSTEM_H
|
||||||
|
|
||||||
|
void start_reboot_timer(int sec);
|
||||||
|
void stop_reboot_timer();
|
||||||
|
void start_webserver();
|
||||||
|
|
||||||
|
#endif //SYSTEM_H
|
||||||
23
main/include/wifi.h
Normal file
23
main/include/wifi.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 7. 10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef WIFI_H
|
||||||
|
#define WIFI_H
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_netif_types.h"
|
||||||
|
#include "esp_wifi_types_generic.h"
|
||||||
|
|
||||||
|
const char* auth_mode_str(wifi_auth_mode_t mode);
|
||||||
|
esp_err_t wifi_connect(void);
|
||||||
|
esp_err_t wifi_disconnect(void);
|
||||||
|
void wifi_scan_aps(wifi_ap_record_t **ap_records, uint16_t* count);
|
||||||
|
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t *ap_info);
|
||||||
|
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t *ip_info);
|
||||||
|
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t *dns_info);
|
||||||
|
esp_err_t wifi_use_dhcp(void);
|
||||||
|
esp_err_t wifi_use_static(const char *ip, const char *gw, const char *netmask, const char *dns1, const char *dns2);
|
||||||
|
esp_err_t wifi_switch_mode(const char* mode);
|
||||||
|
void sync_time();
|
||||||
|
|
||||||
|
#endif //WIFI_H
|
||||||
109
main/indicator/indicator.c
Normal file
109
main/indicator/indicator.c
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 7. 29.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "indicator.h"
|
||||||
|
|
||||||
|
#include <led_indicator.h>
|
||||||
|
|
||||||
|
#define LED_STATUS_GPIO CONFIG_GPIO_LED_STATUS
|
||||||
|
#define LED_WIFI_GPIO CONFIG_GPIO_LED_WIFI
|
||||||
|
|
||||||
|
static const blink_step_t slow_blink[] = {
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 500},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 500},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 500},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 500},
|
||||||
|
{LED_BLINK_LOOP, 0, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const blink_step_t fast_blink[] = {
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 100},
|
||||||
|
{LED_BLINK_LOOP, 0, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const blink_step_t double_blink[] = {
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 500},
|
||||||
|
{LED_BLINK_LOOP, 0, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const blink_step_t triple_blink[] = {
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_OFF, 500},
|
||||||
|
{LED_BLINK_LOOP, 0, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const blink_step_t solid_blink[] = {
|
||||||
|
{LED_BLINK_HOLD, LED_STATE_ON, 100},
|
||||||
|
{LED_BLINK_LOOP, 0, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
blink_step_t const *led_mode[] = {
|
||||||
|
[BLINK_SLOW] = slow_blink,
|
||||||
|
[BLINK_FAST] = fast_blink,
|
||||||
|
[BLINK_DOUBLE] = double_blink,
|
||||||
|
[BLINK_TRIPLE] = triple_blink,
|
||||||
|
[BLINK_SOLID] = solid_blink,
|
||||||
|
[BLINK_MAX] = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
led_indicator_handle_t led_handle[LED_MAX] = {0};
|
||||||
|
int recent_type[LED_MAX] = {-1, -1};
|
||||||
|
|
||||||
|
void init_led(void)
|
||||||
|
{
|
||||||
|
led_indicator_ledc_config_t ledc_config = {0};
|
||||||
|
led_indicator_config_t config = {0};
|
||||||
|
|
||||||
|
ledc_config.is_active_level_high = true;
|
||||||
|
ledc_config.timer_inited = false;
|
||||||
|
ledc_config.timer_num = LEDC_TIMER_0;
|
||||||
|
ledc_config.gpio_num = LED_STATUS_GPIO;
|
||||||
|
ledc_config.channel = LEDC_CHANNEL_0;
|
||||||
|
|
||||||
|
config.mode = LED_LEDC_MODE;
|
||||||
|
config.led_indicator_ledc_config = &ledc_config;
|
||||||
|
config.blink_lists = led_mode;
|
||||||
|
config.blink_list_num = BLINK_MAX;
|
||||||
|
|
||||||
|
led_handle[LED_RED] = led_indicator_create(&config);
|
||||||
|
|
||||||
|
ledc_config.is_active_level_high = true;
|
||||||
|
ledc_config.timer_inited = false;
|
||||||
|
ledc_config.timer_num = LEDC_TIMER_0;
|
||||||
|
ledc_config.gpio_num = LED_WIFI_GPIO;
|
||||||
|
ledc_config.channel = LEDC_CHANNEL_1;
|
||||||
|
|
||||||
|
config.mode = LED_LEDC_MODE;
|
||||||
|
config.led_indicator_ledc_config = &ledc_config;
|
||||||
|
config.blink_lists = led_mode;
|
||||||
|
config.blink_list_num = BLINK_MAX;
|
||||||
|
|
||||||
|
led_handle[LED_BLU] = led_indicator_create(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void led_set(enum blink_led led, enum blink_type type)
|
||||||
|
{
|
||||||
|
if (recent_type[led] != -1)
|
||||||
|
led_indicator_stop(led_handle[led], recent_type[led]);
|
||||||
|
|
||||||
|
recent_type[led] = type;
|
||||||
|
led_indicator_start(led_handle[led], type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void led_off(enum blink_led led)
|
||||||
|
{
|
||||||
|
if (recent_type[led] != -1)
|
||||||
|
led_indicator_stop(led_handle[led], recent_type[led]);
|
||||||
|
recent_type[led] = -1;
|
||||||
|
}
|
||||||
85
main/nconfig/nconfig.c
Normal file
85
main/nconfig/nconfig.c
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 7. 10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "nconfig.h"
|
||||||
|
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
static nvs_handle_t handle;
|
||||||
|
|
||||||
|
const static char *keys[NCONFIG_TYPE_MAX] = {
|
||||||
|
[WIFI_SSID] = "wifi_ssid",
|
||||||
|
[WIFI_PASSWORD] = "wifi_pw",
|
||||||
|
[WIFI_MODE] = "wifi_mode",
|
||||||
|
[AP_SSID] = "ap_ssid",
|
||||||
|
[AP_PASSWORD] = "ap_pw",
|
||||||
|
[NETIF_HOSTNAME] = "hostname",
|
||||||
|
[NETIF_IP] = "ip",
|
||||||
|
[NETIF_GATEWAY] = "gw",
|
||||||
|
[NETIF_SUBNET] = "sn",
|
||||||
|
[NETIF_DNS1] = "dns1",
|
||||||
|
[NETIF_DNS2] = "dns2",
|
||||||
|
[NETIF_TYPE] = "dhcp",
|
||||||
|
[UART_BAUD_RATE] = "baudrate",
|
||||||
|
};
|
||||||
|
|
||||||
|
struct default_value {
|
||||||
|
enum nconfig_type type;
|
||||||
|
const char *value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct default_value const default_values[] = {
|
||||||
|
{WIFI_SSID, "HK_BOB_24G"},
|
||||||
|
{WIFI_PASSWORD, ""},
|
||||||
|
{NETIF_TYPE, "dhcp"},
|
||||||
|
{NETIF_HOSTNAME, "odroid"},
|
||||||
|
{UART_BAUD_RATE, "1500000"},
|
||||||
|
{NETIF_DNS1, "8.8.8.8"},
|
||||||
|
{NETIF_DNS2, "8.8.4.4"},
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t init_nconfig()
|
||||||
|
{
|
||||||
|
esp_err_t ret = nvs_open(NCONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||||
|
if (ret != ESP_OK) return ret;
|
||||||
|
|
||||||
|
for (int i = 0; i < sizeof(default_values) / sizeof(default_values[0]); ++i) {
|
||||||
|
// check key is not exist or value is null
|
||||||
|
size_t len = 0;
|
||||||
|
nconfig_get_str_len(default_values[i].type, &len);
|
||||||
|
if (len <= 1) // nconfig_get_str_len return err or value is '\0'
|
||||||
|
{
|
||||||
|
if (nconfig_write(default_values[i].type, default_values[i].value) != ESP_OK) // if nconfig write fail, system panic
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t nconfig_write(enum nconfig_type type, const char* data)
|
||||||
|
{
|
||||||
|
return nvs_set_str(handle, keys[type], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t nconfig_delete(enum nconfig_type type)
|
||||||
|
{
|
||||||
|
return nvs_erase_key(handle, keys[type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t *len)
|
||||||
|
{
|
||||||
|
return nvs_get_str(handle, keys[type], NULL, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t nconfig_read(enum nconfig_type type, char* data, size_t len)
|
||||||
|
{
|
||||||
|
return nvs_get_str(handle, keys[type], data, &len);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t nconfig_read_bool(enum nconfig_type type, char* data, size_t len)
|
||||||
|
{
|
||||||
|
return nvs_get_str(handle, keys[type], data, &len);
|
||||||
|
}
|
||||||
192
main/service/control.c
Normal file
192
main/service/control.c
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
#include "webserver.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
|
||||||
|
static const char *TAG = "CONTROL";
|
||||||
|
|
||||||
|
// --- GPIO 핀 정의 ---
|
||||||
|
#define GPIO_12V_SWITCH CONFIG_GPIO_SW_12V
|
||||||
|
#define GPIO_5V_SWITCH CONFIG_GPIO_SW_5V
|
||||||
|
#define GPIO_POWER_TRIGGER CONFIG_GPIO_TRIGGER_POWER
|
||||||
|
#define GPIO_RESET_TRIGGER CONFIG_GPIO_TRIGGER_RESET
|
||||||
|
|
||||||
|
// --- 상태 변수, 뮤텍스 및 타이머 핸들 ---
|
||||||
|
static bool status_12v_on = false;
|
||||||
|
static bool status_5v_on = false;
|
||||||
|
static SemaphoreHandle_t state_mutex;
|
||||||
|
static esp_timer_handle_t power_trigger_timer;
|
||||||
|
static esp_timer_handle_t reset_trigger_timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 타이머 만료 시 GPIO를 다시 HIGH로 설정하는 콜백 함수
|
||||||
|
*/
|
||||||
|
static void trigger_off_callback(void* arg)
|
||||||
|
{
|
||||||
|
gpio_num_t gpio_pin = (int) arg;
|
||||||
|
gpio_set_level(gpio_pin, 1); // 핀을 다시 HIGH로 복구
|
||||||
|
ESP_LOGI(TAG, "GPIO %d trigger finished.", gpio_pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_gpio_switches()
|
||||||
|
{
|
||||||
|
gpio_set_level(GPIO_12V_SWITCH, status_12v_on);
|
||||||
|
gpio_set_level(GPIO_5V_SWITCH, status_5v_on);
|
||||||
|
ESP_LOGI(TAG, "Switches updated: 12V=%s, 5V=%s", status_12v_on ? "ON" : "OFF", status_5v_on ? "ON" : "OFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t control_get_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
|
||||||
|
xSemaphoreTake(state_mutex, portMAX_DELAY);
|
||||||
|
cJSON_AddBoolToObject(root, "load_12v_on", status_12v_on);
|
||||||
|
cJSON_AddBoolToObject(root, "load_5v_on", status_5v_on);
|
||||||
|
xSemaphoreGive(state_mutex);
|
||||||
|
|
||||||
|
char *json_string = cJSON_Print(root);
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_send(req, json_string, strlen(json_string));
|
||||||
|
|
||||||
|
free(json_string);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t control_post_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
char buf[128];
|
||||||
|
int ret, remaining = req->content_len;
|
||||||
|
|
||||||
|
if (remaining >= sizeof(buf)) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Request content too long");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = httpd_req_recv(req, buf, remaining);
|
||||||
|
if (ret <= 0) {
|
||||||
|
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||||
|
httpd_resp_send_408(req);
|
||||||
|
}
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
buf[ret] = '\0';
|
||||||
|
ESP_LOGI(TAG, "Received JSON: %s", buf);
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(buf);
|
||||||
|
if (root == NULL) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON format");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool state_changed = false;
|
||||||
|
xSemaphoreTake(state_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
|
cJSON *item_12v = cJSON_GetObjectItem(root, "load_12v_on");
|
||||||
|
if (cJSON_IsBool(item_12v)) {
|
||||||
|
status_12v_on = cJSON_IsTrue(item_12v);
|
||||||
|
state_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *item_5v = cJSON_GetObjectItem(root, "load_5v_on");
|
||||||
|
if (cJSON_IsBool(item_5v)) {
|
||||||
|
status_5v_on = cJSON_IsTrue(item_5v);
|
||||||
|
state_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_changed) {
|
||||||
|
update_gpio_switches();
|
||||||
|
}
|
||||||
|
xSemaphoreGive(state_mutex);
|
||||||
|
|
||||||
|
cJSON *power_trigger = cJSON_GetObjectItem(root, "power_trigger");
|
||||||
|
if (cJSON_IsTrue(power_trigger)) {
|
||||||
|
ESP_LOGI(TAG, "Triggering GPIO %d LOW for 3 seconds...", GPIO_POWER_TRIGGER);
|
||||||
|
gpio_set_level(GPIO_POWER_TRIGGER, 0);
|
||||||
|
esp_timer_stop(power_trigger_timer); // Stop timer if it's already running
|
||||||
|
ESP_ERROR_CHECK(esp_timer_start_once(power_trigger_timer, 3000000)); // 3초
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *reset_trigger = cJSON_GetObjectItem(root, "reset_trigger");
|
||||||
|
if (cJSON_IsTrue(reset_trigger)) {
|
||||||
|
ESP_LOGI(TAG, "Triggering GPIO %d LOW for 3 seconds...", GPIO_RESET_TRIGGER);
|
||||||
|
gpio_set_level(GPIO_RESET_TRIGGER, 0);
|
||||||
|
esp_timer_stop(reset_trigger_timer); // Stop timer if it's already running
|
||||||
|
ESP_ERROR_CHECK(esp_timer_start_once(reset_trigger_timer, 3000000)); // 3초
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
|
||||||
|
httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void control_module_init(void)
|
||||||
|
{
|
||||||
|
state_mutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
gpio_config_t switch_conf = {
|
||||||
|
.pin_bit_mask = (1ULL << GPIO_12V_SWITCH) | (1ULL << GPIO_5V_SWITCH),
|
||||||
|
.mode = GPIO_MODE_OUTPUT,
|
||||||
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_DISABLE,
|
||||||
|
};
|
||||||
|
gpio_config(&switch_conf);
|
||||||
|
update_gpio_switches();
|
||||||
|
|
||||||
|
gpio_config_t trigger_conf = {
|
||||||
|
.pin_bit_mask = (1ULL << GPIO_POWER_TRIGGER) | (1ULL << GPIO_RESET_TRIGGER),
|
||||||
|
.mode = GPIO_MODE_OUTPUT,
|
||||||
|
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_DISABLE,
|
||||||
|
};
|
||||||
|
gpio_config(&trigger_conf);
|
||||||
|
gpio_set_level(GPIO_POWER_TRIGGER, 1);
|
||||||
|
gpio_set_level(GPIO_RESET_TRIGGER, 1);
|
||||||
|
|
||||||
|
const esp_timer_create_args_t power_timer_args = {
|
||||||
|
.callback = &trigger_off_callback,
|
||||||
|
.arg = (void*) GPIO_POWER_TRIGGER,
|
||||||
|
.name = "power_trigger_off"
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(esp_timer_create(&power_timer_args, &power_trigger_timer));
|
||||||
|
|
||||||
|
const esp_timer_create_args_t reset_timer_args = {
|
||||||
|
.callback = &trigger_off_callback,
|
||||||
|
.arg = (void*) GPIO_RESET_TRIGGER,
|
||||||
|
.name = "reset_trigger_off"
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(esp_timer_create(&reset_timer_args, &reset_trigger_timer));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Control module initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_control_endpoint(httpd_handle_t server)
|
||||||
|
{
|
||||||
|
control_module_init();
|
||||||
|
httpd_uri_t get_uri = {
|
||||||
|
.uri = "/api/control",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = control_get_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &get_uri);
|
||||||
|
|
||||||
|
httpd_uri_t post_uri = {
|
||||||
|
.uri = "/api/control",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = control_post_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &post_uri);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Registered /api/control endpoints (GET, POST)");
|
||||||
|
}
|
||||||
175
main/service/datalog.c
Normal file
175
main/service/datalog.c
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#include "datalog.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include "esp_littlefs.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
static const char* TAG = "DATALOG";
|
||||||
|
static const char* LOG_FILE_PATH = "/littlefs/datalog.csv";
|
||||||
|
static const char* TEMP_LOG_FILE_PATH = "/littlefs/datalog.tmp";
|
||||||
|
#define MAX_LOG_SIZE (1024 * 1024)
|
||||||
|
|
||||||
|
void datalog_init(void)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Initializing DataLog with LittleFS");
|
||||||
|
|
||||||
|
esp_vfs_littlefs_conf_t conf = {
|
||||||
|
.base_path = "/littlefs",
|
||||||
|
.partition_label = "littlefs",
|
||||||
|
.format_if_mount_failed = true,
|
||||||
|
.dont_mount = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 LittleFS partition");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total = 0, used = 0;
|
||||||
|
ret = esp_littlefs_info(NULL, &total, &used);
|
||||||
|
if (ret != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to get LittleFS partition information (%s)", esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
FILE* f = fopen(LOG_FILE_PATH, "r");
|
||||||
|
if (f == NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Log file not found, creating new one.");
|
||||||
|
FILE* f_write = fopen(LOG_FILE_PATH, "w");
|
||||||
|
if (f_write == NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to create log file.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Add header
|
||||||
|
fprintf(f_write, "timestamp,voltage,current,power\n");
|
||||||
|
fclose(f_write);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Log file found.");
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void datalog_add(uint32_t timestamp, float voltage, float current, float power)
|
||||||
|
{
|
||||||
|
char new_line[100];
|
||||||
|
int new_line_len = snprintf(new_line, sizeof(new_line), "%lu,%.3f,%.3f,%.3f\n", timestamp, voltage, current, power);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
long size = 0;
|
||||||
|
if (stat(LOG_FILE_PATH, &st) == 0)
|
||||||
|
{
|
||||||
|
size = st.st_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size + new_line_len <= MAX_LOG_SIZE)
|
||||||
|
{
|
||||||
|
FILE* f = fopen(LOG_FILE_PATH, "a");
|
||||||
|
if (f == NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to open log file for appending.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fputs(new_line, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Log file is full. Rotating log file.");
|
||||||
|
FILE* f_read = fopen(LOG_FILE_PATH, "r");
|
||||||
|
if (f_read == NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Could not open log for reading");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* f_write = fopen(TEMP_LOG_FILE_PATH, "w");
|
||||||
|
if (f_write == NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Could not open temp file for writing");
|
||||||
|
fclose(f_read);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long size_to_remove = (size + new_line_len) - MAX_LOG_SIZE;
|
||||||
|
char line[256];
|
||||||
|
|
||||||
|
// Keep header
|
||||||
|
if (fgets(line, sizeof(line), f_read) != NULL)
|
||||||
|
{
|
||||||
|
fputs(line, f_write);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Could not read header");
|
||||||
|
fclose(f_read);
|
||||||
|
fclose(f_write);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long bytes_skipped = 0;
|
||||||
|
while (fgets(line, sizeof(line), f_read) != NULL)
|
||||||
|
{
|
||||||
|
bytes_skipped += strlen(line);
|
||||||
|
if (bytes_skipped >= size_to_remove)
|
||||||
|
{
|
||||||
|
fputs(line, f_write);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fgets(line, sizeof(line), f_read) != NULL)
|
||||||
|
{
|
||||||
|
fputs(line, f_write);
|
||||||
|
}
|
||||||
|
|
||||||
|
fputs(new_line, f_write);
|
||||||
|
|
||||||
|
fclose(f_read);
|
||||||
|
fclose(f_write);
|
||||||
|
|
||||||
|
if (remove(LOG_FILE_PATH) != 0)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to remove old log file");
|
||||||
|
}
|
||||||
|
else if (rename(TEMP_LOG_FILE_PATH, LOG_FILE_PATH) != 0)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to rename temp file");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Log file rotated successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* datalog_get_path(void)
|
||||||
|
{
|
||||||
|
return LOG_FILE_PATH;
|
||||||
|
}
|
||||||
10
main/service/datalog.h
Normal file
10
main/service/datalog.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef MAIN_SERVICE_DATALOG_H_
|
||||||
|
#define MAIN_SERVICE_DATALOG_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
void datalog_init(void);
|
||||||
|
void datalog_add(uint32_t timestamp, float voltage, float current, float power);
|
||||||
|
const char* datalog_get_path(void);
|
||||||
|
|
||||||
|
#endif /* MAIN_SERVICE_DATALOG_H_ */
|
||||||
135
main/service/monitor.c
Normal file
135
main/service/monitor.c
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 8. 18..
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
#include "monitor.h"
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_wifi_types_generic.h"
|
||||||
|
#include "ina226.h"
|
||||||
|
#include "webserver.h"
|
||||||
|
#include "wifi.h"
|
||||||
|
#include "datalog.h"
|
||||||
|
|
||||||
|
#define INA226_SDA CONFIG_GPIO_INA226_SDA
|
||||||
|
#define INA226_SCL CONFIG_GPIO_INA226_SCL
|
||||||
|
|
||||||
|
ina226_t ina;
|
||||||
|
i2c_master_bus_handle_t bus_handle;
|
||||||
|
i2c_master_dev_handle_t dev_handle;
|
||||||
|
|
||||||
|
// Timer callback function to read sensor data
|
||||||
|
static void sensor_timer_callback(void *arg)
|
||||||
|
{
|
||||||
|
// Generate random sensor data
|
||||||
|
float voltage = 0;
|
||||||
|
float current = 0;
|
||||||
|
float power = 0;
|
||||||
|
|
||||||
|
ina226_get_bus_voltage(&ina, &voltage);
|
||||||
|
ina226_get_power(&ina, &power);
|
||||||
|
ina226_get_current(&ina, ¤t);
|
||||||
|
|
||||||
|
// Get system uptime
|
||||||
|
int64_t uptime_us = esp_timer_get_time();
|
||||||
|
uint32_t uptime_sec = (uint32_t)(uptime_us / 1000000);
|
||||||
|
uint32_t timestamp = (uint32_t)time(NULL);
|
||||||
|
|
||||||
|
datalog_add(timestamp, voltage, current, power);
|
||||||
|
|
||||||
|
// Create JSON object with sensor data
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(root, "type", "sensor_data");
|
||||||
|
cJSON_AddNumberToObject(root, "voltage", voltage);
|
||||||
|
cJSON_AddNumberToObject(root, "current", current);
|
||||||
|
cJSON_AddNumberToObject(root, "power", power);
|
||||||
|
cJSON_AddNumberToObject(root, "timestamp", timestamp);
|
||||||
|
cJSON_AddNumberToObject(root, "uptime_sec", uptime_sec);
|
||||||
|
|
||||||
|
// Push data to WebSocket clients
|
||||||
|
push_data_to_ws(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void status_wifi_callback(void *arg)
|
||||||
|
{
|
||||||
|
wifi_ap_record_t ap_info;
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
|
||||||
|
if (wifi_get_current_ap_info(&ap_info) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(root, "type", "wifi_status");
|
||||||
|
cJSON_AddBoolToObject(root, "connected", true);
|
||||||
|
cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid);
|
||||||
|
cJSON_AddNumberToObject(root, "rssi", ap_info.rssi);
|
||||||
|
} else {
|
||||||
|
cJSON_AddBoolToObject(root, "connected", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
push_data_to_ws(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
ina226_config_t ina_config = {
|
||||||
|
.i2c_port = I2C_NUM_0,
|
||||||
|
.i2c_addr = 0x40,
|
||||||
|
.timeout_ms = 100,
|
||||||
|
.averages = INA226_AVERAGES_16,
|
||||||
|
.bus_conv_time = INA226_BUS_CONV_TIME_1100_US,
|
||||||
|
.shunt_conv_time = INA226_SHUNT_CONV_TIME_1100_US,
|
||||||
|
.mode = INA226_MODE_SHUNT_BUS_CONT,
|
||||||
|
.r_shunt = 0.01f,
|
||||||
|
.max_current = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
static void init_ina226()
|
||||||
|
{
|
||||||
|
i2c_master_bus_config_t bus_config = {
|
||||||
|
.i2c_port = I2C_NUM_0,
|
||||||
|
.sda_io_num = (gpio_num_t) INA226_SDA,
|
||||||
|
.scl_io_num = (gpio_num_t) INA226_SCL,
|
||||||
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||||
|
.glitch_ignore_cnt = 7,
|
||||||
|
.flags.enable_internal_pullup = false,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
|
||||||
|
|
||||||
|
i2c_device_config_t dev_config = {
|
||||||
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||||
|
.device_address = 0x40,
|
||||||
|
.scl_speed_hz = 400000,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &dev_config, &dev_handle));
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(ina226_init(&ina, dev_handle, &ina_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_timer_handle_t sensor_timer;
|
||||||
|
static esp_timer_handle_t wifi_status_timer;
|
||||||
|
|
||||||
|
void init_status_monitor()
|
||||||
|
{
|
||||||
|
init_ina226();
|
||||||
|
datalog_init();
|
||||||
|
|
||||||
|
// Timer configuration
|
||||||
|
const esp_timer_create_args_t sensor_timer_args = {
|
||||||
|
.callback = &sensor_timer_callback,
|
||||||
|
.name = "sensor_reading_timer" // Optional name for debugging
|
||||||
|
};
|
||||||
|
|
||||||
|
const esp_timer_create_args_t wifi_timer_args = {
|
||||||
|
.callback = &status_wifi_callback,
|
||||||
|
.name = "wifi_status_timer" // Optional name for debugging
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_timer_create(&sensor_timer_args, &sensor_timer));
|
||||||
|
ESP_ERROR_CHECK(esp_timer_create(&wifi_timer_args, &wifi_status_timer));
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_timer_start_periodic(sensor_timer, 1000000)); // 1sec
|
||||||
|
ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_status_timer, 1000000 * 5)); // 5s in microseconds
|
||||||
|
}
|
||||||
25
main/service/monitor.h
Normal file
25
main/service/monitor.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 8. 18..
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef ODROID_REMOTE_HTTP_MONITOR_H
|
||||||
|
#define ODROID_REMOTE_HTTP_MONITOR_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
|
||||||
|
// 버퍼에 저장할 데이터의 개수
|
||||||
|
#define SENSOR_BUFFER_SIZE 100
|
||||||
|
|
||||||
|
// 단일 센서 데이터를 저장하기 위한 구조체
|
||||||
|
typedef struct {
|
||||||
|
float voltage;
|
||||||
|
float current;
|
||||||
|
float power;
|
||||||
|
uint32_t timestamp; // 데이터를 읽은 시간 (부팅 후 ms)
|
||||||
|
} sensor_data_t;
|
||||||
|
|
||||||
|
void init_status_monitor();
|
||||||
|
|
||||||
|
#endif //ODROID_REMOTE_HTTP_MONITOR_H
|
||||||
249
main/service/setting.c
Normal file
249
main/service/setting.c
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
#include "webserver.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "nconfig.h"
|
||||||
|
#include "wifi.h"
|
||||||
|
#include "system.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
static const char *TAG = "webserver";
|
||||||
|
|
||||||
|
static esp_err_t setting_get_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
wifi_ap_record_t ap_info;
|
||||||
|
cJSON *root = cJSON_CreateObject();
|
||||||
|
|
||||||
|
char mode_buf[16];
|
||||||
|
if (nconfig_read(WIFI_MODE, mode_buf, sizeof(mode_buf)) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(root, "mode", mode_buf);
|
||||||
|
} else {
|
||||||
|
cJSON_AddStringToObject(root, "mode", "sta"); // Default to sta
|
||||||
|
}
|
||||||
|
|
||||||
|
char net_type_buf[16];
|
||||||
|
if (nconfig_read(NETIF_TYPE, net_type_buf, sizeof(net_type_buf)) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(root, "net_type", net_type_buf);
|
||||||
|
} else {
|
||||||
|
cJSON_AddStringToObject(root, "net_type", "dhcp"); // Default to dhcp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add baudrate to the response
|
||||||
|
char baud_buf[16];
|
||||||
|
if (nconfig_read(UART_BAUD_RATE, baud_buf, sizeof(baud_buf)) == ESP_OK) {
|
||||||
|
cJSON_AddStringToObject(root, "baudrate", baud_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wifi_get_current_ap_info(&ap_info) == ESP_OK) {
|
||||||
|
cJSON_AddBoolToObject(root, "connected", true);
|
||||||
|
cJSON_AddStringToObject(root, "ssid", (const char *)ap_info.ssid);
|
||||||
|
cJSON_AddNumberToObject(root, "rssi", ap_info.rssi);
|
||||||
|
|
||||||
|
esp_netif_ip_info_t ip_info;
|
||||||
|
cJSON* ip_obj = cJSON_CreateObject();
|
||||||
|
if (wifi_get_current_ip_info(&ip_info) == ESP_OK) {
|
||||||
|
char ip_str[16];
|
||||||
|
esp_ip4addr_ntoa(&ip_info.ip, ip_str, sizeof(ip_str));
|
||||||
|
cJSON_AddStringToObject(ip_obj, "ip", ip_str);
|
||||||
|
esp_ip4addr_ntoa(&ip_info.gw, ip_str, sizeof(ip_str));
|
||||||
|
cJSON_AddStringToObject(ip_obj, "gateway", ip_str);
|
||||||
|
esp_ip4addr_ntoa(&ip_info.netmask, ip_str, sizeof(ip_str));
|
||||||
|
cJSON_AddStringToObject(ip_obj, "subnet", ip_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_netif_dns_info_t dns_info;
|
||||||
|
char dns_str[16];
|
||||||
|
if (wifi_get_dns_info(ESP_NETIF_DNS_MAIN, &dns_info) == ESP_OK) {
|
||||||
|
esp_ip4addr_ntoa(&dns_info.ip.u_addr.ip4, dns_str, sizeof(dns_str));
|
||||||
|
cJSON_AddStringToObject(ip_obj, "dns1", dns_str);
|
||||||
|
}
|
||||||
|
if (wifi_get_dns_info(ESP_NETIF_DNS_BACKUP, &dns_info) == ESP_OK) {
|
||||||
|
esp_ip4addr_ntoa(&dns_info.ip.u_addr.ip4, dns_str, sizeof(dns_str));
|
||||||
|
cJSON_AddStringToObject(ip_obj, "dns2", dns_str);
|
||||||
|
}
|
||||||
|
cJSON_AddItemToObject(root, "ip", ip_obj);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cJSON_AddBoolToObject(root, "connected", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *json_string = cJSON_Print(root);
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_send(req, json_string, HTTPD_RESP_USE_STRLEN);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
free((void*)json_string);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_scan(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
wifi_ap_record_t *ap_records;
|
||||||
|
uint16_t count;
|
||||||
|
|
||||||
|
wifi_scan_aps(&ap_records, &count);
|
||||||
|
|
||||||
|
cJSON *root = cJSON_CreateArray();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
cJSON *ap_obj = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(ap_obj, "ssid", (const char *)ap_records[i].ssid);
|
||||||
|
cJSON_AddNumberToObject(ap_obj, "rssi", ap_records[i].rssi);
|
||||||
|
cJSON_AddStringToObject(ap_obj, "authmode", auth_mode_str(ap_records[i].authmode));
|
||||||
|
cJSON_AddItemToArray(root, ap_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0)
|
||||||
|
free(ap_records);
|
||||||
|
|
||||||
|
|
||||||
|
const char *json_string = cJSON_Print(root);
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_send(req, json_string, HTTPD_RESP_USE_STRLEN);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
free((void*)json_string);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t setting_post_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
char buf[512];
|
||||||
|
int received = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||||
|
|
||||||
|
if (received <= 0) {
|
||||||
|
if (received == HTTPD_SOCK_ERR_TIMEOUT) httpd_resp_send_408(req);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
buf[received] = '\0';
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(buf);
|
||||||
|
if (root == NULL) {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *mode_item = cJSON_GetObjectItem(root, "mode");
|
||||||
|
cJSON *net_type_item = cJSON_GetObjectItem(root, "net_type");
|
||||||
|
cJSON *ssid_item = cJSON_GetObjectItem(root, "ssid");
|
||||||
|
cJSON *baud_item = cJSON_GetObjectItem(root, "baudrate");
|
||||||
|
|
||||||
|
if (mode_item && cJSON_IsString(mode_item)) {
|
||||||
|
const char* mode = mode_item->valuestring;
|
||||||
|
ESP_LOGI(TAG, "Received mode switch request: %s", mode);
|
||||||
|
|
||||||
|
if (strcmp(mode, "sta") == 0 || strcmp(mode, "apsta") == 0) {
|
||||||
|
if (strcmp(mode, "apsta") == 0) {
|
||||||
|
cJSON *ap_ssid_item = cJSON_GetObjectItem(root, "ap_ssid");
|
||||||
|
cJSON *ap_pass_item = cJSON_GetObjectItem(root, "ap_password");
|
||||||
|
|
||||||
|
if (ap_ssid_item && cJSON_IsString(ap_ssid_item)) {
|
||||||
|
nconfig_write(AP_SSID, ap_ssid_item->valuestring);
|
||||||
|
} else {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "AP SSID required for APSTA mode");
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ap_pass_item && cJSON_IsString(ap_pass_item)) {
|
||||||
|
nconfig_write(AP_PASSWORD, ap_pass_item->valuestring);
|
||||||
|
} else {
|
||||||
|
nconfig_delete(AP_PASSWORD); // Open network
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_switch_mode(mode);
|
||||||
|
httpd_resp_sendstr(req, "{\"status\":\"mode_switch_initiated\"}");
|
||||||
|
} else {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid mode");
|
||||||
|
}
|
||||||
|
} else if (net_type_item && cJSON_IsString(net_type_item)) {
|
||||||
|
const char* type = net_type_item->valuestring;
|
||||||
|
ESP_LOGI(TAG, "Received network config: %s", type);
|
||||||
|
|
||||||
|
if (strcmp(type, "static") == 0) {
|
||||||
|
cJSON *ip_item = cJSON_GetObjectItem(root, "ip");
|
||||||
|
cJSON *gw_item = cJSON_GetObjectItem(root, "gateway");
|
||||||
|
cJSON *sn_item = cJSON_GetObjectItem(root, "subnet");
|
||||||
|
cJSON *d1_item = cJSON_GetObjectItem(root, "dns1");
|
||||||
|
cJSON *d2_item = cJSON_GetObjectItem(root, "dns2");
|
||||||
|
|
||||||
|
const char* ip = cJSON_IsString(ip_item) ? ip_item->valuestring : NULL;
|
||||||
|
const char* gw = cJSON_IsString(gw_item) ? gw_item->valuestring : NULL;
|
||||||
|
const char* sn = cJSON_IsString(sn_item) ? sn_item->valuestring : NULL;
|
||||||
|
const char* d1 = cJSON_IsString(d1_item) ? d1_item->valuestring : NULL;
|
||||||
|
const char* d2 = cJSON_IsString(d2_item) ? d2_item->valuestring : NULL;
|
||||||
|
|
||||||
|
if (ip && gw && sn && d1) {
|
||||||
|
nconfig_write(NETIF_TYPE, "static");
|
||||||
|
nconfig_write(NETIF_IP, ip);
|
||||||
|
nconfig_write(NETIF_GATEWAY, gw);
|
||||||
|
nconfig_write(NETIF_SUBNET, sn);
|
||||||
|
nconfig_write(NETIF_DNS1, d1);
|
||||||
|
if (d2) nconfig_write(NETIF_DNS2, d2); else nconfig_delete(NETIF_DNS2);
|
||||||
|
|
||||||
|
wifi_use_static(ip, gw, sn, d1, d2);
|
||||||
|
httpd_resp_sendstr(req, "{\"status\":\"static_config_applied\"}");
|
||||||
|
} else {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing static IP fields");
|
||||||
|
}
|
||||||
|
} else if (strcmp(type, "dhcp") == 0) {
|
||||||
|
nconfig_write(NETIF_TYPE, "dhcp");
|
||||||
|
wifi_use_dhcp();
|
||||||
|
httpd_resp_sendstr(req, "{\"status\":\"dhcp_config_applied\"}");
|
||||||
|
}
|
||||||
|
} else if (ssid_item && cJSON_IsString(ssid_item)) {
|
||||||
|
cJSON *pass_item = cJSON_GetObjectItem(root, "password");
|
||||||
|
if (cJSON_IsString(pass_item)) {
|
||||||
|
nconfig_write(WIFI_SSID, ssid_item->valuestring);
|
||||||
|
nconfig_write(WIFI_PASSWORD, pass_item->valuestring);
|
||||||
|
nconfig_write(NETIF_TYPE, "dhcp"); // Default to DHCP on new connection
|
||||||
|
|
||||||
|
httpd_resp_sendstr(req, "{\"status\":\"connection_initiated\"}");
|
||||||
|
wifi_disconnect();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
wifi_connect();
|
||||||
|
} else {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Password required");
|
||||||
|
}
|
||||||
|
} else if (baud_item && cJSON_IsString(baud_item)) {
|
||||||
|
const char* baudrate = baud_item->valuestring;
|
||||||
|
ESP_LOGI(TAG, "Received baudrate set request: %s", baudrate);
|
||||||
|
nconfig_write(UART_BAUD_RATE, baudrate);
|
||||||
|
change_baud_rate(strtol(baudrate, NULL, 10));
|
||||||
|
httpd_resp_sendstr(req, "{\"status\":\"baudrate_updated\"}");
|
||||||
|
} else {
|
||||||
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_wifi_endpoint(httpd_handle_t server)
|
||||||
|
{
|
||||||
|
httpd_uri_t status = {
|
||||||
|
.uri = "/api/setting",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = setting_get_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &status);
|
||||||
|
|
||||||
|
httpd_uri_t set = {
|
||||||
|
.uri = "/api/setting",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = setting_post_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &set);
|
||||||
|
|
||||||
|
httpd_uri_t scan = {
|
||||||
|
.uri = "/api/wifi/scan",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = wifi_scan,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &scan);
|
||||||
|
}
|
||||||
102
main/service/webserver.c
Normal file
102
main/service/webserver.c
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#include "webserver.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "indicator.h"
|
||||||
|
#include "nconfig.h"
|
||||||
|
#include "monitor.h"
|
||||||
|
#include "wifi.h"
|
||||||
|
#include "datalog.h"
|
||||||
|
|
||||||
|
#include "lwip/err.h"
|
||||||
|
#include "lwip/sockets.h"
|
||||||
|
#include "lwip/sys.h"
|
||||||
|
|
||||||
|
static const char *TAG = "WEBSERVER";
|
||||||
|
|
||||||
|
static esp_err_t index_handler(httpd_req_t *req) {
|
||||||
|
extern const unsigned char index_html_start[] asm("_binary_index_html_gz_start");
|
||||||
|
extern const unsigned char index_html_end[] asm("_binary_index_html_gz_end");
|
||||||
|
const size_t index_html_size = (index_html_end - index_html_start);
|
||||||
|
|
||||||
|
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
|
||||||
|
httpd_resp_set_type(req, "text/html");
|
||||||
|
httpd_resp_send(req, (const char *)index_html_start, index_html_size);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t datalog_download_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
const char *filepath = datalog_get_path();
|
||||||
|
FILE *f = fopen(filepath, "r");
|
||||||
|
if (f == NULL) {
|
||||||
|
ESP_LOGE(TAG, "Failed to open datalog file for reading");
|
||||||
|
httpd_resp_send_404(req);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_type(req, "text/csv");
|
||||||
|
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"datalog.csv\"");
|
||||||
|
|
||||||
|
char buffer[1024];
|
||||||
|
size_t bytes_read;
|
||||||
|
while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) {
|
||||||
|
if (httpd_resp_send_chunk(req, buffer, bytes_read) != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "File sending failed!");
|
||||||
|
fclose(f);
|
||||||
|
httpd_resp_send_chunk(req, NULL, 0);
|
||||||
|
httpd_resp_send_500(req);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
httpd_resp_send_chunk(req, NULL, 0);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP 서버 시작
|
||||||
|
void start_webserver(void) {
|
||||||
|
httpd_handle_t server = NULL;
|
||||||
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
|
config.stack_size = 1024 * 8;
|
||||||
|
config.max_uri_handlers = 10;
|
||||||
|
|
||||||
|
if (httpd_start(&server, &config) != ESP_OK) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index page
|
||||||
|
httpd_uri_t index = {
|
||||||
|
.uri = "/",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = index_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &index);
|
||||||
|
|
||||||
|
httpd_uri_t datalog_uri = {
|
||||||
|
.uri = "/datalog.csv",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = datalog_download_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &datalog_uri);
|
||||||
|
|
||||||
|
register_wifi_endpoint(server);
|
||||||
|
register_ws_endpoint(server);
|
||||||
|
register_control_endpoint(server);
|
||||||
|
init_status_monitor();
|
||||||
|
}
|
||||||
17
main/service/webserver.h
Normal file
17
main/service/webserver.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 8. 18..
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||||
|
#define ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "system.h"
|
||||||
|
|
||||||
|
void register_wifi_endpoint(httpd_handle_t server);
|
||||||
|
void register_ws_endpoint(httpd_handle_t server);
|
||||||
|
void register_control_endpoint(httpd_handle_t server);
|
||||||
|
void push_data_to_ws(cJSON *data);
|
||||||
|
esp_err_t change_baud_rate(int baud_rate);
|
||||||
|
|
||||||
|
#endif //ODROID_REMOTE_HTTP_WEBSERVER_H
|
||||||
178
main/service/ws.c
Normal file
178
main/service/ws.c
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 8. 18..
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "webserver.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "nconfig.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
|
||||||
|
#define UART_NUM UART_NUM_1
|
||||||
|
#define BUF_SIZE (4096)
|
||||||
|
#define RD_BUF_SIZE (BUF_SIZE)
|
||||||
|
#define UART_TX_PIN CONFIG_GPIO_UART_TX
|
||||||
|
#define UART_RX_PIN CONFIG_GPIO_UART_RX
|
||||||
|
|
||||||
|
static const char *TAG = "ws-uart";
|
||||||
|
|
||||||
|
static int client_fd = -1;
|
||||||
|
|
||||||
|
struct status_message
|
||||||
|
{
|
||||||
|
cJSON *data;
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueHandle_t status_queue;
|
||||||
|
|
||||||
|
// Status task
|
||||||
|
static void status_task(void *arg)
|
||||||
|
{
|
||||||
|
httpd_handle_t server = (httpd_handle_t)arg;
|
||||||
|
struct status_message msg;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
if (xQueueReceive(status_queue, &msg, portMAX_DELAY)) {
|
||||||
|
if (client_fd <= 0) continue;
|
||||||
|
|
||||||
|
char *json_string = cJSON_Print(msg.data);
|
||||||
|
httpd_ws_frame_t ws_pkt;
|
||||||
|
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||||
|
ws_pkt.payload = (uint8_t *)json_string;
|
||||||
|
ws_pkt.len = strlen(json_string);
|
||||||
|
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
|
||||||
|
esp_err_t err = httpd_ws_send_frame_async(server, client_fd, &ws_pkt);
|
||||||
|
free(json_string);
|
||||||
|
cJSON_Delete(msg.data);
|
||||||
|
|
||||||
|
if (err != ESP_OK)
|
||||||
|
{
|
||||||
|
// try close...
|
||||||
|
httpd_ws_frame_t close_frame = {
|
||||||
|
.final = true,
|
||||||
|
.fragmented = false,
|
||||||
|
.type = HTTPD_WS_TYPE_CLOSE,
|
||||||
|
.payload = NULL,
|
||||||
|
.len = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
httpd_ws_send_frame_async(server, client_fd, &close_frame);
|
||||||
|
client_fd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vTaskDelay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UART task
|
||||||
|
static void uart_read_task(void *arg) {
|
||||||
|
httpd_handle_t server = (httpd_handle_t)arg;
|
||||||
|
|
||||||
|
uint8_t data[RD_BUF_SIZE];
|
||||||
|
while (1) {
|
||||||
|
int len = uart_read_bytes(UART_NUM, data, RD_BUF_SIZE, 10 / portTICK_PERIOD_MS);
|
||||||
|
if (len > 0 && client_fd != -1) {
|
||||||
|
httpd_ws_frame_t ws_pkt;
|
||||||
|
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||||
|
ws_pkt.payload = data;
|
||||||
|
ws_pkt.len = len;
|
||||||
|
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
|
||||||
|
|
||||||
|
esp_err_t err = httpd_ws_send_frame_async(server, client_fd, &ws_pkt);
|
||||||
|
if (err != ESP_OK)
|
||||||
|
{
|
||||||
|
// try close...
|
||||||
|
httpd_ws_frame_t close_frame = {
|
||||||
|
.final = true,
|
||||||
|
.fragmented = false,
|
||||||
|
.type = HTTPD_WS_TYPE_CLOSE,
|
||||||
|
.payload = NULL,
|
||||||
|
.len = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
httpd_ws_send_frame_async(server, client_fd, &close_frame);
|
||||||
|
client_fd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vTaskDelay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 웹소켓 처리 핸들러
|
||||||
|
static esp_err_t ws_handler(httpd_req_t *req) {
|
||||||
|
if (req->method == HTTP_GET) {
|
||||||
|
ESP_LOGI(TAG, "Accept websocket connection");
|
||||||
|
client_fd = httpd_req_to_sockfd(req);
|
||||||
|
xQueueReset(status_queue);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_ws_frame_t ws_pkt;
|
||||||
|
uint8_t buf[BUF_SIZE];
|
||||||
|
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||||
|
ws_pkt.payload = buf;
|
||||||
|
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
|
||||||
|
|
||||||
|
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, BUF_SIZE);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "웹소켓 프레임 수신 실패");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
uart_write_bytes(UART_NUM, (const char *)ws_pkt.payload, ws_pkt.len);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void register_ws_endpoint(httpd_handle_t server)
|
||||||
|
{
|
||||||
|
size_t baud_rate_len;
|
||||||
|
|
||||||
|
nconfig_get_str_len(UART_BAUD_RATE, &baud_rate_len);
|
||||||
|
char buf[baud_rate_len];
|
||||||
|
nconfig_read(UART_BAUD_RATE, buf, baud_rate_len);
|
||||||
|
|
||||||
|
uart_config_t uart_config = {
|
||||||
|
.baud_rate = strtol(buf, NULL, 10),
|
||||||
|
.data_bits = UART_DATA_8_BITS,
|
||||||
|
.parity = UART_PARITY_DISABLE,
|
||||||
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
|
// .source_clk = UART_SCLK_APB,
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
|
||||||
|
ESP_ERROR_CHECK(uart_set_pin(UART_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
||||||
|
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 0, NULL, ESP_INTR_FLAG_IRAM));
|
||||||
|
|
||||||
|
httpd_uri_t ws = {
|
||||||
|
.uri = "/ws",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = ws_handler,
|
||||||
|
.user_ctx = NULL,
|
||||||
|
.is_websocket = true
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(server, &ws);
|
||||||
|
|
||||||
|
status_queue = xQueueCreate(10, sizeof(struct status_message));
|
||||||
|
|
||||||
|
xTaskCreate(uart_read_task, "uart_read_task", 1024*6, server, 8, NULL);
|
||||||
|
xTaskCreate(status_task, "status_task", 4096, server, 7, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void push_data_to_ws(cJSON *data)
|
||||||
|
{
|
||||||
|
struct status_message msg;
|
||||||
|
msg.data = data;
|
||||||
|
if (xQueueSend(status_queue, &msg, 10) != pdPASS)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "Queue full");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t change_baud_rate(int baud_rate)
|
||||||
|
{
|
||||||
|
return uart_set_baudrate(UART_NUM, baud_rate);
|
||||||
|
}
|
||||||
46
main/system/system.c
Normal file
46
main/system/system.c
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 8. 5.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <system.h>
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
static const char *TAG = "odroid";
|
||||||
|
int t = 0;
|
||||||
|
|
||||||
|
TaskHandle_t reboot_handle = NULL;
|
||||||
|
|
||||||
|
static void reboot_task(void *arg)
|
||||||
|
{
|
||||||
|
while (t > 0)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "ESP will reboot in [%d] sec..., If you want stop reboot, use command \"reboot -s\"", t);
|
||||||
|
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||||
|
--t;
|
||||||
|
}
|
||||||
|
esp_restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_reboot_timer(int sec)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (reboot_handle != NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "The reboot timer is already running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t = sec;
|
||||||
|
xTaskCreate(reboot_task, "reboot_task", 2048, NULL, 8, &reboot_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_reboot_timer()
|
||||||
|
{
|
||||||
|
if (reboot_handle == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vTaskDelete(reboot_handle);
|
||||||
|
}
|
||||||
550
main/wifi/wifi.c
Normal file
550
main/wifi/wifi.c
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
//
|
||||||
|
// Created by shinys on 25. 7. 10.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "wifi.h"
|
||||||
|
|
||||||
|
#include "nconfig.h"
|
||||||
|
#include "indicator.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <system.h>
|
||||||
|
#include <lwip/sockets.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_wifi_default.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_netif_sntp.h"
|
||||||
|
#include "rom/ets_sys.h"
|
||||||
|
|
||||||
|
static const char *TAG = "odroid";
|
||||||
|
#define MAX_RETRY 10
|
||||||
|
#define MAX_SCAN 20
|
||||||
|
|
||||||
|
|
||||||
|
const char* auth_mode_str(wifi_auth_mode_t mode)
|
||||||
|
{
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case WIFI_AUTH_OPEN:
|
||||||
|
return "OPEN";
|
||||||
|
case WIFI_AUTH_WEP:
|
||||||
|
return "WEP";
|
||||||
|
case WIFI_AUTH_WPA_PSK:
|
||||||
|
return "WPA_PSK";
|
||||||
|
case WIFI_AUTH_WPA2_PSK:
|
||||||
|
return "WPA2_PSK";
|
||||||
|
case WIFI_AUTH_WPA_WPA2_PSK:
|
||||||
|
return "WPA_WPA2_PSK";
|
||||||
|
case WIFI_AUTH_ENTERPRISE:
|
||||||
|
return "ENTERPRISE";
|
||||||
|
case WIFI_AUTH_WPA3_PSK:
|
||||||
|
return "WPA3_PSK";
|
||||||
|
case WIFI_AUTH_WPA2_WPA3_PSK:
|
||||||
|
return "WPA2_WPA3_PSK";
|
||||||
|
case WIFI_AUTH_WAPI_PSK:
|
||||||
|
return "WAPI_PSK";
|
||||||
|
case WIFI_AUTH_OWE:
|
||||||
|
return "OWE";
|
||||||
|
case WIFI_AUTH_WPA3_ENT_192:
|
||||||
|
return "WPA3_ENT_192";
|
||||||
|
case WIFI_AUTH_WPA3_EXT_PSK:
|
||||||
|
return "WPA3_EXT_PSK";
|
||||||
|
case WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE:
|
||||||
|
return "WPA3_EXT_PSK_MIXED_MODE";
|
||||||
|
case WIFI_AUTH_DPP:
|
||||||
|
return "DPP";
|
||||||
|
case WIFI_AUTH_WPA3_ENTERPRISE:
|
||||||
|
return "WPA3_ENTERPRISE";
|
||||||
|
case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
|
||||||
|
return "WPA2_WPA3_ENTERPRISE";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* wifi_reason_str(wifi_err_reason_t reason) {
|
||||||
|
switch (reason) {
|
||||||
|
case WIFI_REASON_UNSPECIFIED: return "UNSPECIFIED";
|
||||||
|
case WIFI_REASON_AUTH_EXPIRE: return "AUTH_EXPIRE";
|
||||||
|
case WIFI_REASON_AUTH_LEAVE: return "AUTH_LEAVE";
|
||||||
|
case WIFI_REASON_ASSOC_EXPIRE: return "ASSOC_EXPIRE";
|
||||||
|
case WIFI_REASON_ASSOC_TOOMANY: return "ASSOC_TOOMANY";
|
||||||
|
case WIFI_REASON_NOT_AUTHED: return "NOT_AUTHED";
|
||||||
|
case WIFI_REASON_NOT_ASSOCED: return "NOT_ASSOCED";
|
||||||
|
case WIFI_REASON_ASSOC_LEAVE: return "ASSOC_LEAVE";
|
||||||
|
case WIFI_REASON_ASSOC_NOT_AUTHED: return "ASSOC_NOT_AUTHED";
|
||||||
|
case WIFI_REASON_DISASSOC_PWRCAP_BAD: return "DISASSOC_PWRCAP_BAD";
|
||||||
|
case WIFI_REASON_DISASSOC_SUPCHAN_BAD: return "DISASSOC_SUPCHAN_BAD";
|
||||||
|
case WIFI_REASON_IE_INVALID: return "IE_INVALID";
|
||||||
|
case WIFI_REASON_MIC_FAILURE: return "MIC_FAILURE";
|
||||||
|
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: return "4WAY_HANDSHAKE_TIMEOUT";
|
||||||
|
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: return "GROUP_KEY_UPDATE_TIMEOUT";
|
||||||
|
case WIFI_REASON_IE_IN_4WAY_DIFFERS: return "IE_IN_4WAY_DIFFERS";
|
||||||
|
case WIFI_REASON_GROUP_CIPHER_INVALID: return "GROUP_CIPHER_INVALID";
|
||||||
|
case WIFI_REASON_PAIRWISE_CIPHER_INVALID: return "PAIRWISE_CIPHER_INVALID";
|
||||||
|
case WIFI_REASON_AKMP_INVALID: return "AKMP_INVALID";
|
||||||
|
case WIFI_REASON_UNSUPP_RSN_IE_VERSION: return "UNSUPP_RSN_IE_VERSION";
|
||||||
|
case WIFI_REASON_INVALID_RSN_IE_CAP: return "INVALID_RSN_IE_CAP";
|
||||||
|
case WIFI_REASON_802_1X_AUTH_FAILED: return "802_1X_AUTH_FAILED";
|
||||||
|
case WIFI_REASON_CIPHER_SUITE_REJECTED: return "CIPHER_SUITE_REJECTED";
|
||||||
|
case WIFI_REASON_INVALID_PMKID: return "INVALID_PMKID";
|
||||||
|
case WIFI_REASON_BEACON_TIMEOUT: return "BEACON_TIMEOUT";
|
||||||
|
case WIFI_REASON_NO_AP_FOUND: return "NO_AP_FOUND";
|
||||||
|
case WIFI_REASON_AUTH_FAIL: return "AUTH_FAIL";
|
||||||
|
case WIFI_REASON_ASSOC_FAIL: return "ASSOC_FAIL";
|
||||||
|
case WIFI_REASON_HANDSHAKE_TIMEOUT: return "HANDSHAKE_TIMEOUT";
|
||||||
|
case WIFI_REASON_CONNECTION_FAIL: return "CONNECTION_FAIL";
|
||||||
|
case WIFI_REASON_AP_TSF_RESET: return "AP_TSF_RESET";
|
||||||
|
case WIFI_REASON_ROAMING: return "ROAMING";
|
||||||
|
default: return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_netif_t *wifi_sta_netif = NULL;
|
||||||
|
static esp_netif_t *wifi_ap_netif = NULL;
|
||||||
|
|
||||||
|
static int s_retry_num = 0;
|
||||||
|
|
||||||
|
static esp_err_t wifi_sta_do_disconnect(void);
|
||||||
|
|
||||||
|
static void sntp_sync_time_cb(struct timeval *tv)
|
||||||
|
{
|
||||||
|
time_t now = 0;
|
||||||
|
struct tm timeinfo = { 0 };
|
||||||
|
time(&now);
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
char strftime_buf[64];
|
||||||
|
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
|
||||||
|
ESP_LOGI(TAG, "Time synchronized: %s", strftime_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handler_on_wifi_disconnect(void *arg, esp_event_base_t event_base,
|
||||||
|
int32_t event_id, void *event_data)
|
||||||
|
{
|
||||||
|
s_retry_num++;
|
||||||
|
if (s_retry_num > MAX_RETRY) {
|
||||||
|
ESP_LOGW(TAG, "WiFi Connect failed %d times, stop reconnect.", s_retry_num);
|
||||||
|
/* let example_wifi_sta_do_connect() return */
|
||||||
|
|
||||||
|
wifi_sta_do_disconnect();
|
||||||
|
start_reboot_timer(60);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wifi_event_sta_disconnected_t *disconn = event_data;
|
||||||
|
if (disconn->reason == WIFI_REASON_ROAMING) {
|
||||||
|
ESP_LOGD(TAG, "station roaming, do nothing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "Wi-Fi disconnected, reason: (%s)", wifi_reason_str(disconn->reason));
|
||||||
|
ESP_LOGI(TAG, "Trying to reconnect...");
|
||||||
|
esp_err_t err = esp_wifi_connect();
|
||||||
|
if (err == ESP_ERR_WIFI_NOT_STARTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handler_on_wifi_connect(void *esp_netif, esp_event_base_t event_base,
|
||||||
|
int32_t event_id, void *event_data)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handler_on_sta_got_ip(void *arg, esp_event_base_t event_base,
|
||||||
|
int32_t event_id, void *event_data)
|
||||||
|
{
|
||||||
|
stop_reboot_timer();
|
||||||
|
s_retry_num = 0;
|
||||||
|
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||||
|
if (strcmp("sta", esp_netif_get_desc(event->esp_netif)) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s\" address: " IPSTR, esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip));
|
||||||
|
ESP_LOGI(TAG, "- IPv4 address: " IPSTR ",", IP2STR(&event->ip_info.ip));
|
||||||
|
sync_time();
|
||||||
|
led_set(LED_BLU, BLINK_SOLID);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_ap_event_handler(void* arg, esp_event_base_t event_base,
|
||||||
|
int32_t event_id, void* event_data)
|
||||||
|
{
|
||||||
|
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||||
|
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
|
||||||
|
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
|
||||||
|
MAC2STR(event->mac), event->aid);
|
||||||
|
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||||
|
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
|
||||||
|
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
|
||||||
|
MAC2STR(event->mac), event->aid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static esp_err_t set_hostname(esp_netif_t* esp_netif, const char *hostname)
|
||||||
|
{
|
||||||
|
if (esp_netif_set_hostname(esp_netif, hostname) != ESP_OK) return ESP_FAIL;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_start(wifi_mode_t mode)
|
||||||
|
{
|
||||||
|
size_t hostname_len;
|
||||||
|
char type_buf[16];
|
||||||
|
|
||||||
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||||
|
|
||||||
|
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_APSTA) {
|
||||||
|
esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_WIFI_STA();
|
||||||
|
wifi_sta_netif = esp_netif_create_wifi(WIFI_IF_STA, &esp_netif_config);
|
||||||
|
|
||||||
|
if (nconfig_read(NETIF_TYPE, type_buf, sizeof(type_buf)) == ESP_OK && strcmp(type_buf, "static") == 0) {
|
||||||
|
ESP_LOGI(TAG, "Using static IP configuration");
|
||||||
|
char ip_buf[16], gw_buf[16], mask_buf[16], dns1_buf[16], dns2_buf[16];
|
||||||
|
nconfig_read(NETIF_IP, ip_buf, sizeof(ip_buf));
|
||||||
|
nconfig_read(NETIF_GATEWAY, gw_buf, sizeof(gw_buf));
|
||||||
|
nconfig_read(NETIF_SUBNET, mask_buf, sizeof(mask_buf));
|
||||||
|
const char* dns1 = (nconfig_read(NETIF_DNS1, dns1_buf, sizeof(dns1_buf)) == ESP_OK) ? dns1_buf : NULL;
|
||||||
|
const char* dns2 = (nconfig_read(NETIF_DNS2, dns2_buf, sizeof(dns2_buf)) == ESP_OK) ? dns2_buf : NULL;
|
||||||
|
if (dns1 == NULL)
|
||||||
|
wifi_use_static(ip_buf, gw_buf, mask_buf, "8.8.8.8", "8.8.4.4");
|
||||||
|
else
|
||||||
|
wifi_use_static(ip_buf, gw_buf, mask_buf, dns1, dns2);
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Using DHCP configuration");
|
||||||
|
wifi_use_dhcp();
|
||||||
|
}
|
||||||
|
|
||||||
|
nconfig_get_str_len(NETIF_HOSTNAME, &hostname_len);
|
||||||
|
char buf[hostname_len];
|
||||||
|
nconfig_read(NETIF_HOSTNAME, buf, sizeof(buf));
|
||||||
|
set_hostname(wifi_sta_netif, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA) {
|
||||||
|
esp_netif_inherent_config_t esp_netif_config_ap = ESP_NETIF_INHERENT_DEFAULT_WIFI_AP();
|
||||||
|
wifi_ap_netif = esp_netif_create_wifi(WIFI_IF_AP, &esp_netif_config_ap);
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &wifi_ap_event_handler, NULL));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &wifi_ap_event_handler, NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_wifi_set_default_wifi_sta_handlers();
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_mode(mode));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_start());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void wifi_stop(void)
|
||||||
|
{
|
||||||
|
esp_err_t err = esp_wifi_stop();
|
||||||
|
if (err == ESP_ERR_WIFI_NOT_INIT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(err);
|
||||||
|
|
||||||
|
if (wifi_ap_netif) {
|
||||||
|
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STACONNECTED, &wifi_ap_event_handler);
|
||||||
|
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, &wifi_ap_event_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_deinit());
|
||||||
|
|
||||||
|
if (wifi_sta_netif) {
|
||||||
|
esp_netif_destroy(wifi_sta_netif);
|
||||||
|
wifi_sta_netif = NULL;
|
||||||
|
}
|
||||||
|
if (wifi_ap_netif) {
|
||||||
|
esp_netif_destroy(wifi_ap_netif);
|
||||||
|
wifi_ap_netif = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static esp_err_t wifi_sta_do_connect(wifi_config_t wifi_config)
|
||||||
|
{
|
||||||
|
stop_reboot_timer();
|
||||||
|
s_retry_num = 0;
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, handler_on_wifi_disconnect, NULL));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, handler_on_sta_got_ip, NULL));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, handler_on_wifi_connect, wifi_sta_netif));
|
||||||
|
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Connecting to %s...", (char*)wifi_config.sta.ssid);
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||||
|
esp_err_t ret = esp_wifi_connect();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "WiFi connect failed! ret:%x", ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_sta_do_disconnect(void)
|
||||||
|
{
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handler_on_wifi_disconnect));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &handler_on_sta_got_ip));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handler_on_wifi_connect));
|
||||||
|
led_set(LED_BLU, BLINK_DOUBLE);
|
||||||
|
|
||||||
|
return esp_wifi_disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_shutdown(void)
|
||||||
|
{
|
||||||
|
wifi_sta_do_disconnect();
|
||||||
|
wifi_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t do_connect(void)
|
||||||
|
{
|
||||||
|
esp_err_t err;
|
||||||
|
char mode_buf[16] = {0};
|
||||||
|
wifi_mode_t mode = WIFI_MODE_STA; // Default mode
|
||||||
|
|
||||||
|
if (nconfig_read(WIFI_MODE, mode_buf, sizeof(mode_buf)) == ESP_OK) {
|
||||||
|
if (strcmp(mode_buf, "apsta") == 0) {
|
||||||
|
mode = WIFI_MODE_APSTA;
|
||||||
|
ESP_LOGI(TAG, "Starting in APSTA mode");
|
||||||
|
} else { // "sta" or anything else defaults to STA
|
||||||
|
mode = WIFI_MODE_STA;
|
||||||
|
ESP_LOGI(TAG, "Starting in STA mode");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "WIFI_MODE not set, defaulting to STA mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_start(mode);
|
||||||
|
|
||||||
|
// Configure and connect STA interface if needed
|
||||||
|
if (mode == WIFI_MODE_STA || mode == WIFI_MODE_APSTA) {
|
||||||
|
wifi_config_t sta_config = {0};
|
||||||
|
bool sta_creds_ok = false;
|
||||||
|
if (nconfig_read(WIFI_SSID, (char*)sta_config.sta.ssid, 32) == ESP_OK && strlen((char*)sta_config.sta.ssid) > 0) {
|
||||||
|
if (nconfig_read(WIFI_PASSWORD, (char*)sta_config.sta.password, 64) == ESP_OK) {
|
||||||
|
sta_creds_ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sta_creds_ok) {
|
||||||
|
err = wifi_sta_do_connect(sta_config);
|
||||||
|
if (err != ESP_OK && mode == WIFI_MODE_STA) {
|
||||||
|
// In STA-only mode, failure to connect is a fatal error
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
} else if (mode == WIFI_MODE_STA) {
|
||||||
|
// In STA-only mode, missing credentials is a fatal error
|
||||||
|
ESP_LOGE(TAG, "Missing STA credentials in STA mode.");
|
||||||
|
return ESP_FAIL;
|
||||||
|
} else {
|
||||||
|
// In APSTA mode, missing credentials is a warning
|
||||||
|
ESP_LOGW(TAG, "Missing STA credentials in APSTA mode. STA will not connect.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure AP interface if needed
|
||||||
|
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA) {
|
||||||
|
char ap_ssid[32], ap_pass[64];
|
||||||
|
wifi_config_t ap_config = {
|
||||||
|
.ap = {
|
||||||
|
.channel = 1,
|
||||||
|
.max_connection = 4,
|
||||||
|
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||||
|
.ssid_hidden = 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nconfig_read(AP_SSID, ap_ssid, sizeof(ap_ssid)) == ESP_OK && strlen(ap_ssid) > 0) {
|
||||||
|
strcpy((char*)ap_config.ap.ssid, ap_ssid);
|
||||||
|
} else {
|
||||||
|
strcpy((char*)ap_config.ap.ssid, "ODROID-REMOTE-AP");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nconfig_read(AP_PASSWORD, ap_pass, sizeof(ap_pass)) == ESP_OK && strlen(ap_pass) >= 8) {
|
||||||
|
strcpy((char*)ap_config.ap.password, ap_pass);
|
||||||
|
} else {
|
||||||
|
ap_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||||
|
memset(ap_config.ap.password, 0, sizeof(ap_config.ap.password));
|
||||||
|
}
|
||||||
|
ap_config.ap.ssid_len = strlen((char*)ap_config.ap.ssid);
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));
|
||||||
|
ESP_LOGI(TAG, "AP configured, SSID: %s", ap_config.ap.ssid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_connect(void)
|
||||||
|
{
|
||||||
|
led_set(LED_BLU, BLINK_DOUBLE);
|
||||||
|
|
||||||
|
static esp_sntp_config_t ntp_cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG_MULTIPLE(3,
|
||||||
|
ESP_SNTP_SERVER_LIST("time.windows.com", "pool.ntp.org", "216.239.35.0")); // google public ntp
|
||||||
|
ntp_cfg.start = false;
|
||||||
|
ntp_cfg.sync_cb = sntp_sync_time_cb;
|
||||||
|
ntp_cfg.smooth_sync = true; // Sync immediately when started
|
||||||
|
esp_netif_sntp_init(&ntp_cfg);
|
||||||
|
|
||||||
|
if (do_connect() != ESP_OK) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(esp_register_shutdown_handler(&wifi_shutdown));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
esp_err_t wifi_disconnect(void)
|
||||||
|
{
|
||||||
|
wifi_shutdown();
|
||||||
|
ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&wifi_shutdown));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wifi_scan_aps(wifi_ap_record_t **ap_records, uint16_t* count)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Starting WiFi scan...");
|
||||||
|
|
||||||
|
esp_err_t err = esp_wifi_scan_start(NULL, true);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_wifi_scan_start failed: %s", esp_err_to_name(err));
|
||||||
|
*count = 0;
|
||||||
|
*ap_records = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(count));
|
||||||
|
ESP_LOGI(TAG, "Found %u access points", *count);
|
||||||
|
|
||||||
|
if (*count == 0)
|
||||||
|
*ap_records = NULL;
|
||||||
|
else
|
||||||
|
*ap_records = calloc(*count, sizeof(wifi_ap_record_t));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(count, *ap_records));
|
||||||
|
ESP_LOGI(TAG, "Scan done");
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t *ap_info)
|
||||||
|
{
|
||||||
|
esp_err_t ret = esp_wifi_sta_get_ap_info(ap_info);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
// Clear ssid and set invalid rssi on error
|
||||||
|
memset(ap_info->ssid, 0, sizeof(ap_info->ssid));
|
||||||
|
ap_info->rssi = -127;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t *ip_info)
|
||||||
|
{
|
||||||
|
return esp_netif_get_ip_info(wifi_sta_netif, ip_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t *dns_info)
|
||||||
|
{
|
||||||
|
if (wifi_sta_netif) {
|
||||||
|
return esp_netif_get_dns_info(wifi_sta_netif, type, dns_info);
|
||||||
|
}
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_use_static(const char *ip, const char *gw, const char *netmask, const char *dns1, const char *dns2)
|
||||||
|
{
|
||||||
|
if (wifi_sta_netif == NULL) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = esp_netif_dhcpc_stop(wifi_sta_netif);
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_NOT_STOPPED) {
|
||||||
|
ESP_LOGE(TAG, "Failed to stop DHCP client: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_netif_ip_info_t ip_info;
|
||||||
|
inet_pton(AF_INET, ip, &ip_info.ip);
|
||||||
|
inet_pton(AF_INET, gw, &ip_info.gw);
|
||||||
|
inet_pton(AF_INET, netmask, &ip_info.netmask);
|
||||||
|
err = esp_netif_set_ip_info(wifi_sta_netif, &ip_info);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set static IP info: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_netif_dns_info_t dns_info;
|
||||||
|
if (dns1 && strlen(dns1) > 0) {
|
||||||
|
inet_pton(AF_INET, dns1, &dns_info.ip.u_addr.ip4);
|
||||||
|
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
||||||
|
} else {
|
||||||
|
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_MAIN, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dns2 && strlen(dns2) > 0) {
|
||||||
|
inet_pton(AF_INET, dns2, &dns_info.ip.u_addr.ip4);
|
||||||
|
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_BACKUP, &dns_info);
|
||||||
|
} else {
|
||||||
|
esp_netif_set_dns_info(wifi_sta_netif, ESP_NETIF_DNS_BACKUP, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_use_dhcp(void)
|
||||||
|
{
|
||||||
|
if (wifi_sta_netif == NULL) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
esp_err_t err = esp_netif_dhcpc_start(wifi_sta_netif);
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start DHCP client: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t wifi_switch_mode(const char* mode)
|
||||||
|
{
|
||||||
|
if (strcmp(mode, "sta") != 0 && strcmp(mode, "apsta") != 0) {
|
||||||
|
ESP_LOGE(TAG, "Invalid mode specified: %s. Use 'sta' or 'apsta'.", mode);
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
char current_mode_buf[16] = {0};
|
||||||
|
if (nconfig_read(WIFI_MODE, current_mode_buf, sizeof(current_mode_buf)) == ESP_OK) {
|
||||||
|
if (strcmp(current_mode_buf, mode) == 0) {
|
||||||
|
ESP_LOGI(TAG, "Already in %s mode.", mode);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Switching Wi-Fi mode to %s.", mode);
|
||||||
|
|
||||||
|
esp_err_t err = nconfig_write(WIFI_MODE, mode);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to save new Wi-Fi mode to NVS");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_disconnect();
|
||||||
|
err = wifi_connect();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to connect in new mode %s", mode);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Successfully switched to %s mode.", mode);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sync_time()
|
||||||
|
{
|
||||||
|
esp_netif_sntp_start();
|
||||||
|
ESP_LOGI(TAG, "SNTP service started, waiting for time synchronization...");
|
||||||
|
}
|
||||||
24
page/.gitignore
vendored
Normal file
24
page/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
319
page/index.html
Normal file
319
page/index.html
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" data-bs-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ODROID Remote</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<header class="d-flex justify-content-between align-items-center mb-3 main-header">
|
||||||
|
<div class="order-md-1" style="flex: 1;">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span id="wifi-status" class="d-flex align-items-center text-muted">
|
||||||
|
<i id="wifi-icon" class="bi bi-wifi-off me-2"></i>
|
||||||
|
<span id="wifi-ssid-status">Disconnected</span>
|
||||||
|
</span>
|
||||||
|
<span id="websocket-status" class="d-flex align-items-center text-danger ms-3">
|
||||||
|
<i id="websocket-icon" class="bi bi-x-circle-fill me-2"></i>
|
||||||
|
<span id="websocket-status-text">Offline</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-monospace mt-1 d-none d-md-inline">
|
||||||
|
<span id="voltage-display" class="text-primary">--.-- V</span> |
|
||||||
|
<span id="current-display" class="text-primary">--.-- A</span> |
|
||||||
|
<span id="power-display" class="text-primary">--.-- W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-primary text-center order-md-2 mx-auto">ODROID Power Mate</h1>
|
||||||
|
<div class="d-flex align-items-center justify-content-end order-md-3 header-controls" style="flex: 1;">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="theme-toggle">
|
||||||
|
<label class="form-check-label" for="theme-toggle"><i id="theme-icon" class="bi bi-moon-stars-fill"></i></label>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-secondary ms-3" id="settings-button" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#settingsModal">
|
||||||
|
<i class="bi bi-gear"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs" id="main-tabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="terminal-tab-btn" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#terminal-tab-pane" type="button" role="tab">Terminal
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="graph-tab-btn" data-bs-toggle="tab" data-bs-target="#graph-tab-pane"
|
||||||
|
type="button" role="tab">Metrics
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="myTabContent">
|
||||||
|
<div class="tab-pane fade show active" id="terminal-tab-pane" role="tabpanel">
|
||||||
|
<div class="card border-top-0 rounded-0 rounded-bottom">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="terminal-wrapper">
|
||||||
|
<div id="terminal-container" class="border rounded">
|
||||||
|
<div id="terminal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
<button id="download-button" class="btn btn-primary me-2"><i class="bi bi-download me-1"></i>Download Log</button>
|
||||||
|
<button id="clear-button" class="btn btn-secondary">Clear Terminal</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Moved from Control Tab -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
|
||||||
|
Main Power (12V)
|
||||||
|
<div class="control-wrapper">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="main-power-toggle">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
|
||||||
|
USB Power (5V)
|
||||||
|
<div class="control-wrapper">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="usb-power-toggle">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
|
||||||
|
Power Actions
|
||||||
|
<div class="control-wrapper">
|
||||||
|
<button id="reset-button" class="btn btn-secondary">Reset</button>
|
||||||
|
<button id="power-action-button" class="btn btn-danger ms-2">Power</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
System Uptime
|
||||||
|
<div class="control-wrapper">
|
||||||
|
<span id="uptime-display" class="font-monospace text-success">--:--:--</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="graph-tab-pane" role="tabpanel">
|
||||||
|
<div class="card border-top-0 rounded-0 rounded-bottom">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-end mb-3">
|
||||||
|
<a href="/datalog.csv" class="btn btn-primary" download="datalog.csv"><i class="bi bi-download me-1"></i> Download CSV</a>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-center mb-3">Power Input</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3 mb-md-0">
|
||||||
|
<canvas id="powerChart" class="chart-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3 mb-md-0">
|
||||||
|
<canvas id="voltageChart" class="chart-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<canvas id="currentChart" class="chart-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-body-tertiary text-center p-3">
|
||||||
|
<a href="https://www.hardkernel.com/" target="_blank" class="link-secondary">Hardkernel</a> |
|
||||||
|
<a href="https://wiki.odroid.com/start" target="_blank" class="link-secondary">Wiki</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="settingsModalLabel">Settings</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ul class="nav nav-tabs" id="settingsTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="wifi-settings-tab" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#wifi-settings-pane" type="button" role="tab">Wi-Fi
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="network-settings-tab" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#network-settings-pane" type="button" role="tab">Network
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="ap-mode-settings-tab" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#ap-mode-settings-pane" type="button" role="tab">AP Mode
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="device-settings-tab" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#device-settings-pane" type="button" role="tab">Device
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content pt-3" id="settingsTabContent">
|
||||||
|
<div class="tab-pane fade show active" id="wifi-settings-pane" role="tabpanel">
|
||||||
|
<div class="alert alert-danger mt-3" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
|
<strong>Warning:</strong> Disconnecting or connecting to a new network will restart the Wi-Fi service. You may need to reconnect to the device.
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6>Current Connection</h6>
|
||||||
|
<div class="d-flex justify-content-between align-items-center p-2 rounded bg-body-secondary">
|
||||||
|
<div>
|
||||||
|
<strong>Connected to: </strong><span id="current-wifi-ssid">MyHome_WiFi</span><br>
|
||||||
|
<small class="text-muted" id="current-wifi-ip">IP Address: -</small>
|
||||||
|
</div>
|
||||||
|
<!-- <button class="btn btn-warning btn-sm" id="wifi-disconnect-button">Disconnect</button>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h6>Available Wi-Fi Networks</h6>
|
||||||
|
<div class="table-responsive" style="max-height: 200px;">
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">SSID</th>
|
||||||
|
<th scope="col">Signal</th>
|
||||||
|
<th scope="col">Security</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="wifi-ap-list">
|
||||||
|
<tr><td colspan="3" class="text-center text-muted">Click 'Scan' to find networks.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||||
|
<button type="button" class="btn btn-primary me-2" id="scan-wifi-button">Scan</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="network-settings-pane" role="tabpanel">
|
||||||
|
<div class="alert alert-danger mt-3" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
|
<strong>Warning:</strong> Applying these settings will restart the Wi-Fi service. You may need to reconnect to the device.
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="static-ip-toggle">
|
||||||
|
<label class="form-check-label" for="static-ip-toggle">Use Static IP</label>
|
||||||
|
</div>
|
||||||
|
<div id="static-ip-config" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ip-address" class="form-label">IP Address</label>
|
||||||
|
<input type="text" class="form-control" id="ip-address" placeholder="192.168.0.100">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gateway" class="form-label">Gateway</label>
|
||||||
|
<input type="text" class="form-control" id="gateway" placeholder="192.168.0.1">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="netmask" class="form-label">Netmask</label>
|
||||||
|
<input type="text" class="form-control" id="netmask" placeholder="255.255.255.0">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dns1" class="form-label">DNS Server</label>
|
||||||
|
<input type="text" class="form-control" id="dns1" placeholder="8.8.8.8">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="dns2" class="form-label">Secondary DNS Server</label>
|
||||||
|
<input type="text" class="form-control" id="dns2" placeholder="8.8.4.4">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||||
|
<button type="button" class="btn btn-primary me-2" id="network-apply-button">Apply</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="ap-mode-settings-pane" role="tabpanel">
|
||||||
|
<div class="alert alert-danger mt-3" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||||
|
<strong>Warning:</strong> Applying these settings will restart the Wi-Fi service. You may need to reconnect to the device.
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="ap-mode-toggle">
|
||||||
|
<label class="form-check-label" for="ap-mode-toggle">Enable AP+STA Mode</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small">Disable to use Station (STA) mode only. Enable to use Access Point + Station (APSTA) mode, allowing other devices to connect to this one.</p>
|
||||||
|
<div id="ap-mode-config" style="display: none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ap-ssid" class="form-label">AP SSID</label>
|
||||||
|
<input type="text" class="form-control" id="ap-ssid" placeholder="ODROID-Remote-AP">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ap-password" class="form-label">AP Password</label>
|
||||||
|
<input type="password" class="form-control" id="ap-password" placeholder="Leave blank for open network">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||||
|
<button type="button" class="btn btn-primary me-2" id="ap-mode-apply-button">Apply</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="device-settings-pane" role="tabpanel">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="baud-rate-select" class="form-label">UART Baud Rate</label>
|
||||||
|
<select class="form-select" id="baud-rate-select">
|
||||||
|
<option value="9600">9600</option>
|
||||||
|
<option value="19200">19200</option>
|
||||||
|
<option value="38400">38400</option>
|
||||||
|
<option value="57600">57600</option>
|
||||||
|
<option value="115200">115200</option>
|
||||||
|
<option value="230400">230400</option>
|
||||||
|
<option value="460800">460800</option>
|
||||||
|
<option value="921600">921600</option>
|
||||||
|
<option value="1500000" selected>1500000</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end pt-3 border-top mt-3">
|
||||||
|
<button type="button" class="btn btn-primary me-2" id="baud-rate-apply-button">Apply</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wi-Fi Connection Modal -->
|
||||||
|
<div class="modal fade" id="wifiModal" tabindex="-1" aria-labelledby="wifiModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="wifiModalLabel">Connect to Wi-Fi</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wifi-ssid-connect" class="form-label">SSID</label>
|
||||||
|
<input type="text" class="form-control" id="wifi-ssid-connect" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wifi-password-connect" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="wifi-password-connect">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="wifi-connect-button">Connect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1283
page/package-lock.json
generated
Normal file
1283
page/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
page/package.json
Normal file
23
page/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "odroid-remote-page",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^7.0.4",
|
||||||
|
"vite-plugin-singlefile": "^2.0.1",
|
||||||
|
"vite-plugin-compression": "^0.5.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"@xterm/addon-fit": "^0.9.0",
|
||||||
|
"chart.js": "^4.4.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
112
page/src/api.js
Normal file
112
page/src/api.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* @file api.js
|
||||||
|
* @description This module centralizes all API calls to the server's RESTful endpoints.
|
||||||
|
* It abstracts the fetch logic, error handling, and JSON parsing for network and control operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the list of available Wi-Fi networks from the server.
|
||||||
|
* @returns {Promise<Array<Object>>} A promise that resolves to an array of Wi-Fi access point objects.
|
||||||
|
* @throws {Error} Throws an error if the network request fails.
|
||||||
|
*/
|
||||||
|
export async function fetchWifiScan() {
|
||||||
|
const response = await fetch('/api/wifi/scan');
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a request to connect to a specific Wi-Fi network.
|
||||||
|
* @param {string} ssid The SSID of the network to connect to.
|
||||||
|
* @param {string} password The password for the network.
|
||||||
|
* @returns {Promise<Object>} A promise that resolves to the server's JSON response.
|
||||||
|
* @throws {Error} Throws an error if the connection request fails.
|
||||||
|
*/
|
||||||
|
export async function postWifiConnect(ssid, password) {
|
||||||
|
const response = await fetch('/api/setting', { // Updated URL
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ssid, password }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(errorText || `Connection failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts updated network settings (e.g., static IP, DHCP, AP mode) to the server.
|
||||||
|
* @param {Object} payload The settings object to be sent.
|
||||||
|
* @returns {Promise<Response>} A promise that resolves to the raw fetch response.
|
||||||
|
* @throws {Error} Throws an error if the request fails.
|
||||||
|
*/
|
||||||
|
export async function postNetworkSettings(payload) {
|
||||||
|
const response = await fetch('/api/setting', { // Updated URL
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(errorText || `Failed to apply settings with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts the selected UART baud rate to the server.
|
||||||
|
* @param {string} baudrate The selected baud rate.
|
||||||
|
* @returns {Promise<Response>} A promise that resolves to the raw fetch response.
|
||||||
|
* @throws {Error} Throws an error if the request fails.
|
||||||
|
*/
|
||||||
|
export async function postBaudRateSetting(baudrate) {
|
||||||
|
const response = await fetch('/api/setting', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ baudrate }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(errorText || `Failed to apply baudrate with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current network settings and Wi-Fi status from the server.
|
||||||
|
* @returns {Promise<Object>} A promise that resolves to an object containing the current settings.
|
||||||
|
* @throws {Error} Throws an error if the network request fails.
|
||||||
|
*/
|
||||||
|
export async function fetchSettings() {
|
||||||
|
const response = await fetch('/api/setting'); // Updated URL
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current status of the power control relays (12V and 5V).
|
||||||
|
* @returns {Promise<Object>} A promise that resolves to an object with the power status.
|
||||||
|
* @throws {Error} Throws an error if the network request fails.
|
||||||
|
*/
|
||||||
|
export async function fetchControlStatus() {
|
||||||
|
const response = await fetch('/api/control');
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a command to the server to control power functions (e.g., toggle relays, trigger reset).
|
||||||
|
* @param {Object} command The control command object.
|
||||||
|
* @returns {Promise<Response>} A promise that resolves to the raw fetch response.
|
||||||
|
* @throws {Error} Throws an error if the request fails.
|
||||||
|
*/
|
||||||
|
export async function postControlCommand(command) {
|
||||||
|
const response = await fetch('/api/control', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(command)
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
211
page/src/chart.js
Normal file
211
page/src/chart.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* @file chart.js
|
||||||
|
* @description This module manages the Chart.js instances for visualizing sensor data.
|
||||||
|
* It handles initialization, theme updates, data updates, and resizing for the three separate charts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
import { powerChartCtx, voltageChartCtx, currentChartCtx, htmlEl, graphTabPane } from './dom.js';
|
||||||
|
|
||||||
|
// Register all necessary Chart.js components
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
|
// Store chart instances in an object
|
||||||
|
export const charts = {
|
||||||
|
power: null,
|
||||||
|
voltage: null,
|
||||||
|
current: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of empty labels for initial chart rendering.
|
||||||
|
* @returns {Array<string>} An array of empty strings.
|
||||||
|
*/
|
||||||
|
function initialLabels() {
|
||||||
|
return Array(CHART_DATA_POINTS).fill('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of null data points for initial chart rendering.
|
||||||
|
* @returns {Array<null>} An array of nulls.
|
||||||
|
*/
|
||||||
|
function initialData() {
|
||||||
|
return Array(CHART_DATA_POINTS).fill(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a common configuration object for a single line chart.
|
||||||
|
* @param {string} title - The title of the chart (e.g., 'Power (W)').
|
||||||
|
* @returns {Object} A Chart.js options object.
|
||||||
|
*/
|
||||||
|
function createChartOptions(title) {
|
||||||
|
return {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: { mode: 'index', intersect: false },
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'top' },
|
||||||
|
title: { display: true, text: title }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { autoSkipPadding: 10, maxRotation: 0, minRotation: 0 } },
|
||||||
|
y: { }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes all three charts (Power, Voltage, Current).
|
||||||
|
* If chart instances already exist, they are destroyed and new ones are created.
|
||||||
|
*/
|
||||||
|
export function initCharts() {
|
||||||
|
// Destroy existing charts if they exist
|
||||||
|
for (const key in charts) {
|
||||||
|
if (charts[key]) {
|
||||||
|
charts[key].destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Power Chart
|
||||||
|
if (powerChartCtx) {
|
||||||
|
charts.power = new Chart(powerChartCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: initialLabels(),
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Power (W)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
|
||||||
|
{ label: 'Avg Power', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: createChartOptions('Power')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Voltage Chart
|
||||||
|
if (voltageChartCtx) {
|
||||||
|
charts.voltage = new Chart(voltageChartCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: initialLabels(),
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Voltage (V)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
|
||||||
|
{ label: 'Avg Voltage', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: createChartOptions('Voltage')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Current Chart
|
||||||
|
if (currentChartCtx) {
|
||||||
|
charts.current = new Chart(currentChartCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: initialLabels(),
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Current (A)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
|
||||||
|
{ label: 'Avg Current', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: createChartOptions('Current')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a new theme (light or dark) to all charts.
|
||||||
|
* @param {string} themeName - The name of the theme to apply ('light' or 'dark').
|
||||||
|
*/
|
||||||
|
export function applyChartsTheme(themeName) {
|
||||||
|
const isDark = themeName === 'dark';
|
||||||
|
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
const labelColor = isDark ? '#dee2e6' : '#212529';
|
||||||
|
|
||||||
|
const powerColor = getComputedStyle(htmlEl).getPropertyValue('--chart-power-color');
|
||||||
|
const voltageColor = getComputedStyle(htmlEl).getPropertyValue('--chart-voltage-color');
|
||||||
|
const currentColor = getComputedStyle(htmlEl).getPropertyValue('--chart-current-color');
|
||||||
|
|
||||||
|
const updateThemeForChart = (chart, color) => {
|
||||||
|
if (!chart) return;
|
||||||
|
chart.options.scales.x.grid.color = gridColor;
|
||||||
|
chart.options.scales.y.grid.color = gridColor;
|
||||||
|
chart.options.scales.x.ticks.color = labelColor;
|
||||||
|
chart.options.scales.y.ticks.color = labelColor;
|
||||||
|
chart.options.plugins.legend.labels.color = labelColor;
|
||||||
|
chart.options.plugins.title.color = labelColor;
|
||||||
|
chart.data.datasets[0].borderColor = color;
|
||||||
|
chart.data.datasets[1].borderColor = color;
|
||||||
|
chart.data.datasets[1].borderDash = [10, 5];
|
||||||
|
chart.update('none');
|
||||||
|
};
|
||||||
|
|
||||||
|
updateThemeForChart(charts.power, powerColor);
|
||||||
|
updateThemeForChart(charts.voltage, voltageColor);
|
||||||
|
updateThemeForChart(charts.current, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all charts with new sensor data.
|
||||||
|
* @param {Object} data - The new sensor data object from the WebSocket.
|
||||||
|
*/
|
||||||
|
export function updateCharts(data) {
|
||||||
|
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
|
||||||
|
|
||||||
|
const updateSingleChart = (chart, value) => {
|
||||||
|
if (!chart) return;
|
||||||
|
|
||||||
|
// Shift old data
|
||||||
|
chart.data.labels.shift();
|
||||||
|
chart.data.datasets.forEach(dataset => dataset.data.shift());
|
||||||
|
|
||||||
|
// Push new data
|
||||||
|
chart.data.labels.push(timeLabel);
|
||||||
|
chart.data.datasets[0].data.push(value.toFixed(2));
|
||||||
|
|
||||||
|
// Calculate average and adjust Y-axis scale
|
||||||
|
const dataArray = chart.data.datasets[0].data.filter(v => v !== null).map(v => parseFloat(v));
|
||||||
|
if (dataArray.length > 0) {
|
||||||
|
const sum = dataArray.reduce((acc, val) => acc + val, 0);
|
||||||
|
const avg = (sum / dataArray.length).toFixed(2);
|
||||||
|
chart.data.datasets[1].data.push(avg);
|
||||||
|
|
||||||
|
// Adjust Y-axis scale for centering
|
||||||
|
const minVal = Math.min(...dataArray);
|
||||||
|
const maxVal = Math.max(...dataArray);
|
||||||
|
let padding = (maxVal - minVal) * 0.1; // 10% padding of the range
|
||||||
|
|
||||||
|
if (padding === 0) {
|
||||||
|
// If all values are the same, add 10% padding of the value itself, or a small default
|
||||||
|
padding = maxVal > 0 ? maxVal * 0.1 : 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.options.scales.y.min = Math.max(0, minVal - padding);
|
||||||
|
chart.options.scales.y.max = maxVal + padding;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
chart.data.datasets[1].data.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update the chart if the tab is visible
|
||||||
|
if (graphTabPane.classList.contains('show')) {
|
||||||
|
chart.update('none');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSingleChart(charts.power, data.power);
|
||||||
|
updateSingleChart(charts.voltage, data.voltage);
|
||||||
|
updateSingleChart(charts.current, data.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes all chart canvases. This is typically called on window resize events.
|
||||||
|
*/
|
||||||
|
export function resizeCharts() {
|
||||||
|
for (const key in charts) {
|
||||||
|
if (charts[key]) {
|
||||||
|
charts[key].resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
page/src/dom.js
Normal file
78
page/src/dom.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* @file dom.js
|
||||||
|
* @description This module selects and exports all necessary DOM elements for the application.
|
||||||
|
* This centralizes DOM access, making it easier to manage and modify element selectors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Theme Elements ---
|
||||||
|
export const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
export const themeIcon = document.getElementById('theme-icon');
|
||||||
|
export const htmlEl = document.documentElement;
|
||||||
|
|
||||||
|
// --- Control & Status Elements ---
|
||||||
|
export const mainPowerToggle = document.getElementById('main-power-toggle');
|
||||||
|
export const usbPowerToggle = document.getElementById('usb-power-toggle');
|
||||||
|
export const resetButton = document.getElementById('reset-button');
|
||||||
|
export const powerActionButton = document.getElementById('power-action-button');
|
||||||
|
export const voltageDisplay = document.getElementById('voltage-display');
|
||||||
|
export const currentDisplay = document.getElementById('current-display');
|
||||||
|
export const powerDisplay = document.getElementById('power-display');
|
||||||
|
export const uptimeDisplay = document.getElementById('uptime-display');
|
||||||
|
|
||||||
|
// --- Terminal Elements ---
|
||||||
|
export const terminalContainer = document.getElementById('terminal');
|
||||||
|
export const clearButton = document.getElementById('clear-button');
|
||||||
|
export const downloadButton = document.getElementById('download-button');
|
||||||
|
|
||||||
|
// --- Chart & Graph Elements ---
|
||||||
|
export const graphTabPane = document.getElementById('graph-tab-pane');
|
||||||
|
export const powerChartCtx = document.getElementById('powerChart')?.getContext('2d');
|
||||||
|
export const voltageChartCtx = document.getElementById('voltageChart')?.getContext('2d');
|
||||||
|
export const currentChartCtx = document.getElementById('currentChart')?.getContext('2d');
|
||||||
|
|
||||||
|
// --- WebSocket Status Elements ---
|
||||||
|
export const websocketStatus = document.getElementById('websocket-status');
|
||||||
|
export const websocketIcon = document.getElementById('websocket-icon');
|
||||||
|
export const websocketStatusText = document.getElementById('websocket-status-text');
|
||||||
|
|
||||||
|
// --- Header Wi-Fi Status ---
|
||||||
|
export const wifiStatus = document.getElementById('wifi-status');
|
||||||
|
export const wifiIcon = document.getElementById('wifi-icon');
|
||||||
|
export const wifiSsidStatus = document.getElementById('wifi-ssid-status');
|
||||||
|
|
||||||
|
// --- Settings Modal & General ---
|
||||||
|
export const settingsButton = document.getElementById('settings-button');
|
||||||
|
export const settingsModal = document.getElementById('settingsModal');
|
||||||
|
|
||||||
|
// --- Wi-Fi Settings Elements ---
|
||||||
|
export const wifiApList = document.getElementById('wifi-ap-list');
|
||||||
|
export const scanWifiButton = document.getElementById('scan-wifi-button');
|
||||||
|
export const currentWifiSsid = document.getElementById('current-wifi-ssid');
|
||||||
|
export const currentWifiIp = document.getElementById('current-wifi-ip');
|
||||||
|
|
||||||
|
// --- Wi-Fi Connection Modal Elements ---
|
||||||
|
export const wifiModalEl = document.getElementById('wifiModal');
|
||||||
|
export const wifiSsidConnectInput = document.getElementById('wifi-ssid-connect');
|
||||||
|
export const wifiPasswordConnectInput = document.getElementById('wifi-password-connect');
|
||||||
|
export const wifiConnectButton = document.getElementById('wifi-connect-button');
|
||||||
|
|
||||||
|
// --- Static IP Config Elements ---
|
||||||
|
export const staticIpToggle = document.getElementById('static-ip-toggle');
|
||||||
|
export const staticIpConfig = document.getElementById('static-ip-config');
|
||||||
|
export const networkApplyButton = document.getElementById('network-apply-button');
|
||||||
|
export const staticIpInput = document.getElementById('ip-address');
|
||||||
|
export const staticGatewayInput = document.getElementById('gateway');
|
||||||
|
export const staticNetmaskInput = document.getElementById('netmask');
|
||||||
|
export const dns1Input = document.getElementById('dns1');
|
||||||
|
export const dns2Input = document.getElementById('dns2');
|
||||||
|
|
||||||
|
// --- AP Mode Config Elements ---
|
||||||
|
export const apModeToggle = document.getElementById('ap-mode-toggle');
|
||||||
|
export const apModeConfig = document.getElementById('ap-mode-config');
|
||||||
|
export const apModeApplyButton = document.getElementById('ap-mode-apply-button');
|
||||||
|
export const apSsidInput = document.getElementById('ap-ssid');
|
||||||
|
export const apPasswordInput = document.getElementById('ap-password');
|
||||||
|
|
||||||
|
// --- Device Settings Elements ---
|
||||||
|
export const baudRateSelect = document.getElementById('baud-rate-select');
|
||||||
|
export const baudRateApplyButton = document.getElementById('baud-rate-apply-button');
|
||||||
93
page/src/events.js
Normal file
93
page/src/events.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @file events.js
|
||||||
|
* @description This module sets up all the event listeners for the application.
|
||||||
|
* It connects user interactions (like clicks and toggles) to the corresponding
|
||||||
|
* functions in other modules (UI, API, etc.).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as dom from './dom.js';
|
||||||
|
import * as api from './api.js';
|
||||||
|
import * as ui from './ui.js';
|
||||||
|
import { clearTerminal, fitTerminal, downloadTerminalOutput } from './terminal.js';
|
||||||
|
import { debounce, isMobile } from './utils.js';
|
||||||
|
|
||||||
|
// A flag to track if charts have been initialized
|
||||||
|
let chartsInitialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up all event listeners for the application's interactive elements.
|
||||||
|
*/
|
||||||
|
export function setupEventListeners() {
|
||||||
|
// --- Theme Toggle ---
|
||||||
|
dom.themeToggle.addEventListener('change', () => {
|
||||||
|
const newTheme = dom.themeToggle.checked ? 'dark' : 'light';
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
ui.applyTheme(newTheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Terminal Controls ---
|
||||||
|
dom.clearButton.addEventListener('click', clearTerminal);
|
||||||
|
dom.downloadButton.addEventListener('click', downloadTerminalOutput);
|
||||||
|
|
||||||
|
// --- Power Controls ---
|
||||||
|
dom.mainPowerToggle.addEventListener('change', () => api.postControlCommand({'load_12v_on': dom.mainPowerToggle.checked}).then(ui.updateControlStatus));
|
||||||
|
dom.usbPowerToggle.addEventListener('change', () => api.postControlCommand({'load_5v_on': dom.usbPowerToggle.checked}).then(ui.updateControlStatus));
|
||||||
|
dom.resetButton.addEventListener('click', () => api.postControlCommand({'reset_trigger': true}));
|
||||||
|
dom.powerActionButton.addEventListener('click', () => api.postControlCommand({'power_trigger': true}));
|
||||||
|
|
||||||
|
// --- Settings Modal Controls ---
|
||||||
|
dom.scanWifiButton.addEventListener('click', ui.scanForWifi);
|
||||||
|
dom.wifiConnectButton.addEventListener('click', ui.connectToWifi);
|
||||||
|
dom.networkApplyButton.addEventListener('click', ui.applyNetworkSettings);
|
||||||
|
dom.apModeApplyButton.addEventListener('click', ui.applyApModeSettings);
|
||||||
|
dom.baudRateApplyButton.addEventListener('click', ui.applyBaudRateSettings);
|
||||||
|
|
||||||
|
// --- Settings Modal Toggles (for showing/hiding sections) ---
|
||||||
|
dom.apModeToggle.addEventListener('change', () => {
|
||||||
|
dom.apModeConfig.style.display = dom.apModeToggle.checked ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.staticIpToggle.addEventListener('change', () => {
|
||||||
|
dom.staticIpConfig.style.display = dom.staticIpToggle.checked ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- General App Listeners ---
|
||||||
|
dom.settingsButton.addEventListener('click', ui.initializeSettings);
|
||||||
|
|
||||||
|
// --- Accessibility: Remove focus from modal elements before hiding ---
|
||||||
|
const blurActiveElement = () => {
|
||||||
|
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dom.settingsModal.addEventListener('hide.bs.modal', blurActiveElement);
|
||||||
|
dom.wifiModalEl.addEventListener('hide.bs.modal', blurActiveElement);
|
||||||
|
|
||||||
|
// --- Bootstrap Tab Events ---
|
||||||
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(tabEl => {
|
||||||
|
tabEl.addEventListener('shown.bs.tab', async (event) => {
|
||||||
|
const tabId = event.target.getAttribute('data-bs-target');
|
||||||
|
|
||||||
|
if (tabId === '#graph-tab-pane') {
|
||||||
|
// Dynamically import the chart module only when the tab is shown
|
||||||
|
const chartModule = await import('./chart.js');
|
||||||
|
|
||||||
|
if (!chartsInitialized) {
|
||||||
|
chartModule.initCharts();
|
||||||
|
chartsInitialized = true;
|
||||||
|
} else {
|
||||||
|
chartModule.resizeCharts();
|
||||||
|
}
|
||||||
|
} else if (tabId === '#terminal-tab-pane') {
|
||||||
|
// Fit the terminal when its tab is shown, especially for mobile.
|
||||||
|
if (isMobile()) {
|
||||||
|
fitTerminal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Window Resize Event ---
|
||||||
|
// Debounced to avoid excessive calls during resizing.
|
||||||
|
window.addEventListener('resize', debounce(ui.handleResize, 150));
|
||||||
|
}
|
||||||
109
page/src/main.js
Normal file
109
page/src/main.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* @file main.js
|
||||||
|
* @description The main entry point for the web application.
|
||||||
|
* This file imports all necessary modules, sets up the application structure,
|
||||||
|
* initializes components, and establishes the WebSocket connection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Stylesheets ---
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
// --- Module Imports ---
|
||||||
|
import { initWebSocket } from './websocket.js';
|
||||||
|
import { setupTerminal, term } from './terminal.js';
|
||||||
|
import {
|
||||||
|
applyTheme,
|
||||||
|
initUI,
|
||||||
|
updateControlStatus,
|
||||||
|
updateSensorUI,
|
||||||
|
updateWifiStatusUI,
|
||||||
|
updateWebsocketStatus
|
||||||
|
} from './ui.js';
|
||||||
|
import { setupEventListeners } from './events.js';
|
||||||
|
|
||||||
|
// --- WebSocket Event Handlers ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for when the WebSocket connection is successfully opened.
|
||||||
|
* Updates the UI to show an 'Online' status and fetches the initial control status.
|
||||||
|
*/
|
||||||
|
function onWsOpen() {
|
||||||
|
updateWebsocketStatus(true);
|
||||||
|
if (term) {
|
||||||
|
term.write('\x1b[32mConnected to WebSocket Server\x1b[0m\r\n');
|
||||||
|
}
|
||||||
|
updateControlStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for when the WebSocket connection is closed.
|
||||||
|
* Updates the UI to show an 'Offline' status and attempts to reconnect after a delay.
|
||||||
|
*/
|
||||||
|
function onWsClose() {
|
||||||
|
updateWebsocketStatus(false);
|
||||||
|
if (term) {
|
||||||
|
term.write('\r\n\x1b[31mConnection closed. Reconnecting...\x1b[0m\r\n');
|
||||||
|
}
|
||||||
|
// Attempt to re-establish the connection after 2 seconds
|
||||||
|
setTimeout(initialize, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for when a message is received from the WebSocket server.
|
||||||
|
* It handles both JSON messages (for sensor and status updates) and binary data (for the terminal).
|
||||||
|
* @param {MessageEvent} event - The WebSocket message event.
|
||||||
|
*/
|
||||||
|
function onWsMessage(event) {
|
||||||
|
if (typeof event.data === 'string') {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
if (message.type === 'sensor_data') {
|
||||||
|
updateSensorUI(message);
|
||||||
|
} else if (message.type === 'wifi_status') {
|
||||||
|
updateWifiStatusUI(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore non-JSON string messages
|
||||||
|
}
|
||||||
|
} else if (term && event.data instanceof ArrayBuffer) {
|
||||||
|
// Write raw UART data to the terminal
|
||||||
|
const data = new Uint8Array(event.data);
|
||||||
|
term.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application Initialization ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the entire application.
|
||||||
|
* This function sets up the UI, theme, terminal, chart, WebSocket connection, and event listeners.
|
||||||
|
*/
|
||||||
|
function initialize() {
|
||||||
|
// Initialize basic UI components
|
||||||
|
initUI();
|
||||||
|
|
||||||
|
// Set up the interactive components first
|
||||||
|
setupTerminal();
|
||||||
|
|
||||||
|
// Apply the saved theme or detect the user's preferred theme.
|
||||||
|
// This must be done AFTER the chart and terminal are initialized.
|
||||||
|
const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||||
|
applyTheme(savedTheme);
|
||||||
|
|
||||||
|
// Establish the WebSocket connection with the defined handlers
|
||||||
|
initWebSocket({
|
||||||
|
onOpen: onWsOpen,
|
||||||
|
onClose: onWsClose,
|
||||||
|
onMessage: onWsMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach all event listeners to the DOM elements
|
||||||
|
setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Start Application ---
|
||||||
|
|
||||||
|
// Wait for the DOM to be fully loaded before initializing the application.
|
||||||
|
document.addEventListener('DOMContentLoaded', initialize);
|
||||||
153
page/src/style.css
Normal file
153
page/src/style.css
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
|
||||||
|
:root {
|
||||||
|
--bs-body-font-family: 'Courier New', Courier, monospace;
|
||||||
|
--chart-power-color: #007bff;
|
||||||
|
--chart-voltage-color: #28a745;
|
||||||
|
--chart-current-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
--chart-power-color: #569cd6;
|
||||||
|
--chart-voltage-color: #4ec9b0;
|
||||||
|
--chart-current-color: #dcdcaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, .card, .modal-content, .list-group-item, .nav-tabs .nav-link {
|
||||||
|
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-tab-pane .card-body {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-container {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
/* Removed height: 100% to allow shrink-wrapping */
|
||||||
|
}
|
||||||
|
|
||||||
|
#graph-tab-pane .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center; /* Vertically center the chart content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-viewport {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport::-webkit-scrollbar-track {
|
||||||
|
background: var(--bs-tertiary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--bs-secondary-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 3px solid var(--bs-tertiary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--bs-secondary-bg) var(--bs-tertiary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
height: 30rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrapper {
|
||||||
|
min-height: 38px; /* Match button height for alignment */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrapper .form-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-ap-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Optimizations */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.main-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-list-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-list-item > .control-wrapper {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-list-item > .control-wrapper > .btn {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-wrapper, #terminal-container {
|
||||||
|
width: 100%;
|
||||||
|
/* Removed height: 100% */
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-tab-pane .card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
page/src/terminal.js
Normal file
135
page/src/terminal.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* @file terminal.js
|
||||||
|
* @description This module manages the Xterm.js terminal instance, including setup,
|
||||||
|
* theme handling, and data communication with the WebSocket.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
import '@xterm/xterm/css/xterm.css';
|
||||||
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
|
import { terminalContainer } from './dom.js';
|
||||||
|
import { isMobile } from './utils.js';
|
||||||
|
import { websocket, sendWebsocketMessage } from './websocket.js';
|
||||||
|
|
||||||
|
// Exported terminal instance and addon for global access
|
||||||
|
export let term;
|
||||||
|
export let fitAddon;
|
||||||
|
|
||||||
|
// Theme definitions for the terminal
|
||||||
|
const lightTheme = {
|
||||||
|
background: 'transparent',
|
||||||
|
foreground: '#212529',
|
||||||
|
cursor: '#212529',
|
||||||
|
selectionBackground: 'rgba(0,0,0,0.1)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const darkTheme = {
|
||||||
|
background: 'transparent',
|
||||||
|
foreground: '#dee2e6',
|
||||||
|
cursor: '#dee2e6',
|
||||||
|
selectionBackground: 'rgba(255,255,255,0.1)'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Xterm.js terminal, loads addons, and attaches it to the DOM.
|
||||||
|
* Sets up the initial size and the data handler for sending input to the WebSocket.
|
||||||
|
*/
|
||||||
|
export function setupTerminal() {
|
||||||
|
// Ensure the container is empty before creating a new terminal
|
||||||
|
while (terminalContainer.firstChild) {
|
||||||
|
terminalContainer.removeChild(terminalContainer.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
fitAddon = new FitAddon();
|
||||||
|
term = new Terminal({ convertEol: true, cursorBlink: true });
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
term.open(terminalContainer);
|
||||||
|
|
||||||
|
// Adjust terminal size based on device type
|
||||||
|
if (isMobile()) {
|
||||||
|
fitAddon.fit();
|
||||||
|
} else {
|
||||||
|
term.resize(80, 24); // Default size for desktop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user input and send it over the WebSocket
|
||||||
|
term.onData(data => {
|
||||||
|
sendWebsocketMessage(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a new theme (light or dark) to the terminal.
|
||||||
|
* @param {string} themeName - The name of the theme to apply ('light' or 'dark').
|
||||||
|
*/
|
||||||
|
export function applyTerminalTheme(themeName) {
|
||||||
|
if (!term) return;
|
||||||
|
const isDark = themeName === 'dark';
|
||||||
|
term.options.theme = isDark ? darkTheme : lightTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the terminal screen.
|
||||||
|
*/
|
||||||
|
export function clearTerminal() {
|
||||||
|
if (term) {
|
||||||
|
term.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fits the terminal to the size of its container element.
|
||||||
|
* Useful for responsive adjustments on window resize.
|
||||||
|
*/
|
||||||
|
export function fitTerminal() {
|
||||||
|
if (fitAddon) {
|
||||||
|
fitAddon.fit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers all text from the terminal buffer and downloads it as a .log file.
|
||||||
|
*/
|
||||||
|
export function downloadTerminalOutput() {
|
||||||
|
if (!term) {
|
||||||
|
console.error("Terminal is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = term.buffer.active;
|
||||||
|
let fullText = '';
|
||||||
|
|
||||||
|
// Iterate through the buffer to get all lines
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
const line = buffer.getLine(i);
|
||||||
|
if (line) {
|
||||||
|
fullText += line.translateToString() + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullText.trim() === '') {
|
||||||
|
console.warn("Terminal is empty, nothing to download.");
|
||||||
|
// Optionally, provide user feedback here (e.g., a toast notification)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a blob from the text content
|
||||||
|
const blob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
|
// Create a link element to trigger the download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Generate a filename with a timestamp
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
link.download = `odroid-log-${timestamp}.log`;
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// Append to the document, click, and then remove
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
// Clean up the object URL
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
346
page/src/ui.js
Normal file
346
page/src/ui.js
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* @file ui.js
|
||||||
|
* @description This module manages all UI interactions and updates.
|
||||||
|
* It bridges the gap between backend data (from API and WebSockets) and the user-facing components,
|
||||||
|
* handling everything from theme changes to dynamic content updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as bootstrap from 'bootstrap';
|
||||||
|
import * as dom from './dom.js';
|
||||||
|
import * as api from './api.js';
|
||||||
|
import { formatUptime, isMobile } from './utils.js';
|
||||||
|
import { applyTerminalTheme, fitTerminal } from './terminal.js';
|
||||||
|
import { applyChartsTheme, resizeCharts, updateCharts } from './chart.js';
|
||||||
|
|
||||||
|
// Instance of the Bootstrap Modal for Wi-Fi connection
|
||||||
|
let wifiModal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the UI components, such as the Bootstrap modal.
|
||||||
|
*/
|
||||||
|
export function initUI() {
|
||||||
|
wifiModal = new bootstrap.Modal(dom.wifiModalEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the selected theme (light or dark) to the entire application.
|
||||||
|
* @param {string} themeName - The name of the theme ('light' or 'dark').
|
||||||
|
*/
|
||||||
|
export function applyTheme(themeName) {
|
||||||
|
const isDark = themeName === 'dark';
|
||||||
|
dom.htmlEl.setAttribute('data-bs-theme', themeName);
|
||||||
|
dom.themeIcon.className = isDark ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
||||||
|
dom.themeToggle.checked = isDark;
|
||||||
|
applyTerminalTheme(themeName);
|
||||||
|
applyChartsTheme(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the UI with the latest sensor data.
|
||||||
|
* @param {Object} data - The sensor data object from the WebSocket.
|
||||||
|
*/
|
||||||
|
export function updateSensorUI(data) {
|
||||||
|
dom.voltageDisplay.textContent = `${data.voltage.toFixed(2)} V`;
|
||||||
|
dom.currentDisplay.textContent = `${data.current.toFixed(2)} A`;
|
||||||
|
dom.powerDisplay.textContent = `${data.power.toFixed(2)} W`;
|
||||||
|
if (data.uptime_sec !== undefined) {
|
||||||
|
dom.uptimeDisplay.textContent = formatUptime(data.uptime_sec);
|
||||||
|
}
|
||||||
|
updateCharts(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the Wi-Fi status indicator in the header.
|
||||||
|
* @param {Object} data - The Wi-Fi status object from the WebSocket.
|
||||||
|
*/
|
||||||
|
export function updateWifiStatusUI(data) {
|
||||||
|
if (data.connected) {
|
||||||
|
dom.wifiSsidStatus.textContent = data.ssid;
|
||||||
|
dom.wifiStatus.title = `Signal Strength: ${data.rssi} dBm`;
|
||||||
|
let iconClass = 'bi me-2 ';
|
||||||
|
if (data.rssi >= -60) iconClass += 'bi-wifi';
|
||||||
|
else if (data.rssi >= -75) iconClass += 'bi-wifi-2';
|
||||||
|
else iconClass += 'bi-wifi-1';
|
||||||
|
dom.wifiIcon.className = iconClass;
|
||||||
|
dom.wifiStatus.classList.replace('text-muted', 'text-success');
|
||||||
|
dom.wifiStatus.classList.remove('text-danger');
|
||||||
|
} else {
|
||||||
|
dom.wifiSsidStatus.textContent = 'Disconnected';
|
||||||
|
dom.wifiStatus.title = '';
|
||||||
|
dom.wifiIcon.className = 'bi bi-wifi-off me-2';
|
||||||
|
dom.wifiStatus.classList.replace('text-success', 'text-muted');
|
||||||
|
dom.wifiStatus.classList.remove('text-danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a Wi-Fi scan and updates the settings modal with the results.
|
||||||
|
*/
|
||||||
|
export async function scanForWifi() {
|
||||||
|
dom.scanWifiButton.disabled = true;
|
||||||
|
dom.scanWifiButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Scanning...`;
|
||||||
|
dom.wifiApList.innerHTML = '<tr><td colspan="3" class="text-center">Scanning for networks...</td></tr>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apRecords = await api.fetchWifiScan();
|
||||||
|
dom.wifiApList.innerHTML = ''; // Clear loading message
|
||||||
|
|
||||||
|
if (apRecords.length === 0) {
|
||||||
|
dom.wifiApList.innerHTML = '<tr><td colspan="3" class="text-center">No networks found.</td></tr>';
|
||||||
|
} else {
|
||||||
|
apRecords.forEach(ap => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'wifi-ap-row';
|
||||||
|
|
||||||
|
let rssiIcon;
|
||||||
|
if (ap.rssi >= -60) rssiIcon = 'bi-wifi';
|
||||||
|
else if (ap.rssi >= -75) rssiIcon = 'bi-wifi-2';
|
||||||
|
else rssiIcon = 'bi-wifi-1';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${ap.ssid}</td>
|
||||||
|
<td class="text-center"><i class="bi ${rssiIcon}"></i></td>
|
||||||
|
<td>${ap.authmode}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
row.addEventListener('click', () => {
|
||||||
|
dom.wifiSsidConnectInput.value = ap.ssid;
|
||||||
|
dom.wifiPasswordConnectInput.value = '';
|
||||||
|
wifiModal.show();
|
||||||
|
dom.wifiModalEl.addEventListener('shown.bs.modal', () => {
|
||||||
|
dom.wifiPasswordConnectInput.focus();
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.wifiApList.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error scanning for Wi-Fi:', error);
|
||||||
|
dom.wifiApList.innerHTML = `<tr><td colspan="3" class="text-center text-danger">Scan failed: ${error.message}</td></tr>`;
|
||||||
|
} finally {
|
||||||
|
dom.scanWifiButton.disabled = false;
|
||||||
|
dom.scanWifiButton.innerHTML = 'Scan';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the Wi-Fi connection process, sending credentials to the server.
|
||||||
|
*/
|
||||||
|
export async function connectToWifi() {
|
||||||
|
const ssid = dom.wifiSsidConnectInput.value;
|
||||||
|
const password = dom.wifiPasswordConnectInput.value;
|
||||||
|
if (!ssid) return;
|
||||||
|
|
||||||
|
dom.wifiConnectButton.disabled = true;
|
||||||
|
dom.wifiConnectButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Connecting...`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.postWifiConnect(ssid, password);
|
||||||
|
if (result.status === 'connection_initiated') {
|
||||||
|
wifiModal.hide();
|
||||||
|
setTimeout(() => {
|
||||||
|
alert(`Connection to "${ssid}" initiated. The device will try to reconnect. Please check the Wi-Fi status icon.`);
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || 'Unknown server response.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connecting to Wi-Fi:', error);
|
||||||
|
alert(`Failed to connect: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
dom.wifiConnectButton.disabled = false;
|
||||||
|
dom.wifiConnectButton.innerHTML = 'Connect';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies network settings (Static IP or DHCP) by sending the configuration to the server.
|
||||||
|
*/
|
||||||
|
export async function applyNetworkSettings() {
|
||||||
|
const useStatic = dom.staticIpToggle.checked;
|
||||||
|
let payload;
|
||||||
|
|
||||||
|
dom.networkApplyButton.disabled = true;
|
||||||
|
dom.networkApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
||||||
|
|
||||||
|
if (useStatic) {
|
||||||
|
const ip = dom.staticIpInput.value;
|
||||||
|
const gateway = dom.staticGatewayInput.value;
|
||||||
|
const subnet = dom.staticNetmaskInput.value;
|
||||||
|
const dns1 = dom.dns1Input.value;
|
||||||
|
const dns2 = dom.dns2Input.value;
|
||||||
|
|
||||||
|
if (!ip || !gateway || !subnet || !dns1) {
|
||||||
|
alert('For static IP, you must provide IP Address, Gateway, Netmask, and DNS Server.');
|
||||||
|
dom.networkApplyButton.disabled = false;
|
||||||
|
dom.networkApplyButton.innerHTML = 'Apply';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = { net_type: 'static', ip, gateway, subnet, dns1 };
|
||||||
|
if (dns2) payload.dns2 = dns2;
|
||||||
|
} else {
|
||||||
|
payload = { net_type: 'dhcp' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.postNetworkSettings(payload);
|
||||||
|
alert('Network settings applied. Reconnect to the network for changes to take effect.');
|
||||||
|
initializeSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying network settings:', error);
|
||||||
|
alert(`Failed to apply settings: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
dom.networkApplyButton.disabled = false;
|
||||||
|
dom.networkApplyButton.innerHTML = 'Apply';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies AP Mode settings (AP+STA or STA) by sending the configuration to the server.
|
||||||
|
*/
|
||||||
|
export async function applyApModeSettings() {
|
||||||
|
const mode = dom.apModeToggle.checked ? 'apsta' : 'sta';
|
||||||
|
let payload = { mode };
|
||||||
|
|
||||||
|
dom.apModeApplyButton.disabled = true;
|
||||||
|
dom.apModeApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
||||||
|
|
||||||
|
if (mode === 'apsta') {
|
||||||
|
const ap_ssid = dom.apSsidInput.value;
|
||||||
|
const ap_password = dom.apPasswordInput.value;
|
||||||
|
|
||||||
|
if (!ap_ssid) {
|
||||||
|
alert('AP SSID cannot be empty when enabling APSTA mode.');
|
||||||
|
dom.apModeApplyButton.disabled = false;
|
||||||
|
dom.apModeApplyButton.innerHTML = 'Apply';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.ap_ssid = ap_ssid;
|
||||||
|
if (ap_password) {
|
||||||
|
payload.ap_password = ap_password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.postNetworkSettings(payload); // Reuses the same API endpoint
|
||||||
|
alert(`Successfully switched mode to ${mode}. The device will now reconfigure.`);
|
||||||
|
initializeSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error switching Wi-Fi mode:', error);
|
||||||
|
alert(`Failed to switch mode: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
dom.apModeApplyButton.disabled = false;
|
||||||
|
dom.apModeApplyButton.innerHTML = 'Apply';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the selected UART baud rate by sending it to the server.
|
||||||
|
*/
|
||||||
|
export async function applyBaudRateSettings() {
|
||||||
|
const baudrate = dom.baudRateSelect.value;
|
||||||
|
dom.baudRateApplyButton.disabled = true;
|
||||||
|
dom.baudRateApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.postBaudRateSetting(baudrate);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying baud rate:', error);
|
||||||
|
} finally {
|
||||||
|
dom.baudRateApplyButton.disabled = false;
|
||||||
|
dom.baudRateApplyButton.innerHTML = 'Apply';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and displays the current network and device settings in the settings modal.
|
||||||
|
*/
|
||||||
|
export async function initializeSettings() {
|
||||||
|
try {
|
||||||
|
const data = await api.fetchSettings();
|
||||||
|
|
||||||
|
// Wi-Fi Connection Status
|
||||||
|
if (data.connected) {
|
||||||
|
dom.currentWifiSsid.textContent = data.ssid;
|
||||||
|
dom.currentWifiIp.textContent = `IP Address: ${data.ip ? data.ip.ip : 'N/A'}`;
|
||||||
|
} else {
|
||||||
|
dom.currentWifiSsid.textContent = 'Not Connected';
|
||||||
|
dom.currentWifiIp.textContent = 'IP Address: -';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network (Static/DHCP) Settings
|
||||||
|
if (data.ip) {
|
||||||
|
dom.staticIpInput.value = data.ip.ip || '';
|
||||||
|
dom.staticGatewayInput.value = data.ip.gateway || '';
|
||||||
|
dom.staticNetmaskInput.value = data.ip.subnet || '';
|
||||||
|
dom.dns1Input.value = data.ip.dns1 || '';
|
||||||
|
dom.dns2Input.value = data.ip.dns2 || '';
|
||||||
|
}
|
||||||
|
dom.staticIpToggle.checked = data.net_type === 'static';
|
||||||
|
dom.staticIpConfig.style.display = dom.staticIpToggle.checked ? 'block' : 'none';
|
||||||
|
|
||||||
|
// AP Mode Settings
|
||||||
|
dom.apModeToggle.checked = data.mode === 'apsta';
|
||||||
|
dom.apModeConfig.style.display = dom.apModeToggle.checked ? 'block' : 'none';
|
||||||
|
dom.apSsidInput.value = ''; // For security, don't pre-fill
|
||||||
|
dom.apPasswordInput.value = '';
|
||||||
|
|
||||||
|
// Device Settings
|
||||||
|
if (data.baudrate) {
|
||||||
|
dom.baudRateSelect.value = data.baudrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing settings:', error);
|
||||||
|
// Reset fields on error
|
||||||
|
dom.currentWifiSsid.textContent = 'Status Unknown';
|
||||||
|
dom.currentWifiIp.textContent = 'IP Address: -';
|
||||||
|
dom.staticIpToggle.checked = false;
|
||||||
|
dom.staticIpConfig.style.display = 'none';
|
||||||
|
dom.apModeToggle.checked = false;
|
||||||
|
dom.apModeConfig.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and updates the status of the power control toggles.
|
||||||
|
*/
|
||||||
|
export async function updateControlStatus() {
|
||||||
|
try {
|
||||||
|
const status = await api.fetchControlStatus();
|
||||||
|
dom.mainPowerToggle.checked = status.load_12v_on;
|
||||||
|
dom.usbPowerToggle.checked = status.load_5v_on;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching control status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles window resize events to make components responsive.
|
||||||
|
*/
|
||||||
|
export function handleResize() {
|
||||||
|
if (isMobile()) {
|
||||||
|
fitTerminal();
|
||||||
|
}
|
||||||
|
resizeCharts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the WebSocket connection status indicator in the header.
|
||||||
|
* @param {boolean} isConnected - True if the WebSocket is connected, false otherwise.
|
||||||
|
*/
|
||||||
|
export function updateWebsocketStatus(isConnected) {
|
||||||
|
if (isConnected) {
|
||||||
|
dom.websocketStatusText.textContent = 'Online';
|
||||||
|
dom.websocketIcon.className = 'bi bi-check-circle-fill me-2';
|
||||||
|
dom.websocketStatus.classList.remove('text-danger');
|
||||||
|
dom.websocketStatus.classList.add('text-success');
|
||||||
|
} else {
|
||||||
|
dom.websocketStatusText.textContent = 'Offline';
|
||||||
|
dom.websocketIcon.className = 'bi bi-x-circle-fill me-2';
|
||||||
|
dom.websocketStatus.classList.remove('text-success');
|
||||||
|
dom.websocketStatus.classList.add('text-danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
44
page/src/utils.js
Normal file
44
page/src/utils.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* @file utils.js
|
||||||
|
* @description Provides utility functions used throughout the application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a debounced function that delays invoking `func` until after `delay` milliseconds
|
||||||
|
* have elapsed since the last time the debounced function was invoked.
|
||||||
|
* @param {Function} func The function to debounce.
|
||||||
|
* @param {number} delay The number of milliseconds to delay.
|
||||||
|
* @returns {Function} Returns the new debounced function.
|
||||||
|
*/
|
||||||
|
export function debounce(func, delay) {
|
||||||
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
const context = this;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(context, args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a duration in total seconds into a human-readable string (e.g., "1d 02:30:15").
|
||||||
|
* @param {number} totalSeconds The total seconds to format.
|
||||||
|
* @returns {string} The formatted uptime string.
|
||||||
|
*/
|
||||||
|
export function formatUptime(totalSeconds) {
|
||||||
|
const days = Math.floor(totalSeconds / 86400);
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
const pad = (num) => String(num).padStart(2, '0');
|
||||||
|
const timeString = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||||
|
return days > 0 ? `${days}d ${timeString}` : timeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current device is likely a mobile device based on screen width.
|
||||||
|
* @returns {boolean} True if the window width is less than or equal to 767.98px, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isMobile() {
|
||||||
|
// The 767.98px breakpoint is typically used by Bootstrap for medium (md) devices.
|
||||||
|
return window.innerWidth <= 767.98;
|
||||||
|
}
|
||||||
41
page/src/websocket.js
Normal file
41
page/src/websocket.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @file websocket.js
|
||||||
|
* @description This module handles the WebSocket connection for real-time, two-way
|
||||||
|
* communication with the server. It provides functions to initialize the connection
|
||||||
|
* and send messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The WebSocket instance, exported for potential direct access if needed.
|
||||||
|
export let websocket;
|
||||||
|
|
||||||
|
// The WebSocket server address, derived from the current page's hostname.
|
||||||
|
const gateway = `ws://${window.location.hostname}/ws`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the WebSocket connection and sets up event handlers.
|
||||||
|
* @param {Object} callbacks - An object containing callback functions for WebSocket events.
|
||||||
|
* @param {function} callbacks.onOpen - Called when the connection is successfully opened.
|
||||||
|
* @param {function} callbacks.onClose - Called when the connection is closed.
|
||||||
|
* @param {function} callbacks.onMessage - Called when a message is received from the server.
|
||||||
|
*/
|
||||||
|
export function initWebSocket({ onOpen, onClose, onMessage }) {
|
||||||
|
console.log(`Trying to open a WebSocket connection to ${gateway}...`);
|
||||||
|
websocket = new WebSocket(gateway);
|
||||||
|
// Set binary type to arraybuffer to handle raw binary data from the UART.
|
||||||
|
websocket.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
// Assign event handlers from the provided callbacks
|
||||||
|
if (onOpen) websocket.onopen = onOpen;
|
||||||
|
if (onClose) websocket.onclose = onClose;
|
||||||
|
if (onMessage) websocket.onmessage = onMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends data over the WebSocket connection if it is open.
|
||||||
|
* @param {string | ArrayBuffer} data - The data to send to the server.
|
||||||
|
*/
|
||||||
|
export function sendWebsocketMessage(data) {
|
||||||
|
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||||
|
websocket.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
page/vite.config.js
Normal file
10
page/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
viteSingleFile(),
|
||||||
|
viteCompression(),
|
||||||
|
],
|
||||||
|
});
|
||||||
6
partitions.csv
Normal file
6
partitions.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# ESP-IDF Partition Table
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs,data,nvs,0x9000,24K,
|
||||||
|
phy_init,data,phy,0xf000,4K,
|
||||||
|
factory,app,factory,0x10000,2M,
|
||||||
|
littlefs, data, littlefs, ,1536K,
|
||||||
|
8
sdkconfig.defaults
Normal file
8
sdkconfig.defaults
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||||
|
# Espressif IoT Development Framework (ESP-IDF) 5.4.0 Project Minimal Configuration
|
||||||
|
#
|
||||||
|
CONFIG_IDF_TARGET="esp32c3"
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_UART_ISR_IN_IRAM=y
|
||||||
|
CONFIG_HTTPD_WS_SUPPORT=y
|
||||||
Reference in New Issue
Block a user