2 Commits

Author SHA1 Message Date
b9225cc970 Fix and optimization datalog
Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
2025-08-28 12:48:06 +09:00
a3569ab580 Change project name
Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
2025-08-28 12:48:06 +09:00
76 changed files with 4136 additions and 4843 deletions

View File

@@ -1,55 +0,0 @@
# Generated from CLion C/C++ Code Style settings
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignOperands: false
AlignTrailingComments: false
AlwaysBreakTemplateDeclarations: Yes
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
AfterControlStatement: true
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterStruct: true
AfterUnion: true
AfterExternBlock: false
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: true
BeforeWhile: true
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBraces: Custom
BreakConstructorInitializers: AfterColon
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 120
ConstructorInitializerAllOnOneLineOrOnePerLine: false
IncludeCategories:
- Regex: '^<.*'
Priority: 1
- Regex: '^".*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseBlocks: true
IndentWidth: 4
InsertNewlineAtEOF: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 2
NamespaceIndentation: All
PointerAlignment: Left
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
TabWidth: 4
...

4
.gitignore vendored
View File

@@ -1,5 +1,3 @@
/.idea
sdkconfig.old
/managed_components
sdkconfig
dependencies.lock
/managed_components

View File

@@ -20,15 +20,8 @@ This project provides a comprehensive web interface to control power, monitor re
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/esp32c3/get-started/index.html)**: This project is developed and tested with ESP-IDF v5.4 or later.
- **[ESP-IDF (Espressif IoT Development Framework)](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/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.
- **[Nanopb](https://github.com/nanopb/nanopb)**: Required to build for protobuf.
### Install dependencies (Ubuntu)
```bash
sudo apt install nodejs npm nanopb
```
## How to Build and Flash

59
dependencies.lock Normal file
View 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

View File

@@ -6,44 +6,46 @@ This document outlines the HTTP REST and WebSocket APIs for communication betwee
## WebSocket API
The WebSocket API provides a full-duplex communication channel for real-time data, such as sensor metrics and the
interactive serial console. All server-to-client communication is done via binary WebSocket frames containing Protocol
Buffers (protobuf) messages.
The WebSocket API provides a full-duplex communication channel for real-time data, such as sensor metrics and the interactive serial console.
**Endpoint**: `/ws`
> **Note**: The server only accepts one WebSocket client at a time. Subsequent connection attempts will be rejected with a `403 Forbidden` error until the active client disconnects.
### Server-to-Client Messages
The server pushes binary messages to the client. Each message is a Protocol Buffers (protobuf) encoded `StatusMessage`.
This top-level message uses a `oneof` field to carry different payload types.
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.
The protobuf schema is defined in `proto/status.proto`.
#### JSON Messages
```proto
// Top-level message for all websocket communication
message StatusMessage {
oneof payload {
SensorData sensor_data = 1;
WifiStatus wifi_status = 2;
UartData uart_data = 3;
}
}
```
| 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}` |
The client decodes the `StatusMessage` and then handles the specific payload:
**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.
| `oneof` payload field | Contained Message | Description |
|-----------------------|-------------------|-----------------------------------------------------------------------------------------|
| `sensor_data` | `SensorData` | Pushed periodically (e.g., every second) with the latest power metrics. |
| `wifi_status` | `WifiStatus` | Pushed periodically or on change to update the current Wi-Fi connection status. |
| `uart_data` | `UartData` | Forwards raw binary data from the ODROID's serial (UART) port to the client's terminal. |
#### 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 sends raw binary/text data, which is interpreted as terminal input.
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**: `(raw data)`
- **Payload**: `(binary data)`
---
@@ -114,9 +116,6 @@ Retrieves the complete current network and system configuration.
"mode": "apsta",
"net_type": "static",
"baudrate": "115200",
"vin_current_limit": 8.0,
"main_current_limit": 7.0,
"usb_current_limit": 5.0,
"ip": {
"ip": "192.168.1.100",
"gateway": "192.168.1.1",
@@ -133,9 +132,6 @@ Retrieves the complete current network and system configuration.
- `mode` (string): The current Wi-Fi mode (`"sta"` or `"apsta"`).
- `net_type` (string): The network type (`"dhcp"` or `"static"`).
- `baudrate` (string): The current UART baud rate.
- `vin_current_limit` (number): The current limit for VIN in Amps. `0` means disabled.
- `main_current_limit` (number): The current limit for the Main channel in Amps. `0` means disabled.
- `usb_current_limit` (number): The current limit for the USB channel in Amps. `0` means disabled.
- `ip` (object): Contains IP configuration details. Present even if using DHCP (may show the last-leased IP).
- `ip` (string): The device's IP address.
- `gateway` (string): The network gateway address.
@@ -166,14 +162,17 @@ This is a multi-purpose endpoint. The server determines the action based on the
{ "net_type": "dhcp" }
```
- **Request Body (for Static IP)**:
*Note: The `ip` object structure is consistent with the `GET /api/setting` response.*
```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"
"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"
}
}
```
- **Success Response (200 OK)**:
@@ -206,18 +205,6 @@ This is a multi-purpose endpoint. The server determines the action based on the
{ "status": "baudrate_updated" }
```
- **Action: Configure Current Limits**
- **Request Body**:
*Note: You can set one or more limits in a single request. A value of `-1.0` disables the limit.*
```json
{
"vin_current_limit": 7.5,
"main_current_limit": 6.0,
"usb_current_limit": -1.0
}
```
- **Success Response (200 OK)**: `{"status":"current_limit_updated"}`
---
### Endpoint: `/api/wifi/scan`

View File

@@ -1,66 +0,0 @@
# Custom firmware
If you want, you can create your own custom firmware.
## Install Tools
To build the source code, you need the following packages on Ubuntu 24.04.
```bash
sudo apt install nodejs npm nanopb
```
You need `esp-idf` version 5.4. It might be possible to build with the latest version, but it has not been confirmed.
First, install the necessary tools for esp-idf.
```bash
sudo apt install git wget flex bison gperf python3 python3-pip python3-venv \
cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \
```
Next, clone the esp-idf tool.
```bash
mkdir -p ~/esp
cd ~/esp
git clone -b release/v5.4 --recursive https://github.com/espressif/esp-idf.git
```
You need to install tools such as a compiler and a debugger.
```bash
cd ~/esp/esp-idf
./install.sh esp32c3
```
Please prepare to build the source by running the shell for using esp-idf.
```bash
. $HOME/esp/esp-idf/export.sh
```
## Build custom firmware based on PowerMate
First, clone the PowerMate source code.
```bash
git clone https://github.com/shinys000114/odroid-powermate.git
```
After connecting the USB Type-C cable to the PowerMate and the host PC, run the command below to build and update PowerMate.
```bash
cd odroid-powermate
idf.py app flash monitor
```
## Create a new project
Create a project with the command below and create your own firmware.
```bash
idf.py create-project proj
cd proj
idf.py set-target esp32c3
```
For a more detailed guide, please refer to the link below.
[https://docs.espressif.com/projects/esp-idf/en/release-v5.4/esp32/get-started/linux-macos-setup.html#](https://docs.espressif.com/projects/esp-idf/en/release-v5.4/esp32/get-started/linux-macos-setup.html#)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -1,52 +0,0 @@
# Connector and Pin Header Description
## J1 USB-C Connector
This terminal is for uploading firmware and checking program logs.
It is recognized as a serial device and as a `ttyACM` device in Linux.
To view the logs, you can use the command below.
```bash
minicom -D /dev/ttyACM0 -b 115200
```
## J2 UART Pin header
Connected to the `U0RXD`, `U0TXD` system UART of the ESP32-C3. Currently not used on this board.
It can be used as GPIO through appropriate configuration in esp-idf.
## J3 ODROID UART Connector
Connects to the UART debug port of the ODROID.
This connector is `X8821WRS-04`. you can use the [cable from the USB-UART 2 board](https://www.hardkernel.com/shop/usb-uart-2-module-kit-copy/).
## J4 GPIO Output
4 GPIO output pin headers. Used to trigger the power of external devices.
| Pin Number | Function | Function | Pin Number |
|------------|--------------------|--------------------|------------|
| 3 | Reserved | Reserved | 1 |
| 4 | Reset (Open Drain) | Power (Open Drain) | 2 |
The Reset and Power pins can be used on the ODROID-M series and H series.
## J5 DC Input
Supports 9~21v input. You can use the DC power supply sold by Hardkernel.
## J6 DC Out
Outputs the switched power from the J5 input.
## J7 USB Power Out
Switched 5v voltage output.
The output is set to 5.25V considering voltage drop. Please use with caution.
## J8 OLED Display Connector
**Reserved**
SSD1309 OLED Connector
It cannot be used as GPIO for other purposes because it shares the I2C bus with `PCA9557PW`, `INA3221`.

Binary file not shown.

View File

@@ -1,77 +0,0 @@
# Settings
> **Warning**
> Wi-Fi and IP related settings will restart the Wi-Fi service.
> After applying the settings, you may need to reconnect to the page.
>
> Please be careful when applying settings. Incorrect settings may prevent you from connecting to the device.
> If you have problems with the device settings, connect the debug USB-C connector to check the device's logs.
> If necessary, you can re-update the firmware to initialize the device.
## Connect to Wi-Fi
The device boots in APSTA mode by default and services the following AP.
- SSID: powermate
- Password: hardkernel
After connecting to the above AP using a smartphone, etc., you can configure the device by accessing the
`http://192.168.4.1` address.
> **Warning**
> A warning that the AP does not have internet may appear. Please press `Allow connection` to connect to the device.
You can open the settings window by pressing the gear button at the top.
Press the Scan button to see a list of nearby APs.
<img src="img/1000030234.jpg" width="300">
After entering the password, press the Connect button and PowerMate will attempt to connect to the AP.
For public APs, leave the Password blank.
<img src="img/1000030235.jpg" width="300">
<img src="img/1000030236.jpg" width="300">
When Wi-Fi is connected, the green SSID name appears at the top of the page.
<img src="img/1000030237.jpg" width="300">
## Set static ip
You can assign the IP of the device yourself.
You can set the IP directly by turning on the `Use Static IP` toggle in the `Network` tab.
![static.png](img/static.png)
## AP Mode
Configure the AP function of PowerMate.
If you do not need the AP service, it is recommended to disable it.
![apmode.png](img/apmode.png)
## Current Limit
Monitors the current of PowerMate, and if a value higher than the set current is detected, all load switches are turned
off.
![current.png](img/current.png)
## Device
Sets the UART Baudrate. Please match it with the baudrate of the connected ODROID.
| ODROID | Baud rate |
|------------------|----------------------------|
| ODROID-C4 | 115200 |
| ODROID-C5 | 921600 |
| ODROID-M1/M1S/M2 | 1500000 |
| ODROID-H3/4 | According to user settings |
You can reboot the PowerMate. The state of the load switch does not change due to rebooting.
![device.png](img/device.png)

View File

@@ -1,24 +0,0 @@
# UART Terminal
> **Warning**
> Data drop may occur depending on the communication quality.
![uart.png](img/uart.png)
You can see the UART terminal in the Device tab.
You can specify the baud rate in `Setting > Device`, and the supported baud rates are 9600-1500000.
![device.png](img/device.png)
## ANSI Color
Supports the color table of the terminal.
```bash
odroid@server:~$ TERM=xterm-256color /bin/bash
```
![color.png](img/color.png)
![htop.png](img/htop.png)

View File

@@ -2,34 +2,28 @@
set(WEB_APP_SOURCE_DIR ${CMAKE_SOURCE_DIR}/page)
set(GZ_OUTPUT_FILE ${WEB_APP_SOURCE_DIR}/dist/index.html.gz)
set(PROTO_DIR ${CMAKE_SOURCE_DIR}/proto)
set(PROTO_OUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto)
set(PROTO_FILE ${PROTO_DIR}/status.proto)
set(PROTO_C_FILE ${PROTO_OUT_DIR}/status.pb.c)
set(PROTO_H_FILE ${PROTO_OUT_DIR}/status.pb.h)
# Check npm is available
find_program(NPM_EXECUTABLE npm)
if (NOT NPM_EXECUTABLE)
if(NOT NPM_EXECUTABLE)
message(FATAL_ERROR "npm not found! Please install Node.js and npm.")
endif ()
endif()
# 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" "service" "proto"
INCLUDE_DIRS "include" "proto"
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 any of these files change
DEPENDS
OUTPUT ${GZ_OUTPUT_FILE}
COMMAND npm install
COMMAND npm run build
WORKING_DIRECTORY ${WEB_APP_SOURCE_DIR}
# Re-run the build if any of these files change
DEPENDS
${WEB_APP_SOURCE_DIR}/package.json
${WEB_APP_SOURCE_DIR}/vite.config.js
${WEB_APP_SOURCE_DIR}/index.html
@@ -44,46 +38,14 @@ add_custom_command(
${WEB_APP_SOURCE_DIR}/src/utils.js
${WEB_APP_SOURCE_DIR}/src/websocket.js
COMMENT "Building Node.js project (npm install && npm run build)"
VERBATIM
COMMENT "Building Node.js project (npm install && npm run build)"
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}
DEPENDS ${GZ_OUTPUT_FILE}
)
add_custom_command(
OUTPUT ${PROTO_C_FILE} ${PROTO_H_FILE}
COMMAND protoc --nanopb_out=${PROTO_OUT_DIR} status.proto
WORKING_DIRECTORY ${PROTO_DIR}
DEPENDS ${PROTO_FILE}
COMMENT "Generating C sources from ${PROTO_FILE} using nanopb"
VERBATIM
)
add_custom_target(protobuf_generate ALL
DEPENDS ${PROTO_C_FILE} ${PROTO_H_FILE}
)
add_dependencies(${COMPONENT_LIB} build_web_app)
add_dependencies(${COMPONENT_LIB} protobuf_generate)
execute_process(
COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
add_compile_definitions(VERSION_HASH="${GIT_HASH}")
execute_process(
COMMAND git describe --tags --abbrev=0
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
)
add_compile_definitions(VERSION_TAG="${GIT_TAG}")

View File

@@ -2,33 +2,26 @@ menu "ODROID-MONITOR"
menu "GPIO"
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
config I2C_GPIO_SCL
config GPIO_INA226_SCL
int "INA226 SCL GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 1
help
GPIO number for I2C Master data line.
config I2C_GPIO_SDA
int "INA226 SDA GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 0
help
GPIO number for I2C Master data line.
config GPIO_INA3221_INT_CRITICAL
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 9
default 10
help
GPIO number for critical int pin.
config GPIO_INA3221_INT_WARNING
int "INA226 WARNING GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 5
help
GPIO number for critical int pin.
GPIO number for I2C Master data line.
config GPIO_UART_TX
int "UART TX GPIO Num"
@@ -47,64 +40,43 @@ menu "ODROID-MONITOR"
config GPIO_LED_STATUS
int "Status LED GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 2
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 3
default 9
help
GPIO number for LED.
config GPIO_EXPANDER_RESET
int "Trigger reset GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 8
help
GPIO number for Reset expander.
config EXPANDER_GPIO_SW_12V
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 Load switch.
GPIO number for Trigger.
config EXPANDER_GPIO_SW_5V
int "5v Load Switch GPIO Num"
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 Load switch.
config EXPANDER_GPIO_TRIGGER_POWER
int "Trigger power GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 0
help
GPIO number for Trigger.
config EXPANDER_GPIO_TRIGGER_RESET
int "Trigger reset GPIO Num"
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
default 1
help
GPIO number for Trigger.
config TRIGGER_POWER_DELAY_MS
int "Trigger reset GPIO Num"
range 100 5000
default 3000
help
Reset delay ms.
config TRIGGER_RESET_DELAY_MS
int "Trigger reset GPIO Num"
range 100 5000
default 1000
help
Reset delay ms.
endmenu
endmenu

View File

@@ -1,46 +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 "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "i2cdev.h"
#include "driver/uart.h"
#include "esp_http_server.h"
#include "indicator.h"
#include "nconfig.h"
#include "nvs_flash.h"
#include "system.h"
#include "wifi.h"
#include "storage.h"
void app_main(void)
{
printf("\n\n== ODROID POWER-MATE ===\n");
printf("Version: %s-%s\n\n", VERSION_TAG, VERSION_HASH);
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
ESP_ERROR_CHECK(i2cdev_init());
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)
{
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);
storage_init();
// 네트워크 초기화
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(init_nconfig());
wifi_init();
// WiFi 연결
wifi_connect();
sync_time();
start_webserver();
}
}

View File

@@ -1,6 +1,3 @@
dependencies:
espressif/led_indicator: ^1.1.1
joltwallet/littlefs: ==1.20.1
esp-idf-lib/ina3221: ^1.1.7
esp-idf-lib/pca9557: ^1.0.7
nikas-belogolov/nanopb: ^1.0.0

118
main/ina226/ina226.c Normal file
View 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, &reg_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
View 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

View File

@@ -5,50 +5,25 @@
#ifndef LED_H
#define LED_H
/**
* @brief Defines the different blinking patterns for the LEDs.
*/
enum blink_type
{
BLINK_SLOW = 0, ///< Slow blinking pattern.
BLINK_FAST, ///< Fast blinking pattern.
BLINK_DOUBLE, ///< Double blink pattern.
BLINK_TRIPLE, ///< Triple blink pattern.
BLINK_SOLID, ///< Solid (always on) state.
BLINK_MAX, ///< Sentinel for the number of blink types.
BLINK_SLOW = 0,
BLINK_FAST,
BLINK_DOUBLE,
BLINK_TRIPLE,
BLINK_SOLID,
BLINK_MAX,
};
/**
* @brief Defines the available LEDs that can be controlled.
*/
enum blink_led
{
LED_RED = 0, ///< The red LED.
LED_BLU = 1, ///< The blue LED.
LED_MAX, ///< Sentinel for the number of LEDs.
LED_RED = 0,
LED_BLU = 1,
LED_MAX,
};
/**
* @brief Initializes the LED indicator system.
*
* This function sets up the GPIOs and timers required for the LED blinking patterns.
* It should be called once during application startup.
*/
void init_led(void);
/**
* @brief Sets a specific blinking pattern for an LED.
*
* @param led The LED to control (e.g., LED_RED, LED_BLU).
* @param type The blinking pattern to apply (e.g., BLINK_FAST, BLINK_SOLID).
*/
void led_set(enum blink_led led, enum blink_type type);
/**
* @brief Turns off a specific LED.
*
* @param led The LED to turn off.
*/
void led_off(enum blink_led led);
#endif // LED_H
#endif //LED_H

View File

