Add pot/thermometer visualization to browser GUI
- Add volumen field to all sud JSON pot entries (30 L) - Fix parseSudDoc to read from nested doc.pot (was using stale flat keys) - SVG widget shows transparent rectangular pot with stirrer, thermometer with live temperature, and water fill level (water_mass/volumen) when a sud is loaded; no water shown when no sud is loaded Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DGKe8JzTM2Gr1fdx9wYXTi
This commit is contained in:
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
"grain_mass": 5.21,
|
"grain_mass": 5.21,
|
||||||
"water_mass": 22
|
"water_mass": 22,
|
||||||
|
"volumen": 30
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
"grain_mass": 5.21,
|
"grain_mass": 5.21,
|
||||||
"water_mass": 22
|
"water_mass": 22,
|
||||||
|
"volumen": 30
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
"grain_mass": 0,
|
"grain_mass": 0,
|
||||||
"water_mass": 22
|
"water_mass": 22,
|
||||||
|
"volumen": 30
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
"grain_mass": 0,
|
"grain_mass": 0,
|
||||||
"water_mass": 22
|
"water_mass": 22,
|
||||||
|
"volumen": 30
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
"grain_mass": 0,
|
"grain_mass": 0,
|
||||||
"water_mass": 10
|
"water_mass": 10,
|
||||||
|
"volumen": 30
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
|
|||||||
+60
-3
@@ -43,6 +43,7 @@ let sudName = '';
|
|||||||
let sudPotMass = 0;
|
let sudPotMass = 0;
|
||||||
let sudGrainMass = 0;
|
let sudGrainMass = 0;
|
||||||
let sudWaterMass = 0;
|
let sudWaterMass = 0;
|
||||||
|
let sudVolumen = 0;
|
||||||
let sudEmpty = true;
|
let sudEmpty = true;
|
||||||
let sudLastLoadedDoc = null;
|
let sudLastLoadedDoc = null;
|
||||||
let sudState = null;
|
let sudState = null;
|
||||||
@@ -127,14 +128,16 @@ function buildStep(number, defaults, rawStep) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseSudDoc(doc) {
|
function parseSudDoc(doc) {
|
||||||
|
const pot = doc.pot || {};
|
||||||
const stepDefaults = (doc.default && doc.default.step) || {};
|
const stepDefaults = (doc.default && doc.default.step) || {};
|
||||||
const schedule = (doc.steps || []).map((raw, i) => buildStep(i + 1, stepDefaults, raw));
|
const schedule = (doc.steps || []).map((raw, i) => buildStep(i + 1, stepDefaults, raw));
|
||||||
return {
|
return {
|
||||||
name: doc.Name || '',
|
name: doc.Name || '',
|
||||||
schedule: schedule,
|
schedule: schedule,
|
||||||
potMass: doc.pot_mass || 0,
|
potMass: pot.mass || 0,
|
||||||
grainMass: doc.grain_mass || 0,
|
grainMass: pot.grain_mass || 0,
|
||||||
waterMass: doc.water_mass || 0,
|
waterMass: pot.water_mass || 0,
|
||||||
|
volumen: pot.volumen || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +265,56 @@ function updateStepPlates() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Pot visualization ---
|
||||||
|
|
||||||
|
function wavePathD(x1, x2, y, amplitude, wavelength) {
|
||||||
|
let d = `M ${x1} ${y.toFixed(1)}`;
|
||||||
|
for (let x = x1 + 3; x <= x2; x += 3) {
|
||||||
|
const yw = y + amplitude * Math.sin(2 * Math.PI * (x - x1) / wavelength);
|
||||||
|
d += ` L ${x} ${yw.toFixed(1)}`;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentStepWaterMass() {
|
||||||
|
if (sudStepIndex !== null && sudStepIndex >= 0 && sudStepIndex < sudSchedule.length) {
|
||||||
|
return sudSchedule[sudStepIndex].water_mass || 0;
|
||||||
|
}
|
||||||
|
return sudWaterMass;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePotVisualization() {
|
||||||
|
// Thermometer mercury column (0°C = y=174, 100°C = y=28, tube length = 146)
|
||||||
|
const tubeBottom = 174;
|
||||||
|
const tubeLen = 146;
|
||||||
|
const temp = plotTempIst !== null ? Math.max(0, Math.min(100, plotTempIst)) : 0;
|
||||||
|
const mercuryH = (temp / 100) * tubeLen;
|
||||||
|
document.getElementById('thermo-mercury').setAttribute('y', (tubeBottom - mercuryH).toFixed(1));
|
||||||
|
document.getElementById('thermo-mercury').setAttribute('height', mercuryH.toFixed(1));
|
||||||
|
document.getElementById('thermo-temp-label').textContent =
|
||||||
|
plotTempIst !== null ? plotTempIst.toFixed(1) + ' °C' : '--';
|
||||||
|
|
||||||
|
// Water fill
|
||||||
|
const waterFillEl = document.getElementById('water-fill');
|
||||||
|
const waterWaveEl = document.getElementById('water-wave');
|
||||||
|
const potInnerTop = 30;
|
||||||
|
const potInnerBottom = 172;
|
||||||
|
const potInnerH = potInnerBottom - potInnerTop; // 142
|
||||||
|
|
||||||
|
if (sudEmpty || sudVolumen <= 0) {
|
||||||
|
waterFillEl.setAttribute('height', '0');
|
||||||
|
waterWaveEl.setAttribute('d', '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillFraction = Math.min(1, Math.max(0, currentStepWaterMass() / sudVolumen));
|
||||||
|
const waterH = fillFraction * potInnerH;
|
||||||
|
const waterTop = potInnerBottom - waterH;
|
||||||
|
waterFillEl.setAttribute('y', waterTop.toFixed(1));
|
||||||
|
waterFillEl.setAttribute('height', waterH.toFixed(1));
|
||||||
|
waterWaveEl.setAttribute('d', wavePathD(63, 177, waterTop, 3, 28));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Status line - mirrors update_status_step_label(). ---
|
// --- Status line - mirrors update_status_step_label(). ---
|
||||||
|
|
||||||
function updateStatusLine() {
|
function updateStatusLine() {
|
||||||
@@ -345,6 +398,7 @@ function updateSudForecast(doc) {
|
|||||||
sudPotMass = parsed.potMass;
|
sudPotMass = parsed.potMass;
|
||||||
sudGrainMass = parsed.grainMass;
|
sudGrainMass = parsed.grainMass;
|
||||||
sudWaterMass = parsed.waterMass;
|
sudWaterMass = parsed.waterMass;
|
||||||
|
sudVolumen = parsed.volumen;
|
||||||
sudEmpty = sudSchedule.length === 0;
|
sudEmpty = sudSchedule.length === 0;
|
||||||
if (isNewSchedule) {
|
if (isNewSchedule) {
|
||||||
sudStepDescr = null;
|
sudStepDescr = null;
|
||||||
@@ -362,6 +416,7 @@ function updateSudForecast(doc) {
|
|||||||
rebuildStepPlates();
|
rebuildStepPlates();
|
||||||
updateStatusLine();
|
updateStatusLine();
|
||||||
updateSudActions();
|
updateSudActions();
|
||||||
|
updatePotVisualization();
|
||||||
document.title = sudName ? `BrewPi - ${sudName}` : 'BrewPi';
|
document.title = sudName ? `BrewPi - ${sudName}` : 'BrewPi';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +505,7 @@ function onTempCtrlChanged(msg) {
|
|||||||
plotTempIst = ist.Temp;
|
plotTempIst = ist.Temp;
|
||||||
document.getElementById('lcd-temp-ist').textContent = ist.Temp.toFixed(1);
|
document.getElementById('lcd-temp-ist').textContent = ist.Temp.toFixed(1);
|
||||||
updateStepPlates();
|
updateStepPlates();
|
||||||
|
updatePotVisualization();
|
||||||
}
|
}
|
||||||
if ('Rate' in ist) {
|
if ('Rate' in ist) {
|
||||||
document.getElementById('lcd-rate-ist').textContent = ist.Rate.toFixed(1);
|
document.getElementById('lcd-rate-ist').textContent = ist.Rate.toFixed(1);
|
||||||
@@ -504,6 +560,7 @@ function onSudChanged(msg) {
|
|||||||
sudStepType = step.Type;
|
sudStepType = step.Type;
|
||||||
updateStatusLine();
|
updateStatusLine();
|
||||||
updateStepPlates();
|
updateStepPlates();
|
||||||
|
updatePotVisualization();
|
||||||
} else if (key === 'HoldRemaining') {
|
} else if (key === 'HoldRemaining') {
|
||||||
sudHoldRemaining = msg.HoldRemaining;
|
sudHoldRemaining = msg.HoldRemaining;
|
||||||
updateStatusLine();
|
updateStatusLine();
|
||||||
|
|||||||
@@ -45,6 +45,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="pot-view">
|
||||||
|
<svg id="pot-svg" viewBox="0 0 290 205" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="pot-clip">
|
||||||
|
<rect x="63" y="30" width="114" height="142"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="tube-clip">
|
||||||
|
<rect x="215" y="28" width="8" height="146"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Water fill (behind pot walls) -->
|
||||||
|
<rect id="water-fill" x="63" y="172" width="114" height="0"
|
||||||
|
fill="#1a4a72" clip-path="url(#pot-clip)"/>
|
||||||
|
<path id="water-wave" d=""
|
||||||
|
fill="none" stroke="#4a9ed8" stroke-width="2"
|
||||||
|
clip-path="url(#pot-clip)"/>
|
||||||
|
|
||||||
|
<!-- Pot outline (transparent walls) -->
|
||||||
|
<line x1="60" y1="28" x2="60" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<line x1="180" y1="28" x2="180" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<line x1="60" y1="174" x2="180" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<line x1="60" y1="28" x2="180" y2="28" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Stirrer blades (inside pot) -->
|
||||||
|
<rect x="87" y="157" width="31" height="5" fill="#666" rx="1"/>
|
||||||
|
<rect x="122" y="157" width="31" height="5" fill="#666" rx="1"/>
|
||||||
|
|
||||||
|
<!-- Lid -->
|
||||||
|
<rect x="56" y="20" width="128" height="10" fill="#444" rx="2"/>
|
||||||
|
<rect x="56" y="20" width="128" height="3" fill="#555"/>
|
||||||
|
|
||||||
|
<!-- Stirrer shaft (over lid) -->
|
||||||
|
<rect x="118" y="2" width="4" height="158" fill="#777"/>
|
||||||
|
|
||||||
|
<!-- Motor housing -->
|
||||||
|
<rect x="106" y="2" width="28" height="20" fill="#333" rx="3"/>
|
||||||
|
<rect x="110" y="6" width="10" height="8" fill="#4a4a4a" rx="1"/>
|
||||||
|
<circle cx="129" cy="11" r="3" fill="#cc2222"/>
|
||||||
|
|
||||||
|
<!-- Thermometer tube -->
|
||||||
|
<rect x="212" y="28" width="14" height="146" rx="7"
|
||||||
|
fill="#1e1e1e" stroke="#aaa" stroke-width="1.5"/>
|
||||||
|
<!-- Thermometer bulb -->
|
||||||
|
<circle cx="219" cy="178" r="13" fill="#1e1e1e" stroke="#aaa" stroke-width="1.5"/>
|
||||||
|
<!-- Mercury: bulb always red -->
|
||||||
|
<circle cx="219" cy="178" r="10" fill="#e0190a"/>
|
||||||
|
<!-- Mercury column (JS sets y and height) -->
|
||||||
|
<rect id="thermo-mercury" x="215" y="174" width="8" height="0"
|
||||||
|
fill="#e0190a" clip-path="url(#tube-clip)"/>
|
||||||
|
|
||||||
|
<!-- Scale ticks -->
|
||||||
|
<line x1="227" y1="28" x2="236" y2="28" stroke="#9e9e9e" stroke-width="1.5"/>
|
||||||
|
<text x="239" y="32" font-size="9" fill="#9e9e9e">100</text>
|
||||||
|
<line x1="227" y1="65" x2="232" y2="65" stroke="#9e9e9e" stroke-width="1"/>
|
||||||
|
<line x1="227" y1="101" x2="236" y2="101" stroke="#9e9e9e" stroke-width="1.5"/>
|
||||||
|
<text x="239" y="105" font-size="9" fill="#9e9e9e">50</text>
|
||||||
|
<line x1="227" y1="137" x2="232" y2="137" stroke="#9e9e9e" stroke-width="1"/>
|
||||||
|
<line x1="227" y1="174" x2="236" y2="174" stroke="#9e9e9e" stroke-width="1.5"/>
|
||||||
|
<text x="239" y="178" font-size="9" fill="#9e9e9e">0 °C</text>
|
||||||
|
|
||||||
|
<!-- Current temperature label -->
|
||||||
|
<text id="thermo-temp-label" x="219" y="201" font-size="10"
|
||||||
|
fill="#e0e0e0" text-anchor="middle">--</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav id="tabs">
|
<nav id="tabs">
|
||||||
<button class="tab-btn active" data-tab="manual">Manual</button>
|
<button class="tab-btn active" data-tab="manual">Manual</button>
|
||||||
<button class="tab-btn" data-tab="progress">Progress</button>
|
<button class="tab-btn" data-tab="progress">Progress</button>
|
||||||
|
|||||||
@@ -133,6 +133,18 @@ header#connection-bar {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Pot visualization --- */
|
||||||
|
|
||||||
|
#pot-view {
|
||||||
|
margin: 0.5em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
#pot-svg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 180px;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Tabs --- */
|
/* --- Tabs --- */
|
||||||
|
|
||||||
nav#tabs {
|
nav#tabs {
|
||||||
|
|||||||
Reference in New Issue
Block a user