Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/display/core/Controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,25 @@ void Controller::deactivate() {
currentProcess = nullptr;
if (lastProcess->getType() == MODE_BREW) {
pluginManager->trigger("controller:brew:end");
// Reload the selected profile from disk so any temporary temp/weight
// adjustments made via the dashboard +/- steppers or the display's
// gear-icon BrewScreen Settings panel (raise/lowerTemp,
// raise/lowerBrewTarget) revert back to the profile's saved values
// after each brew. For permanent changes the user should save via
// the Profiles page or the display's Save button.
//
// Reset the profile in place before reload: parseProfile() appends to
// profile.phases rather than replacing, so feeding it the already-
// populated selectedProfile would double the phases on every brew
// (60s utility profile reading 1:00 → 2:00 → 3:00 across runs, and
// the brew loop actually executing the duplicated phases). Mirrors
// the `selectedProfile = Profile{}` pattern that
// ProfileManager::selectProfile already uses for the same reason.
if (profileManager) {
Profile &p = profileManager->getSelectedProfile();
p = Profile{};
profileManager->loadSelectedProfile(p);
}
} else if (lastProcess->getType() == MODE_GRIND) {
pluginManager->trigger("controller:grind:end");
}
Expand Down
7 changes: 7 additions & 0 deletions src/display/plugins/WebUIPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,13 @@ void WebUIPlugin::handleWebSocketData(AsyncWebSocket *server, AsyncWebSocketClie
controller->raiseGrindTarget();
} else if (msgType == "req:lower-grind-target") {
controller->lowerGrindTarget();
} else if (msgType == "req:raise-brew-target") {
// Web dashboard Brew-mode +/- buttons. The web client has
// long been sending this via raiseTarget() but no firmware
// handler existed, so the message was silently dropped.
controller->raiseBrewTarget();
} else if (msgType == "req:lower-brew-target") {
controller->lowerBrewTarget();
} else if (msgType == "req:change-mode") {
if (doc["mode"].is<uint8_t>()) {
auto mode = doc["mode"].as<uint8_t>();
Expand Down
75 changes: 70 additions & 5 deletions web/src/pages/Home/CompactProcessControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { ApiServiceContext, machine } from '../../services/ApiService.js';
import { ModeTab } from './ModeTab.jsx';
import {
fmtDuration,
fmtElapsed,
fmtPhaseTarget,
getPhaseLabel,
Expand All @@ -32,10 +33,10 @@

const status = computed(() => machine.value.status);

const Metric = ({ icon, current, target, unit, rotation }) => (
const Metric = ({ icon, current, target, unit, rotation, currentClassName = 'text-base-content' }) => (
<div className='flex items-center gap-1.5'>
<FontAwesomeIcon icon={icon} className='text-base-content/60 text-xs' />
<span className='text-base-content tabular-nums'>{current}</span>
<span className={`${currentClassName} tabular-nums`}>{current}</span>
<span className='text-success font-semibold tabular-nums'>
/ {target}
{unit}
Expand All @@ -58,6 +59,29 @@
);
};

// Compact two-up stepper for Brew mode (TEMP + WEIGHT side-by-side).
// Slimmer than the full <Adjuster> so two of them fit in the narrow Compact
// card width even on smaller tablets / portrait windows.
const MiniStepper = ({ label, value, onDecrease, onIncrease }) => {
const btn = 'btn btn-ghost btn-xs flex h-6 w-6 items-center justify-center rounded-full p-0';
return (
<div className='flex flex-col items-center gap-1'>
<div className='text-base-content/60 text-[0.6rem] font-light tracking-wider'>{label}</div>
<div className='flex items-center space-x-1'>
<button onClick={onDecrease} className={btn} aria-label={`Lower ${label}`}>
<FontAwesomeIcon icon={faMinus} className='h-2.5 w-2.5' />
</button>
<div className='text-base-content min-w-[44px] text-center text-sm font-bold tabular-nums'>
{value}
</div>
<button onClick={onIncrease} className={btn} aria-label={`Raise ${label}`}>
<FontAwesomeIcon icon={faPlus} className='h-2.5 w-2.5' />
</button>
</div>
</div>
);
};

const Adjuster = ({ label, value, onDecrease, onIncrease }) => {
const btn = 'btn btn-ghost btn-sm flex h-8 w-8 items-center justify-center rounded-full p-0';
return (
Expand Down Expand Up @@ -126,7 +150,7 @@
</div>
);

const BrewIdleView = ({ s, brewTarget, sendTarget }) => (
const BrewIdleView = ({ s, brewTarget, sendTarget, send }) => (
<div className='flex w-full max-w-sm min-w-0 flex-col items-stretch gap-3'>
<a
href='/profiles'
Expand All @@ -145,7 +169,37 @@
</span>
</span>
</a>
{s.volumetricAvailable && <TargetToggle value={brewTarget ? 1 : 0} onChange={sendTarget} />}
{/* Mirrors the device's gear-icon BrewScreen Settings panel exactly:
always show TEMP, plus a second slot that morphs between WEIGHT and
TIME based on `brewTarget` (firmware's `bt` flag = scale connected
AND profile is volumetric). Both modes call the SAME Controller
methods — `raiseBrewTarget` / `lowerBrewTarget` — which decide
internally whether to adjust the profile's volumetric target or its
phase duration based on the same condition. Format is the only
visible difference. */}
<div className='flex flex-row items-start justify-center gap-8'>
<MiniStepper
label='TEMP'
value={`${s.targetTemperature ?? 0}°C`}
onDecrease={() => send('req:lower-temp')}
onIncrease={() => send('req:raise-temp')}
/>
{/* "TARGET" prefix on the morphing label makes the read honest:
the value is the brew-stopping condition the firmware will use,
and that condition depends on the loaded profile (volumetric or
duration). Matches the wording the full-layout ProcessControls
already uses (TARGET WEIGHT / TARGET TIME). */}
<MiniStepper
label={brewTarget ? 'TARGET WEIGHT' : 'TARGET TIME'}
value={
brewTarget
? `${(s.targetWeight ?? 0).toFixed(0)}g`
: fmtDuration(s.brewTargetDuration ?? 0)
}
onDecrease={() => send('req:lower-brew-target')}
onIncrease={() => send('req:raise-brew-target')}
/>
</div>
</div>
);

Expand Down Expand Up @@ -241,7 +295,8 @@
if (processRunning && finished) return <FinishedView elapsed={fmtElapsed(p?.e)} />;
if (processRunning) return <ActiveView p={p} grind={grind} />;
if (mode === 0) return <InfoView title='Standby' hint='Machine is ready' />;
if (mode === 1) return <BrewIdleView s={s} brewTarget={brewTarget} sendTarget={sendTarget} />;
if (mode === 1)
return <BrewIdleView s={s} brewTarget={brewTarget} sendTarget={sendTarget} send={send} />;
if (mode === 2 || mode === 3) return <TemperatureView s={s} mode={mode} send={send} />;
if (grind && !grindAvailable)
return <InfoView title='Grind' hint='Grind function not available' />;
Expand Down Expand Up @@ -283,6 +338,16 @@
current={(s.currentPressure ?? 0).toFixed(1)}
target={(s.targetPressure ?? 0).toFixed(1)}
unit=' bar'
/* Red alert when over 1 bar while sitting idle in Brew mode —
boiler is likely over-pressurized from previous shot/heat-up;
flush or open steam wand before brewing. Suppressed during an
active brew (high pressure is expected) and immediately after
(pressure takes a moment to bleed off). */
currentClassName={
brew && !active && !finished && (s.currentPressure ?? 0) > 1.0

Check warning on line 347 in web/src/pages/Home/CompactProcessControls.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't use a zero fraction in the number.

See more on https://sonarcloud.io/project/issues?id=jniebuhr_gaggimate&issues=AZ5nseaOboUA2ms0g2CT&open=AZ5nseaOboUA2ms0g2CT&pullRequest=712
? 'text-error font-semibold'
: 'text-base-content'
}
/>
</div>

Expand Down
107 changes: 101 additions & 6 deletions web/src/pages/Home/ProcessControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus';
import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus';
import { Tooltip } from '../../components/Tooltip.jsx';
import { MODES } from './utils.js';
import { fmtDuration, MODES } from './utils.js';
import { ModeTab } from './ModeTab.jsx';

const status = computed(() => machine.value.status);
Expand Down Expand Up @@ -321,8 +321,8 @@
{status.value.currentTemperature.toFixed(1) || 0}
</span>
<span className='text-success font-semibold'>
{' '}
/ {status.value.targetTemperature || 0}°C
{' / '}
{status.value.targetTemperature || 0}°C
</span>
</div>
{status.value.volumetricAvailable && mode !== 0 && (
Expand All @@ -334,8 +334,8 @@
{(status.value.currentWeight ?? 0).toFixed(1)}g
</span>
<span className='text-success font-semibold'>
{' '}
/ {(status.value.targetWeight ?? 0).toFixed(0)}g
{' / '}
{(status.value.targetWeight ?? 0).toFixed(0)}g
</span>
</>
)}
Expand All @@ -344,7 +344,27 @@
<div className='flex flex-row items-center gap-2 text-center text-base sm:text-right sm:text-lg'>
<FontAwesomeIcon icon={faGauge} className='text-base-content/60' />
<span className='text-base-content'>
{status.value.currentPressure?.toFixed(1) || 0} /{' '}
{/* Only the CURRENT pressure value turns red when above 1 bar
while sitting idle in Brew mode — typically means the boiler
is over-pressurized from the previous shot or heat-up cycle,
and the user should flush or open the steam wand before
brewing to avoid wrecking the puck. Suppressed once a brew
is active or just finished, since high pressure is expected
during the shot and takes a moment to bleed off after. The
target pressure and units always stay normal color. */}
<span
className={
brew &&
!active &&
!finished &&
(status.value.currentPressure ?? 0) > 1.0

Check warning on line 360 in web/src/pages/Home/ProcessControls.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't use a zero fraction in the number.

See more on https://sonarcloud.io/project/issues?id=jniebuhr_gaggimate&issues=AZ5nsejOboUA2ms0g2CU&open=AZ5nsejOboUA2ms0g2CU&pullRequest=712
? 'text-error font-semibold'
: ''
}
>
{status.value.currentPressure?.toFixed(1) || 0}
</span>
{' / '}
{status.value.targetPressure?.toFixed(1) || 0} bar
</span>
</div>
Expand Down Expand Up @@ -476,6 +496,81 @@
</div>
)}
{/* Controls for different modes */}
{mode === 1 && !active && !finished && (
<div className='flex flex-col items-center gap-4'>
{/* Brew mode quick steppers — mirrors the device's gear-icon
BrewScreen Settings panel: always show TEMP, plus a single
second slot that morphs between WEIGHT and TIME based on
`brewTarget` (firmware's `bt` flag = scale connected AND
profile is volumetric). Both modes call the same Controller
methods (raiseBrewTarget / lowerBrewTarget) which decide
internally whether to adjust the profile's volumetric target
or its phase duration based on the same condition. Changes
are transient — firmware reverts the in-memory profile to its
on-disk values after each brew (Controller::deactivate). For
permanent edits use the Profiles page. */}
<div className='flex flex-row flex-wrap items-start justify-center gap-x-8 gap-y-4'>
<div className='flex flex-col items-center gap-2'>
<div className='text-base-content/60 text-xs font-light tracking-wider'>
TEMPERATURE
</div>
<div className='flex items-center space-x-2'>
<Tooltip content='Lower temperature'>
<button
onClick={lowerTemp}
className='btn btn-ghost btn-sm flex h-8 w-8 items-center justify-center rounded-full p-0'
aria-label='Lower target temperature'
>
<FontAwesomeIcon icon={faMinus} className='h-3 w-3' />
</button>
</Tooltip>
<div className='text-base-content min-w-[80px] text-center text-lg font-bold'>
{status.value.targetTemperature || 0}°C
</div>
<Tooltip content='Raise temperature'>
<button
onClick={raiseTemp}
className='btn btn-ghost btn-sm flex h-8 w-8 items-center justify-center rounded-full p-0'
aria-label='Raise target temperature'
>
<FontAwesomeIcon icon={faPlus} className='h-3 w-3' />
</button>
</Tooltip>
</div>
</div>
<div className='flex flex-col items-center gap-2'>
<div className='text-base-content/60 text-xs font-light tracking-wider'>
{brewTarget ? 'TARGET WEIGHT' : 'TARGET TIME'}
</div>
<div className='flex items-center space-x-2'>
<Tooltip content={brewTarget ? 'Lower target weight' : 'Lower target time'}>
<button
onClick={lowerTarget}
className='btn btn-ghost btn-sm flex h-8 w-8 items-center justify-center rounded-full p-0'
aria-label={brewTarget ? 'Lower target weight' : 'Lower target time'}
>
<FontAwesomeIcon icon={faMinus} className='h-3 w-3' />
</button>
</Tooltip>
<div className='text-base-content min-w-[80px] text-center text-lg font-bold'>
{brewTarget
? `${(status.value.targetWeight ?? 0).toFixed(0)}g`
: fmtDuration(status.value.brewTargetDuration ?? 0)}
</div>
<Tooltip content={brewTarget ? 'Raise target weight' : 'Raise target time'}>
<button
onClick={raiseTarget}
className='btn btn-ghost btn-sm flex h-8 w-8 items-center justify-center rounded-full p-0'
aria-label={brewTarget ? 'Raise target weight' : 'Raise target time'}
>
<FontAwesomeIcon icon={faPlus} className='h-3 w-3' />
</button>
</Tooltip>
</div>
</div>
</div>
</div>
)}
{mode === 2 && (
<div className='flex flex-col items-center gap-4 space-y-4'>
{/* Temperature adjustment controls for steam mode */}
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/Home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ export function Home() {
className={`landscape:max-lg:min-h-0 landscape:sm:col-span-5 ${dashboardLayout === DASHBOARD_LAYOUTS.ORDER_FIRST ? 'order-first' : 'order-last'}`}
title='Process Controls'
>
<div className='landscape:hmd:hidden potrait:md:hidden contents'>
<div className='landscape:hmd:hidden portrait:md:hidden contents'>
<CompactProcessControls brew={mode === 1} mode={mode} changeMode={changeMode} />
</div>
<div className='landscape:hmd:contents potrait:md:contents hidden'>
<div className='landscape:hmd:contents portrait:md:contents hidden'>
<ProcessControls brew={mode === 1} mode={mode} changeMode={changeMode} />
</div>
</Card>
Expand Down
8 changes: 8 additions & 0 deletions web/src/pages/Home/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export const fmtElapsed = (ms = 0) => {
return `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`;
};

// fmtDuration takes a value in SECONDS (vs fmtElapsed which takes ms) and
// renders M:SS. Used by the Brew-mode stepper when the profile/scale combo
// puts it in time mode rather than weight mode.
export const fmtDuration = (s = 0) => {
const secs = Math.floor(s);
return `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`;
};

export const fmtPhaseTarget = (p, grind) => {
if (p?.tt === 'time') return `${(p.pt / 1000).toFixed(0)}s`;
if (p?.tt === 'volumetric') return `${p.pt.toFixed(grind ? 1 : 0)}g`;
Expand Down
Loading