@@ -1,99 +1,46 @@
/**
* @file nconfig.h
* @brief Provides an interface for managing system configuration using Non-Volatile Storage (NVS).
*/
//
// Created by shinys on 25. 7. 10.
//
#ifndef NCONFIG_H
#define NCONFIG_H
#include "esp_err.h"
#include "nvs.h"
#include "esp_err.h"
#define NCONFIG_NVS_NAMESPACE "er" ///< The NVS namespace where all configurations are stored.
#define NCONFIG_NOT_FOUND ESP_ERR_NVS_NOT_FOUND ///< Alias for the NVS not found error code.
#define NCONFIG_NVS_NAMESPACE "er"
#define NCONFIG_NOT_FOUND ESP_ERR_NVS_NOT_FOUND
/**
* @brief Initializes the nconfig module.
*
* This function initializes the Non-Volatile Storage (NVS) component, which is required
* for all other nconfig operations. It should be called once at application startup.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t init_nconfig();
/**
* @brief Defines the keys for all configuration values managed by nconfig.
*/
enum nconfig_type
{
WIFI_SSID, ///< The SSID of the Wi-Fi network to connect to (STA mode).
WIFI_PASSWORD, ///< The password for the Wi-Fi network (STA mode).
WIFI_MODE, ///< The Wi-Fi operating mode (e.g., "sta" or "apsta").
AP_SSID, ///< The SSID for the device's Access Point mode.
AP_PASSWORD, ///< The password for the device's Access Point mode.
NETIF_HOSTNAME, ///< The hostname of the device on the network.
NETIF_IP, ///< The static IP address for the STA interface.
NETIF_GATEWAY, ///< The gateway address for the STA interface.
NETIF_SUBNET, ///< The subnet mask for the STA interface.
NETIF_DNS1, ///< The primary DNS server address.
NETIF_DNS2, ///< The secondary DNS server address.
NETIF_TYPE, ///< The network interface type (e.g., "dhcp" or "static").
UART_BAUD_RATE, ///< The baud rate for the UART communication.
VIN_CURRENT_LIMIT, ///< The maximum current limit for the VIN.
MAIN_CURRENT_LIMIT, ///< The maximum current limit for the MAIN out.
USB_CURRENT_LIMIT, ///< The maximum current limit for the USB out.
PAGE_USERNAME, ///< Webpage username
PAGE_PASSWORD, ///< Webpage password
NCONFIG_TYPE_MAX, ///< Sentinel for the maximum number of configuration types.
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,
};
/**
* @brief Erase all of nvs data and restart system
*/
void reset_nconfig();
/**
* @brief Checks if a specific configuration value has been set.
*
* @param type The configuration key to check.
* @return True if the value is not set, false otherwise.
*/
bool nconfig_value_is_not_set(enum nconfig_type type);
/**
* @brief Writes a configuration value to NVS.
*
* @param type The configuration key to write.
* @param data A pointer to the string data to be stored.
* @return ESP_OK on success, or an error code on failure.
*/
// Write config
esp_err_t nconfig_write(enum nconfig_type type, const char* data);
/**
* @brief Gets the length of a stored string configuration value.
*
* @param type The configuration key to query.
* @param[out] len A pointer to a size_t variable to store the length.
* @return ESP_OK on success, NCONFIG_NOT_FOUND if the key doesn't exist, or another error code.
*/
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t* len);
// Check config is set and get config value length
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t *len);
/**
* @brief Reads a configuration value from NVS.
*
* @param type The configuration key to read.
* @param[out] data A buffer to store the read data.
* @param len The length of the buffer. It should be large enough to hold the value.
* @return ESP_OK on success, or an error code on failure.
*/
// Read config
esp_err_t nconfig_read(enum nconfig_type type, char* data, size_t len);
/**
* @brief Deletes a configuration key-value pair from NVS.
*
* @param type The configuration key to delete.
* @return ESP_OK on success, or an error code on failure.
*/
// Remove key
esp_err_t nconfig_delete(enum nconfig_type type);
#endif // NCONFIG_H
#endif //NCONFIG_H

View File

@@ -1,6 +0,0 @@
#ifndef STORAGE_H_
#define STORAGE_H_
void storage_init(void);
#endif /* STORAGE_H_ */

View File

@@ -1,33 +1,12 @@
/**
* @file system.h
* @brief Declares system-level functions for core operations like rebooting and starting services.
*/
//
// Created by shinys on 25. 8. 5.
//
#ifndef SYSTEM_H
#define SYSTEM_H
/**
* @brief Starts a timer that will reboot the system after a specified duration.
*
* This function is non-blocking. It schedules a system reboot to occur in the future.
* @param sec The number of seconds to wait before rebooting.
*/
void start_reboot_timer(int sec);
/**
* @brief Stops any pending reboot timer that was previously started.
*
* If a reboot timer is active, this function will cancel it, preventing the system from rebooting.
*/
void stop_reboot_timer();
void start_webserver();
/**
* @brief Initializes and starts the main web server.
*
* This function sets up the HTTP server, registers all URI handlers for web pages,
* API endpoints (like control and settings), and the WebSocket endpoint. It also
* initializes the status monitor that provides real-time data.
*/
void start_webserver(void);
#endif // SYSTEM_H
#endif //SYSTEM_H

View File

@@ -8,106 +8,16 @@
#include "esp_netif_types.h"
#include "esp_wifi_types_generic.h"
/**
* @brief Initializes the Wi-Fi driver, network interfaces, and event handlers.
*/
void wifi_init(void);
/**
* @brief Converts a Wi-Fi authentication mode enum to its string representation.
* @param mode The Wi-Fi authentication mode.
* @return A string describing the authentication mode.
*/
const char* auth_mode_str(wifi_auth_mode_t mode);
/**
* @brief Converts a Wi-Fi disconnection reason enum to its string representation.
* @param reason The reason for disconnection.
* @return A string describing the reason.
*/
const char* wifi_reason_str(wifi_err_reason_t reason);
/**
* @brief Connects the device to the configured Wi-Fi AP in STA mode.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_connect(void);
/**
* @brief Disconnects the device from the current Wi-Fi AP in STA mode.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_disconnect(void);
/**
* @brief Scans for available Wi-Fi access points.
* @param ap_records A pointer to store the found AP records.
* @param count A pointer to store the number of found APs.
*/
void wifi_scan_aps(wifi_ap_record_t** ap_records, uint16_t* count);
/**
* @brief Gets information about the currently connected access point.
* @param ap_info A pointer to a structure to store the AP information.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t* ap_info);
/**
* @brief Gets the current IP information for the STA interface.
* @param ip_info A pointer to a structure to store the IP information.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t* ip_info);
/**
* @brief Gets the DNS server information for the STA interface.
* @param type The type of DNS server (main, backup, fallback).
* @param dns_info A pointer to a structure to store the DNS information.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t* dns_info);
/**
* @brief Configures the STA interface to use DHCP.
* @return ESP_OK on success, or an error code on failure.
*/
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);
/**
* @brief Configures the STA interface to use a static IP address.
* @param ip The static IP address.
* @param gw The gateway address.
* @param netmask The subnet mask.
* @param dns1 The primary DNS server.
* @param dns2 The secondary DNS server (optional).
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_use_static(const char* ip, const char* gw, const char* netmask, const char* dns1, const char* dns2);
/**
* @brief Switches the Wi-Fi operating mode (e.g., sta, apsta).
* @param mode The target Wi-Fi mode as a string ("sta" or "apsta").
* @return ESP_OK on success, or an error code on failure.
*/
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);
/**
* @brief Initializes the SNTP service for time synchronization.
*/
void initialize_sntp(void);
/**
* @brief Starts the SNTP time synchronization process.
*/
void sync_time();
/**
* @brief Sets the SSID and password for the STA mode, saves them to NVS, and connects to the AP.
* @param ssid The SSID of the access point.
* @param password The password of the access point.
* @return ESP_OK on success, or an error code on failure.
*/
esp_err_t wifi_sta_set_ap(const char* ssid, const char* password);
#endif // WIFI_H
#endif //WIFI_H

View File

@@ -48,9 +48,13 @@ static const blink_step_t solid_blink[] = {
{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,
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};
@@ -61,7 +65,7 @@ void init_led(void)
led_indicator_ledc_config_t ledc_config = {0};
led_indicator_config_t config = {0};
ledc_config.is_active_level_high = false;
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;
@@ -74,7 +78,7 @@ void init_led(void)
led_handle[LED_RED] = led_indicator_create(&config);
ledc_config.is_active_level_high = false;
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;

View File

@@ -4,14 +4,12 @@
#include "nconfig.h"
#include "indicator.h"
#include "system.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "esp_err.h"
static nvs_handle_t handle;
const static char* keys[NCONFIG_TYPE_MAX] = {
const static char *keys[NCONFIG_TYPE_MAX] = {
[WIFI_SSID] = "wifi_ssid",
[WIFI_PASSWORD] = "wifi_pw",
[WIFI_MODE] = "wifi_mode",
@@ -25,21 +23,15 @@ const static char* keys[NCONFIG_TYPE_MAX] = {
[NETIF_DNS2] = "dns2",
[NETIF_TYPE] = "dhcp",
[UART_BAUD_RATE] = "baudrate",
[VIN_CURRENT_LIMIT] = "vin_climit",
[MAIN_CURRENT_LIMIT] = "main_climit",
[USB_CURRENT_LIMIT] = "usb_climit",
[PAGE_USERNAME] = "username",
[PAGE_PASSWORD] = "password",
};
struct default_value
{
struct default_value {
enum nconfig_type type;
const char* value;
const char *value;
};
struct default_value const default_values[] = {
{WIFI_SSID, ""},
{WIFI_SSID, "HK_BOB_24G"},
{WIFI_PASSWORD, ""},
{NETIF_TYPE, "dhcp"},
{NETIF_HOSTNAME, "powermate"},
@@ -47,28 +39,22 @@ struct default_value const default_values[] = {
{NETIF_DNS1, "8.8.8.8"},
{NETIF_DNS2, "8.8.4.4"},
{WIFI_MODE, "apsta"},
{AP_SSID, "powermate"},
{AP_PASSWORD, "hardkernel"},
{VIN_CURRENT_LIMIT, "4.0"},
{MAIN_CURRENT_LIMIT, "3.0"},
{USB_CURRENT_LIMIT, "3.0"},
{PAGE_USERNAME, "admin"},
{PAGE_PASSWORD, "password"},
{AP_SSID, "odroid-pm"},
{AP_PASSWORD, "powermate"},
};
esp_err_t init_nconfig()
{
esp_err_t ret = nvs_open(NCONFIG_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (ret != ESP_OK)
return ret;
if (ret != ESP_OK) return ret;
for (int i = 0; i < sizeof(default_values) / sizeof(default_values[0]); ++i)
{
for (int i = 0; i < sizeof(default_values) / sizeof(default_values[0]); ++i) {
// check key is not exist or value is null
if (nconfig_value_is_not_set(default_values[i].type))
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
if (nconfig_write(default_values[i].type, default_values[i].value) != ESP_OK) // if nconfig write fail, system panic
return ESP_FAIL;
}
}
@@ -76,25 +62,17 @@ esp_err_t init_nconfig()
return ESP_OK;
}
void reset_nconfig()
esp_err_t nconfig_write(enum nconfig_type type, const char* data)
{
nvs_erase_all(handle);
led_set(LED_RED, BLINK_FAST);
start_reboot_timer(1);
return nvs_set_str(handle, keys[type], data);
}
bool nconfig_value_is_not_set(enum nconfig_type type)
esp_err_t nconfig_delete(enum nconfig_type type)
{
size_t len = 0;
esp_err_t err = nconfig_get_str_len(type, &len);
return (err != ESP_OK || len <= 1);
return nvs_erase_key(handle, keys[type]);
}
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)
esp_err_t nconfig_get_str_len(enum nconfig_type type, size_t *len)
{
return nvs_get_str(handle, keys[type], NULL, len);
}
@@ -107,4 +85,4 @@ esp_err_t nconfig_read(enum nconfig_type type, char* data, size_t 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);
}
}

View File

@@ -1 +0,0 @@
DisableFormat: true

View File

@@ -1,27 +0,0 @@
/* Automatically generated nanopb constant definitions */
/* Generated by nanopb-0.4.8 */
#include "status.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
PB_BIND(SensorChannelData, SensorChannelData, AUTO)
PB_BIND(SensorData, SensorData, AUTO)
PB_BIND(WifiStatus, WifiStatus, AUTO)
PB_BIND(UartData, UartData, AUTO)
PB_BIND(LoadSwStatus, LoadSwStatus, AUTO)
PB_BIND(StatusMessage, StatusMessage, AUTO)

View File

@@ -1,181 +0,0 @@
/* Automatically generated nanopb header */
/* Generated by nanopb-0.4.8 */
#ifndef PB_STATUS_PB_H_INCLUDED
#define PB_STATUS_PB_H_INCLUDED
#include <pb.h>
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
/* Struct definitions */
/* Represents data for a single sensor channel */
typedef struct _SensorChannelData {
float voltage;
float current;
float power;
} SensorChannelData;
/* Contains data for all sensor channels and system info */
typedef struct _SensorData {
bool has_usb;
SensorChannelData usb;
bool has_main;
SensorChannelData main;
bool has_vin;
SensorChannelData vin;
uint32_t timestamp;
uint32_t uptime_sec;
} SensorData;
/* Contains WiFi connection status */
typedef struct _WifiStatus {
bool connected;
pb_callback_t ssid;
int32_t rssi;
pb_callback_t ip_address;
} WifiStatus;
/* Contains raw UART data */
typedef struct _UartData {
pb_callback_t data;
} UartData;
/* Contains load sw status */
typedef struct _LoadSwStatus {
bool main;
bool usb;
} LoadSwStatus;
/* Top-level message for all websocket communication */
typedef struct _StatusMessage {
pb_size_t which_payload;
union {
SensorData sensor_data;
WifiStatus wifi_status;
LoadSwStatus sw_status;
UartData uart_data;
} payload;
} StatusMessage;
#ifdef __cplusplus
extern "C" {
#endif
/* Initializer values for message structs */
#define SensorChannelData_init_default {0, 0, 0}
#define SensorData_init_default {false, SensorChannelData_init_default, false, SensorChannelData_init_default, false, SensorChannelData_init_default, 0, 0}
#define WifiStatus_init_default {0, {{NULL}, NULL}, 0, {{NULL}, NULL}}
#define UartData_init_default {{{NULL}, NULL}}
#define LoadSwStatus_init_default {0, 0}
#define StatusMessage_init_default {0, {SensorData_init_default}}
#define SensorChannelData_init_zero {0, 0, 0}
#define SensorData_init_zero {false, SensorChannelData_init_zero, false, SensorChannelData_init_zero, false, SensorChannelData_init_zero, 0, 0}
#define WifiStatus_init_zero {0, {{NULL}, NULL}, 0, {{NULL}, NULL}}
#define UartData_init_zero {{{NULL}, NULL}}
#define LoadSwStatus_init_zero {0, 0}
#define StatusMessage_init_zero {0, {SensorData_init_zero}}
/* Field tags (for use in manual encoding/decoding) */
#define SensorChannelData_voltage_tag 1
#define SensorChannelData_current_tag 2
#define SensorChannelData_power_tag 3
#define SensorData_usb_tag 1
#define SensorData_main_tag 2
#define SensorData_vin_tag 3
#define SensorData_timestamp_tag 4
#define SensorData_uptime_sec_tag 5
#define WifiStatus_connected_tag 1
#define WifiStatus_ssid_tag 2
#define WifiStatus_rssi_tag 3
#define WifiStatus_ip_address_tag 4
#define UartData_data_tag 1
#define LoadSwStatus_main_tag 1
#define LoadSwStatus_usb_tag 2
#define StatusMessage_sensor_data_tag 1
#define StatusMessage_wifi_status_tag 2
#define StatusMessage_sw_status_tag 3
#define StatusMessage_uart_data_tag 4
/* Struct field encoding specification for nanopb */
#define SensorChannelData_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, FLOAT, voltage, 1) \
X(a, STATIC, SINGULAR, FLOAT, current, 2) \
X(a, STATIC, SINGULAR, FLOAT, power, 3)
#define SensorChannelData_CALLBACK NULL
#define SensorChannelData_DEFAULT NULL
#define SensorData_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, MESSAGE, usb, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, main, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, vin, 3) \
X(a, STATIC, SINGULAR, UINT32, timestamp, 4) \
X(a, STATIC, SINGULAR, UINT32, uptime_sec, 5)
#define SensorData_CALLBACK NULL
#define SensorData_DEFAULT NULL
#define SensorData_usb_MSGTYPE SensorChannelData
#define SensorData_main_MSGTYPE SensorChannelData
#define SensorData_vin_MSGTYPE SensorChannelData
#define WifiStatus_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, connected, 1) \
X(a, CALLBACK, SINGULAR, STRING, ssid, 2) \
X(a, STATIC, SINGULAR, INT32, rssi, 3) \
X(a, CALLBACK, SINGULAR, STRING, ip_address, 4)
#define WifiStatus_CALLBACK pb_default_field_callback
#define WifiStatus_DEFAULT NULL
#define UartData_FIELDLIST(X, a) \
X(a, CALLBACK, SINGULAR, BYTES, data, 1)
#define UartData_CALLBACK pb_default_field_callback
#define UartData_DEFAULT NULL
#define LoadSwStatus_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, main, 1) \
X(a, STATIC, SINGULAR, BOOL, usb, 2)
#define LoadSwStatus_CALLBACK NULL
#define LoadSwStatus_DEFAULT NULL
#define StatusMessage_FIELDLIST(X, a) \
X(a, STATIC, ONEOF, MESSAGE, (payload,sensor_data,payload.sensor_data), 1) \
X(a, STATIC, ONEOF, MESSAGE, (payload,wifi_status,payload.wifi_status), 2) \
X(a, STATIC, ONEOF, MESSAGE, (payload,sw_status,payload.sw_status), 3) \
X(a, STATIC, ONEOF, MESSAGE, (payload,uart_data,payload.uart_data), 4)
#define StatusMessage_CALLBACK NULL
#define StatusMessage_DEFAULT NULL
#define StatusMessage_payload_sensor_data_MSGTYPE SensorData
#define StatusMessage_payload_wifi_status_MSGTYPE WifiStatus
#define StatusMessage_payload_sw_status_MSGTYPE LoadSwStatus
#define StatusMessage_payload_uart_data_MSGTYPE UartData
extern const pb_msgdesc_t SensorChannelData_msg;
extern const pb_msgdesc_t SensorData_msg;
extern const pb_msgdesc_t WifiStatus_msg;
extern const pb_msgdesc_t UartData_msg;
extern const pb_msgdesc_t LoadSwStatus_msg;
extern const pb_msgdesc_t StatusMessage_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define SensorChannelData_fields &SensorChannelData_msg
#define SensorData_fields &SensorData_msg
#define WifiStatus_fields &WifiStatus_msg
#define UartData_fields &UartData_msg
#define LoadSwStatus_fields &LoadSwStatus_msg
#define StatusMessage_fields &StatusMessage_msg
/* Maximum encoded size of messages (where known) */
/* WifiStatus_size depends on runtime parameters */
/* UartData_size depends on runtime parameters */
/* StatusMessage_size depends on runtime parameters */
#define LoadSwStatus_size 4
#define STATUS_PB_H_MAX_SIZE SensorData_size
#define SensorChannelData_size 15
#define SensorData_size 63
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif

View File

@@ -1,222 +0,0 @@
#include "auth.h"
#include <esp_http_server.h>
#include <esp_random.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
static const char* TAG = "AUTH";
typedef struct
{
char token[TOKEN_LENGTH];
bool active;
time_t creation_time;
} auth_token_t;
static auth_token_t s_tokens[MAX_TOKENS];
static SemaphoreHandle_t s_token_mutex;
void auth_init(void)
{
s_token_mutex = xSemaphoreCreateMutex();
if (s_token_mutex == NULL)
{
ESP_LOGE(TAG, "Failed to create token mutex");
return;
}
for (int i = 0; i < MAX_TOKENS; i++)
{
s_tokens[i].active = false;
s_tokens[i].token[0] = '\0';
}
ESP_LOGI(TAG, "Auth module initialized.");
}
char* auth_generate_token(void)
{
if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE)
{
ESP_LOGE(TAG, "Failed to take token mutex");
return NULL;
}
int free_slot = -1;
for (int i = 0; i < MAX_TOKENS; i++)
{
if (!s_tokens[i].active)
{
free_slot = i;
break;
}
}
if (free_slot == -1)
{
ESP_LOGW(TAG, "No free token slots available. Invalidating oldest token.");
time_t oldest_time = time(NULL);
int oldest_idx = -1;
for (int i = 0; i < MAX_TOKENS; i++)
{
if (s_tokens[i].active && s_tokens[i].creation_time < oldest_time)
{
oldest_time = s_tokens[i].creation_time;
oldest_idx = i;
}
}
if (oldest_idx != -1)
{
s_tokens[oldest_idx].active = false;
free_slot = oldest_idx;
ESP_LOGI(TAG, "Oldest token at index %d invalidated.", oldest_idx);
}
else
{
ESP_LOGE(TAG, "Could not find an oldest token to invalidate. This should not happen if all are active.");
xSemaphoreGive(s_token_mutex);
return NULL;
}
}
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
char* new_token = (char*)malloc(TOKEN_LENGTH);
if (new_token == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory for new token");
xSemaphoreGive(s_token_mutex);
return NULL;
}
for (int i = 0; i < TOKEN_LENGTH - 1; i++)
{
new_token[i] = charset[esp_random() % (sizeof(charset) - 1)];
}
new_token[TOKEN_LENGTH - 1] = '\0';
strncpy(s_tokens[free_slot].token, new_token, TOKEN_LENGTH);
s_tokens[free_slot].active = true;
s_tokens[free_slot].creation_time = time(NULL);
ESP_LOGI(TAG, "Generated new token at slot %d: %s", free_slot, new_token);
xSemaphoreGive(s_token_mutex);
return new_token;
}
bool auth_validate_token(const char* token)
{
if (token == NULL)
{
return false;
}
if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE)
{
ESP_LOGE(TAG, "Failed to take token mutex");
return false;
}
bool valid = false;
for (int i = 0; i < MAX_TOKENS; i++)
{
if (s_tokens[i].active && strcmp(s_tokens[i].token, token) == 0)
{
valid = true;
break;
}
}
xSemaphoreGive(s_token_mutex);
return valid;
}
void auth_invalidate_token(const char* token)
{
if (token == NULL)
{
return;
}
if (xSemaphoreTake(s_token_mutex, portMAX_DELAY) != pdTRUE)
{
ESP_LOGE(TAG, "Failed to take token mutex");
return;
}
for (int i = 0; i < MAX_TOKENS; i++)
{
if (s_tokens[i].active && strcmp(s_tokens[i].token, token) == 0)
{
s_tokens[i].active = false;
s_tokens[i].token[0] = '\0'; // Clear token string
ESP_LOGI(TAG, "Token at slot %d invalidated.", i);
break;
}
}
xSemaphoreGive(s_token_mutex);
}
void auth_cleanup_expired_tokens(void) { ESP_LOGD(TAG, "auth_cleanup_expired_tokens called (no-op for now)."); }
static const char* get_token_from_header(httpd_req_t* req)
{
char* auth_header = NULL;
size_t buf_len;
if (httpd_req_get_hdr_value_len(req, "Authorization") == 0)
{
return NULL;
}
buf_len = httpd_req_get_hdr_value_len(req, "Authorization") + 1;
auth_header = (char*)malloc(buf_len);
if (auth_header == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory for auth header");
return NULL;
}
if (httpd_req_get_hdr_value_str(req, "Authorization", auth_header, buf_len) != ESP_OK)
{
free(auth_header);
return NULL;
}
if (strncmp(auth_header, "Bearer ", 7) == 0)
{
const char* token = auth_header + 7;
return token;
}
free(auth_header);
return NULL;
}
esp_err_t api_auth_check(httpd_req_t* req)
{
const char* token = get_token_from_header(req);
if (token == NULL)
{
ESP_LOGW(TAG, "API access attempt without token for URI: %s", req->uri);
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Authorization token required");
return ESP_FAIL;
}
if (!auth_validate_token(token))
{
ESP_LOGW(TAG, "API access attempt with invalid token for URI: %s", req->uri);
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid or expired token");
free((void*)token - 7);
return ESP_FAIL;
}
ESP_LOGD(TAG, "Token validated for URI: %s", req->uri);
free((void*)token - 7);
return ESP_OK;
}

View File

@@ -1,28 +0,0 @@
#ifndef AUTH_H
#define AUTH_H
#include <stdbool.h>
#include "esp_err.h"
#include "esp_http_server.h"
#define MAX_TOKENS 4
#define TOKEN_LENGTH 33 // 32 characters + null terminator
// Function to initialize the authentication module
void auth_init(void);
// Function to generate a new token
char* auth_generate_token(void);
// Function to validate a token
bool auth_validate_token(const char* token);
// Function to invalidate a token (e.g., on logout)
void auth_invalidate_token(const char* token);
// Function to clean up expired tokens (if any)
void auth_cleanup_expired_tokens(void);
esp_err_t api_auth_check(httpd_req_t* req);
#endif // AUTH_H

View File

@@ -1,21 +0,0 @@
//
// Created by shinys on 25. 9. 4..
//
#ifndef ODROID_POWER_MATE_CLIMIT_H
#define ODROID_POWER_MATE_CLIMIT_H
#include "esp_err.h"
#include <stdbool.h>
#define VIN_CURRENT_LIMIT_MAX 8.0f
#define MAIN_CURRENT_LIMIT_MAX 7.5f
#define USB_CURRENT_LIMIT_MAX 4.5f
esp_err_t climit_set_vin(double value);
esp_err_t climit_set_main(double value);
esp_err_t climit_set_usb(double value);
bool is_overcurrent();
#endif // ODROID_POWER_MATE_CLIMIT_H

View File

@@ -1,26 +1,49 @@
#include "auth.h"
#include "cJSON.h"
#include "webserver.h"
#include "driver/gpio.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "cJSON.h"
#include "freertos/FreeRTOS.h"
#include "sw.h"
#include "webserver.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_timer.h"
static esp_err_t control_get_handler(httpd_req_t* req)
#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)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
gpio_num_t gpio_pin = (int) arg;
gpio_set_level(gpio_pin, 1); // 핀을 다시 HIGH로 복구
}
cJSON* root = cJSON_CreateObject();
static void update_gpio_switches()
{
gpio_set_level(GPIO_12V_SWITCH, status_12v_on);
gpio_set_level(GPIO_5V_SWITCH, status_5v_on);
}
cJSON_AddBoolToObject(root, "load_12v_on", get_main_load_switch());
cJSON_AddBoolToObject(root, "load_5v_on", get_usb_load_switch());
static esp_err_t control_get_handler(httpd_req_t *req)
{
cJSON *root = cJSON_CreateObject();
char* json_string = cJSON_Print(root);
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));
@@ -30,56 +53,64 @@ static esp_err_t control_get_handler(httpd_req_t* req)
return ESP_OK;
}
static esp_err_t control_post_handler(httpd_req_t* req)
static esp_err_t control_post_handler(httpd_req_t *req)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
char buf[128];
int ret, remaining = req->content_len;
if (remaining >= sizeof(buf))
{
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)
{
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
buf[ret] = '\0';
cJSON* root = cJSON_Parse(buf);
if (root == NULL)
{
cJSON *root = cJSON_Parse(buf);
if (root == NULL) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON format");
return ESP_FAIL;
}
cJSON* item_12v = cJSON_GetObjectItem(root, "load_12v_on");
if (cJSON_IsBool(item_12v))
set_main_load_switch(cJSON_IsTrue(item_12v));
bool state_changed = false;
xSemaphoreTake(state_mutex, portMAX_DELAY);
cJSON* item_5v = cJSON_GetObjectItem(root, "load_5v_on");
if (cJSON_IsBool(item_5v))
set_usb_load_switch(cJSON_IsTrue(item_5v));
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* power_trigger = cJSON_GetObjectItem(root, "power_trigger");
if (cJSON_IsTrue(power_trigger))
trig_power();
cJSON *item_5v = cJSON_GetObjectItem(root, "load_5v_on");
if (cJSON_IsBool(item_5v)) {
status_5v_on = cJSON_IsTrue(item_5v);
state_changed = true;
}
cJSON* reset_trigger = cJSON_GetObjectItem(root, "reset_trigger");
if (cJSON_IsTrue(reset_trigger))
trig_reset();
if (state_changed) {
update_gpio_switches();
}
xSemaphoreGive(state_mutex);
cJSON *power_trigger = cJSON_GetObjectItem(root, "power_trigger");
if (cJSON_IsTrue(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)) {
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);
@@ -87,13 +118,64 @@ static esp_err_t control_post_handler(httpd_req_t* req)
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));
}
void register_control_endpoint(httpd_handle_t server)
{
init_sw();
httpd_uri_t get_uri = {.uri = "/api/control", .method = HTTP_GET, .handler = control_get_handler, .user_ctx = NULL};
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};
.uri = "/api/control",
.method = HTTP_POST,
.handler = control_post_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &post_uri);
}
}

