394 lines
14 KiB
JavaScript
394 lines
14 KiB
JavaScript
/**
|
|
* @file ui.js
|
|
* @description This module manages all UI interactions and updates.
|
|
* It bridges the gap between backend data (from API and WebSockets) and the user-facing components,
|
|
* handling everything from theme changes to dynamic content updates.
|
|
*/
|
|
|
|
import * as bootstrap from 'bootstrap';
|
|
import * as dom from './dom.js';
|
|
import * as api from './api.js';
|
|
import {formatUptime, isMobile} from './utils.js';
|
|
import {applyTerminalTheme, fitTerminal} from './terminal.js';
|
|
import {applyChartsTheme, resizeCharts, updateCharts} from './chart.js';
|
|
|
|
// Instance of the Bootstrap Modal for Wi-Fi connection
|
|
let wifiModal;
|
|
|
|
/**
|
|
* Initializes the UI components, such as the Bootstrap modal.
|
|
*/
|
|
export function initUI() {
|
|
wifiModal = new bootstrap.Modal(dom.wifiModalEl);
|
|
}
|
|
|
|
/**
|
|
* Applies the selected theme (light or dark) to the entire application.
|
|
* @param {string} themeName - The name of the theme ('light' or 'dark').
|
|
*/
|
|
export function applyTheme(themeName) {
|
|
const isDark = themeName === 'dark';
|
|
dom.htmlEl.setAttribute('data-bs-theme', themeName);
|
|
dom.themeIcon.className = isDark ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
|
dom.themeToggle.checked = isDark;
|
|
applyTerminalTheme(themeName);
|
|
applyChartsTheme(themeName);
|
|
}
|
|
|
|
/**
|
|
* Updates the UI with the latest sensor data.
|
|
* @param {Object} data - The sensor data object from the WebSocket.
|
|
*/
|
|
export function updateSensorUI(data) {
|
|
// 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`;
|
|
}
|
|
|
|
// 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 ';
|
|
if (data.rssi >= -60) iconClass += 'bi-wifi';
|
|
else if (data.rssi >= -75) iconClass += 'bi-wifi-2';
|
|
else iconClass += 'bi-wifi-1';
|
|
dom.wifiIcon.className = iconClass;
|
|
dom.wifiStatus.classList.replace('text-muted', 'text-success');
|
|
dom.wifiStatus.classList.remove('text-danger');
|
|
|
|
// 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}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiates a Wi-Fi scan and updates the settings modal with the results.
|
|
*/
|
|
export async function scanForWifi() {
|
|
dom.scanWifiButton.disabled = true;
|
|
dom.scanWifiButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Scanning...`;
|
|
dom.wifiApList.innerHTML = '<tr><td colspan="3" class="text-center">Scanning for networks...</td></tr>';
|
|
|
|
try {
|
|
const apRecords = await api.fetchWifiScan();
|
|
dom.wifiApList.innerHTML = ''; // Clear loading message
|
|
|
|
if (apRecords.length === 0) {
|
|
dom.wifiApList.innerHTML = '<tr><td colspan="3" class="text-center">No networks found.</td></tr>';
|
|
} else {
|
|
apRecords.forEach(ap => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'wifi-ap-row';
|
|
|
|
let rssiIcon;
|
|
if (ap.rssi >= -60) rssiIcon = 'bi-wifi';
|
|
else if (ap.rssi >= -75) rssiIcon = 'bi-wifi-2';
|
|
else rssiIcon = 'bi-wifi-1';
|
|
|
|
row.innerHTML = `
|
|
<td>${ap.ssid}</td>
|
|
<td class="text-center"><i class="bi ${rssiIcon}"></i></td>
|
|
<td>${ap.authmode}</td>
|
|
`;
|
|
|
|
row.addEventListener('click', () => {
|
|
dom.wifiSsidConnectInput.value = ap.ssid;
|
|
dom.wifiPasswordConnectInput.value = '';
|
|
wifiModal.show();
|
|
dom.wifiModalEl.addEventListener('shown.bs.modal', () => {
|
|
dom.wifiPasswordConnectInput.focus();
|
|
}, {once: true});
|
|
});
|
|
|
|
dom.wifiApList.appendChild(row);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error scanning for Wi-Fi:', error);
|
|
dom.wifiApList.innerHTML = `<tr><td colspan="3" class="text-center text-danger">Scan failed: ${error.message}</td></tr>`;
|
|
} finally {
|
|
dom.scanWifiButton.disabled = false;
|
|
dom.scanWifiButton.innerHTML = 'Scan';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the Wi-Fi connection process, sending credentials to the server.
|
|
*/
|
|
export async function connectToWifi() {
|
|
const ssid = dom.wifiSsidConnectInput.value;
|
|
const password = dom.wifiPasswordConnectInput.value;
|
|
if (!ssid) return;
|
|
|
|
dom.wifiConnectButton.disabled = true;
|
|
dom.wifiConnectButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Connecting...`;
|
|
|
|
try {
|
|
const result = await api.postWifiConnect(ssid, password);
|
|
if (result.status === 'connection_initiated') {
|
|
wifiModal.hide();
|
|
setTimeout(() => {
|
|
alert(`Connection to "${ssid}" initiated. The device will try to reconnect. Please check the Wi-Fi status icon.`);
|
|
}, 500);
|
|
} else {
|
|
throw new Error(result.message || 'Unknown server response.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error connecting to Wi-Fi:', error);
|
|
alert(`Failed to connect: ${error.message}`);
|
|
} finally {
|
|
dom.wifiConnectButton.disabled = false;
|
|
dom.wifiConnectButton.innerHTML = 'Connect';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies network settings (Static IP or DHCP) by sending the configuration to the server.
|
|
*/
|
|
export async function applyNetworkSettings() {
|
|
const useStatic = dom.staticIpToggle.checked;
|
|
let payload;
|
|
|
|
dom.networkApplyButton.disabled = true;
|
|
dom.networkApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
|
|
|
if (useStatic) {
|
|
const ip = dom.staticIpInput.value;
|
|
const gateway = dom.staticGatewayInput.value;
|
|
const subnet = dom.staticNetmaskInput.value;
|
|
const dns1 = dom.dns1Input.value;
|
|
const dns2 = dom.dns2Input.value;
|
|
|
|
if (!ip || !gateway || !subnet || !dns1) {
|
|
alert('For static IP, you must provide IP Address, Gateway, Netmask, and DNS Server.');
|
|
dom.networkApplyButton.disabled = false;
|
|
dom.networkApplyButton.innerHTML = 'Apply';
|
|
return;
|
|
}
|
|
|
|
payload = {net_type: 'static', ip, gateway, subnet, dns1};
|
|
if (dns2) payload.dns2 = dns2;
|
|
} else {
|
|
payload = {net_type: 'dhcp'};
|
|
}
|
|
|
|
try {
|
|
await api.postNetworkSettings(payload);
|
|
alert('Network settings applied. Reconnect to the network for changes to take effect.');
|
|
initializeSettings();
|
|
} catch (error) {
|
|
console.error('Error applying network settings:', error);
|
|
alert(`Failed to apply settings: ${error.message}`);
|
|
} finally {
|
|
dom.networkApplyButton.disabled = false;
|
|
dom.networkApplyButton.innerHTML = 'Apply';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies AP Mode settings (AP+STA or STA) by sending the configuration to the server.
|
|
*/
|
|
export async function applyApModeSettings() {
|
|
const mode = dom.apModeToggle.checked ? 'apsta' : 'sta';
|
|
let payload = {mode};
|
|
|
|
dom.apModeApplyButton.disabled = true;
|
|
dom.apModeApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
|
|
|
if (mode === 'apsta') {
|
|
const ap_ssid = dom.apSsidInput.value;
|
|
const ap_password = dom.apPasswordInput.value;
|
|
|
|
if (!ap_ssid) {
|
|
alert('AP SSID cannot be empty when enabling APSTA mode.');
|
|
dom.apModeApplyButton.disabled = false;
|
|
dom.apModeApplyButton.innerHTML = 'Apply';
|
|
return;
|
|
}
|
|
payload.ap_ssid = ap_ssid;
|
|
if (ap_password) {
|
|
payload.ap_password = ap_password;
|
|
}
|
|
}
|
|
|
|
try {
|
|
await api.postNetworkSettings(payload); // Reuses the same API endpoint
|
|
alert(`Successfully switched mode to ${mode}. The device will now reconfigure.`);
|
|
initializeSettings();
|
|
} catch (error) {
|
|
console.error('Error switching Wi-Fi mode:', error);
|
|
alert(`Failed to switch mode: ${error.message}`);
|
|
} finally {
|
|
dom.apModeApplyButton.disabled = false;
|
|
dom.apModeApplyButton.innerHTML = 'Apply';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies the selected UART baud rate by sending it to the server.
|
|
*/
|
|
export async function applyBaudRateSettings() {
|
|
const baudrate = dom.baudRateSelect.value;
|
|
dom.baudRateApplyButton.disabled = true;
|
|
dom.baudRateApplyButton.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> Applying...`;
|
|
|
|
try {
|
|
await api.postBaudRateSetting(baudrate);
|
|
} catch (error) {
|
|
console.error('Error applying baud rate:', error);
|
|
} finally {
|
|
dom.baudRateApplyButton.disabled = false;
|
|
dom.baudRateApplyButton.innerHTML = 'Apply';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches and displays the current network and device settings in the settings modal.
|
|
*/
|
|
export async function initializeSettings() {
|
|
try {
|
|
const data = await api.fetchSettings();
|
|
|
|
// Wi-Fi Connection Status
|
|
if (data.connected) {
|
|
dom.currentWifiSsid.textContent = data.ssid;
|
|
dom.currentWifiIp.textContent = `IP Address: ${data.ip ? data.ip.ip : 'N/A'}`;
|
|
} else {
|
|
dom.currentWifiSsid.textContent = 'Not Connected';
|
|
dom.currentWifiIp.textContent = 'IP Address: -';
|
|
}
|
|
|
|
// Network (Static/DHCP) Settings
|
|
if (data.ip) {
|
|
dom.staticIpInput.value = data.ip.ip || '';
|
|
dom.staticGatewayInput.value = data.ip.gateway || '';
|
|
dom.staticNetmaskInput.value = data.ip.subnet || '';
|
|
dom.dns1Input.value = data.ip.dns1 || '';
|
|
dom.dns2Input.value = data.ip.dns2 || '';
|
|
}
|
|
dom.staticIpToggle.checked = data.net_type === 'static';
|
|
dom.staticIpConfig.style.display = dom.staticIpToggle.checked ? 'block' : 'none';
|
|
|
|
// AP Mode Settings
|
|
dom.apModeToggle.checked = data.mode === 'apsta';
|
|
dom.apModeConfig.style.display = dom.apModeToggle.checked ? 'block' : 'none';
|
|
dom.apSsidInput.value = ''; // For security, don't pre-fill
|
|
dom.apPasswordInput.value = '';
|
|
|
|
// Device Settings
|
|
if (data.baudrate) {
|
|
dom.baudRateSelect.value = data.baudrate;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error initializing settings:', error);
|
|
// Reset fields on error
|
|
dom.currentWifiSsid.textContent = 'Status Unknown';
|
|
dom.currentWifiIp.textContent = 'IP Address: -';
|
|
dom.staticIpToggle.checked = false;
|
|
dom.staticIpConfig.style.display = 'none';
|
|
dom.apModeToggle.checked = false;
|
|
dom.apModeConfig.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches and updates the status of the power control toggles.
|
|
*/
|
|
export async function updateControlStatus() {
|
|
try {
|
|
const status = await api.fetchControlStatus();
|
|
dom.mainPowerToggle.checked = status.load_12v_on;
|
|
dom.usbPowerToggle.checked = status.load_5v_on;
|
|
} catch (error) {
|
|
console.error('Error fetching control status:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles window resize events to make components responsive.
|
|
*/
|
|
export function handleResize() {
|
|
if (isMobile()) {
|
|
fitTerminal();
|
|
}
|
|
resizeCharts();
|
|
}
|
|
|
|
/**
|
|
* Updates the WebSocket connection status indicator in the header.
|
|
* @param {boolean} isConnected - True if the WebSocket is connected, false otherwise.
|
|
*/
|
|
export function updateWebsocketStatus(isConnected) {
|
|
if (isConnected) {
|
|
dom.websocketStatusText.textContent = 'Online';
|
|
dom.websocketIcon.className = 'bi bi-check-circle-fill me-2';
|
|
dom.websocketStatus.classList.remove('text-danger');
|
|
dom.websocketStatus.classList.add('text-success');
|
|
} else {
|
|
dom.websocketStatusText.textContent = 'Offline';
|
|
dom.websocketIcon.className = 'bi bi-x-circle-fill me-2';
|
|
dom.websocketStatus.classList.remove('text-success');
|
|
dom.websocketStatus.classList.add('text-danger');
|
|
}
|
|
}
|