| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>Live Irrigation Status | Plant Disease Detector</title>
|
| <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
| <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
|
| <link rel="preconnect" href="https://fonts.googleapis.com">
|
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
| <style>
|
|
|
| :root {
|
| --deep-green: #1a5d3a;
|
| --accent-green: #198754;
|
| --darker-accent: #143d2e;
|
| --bg-color: #f8f9fa;
|
| --surface: #ffffff;
|
| --text-dark: #212529;
|
| --text-muted: #6c757d;
|
| --border-color: #dee2e6;
|
| }
|
|
|
|
|
| body {
|
| font-family: 'Outfit', sans-serif;
|
| background-color: var(--bg-color);
|
| color: var(--text-dark);
|
| font-weight: 400;
|
| }
|
|
|
| h1, h2, h3, h4, h5, h6 {
|
| font-weight: 600;
|
| }
|
|
|
|
|
| .page-header {
|
| background-color: var(--deep-green);
|
| color: white;
|
| padding: 2rem 0;
|
| margin-bottom: 2rem;
|
| }
|
|
|
| .page-header h1 {
|
| font-weight: 700;
|
| font-size: 2rem;
|
| margin: 0;
|
| }
|
|
|
|
|
| .diagnostic-timeline {
|
| display: flex;
|
| justify-content: center;
|
| align-items: center;
|
| gap: 1.5rem;
|
| margin: 2rem 0;
|
| position: relative;
|
| }
|
|
|
| .timeline-step {
|
| display: flex;
|
| flex-direction: column;
|
| align-items: center;
|
| gap: 0.5rem;
|
| position: relative;
|
| z-index: 2;
|
| }
|
|
|
| .timeline-icon {
|
| width: 50px;
|
| height: 50px;
|
| border-radius: 50%;
|
| background-color: var(--accent-green);
|
| color: white;
|
| display: flex;
|
| align-items: center;
|
| justify-content: center;
|
| font-size: 1.5rem;
|
| box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3);
|
| }
|
|
|
| .timeline-label {
|
| font-weight: 500;
|
| font-size: 0.85rem;
|
| color: var(--text-dark);
|
| }
|
|
|
| .timeline-connector {
|
| flex: 1;
|
| height: 2px;
|
| background-color: var(--accent-green);
|
| margin: 0 -0.5rem;
|
| position: relative;
|
| top: -15px;
|
| }
|
|
|
|
|
| .card {
|
| background: var(--surface);
|
| border-radius: 20px;
|
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
|
| border: none;
|
| margin-bottom: 1.5rem;
|
| }
|
|
|
| .card-header {
|
| background-color: var(--deep-green);
|
| color: white;
|
| font-weight: 600;
|
| font-size: 1.25rem;
|
| padding: 1rem 1.5rem;
|
| border-radius: 20px 20px 0 0;
|
| text-align: center;
|
| }
|
|
|
| .card-body {
|
| padding: 2rem;
|
| }
|
|
|
|
|
| .status-card {
|
| text-align: center;
|
| }
|
|
|
| .status-label {
|
| font-size: 1rem;
|
| color: var(--text-muted);
|
| font-weight: 500;
|
| margin-bottom: 0.5rem;
|
| }
|
|
|
| .status-text {
|
| font-size: 1.75rem;
|
| font-weight: 600;
|
| padding: 0.75rem 1.5rem;
|
| border-radius: 12px;
|
| display: inline-block;
|
| transition: all 0.3s ease;
|
| }
|
|
|
| .status-text.on {
|
| color: var(--accent-green);
|
| background-color: #e9f5e9;
|
| border: 2px solid var(--accent-green);
|
| }
|
|
|
| .status-text.off {
|
| color: #dc3545;
|
| background-color: #f8d7da;
|
| border: 2px solid #dc3545;
|
| }
|
|
|
|
|
| .chart-container {
|
| width: 100%;
|
| height: 400px;
|
| }
|
|
|
|
|
| @media (max-width: 768px) {
|
| .page-header h1 {
|
| font-size: 1.5rem;
|
| }
|
|
|
| .diagnostic-timeline {
|
| gap: 1rem;
|
| }
|
|
|
| .timeline-connector {
|
| display: none;
|
| }
|
|
|
| .card-body {
|
| padding: 1.5rem;
|
| }
|
| }
|
| </style>
|
| </head>
|
| <body>
|
|
|
| <div class="page-header">
|
| <div class="container">
|
| <h1><i class="bi bi-droplet-fill"></i> Live Irrigation Status</h1>
|
| </div>
|
| </div>
|
|
|
| <div class="container">
|
|
|
| <div class="diagnostic-timeline">
|
| <div class="timeline-step">
|
| <div class="timeline-icon">
|
| <i class="bi bi-search"></i>
|
| </div>
|
| <div class="timeline-label">Monitor</div>
|
| </div>
|
| <div class="timeline-connector"></div>
|
| <div class="timeline-step">
|
| <div class="timeline-icon">
|
| <i class="bi bi-graph-up"></i>
|
| </div>
|
| <div class="timeline-label">Analyze</div>
|
| </div>
|
| <div class="timeline-connector"></div>
|
| <div class="timeline-step">
|
| <div class="timeline-icon">
|
| <i class="bi bi-gear-fill"></i>
|
| </div>
|
| <div class="timeline-label">Control</div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="card">
|
| <div class="card-header">Real-Time Irrigation Dashboard</div>
|
| <div class="card-body">
|
| <div class="row text-center mb-4">
|
| <div class="col-md-6">
|
| <div class="status-label">Current Pump Status</div>
|
| <div id="pumpStatus" class="status-text off">Loading...</div>
|
| </div>
|
| <div class="col-md-6 mt-3 mt-md-0">
|
| <div class="status-label">Time Elapsed</div>
|
| <div id="time-counter" class="status-text" style="color: #5a2d0c; background-color: #f0e6e0; border-color: #5a2d0c;">0 seconds</div>
|
| </div>
|
| </div>
|
|
|
| <div class="row">
|
| <div class="col-lg-6 mb-4">
|
| <div class="card h-100">
|
| <div class="card-body">
|
| <div id="gauge" class="chart-container"></div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="col-lg-6 mb-4">
|
| <div class="card h-100">
|
| <div class="card-body">
|
| <div id="graph1" class="chart-container"></div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="col-lg-6 mb-4">
|
| <div class="card h-100">
|
| <div class="card-body">
|
| <div id="graph2" class="chart-container"></div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="col-lg-6 mb-4">
|
| <div class="card h-100">
|
| <div class="card-body">
|
| <div id="graph3" class="chart-container"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <script>
|
| let alertSound;
|
|
|
| function initializeAudio() {
|
| if (!alertSound) {
|
| alertSound = new Audio('{{ url_for("static", filename="alarn_tune.mp3") }}');
|
| console.log("Audio initialized.");
|
| }
|
| }
|
| document.body.addEventListener('click', initializeAudio, { once: true });
|
|
|
| function fetchPumpStatus() {
|
| fetch('/update_pump_status')
|
| .then(response => response.json())
|
| .then(data => {
|
| const statusElement = document.getElementById('pumpStatus');
|
| const newStatus = data.pump_status;
|
| const oldStatus = statusElement.innerText;
|
|
|
| statusElement.innerText = newStatus;
|
| statusElement.className = 'status-text ' + (newStatus === 'On' ? 'on' : 'off');
|
|
|
| if (newStatus === 'Off' && oldStatus === 'On' && alertSound) {
|
| alertSound.play().catch(e => console.error("Audio play failed:", e));
|
| }
|
| });
|
| }
|
|
|
| function fetchGraphData() {
|
| fetch('/update_graph')
|
| .then(response => response.json())
|
| .then(data => {
|
| if (data.length === 0) return;
|
|
|
| const time = data.map((_, i) => i * 2);
|
| const soilMoisture = data.map(entry => entry[0]);
|
| const pumpStatus = data.map(entry => entry[1]);
|
| const currentSoilMoisture = soilMoisture[soilMoisture.length - 1];
|
|
|
|
|
| const responsiveLayout = { margin: { t: 40, b: 50, l: 50, r: 20 }, autosize: true };
|
|
|
| Plotly.react('gauge', getGaugeData(currentSoilMoisture), { ...responsiveLayout, title: 'Soil Moisture' });
|
| Plotly.react('graph1', getPumpStatusData(time, pumpStatus), { ...responsiveLayout, title: 'Pump Status vs. Time', yaxis: { tickvals: [-1, 1], ticktext: ['Off', 'On'], range: [-1.5, 1.5] }});
|
| Plotly.react('graph2', getSoilMoistureData(time, soilMoisture), { ...responsiveLayout, title: 'Soil Moisture vs. Time' });
|
| Plotly.react('graph3', getMoistureVsStatusData(soilMoisture, pumpStatus), { ...responsiveLayout, title: 'Pump Status vs. Soil Moisture', yaxis: { tickvals: [-1, 1], ticktext: ['Off', 'On'], range: [-1.5, 1.5] }});
|
| });
|
| }
|
|
|
|
|
| const getGaugeData = value => [{ type: "indicator", mode: "gauge+number", value: value, gauge: { axis: { range: [0, 100] }, steps: [{ range: [0, 30], color: "#ea4335" },{ range: [30, 60], color: "#fbbc05" },{ range: [60, 100], color: "#34a853" }]}}];
|
| const getPumpStatusData = (x, y) => [{ x, y, mode: 'lines+markers', type: 'scatter', line: { color: '#4285f4' } }];
|
| const getSoilMoistureData = (x, y) => [{ x, y, mode: 'lines+markers', type: 'scatter', line: { color: '#34a853' } }];
|
| const getMoistureVsStatusData = (x, y) => [{ x, y, mode: 'lines', type: 'scatter', line: { color: '#ea4335' } }];
|
|
|
| let timeCounter = 0;
|
| setInterval(() => { document.getElementById('time-counter').innerText = `${++timeCounter} seconds`; }, 1000);
|
| setInterval(fetchPumpStatus, 2000);
|
| setInterval(fetchGraphData, 2000);
|
| </script>
|
| </body>
|
| </html> |