149
main/service/datalog.c Normal file
View File

@@ -0,0 +1,149 @@
#include "datalog.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include "esp_littlefs.h"
#include "esp_log.h"
static const char* TAG = "DATALOG";
static const char* LOG_FILE_PATH = "/littlefs/datalog.csv";
#define MAX_LOG_SIZE (512 * 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)
{
struct stat st;
if (stat(LOG_FILE_PATH, &st) == 0)
{
if (st.st_size >= MAX_LOG_SIZE)
{
ESP_LOGI(TAG, "Log file size (%ld) >= MAX_LOG_SIZE (%d). Truncating.", st.st_size, MAX_LOG_SIZE);
const char* temp_path = "/littlefs/datalog.tmp";
FILE* f_read = fopen(LOG_FILE_PATH, "r");
FILE* f_write = fopen(temp_path, "w");
if (f_read == NULL || f_write == NULL)
{
ESP_LOGE(TAG, "Failed to open files for truncation.");
if (f_read) fclose(f_read);
if (f_write) fclose(f_write);
return;
}
char line[256];
// Copy header
if (fgets(line, sizeof(line), f_read) != NULL)
{
fputs(line, f_write);
}
// Skip the oldest data line
if (fgets(line, sizeof(line), f_read) == NULL)
{
// No data lines to skip, something is wrong if we are truncating
}
// Copy the rest of the lines
while (fgets(line, sizeof(line), f_read) != NULL)
{
fputs(line, f_write);
}
fclose(f_read);
fclose(f_write);
// Replace the old log with the new one
if (remove(LOG_FILE_PATH) != 0)
{
ESP_LOGE(TAG, "Failed to remove old log file.");
}
if (rename(temp_path, LOG_FILE_PATH) != 0)
{
ESP_LOGE(TAG, "Failed to rename temp file.");
return;
}
}
}
FILE* f = fopen(LOG_FILE_PATH, "a");
if (f == NULL)
{
ESP_LOGE(TAG, "Failed to open log file for appending.");
return;
}
fprintf(f, "%lu,%.3f,%.3f,%.3f\n", timestamp, voltage, current, power);
fclose(f);
}
const char* datalog_get_path(void)
{
return LOG_FILE_PATH;
}

10
main/service/datalog.h Normal file
View 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_ */

View File

@@ -2,293 +2,134 @@
// Created by shinys on 25. 8. 18..
//
#include "monitor.h"
#include <nconfig.h>
#include <time.h>
#include "climit.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_wifi_types_generic.h"
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" // Added for FreeRTOS tasks
#include "ina3221.h"
#include "pb.h"
#include "pb_encode.h"
#include "status.pb.h"
#include "sw.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 CHANNEL_VIN INA3221_CHANNEL_3
#define CHANNEL_MAIN INA3221_CHANNEL_2
#define CHANNEL_USB INA3221_CHANNEL_1
#define INA226_SDA CONFIG_GPIO_INA226_SDA
#define INA226_SCL CONFIG_GPIO_INA226_SCL
#define PM_SDA CONFIG_I2C_GPIO_SDA
#define PM_SCL CONFIG_I2C_GPIO_SCL
ina226_t ina;
i2c_master_bus_handle_t bus_handle;
i2c_master_dev_handle_t dev_handle;
#define PM_INT_CRITICAL CONFIG_GPIO_INA3221_INT_CRITICAL
#define PM_EXPANDER_RST CONFIG_GPIO_EXPANDER_RESET
#define PB_BUFFER_SIZE 256
static const char* TAG = "monitor";
static esp_timer_handle_t sensor_timer;
static esp_timer_handle_t wifi_status_timer;
static esp_timer_handle_t long_press_timer;
// static esp_timer_handle_t shutdown_load_sw; // No longer needed
static TaskHandle_t shutdown_task_handle = NULL; // Global task handle
ina3221_t ina3221 = {
.shunt = {10, 10, 10},
.mask.mask_register = INA3221_DEFAULT_MASK,
.i2c_dev = {0},
.config =
{
.mode = true, // mode selection
.esht = true, // shunt enable
.ebus = true, // bus enable
.ch1 = true, // channel 1 enable
.ch2 = true, // channel 2 enable
.ch3 = true, // channel 3 enable
.avg = INA3221_AVG_64, // 64 samples average
.vbus = INA3221_CT_2116, // 2ms by channel (bus)
.vsht = INA3221_CT_2116, // 2ms by channel (shunt)
},
};
static bool encode_string(pb_ostream_t* stream, const pb_field_t* field, void* const* arg)
// Timer callback function to read sensor data
static void sensor_timer_callback(void *arg)
{
const char* str = (const char*)(*arg);
if (!str)
{
return true; // Nothing to encode
}
if (!pb_encode_tag_for_field(stream, field))
{
return false;
}
return pb_encode_string(stream, (uint8_t*)str, strlen(str));
}
// Generate random sensor data
float voltage = 0;
float current = 0;
float power = 0;
static void send_pb_message(const pb_msgdesc_t* fields, const void* src_struct)
{
uint8_t buffer[PB_BUFFER_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
ina226_get_bus_voltage(&ina, &voltage);
ina226_get_power(&ina, &power);
ina226_get_current(&ina, &current);
if (!pb_encode(&stream, fields, src_struct))
{
ESP_LOGE(TAG, "Failed to encode protobuf message: %s", PB_GET_ERROR(&stream));
return;
}
push_data_to_ws(buffer, stream.bytes_written);
}
static void sensor_timer_callback(void* arg)
{
// 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);
StatusMessage message = StatusMessage_init_zero;
message.which_payload = StatusMessage_sensor_data_tag;
SensorData* sensor_data = &message.payload.sensor_data;
datalog_add(timestamp, voltage, current, power);
sensor_data->has_usb = true;
sensor_data->has_main = true;
sensor_data->has_vin = true;
// 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);
SensorChannelData* channels[] = {&sensor_data->usb, &sensor_data->main, &sensor_data->vin};
for (uint8_t i = 0; i < INA3221_BUS_NUMBER; i++)
{
float voltage, current, power;
ina3221_get_bus_voltage(&ina3221, i, &voltage);
ina3221_get_shunt_value(&ina3221, i, NULL, &current);
current /= 1000.0f; // mA to A
power = voltage * current;
// For protobuf
channels[i]->voltage = voltage;
channels[i]->current = current;
channels[i]->power = power;
}
// datalog_add(timestamp, channel_data_log);
sensor_data->timestamp = timestamp;
sensor_data->uptime_sec = uptime_sec;
send_pb_message(StatusMessage_fields, &message);
// Push data to WebSocket clients
push_data_to_ws(root);
}
static void status_wifi_callback(void* arg)
static void status_wifi_callback(void *arg)
{
wifi_ap_record_t ap_info;
StatusMessage message = StatusMessage_init_zero;
message.which_payload = StatusMessage_wifi_status_tag;
WifiStatus* wifi_status = &message.payload.wifi_status;
char ip_str[16];
esp_netif_ip_info_t ip_info;
cJSON *root = cJSON_CreateObject();
if (wifi_get_current_ap_info(&ap_info) == ESP_OK)
{
wifi_status->connected = true;
wifi_status->ssid.funcs.encode = &encode_string;
wifi_status->ssid.arg = (void*)ap_info.ssid;
wifi_status->rssi = ap_info.rssi;
}
else
{
wifi_status->connected = false;
wifi_status->ssid.arg = ""; // Empty string
wifi_status->rssi = 0;
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);
}
if (wifi_get_current_ip_info(&ip_info) == ESP_OK)
{
esp_ip4addr_ntoa(&ip_info.ip, ip_str, sizeof(ip_str));
wifi_status->ip_address.funcs.encode = &encode_string;
wifi_status->ip_address.arg = ip_str;
}
else
{
wifi_status->ip_address.arg = ""; // Empty string
}
send_pb_message(StatusMessage_fields, &message);
push_data_to_ws(root);
}
// Placeholder for long press action
static void handle_critical_long_press(void)
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()
{
ESP_LOGW(TAG, "Config reset triggered...");
reset_nconfig();
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 = true,
};
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));
}
// Timer callback for long press detection
static void long_press_timer_callback(void* arg)
{
if (gpio_get_level(PM_INT_CRITICAL) == 0)
{
handle_critical_long_press();
}
}
// New FreeRTOS task for shutdown logic
static void shutdown_load_sw_task(void* pvParameters)
{
while (1)
{
// Wait indefinitely for a notification from the ISR
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
ESP_LOGW(TAG, "critical interrupt triggered (via task)");
gpio_set_level(PM_EXPANDER_RST, 0);
vTaskDelay(100 / portTICK_PERIOD_MS);
gpio_set_level(PM_EXPANDER_RST, 1);
config_sw();
// Start a 5-second timer to check for long press
esp_timer_start_once(long_press_timer, 5000000);
}
}
static void IRAM_ATTR critical_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (gpio_get_level(PM_INT_CRITICAL) == 0) // Falling edge
{
if (shutdown_task_handle != NULL)
{
vTaskNotifyGiveFromISR(shutdown_task_handle, &xHigherPriorityTaskWoken);
}
}
else // Rising edge
{
// Stop the timer if the button is released
esp_timer_stop(long_press_timer);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
static void gpio_init()
{
// critical int
gpio_set_intr_type(PM_INT_CRITICAL, GPIO_INTR_ANYEDGE);
gpio_set_direction(PM_INT_CRITICAL, GPIO_MODE_INPUT);
gpio_install_isr_service(0);
gpio_isr_handler_add(PM_INT_CRITICAL, critical_isr_handler, (void*)PM_INT_CRITICAL);
// rst expander
gpio_set_level(PM_EXPANDER_RST, 1);
gpio_set_direction(PM_EXPANDER_RST, GPIO_MODE_OUTPUT);
}
esp_err_t climit_set_vin(double value)
{
float lim = (float)(value * 1000);
ESP_LOGI(TAG, "Setting VIN current limit to: %fmA", lim);
if (value > 0.0f)
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, lim);
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, (15.0f * 1000.0f));
}
esp_err_t climit_set_main(double value)
{
float lim = (float)(value * 1000);
ESP_LOGI(TAG, "Setting MAIN current limit to: %fmA", lim);
if (value > 0.0f)
return ina3221_set_critical_alert(&ina3221, CHANNEL_MAIN, lim);
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, (15.0f * 1000.0f));
}
esp_err_t climit_set_usb(double value)
{
float lim = (float)(value * 1000);
ESP_LOGI(TAG, "Setting USB current limit to: %fmA", lim);
if (value > 0.0f)
return ina3221_set_critical_alert(&ina3221, CHANNEL_USB, lim);
return ina3221_set_critical_alert(&ina3221, CHANNEL_VIN, (15.0f * 1000.0f));
}
static esp_timer_handle_t sensor_timer;
static esp_timer_handle_t wifi_status_timer;
void init_status_monitor()
{
gpio_init();
ESP_ERROR_CHECK(ina3221_init_desc(&ina3221, 0x40, 0, PM_SDA, PM_SCL));
init_ina226();
datalog_init();
double lim;
char buf[10];
// Timer configuration
const esp_timer_create_args_t sensor_timer_args = {
.callback = &sensor_timer_callback,
.name = "sensor_reading_timer" // Optional name for debugging
};
nconfig_read(VIN_CURRENT_LIMIT, buf, sizeof(buf));
lim = atof(buf);
climit_set_vin(lim);
nconfig_read(MAIN_CURRENT_LIMIT, buf, sizeof(buf));
lim = atof(buf);
climit_set_main(lim);
nconfig_read(USB_CURRENT_LIMIT, buf, sizeof(buf));
lim = atof(buf);
climit_set_usb(lim);
const esp_timer_create_args_t sensor_timer_args = {.callback = &sensor_timer_callback,
.name = "sensor_reading_timer"};
const esp_timer_create_args_t wifi_timer_args = {.callback = &status_wifi_callback, .name = "wifi_status_timer"};
const esp_timer_create_args_t long_press_timer_args = {.callback = &long_press_timer_callback,
.name = "long_press_timer"};
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_create(&long_press_timer_args, &long_press_timer));
xTaskCreate(shutdown_load_sw_task, "shutdown_sw_task", configMINIMAL_STACK_SIZE * 3, NULL, 15,
&shutdown_task_handle);
ESP_ERROR_CHECK(esp_timer_start_periodic(sensor_timer, 1000000));
ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_status_timer, 1000000 * 5));
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
}

