Compare commits
4 Commits
e07aad2d7d
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e7d3cabfe | |||
| 40fd0a667a | |||
| 89e17efcfc | |||
| fb2ab88bc9 |
@@ -42,6 +42,8 @@ enum nconfig_type
|
||||
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.
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ const static char* keys[NCONFIG_TYPE_MAX] = {
|
||||
[VIN_CURRENT_LIMIT] = "vin_climit",
|
||||
[MAIN_CURRENT_LIMIT] = "main_climit",
|
||||
[USB_CURRENT_LIMIT] = "usb_climit",
|
||||
[PAGE_USERNAME] = "username",
|
||||
[PAGE_PASSWORD] = "password",
|
||||
};
|
||||
|
||||
struct default_value
|
||||
@@ -50,6 +52,8 @@ struct default_value const default_values[] = {
|
||||
{VIN_CURRENT_LIMIT, "4.0"},
|
||||
{MAIN_CURRENT_LIMIT, "3.0"},
|
||||
{USB_CURRENT_LIMIT, "3.0"},
|
||||
{PAGE_USERNAME, "admin"},
|
||||
{PAGE_PASSWORD, "password"},
|
||||
};
|
||||
|
||||
esp_err_t init_nconfig()
|
||||
|
||||
@@ -177,6 +177,8 @@ static esp_err_t setting_post_handler(httpd_req_t* req)
|
||||
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");
|
||||
|
||||
if (mode_item && cJSON_IsString(mode_item))
|
||||
{
|
||||
@@ -322,6 +324,17 @@ static esp_err_t setting_post_handler(httpd_req_t* req)
|
||||
}
|
||||
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
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid payload");
|
||||
|
||||
@@ -80,12 +80,58 @@ static esp_err_t login_handler(httpd_req_t* req)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
const char* username = username_json->valuestring;
|
||||
const char* password = password_json->valuestring;
|
||||
const char* received_username = username_json->valuestring;
|
||||
const char* received_password = password_json->valuestring;
|
||||
|
||||
// TODO: Implement actual credential validation
|
||||
// For now, a simple hardcoded check
|
||||
if (strcmp(username, "admin") == 0 && strcmp(password, "password") == 0)
|
||||
// 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)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "pb.h"
|
||||
#include "pb_encode.h"
|
||||
#include "status.pb.h"
|
||||
#include "string.h" // Added for strlen and strncmp
|
||||
#include "webserver.h"
|
||||
|
||||
#define UART_NUM UART_NUM_1
|
||||
@@ -264,7 +265,6 @@ static esp_err_t ws_handler(httpd_req_t* req)
|
||||
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)
|
||||
@@ -273,7 +273,33 @@ static esp_err_t ws_handler(httpd_req_t* req)
|
||||
return ret;
|
||||
}
|
||||
|
||||
uart_write_bytes(UART_NUM, (const char*)ws_pkt.payload, ws_pkt.len);
|
||||
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);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -327,4 +353,4 @@ void push_data_to_ws(const uint8_t* data, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
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); }
|
||||
@@ -197,6 +197,11 @@
|
||||
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
|
||||
@@ -331,6 +336,28 @@
|
||||
<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>
|
||||
|
||||
@@ -171,3 +171,22 @@ export async function fetchVersion() {
|
||||
});
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
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 {getAuthHeaders, handleResponse} from './api.js'; // Import auth functions
|
||||
|
||||
// A flag to track if charts have been initialized
|
||||
let chartsInitialized = false;
|
||||
@@ -60,7 +60,6 @@ export function setupEventListeners() {
|
||||
console.log("Event listeners already attached. Skipping.");
|
||||
return;
|
||||
}
|
||||
console.log("Attaching event listeners...");
|
||||
|
||||
// --- Terminal Controls ---
|
||||
dom.clearButton.addEventListener('click', clearTerminal);
|
||||
@@ -189,5 +188,4 @@ export function setupEventListeners() {
|
||||
window.addEventListener('resize', debounce(ui.handleResize, 150));
|
||||
|
||||
listenersAttached = true;
|
||||
console.log("Event listeners attached successfully.");
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ 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');
|
||||
|
||||
|
||||
// --- WebSocket Event Handlers ---
|
||||
|
||||
@@ -168,6 +174,38 @@ function handleLogout() {
|
||||
// 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
|
||||
@@ -230,6 +268,11 @@ function initializeMainAppContent() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @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.
|
||||
* and send messages, including a heartbeat mechanism to detect disconnections.
|
||||
*/
|
||||
|
||||
// The WebSocket instance, exported for potential direct access if needed.
|
||||
@@ -11,14 +11,57 @@ 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'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
export function initWebSocket({onOpen, onClose, onMessage}) {
|
||||
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.
|
||||
* @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.
|
||||
*/
|
||||
export function initWebSocket({onOpen, onClose, onMessage, onError}) {
|
||||
const token = localStorage.getItem('authToken');
|
||||
let gateway = baseGateway;
|
||||
|
||||
@@ -31,10 +74,44 @@ export function initWebSocket({onOpen, onClose, onMessage}) {
|
||||
// Set binary type to arraybuffer to handle raw binary data from the UART.
|
||||
websocket.binaryType = "arraybuffer";
|
||||
|
||||
// Assign event handlers from the provided callbacks
|
||||
if (onOpen) websocket.onopen = onOpen;
|
||||
if (onClose) websocket.onclose = onClose;
|
||||
if (onMessage) websocket.onmessage = onMessage;
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,5 +121,7 @@ export function initWebSocket({onOpen, onClose, onMessage}) {
|
||||
export function sendWebsocketMessage(data) {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(data);
|
||||
} else {
|
||||
console.warn('WebSocket is not open. Message not sent:', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user