document.addEventListener('DOMContentLoaded', () => { // --- CONFIGURATION --- const SOCKET_URL = "http://192.168.1.10:5000"; const API_BASE = "http://192.168.1.10:5000/api"; // Added for API calls // --- DOM ELEMENT REFERENCES --- const grid = document.getElementById('chambersGrid'); const chamberTmpl = document.getElementById('chamberTemplate'); const logTextArea = document.getElementById('instance-log'); const connChip = document.getElementById('connection-status-chip'); const stationNameEl = document.getElementById('station-name'); const stationLocationEl = document.getElementById('station-location'); const deviceIdEl = document.getElementById('device-id'); const productIdEl = document.getElementById('product-id'); const lastUpdateEl = document.getElementById('last-update-status'); const stationDiagCodeEl = document.getElementById('station-diag-code'); const backupPowerChip = document.getElementById('backup-power-chip'); const diagFlagsGrid = document.getElementById('diag-flags-grid'); const audioSelect = document.getElementById('audio-command-select'); // Header Buttons const refreshBtn = document.getElementById('refreshBtn'); const downloadBtn = document.getElementById('downloadBtn'); const logoutBtn = document.getElementById('logout-btn'); // --- STATE --- let selectedStation = null; let socket; let statusPollingInterval; // To hold our interval timer let chamberData = Array(9).fill({ batteryPresent: false }); // The list of errors from your Python code const DIAGNOSTIC_ERRORS = [ "Lock Power Cut", "Main Power Cut", "Relayboard CAN", "DB CAN Recv", "MB Can Recv", "Smoke Alarm", "Water Alarm", "Phase Failure", "Earth Leakage" ]; const BATTERY_FAULT_MAP = { 8: "UT", // Under Temperature 4: "OV", // Over Voltage 2: "OT", // Over Temperature 1: "OC" // Over Current }; const CHARGER_FAULT_MAP = { 1: "OV", // Over Voltage 2: "UV", // Under Voltage 4: "OT", // Over Temperature 8: "CAN Failure" // Add other charger fault codes here }; // --- NEW: SWAP PROCESS ELEMENTS & LOGIC --- const swapIdleText = document.getElementById('swap-idle-text'); const swapPairsList = document.getElementById('swap-pairs-list'); const startSwapBtn = document.getElementById('start-swap-btn'); const abortSwapBtn = document.getElementById('abort-swap-btn'); const clearSwapBtn = document.getElementById('clear-swap-btn'); const resetBtn = document.getElementById('station-reset-btn'); let currentPair = [], swapPairs = []; // --- SWAP UI LOGIC --- function updateSwapUI() { const isBuilding = currentPair.length > 0 || swapPairs.length > 0; if (swapIdleText) swapIdleText.style.display = isBuilding ? 'none' : 'block'; if (startSwapBtn) startSwapBtn.disabled = swapPairs.length === 0; const pairedOut = swapPairs.map(p => p[0]), pairedIn = swapPairs.map(p => p[1]); document.querySelectorAll('.chamber-card').forEach(card => { const n = parseInt(card.dataset.chamberId, 10); card.classList.remove('paired', 'pending'); if (pairedOut.includes(n) || pairedIn.includes(n)) card.classList.add('paired'); else if (currentPair.includes(n)) card.classList.add('pending'); }); if (swapPairsList) { swapPairsList.innerHTML = ''; if (isBuilding) { swapPairs.forEach(p => { const e = document.createElement('div'); e.className = 'text-sm font-semibold flex items-center justify-center gap-2 bg-black/20 rounded p-1.5'; e.innerHTML = `${p[0]}${p[1]}`; swapPairsList.appendChild(e); }); if (currentPair.length > 0) { const e = document.createElement('div'); e.className = 'text-sm font-semibold flex items-center justify-center gap-2 bg-black/20 rounded p-1.5 ring-2 ring-sky-500'; e.innerHTML = `${currentPair[0]}?`; swapPairsList.appendChild(e); } } } } function handleChamberClick(num) { // Deselection logic if (currentPair.length === 1 && currentPair[0] === num) { currentPair = []; updateSwapUI(); return; } const isAlreadyPaired = swapPairs.flat().includes(num); if (isAlreadyPaired) { swapPairs = swapPairs.filter(pair => !pair.includes(num)); updateSwapUI(); return; } // Selection logic if (swapPairs.length >= 4) return alert('Maximum of 4 swap pairs reached.'); // Note: Live validation would go here. Removed for testing. currentPair.push(num); if (currentPair.length === 2) { swapPairs.push([...currentPair]); currentPair = []; } updateSwapUI(); } function clearSelection() { currentPair = []; swapPairs = []; updateSwapUI(); } // --- HELPER FUNCTIONS (Your original code is unchanged) --- const applyButtonFeedback = (button) => { if (!button) return; button.classList.add('btn-feedback'); setTimeout(() => { button.classList.remove('btn-feedback'); }, 150); }; // And ensure your sendCommand function does NOT have the feedback logic const sendCommand = (command, data = null) => { if (!selectedStation || !socket || !socket.connected) { logToInstance(`Cannot send command '${command}', not connected.`, "error"); return; } const payload = { station_id: selectedStation.id, command: command, data: data }; socket.emit('rpc_request', payload); logToInstance(`Sent command: ${command}`, 'cmd'); }; const setChipStyle = (element, text, style) => { if (!element) return; const baseClass = element.className.split(' ')[0]; element.textContent = text; element.className = `${baseClass} chip-${style}`; }; const logToInstance = (message, type = 'info') => { if (!logTextArea) return; const timestamp = new Date().toLocaleTimeString(); const typeIndicator = type === 'error' ? '[ERROR]' : type === 'cmd' ? '[CMD]' : '[INFO]'; const newLog = `[${timestamp}] ${typeIndicator} ${message}\n`; logTextArea.value = newLog + logTextArea.value; }; /** * Decodes a fault bitmask into a human-readable string using a given map. * @param {number} faultCode The fault code number. * @param {object} faultMap The map to use for decoding (e.g., BATTERY_FAULT_MAP). * @returns {string} A comma-separated string of active faults, or "—" if none. */ const decodeFaults = (faultCode, faultMap) => { if (!faultCode || faultCode === 0) { return "—"; // No fault } const activeFaults = []; for (const bit in faultMap) { if ((faultCode & bit) !== 0) { activeFaults.push(faultMap[bit]); } } return activeFaults.length > 0 ? activeFaults.join(', ') : "—"; }; const updateChamberUI = (card, slot) => { if (!card || !slot) return; const filledState = card.querySelector('.filled-state'); const emptyState = card.querySelector('.empty-state'); const doorPill = card.querySelector('.door-pill'); // Always update door status doorPill.textContent = slot.doorStatus ? 'OPEN' : 'CLOSED'; doorPill.className = slot.doorStatus ? 'door-pill door-open' : 'door-pill door-close'; const slotTempText = `${((slot.slotTemperature || 0) / 10).toFixed(1)} °C`; if (slot.batteryPresent) { filledState.style.display = 'flex'; emptyState.style.display = 'none'; // Update the detailed view card.querySelector('.slot-temp').textContent = slotTempText; card.querySelector('.bat-id-big').textContent = slot.batteryIdentification || '—'; card.querySelector('.soc').textContent = `${slot.soc || 0}%`; // --- Populate the detailed view --- card.querySelector('.bat-id-big').textContent = slot.batteryIdentification || '—'; card.querySelector('.soc').textContent = `${slot.soc || 0}%`; card.querySelector('.voltage').textContent = `${((slot.voltage || 0) / 1000).toFixed(1)} V`; card.querySelector('.bat-temp').textContent = `${((slot.batteryMaxTemp || 0) / 10).toFixed(1)} °C`; // card.querySelector('.bat-fault').textContent = slot.batteryFaultCode || '—'; card.querySelector('.bat-fault').textContent = decodeFaults(slot.batteryFaultCode, BATTERY_FAULT_MAP); // card.querySelector('.bat-fault').textContent = decodeBatteryFaults(slot.batteryFaultCode); card.querySelector('.current').textContent = `${((slot.current || 0) / 1000).toFixed(1)} A`; card.querySelector('.slot-temp').textContent = `${((slot.slotTemperature || 0) / 10).toFixed(1)} °C`; card.querySelector('.chg-temp').textContent = `${((slot.chargerMaxTemp || 0) / 10).toFixed(1)} °C`; // card.querySelector('.chg-fault').textContent = slot.chargerFaultCode || '—'; card.querySelector('.chg-fault').textContent = decodeFaults(slot.chargerFaultCode, CHARGER_FAULT_MAP); const batPill = card.querySelector('.battery-status-pill'); batPill.innerHTML = ` Present`; batPill.className = 'battery-status-pill chip chip-emerald'; const chgPill = card.querySelector('.charger-status-pill'); if (slot.chargerPresent && slot.current > 0) { chgPill.innerHTML = ` Charging`; chgPill.className = 'charger-status-pill chip chip-sky'; } else { chgPill.innerHTML = ` Idle`; chgPill.className = 'charger-status-pill chip chip-slate'; } } else { // Show the empty view filledState.style.display = 'none'; emptyState.style.display = 'flex'; // --- DEBUGGING LOGIC --- const tempElement = card.querySelector('.slot-temp-empty'); if (tempElement) { tempElement.textContent = slotTempText; // console.log(`Chamber ${slot.chamberNo}: Found .slot-temp-empty, setting text to: ${slotTempText}`); } else { // console.error(`Chamber ${slot.chamberNo}: Element .slot-temp-empty NOT FOUND! Check your HTML template.`); } } // Check if the icon library is loaded and then render the icons if (typeof lucide !== 'undefined') { lucide.createIcons(); } else { console.error('Lucide icon script is not loaded. Please check dashboard.html'); } }; // --- NEW: Function to decode the SDC and update the UI --- const updateDiagnosticsUI = (sdcCode) => { if (!diagFlagsGrid) return; diagFlagsGrid.innerHTML = ''; // Clear previous statuses DIAGNOSTIC_ERRORS.forEach((errorText, index) => { // Use bitwise AND to check if the bit at this index is set const isActive = (sdcCode & (1 << index)) !== 0; const div = document.createElement('div'); div.textContent = errorText; // Apply different styles based on whether the alarm is active if (isActive) { div.className = 'text-rose-300 text-center font-semibold'; } else { div.className = 'text-slate-500 text-center'; } diagFlagsGrid.appendChild(div); }); }; const checkStationStatus = async () => { if (!selectedStation) return; try { const response = await fetch(`${API_BASE}/stations`); if (!response.ok) return; const stations = await response.json(); const thisStation = stations.find(s => s.id === selectedStation.id); if (thisStation && connChip) { if (thisStation.status === 'Online') { connChip.innerHTML = ` Online`; connChip.className = 'cham_chip cham_chip-emerald'; } else { connChip.innerHTML = ` Offline`; connChip.className = 'cham_chip cham_chip-rose'; if (lastUpdateEl) lastUpdateEl.textContent = "Waiting for data..."; // Optionally reset the dashboard if the station goes offline // resetDashboardUI(); } } } catch (error) { console.error("Failed to fetch station status:", error); } }; const resetDashboardUI = () => { grid.querySelectorAll('.chamber-card').forEach(card => { card.querySelector('.bat-id-big').textContent = 'Waiting...'; card.querySelector('.soc').textContent = '—'; card.querySelector('.voltage').textContent = '—'; card.querySelector('.bat-temp').textContent = '—'; card.querySelector('.bat-fault').textContent = '—'; card.querySelector('.current').textContent = '—'; card.querySelector('.slot-temp').textContent = '—'; card.querySelector('.chg-fault').textContent = '—'; // Show the "empty" view by default when resetting card.querySelector('.filled-state').style.display = 'none'; card.querySelector('.empty-state').style.display = 'flex'; }); logToInstance("Station is offline. Clearing stale data.", "error"); }; // --- NEW: This function polls the API for the true station status --- // const checkStationStatus = async () => { // if (!selectedStation) return; // try { // const response = await fetch(`${API_BASE}/stations`); // if (!response.ok) return; // const stations = await response.json(); // const thisStation = stations.find(s => s.id === selectedStation.id); // if (thisStation && connChip) { // stationNameEl.textContent = thisStation.name; // stationLocationEl.textContent = thisStation.location; // if (thisStation.status === 'Online') { // connChip.innerHTML = ` Online`; // connChip.className = 'cham_chip cham_chip-emerald'; // } else { // connChip.innerHTML = ` Offline`; // connChip.className = 'cham_chip cham_chip-rose'; // lastUpdateEl.textContent = "Waiting for data..."; // resetDashboardUI(); // } // } // } catch (error) { // console.error("Failed to fetch station status:", error); // } // }; // --- MAIN LOGIC (Your original code is unchanged) --- const initializeDashboard = () => { try { selectedStation = JSON.parse(localStorage.getItem('selected_station')); if (!selectedStation || !selectedStation.id) { window.location.href = './index.html'; } stationNameEl.textContent = selectedStation.name || 'Unknown Station'; stationLocationEl.textContent = selectedStation.location || 'No location'; // This populates the span with id="device-id" with the Station's ID deviceIdEl.textContent = selectedStation.id; // This populates the span with id="product-id" with the Product ID // productIdEl.textContent = selectedStation.product_id; } catch (e) { document.body.innerHTML = `
${e.message} Go Back
`; return; } grid.innerHTML = ''; for (let i = 1; i <= 9; i++) { const node = chamberTmpl.content.cloneNode(true); const card = node.querySelector('.chamber-card'); card.dataset.chamberId = i; card.querySelector('.slotNo').textContent = i; grid.appendChild(node); } logToInstance(`Dashboard initialized for station: ${selectedStation.name} (${selectedStation.id})`); // --- EVENT LISTENERS --- // NEW: A single, global click listener for all button feedback document.addEventListener('click', (event) => { const button = event.target.closest('.btn'); if (button) { applyButtonFeedback(button); } }); // Chamber Card and Swap Selection document.querySelectorAll('.chamber-card').forEach(card => { // Listener for swap selection card.addEventListener('click', () => handleChamberClick(parseInt(card.dataset.chamberId, 10))); // Listeners for command buttons inside the card card.querySelectorAll('.btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const commandText = e.currentTarget.textContent.trim(); const command = commandText.replace(' ', '_'); const chamberNum = parseInt(e.currentTarget.closest('.chamber-card').dataset.chamberId, 10); if (command === 'OPEN') { if (confirm(`Are you sure you want to open door for Chamber ${chamberNum}?`)) { sendCommand(command, chamberNum); } else { logToInstance(`Open Door command for Chamber ${chamberNum} cancelled.`); } } else { sendCommand(command, chamberNum); } }); }); }); // Swap Panel Buttons if (clearSwapBtn) clearSwapBtn.addEventListener('click', clearSelection); if (startSwapBtn) { startSwapBtn.addEventListener('click', () => { if (swapPairs.length > 0) { const formattedPairs = swapPairs.map(p => `[${p[0]},${p[1]}]`).join(','); sendCommand('START_SWAP', formattedPairs); clearSelection(); } }); } if (abortSwapBtn) abortSwapBtn.addEventListener('click', () => sendCommand('ABORT_SWAP')); if (resetBtn) { resetBtn.addEventListener('click', () => { if (confirm('Are you sure you want to reset the station?')) { sendCommand('STATION_RESET'); } }); } // Header Buttons if (refreshBtn) refreshBtn.addEventListener('click', () => window.location.reload()); if (logoutBtn) { logoutBtn.addEventListener('click', () => { localStorage.clear(); window.location.href = './index.html'; }); } // Audio Command Button (assuming it exists in your HTML) const sendAudioBtn = document.getElementById('send-audio-btn'); if (sendAudioBtn) { sendAudioBtn.addEventListener('click', () => { const language = document.getElementById('audio-command-select').value; sendCommand('LANGUAGE_UPDATE', language); }); } updateSwapUI(); connectSocket(); checkStationStatus(); statusPollingInterval = setInterval(checkStationStatus, 10000); }; const connectSocket = () => { socket = io(SOCKET_URL); // --- CHANGED: No longer sets status to "Online" on its own --- socket.on('connect', () => { logToInstance("Successfully connected to the backend WebSocket."); socket.emit('join_station_room', { station_id: selectedStation.id }); }); // --- CHANGED: Sets status to "Offline" correctly on disconnect --- socket.on('disconnect', () => { logToInstance("Disconnected from the backend WebSocket.", "error"); connChip.innerHTML = ` Offline`; connChip.className = 'cham_chip cham_chip-rose'; }); socket.on('dashboard_update', (message) => { console.log("DEBUG: Received 'dashboard_update' message:", message); const { stationId, data } = message; console.log("Received data payload:", data); if (stationId !== selectedStation.id) { console.warn(`Ignoring message for wrong station. Expected ${selectedStation.id}, got ${stationId}`); return; } lastUpdateEl.textContent = `Last Recv ${new Date().toLocaleTimeString()}`; // 1. Check if the 'stationDiagnosticCode' key exists in the data. if (data.hasOwnProperty('stationDiagnosticCode')) { const sdc = data.stationDiagnosticCode; stationDiagCodeEl.textContent = sdc; updateDiagnosticsUI(sdc); } // 2. Check if the 'backupSupplyStatus' key exists. if (data.hasOwnProperty('backupSupplyStatus')) { if (data.backupSupplyStatus === 1) { backupPowerChip.textContent = 'On Backup'; backupPowerChip.className = 'cham_chip w-full justify-center mt-3 cham_chip-amber'; } else { backupPowerChip.textContent = 'On Mains Power'; backupPowerChip.className = 'cham_chip w-full justify-center mt-3 cham_chip-emerald'; } } // 3. Only process chamber-level data if it exists. if (data.slotLevelPayload && Array.isArray(data.slotLevelPayload)) { data.slotLevelPayload.forEach((slotData, index) => { const slotId = index + 1; const card = grid.querySelector(`.chamber-card[data-chamber-id="${slotId}"]`); if (card) { updateChamberUI(card, slotData); } }); } }); }; // --- SCRIPT EXECUTION --- initializeDashboard(); });