View File

@@ -9,16 +9,17 @@
#include "esp_http_server.h"
// 버퍼에 저장할 데이터의 개수
#define SENSOR_BUFFER_SIZE 100
typedef struct
{
// 단일 센서 데이터를 저장하기 위한 구조체
typedef struct {
float voltage;
float current;
float power;
uint32_t timestamp;
uint32_t timestamp; // 데이터를 읽은 시간 (부팅 후 ms)
} sensor_data_t;
void init_status_monitor();
#endif // ODROID_REMOTE_HTTP_MONITOR_H
#endif //ODROID_REMOTE_HTTP_MONITOR_H

View File

@@ -1,76 +1,48 @@
#include <stdlib.h>
#include "auth.h"
#include "webserver.h"
#include "cJSON.h"
#include "climit.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "nconfig.h"
#include "webserver.h"
#include "wifi.h"
#include "system.h"
#include "esp_netif.h"
#include "freertos/task.h"
static const char* TAG = "webserver";
static const char *TAG = "webserver";
static esp_err_t setting_get_handler(httpd_req_t* req)
static esp_err_t setting_get_handler(httpd_req_t *req)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
wifi_ap_record_t ap_info;
cJSON* root = cJSON_CreateObject();
cJSON *root = cJSON_CreateObject();
char buf[16];
if (nconfig_read(WIFI_MODE, buf, sizeof(buf)) == ESP_OK)
{
cJSON_AddStringToObject(root, "mode", buf);
}
else
{
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
}
if (nconfig_read(NETIF_TYPE, buf, sizeof(buf)) == ESP_OK)
{
cJSON_AddStringToObject(root, "net_type", buf);
}
else
{
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
}
if (nconfig_read(UART_BAUD_RATE, buf, sizeof(buf)) == ESP_OK)
{
cJSON_AddStringToObject(root, "baudrate", buf);
// 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);
}
// Add current limits to the response
if (nconfig_read(VIN_CURRENT_LIMIT, buf, sizeof(buf)) == ESP_OK)
{
cJSON_AddNumberToObject(root, "vin_current_limit", atof(buf));
}
if (nconfig_read(MAIN_CURRENT_LIMIT, buf, sizeof(buf)) == ESP_OK)
{
cJSON_AddNumberToObject(root, "main_current_limit", atof(buf));
}
if (nconfig_read(USB_CURRENT_LIMIT, buf, sizeof(buf)) == ESP_OK)
{
cJSON_AddNumberToObject(root, "usb_current_limit", atof(buf));
}
if (wifi_get_current_ap_info(&ap_info) == ESP_OK)
{
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_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)
{
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);
@@ -82,24 +54,21 @@ static esp_err_t setting_get_handler(httpd_req_t* req)
esp_netif_dns_info_t dns_info;
char dns_str[16];
if (wifi_get_dns_info(ESP_NETIF_DNS_MAIN, &dns_info) == ESP_OK)
{
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)
{
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
{
} else {
cJSON_AddBoolToObject(root, "connected", false);
}
const char* json_string = cJSON_Print(root);
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);
@@ -108,24 +77,18 @@ static esp_err_t setting_get_handler(httpd_req_t* req)
return ESP_OK;
}
static esp_err_t wifi_scan(httpd_req_t* req)
static esp_err_t wifi_scan(httpd_req_t *req)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
wifi_ap_record_t* ap_records;
wifi_ap_record_t *ap_records;
uint16_t count;
wifi_scan_aps(&ap_records, &count);
cJSON* root = cJSON_CreateArray();
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 *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);
@@ -135,7 +98,7 @@ static esp_err_t wifi_scan(httpd_req_t* req)
free(ap_records);
const char* json_string = cJSON_Print(root);
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);
@@ -144,95 +107,67 @@ static esp_err_t wifi_scan(httpd_req_t* req)
return ESP_OK;
}
static esp_err_t setting_post_handler(httpd_req_t* req)
static esp_err_t setting_post_handler(httpd_req_t *req)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
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);
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)
{
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");
cJSON* vin_climit_item = cJSON_GetObjectItem(root, "vin_current_limit");
cJSON* main_climit_item = cJSON_GetObjectItem(root, "main_current_limit");
cJSON* usb_climit_item = cJSON_GetObjectItem(root, "usb_current_limit");
cJSON* new_username_item = cJSON_GetObjectItem(root, "new_username");
cJSON* new_password_item = cJSON_GetObjectItem(root, "new_password");
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))
{
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 (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))
{
if (ap_ssid_item && cJSON_IsString(ap_ssid_item)) {
nconfig_write(AP_SSID, ap_ssid_item->valuestring);
}
else
{
} 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))
{
if (ap_pass_item && cJSON_IsString(ap_pass_item)) {
nconfig_write(AP_PASSWORD, ap_pass_item->valuestring);
}
else
{
} else {
nconfig_delete(AP_PASSWORD); // Open network
}
}
wifi_switch_mode(mode);
httpd_resp_sendstr(req, "{\"status\":\"mode_switch_initiated\"}");
}
else
{
} else {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid mode");
}
}
else if (net_type_item && cJSON_IsString(net_type_item))
{
} 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");
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;
@@ -240,103 +175,45 @@ static esp_err_t setting_post_handler(httpd_req_t* req)
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)
{
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);
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
{
} else {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing static IP fields");
}
}
else if (strcmp(type, "dhcp") == 0)
{
} 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))
{
httpd_resp_sendstr(req, "{\"status\":\"connection_initiated\"}");
} 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
wifi_sta_set_ap(ssid_item->valuestring, pass_item->valuestring);
}
else
{
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))
{
} 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 if (vin_climit_item || main_climit_item || usb_climit_item)
{
char num_buf[10];
if (vin_climit_item && cJSON_IsNumber(vin_climit_item))
{
double val = vin_climit_item->valuedouble;
if (val >= 0.0 && val <= VIN_CURRENT_LIMIT_MAX)
{
snprintf(num_buf, sizeof(num_buf), "%.2f", val);
nconfig_write(VIN_CURRENT_LIMIT, num_buf);
climit_set_vin(val);
}
}
if (main_climit_item && cJSON_IsNumber(main_climit_item))
{
double val = main_climit_item->valuedouble;
if (val >= 0.0 && val <= MAIN_CURRENT_LIMIT_MAX)
{
snprintf(num_buf, sizeof(num_buf), "%.2f", val);
nconfig_write(MAIN_CURRENT_LIMIT, num_buf);
climit_set_main(val);
}
}
if (usb_climit_item && cJSON_IsNumber(usb_climit_item))
{
double val = usb_climit_item->valuedouble;
if (val >= 0.0 && val <= USB_CURRENT_LIMIT_MAX)
{
snprintf(num_buf, sizeof(num_buf), "%.2f", val);
nconfig_write(USB_CURRENT_LIMIT, num_buf);
climit_set_usb(val);
}
}
httpd_resp_sendstr(req, "{\"status\":\"current_limit_updated\"}");
}
else if (new_username_item && cJSON_IsString(new_username_item) && new_password_item &&
cJSON_IsString(new_password_item))
{
const char* new_username = new_username_item->valuestring;
const char* new_password = new_password_item->valuestring;
nconfig_write(PAGE_USERNAME, new_username);
nconfig_write(PAGE_PASSWORD, new_password);
ESP_LOGI(TAG, "Username and password updated successfully.");
httpd_resp_sendstr(req, "{\"status\":\"user_credentials_updated\"}");
}
else
{
} else {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid payload");
}
@@ -346,12 +223,27 @@ static esp_err_t setting_post_handler(httpd_req_t* req)
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_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_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_uri_t scan = {
.uri = "/api/wifi/scan",
.method = HTTP_GET,
.handler = wifi_scan,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &scan);
}

View File

@@ -1,53 +0,0 @@
#include "storage.h"
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "esp_littlefs.h"
#include "esp_log.h"
static const char* TAG = "datalog";
#define MAX_LOG_SIZE (700 * 1024)
void storage_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(conf.partition_label, &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);
}
}

View File

@@ -1,183 +0,0 @@
//
// Created by vl011 on 2025-08-28.
//
#include "sw.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <ina3221.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "pb.h"
#include "pb_encode.h"
#include "pca9557.h"
#include "status.pb.h"
#include "webserver.h"
#define I2C_PORT 0
#define GPIO_SDA CONFIG_I2C_GPIO_SDA
#define GPIO_SCL CONFIG_I2C_GPIO_SCL
#define GPIO_MAIN CONFIG_EXPANDER_GPIO_SW_12V
#define GPIO_USB CONFIG_EXPANDER_GPIO_SW_5V
#define GPIO_PWR CONFIG_EXPANDER_GPIO_TRIGGER_POWER
#define GPIO_RST CONFIG_EXPANDER_GPIO_TRIGGER_RESET
#define POWER_DELAY (CONFIG_TRIGGER_POWER_DELAY_MS * 1000)
#define RESET_DELAY (CONFIG_TRIGGER_RESET_DELAY_MS * 1000)
#define PB_BUFFER_SIZE 256
static const char* TAG = "control";
static bool load_switch_12v_status = false;
static bool load_switch_5v_status = false;
static SemaphoreHandle_t expander_mutex;
#define MUTEX_TIMEOUT (pdMS_TO_TICKS(100))
static i2c_dev_t pca = {0};
static esp_timer_handle_t power_trigger_timer;
static esp_timer_handle_t reset_trigger_timer;
static void send_sw_status_message()
{
StatusMessage message = StatusMessage_init_zero;
message.which_payload = StatusMessage_sw_status_tag;
LoadSwStatus* sw_status = &message.payload.sw_status;
sw_status->main = load_switch_12v_status;
sw_status->usb = load_switch_5v_status;
uint8_t buffer[PB_BUFFER_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&stream, StatusMessage_fields, &message))
{
ESP_LOGE(TAG, "Failed to encode protobuf message: %s", PB_GET_ERROR(&stream));
return;
}
push_data_to_ws(buffer, stream.bytes_written);
}
static void trigger_off_callback(void* arg)
{
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
{
ESP_LOGW(TAG, "Control error");
return;
}
uint32_t gpio_pin = (int)arg;
pca9557_set_level(&pca, gpio_pin, 1);
xSemaphoreGive(expander_mutex);
}
void config_sw()
{
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_MAIN, PCA9557_MODE_OUTPUT));
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_USB, PCA9557_MODE_OUTPUT));
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_PWR, PCA9557_MODE_OUTPUT));
ESP_ERROR_CHECK(pca9557_set_mode(&pca, GPIO_RST, PCA9557_MODE_OUTPUT));
ESP_ERROR_CHECK(pca9557_set_level(&pca, GPIO_PWR, 1));
ESP_ERROR_CHECK(pca9557_set_level(&pca, GPIO_RST, 1));
uint32_t val = 0;
ESP_ERROR_CHECK(pca9557_get_level(&pca, CONFIG_EXPANDER_GPIO_SW_12V, &val));
load_switch_12v_status = val != 0 ? true : false;
ESP_ERROR_CHECK(pca9557_get_level(&pca, CONFIG_EXPANDER_GPIO_SW_5V, &val));
load_switch_5v_status = val != 0 ? true : false;
send_sw_status_message();
}
void init_sw()
{
ESP_ERROR_CHECK(pca9557_init_desc(&pca, 0x18, I2C_PORT, GPIO_SDA, GPIO_SCL));
config_sw();
const esp_timer_create_args_t power_timer_args = {
.callback = &trigger_off_callback, .arg = (void*)GPIO_PWR, .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_RST, .name = "power_trigger_off"};
ESP_ERROR_CHECK(esp_timer_create(&reset_timer_args, &reset_trigger_timer));
expander_mutex = xSemaphoreCreateMutex();
}
void trig_power()
{
ESP_LOGI(TAG, "Trig power");
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
{
ESP_LOGW(TAG, "Control error");
return;
}
pca9557_set_level(&pca, GPIO_PWR, 0);
xSemaphoreGive(expander_mutex);
esp_timer_stop(power_trigger_timer);
esp_timer_start_once(power_trigger_timer, POWER_DELAY);
}
void trig_reset()
{
ESP_LOGI(TAG, "Trig reset");
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
{
ESP_LOGW(TAG, "Control error");
return;
}
pca9557_set_level(&pca, GPIO_RST, 0);
xSemaphoreGive(expander_mutex);
esp_timer_stop(reset_trigger_timer);
esp_timer_start_once(reset_trigger_timer, RESET_DELAY);
}
void set_main_load_switch(bool on)
{
ESP_LOGI(TAG, "Set main load switch to %s", on ? "on" : "off");
if (load_switch_12v_status == on)
return;
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
{
ESP_LOGW(TAG, "Control error");
return;
}
pca9557_set_level(&pca, GPIO_MAIN, on);
load_switch_12v_status = on;
xSemaphoreGive(expander_mutex);
send_sw_status_message();
}
void set_usb_load_switch(bool on)
{
ESP_LOGI(TAG, "Set usb load switch to %s", on ? "on" : "off");
if (load_switch_5v_status == on)
return;
if (xSemaphoreTake(expander_mutex, MUTEX_TIMEOUT) == pdFALSE)
{
ESP_LOGW(TAG, "Control error");
return;
}
pca9557_set_level(&pca, GPIO_USB, on);
load_switch_5v_status = on;
xSemaphoreGive(expander_mutex);
send_sw_status_message();
}
bool get_main_load_switch() { return load_switch_12v_status; }
bool get_usb_load_switch() { return load_switch_5v_status; }

View File

@@ -1,18 +0,0 @@
//
// Created by vl011 on 2025-08-28.
//
#ifndef ODROID_POWER_MATE_SW_H
#define ODROID_POWER_MATE_SW_H
#include <stdbool.h>
void init_sw();
void config_sw();
void trig_power();
void trig_reset();
void set_main_load_switch(bool on);
void set_usb_load_switch(bool on);
bool get_main_load_switch();
bool get_usb_load_switch();
#endif // ODROID_POWER_MATE_SW_H

View File

@@ -1,108 +0,0 @@
//
// Created by shinys on 25. 8. 5.
//
#include <system.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <string.h>
#include "auth.h"
#include "esp_http_server.h"
#include "esp_system.h"
static const char* TAG = "odroid";
static esp_timer_handle_t reboot_timer_handle = NULL;
static void reboot_timer_callback(void* arg)
{
ESP_LOGI(TAG, "Rebooting now...");
esp_restart();
}
void start_reboot_timer(int sec)
{
if (reboot_timer_handle != NULL)
{
ESP_LOGW(TAG, "The reboot timer is already running.");
return;
}
ESP_LOGI(TAG, "Device will reboot in %d seconds.", sec);
const esp_timer_create_args_t reboot_timer_args = {.callback = &reboot_timer_callback, .name = "reboot-timer"};
if (esp_timer_create(&reboot_timer_args, &reboot_timer_handle) != ESP_OK)
{
ESP_LOGE(TAG, "Failed to create reboot timer.");
reboot_timer_handle = NULL;
return;
}
if (esp_timer_start_once(reboot_timer_handle, (uint64_t)sec * 1000000) != ESP_OK)
{
ESP_LOGE(TAG, "Failed to start reboot timer.");
esp_timer_delete(reboot_timer_handle);
reboot_timer_handle = NULL;
return;
}
}
static esp_err_t reboot_post_handler(httpd_req_t* req)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
httpd_resp_set_type(req, "application/json");
const char* resp_str = "{\"status\": \"reboot timer started\"}";
httpd_resp_send(req, resp_str, strlen(resp_str));
start_reboot_timer(3);
return ESP_OK;
}
void stop_reboot_timer()
{
if (reboot_timer_handle == NULL)
{
return;
}
esp_timer_stop(reboot_timer_handle);
esp_timer_delete(reboot_timer_handle);
reboot_timer_handle = NULL;
ESP_LOGI(TAG, "Reboot timer stopped.");
}
void register_reboot_endpoint(httpd_handle_t server)
{
httpd_uri_t post_uri = {
.uri = "/api/reboot", .method = HTTP_POST, .handler = reboot_post_handler, .user_ctx = NULL};
httpd_register_uri_handler(server, &post_uri);
}
static esp_err_t version_get_handler(httpd_req_t* req)
{
esp_err_t err = api_auth_check(req);
if (err != ESP_OK)
{
return err;
}
httpd_resp_set_type(req, "application/json");
char buf[100];
sprintf(buf, "{\"version\": \"%s-%s\"}", VERSION_TAG, VERSION_HASH);
httpd_resp_send(req, buf, strlen(buf));
return ESP_OK;
}
void register_version_endpoint(httpd_handle_t server)
{
httpd_uri_t post_uri = {
.uri = "/api/version", .method = HTTP_GET, .handler = version_get_handler, .user_ctx = NULL};
httpd_register_uri_handler(server, &post_uri);
}

View File

@@ -1,196 +1,92 @@
#include "webserver.h"
#include <stdio.h>
#include <string.h>
#include "auth.h"
#include "cJSON.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "esp_http_server.h"
#include "nconfig.h"
#include "monitor.h"
#include "datalog.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "monitor.h"
#include "nconfig.h"
#include "system.h"
static const char* TAG = "WEBSERVER";
static const char *TAG = "WEBSERVER";
static esp_err_t index_handler(httpd_req_t* req)
{
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_hdr(req, "Cache-Control", "max-age=3600");
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, (const char *)index_html_start, index_html_size);
size_t remaining = index_html_size;
const char* ptr = (const char*)index_html_start;
while (remaining > 0)
{
size_t to_send = remaining < 2048 ? remaining : 2048;
if (httpd_resp_send_chunk(req, ptr, to_send) != ESP_OK)
{
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;
}
ptr += to_send;
remaining -= to_send;
}
fclose(f);
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
static esp_err_t login_handler(httpd_req_t* req)
{
char content[100]; // Adjust size as needed for username/password
int ret = httpd_req_recv(req, content, sizeof(content) - 1); // -1 for null terminator
if (ret <= 0)
{ // 0 means connection closed, < 0 means error
if (ret == HTTPD_SOCK_ERR_TIMEOUT)
{
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
content[ret] = '\0'; // Null-terminate the received data
ESP_LOGI(TAG, "Received login request: %s", content);
cJSON* root = cJSON_Parse(content);
if (root == NULL)
{
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL;
}
cJSON* username_json = cJSON_GetObjectItemCaseSensitive(root, "username");
cJSON* password_json = cJSON_GetObjectItemCaseSensitive(root, "password");
if (!cJSON_IsString(username_json) || (username_json->valuestring == NULL) || !cJSON_IsString(password_json) ||
(password_json->valuestring == NULL))
{
cJSON_Delete(root);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing username or password");
return ESP_FAIL;
}
const char* received_username = username_json->valuestring;
const char* received_password = password_json->valuestring;
// Get stored username and password from nconfig
size_t stored_username_len = 0;
size_t stored_password_len = 0;
char* stored_username = NULL;
char* stored_password = NULL;
bool credentials_match = false;
if (nconfig_get_str_len(PAGE_USERNAME, &stored_username_len) == ESP_OK && stored_username_len > 1)
{
stored_username = (char*)malloc(stored_username_len);
if (stored_username)
{
if (nconfig_read(PAGE_USERNAME, stored_username, stored_username_len) != ESP_OK)
{
ESP_LOGE(TAG, "Failed to read stored username from nconfig");
free(stored_username);
stored_username = NULL;
}
}
}
if (nconfig_get_str_len(PAGE_PASSWORD, &stored_password_len) == ESP_OK && stored_password_len > 1)
{
stored_password = (char*)malloc(stored_password_len);
if (stored_password)
{
if (nconfig_read(PAGE_PASSWORD, stored_password, stored_password_len) != ESP_OK)
{
ESP_LOGE(TAG, "Failed to read stored password from nconfig");
free(stored_password);
stored_password = NULL;
}
}
}
if (stored_username && stored_password)
{
if (strcmp(received_username, stored_username) == 0 && strcmp(received_password, stored_password) == 0)
{
credentials_match = true;
}
}
if (stored_username)
free(stored_username);
if (stored_password)
free(stored_password);
if (credentials_match)
{
char* token = auth_generate_token();
if (token)
{
cJSON* response_root = cJSON_CreateObject();
cJSON_AddStringToObject(response_root, "token", token);
char* json_response = cJSON_Print(response_root);
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, json_response);
free(token); // Free the token generated by auth_generate_token
free(json_response);
cJSON_Delete(response_root);
}
else
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to generate token");
}
}
else
{
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid credentials");
}
cJSON_Delete(root);
return ESP_OK;
}
void start_webserver(void)
{
auth_init();
// 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;
config.task_priority = 12;
config.max_open_sockets = 7;
if (httpd_start(&server, &config) != ESP_OK)
{
return;
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_uri_t index = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &index);
// Login endpoint
httpd_uri_t login = {.uri = "/login", .method = HTTP_POST, .handler = login_handler, .user_ctx = NULL};
httpd_register_uri_handler(server, &login);
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);
register_reboot_endpoint(server);
register_version_endpoint(server);
init_status_monitor();
}

