Files
mht-siemens/solution/client/DemoPage.html
T

541 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PlcVanguard | Industrial Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; background-color: #f3f4f6; }
.warning-bg { background-color: #fef08a; } /* yellow-200 */
.error-bg { background-color: #fee2e2; } /* red-200 */
</style>
</head>
<body class="text-gray-800">
<!-- Navbar -->
<nav class="bg-slate-900 text-white p-4 shadow-lg">
<div class="container mx-auto flex justify-between items-center">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center font-bold text-white">P</div>
<span class="text-xl font-bold tracking-tight">PlcVanguard</span>
</div>
<div class="flex items-center space-x-4">
<span class="text-sm text-green-400 flex items-center">
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
Connected: 192.168.0.102 (S7-1500)
</span>
<button class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-semibold transition">Settings</button>
</div>
</div>
</nav>
<main class="container mx-auto py-8 px-4">
<header class="mb-8">
<h1 id="mainTitle" class="text-3xl font-bold mb-2">Live Monitoring Dashboard</h1>
<p id="mainSubtitle" class="text-gray-600">Real-time polling of DB100 and event-triggered acquisition from DB101.</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Status Cards -->
<div class="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div class="flex justify-between items-center mb-4">
<h2 id="chartTitle" class="font-semibold text-lg">Live Acquisition (5Hz)</h2>
<div id="timerDisplay" class="text-blue-600 font-mono font-bold">Time Remaining: 30s</div>
</div>
<div class="h-[450px] relative">
<canvas id="liveChart"></canvas>
</div>
</div>
<!-- Simulation Controls & Status -->
<div class="space-y-6">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<h2 class="font-semibold text-lg mb-4">Control Panel</h2>
<div class="space-y-4">
<p class="text-sm text-gray-500">Target Mean: 210mm<br>Control Limits: ±2mm (Warning), ±3mm (Error)</p>
<button id="startBtn" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg transition shadow-md">
Start Acquisition
</button>
<div id="progressContainer" class="hidden">
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="progressBar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
<p class="text-xs text-gray-400 mt-2">Data collection in progress...</p>
</div>
</div>
</div>
<div class="bg-blue-900 text-white p-6 rounded-xl shadow-lg">
<h2 class="font-semibold text-lg mb-2">System Overview</h2>
<ul class="text-sm space-y-2 opacity-90">
<li>• PLC: Siemens S7-1500</li>
<li>• Area: DB100 (Continuous)</li>
<li>• Mode: 5Hz Polling</li>
<li>• Target: File System (JSON)</li>
</ul>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- LEFT COLUMN: Session Selection (History) -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<h2 id="historyHeader" class="font-semibold text-lg mb-4">Recording Sessions</h2>
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead class="bg-gray-50">
<tr class="border-b">
<th class="p-2">ID</th>
<th class="p-2">Timestamp</th>
<th class="p-2">Status</th>
<th class="p-2">Action</th>
</tr>
</thead>
<tbody id="historyTableBody">
<!-- Sessions injected by JS -->
</tbody>
</table>
</div>
<div class="flex justify-between items-center mt-4">
<button id="prevPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Prev</button>
<span id="pageInfo" class="text-xs">Page 1</span>
<button id="nextPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Next</button>
</div>
</div>
<!-- RIGHT COLUMN: Dynamic Detail View -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<h2 id="detailTitle" class="font-semibold text-lg mb-4">Detail View</h2>
<div id="detailPlaceholder" class="text-gray-400 italic text-center py-20">
Click a row in the history table to view all captured data points.
</div>
<!-- Live Data View (Only during acquisition) -->
<div id="liveDataContainer" class="hidden space-y-4">
<h3 class="text-[10px] uppercase font-bold text-gray-400">Live Session Stream</h3>
<div class="overflow-x-auto">
<table class="w-full text-left text-[10px]">
<thead class="bg-gray-50">
<tr class="border-b">
<th class="p-2">Index</th>
<th class="p-2">Timestamp</th>
<th class="p-2">Value (mm)</th>
<th class="p-2">Status</th>
</tr>
</thead>
<tbody id="fullDataTableBody">
<!-- Data points injected by JS -->
</tbody>
</table>
</div>
<div class="flex justify-between items-center mt-4">
<button id="fullPrevPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Prev</button>
<span id="fullPageInfo" class="text-xs">Page 1</span>
<button id="fullNextPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Next</button>
</div>
</div>
<!-- History Detail View (Only for loaded sessions) -->
<div id="historyDetailContainer" class="hidden space-y-4">
<h3 class="text-[10px] uppercase font-bold text-gray-400">Session Summary</h3>
<div class="p-3 bg-gray-50 rounded-lg border border-gray-100">
<table class="w-full text-left text-[10px]">
<thead class="bg-gray-50">
<tr class="border-b">
<th class="p-3">Timestamp</th>
<th class="p-3">Value (mm)</th>
<th class="p-3">Status</th>
</tr>
</thead>
<tbody id="measurementsTable">
<!-- Last 3 rows injected by JS -->
</tbody>
</table>
</div>
<!-- FIX: Added missing detailContent container -->
<div id="detailContent" class="hidden mt-4 p-4 bg-blue-50 border border-blue-200 rounded">
<div id="selectedData" class="text-xs font-mono"></div>
</div>
</div>
</div>
</div>
</main>
<script>
// Configuration
const MEAN = 210;
const WARNING_LIMIT = 2;
const ERROR_LIMIT = 3;
const SAMPLE_RATE = 5;
const TOTAL_TIME = 30;
const TOTAL_SAMPLES = TOTAL_TIME * SAMPLE_RATE;
const TARGET_VALUE = 210;
const ACQUISITION_LEN = 500;
const itemsPerPage = 10;
// State
let dataPoints = [];
let chart;
let timerInterval;
let isAcquiring = false;
let currentMode = 'live';
let activeSessionData = [];
let currentPage = 1;
let fullDataPage = 1;
// Mock Recording Sessions
const mockSessions = [
{ id: 'REC-001', ts: new Date(Date.now() - 3600000).toISOString(), status: 'Success', data: Array.from({length: 100}, (_, i) => ({
ts: new Date(Date.now() - (3600000 - (i * 1000))),
val: generateNormal(210, 1.5),
x: (i / (100 - 1)) * ACQUISITION_LEN
})) },
{ id: 'REC-002', ts: new Date(Date.now() - 7200000).toISOString(), status: 'Success', data: Array.from({length: 100}, (_, i) => ({
ts: new Date(Date.now() - (7200000 - (i * 1000))),
val: generateNormal(212, 2.0),
x: (i / (100 - 1)) * ACQUISITION_LEN
})) },
{ id: 'REC-003', ts: new Date(Date.now() - 10800000).toISOString(), status: 'Completed', data: Array.from({length: 100}, (_, i) => ({
ts: new Date(Date.now() - (10800000 - (i * 1000))),
val: generateNormal(208, 0.5),
x: (i / (100 - 1)) * ACQUISITION_LEN
})) }
];
// DOM Elements
const startBtn = document.getElementById('startBtn');
const progressBar = document.getElementById('progressBar');
const progressContainer = document.getElementById('progressContainer');
const timerDisplay = document.getElementById('timerDisplay');
const tableBody = document.getElementById('measurementsTable');
const detailPlaceholder = document.getElementById('detailPlaceholder');
const detailContent = document.getElementById('detailContent');
const fullDataTableBody = document.getElementById('fullDataTableBody');
const historyTableBody = document.getElementById('historyTableBody');
const mainTitle = document.getElementById('mainTitle');
const mainSubtitle = document.getElementById('mainSubtitle');
const chartTitle = document.getElementById('chartTitle');
const pageInfo = document.getElementById('pageInfo');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
const fullPrevBtn = document.getElementById('fullPrevPage');
const fullNextBtn = document.getElementById('fullNextPage');
const liveDataContainer = document.getElementById('liveDataContainer');
const historyDetailContainer = document.getElementById('historyDetailContainer');
const selectedData = document.getElementById('selectedData');
// Initialize Chart
const ctx = document.getElementById('liveChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [
{ label: 'Value (mm)', data: [], borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, pointRadius: 0, borderWidth: 2, tension: 0 },
{ label: 'Warning UCL', data: [], borderColor: '#facc15', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
{ label: 'Warning LCL', data: [], borderColor: '#facc15', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
{ label: 'Error UCL', data: [], borderColor: '#f87171', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
{ label: 'Error LCL', data: [], borderColor: '#f87171', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
{ label: 'Target Value', data: [], borderColor: '#22c55e', borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { type: 'linear', min: 0, max: ACQUISITION_LEN, title: { display: true, text: 'Position (mm)' } },
y: { min: 200, max: 220 }
},
animation: false
}
});
function generateNormal(mean, stdDev) {
const u1 = Math.random();
const u2 = Math.random();
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
return z0 * stdDev + mean;
}
function updateTable() {
tableBody.innerHTML = '';
const lastData = dataPoints.slice(-3).reverse();
lastData.forEach((point) => {
const diff = Math.abs(point.val - MEAN);
let rowClass = 'hover:bg-blue-50 cursor-pointer transition';
let status = 'OK';
let colorClass = 'text-green-600 border-green-600';
if (diff > ERROR_LIMIT) {
rowClass += ' error-bg';
status = 'ERROR';
colorClass = 'text-red-600 border-red-600';
} else if (diff > WARNING_LIMIT) {
rowClass += ' warning-bg';
status = 'WARNING';
colorClass = 'text-yellow-600 border-yellow-600';
}
const row = document.createElement('tr');
row.innerHTML = `
<td class="p-2 text-[10px] text-gray-500">${point.ts.toLocaleTimeString()}</td>
<td class="p-2 font-mono font-bold">${point.val.toFixed(2)}</td>
<td class="p-2">
<span class="px-1 py-0.5 bg-white rounded-full text-[9px] font-bold border ${colorClass}">${status}</span>
</td>
`;
tableBody.appendChild(row);
});
}
function toggleDetailVisibility() {
detailPlaceholder.classList.add('hidden');
detailContent.classList.add('hidden');
if (isAcquiring || currentMode === 'history') {
liveDataContainer.classList.remove('hidden');
historyDetailContainer.classList.add('hidden');
if (isAcquiring) {
activeSessionData = [...dataPoints];
renderFullSessionTable();
} else {
// For history, activeSessionData is already sorted descending in loadSession
renderFullSessionTable();
}
} else {
liveDataContainer.classList.add('hidden');
historyDetailContainer.classList.add('hidden');
detailPlaceholder.classList.remove('hidden');
}
}
startBtn.addEventListener('click', () => {
if (isAcquiring) return;
dataPoints = [];
activeSessionData = [];
chart.data.labels = [];
chart.data.datasets.forEach(ds => ds.data = []);
chart.update();
isAcquiring = true;
startBtn.disabled = true;
startBtn.classList.replace('bg-green-600', 'bg-gray-400');
progressContainer.classList.remove('hidden');
currentMode = 'live';
toggleDetailVisibility();
let currentStep = 0;
timerInterval = setInterval(() => {
const point = {
ts: new Date(),
val: generateNormal(MEAN, 1),
x: (currentStep / (TOTAL_SAMPLES - 1)) * ACQUISITION_LEN
};
dataPoints.push(point);
activeSessionData = [...dataPoints];
chart.data.datasets[0].data.push({x: point.x, y: point.val});
chart.data.datasets[1].data.push({x: point.x, y: MEAN + WARNING_LIMIT});
chart.data.datasets[2].data.push({x: point.x, y: MEAN - WARNING_LIMIT});
chart.data.datasets[3].data.push({x: point.x, y: MEAN + ERROR_LIMIT});
chart.data.datasets[4].data.push({x: point.x, y: MEAN - ERROR_LIMIT});
chart.data.datasets[5].data.push({x: point.x, y: TARGET_VALUE});
if (chart.data.labels.length > 100) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.update();
if (dataPoints.length % 3 === 0) {
updateTable();
}
currentStep++;
const progress = (currentStep / TOTAL_SAMPLES) * 100;
progressBar.style.width = `${progress}%`;
timerDisplay.innerText = `Time Remaining: ${Math.max(0, Math.ceil((TOTAL_SAMPLES - currentStep) / SAMPLE_RATE))}s`;
if (currentStep >= TOTAL_SAMPLES) {
clearInterval(timerInterval);
completeAcquisition();
}
}, 1000 / SAMPLE_RATE);
});
function completeAcquisition() {
isAcquiring = false;
startBtn.disabled = false;
startBtn.classList.replace('bg-gray-400', 'bg-green-600');
timerDisplay.innerText = "Acquisition Complete";
timerDisplay.classList.replace('text-blue-600', 'text-green-600');
// Sort data descending by timestamp for the Detail View (Newest First)
activeSessionData.sort((a, b) => b.ts - a.ts);
// Ensure the live data container (table) is visible
liveDataContainer.classList.remove('hidden');
historyDetailContainer.classList.add('hidden');
renderFullSessionTable();
const highResData = Array.from({length: 4096}, () => generateNormal(MEAN, 1));
detailPlaceholder.classList.add('hidden');
detailContent.classList.remove('hidden');
selectedData.innerText = `High Resolution Data (First 5):\n\n[Success: 4096 points captured]\nMean: ${highResData.reduce((a,b)=>a+b,0)/4096}\nMax: ${Math.max(...highResData)}\nMin: ${Math.min(...highResData)}\n\nRaw Array (Sample): ${highResData.slice(0, 5).join(', ')}...`;
}
window.loadSession = (id) => {
currentMode = 'history';
mainTitle.innerText = "Recording History View";
mainSubtitle.innerText = "Visualizing historical data from recorded PLC sessions.";
chartTitle.innerText = `Session: ${id}`;
const session = mockSessions.find(s => s.id === id);
if (session) {
// Sort descending for the table/detail view (Newest First)
activeSessionData = [...session.data].sort((a, b) => b.ts - a.ts);
// Sort ascending for the chart (Oldest -> Newest left to right)
dataPoints = [...session.data].sort((a, b) => a.ts - b.ts);
} else {
activeSessionData = [];
dataPoints = [];
}
updateTable();
chart.data.labels = dataPoints.map((_, i) => (i / (dataPoints.length - 1)) * ACQUISITION_LEN);
chart.data.datasets[0].data = dataPoints.map(p => p.val);
chart.data.datasets[1].data = Array(dataPoints.length).fill(MEAN + WARNING_LIMIT);
chart.data.datasets[2].data = Array(dataPoints.length).fill(MEAN - WARNING_LIMIT);
chart.data.datasets[3].data = Array(dataPoints.length).fill(MEAN + ERROR_LIMIT);
chart.data.datasets[4].data = Array(dataPoints.length).fill(MEAN - ERROR_LIMIT);
chart.data.datasets[5].data = Array(dataPoints.length).fill(TARGET_VALUE);
chart.update();
fullDataPage = 1;
renderFullSessionTable();
toggleDetailVisibility();
};
function renderFullSessionTable() {
fullDataTableBody.innerHTML = '';
const start = (fullDataPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageData = activeSessionData.slice(start, end);
pageData.forEach((p, i) => {
const row = document.createElement('tr');
const diff = Math.abs(p.val - MEAN);
let status = 'OK';
let colorClass = 'text-green-600 border-green-600';
if (diff > ERROR_LIMIT) {
status = 'ERROR';
colorClass = 'text-red-600 border-red-600';
} else if (diff > WARNING_LIMIT) {
status = 'WARNING';
colorClass = 'text-yellow-600 border-yellow-600';
}
row.innerHTML = `
<td class="p-2 border-b">${start + i + 1}</td>
<td class="p-2 border-b">${p.ts.toLocaleTimeString()}</td>
<td class="p-2 border-b font-mono">${p.val.toFixed(2)}</td>
<td class="p-2 border-b">
<span class="px-1 py-0.5 bg-white rounded-full text-[9px] font-bold border ${colorClass}">${status}</span>
</td>
`;
fullDataTableBody.appendChild(row);
});
fullPageInfo.innerText = `Page ${fullDataPage} of ${Math.ceil(activeSessionData.length / itemsPerPage)}`;
fullPrevBtn.disabled = fullDataPage === 1;
fullNextBtn.disabled = fullDataPage * itemsPerPage >= activeSessionData.length;
}
// Event Listeners for Pagination
fullPrevBtn.onclick = () => {
if (fullDataPage > 1) {
fullDataPage--;
renderFullSessionTable();
}
};
fullNextBtn.onclick = () => {
if (fullDataPage * itemsPerPage < activeSessionData.length) {
fullDataPage++;
renderFullSessionTable();
}
};
prevBtn.onclick = () => {
if (currentPage > 1) {
currentPage--;
renderHistoryTable();
}
};
nextBtn.onclick = () => {
if (currentPage * itemsPerPage < mockSessions.length) {
currentPage++;
renderHistoryTable();
}
};
function renderHistoryTable() {
historyTableBody.innerHTML = '';
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageData = mockSessions.slice(start, end);
pageData.forEach(s => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="p-2 border-b">${s.id}</td>
<td class="p-2 border-b">${s.ts}</td>
<td class="p-2 border-b">${s.status}</td>
<td class="p-2 border-b">
<button class="text-blue-600 hover:underline" onclick="loadSession('${s.id}')">Reload</button>
</td>
`;
historyTableBody.appendChild(row);
});
pageInfo.innerText = `Page ${currentPage} of ${Math.ceil(mockSessions.length / itemsPerPage)}`;
}
// Initial Render
window.onload = () => {
const sessions = [
{ id: 'REC-001', ts: new Date(Date.now() - 3600000).toISOString(), status: 'Success' },
{ id: 'REC-002', ts: new Date(Date.now() - 7200000).toISOString(), status: 'Success' },
{ id: 'REC-003', ts: new Date(Date.now() - 10800000).toISOString(), status: 'Completed' }
];
const tableBody = document.getElementById('historyTableBody');
sessions.forEach(s => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="p-2 border-b">${s.id}</td>
<td class="p-2 border-b">${s.ts}</td>
<td class="p-2 border-b">${s.status}</td>
<td class="p-2 border-b">
<button class="text-blue-600 hover:underline" onclick="loadSession('${s.id}')">Reload</button>
</td>
`;
tableBody.appendChild(row);
});
renderHistoryTable();
};
</script>
</body>
</html>