smoke-index/irrigation-controller.html
2026-07-18 08:10:38 +00:00

538 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Irrigation Duty Cycle Controller | Arthur Ibay</title>
<style>
:root {
--bg: #0a0a0a;
--surface: #141414;
--text: #e8e8e8;
--muted: #888;
--accent: #00d4aa;
--accent-dim: #007a60;
--danger: #ff4444;
--warning: #ffaa00;
--success: #00ff88;
--border: #2a2a2a;
--shadow: 0 8px 24px rgba(0,0,0,0.4);
--font-display: 'Segoe UI', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
}
* { box-sizing: border-box; }
body {
background: radial-gradient(circle at 50% -20%, #1a1a2e 0%, var(--bg) 60%);
color: var(--text);
font-family: var(--font-display);
line-height: 1.6;
margin: 0;
padding: 0;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
header {
text-align: center;
margin-bottom: 60px;
position: relative;
}
header::before {
content: '';
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
width: 300px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
h1 {
font-size: clamp(2.5em, 6vw, 4em);
margin: 0 0 20px 0;
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-dim) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
letter-spacing: -2px;
font-weight: 700;
}
.subtitle {
font-size: 1.2em;
color: var(--muted);
font-family: var(--font-mono);
}
.hero-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
margin: 60px 0;
align-items: stretch;
}
@media (max-width: 900px) {
.hero-grid { grid-template-columns: 1fr; }
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 32px;
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}
.panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent-dim), var(--accent), var(--accent-dim));
}
h2 {
font-size: 1.8em;
color: var(--accent);
margin-top: 0;
font-weight: 600;
}
h3 {
color: var(--accent);
font-size: 1.3em;
margin: 30px 0 15px 0;
}
p { color: var(--muted); }
.formula-block {
background: #000;
border-left: 4px solid var(--accent);
padding: 20px;
margin: 25px 0;
font-family: var(--font-mono);
font-size: 1.1em;
border-radius: 0 8px 8px 0;
}
.variable { color: var(--accent); }
.constant { color: var(--warning); }
input, select {
background: #000;
border: 1px solid var(--border);
color: var(--text);
padding: 12px 16px;
border-radius: 8px;
font-family: var(--font-mono);
font-size: 0.95em;
width: 100%;
margin: 8px 0;
transition: border-color 0.2s;
}
input:focus, select:focus {
outline: none;
border-color: var(--accent);
}
label {
display: block;
font-size: 0.85em;
color: var(--muted);
margin: 15px 0 5px 0;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 1px;
}
.input-group { margin-bottom: 20px; }
button.calculate {
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-dim) 100%);
color: #000;
border: none;
padding: 16px 32px;
font-size: 1.1em;
font-weight: 700;
border-radius: 12px;
cursor: pointer;
margin: 30px 0 20px 0;
width: 100%;
transition: transform 0.2s, box-shadow 0.2s;
font-family: var(--font-display);
}
button.calculate:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,212,170,0.3);
}
.result-card {
background: #000;
border: 1px solid var(--accent);
border-radius: 12px;
padding: 24px;
margin: 20px 0;
}
.result-label {
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.result-value {
font-size: 2.5em;
color: var(--accent);
font-weight: 700;
font-family: var(--font-mono);
line-height: 1;
}
.result-unit {
font-size: 1em;
color: var(--muted);
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
margin-top: 15px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-ok { background: rgba(0,255,136,0.15); border: 1px solid var(--success); color: var(--success); }
.status-warn { background: rgba(255,170,0,0.15); border: 1px solid var(--warning); color: var(--warning); }
.status-danger { background: rgba(255,68,68,0.15); border: 1px solid var(--danger); color: var(--danger); }
.image-container {
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
margin: 30px 0;
}
.image-container img {
width: 100%;
height: auto;
display: block;
}
.citation {
font-family: var(--font-mono);
font-size: 0.75em;
color: var(--muted);
margin-top: 15px;
padding-top: 15px;
border-top: 1px dashed var(--border);
}
.nav-pill {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 20px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 50px;
color: var(--accent);
text-decoration: none;
font-size: 0.9em;
margin: 20px 10px 20px 0;
transition: all 0.2s;
}
.nav-pill:hover {
border-color: var(--accent);
transform: translateY(-2px);
}
.data-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: #000;
border: 1px solid var(--border);
border-radius: 6px;
font-family: var(--font-mono);
font-size: 0.7em;
color: var(--muted);
margin-left: 10px;
}
.grid-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-family: var(--font-mono);
font-size: 0.85em;
}
.grid-table th, .grid-table td {
border: 1px solid var(--border);
padding: 12px;
text-align: left;
}
.grid-table th {
background: var(--surface);
color: var(--accent);
}
.grid-table tr:nth-child(even) {
background: rgba(0,0,0,0.2);
}
footer {
text-align: center;
padding: 60px 20px;
color: var(--muted);
font-size: 0.9em;
border-top: 1px solid var(--border);
margin-top: 60px;
}
.workshop-note {
background: linear-gradient(180deg, rgba(0,212,170,0.05) 0%, transparent 100%);
border: 1px dashed var(--accent-dim);
border-radius: 12px;
padding: 20px;
margin: 30px 0;
font-style: italic;
}
</style>
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
</head>
<body>
<div class="container">
<header>
<h1>IRRIGATION<br>DUTY CYCLE CONTROLLER</h1>
<p class="subtitle">Precision pump scheduling for closed-loop hydroponics & field crops</p>
<a href="index.html" class="nav-pill">← Return to Workshop</a>
<span class="data-badge">Q5187947 | FAO-56</span>
</header>
<div class="hero-grid">
<div class="panel">
<h2>THE THEORY</h2>
<p>In precision irrigation, the pump is not a switch. It is a metronome. Every cycle must deliver exactly the volume lost to evapotranspiration plus the maintenance reserve for root zone saturation.</p>
<div class="formula-block">
<span class="variable">V_required</span> = (<span class="variable">ET₀</span> × <span class="variable">Kc</span> × <span class="variable">A_root</span>) <span class="variable">SM_deficit</span><br><br>
<span class="variable">T_pump</span> = <span class="variable">V_required</span> ÷ <span class="variable">Q_pump</span> × 60
</div>
<p>Where:</p>
<ul style="color: var(--muted); margin-left: 20px;">
<li><span class="variable">ET₀</span> = Reference evapotranspiration (mm/day, from Penman-Monteith)</li>
<li><span class="variable">Kc</span> = Crop coefficient (dimensionless, stage-dependent)</li>
<li><span class="variable">A_root</span> = Root zone area (m²)</li>
<li><span class="variable">SM_deficit</span> = Current soil moisture deficit (L)</li>
<li><span class="variable">Q_pump</span> = Pump flow rate (L/min)</li>
</ul>
<div class="citation">
Grounded in Q5187947 (crop coefficient) • FAO Irrigation Paper No. 56 • Allen et al. 1998
</div>
<div class="workshop-note">
<p>Field Note: In my Wasatch trials, I discovered that lettuce at mid-canopy (Kc ≈ 1.15) demands 4.2 mm/day in July. A 12 L/min pump servicing 50 m² must run exactly 11 minutes per 24 hours. Anything less stresses the roots. Anything more leaches nutrients.</p>
</div>
</div>
<div class="panel">
<h2>THE CONTROLLER</h2>
<div class="input-group">
<label>Reference Evapotranspiration (ET₀)</label>
<input type="number" id="eto" placeholder="e.g., 5.2" step="0.1">
<small style="color: var(--muted);">mm/day • From ET Matrix or local station</small>
</div>
<div class="input-group">
<label>Crop Coefficient (Kc)</label>
<select id="kc">
<option value="">Select crop...</option>
<optgroup label="Leafy Greens">
<option value="0.95">Lettuce (early)</option>
<option value="1.15">Lettuce (mid-canopy)</option>
<option value="1.05">Spinach</option>
<option value="0.90">Arugula</option>
</optgroup>
<optgroup label="Fruiting">
<option value="1.10">Tomato (established)</option>
<option value="1.15">Tomato (fruiting)</option>
<option value="1.05">Pepper</option>
<option value="1.20">Cucumber</option>
</optgroup>
<optgroup label="Grains">
<option value="0.40">Wheat (initial)</option>
<option value="1.05">Wheat (mid-season)</option>
<option value="0.50">Corn (early)</option>
<option value="1.15">Corn (peak)</option>
</optgroup>
</select>
</div>
<div class="input-group">
<label>Root Zone Area</label>
<input type="number" id="area" placeholder="e.g., 50" step="1">
<small style="color: var(--muted);">square meters</small>
</div>
<div class="input-group">
<label>Current Soil Moisture Deficit</label>
<input type="number" id="smdeficit" placeholder="e.g., 15" step="1">
<small style="color: var(--muted);">liters remaining from last irrigation</small>
</div>
<div class="input-group">
<label>Pump Flow Rate</label>
<input type="number" id="flowrate" placeholder="e.g., 12" step="0.1">
<small style="color: var(--muted);">liters per minute</small>
</div>
<button class="calculate" onclick="calculate()">COMPUTE DUTY CYCLE</button>
<div id="results" style="display:none;">
<div class="result-card">
<div class="result-label">Daily Water Demand</div>
<span class="result-value" id="volResult">0</span>
<span class="result-unit">liters</span>
</div>
<div class="result-card">
<div class="result-label">Pump Runtime Per Day</div>
<span class="result-value" id="timeResult">0</span>
<span class="result-unit">minutes</span>
</div>
<div class="result-card">
<div class="result-label">Recommended Interval</div>
<span class="result-value" id="intervalResult">0</span>
<span class="result-unit">hours between pulses</span>
</div>
<div id="statusBox"></div>
</div>
</div>
</div>
<div class="panel">
<h2>CROP COEFFICIENT REFERENCE TABLE</h2>
<p>Values sourced from FAO-56, adapted for controlled-environment agriculture.</p>
<table class="grid-table">
<thead>
<tr>
<th>Crop</th>
<th>Initial Stage</th>
<th>Mid-Season</th>
<th>Late Season</th>
<th>Peak Demand</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lettuce</td>
<td>0.95</td>
<td>1.15</td>
<td>1.00</td>
<td>4.2 mm/d</td>
</tr>
<tr>
<td>Tomato</td>
<td>0.65</td>
<td>1.10</td>
<td>1.15</td>
<td>6.8 mm/d</td>
</tr>
<tr>
<td>Pepper</td>
<td>0.60</td>
<td>1.05</td>
<td>1.00</td>
<td>5.4 mm/d</td>
</tr>
<tr>
<td>Corn</td>
<td>0.50</td>
<td>1.15</td>
<td>0.85</td>
<td>7.2 mm/d</td>
</tr>
<tr>
<td>Wheat</td>
<td>0.40</td>
<td>1.05</td>
<td>0.45</td>
<td>4.8 mm/d</td>
</tr>
</tbody>
</table>
<div class="citation">
Machine-readable export: <a href="irrigation-controller.json" style="color: var(--accent);">irrigation-controller.json</a>
</div>
</div>
<div class="image-container">
<img src="https://images.pexels.com/photos/29396001/pexels-photo-29396001.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
alt="Young pepper plant with precision drip irrigation emitter delivering measured flow to root zone">
</div>
<footer>
<p>© 2026 Arthur Ibay | Salt Lake City</p>
<p style="margin-top: 10px; font-family: var(--font-mono); font-size: 0.8em;">
Built from field data. Tested on Wasatch slope. Ready for orbit.
</p>
</footer>
</div>
<script>
function calculate() {
const eto = parseFloat(document.getElementById('eto').value) || 0;
const kc = parseFloat(document.getElementById('kc').value) || 0;
const area = parseFloat(document.getElementById('area').value) || 0;
const smDeficit = parseFloat(document.getElementById('smdeficit').value) || 0;
const flowRate = parseFloat(document.getElementById('flowrate').value) || 0;
// Convert ET₀ (mm/day) to liters: 1 mm over 1 m² = 1 liter
const dailyDemandMM = eto * kc;
const dailyDemandLiters = dailyDemandMM * area;
const netRequired = Math.max(0, dailyDemandLiters - smDeficit);
// Pump runtime in minutes
const pumpMinutes = flowRate > 0 ? (netRequired / flowRate) : 0;
// Optimal interval: split total runtime into 4 pulses/day (every 6 hours)
const intervalHours = pumpMinutes > 0 ? 24 / 4 : 0;
const pulseDuration = pumpMinutes / 4;
document.getElementById('volResult').textContent = netRequired.toFixed(1);
document.getElementById('timeResult').textContent = pumpMinutes.toFixed(1);
document.getElementById('intervalResult').textContent = `${pulseDuration.toFixed(1)} min`;
const resultsDiv = document.getElementById('results');
const statusBox = document.getElementById('statusBox');
resultsDiv.style.display = 'block';
// Status logic
let statusClass, statusText, dotColor;
if (pumpMinutes === 0) {
statusClass = 'status-ok';
statusText = 'NO DEMAND — System idle';
dotColor = '#00ff88';
} else if (pumpMinutes <= 30) {
statusClass = 'status-ok';
statusText = 'OPTIMAL — Pulse schedule viable';
dotColor = '#00ff88';
} else if (pumpMinutes <= 90) {
statusClass = 'status-warn';
statusText = 'HIGH LOAD — Consider additional pumps or expanded reservoir';
dotColor = '#ffaa00';
} else {
statusClass = 'status-danger';
statusText = 'CRITICAL — Exceeds safe duty cycle. Scale infrastructure.';
dotColor = '#ff4444';
}
statusBox.innerHTML = `<div class="status-indicator ${statusClass}"><div class="status-dot" style="background:${dotColor}"></div>${statusText}</div>`;
}
</script>
</body>
</html>