View File

@@ -4,17 +4,14 @@
#ifndef ODROID_REMOTE_HTTP_WEBSERVER_H
#define ODROID_REMOTE_HTTP_WEBSERVER_H
#include <stddef.h>
#include <stdint.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(const uint8_t* data, size_t len);
void register_reboot_endpoint(httpd_handle_t server);
void push_data_to_ws(cJSON *data);
esp_err_t change_baud_rate(int baud_rate);
void register_version_endpoint(httpd_handle_t server);
#endif // ODROID_REMOTE_HTTP_WEBSERVER_H
#endif //ODROID_REMOTE_HTTP_WEBSERVER_H

View File

@@ -2,31 +2,28 @@
// Created by shinys on 25. 8. 18..
//
#include "auth.h"
#include "driver/uart.h"
#include "cJSON.h"
#include "webserver.h"
#include "esp_err.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "nconfig.h"
#include "pb.h"
#include "pb_encode.h"
#include "status.pb.h"
#include "string.h" // Added for strlen and strncmp
#include "webserver.h"
#include "driver/uart.h"
#include "freertos/semphr.h"
#define UART_NUM UART_NUM_1
#define BUF_SIZE (2048)
#define UART_TX_PIN CONFIG_GPIO_UART_TX
#define UART_RX_PIN CONFIG_GPIO_UART_RX
#define CHUNK_SIZE (2048)
#define PB_UART_BUFFER_SIZE (CHUNK_SIZE + 64)
#define CHUNK_SIZE (1024)
static const char* TAG = "ws-uart";
static const char *TAG = "ws-uart";
enum ws_message_type
{
static int client_fd = -1;
static SemaphoreHandle_t client_fd_mutex;
// Unified message structure for the websocket queue
enum ws_message_type {
WS_MSG_STATUS,
WS_MSG_UART
};
@@ -34,272 +31,237 @@ enum ws_message_type
struct ws_message
{
enum ws_message_type type;
uint8_t* data;
size_t len;
union {
struct {
cJSON *data;
} status;
struct {
uint8_t *data;
size_t len;
} uart;
} content;
};
struct bytes_arg
{
const void* data;
size_t len;
};
#define MAX_CLIENT 7
static QueueHandle_t ws_queue;
static QueueHandle_t uart_event_queue;
static int client_fds[MAX_CLIENT];
static bool encode_bytes_callback(pb_ostream_t* stream, const pb_field_t* field, void* const* arg)
{
struct bytes_arg* br = (struct bytes_arg*)(*arg);
if (!pb_encode_tag_for_field(stream, field))
{
return false;
}
return pb_encode_string(stream, (uint8_t*)br->data, br->len);
}
static void unified_ws_sender_task(void* arg)
// Unified task to send data from the queue to the websocket client
static void unified_ws_sender_task(void *arg)
{
httpd_handle_t server = (httpd_handle_t)arg;
struct ws_message msg;
const TickType_t PING_INTERVAL = pdMS_TO_TICKS(5000);
while (1)
{
if (xQueueReceive(ws_queue, &msg, portMAX_DELAY))
{
size_t clients = MAX_CLIENT;
if (httpd_get_client_list(server, &clients, client_fds) != ESP_OK)
{
free(msg.data);
continue;
}
while (1) {
if (xQueueReceive(ws_queue, &msg, PING_INTERVAL)) {
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
int fd = client_fd;
if (clients == 0)
{
free(msg.data);
if (fd <= 0) {
xSemaphoreGive(client_fd_mutex);
// Free memory if client is not connected
if (msg.type == WS_MSG_STATUS) {
cJSON_Delete(msg.content.status.data);
} else {
free(msg.content.uart.data);
}
continue;
}
httpd_ws_frame_t ws_pkt = {0};
ws_pkt.payload = msg.data;
ws_pkt.len = msg.len;
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
esp_err_t err = ESP_FAIL;
for (size_t i = 0; i < clients; ++i)
{
int fd = client_fds[i];
if (httpd_ws_get_fd_info(server, fd) == HTTPD_WS_CLIENT_WEBSOCKET)
{
esp_err_t err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "unified_ws_sender_task: async send failed for fd %d, error: %s", fd,
esp_err_to_name(err));
}
if (msg.type == WS_MSG_STATUS) {
char *json_string = cJSON_Print(msg.content.status.data);
cJSON_Delete(msg.content.status.data);
ws_pkt.payload = (uint8_t *)json_string;
ws_pkt.len = strlen(json_string);
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
free(json_string);
} else { // WS_MSG_UART
ws_pkt.payload = msg.content.uart.data;
ws_pkt.len = msg.content.uart.len;
ws_pkt.type = HTTPD_WS_TYPE_BINARY;
err = httpd_ws_send_frame_async(server, fd, &ws_pkt);
free(msg.content.uart.data);
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "unified_ws_sender_task: async send failed for fd %d, error: %s", fd, esp_err_to_name(err));
client_fd = -1;
}
xSemaphoreGive(client_fd_mutex);
} else {
// Queue receive timed out, send a PING to keep connection alive
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
int fd = client_fd;
if (fd > 0) {
httpd_ws_frame_t ping_pkt = {0};
ping_pkt.type = HTTPD_WS_TYPE_PING;
ping_pkt.final = true;
esp_err_t err = httpd_ws_send_frame_async(server, fd, &ping_pkt);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to send PING frame, closing connection for fd %d, error: %s", fd, esp_err_to_name(err));
client_fd = -1;
}
}
free(msg.data);
xSemaphoreGive(client_fd_mutex);
}
}
free(client_fds);
vTaskDelete(NULL);
}
static void uart_polling_task(void* arg)
static void uart_polling_task(void *arg)
{
static uint8_t data_buf[BUF_SIZE];
static uint8_t pb_buffer[PB_UART_BUFFER_SIZE];
const TickType_t MIN_POLLING_INTERVAL = pdMS_TO_TICKS(1);
const TickType_t MAX_POLLING_INTERVAL = pdMS_TO_TICKS(10);
const TickType_t READ_TIMEOUT = pdMS_TO_TICKS(5);
TickType_t current_interval = MIN_POLLING_INTERVAL;
int consecutive_empty_polls = 0;
int cached_client_fd = -1;
TickType_t last_client_check = 0;
const TickType_t CLIENT_CHECK_INTERVAL = pdMS_TO_TICKS(100);
while(1) {
TickType_t current_time = xTaskGetTickCount();
if (current_time - last_client_check >= CLIENT_CHECK_INTERVAL) {
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
cached_client_fd = client_fd;
xSemaphoreGive(client_fd_mutex);
last_client_check = current_time;
}
while (1)
{
size_t available_len;
uart_get_buffered_data_len(UART_NUM, &available_len);
if (available_len == 0)
{
vTaskDelay(pdMS_TO_TICKS(10));
esp_err_t err = uart_get_buffered_data_len(UART_NUM, &available_len);
if (err != ESP_OK || available_len == 0) {
consecutive_empty_polls++;
if (consecutive_empty_polls > 5) {
current_interval = MAX_POLLING_INTERVAL;
} else if (consecutive_empty_polls > 2) {
current_interval = pdMS_TO_TICKS(5);
}
if (cached_client_fd <= 0) {
vTaskDelay(pdMS_TO_TICKS(50));
continue;
}
vTaskDelay(current_interval);
continue;
}
size_t read_len = (available_len > BUF_SIZE) ? BUF_SIZE : available_len;
int bytes_read = uart_read_bytes(UART_NUM, data_buf, read_len, pdMS_TO_TICKS(5));
consecutive_empty_polls = 0;
current_interval = MIN_POLLING_INTERVAL;
if (bytes_read > 0)
{
if (cached_client_fd <= 0) {
uart_flush_input(UART_NUM);
continue;
}
size_t total_processed = 0;
while (available_len > 0 && total_processed < BUF_SIZE) {
size_t read_size = (available_len > (BUF_SIZE - total_processed)) ?
(BUF_SIZE - total_processed) : available_len;
int bytes_read = uart_read_bytes(UART_NUM, data_buf + total_processed,
read_size, READ_TIMEOUT);
if (bytes_read <= 0) {
break;
}
total_processed += bytes_read;
available_len -= bytes_read;
uart_get_buffered_data_len(UART_NUM, &available_len);
}
if (total_processed > 0) {
size_t offset = 0;
while (offset < bytes_read)
{
size_t chunk_size = (bytes_read - offset > CHUNK_SIZE) ? CHUNK_SIZE : (bytes_read - offset);
StatusMessage message = StatusMessage_init_zero;
message.which_payload = StatusMessage_uart_data_tag;
struct bytes_arg a = {.data = data_buf + offset, .len = chunk_size};
message.payload.uart_data.data.funcs.encode = &encode_bytes_callback;
message.payload.uart_data.data.arg = &a;
pb_ostream_t stream = pb_ostream_from_buffer(pb_buffer, sizeof(pb_buffer));
if (!pb_encode(&stream, StatusMessage_fields, &message))
{
ESP_LOGE(TAG, "Failed to encode uart data: %s", PB_GET_ERROR(&stream));
offset += chunk_size;
continue;
}
while (offset < total_processed) {
const size_t chunk_size = (total_processed - offset > CHUNK_SIZE) ?
CHUNK_SIZE : (total_processed - offset);
struct ws_message msg;
msg.type = WS_MSG_UART;
msg.len = stream.bytes_written;
msg.data = malloc(msg.len);
if (!msg.data)
{
msg.content.uart.data = malloc(chunk_size);
if (!msg.content.uart.data) {
ESP_LOGE(TAG, "Failed to allocate memory for uart ws msg");
offset += chunk_size;
continue;
break;
}
memcpy(msg.content.uart.data, data_buf + offset, chunk_size);
msg.content.uart.len = chunk_size;
memcpy(msg.data, pb_buffer, msg.len);
if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(10)) != pdPASS)
{
ESP_LOGW(TAG, "ws sender queue full, dropping %zu bytes", chunk_size);
free(msg.data);
if (xQueueSend(ws_queue, &msg, 0) != pdPASS) {
if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(5)) != pdPASS) {
ESP_LOGW(TAG, "ws sender queue full, dropping %zu bytes", chunk_size);
free(msg.content.uart.data);
}
}
offset += chunk_size;
}
}
if (available_len > 0) {
vTaskDelay(MIN_POLLING_INTERVAL);
} else {
vTaskDelay(current_interval);
}
}
vTaskDelete(NULL);
}
static void uart_event_task(void* arg)
{
uart_event_t event;
while (1)
{
if (xQueueReceive(uart_event_queue, &event, portMAX_DELAY))
{
switch (event.type)
{
case UART_FIFO_OVF:
ESP_LOGW(TAG, "UART HW FIFO Overflow");
uart_flush_input(UART_NUM);
xQueueReset(uart_event_queue);
break;
case UART_BUFFER_FULL:
ESP_LOGW(TAG, "UART ring buffer full");
uart_flush_input(UART_NUM);
xQueueReset(uart_event_queue);
break;
case UART_DATA:
// Muting this event because it is too noisy
break;
default:
break;
}
}
}
vTaskDelete(NULL);
}
static esp_err_t ws_handler(httpd_req_t* req)
{
if (req->method == HTTP_GET)
{
ESP_LOGI(TAG, "WebSocket GET request received for URI: %s", req->uri);
char* query_str = NULL;
size_t query_len = httpd_req_get_url_query_len(req) + 1;
if (query_len > 1)
{
query_str = malloc(query_len);
if (query_str == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory for query string");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal Server Error");
return ESP_FAIL;
}
if (httpd_req_get_url_query_str(req, query_str, query_len) != ESP_OK)
{
ESP_LOGE(TAG, "Failed to get query string from URI: %s", req->uri);
free(query_str);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal Server Error");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Extracted query string: %s", query_str);
}
char token_str[TOKEN_LENGTH];
esp_err_t err = ESP_FAIL; // Default to fail
if (query_str)
{
err = httpd_query_key_value(query_str, "token", token_str, sizeof(token_str));
free(query_str); // Free allocated query string
}
if (err == ESP_OK)
{
ESP_LOGI(TAG, "Token extracted from query string, value: %s", token_str);
if (!auth_validate_token(token_str))
{
ESP_LOGW(TAG, "WebSocket connection attempt with invalid token for URI: %s", req->uri);
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Invalid or expired token");
return ESP_FAIL;
}
ESP_LOGD(TAG, "WebSocket token validated for URI: %s", req->uri);
}
else
{
ESP_LOGW(TAG, "Failed to extract token from query string or query string not found, error: %s",
esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Authorization token required");
static esp_err_t ws_handler(httpd_req_t *req) {
if (req->method == HTTP_GET) {
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
if (client_fd > 0) {
// A client is already connected. Reject the new connection.
ESP_LOGW(TAG, "Another client tried to connect, but a session is already active. Rejecting.");
xSemaphoreGive(client_fd_mutex);
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Another client is already connected");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Handshake done, the new connection was opened");
// No client is connected. Accept the new one.
int new_fd = httpd_req_to_sockfd(req);
ESP_LOGI(TAG, "Accepting new websocket connection: %d", new_fd);
client_fd = new_fd;
xSemaphoreGive(client_fd_mutex);
// Reset queue and flush UART buffer for the new session
xQueueReset(ws_queue);
uart_flush_input(UART_NUM);
return ESP_OK;
}
httpd_ws_frame_t ws_pkt = {0};
uint8_t buf[BUF_SIZE];
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)
{
if (ret != ESP_OK) {
ESP_LOGW(TAG, "httpd_ws_recv_frame failed with error: %s", esp_err_to_name(ret));
xSemaphoreTake(client_fd_mutex, portMAX_DELAY);
if (httpd_req_to_sockfd(req) == client_fd) {
client_fd = -1;
}
xSemaphoreGive(client_fd_mutex);
return ret;
}
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && ws_pkt.len == strlen("ping") &&
strncmp((const char*)ws_pkt.payload, "ping", ws_pkt.len) == 0)
{
ESP_LOGD(TAG, "Received application-level ping from client, sending pong.");
httpd_ws_frame_t pong_pkt = {
.payload = (uint8_t*)"pong", .len = strlen("pong"), .type = HTTPD_WS_TYPE_TEXT, .final = true};
return httpd_ws_send_frame(req, &pong_pkt);
}
else if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE)
{
ESP_LOGI(TAG, "Client sent close frame, closing connection.");
return ESP_OK;
}
else if (ws_pkt.type == HTTPD_WS_TYPE_PING)
{
ESP_LOGD(TAG, "Received WebSocket PING control frame (handled by httpd).");
return ESP_OK;
}
else if (ws_pkt.type == HTTPD_WS_TYPE_PONG)
{
ESP_LOGD(TAG, "Received WebSocket PONG control frame.");
return ESP_OK;
}
else
{
uart_write_bytes(UART_NUM, (const char*)ws_pkt.payload, ws_pkt.len);
}
uart_write_bytes(UART_NUM, (const char *)ws_pkt.payload, ws_pkt.len);
return ESP_OK;
}
@@ -307,6 +269,7 @@ static esp_err_t ws_handler(httpd_req_t* req)
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);
@@ -317,40 +280,42 @@ void register_ws_endpoint(httpd_handle_t server)
.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, 20, &uart_event_queue, 0));
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 0, NULL, 0));
httpd_uri_t ws = {.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .user_ctx = NULL, .is_websocket = true};
httpd_uri_t ws = {
.uri = "/ws",
.method = HTTP_GET,
.handler = ws_handler,
.user_ctx = NULL,
.is_websocket = true
};
httpd_register_uri_handler(server, &ws);
ws_queue = xQueueCreate(10, sizeof(struct ws_message));
client_fd_mutex = xSemaphoreCreateMutex();
ws_queue = xQueueCreate(10, sizeof(struct ws_message)); // Combined queue
xTaskCreate(uart_polling_task, "uart_polling_task", 1024 * 4, NULL, 8, NULL);
xTaskCreate(unified_ws_sender_task, "ws_sender_task", 1024 * 6, server, 9, NULL);
xTaskCreate(uart_event_task, "uart_event_task", 1024 * 2, NULL, 10, NULL);
xTaskCreate(uart_polling_task, "uart_polling_task", 1024*4, NULL, 8, NULL);
xTaskCreate(unified_ws_sender_task, "ws_sender_task", 1024*6, server, 9, NULL);
}
void push_data_to_ws(const uint8_t* data, size_t len)
void push_data_to_ws(cJSON *data)
{
struct ws_message msg;
msg.type = WS_MSG_STATUS;
msg.data = malloc(len);
if (!msg.data)
{
ESP_LOGE(TAG, "Failed to allocate memory for status ws msg");
return;
}
memcpy(msg.data, data, len);
msg.len = len;
msg.content.status.data = data;
if (xQueueSend(ws_queue, &msg, pdMS_TO_TICKS(10)) != pdPASS)
{
ESP_LOGW(TAG, "WS queue full, dropping status message");
free(msg.data);
cJSON_Delete(data);
}
}
esp_err_t change_baud_rate(int baud_rate) { return uart_set_baudrate(UART_NUM, baud_rate); }
esp_err_t change_baud_rate(int baud_rate)
{
return uart_set_baudrate(UART_NUM, baud_rate);
}

46
main/system/system.c Normal file
View 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);
}

View File

@@ -1,84 +0,0 @@
//
// Created by shinys on 25. 9. 1.
//
#include <string.h>
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "lwip/ip4_addr.h"
#include "nconfig.h"
#include "priv_wifi.h"
#include "wifi.h"
static const char* TAG = "AP";
#define DEFAULT_AP_SSID "odroid-pm"
#define DEFAULT_AP_PASS "powermate"
#define AP_CHANNEL 1
#define AP_MAX_CONN 4
/**
* @brief Initializes and configures the AP mode.
*/
void wifi_init_ap(void)
{
// Get the network interface handle for the AP
esp_netif_t* p_netif_ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (p_netif_ap)
{
ESP_LOGI(TAG, "Setting AP static IP to 192.168.4.1");
esp_netif_dhcps_stop(p_netif_ap); // Stop DHCP server to apply new IP settings
esp_netif_ip_info_t ip_info;
IP4_ADDR(&ip_info.ip, 192, 168, 4, 1);
IP4_ADDR(&ip_info.gw, 192, 168, 4, 1);
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
esp_netif_set_ip_info(p_netif_ap, &ip_info);
esp_netif_dhcps_start(p_netif_ap); // Restart DHCP server
}
// Configure Wi-Fi AP settings
wifi_config_t wifi_config = {
.ap =
{
.password = "",
.channel = AP_CHANNEL,
.max_connection = AP_MAX_CONN,
.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg =
{
.required = false,
},
},
};
// Read SSID and password from NVS (nconfig)
size_t len;
if (nconfig_get_str_len(AP_SSID, &len) == ESP_OK && len > 1)
{
nconfig_read(AP_SSID, (char*)wifi_config.ap.ssid, sizeof(wifi_config.ap.ssid));
}
else
{
strcpy((char*)wifi_config.ap.ssid, DEFAULT_AP_SSID);
}
if (nconfig_get_str_len(AP_PASSWORD, &len) == ESP_OK && len > 1)
{
nconfig_read(AP_PASSWORD, (char*)wifi_config.ap.password, sizeof(wifi_config.ap.password));
}
// If password is not set, use open authentication
if (strlen((char*)wifi_config.ap.password) == 0)
{
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_LOGI(TAG, "wifi_init_ap finished. SSID: %s, Password: %s, Channel: %d", (char*)wifi_config.ap.ssid, "********",
AP_CHANNEL);
}

View File

@@ -1,12 +0,0 @@
//
// Created by shinys on 25. 9. 1..
//
#ifndef ODROID_POWER_MATE_PRIV_WIFI_H
#define ODROID_POWER_MATE_PRIV_WIFI_H
void wifi_init_sta(void);
void wifi_init_ap(void);
void initialize_sntp(void);
#endif // ODROID_POWER_MATE_PRIV_WIFI_H

View File

@@ -1,267 +0,0 @@
//
// Created by shinys on 25. 9. 1.
//
#include <string.h>
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "lwip/inet.h"
#include "nconfig.h"
#include "priv_wifi.h"
#include "wifi.h"
static const char* TAG = "STA";
/**
* @brief Initializes and configures the STA mode.
*/
void wifi_init_sta(void)
{
wifi_config_t wifi_config = {0};
// Read SSID and password from NVS (nconfig)
size_t len;
if (nconfig_get_str_len(WIFI_SSID, &len) == ESP_OK && len > 1)
{
nconfig_read(WIFI_SSID, (char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid));
}
else
{
ESP_LOGW(TAG, "STA SSID not configured in NVS.");
}
if (nconfig_get_str_len(WIFI_PASSWORD, &len) == ESP_OK && len > 1)
{
nconfig_read(WIFI_PASSWORD, (char*)wifi_config.sta.password, sizeof(wifi_config.sta.password));
}
else
{
ESP_LOGW(TAG, "STA Password not configured in NVS.");
}
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
// Check if we should use a static IP
char netif_type[16] = {0};
if (nconfig_get_str_len(NETIF_TYPE, &len) == ESP_OK && len > 1)
{
nconfig_read(NETIF_TYPE, netif_type, sizeof(netif_type));
if (strcmp(netif_type, "static") == 0)
{
ESP_LOGI(TAG, "Using static IP configuration for STA.");
char ip[16], gw[16], netmask[16], dns1[16], dns2[16];
nconfig_read(NETIF_IP, ip, sizeof(ip));
nconfig_read(NETIF_GATEWAY, gw, sizeof(gw));
nconfig_read(NETIF_SUBNET, netmask, sizeof(netmask));
nconfig_read(NETIF_DNS1, dns1, sizeof(dns1));
nconfig_read(NETIF_DNS2, dns2, sizeof(dns2));
wifi_use_static(ip, gw, netmask, dns1, dns2);
}
}
ESP_LOGI(TAG, "wifi_init_sta finished.");
}
esp_err_t wifi_connect(void)
{
ESP_LOGI(TAG, "Connecting to AP...");
return esp_wifi_connect();
}
esp_err_t wifi_disconnect(void)
{
ESP_LOGI(TAG, "Disconnecting from AP...");
return esp_wifi_disconnect();
}
void wifi_scan_aps(wifi_ap_record_t** ap_records, uint16_t* count)
{
ESP_LOGI(TAG, "Scanning for APs...");
*count = 0;
*ap_records = NULL;
// Start scan, this is a blocking call
if (esp_wifi_scan_start(NULL, true) == ESP_OK)
{
esp_wifi_scan_get_ap_num(count);
ESP_LOGI(TAG, "Found %d APs", *count);
if (*count > 0)
{
*ap_records = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * (*count));
if (*ap_records != NULL)
{
esp_wifi_scan_get_ap_records(count, *ap_records);
}
else
{
ESP_LOGE(TAG, "Failed to allocate memory for AP records");
*count = 0;
}
}
}
}
esp_err_t wifi_get_current_ap_info(wifi_ap_record_t* ap_info)
{
if (ap_info == NULL)
{
return ESP_ERR_INVALID_ARG;
}
// This function retrieves the AP record to which the STA is currently connected.
esp_err_t err = esp_wifi_sta_get_ap_info(ap_info);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to get connected AP info: %s", esp_err_to_name(err));
}
return err;
}
esp_err_t wifi_get_current_ip_info(esp_netif_ip_info_t* ip_info)
{
if (ip_info == NULL)
{
return ESP_ERR_INVALID_ARG;
}
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif == NULL)
{
return ESP_FAIL;
}
return esp_netif_get_ip_info(netif, ip_info);
}
esp_err_t wifi_get_dns_info(esp_netif_dns_type_t type, esp_netif_dns_info_t* dns_info)
{
if (dns_info == NULL)
{
return ESP_ERR_INVALID_ARG;
}
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif == NULL)
{
return ESP_FAIL;
}
return esp_netif_get_dns_info(netif, type, dns_info);
}
esp_err_t wifi_use_dhcp(void)
{
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif == NULL)
{
return ESP_FAIL;
}
ESP_LOGI(TAG, "Setting STA to use DHCP");
esp_err_t err = esp_netif_dhcpc_start(netif);
if (err == ESP_OK)
{
nconfig_write(NETIF_TYPE, "dhcp");
}
return err;
}
esp_err_t wifi_use_static(const char* ip, const char* gw, const char* netmask, const char* dns1, const char* dns2)
{
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif == NULL)
{
return ESP_FAIL;
}
ESP_LOGI(TAG, "Setting STA to use static IP");
esp_err_t err = esp_netif_dhcpc_stop(netif);
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED)
{
ESP_LOGE(TAG, "Failed to stop DHCP client: %s", esp_err_to_name(err));
return err;
}
esp_netif_ip_info_t ip_info;
ip_info.ip.addr = ipaddr_addr(ip);
ip_info.gw.addr = ipaddr_addr(gw);
ip_info.netmask.addr = ipaddr_addr(netmask);
err = esp_netif_set_ip_info(netif, &ip_info);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set static IP: %s", esp_err_to_name(err));
return err;
}
esp_netif_dns_info_t dns_info;
dns_info.ip.u_addr.ip4.addr = ipaddr_addr(dns1);
dns_info.ip.type = IPADDR_TYPE_V4;
err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set main DNS: %s", esp_err_to_name(err));
// continue anyway
}
if (dns2 && strlen(dns2) > 0)
{
dns_info.ip.u_addr.ip4.addr = ipaddr_addr(dns2);
err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_BACKUP, &dns_info);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set backup DNS: %s", esp_err_to_name(err));
}
}
// Save settings to NVS
nconfig_write(NETIF_TYPE, "static");
nconfig_write(NETIF_IP, ip);
nconfig_write(NETIF_GATEWAY, gw);
nconfig_write(NETIF_SUBNET, netmask);
nconfig_write(NETIF_DNS1, dns1);
nconfig_write(NETIF_DNS2, dns2);
return ESP_OK;
}
esp_err_t wifi_sta_set_ap(const char* ssid, const char* password)
{
ESP_LOGI(TAG, "Setting new AP with SSID: %s", ssid);
// Save settings to NVS first
esp_err_t err = nconfig_write(WIFI_SSID, ssid);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to save SSID to NVS: %s", esp_err_to_name(err));
return err;
}
err = nconfig_write(WIFI_PASSWORD, password);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to save password to NVS: %s", esp_err_to_name(err));
return err;
}
// Now configure the wifi interface
wifi_config_t wifi_config = {0};
strncpy((char*)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
strncpy((char*)wifi_config.sta.password, password, sizeof(wifi_config.sta.password));
err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set Wi-Fi config: %s", esp_err_to_name(err));
return err;
}
// Disconnect from any current AP and connect to the new one
ESP_LOGI(TAG, "Disconnecting from current AP if connected.");
esp_wifi_disconnect();
ESP_LOGI(TAG, "Connecting to new AP...");
err = esp_wifi_connect();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to start connection to new AP: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}

View File

@@ -1,156 +1,550 @@
//
// Created by shinys on 25. 9. 1.
// Created by shinys on 25. 7. 10.
//
#include <string.h>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nconfig.h"
#include "priv_wifi.h"
#include "wifi.h"
#include "nconfig.h"
#include "indicator.h"
static const char* TAG = "WIFI";
#include <string.h>
#include <system.h>
#include <lwip/sockets.h>
#include <time.h>
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
#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)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED)
switch (mode)
{
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
ESP_LOGI(TAG, "Station " MACSTR " joined, AID=%d", MAC2STR(event->mac), event->aid);
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED)
{
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
ESP_LOGI(TAG, "Station " MACSTR " left, AID=%d", MAC2STR(event->mac), event->aid);
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
ESP_LOGI(TAG, "Station mode started");
// Only try to connect if SSID is configured
if (!nconfig_value_is_not_set(WIFI_SSID))
{
esp_wifi_connect();
}
else
{
ESP_LOGI(TAG, "STA SSID not configured, not connecting.");
}
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
led_set(LED_RED, BLINK_TRIPLE);
wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*)event_data;
ESP_LOGW(TAG, "Disconnected from AP, reason: %s", wifi_reason_str(event->reason));
// ESP-IDF will automatically try to reconnect by default.
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
led_set(LED_BLU, BLINK_SOLID);
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
sync_time();
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";
}
}
void wifi_init(void)
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)
{
// Create network interfaces for both AP and STA.
// This is done unconditionally to allow for dynamic mode switching.
esp_netif_create_default_wifi_ap();
esp_netif_create_default_wifi_sta();
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));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL));
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);
initialize_sntp();
char mode_str[10] = {0};
wifi_mode_t mode = WIFI_MODE_APSTA;
const char* started_mode_str = "APSTA";
if (nconfig_read(WIFI_MODE, mode_str, sizeof(mode_str)) == ESP_OK)
{
if (strcmp(mode_str, "sta") == 0)
{
mode = WIFI_MODE_STA;
started_mode_str = "STA";
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();
}
else if (strcmp(mode_str, "apsta") != 0)
{
ESP_LOGW(TAG, "Invalid Wi-Fi mode in nconfig: '%s'. Defaulting to APSTA.", mode_str);
}
}
else
{
ESP_LOGW(TAG, "Failed to read Wi-Fi mode from nconfig. Defaulting to APSTA.");
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));
if (mode == WIFI_MODE_APSTA)
{
wifi_init_ap();
wifi_init_sta();
}
else if (mode == WIFI_MODE_STA)
{
wifi_init_sta();
}
ESP_ERROR_CHECK(esp_wifi_start());
}
led_set(LED_BLU, BLINK_TRIPLE);
ESP_LOGI(TAG, "wifi_init_all finished. Started in %s mode.", started_mode_str);
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_ALREADY_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)
{
ESP_LOGI(TAG, "Switching Wi-Fi mode to %s", mode);
wifi_mode_t new_mode;
if (strcmp(mode, "sta") == 0)
{
new_mode = WIFI_MODE_STA;
}
else if (strcmp(mode, "apsta") == 0)
{
new_mode = WIFI_MODE_APSTA;
}
else
{
ESP_LOGE(TAG, "Unsupported mode: %s", 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;
}
nconfig_write(WIFI_MODE, mode);
ESP_ERROR_CHECK(esp_wifi_stop());
ESP_ERROR_CHECK(esp_wifi_set_mode(new_mode));
if (new_mode == WIFI_MODE_APSTA)
{
wifi_init_ap();
wifi_init_sta();
}
else if (new_mode == WIFI_MODE_STA)
{
wifi_init_sta();
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_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Switching Wi-Fi mode to %s.", mode);
ESP_LOGI(TAG, "Wi-Fi mode switched 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...");
}

View File

@@ -1,160 +0,0 @@
//
// Created by shinys on 25. 9. 1.
//
#include <time.h>
#include "esp_log.h"
#include "esp_sntp.h"
#include "priv_wifi.h"
#include "wifi.h"
static const char* TAG = "WIFI_HELPER";
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";
}
}
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";
}
}
// Callback function for time synchronization
void time_sync_notification_cb(struct timeval* tv)
{
ESP_LOGI(TAG, "Time synchronized");
// Set timezone to UTC
setenv("TZ", "UTC", 1);
tzset();
char strftime_buf[64];
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in UTC is: %s", strftime_buf);
}
void initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP service");
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
sntp_set_time_sync_notification_cb(time_sync_notification_cb);
}
void sync_time()
{
if (esp_sntp_enabled())
{
esp_sntp_stop();
}
ESP_LOGI(TAG, "Starting SNTP synchronization");
esp_sntp_init();
}

View File

@@ -2,39 +2,13 @@
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<title>ODROID PowerMate</title>
<title>ODROID Remote</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="login-container" class="d-flex flex-column justify-content-center align-items-center vh-100" style="display: none;">
<div class="card p-4 shadow-lg" style="width: 100%; max-width: 400px;">
<div class="card-body">
<h2 class="card-title text-center mb-4">Login to ODROID Power Mate</h2>
<div id="login-alert" class="alert alert-danger d-none" role="alert"></div>
<form id="login-form">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
<div class="form-check form-switch d-flex justify-content-center mt-4">
<input class="form-check-input" type="checkbox" role="switch" id="theme-toggle-login">
<label class="form-check-label ms-2" for="theme-toggle-login"><i id="theme-icon-login" class="bi bi-moon-stars-fill"></i></label>
</div>
</div>
</div>
</div>
<main class="container" style="display: none;">
<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">
@@ -53,10 +27,7 @@
<span id="power-display" class="text-primary">--.-- W</span>
</div>
</div>
<div class="text-center order-md-2 mx-auto">
<h1 class="text-primary mb-0">ODROID Power Mate</h1>
<small class="text-muted" id="version-info"></small>
</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">
@@ -66,9 +37,6 @@
data-bs-target="#settingsModal">
<i class="bi bi-gear"></i>
</button>
<button class="btn btn-outline-secondary ms-3" id="logout-button">
<i class="bi bi-box-arrow-right"></i>
</button>
</div>
</header>
@@ -103,7 +71,7 @@
<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
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">
@@ -111,7 +79,7 @@
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
USB Power
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">
@@ -141,10 +109,9 @@
<div class="card border-top-0 rounded-0 rounded-bottom">
<div class="card-body">
<div class="d-flex justify-content-end mb-3">
<a class="btn btn-primary" download="datalog.csv" href="/datalog.csv" style="display: none"><i
class="bi bi-download me-1"></i> Download CSV</a>
<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 Metrics</h5>
<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>
@@ -163,8 +130,8 @@
</main>
<footer class="bg-body-tertiary text-center p-3">
<a href="https://www.hardkernel.com/" target="_blank" class="link-secondary text-decoration-none">Hardkernel</a> |
<a href="https://wiki.odroid.com/start" target="_blank" class="link-secondary text-decoration-none">Wiki</a>
<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 -->
@@ -192,16 +159,6 @@
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" data-bs-target="#current-limit-settings-pane" data-bs-toggle="tab"
id="current-limit-settings-tab" role="tab" type="button">Current Limit
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-target="#user-settings-pane" data-bs-toggle="tab"
id="user-settings-tab" role="tab" type="button">User Settings
</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
@@ -294,7 +251,7 @@
<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="powermate">
<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>
@@ -303,61 +260,9 @@
</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 class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button>
</div>
</div>
<div class="tab-pane fade" id="current-limit-settings-pane" role="tabpanel">
<div class="alert alert-info mt-3" role="alert">
<i class="bi bi-info-circle-fill me-2"></i>
Set a value of <strong>0.0</strong> to disable a specific current limit. The value will be
rounded to one decimal place.
</div>
<div class="mb-4">
<label class="form-label" for="vin-current-limit-slider">VIN Current Limit: <span
class="fw-bold text-primary" id="vin-current-limit-value">...</span> A</label>
<input class="form-range" id="vin-current-limit-slider" max="8.0" min="0" step="0.1"
type="range">
</div>
<div class="mb-4">
<label class="form-label" for="main-current-limit-slider">Main Current Limit: <span
class="fw-bold text-primary" id="main-current-limit-value">...</span> A</label>
<input class="form-range" id="main-current-limit-slider" max="7.5" min="0" step="0.1"
type="range">
</div>
<div class="mb-4">
<label class="form-label" for="usb-current-limit-slider">USB Current Limit: <span
class="fw-bold text-primary" id="usb-current-limit-value">...</span> A</label>
<input class="form-range" id="usb-current-limit-slider" max="4.5" min="0" step="0.1"
type="range">
</div>
<div class="d-flex justify-content-end pt-3 border-top mt-3">
<button class="btn btn-primary me-2" id="current-limit-apply-button" type="button">Apply
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
<div class="tab-pane fade" id="user-settings-pane" role="tabpanel">
<form id="user-settings-form">
<div class="mb-3">
<label class="form-label" for="new-username">New Username</label>
<input class="form-control" id="new-username" required type="text">
</div>
<div class="mb-3">
<label class="form-label" for="new-password">New Password</label>
<input class="form-control" id="new-password" required type="password">
</div>
<div class="mb-3">
<label class="form-label" for="confirm-password">Confirm New Password</label>
<input class="form-control" id="confirm-password" required type="password">
</div>
<div class="d-flex justify-content-end pt-3 border-top mt-3">
<button class="btn btn-primary me-2" id="user-settings-apply-button" type="submit">
Apply
</button>
<button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button>
</div>
</form>
</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>
@@ -373,12 +278,6 @@
<option value="1500000" selected>1500000</option>
</select>
</div>
<hr>
<div class="mb-3">
<label class="form-label">System Reboot</label>
<p class="text-muted small">This will restart the device. The reboot will occur after 3 seconds.</p>
<button type="button" class="btn btn-danger" id="reboot-button">Reboot Now</button>
</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>
@@ -405,7 +304,7 @@
</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" placeholder="Leave blank for open network">
<input type="password" class="form-control" id="wifi-password-connect">
</div>
</div>
<div class="modal-footer">
@@ -415,5 +314,6 @@
</div>
</div>
</div>
</body>
</html>

740
page/package-lock.json generated
View File

@@ -12,62 +12,14 @@
"@xterm/xterm": "^5.5.0",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.3",
"protobufjs": "^7.5.4"
"chart.js": "^4.4.3"
},
"devDependencies": {
"protobufjs-cli": "^1.1.2",
"vite": "^7.0.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-singlefile": "^2.0.1"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"dev": true,
"dependencies": {
"@babel/types": "^7.28.2"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
@@ -484,18 +436,6 @@
"node": ">=18"
}
},
"node_modules/@jsdoc/salty": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
"integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==",
"dev": true,
"dependencies": {
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v12.0.0"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
@@ -511,60 +451,6 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
@@ -831,36 +717,6 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
"dependencies": {
"undici-types": "~7.10.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0.tgz",
@@ -874,27 +730,6 @@
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -910,24 +745,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"node_modules/bootstrap": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
@@ -961,15 +778,6 @@
}
]
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -982,18 +790,6 @@
"node": ">=8"
}
},
"node_modules/catharsis": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
"integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
"dev": true,
"dependencies": {
"lodash": "^4.17.15"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1056,24 +852,6 @@
}
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
@@ -1115,112 +893,6 @@
"@esbuild/win32-x64": "0.25.8"
}
},
"node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"dev": true,
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=4.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/escodegen/node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
@@ -1261,12 +933,6 @@
"node": ">=12"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1281,26 +947,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1316,23 +962,6 @@
"node": ">=8"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -1342,44 +971,6 @@
"node": ">=0.12.0"
}
},
"node_modules/js2xmlparser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
"integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
"dev": true,
"dependencies": {
"xmlcreate": "^2.0.4"
}
},
"node_modules/jsdoc": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz",
"integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.20.15",
"@jsdoc/salty": "^0.2.1",
"@types/markdown-it": "^14.1.1",
"bluebird": "^3.7.2",
"catharsis": "^0.9.0",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.2",
"klaw": "^3.0.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"marked": "^4.0.10",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
"strip-json-comments": "^3.1.0",
"underscore": "~1.13.2"
},
"bin": {
"jsdoc": "jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
@@ -1392,93 +983,6 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/klaw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.9"
}
},
"node_modules/levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
"dev": true,
"dependencies": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it-anchor": {
"version": "8.6.7",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
"dev": true,
"peerDependencies": {
"@types/markdown-it": "*",
"markdown-it": "*"
}
},
"node_modules/marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"dev": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -1504,39 +1008,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1561,32 +1032,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dev": true,
"dependencies": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.6",
"levn": "~0.3.0",
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"word-wrap": "~1.2.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1633,84 +1078,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
"dev": true,
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/protobufjs-cli": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz",
"integrity": "sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ==",
"dev": true,
"dependencies": {
"chalk": "^4.0.0",
"escodegen": "^1.13.0",
"espree": "^9.0.0",
"estraverse": "^5.1.0",
"glob": "^8.0.0",
"jsdoc": "^4.0.0",
"minimist": "^1.2.0",
"semver": "^7.1.2",
"tmp": "^0.2.1",
"uglify-js": "^3.7.7"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"protobufjs": "^7.0.0"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/requizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
"integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
"dev": true,
"dependencies": {
"lodash": "^4.17.21"
}
},
"node_modules/rollup": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
@@ -1750,28 +1117,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1781,18 +1126,6 @@
"node": ">=0.10.0"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -1821,15 +1154,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"engines": {
"node": ">=14.14"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1842,47 +1166,6 @@
"node": ">=8.0"
}
},
"node_modules/type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
"dev": true,
"dependencies": {
"prelude-ls": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"dev": true
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -1995,27 +1278,6 @@
"rollup": "^4.44.1",
"vite": "^5.4.11 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/xmlcreate": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
"integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
"dev": true
}
}
}

View File

@@ -4,23 +4,20 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "npm run build:proto && vite",
"build": "npm run build:proto && vite build",
"preview": "vite preview",
"build:proto": "pbjs -t static-module -w es6 -o src/proto.js ../proto/status.proto"
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"protobufjs-cli": "^1.1.2",
"vite": "^7.0.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-singlefile": "^2.0.1"
"vite-plugin-singlefile": "^2.0.1",
"vite-plugin-compression": "^0.5.1"
},
"dependencies": {
"@xterm/addon-fit": "^0.9.0",
"@xterm/xterm": "^5.5.0",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.3",
"protobufjs": "^7.5.4"
"@xterm/addon-fit": "^0.9.0",
"chart.js": "^4.4.3"
}
}

View File

@@ -4,63 +4,15 @@
* It abstracts the fetch logic, error handling, and JSON parsing for network and control operations.
*/
// Function to get authentication headers
export function getAuthHeaders() {
const token = localStorage.getItem('authToken');
if (token) {
return { 'Authorization': `Bearer ${token}` };
}
return {};
}
// Global error handler for unauthorized responses
export async function handleResponse(response) {
if (response.status === 401) {
// Unauthorized, log out the user
localStorage.removeItem('authToken');
// Redirect to login or trigger a logout event
// For now, we'll just reload the page, which will trigger the login screen
window.location.reload();
throw new Error('Unauthorized: Session expired or invalid token.');
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `HTTP error! status: ${response.status}`);
}
return response;
}
/**
* Authenticates a user with the provided username and password.
* @param {string} username The user's username.
* @param {string} password The user's password.
* @returns {Promise<Object>} A promise that resolves to the server's JSON response containing a token.
* @throws {Error} Throws an error if the authentication fails.
*/
export async function login(username, password) {
const response = await fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, password }),
});
// Login function does not use handleResponse as it's for obtaining the token
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `Login failed with status: ${response.status}`);
}
return await response.json();
}
/**
* 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', {
headers: getAuthHeaders(),
});
return await handleResponse(response).then(res => res.json());
const response = await fetch('/api/wifi/scan');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
}
/**
@@ -71,15 +23,16 @@ export async function fetchWifiScan() {
* @throws {Error} Throws an error if the connection request fails.
*/
export async function postWifiConnect(ssid, password) {
const response = await fetch('/api/setting', {
const response = await fetch('/api/setting', { // Updated URL
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
body: JSON.stringify({ssid, password}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ssid, password }),
});
return await handleResponse(response).then(res => res.json());
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `Connection failed with status: ${response.status}`);
}
return await response.json();
}
/**
@@ -89,15 +42,16 @@ export async function postWifiConnect(ssid, password) {
* @throws {Error} Throws an error if the request fails.
*/
export async function postNetworkSettings(payload) {
const response = await fetch('/api/setting', {
const response = await fetch('/api/setting', { // Updated URL
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return await handleResponse(response);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `Failed to apply settings with status: ${response.status}`);
}
return response;
}
/**
@@ -109,13 +63,14 @@ export async function postNetworkSettings(payload) {
export async function postBaudRateSetting(baudrate) {
const response = await fetch('/api/setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
body: JSON.stringify({baudrate}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ baudrate }),
});
return await handleResponse(response);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `Failed to apply baudrate with status: ${response.status}`);
}
return response;
}
/**
@@ -124,10 +79,9 @@ export async function postBaudRateSetting(baudrate) {
* @throws {Error} Throws an error if the network request fails.
*/
export async function fetchSettings() {
const response = await fetch('/api/setting', {
headers: getAuthHeaders(),
});
return await handleResponse(response).then(res => res.json());
const response = await fetch('/api/setting'); // Updated URL
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
}
/**
@@ -136,10 +90,9 @@ export async function fetchSettings() {
* @throws {Error} Throws an error if the network request fails.
*/
export async function fetchControlStatus() {
const response = await fetch('/api/control', {
headers: getAuthHeaders(),
});
return await handleResponse(response).then(res => res.json());
const response = await fetch('/api/control');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
}
/**
@@ -151,42 +104,9 @@ export async function fetchControlStatus() {
export async function postControlCommand(command) {
const response = await fetch('/api/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(command)
});
return await handleResponse(response);
}
/**
* Fetches the firmware version from the server.
* @returns {Promise<Object>} A promise that resolves to an object containing the version.
* @throws {Error} Throws an error if the network request fails.
*/
export async function fetchVersion() {
const response = await fetch('/api/version', {
headers: getAuthHeaders(),
});
return await handleResponse(response).then(res => res.json());
}
/**
* Updates the user's username and password on the server.
* @param {string} newUsername The new username.
* @param {string} newPassword The new password.
* @returns {Promise<Object>} A promise that resolves to the server's JSON response.
* @throws {Error} Throws an error if the update fails.
*/
export async function updateUserSettings(newUsername, newPassword) {
const response = await fetch('/api/setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
body: JSON.stringify({new_username: newUsername, new_password: newPassword}),
});
return await handleResponse(response).then(res => res.json());
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response;
}

View File

@@ -4,8 +4,8 @@
* It handles initialization, theme updates, data updates, and resizing for the three separate charts.
*/
import {Chart, registerables} from 'chart.js';
import {currentChartCtx, graphTabPane, htmlEl, powerChartCtx, voltageChartCtx} from './dom.js';
import { Chart, registerables } from 'chart.js';
import { powerChartCtx, voltageChartCtx, currentChartCtx, htmlEl, graphTabPane } from './dom.js';
// Register all necessary Chart.js components
Chart.register(...registerables);
@@ -17,14 +17,6 @@ export const charts = {
current: null
};
// Configuration for dynamic, step-wise Y-axis scaling
const scaleConfig = {
power: {steps: [5, 20, 50, 160]}, // in Watts
voltage: {steps: [5, 10, 15, 25]}, // in Volts
current: {steps: [1, 2.5, 5, 10]} // in Amps
};
const channelKeys = ['USB', 'MAIN', 'VIN'];
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
/**
@@ -46,82 +38,92 @@ function initialData() {
/**
* Creates a common configuration object for a single line chart.
* @param {string} title - The title of the chart (e.g., 'Power (W)').
* @param {number} minValue - The minimum value for Y-axis.
* @param {number} maxValue - The maximum value for Y-axis.
* @returns {Object} A Chart.js options object.
*/
function createChartOptions(title) {
function createChartOptions(title, minValue, maxValue) {
return {
responsive: true,
maintainAspectRatio: false,
interaction: {mode: 'index', intersect: false},
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {position: 'top'},
title: {display: true, text: title}
legend: { position: 'top' },
title: { display: true, text: title }
},
scales: {
x: {ticks: {autoSkipPadding: 10, maxRotation: 0, minRotation: 0}},
y: {
min: 0,
beginAtZero: true,
ticks: {}
x: { ticks: { autoSkipPadding: 10, maxRotation: 0, minRotation: 0 } },
y: {
min: minValue,
max: maxValue,
beginAtZero: false,
ticks: {
stepSize: (maxValue - minValue) / 8
}
}
}
};
}
/**
* Creates the dataset objects for a chart.
* @param {string} unit - The unit for the dataset label (e.g., 'W', 'V', 'A').
* @returns {Array<Object>} An array of Chart.js dataset objects.
*/
function createDatasets(unit) {
return channelKeys.map(channel => ({
label: `${channel} (${unit})`,
data: initialData(),
borderWidth: 2,
fill: false,
tension: 0.2,
pointRadius: 0
}));
}
/**
* Initializes a single chart with its specific configuration.
* @param {CanvasRenderingContext2D} context - The canvas context for the chart.
* @param {string} title - The chart title.
* @param {string} metric - The metric key ('power', 'voltage', 'current').
* @param {string} unit - The data unit ('W', 'V', 'A').
* @returns {Chart} A new Chart.js instance.
*/
function initializeSingleChart(context, title, metric, unit) {
if (!context) return null;
const options = createChartOptions(title);
const initialMax = scaleConfig[metric].steps[0];
options.scales.y.max = initialMax;
options.scales.y.ticks.stepSize = initialMax / 5; // Initial step size
return new Chart(context, {
type: 'line',
data: {labels: initialLabels(), datasets: createDatasets(unit)},
options: options
});
}
/**
* 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
Object.keys(charts).forEach(key => {
for (const key in charts) {
if (charts[key]) {
charts[key].destroy();
}
});
}
charts.power = initializeSingleChart(powerChartCtx, 'Power', 'power', 'W');
charts.voltage = initializeSingleChart(voltageChartCtx, 'Voltage', 'voltage', 'V');
charts.current = initializeSingleChart(currentChartCtx, 'Current', 'current', 'A');
// Create Power Chart
if (powerChartCtx) {
const powerOptions = createChartOptions('Power', 0, 120);
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: powerOptions
});
}
// Create Voltage Chart
if (voltageChartCtx) {
const voltageOptions = createChartOptions('Voltage', 0, 24);
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: voltageOptions
});
}
// Create Current Chart
if (currentChartCtx) {
const currentOptions = createChartOptions('Current', 0, 7);
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: currentOptions
});
}
}
/**
@@ -133,13 +135,11 @@ export function applyChartsTheme(themeName) {
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
const labelColor = isDark ? '#dee2e6' : '#212529';
const channelColors = [
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd',
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754',
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545'
];
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) => {
const updateThemeForChart = (chart, color) => {
if (!chart) return;
chart.options.scales.x.grid.color = gridColor;
chart.options.scales.y.grid.color = gridColor;
@@ -147,70 +147,17 @@ export function applyChartsTheme(themeName) {
chart.options.scales.y.ticks.color = labelColor;
chart.options.plugins.legend.labels.color = labelColor;
chart.options.plugins.title.color = labelColor;
chart.data.datasets.forEach((dataset, index) => {
dataset.borderColor = channelColors[index];
});
chart.data.datasets[0].borderColor = color;
chart.data.datasets[1].borderColor = color;
chart.data.datasets[1].borderDash = [10, 5];
chart.update('none');
};
Object.values(charts).forEach(updateThemeForChart);
updateThemeForChart(charts.power, powerColor);
updateThemeForChart(charts.voltage, voltageColor);
updateThemeForChart(charts.current, currentColor);
}
/**
* Updates a single chart with new data and dynamically adjusts its Y-axis scale.
* @param {Chart} chart - The Chart.js instance to update.
* @param {string} metric - The metric key (e.g., 'power', 'voltage').
* @param {Object} data - The new sensor data object.
* @param {string} timeLabel - The timestamp label for the new data point.
*/
function updateSingleChart(chart, metric, data, timeLabel) {
if (!chart) return;
// Shift old data and push new data
chart.data.labels.shift();
chart.data.labels.push(timeLabel);
chart.data.datasets.forEach((dataset, index) => {
dataset.data.shift();
const channel = channelKeys[index];
const value = data[channel]?.[metric];
dataset.data.push(value !== undefined ? value.toFixed(2) : null);
});
// --- DYNAMIC STEP-WISE Y-AXIS SCALING ---
const config = scaleConfig[metric];
if (config?.steps) {
const allData = chart.data.datasets
.flatMap(dataset => dataset.data)
.filter(v => v !== null)
.map(v => parseFloat(v));
const maxDataValue = allData.length > 0 ? Math.max(...allData) : 0;
// Find the smallest step that is >= maxDataValue
let newMax = config.steps.find(step => maxDataValue <= step);
// If value exceeds all steps, use the largest step. If no data, use the smallest.
if (newMax === undefined) {
newMax = config.steps[config.steps.length - 1];
}
if (chart.options.scales.y.max !== newMax) {
chart.options.scales.y.max = newMax;
// Dynamically adjust stepSize for clearer grid lines
chart.options.scales.y.ticks.stepSize = newMax / 5;
}
}
// --- END DYNAMIC SCALING ---
// Update chart only if its tab is visible for performance.
if (graphTabPane.classList.contains('show')) {
chart.update('none');
}
}
/**
* Updates all charts with new sensor data.
* @param {Object} data - The new sensor data object from the WebSocket.
@@ -218,18 +165,46 @@ function updateSingleChart(chart, metric, data, timeLabel) {
export function updateCharts(data) {
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
updateSingleChart(charts.power, 'power', data, timeLabel);
updateSingleChart(charts.voltage, 'voltage', data, timeLabel);
updateSingleChart(charts.current, 'current', data, timeLabel);
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
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);
} 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() {
Object.values(charts).forEach(chart => {
if (chart) {
chart.resize();
for (const key in charts) {
if (charts[key]) {
charts[key].resize();
}
});
}
}

View File

@@ -76,16 +76,3 @@ 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');
export const rebootButton = document.getElementById('reboot-button');
// --- Current Limit Settings Elements ---
export const vinSlider = document.getElementById('vin-current-limit-slider');
export const vinValueSpan = document.getElementById('vin-current-limit-value');
export const mainSlider = document.getElementById('main-current-limit-slider');
export const mainValueSpan = document.getElementById('main-current-limit-value');
export const usbSlider = document.getElementById('usb-current-limit-slider');
export const usbValueSpan = document.getElementById('usb-current-limit-value');
export const currentLimitApplyButton = document.getElementById('current-limit-apply-button');
// --- Footer ---
export const versionInfo = document.getElementById('version-info');

View File

@@ -7,59 +7,23 @@
import * as dom from './dom.js';
import * as api from './api.js';
import {getAuthHeaders, handleResponse} from './api.js'; // Import auth functions
import * as ui from './ui.js';
import {clearTerminal, downloadTerminalOutput, fitTerminal} from './terminal.js';
import {debounce, isMobile} from './utils.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;
let listenersAttached = false;
// --- Helper functions for settings ---
function updateSliderValue(slider, span) {
if (!slider || !span) return;
let value = parseFloat(slider.value).toFixed(1);
if (value <= 0) {
span.textContent = 'Disabled';
} else {
span.textContent = `${value} A`;
}
}
function loadCurrentLimitSettings() {
fetch('/api/setting', {
headers: getAuthHeaders(), // Add auth headers
})
.then(handleResponse) // Handle response for 401
.then(response => response.json())
.then(data => {
if (data.vin_current_limit !== undefined) {
dom.vinSlider.value = data.vin_current_limit;
updateSliderValue(dom.vinSlider, dom.vinValueSpan);
}
if (data.main_current_limit !== undefined) {
dom.mainSlider.value = data.main_current_limit;
updateSliderValue(dom.mainSlider, dom.mainValueSpan);
}
if (data.usb_current_limit !== undefined) {
dom.usbSlider.value = data.usb_current_limit;
updateSliderValue(dom.usbSlider, dom.usbValueSpan);
}
})
.catch(error => console.error('Error fetching current limit settings:', error));
}
/**
* Sets up all event listeners for the application's interactive elements.
* This function is now idempotent and will only attach listeners once.
*/
export function setupEventListeners() {
if (listenersAttached) {
console.log("Event listeners already attached. Skipping.");
return;
}
// --- 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);
@@ -78,61 +42,6 @@ export function setupEventListeners() {
dom.apModeApplyButton.addEventListener('click', ui.applyApModeSettings);
dom.baudRateApplyButton.addEventListener('click', ui.applyBaudRateSettings);
// --- Device Settings (Reboot) ---
if (dom.rebootButton) {
dom.rebootButton.addEventListener('click', () => {
if (confirm('Are you sure you want to reboot the device?')) {
fetch('/api/reboot', {
method: 'POST',
headers: getAuthHeaders(), // Add auth headers
})
.then(handleResponse) // Handle response for 401
.then(response => response.json())
.then(data => {
console.log('Reboot command sent:', data);
ui.hideSettingsModal();
alert('Reboot command sent. The device will restart in 3 seconds.');
})
.catch(error => {
console.error('Error sending reboot command:', error);
alert('Failed to send reboot command.');
});
}
});
}
// --- Current Limit Settings ---
dom.vinSlider.addEventListener('input', () => updateSliderValue(dom.vinSlider, dom.vinValueSpan));
dom.mainSlider.addEventListener('input', () => updateSliderValue(dom.mainSlider, dom.mainValueSpan));
dom.usbSlider.addEventListener('input', () => updateSliderValue(dom.usbSlider, dom.usbValueSpan));
dom.currentLimitApplyButton.addEventListener('click', () => {
const settings = {
vin_current_limit: parseFloat(dom.vinSlider.value),
main_current_limit: parseFloat(dom.mainSlider.value),
usb_current_limit: parseFloat(dom.usbSlider.value)
};
fetch('/api/setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(), // Add auth headers
},
body: JSON.stringify(settings),
})
.then(handleResponse) // Handle response for 401
.then(response => response.json())
.then(data => {
console.log('Current limit settings applied:', data);
})
.catch((error) => {
console.error('Error applying current limit settings:', error);
alert('Failed to apply current limit settings.');
});
});
// --- Settings Modal Toggles (for showing/hiding sections) ---
dom.apModeToggle.addEventListener('change', () => {
dom.apModeConfig.style.display = dom.apModeToggle.checked ? 'block' : 'none';
@@ -145,12 +54,7 @@ export function setupEventListeners() {
// --- General App Listeners ---
dom.settingsButton.addEventListener('click', ui.initializeSettings);
// --- Accessibility & Modal Events ---
dom.settingsModal.addEventListener('show.bs.modal', () => {
// Load settings when the modal is about to be shown
loadCurrentLimitSettings();
});
// --- Accessibility: Remove focus from modal elements before hiding ---
const blurActiveElement = () => {
if (document.activeElement && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
@@ -186,6 +90,4 @@ export function setupEventListeners() {
// --- Window Resize Event ---
// Debounced to avoid excessive calls during resizing.
window.addEventListener('resize', debounce(ui.handleResize, 150));
listenersAttached = true;
}

View File

@@ -10,285 +10,100 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.css';
import './style.css';
// --- Module Imports -- -
import {StatusMessage} from './proto.js';
import * as api from './api.js';
import {initWebSocket} from './websocket.js';
import {setupTerminal, term} from './terminal.js';
// --- Module Imports ---
import { initWebSocket } from './websocket.js';
import { setupTerminal, term } from './terminal.js';
import {
applyTheme,
initUI,
updateControlStatus,
updateSensorUI,
updateSwitchStatusUI,
updateUptimeUI,
updateVersionUI,
updateWebsocketStatus,
updateWifiStatusUI
updateWifiStatusUI,
updateWebsocketStatus
} from './ui.js';
import {setupEventListeners} from './events.js';
// --- Globals ---
// StatusMessage is imported directly from the generated proto.js file.
// --- DOM Elements ---
const loginContainer = document.getElementById('login-container');
const mainContent = document.querySelector('main.container');
const loginForm = document.getElementById('login-form');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const loginAlert = document.getElementById('login-alert');
const logoutButton = document.getElementById('logout-button');
const themeToggleLogin = document.getElementById('theme-toggle-login');
const themeIconLogin = document.getElementById('theme-icon-login');
const themeToggleMain = document.getElementById('theme-toggle');
const themeIconMain = document.getElementById('theme-icon');
// User Settings DOM Elements
const userSettingsForm = document.getElementById('user-settings-form');
const newUsernameInput = document.getElementById('new-username');
const newPasswordInput = document.getElementById('new-password');
const confirmPasswordInput = document.getElementById('confirm-password');
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);
console.log('Connected to WebSocket Server');
}
function onWsClose() {
updateWebsocketStatus(false);
console.warn('Connection closed. Reconnecting...');
setTimeout(connect, 2000);
if (term) {
term.write('\x1b[32mConnected to WebSocket Server\x1b[0m\r\n');
}
updateControlStatus();
}
/**
* Callback for when a message is received from the WebSocket server.
* 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 (!(event.data instanceof ArrayBuffer)) {
console.warn('Message is not an ArrayBuffer, skipping protobuf decoding.');
return;
}
const buffer = new Uint8Array(event.data);
try {
const decodedMessage = StatusMessage.decode(buffer);
const payloadType = decodedMessage.payload;
switch (payloadType) {
case 'sensorData': {
const sensorData = decodedMessage.sensorData;
if (sensorData) {
// Create a payload for the sensor UI (charts and header)
const sensorPayload = {
USB: sensorData.usb,
MAIN: sensorData.main,
VIN: sensorData.vin,
timestamp: sensorData.timestamp
};
updateSensorUI(sensorPayload);
// Update uptime separately from the sensor data payload
if (sensorData.uptimeSec !== undefined) {
updateUptimeUI(sensorData.uptimeSec);
}
}
break;
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);
}
case 'wifiStatus':
updateWifiStatusUI(decodedMessage.wifiStatus);
break;
case 'swStatus':
if (decodedMessage.swStatus) {
updateSwitchStatusUI(decodedMessage.swStatus);
}
break;
case 'uartData':
if (term && decodedMessage.uartData && decodedMessage.uartData.data) {
term.write(decodedMessage.uartData.data);
}
break;
default:
if (payloadType !== undefined) {
console.warn('Received message with unknown or empty payload type:', payloadType);
}
break;
} catch (e) {
// Ignore non-JSON string messages
}
} catch (e) {
console.error('Error decoding protobuf message:', e);
} else if (term && event.data instanceof ArrayBuffer) {
// Write raw UART data to the terminal
const data = new Uint8Array(event.data);
term.write(data);
}
}
// --- Authentication Functions ---
function checkAuth() {
const token = localStorage.getItem('authToken');
if (token) {
return true;
} else {
return false;
}
}
async function handleLogin(event) {
event.preventDefault();
const username = usernameInput.value;
const password = passwordInput.value;
try {
const response = await api.login(username, password);
if (response && response.token) {
localStorage.setItem('authToken', response.token);
loginAlert.classList.add('d-none');
loginContainer.style.setProperty('display', 'none', 'important');
initializeMainAppContent(); // After successful login, initialize the main app
} else {
loginAlert.textContent = 'Login failed: No token received.';
loginAlert.classList.remove('d-none');
}
} catch (error) {
console.error('Login error:', error);
loginAlert.textContent = `Login failed: ${error.message}`;
loginAlert.classList.remove('d-none');
}
}
function handleLogout() {
localStorage.removeItem('authToken');
// Hide main content and show login form
loginContainer.style.setProperty('display', 'flex', 'important');
mainContent.style.setProperty('display', 'none', 'important');
// Optionally, disconnect WebSocket or perform other cleanup
// For now, just hide the main content.
}
// --- User Settings Functions ---
async function handleUserSettingsSubmit(event) {
event.preventDefault();
const newUsername = newUsernameInput.value;
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (!newUsername || !newPassword || !confirmPassword) {
alert('Please fill in all fields for username and password.');
return;
}
if (newPassword !== confirmPassword) {
alert('New password and confirm password do not match.');
return;
}
try {
const response = await api.updateUserSettings(newUsername, newPassword);
if (response && response.status === 'user_credentials_updated') {
alert('Username and password updated successfully. Please log in again with new credentials.');
handleLogout(); // Force logout to re-authenticate with new credentials
} else {
alert(`Failed to update credentials: ${response.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Error updating user settings:', error);
alert(`Error updating user settings: ${error.message}`);
}
}
// --- Theme Toggle Functions ---
function setupThemeToggles() {
// Initialize theme for login page
const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
applyTheme(savedTheme);
themeToggleLogin.checked = savedTheme === 'dark';
themeIconLogin.className = savedTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
// Sync main theme toggle with login theme toggle (initial state)
themeToggleMain.checked = savedTheme === 'dark';
themeIconMain.className = savedTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
themeToggleLogin.addEventListener('change', () => {
const newTheme = themeToggleLogin.checked ? 'dark' : 'light';
applyTheme(newTheme);
localStorage.setItem('theme', newTheme);
themeIconLogin.className = newTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
themeToggleMain.checked = themeToggleLogin.checked; // Keep main toggle in sync
themeIconMain.className = themeIconLogin.className; // Keep main icon in sync
});
themeToggleMain.addEventListener('change', () => {
const newTheme = themeToggleMain.checked ? 'dark' : 'light';
applyTheme(newTheme);
localStorage.setItem('theme', newTheme);
themeIconMain.className = newTheme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
themeToggleLogin.checked = themeToggleMain.checked; // Keep login toggle in sync
themeIconLogin.className = themeIconMain.className; // Keep login icon in sync
});
}
// --- Application Initialization ---
async function initializeVersion() {
try {
const versionData = await api.fetchVersion();
if (versionData && versionData.version) {
updateVersionUI(versionData.version);
}
} catch (error) {
console.error('Error fetching version:', error);
updateVersionUI('N/A');
}
}
function connect() {
updateControlStatus();
initWebSocket({ onOpen: onWsOpen, onClose: onWsClose, onMessage: onWsMessage });
}
// New function to initialize main app content after successful login or on initial load if authenticated
function initializeMainAppContent() {
loginContainer.style.setProperty('display', 'none', 'important');
mainContent.style.setProperty('display', 'block', 'important');
initUI();
setupTerminal();
initializeVersion();
setupEventListeners(); // Attach main app event listeners
logoutButton.addEventListener('click', handleLogout); // Attach logout listener
connect();
// Attach user settings form listener
if (userSettingsForm) {
userSettingsForm.addEventListener('submit', handleUserSettingsSubmit);
}
}
/**
* Initializes the entire application.
* This function sets up the UI, theme, terminal, chart, WebSocket connection, and event listeners.
*/
function initialize() {
setupThemeToggles(); // Setup theme toggles for both login and main (initial sync)
// Initialize basic UI components
initUI();
// Always attach login form listener
loginForm.addEventListener('submit', handleLogin);
// Set up the interactive components first
setupTerminal();
if (!checkAuth()) { // If NOT authenticated
// Show login form
loginContainer.style.setProperty('display', 'flex', 'important');
mainContent.style.setProperty('display', 'none', 'important');
console.log('Not authenticated. Login form displayed. Main app content NOT initialized.');
return; // IMPORTANT: Stop execution here if not authenticated
}
// 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);
// If authenticated, initialize main content
console.log('Authenticated. Initializing main app content.');
initializeMainAppContent();
// 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);

View File

@@ -1,17 +1,15 @@
:root {
--bs-body-font-family: 'Courier New', Courier, monospace;
/* Chart Channel Colors */
--chart-usb-color: #0d6efd; /* Bootstrap Blue */
--chart-main-color: #198754; /* Bootstrap Green */
--chart-vin-color: #dc3545; /* Bootstrap Red */
--chart-power-color: #007bff;
--chart-voltage-color: #28a745;
--chart-current-color: #ffc107;
}
[data-bs-theme="dark"] {
/* Chart Channel Colors for Dark Theme */
--chart-usb-color: #569cd6; /* A lighter blue for dark backgrounds */
--chart-main-color: #4ec9b0; /* A teal/cyan for dark backgrounds */
--chart-vin-color: #d16969; /* A softer red for dark backgrounds */
--chart-power-color: #569cd6;
--chart-voltage-color: #4ec9b0;
--chart-current-color: #dcdcaa;
}
body, .card, .modal-content, .list-group-item, .nav-tabs .nav-link {
@@ -114,10 +112,6 @@ footer a {
/* Mobile Optimizations */
@media (max-width: 767.98px) {
#login-container {
height: 100%;
}
.main-header {
flex-direction: column;
}

View File

@@ -4,12 +4,12 @@
* theme handling, and data communication with the WebSocket.
*/
import {Terminal} from '@xterm/xterm';
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 {sendWebsocketMessage} from './websocket.js';
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;
@@ -41,7 +41,7 @@ export function setupTerminal() {
}
fitAddon = new FitAddon();
term = new Terminal({convertEol: true, cursorBlink: true});
term = new Terminal({ convertEol: true, cursorBlink: true });
term.loadAddon(fitAddon);
term.open(terminalContainer);
@@ -114,7 +114,7 @@ export function downloadTerminalOutput() {
}
// Create a blob from the text content
const blob = new Blob([fullText], {type: 'text/plain;charset=utf-8'});
const blob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
// Create a link element to trigger the download
const link = document.createElement('a');

View File

@@ -8,9 +8,9 @@
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';
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;
@@ -40,49 +40,21 @@ export function applyTheme(themeName) {
* @param {Object} data - The sensor data object from the WebSocket.
*/
export function updateSensorUI(data) {
// Display VIN channel data in the header as a primary overview
if (data.VIN) {
dom.voltageDisplay.textContent = `${data.VIN.voltage.toFixed(2)} V`;
dom.currentDisplay.textContent = `${data.VIN.current.toFixed(2)} A`;
dom.powerDisplay.textContent = `${data.VIN.power.toFixed(2)} W`;
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);
}
// Pass the entire multi-channel data object to the charts
updateCharts(data);
}
/**
* Updates the system uptime display in the UI.
* @param {number} uptimeInSeconds - The system uptime in seconds.
*/
export function updateUptimeUI(uptimeInSeconds) {
if (uptimeInSeconds !== undefined) {
dom.uptimeDisplay.textContent = formatUptime(uptimeInSeconds);
}
}
/**
* Updates the power switch toggle states based on WebSocket data.
* @param {Object} swStatus - The switch status object from the WebSocket message.
*/
export function updateSwitchStatusUI(swStatus) {
if (swStatus) {
if (swStatus.main !== undefined) {
dom.mainPowerToggle.checked = swStatus.main;
}
if (swStatus.usb !== undefined) {
dom.usbPowerToggle.checked = swStatus.usb;
}
}
}
/**
* 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) {
// Update header status
dom.wifiSsidStatus.textContent = data.ssid;
dom.wifiStatus.title = `Signal Strength: ${data.rssi} dBm`;
let iconClass = 'bi me-2 ';
@@ -92,31 +64,12 @@ export function updateWifiStatusUI(data) {
dom.wifiIcon.className = iconClass;
dom.wifiStatus.classList.replace('text-muted', 'text-success');
dom.wifiStatus.classList.remove('text-danger');
// Update settings modal
dom.currentWifiSsid.textContent = data.ssid;
dom.currentWifiIp.textContent = `IP Address: ${data.ipAddress || 'N/A'}`;
} else {
// Update header status
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');
// Update settings modal
dom.currentWifiSsid.textContent = 'Not Connected';
dom.currentWifiIp.textContent = 'IP Address: -';
}
}
/**
* Updates the version information in the footer.
* @param {string} version - The firmware version string.
*/
export function updateVersionUI(version) {
if (version) {
dom.versionInfo.textContent = `${version}`;
}
}
@@ -156,7 +109,7 @@ export async function scanForWifi() {
wifiModal.show();
dom.wifiModalEl.addEventListener('shown.bs.modal', () => {
dom.wifiPasswordConnectInput.focus();
}, {once: true});
}, { once: true });
});
dom.wifiApList.appendChild(row);
@@ -225,10 +178,10 @@ export async function applyNetworkSettings() {
return;
}
payload = {net_type: 'static', ip, gateway, subnet, dns1};
payload = { net_type: 'static', ip, gateway, subnet, dns1 };
if (dns2) payload.dns2 = dns2;
} else {
payload = {net_type: 'dhcp'};
payload = { net_type: 'dhcp' };
}
try {
@@ -249,7 +202,7 @@ export async function applyNetworkSettings() {
*/
export async function applyApModeSettings() {
const mode = dom.apModeToggle.checked ? 'apsta' : 'sta';
let payload = {mode};
let payload = { mode };
dom.apModeApplyButton.disabled = true;
dom.apModeApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;

View File

@@ -20,7 +20,7 @@ export function debounce(func, delay) {
}
/**
* Formats a duration in total seconds into a human-readable string (e.g., "2days 02:30:15").
* 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.
*/
@@ -31,7 +31,7 @@ export function formatUptime(totalSeconds) {
const seconds = totalSeconds % 60;
const pad = (num) => String(num).padStart(2, '0');
const timeString = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
return days > 0 ? `${days}days ${timeString}` : timeString;
return days > 0 ? `${days}d ${timeString}` : timeString;
}
/**

View File

@@ -2,116 +2,32 @@
* @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, including a heartbeat mechanism to detect disconnections.
* 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 host (hostname + port).
const baseGateway = `ws://${window.location.host}/ws`;
// Heartbeat related variables
let pingIntervalId = null;
let pongTimeoutId = null;
const HEARTBEAT_INTERVAL = 10000; // 10 seconds: How often to send a 'ping'
const HEARTBEAT_TIMEOUT = 5000; // 5 seconds: How long to wait for a 'pong' after sending a 'ping'
// The WebSocket server address, derived from the current page's hostname.
const gateway = `ws://${window.location.hostname}/ws`;
/**
* Starts the heartbeat mechanism.
* Sends a 'ping' message to the server at regular intervals and sets a timeout
* to detect if a 'pong' response is not received.
*/
function startHeartbeat() {
stopHeartbeat(); // Ensure any previous heartbeat is stopped before starting a new one
pingIntervalId = setInterval(() => {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send('ping');
// Set a timeout to check if a pong is received within HEARTBEAT_TIMEOUT
pongTimeoutId = setTimeout(() => {
console.warn('WebSocket: No pong received within timeout, closing connection.');
// If no pong is received, close the connection. This will trigger the onClose handler.
websocket.close();
}, HEARTBEAT_TIMEOUT);
}
}, HEARTBEAT_INTERVAL);
}
/**
* Stops the heartbeat mechanism by clearing the ping interval and pong timeout.
*/
function stopHeartbeat() {
if (pingIntervalId) {
clearInterval(pingIntervalId);
pingIntervalId = null;
}
if (pongTimeoutId) {
clearTimeout(pongTimeoutId);
pongTimeoutId = null;
}
}
/**
* Initializes the WebSocket connection and sets up event handlers, including a heartbeat mechanism.
* 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 (excluding 'pong' messages).
* @param {function} [callbacks.onError] - Called when an error occurs with the WebSocket connection.
* @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, onError}) {
const token = localStorage.getItem('authToken');
let gateway = baseGateway;
if (token) {
gateway = `${baseGateway}?token=${token}`;
}
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, wrapping user-provided callbacks to include heartbeat logic
websocket.onopen = (event) => {
console.log('WebSocket connection opened.');
startHeartbeat(); // Start heartbeat on successful connection
if (onOpen) {
onOpen(event);
}
};
websocket.onclose = (event) => {
console.log('WebSocket connection closed:', event);
stopHeartbeat(); // Stop heartbeat when connection closes
if (onClose) {
onClose(event);
}
};
websocket.onmessage = (event) => {
if (event.data === 'pong') {
// Clear the timeout as pong was received, resetting for the next ping
clearTimeout(pongTimeoutId);
pongTimeoutId = null;
} else {
// If it's not a pong message, pass it to the user's onMessage callback
if (onMessage) {
onMessage(event);
} else {
console.log('WebSocket message received:', event.data);
}
}
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
if (onError) {
onError(error);
}
};
// Assign event handlers from the provided callbacks
if (onOpen) websocket.onopen = onOpen;
if (onClose) websocket.onclose = onClose;
if (onMessage) websocket.onmessage = onMessage;
}
/**
@@ -121,7 +37,5 @@ export function initWebSocket({onOpen, onClose, onMessage, onError}) {
export function sendWebsocketMessage(data) {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send(data);
} else {
console.warn('WebSocket is not open. Message not sent:', data);
}
}
}

View File

@@ -1,46 +0,0 @@
syntax = "proto3";
// Represents data for a single sensor channel
message SensorChannelData {
float voltage = 1;
float current = 2;
float power = 3;
}
// Contains data for all sensor channels and system info
message SensorData {
SensorChannelData usb = 1;
SensorChannelData main = 2;
SensorChannelData vin = 3;
uint32 timestamp = 4;
uint32 uptime_sec = 5;
}
// Contains WiFi connection status
message WifiStatus {
bool connected = 1;
string ssid = 2;
int32 rssi = 3;
string ip_address = 4;
}
// Contains raw UART data
message UartData {
bytes data = 1;
}
// Contains load sw status
message LoadSwStatus {
bool main = 1;
bool usb = 2;
}
// Top-level message for all websocket communication
message StatusMessage {
oneof payload {
SensorData sensor_data = 1;
WifiStatus wifi_status = 2;
LoadSwStatus sw_status = 3;
UartData uart_data = 4;
}
}

2106
sdkconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,10 @@ CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=40
CONFIG_ESP_WIFI_TX_BA_WIN=32
CONFIG_ESP_WIFI_RX_BA_WIN=32
CONFIG_FREERTOS_HZ=500
CONFIG_LWIP_LOCAL_HOSTNAME="odroid-pm"
CONFIG_LWIP_TCPIP_CORE_LOCKING=y
CONFIG_LWIP_TCPIP_CORE_LOCKING_INPUT=y
CONFIG_LWIP_IRAM_OPTIMIZATION=y
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=64
CONFIG_LWIP_IPV6=n
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=65534
CONFIG_LWIP_TCP_WND_DEFAULT=65534
CONFIG_LWIP_TCP_RECVMBOX_SIZE=64