Releases ·

Roastwarden 0.5.0

Everything since v0.4.0 (2026-10-26): roasting itself, then the paperwork again. Autotune 2 (the server's relay test and PI designs) and the device contract with the gains held by the server; deploys that wait for the roaster and a drive that resumes after a restart; roast analysis in the server (roast review, roast compare); auto DROP by time, BT or development; the roaster's own thermal model and its live prediction; roasted coffee with its rest window; a new bean's first roast; the hardening of the release review. Then labels and reports polished from a review: label notes, quicker labels after a roast, a report comparing two roasts, smarter plan and profile reports, a layout check on every report.

Added

  • Compare two roasts on paper (ROADMAP Next 38; PDF reports): a one-page report for an A/B experiment: both roasts' BT and RoR on one chart with their first crack and drop, heater and fan strips, the phases on one time scale, first crack, drop, development and loss as "A → B", every roast compare row with B − A, and each roast's notes. GET /api/reports/compare?roasts=A,B (one roast: it and the roaster's finished roast before; none: the newest two); roast report compare [A] [B]; Reports → "Compare two roasts", History → Compare → "Compare report"; eval task 37.
  • Label notes and a label name (ROADMAP Next 37; Bag labels, Green coffee): a bean's own few tasting words ("chocolate · coconut · apple") and a shorter name for its labels, migration 044; with none, the bean suggests notes from the supplier's (Use … on the bean form). Labels print, first found: the notes asked for, the bean's label notes, the roast's cupping notes, the supplier's cut at a sentence. roast bean add|edit --label-notes --short-name; the skill says how to write them; eval task 36.
  • Gift labels link to the roast's share page: a design names its own QR default (qr: "share"; gift does) with a caption, sized by the code's modules. A gift PDF or print makes the roast's share link if it has none; previews still do not.
  • Printed it, in view: Download, Print and Printed it stay on screen on a roast's Labels page and Label Studio → Print; Printed it moves the sheet's next free label on there. One item editor on both pages. Labels and Report links on the after-roast summary (once weighed) and on each roasted lot on Beans.
  • The report check: every report kind's tests lay it out and fail on a last page with under 20 mm of content, colliding text or text past the paper.
  • The roast-day plan shows each planned coffee's last roast on its profile (first crack, drop, loss, the notes' result) and units in its write-in boxes; a profile with no roasts prints its target curve and what to expect.
  • roast profile edit --expect KEY=LOW-HIGH: a profile's own expected range (development, loss, DTR, colour) over its level's default, as the profile editor sets it; profile show lists them. Light espresso on production: development 1:45–2:45 (owner 2026-10-30), so its 2:15 is no longer flagged against the light level's 1:00–2:00.
  • A new bean's first roast (ROADMAP Next 33; A new bean's first roast): before the first roast of a bean on a roaster, what to expect: the profile to start from, first crack's BT and time on the profile from this roaster's own roasts (± 3 °C, ± 4 °C for naturals, honeys, wet-hulled and unknown processes), a natural's quiet first crack, a "drop by development" offer when the profile's auto DROP is by time, a wet-hulled, low-density or wet bean's slower drying, and the supplier's level and espresso advice quoted. After a roast, while the bean has fewer than three on its profile: where first crack came, how far to move the drop to keep the profile's development, development or loss outside the level's range in 15 s steps, a late first crack, and "if it tastes…". The bean page ("First roast" / "Next roast of this bean"), the Roast page's step card (one line, the rest behind its ?), History → a roast. GET /api/beans/{id}/guide; roast bean guide, roast review prints the next-roast lines; the skill's "A new bean", eval task 35. Nothing is stored and nothing changes a profile by itself.
  • Roasted coffee: resting and ready (ROADMAP Next 31; Roasted coffee): every finished, weighed roast is roasted coffee with its rest window: resting until the bag label's "best from" date (by purpose and level), ready until 35 days (light) or 28 (darker), then past its best. The Beans page opens with it: per espresso and filter what is ready, what is resting and when the next is ready, how fast it goes (from coffee recorded as used) and the day to roast again so a new roast has rested in time, running low below 200 g; each roast's coffee left with Used…, Gave… and Finished; History → a roast shows what is left of it. Settings → Roasting has the best-until days and the low mark. GET /api/roasted, GET /api/roasted/{id}, POST /api/roasted/{id}/moves (used, given, discarded in grams, doses, bags or all; or a count of what is left), DELETE …/moves/{move}, migration 043. roast roasted [--for espresso|filter] [--ready|--resting], roasted show|use|give|toss|count|undo (a roast by id, #N, latest or name), the skill's "Roasted coffee", eval task 34.
  • Prediction from the roaster's own model (ROADMAP Next 30; Prediction): each roaster gets a thermal model fitted from its own roasts (dBT/dt = gain · lag(heater/fan) + g(BT), starting from the virtual roaster's physics), refitted after each roast and scored with every roast left out in turn: on esp32_roaster_01's 12 roasts BT two minutes ahead is within 1.6 °C RMS with the heater as it was, and with the roaster's PID on the plan (scored where the roaster stayed in auto), against the scope projection's 34.5 °C. While a roast runs the server predicts BT for the next two minutes under the plan (the drive's setpoints with the roaster's PID, a held setpoint, or held heater and fan) with a 90 % band and its reason, first crack from the BT it came at on this bean and profile, DROP from the profile, and when the heater would run out; every 15 s it keeps the prediction with the roast and scores it against the old projection. The scope's BT model series (off by default) beside BT →; Roasters → Overview → Model (scores, where BT settles, Refit). GET /api/roaster/{id}/model, POST …/model/fit, GET /api/roaster/{id}/prediction, GET /api/sessions/{id}/prediction, migration 042. roast roaster model [--fit], roast live and roast review show it; the skill, eval task 33.
  • Auto DROP (ROADMAP Next 29; Profile drive, "Auto DROP"): a profile can let Profile drive mark DROP by itself, off by default: at its drop time, at its drop temperature (from first crack on) or at its development time after first crack is marked. A countdown (Settings → Roasting, 10 s by default) ends on the drop itself: the Roast page shows it with Cancel (for that roast only; a resumed drive keeps it cancelled) and phones get a notice; then DROP's step runs as for any DROP (heater off, fan to cool). It stays off when the roaster's DROP step is off. The profile editor and page, PATCH /replay {"auto_drop": false}, migration 041. roast profile edit --auto-drop time|bt|development|off, profile show and roast live (the countdown), the skill, eval task 32; drive-check drops its roast this way.
  • Roast analysis (ROADMAP Next 28; Roast review and compare): one reading of a roast in the server: marks and phases, RoR (the early peak, each minute, the late low, the FC → drop slope), BT against its profile (at 0:30–2:00, the overshoot after CHARGE, RMS from TP), how the heater and fan were used (first 30 s, time at 90 % or more, the last minute), its numbers judged against what was expected, and plain notes. GET /api/sessions/{id}/analysis; GET /api/analysis/compare lines 2–8 roasts up against one (every number with its difference, the curves every 30 s, from when BT stays within 0.5 °C). roast review <roast> [--vs <roast>] and roast compare, which take #12 (a batch number) as well as an id or latest; the skill's "Reading a roast"; eval task 31.
  • Resume a drive cut by a restart (ROADMAP Next 26; Profile drive, "Resume after a restart"): on boot a Profile drive a crash, a power cut or a restart by hand cut mid-roast is kept as a stopped run where it stopped; the Roast page offers Heater on and resume drive with the old settings, and the profile picks up at the time it had reached, the time lost added to its offset (the Drive tab shows "profile at 4:52 (+0:40)"). roast live shows it with the resume call; POST /replay takes resume_at_profile_seconds. Eval task 30.
  • Deploys wait for the roaster (ROADMAP Next 25; Running it): GET /api/busy says whether a roaster is in the middle of something a restart would cut (a profile drive, the server's autotune, cooling after DROP, its heater on, an open roast with beans at 50 °C or more; the virtual roaster never counts). The deploy waits while a real roaster is busy (up to 2 h). roast busy [--wait MINUTES] (exit 1 while busy), roast status, Settings → About → Roasters, the skill, eval task 29.
  • The workflow on the CI runners (docs/DEVELOPMENT.md, "The loop"): the dashboard's browser checks (a driven roast and every page) run in CI against the image just built, and the deploy waits for them; after the deploy CI installs the skill and roast on hermes (Agents, "Kept current"); Lint checks that a feature commit ships its agent path and docs (scripts/ci/agent-path.sh). scripts/dev/prepush.sh and scripts/dev/ci-watch.sh for the laptop. RUSTROAST_MQTT=loopback runs the server with no broker (the virtual roaster only); the local stack uses it.
  • The device contract (Control, "Device contract"; ROADMAP Next 24): what a roaster must do for rustRoast (BT/ET out, heater and fan % in, over-temperature cutoffs, sensor checks, the fan interlock, emergency stop, a comms watchdog, a retained presence) and what it may offer as caps (onboard PID, feedforward, lease, time-proportioned heater, oversampling, RoR limiter). The server runs the roast; the device keeps it safe.
  • Gains after a boot: the server holds each roaster's PID gains (the newest stored change, or the gain schedule's for the fan) and sends them on the first sample after the server starts and when the roaster's uptime goes back (it rebooted), if the roaster reports others. Any transport.
  • A roaster against the contract: GET /api/roaster/{id}/contract, roast roaster contract, and Roasters → Overview → Device contract: its firmware, the readings it sends, its caps, and whether its PID gains are the ones the server holds, with the last check after a boot (what it came up with, what the server sent). Eval task 28. First used on the real roaster's 3.5.0 flash (2026-10-27): it came up on 5.5 / 0.085 / 0, the server sent back 5.561 / 0.08471 / 0.
  • The roaster page's header stacks on a phone (the name, "Selected roaster" and the status badge overlapped).
  • Firmware 3.5.0 (Esp32_FluidBedCoffeeRoaster), per the contract: the onboard relay autotune (with its overshoot guard and Ku/Tu rules) and the NVS-stored gains are gone. Gains live in RAM and boot on the build's defaults (the Autotune 2 PI at 72 % fan) until the server sends them; control/pid and serial PID;T no longer write flash.
  • Autotune 2 (Autotune; ROADMAP Next 23): the server runs the autotune on any roaster that takes a heater % and reports BT, the virtual roaster too (manual heater, a control lease): a heat-up that hands over early by the expected coast, the sensor's noise for the switching band, a symmetric relay that finds the holding power, an asymmetric one (the larger swing downward, where a heat-limited roaster has room). Every sample is kept; the analysis fits integrator, first- and second-order models with dead time, checks them against the oscillation's own harmonics and Berner's half-period ratio, and designs the PI by SIMC (one knob, tight to smooth) with AMIGO and the old Ku/Tu rules beside it, each with Ms, margins, what happens if the roaster changes, the heater jitter and the ramp lag. The dashboard's Autotune tab shows the record against the model and the knob; the gain schedule keeps each row's model and designs the gains between rows. API …/tune/start|stop, GET …/tune, …/autotune/runs/{id}/samples|design|analyse; roast roaster tuning --run ID [--tau-c N]; eval task 27. The method is its own crate (crates/autotune), with no roaster knowledge, for reuse. Research: docs/research/autotune-2026-10-26/. Migration 038.
  • Firmware 3.4.0 (Esp32_FluidBedCoffeeRoaster): the thermocouples are read four times a second and averaged (about half the noise, finer than one 0.25 °C step); temperatures go out with two decimals. The virtual roaster reads the same way.
  • The lookahead adapts (Profile drive, "Lookahead"; ROADMAP Next 22): Profile drive's setpoint lead starts at the lookahead and, from a minute after CHARGE, follows how far BT runs ahead of or behind the profile, between 0 and the lookahead. With the fixed 20 s lead BT ran 10 s, then 20 s, ahead of the profile on the real roasts: first crack at 7:29 against the profile's 7:50. Settings → Roasting → "Adapt to the roaster" (on); the drive tab shows the lead in use (lookahead_now; adapt_lookahead when starting a drive).
  • The gain schedule's advice (docs/autotune.md, "Checking a schedule"): rows with a D term (on this sensor it turns each 0.25 °C step into a heater jump) and neighbouring rows that disagree, under the schedule editor and in roast roaster tuning.
  • Each telemetry sample of a roast keeps the control fields the roaster sent (applied heater, RoR limiter, PID gains, mode, heater power, stop code, feedforward) as extras (migration 037), and its time to the millisecond.
  • The Roast page keeps the screen on while a roast runs; the Dev readout says when BT reaches the profile's drop temperature at the RoR now.

Changed

  • Settings → About and roast status say how far past its release a build is ("0.4.0 + 66 commits since its release", with the commit's date: build in GET /api/about, from git describe stamped by CI), and the dashboard says when an open tab is older than the server (by revision, not only by version, which moves only at a release).
  • A repeat Download of the same labels returns the run not yet printed (made in the last 24 h, not sent or printed; reused: true) instead of a new one; new: true / roast labels run --new always makes one.
  • The message field follows the design: "To" with just the name for a design that prints its own To, hidden for a design with a text to setting.
  • One brew recipe: l.brew on labels is the share page's, by level (light espresso 1:2.5 at 94 °C, 18 g → 45 g; the share page said 1:2 for every espresso).
  • Label text: "Light · Espresso", weights as "250 g · 8.8 oz" rounded down like the net weight, plain grows its type and centres on roomy labels (6874, 5163); the check's cut says how much of the label was free (report.free) and two soft issues leave a label ok: dropped (name, roast date or best-from not printed) and hint (shop with no brand).
  • The coffee report's small multiples are one overlay of every roast's BT and RoR, the newest in colour (schema coffee/2; fixes its A4 break); the coffee, profile and period tables name each roast.
  • The first-crack prompt goes by BT: within 2 °C of the profile's first-crack BT (else 196 °C), after dry end. The RoR-dip rule it replaces came 45 s to 2½ minutes late on every real roast (the PID hides the dip).
  • Auto DRY end comes at the roast profile's own dry-end BT when it has one, else auto_dry_temp.
  • The setpoint goes to the roaster in 0.2 °C steps (follow_hysteresis_c; migration 035 moves a stored 1.0): 1 °C steps kicked the device PID every 7–12 s late in a roast.
  • An autotune does not start while a roast is on the roaster (409) or below 45 % fan; mode: step_response and aggressiveness are gone (the firmware has neither).
  • Fan assist judges the heater the roaster applied (heaterApplied), not the commanded one.
  • The virtual roaster's PID works from the noisy 0.25 °C reading, as the firmware's does.

Removed

  • The firmware's own autotune on the server side (firmware 3.5.0 has none; Autotune 2 replaces it): POST …/autotune/start|stop|apply, GET …/autotune/status[/latest] and …/autotune/results[/latest], the autotune events on both dashboard WebSockets, the dashboard's "Onboard (firmware)" mode and its scope overlay and banner, roast roaster tuning --messages, and the autotune_status / autotune_results tables (migration 040). Stored firmware runs stay in the run history, their rules still apply.
  • POST …/control/ror_limit answers 409 for a roaster that lists its caps without ror (firmware 3.4.1 dropped its RoR limiter and ignored the command).

Fixed

  • "To for Sam" on gift labels (Next 35, L9): the owner's saved stamp (v2) and noel-tag (v3) designs drop a message's own "for" or "to" before their "To" and wrap a long message to two lines; the label check counts "To Sam" as the message "for Sam".
  • Dates by the owner's time zone (Next 35, L8): labels, share pages, print runs, roasted coffee and reports made by the CLI or an agent were dated in UTC (a late-evening roast printed as the next day). A time_zone setting (Settings → Roasting, e.g. America/Chicago) dates any request that sends no offset of its own; the CLI sends its offset only when it is not UTC.
  • Labels and reports polish. Labels: long names and gift messages shrink, then wrap to two balanced lines (round labels too; wrap in rustroast.typ); notes fill the room left (clamp with a height); previews and checks no longer create share links (a placeholder code; only a PDF, print or run makes one); the check no longer reports printed fields as missing (labels get roasted_iso, best_from_iso, rest_days); the calibration page reads offsets from mm scales 3 mm inside each label, clear of the printer's margin. Reports: rings on the spread strips no longer collide, no page holds only a closing note, the roast report's RoR stays on its chart, table numbers line up (grams below 1 kg), a year or month in progress says "so far" and folds empty months, "usual" ranges never go below zero (the dashboard too), overlay and heater/fan labels pushed apart, clearer wording ("Cost per kg roasted", "above light 1:00–2:00"). Dashboard: Enter in the Typst editor makes a new line instead of taking a completion; the cupping Scores, coffee and profile reports open on something real; a report preview clears on an error or a new kind and Download waits for one; Print finds roasts by coffee; a roast's label preview never shows a stale answer; Printers asks each printer again and shows the answer's age; Save version is disabled with nothing changed; Label History reads on a phone; GET /api/sessions/{id}/cost answers null instead of 404 when the cost is unknown.
  • The Modbus master reads each block of its register map from the block's first register (it read from address 0, so a map above register ~125 failed every poll) and never writes a register marked not writable.
  • The "Profile drive stopped" banner made the Roast page's right column scroll at 1280×720 and 1366×768 (also the heater-cut one): below 820 px tall its explanation is a tooltip.
  • Settings → About on a phone: the schema name of migration 040 was wider than the screen and scrolled the page sideways; it wraps now.
  • Starting the server autotune right after a DROP: the roast's cooling ended inside the test and switched the fan off, so the fan interlock held the heater at 0 and the test never heated. Starting a test now ends the cooling first, as a new CHARGE does.
  • "Behind profile" (and any delta_* alarm) no longer fires after DROP: the profile holds its last BT past its end, so the cooling beans read 9 °C behind 7 s after DROP (batches #6, #8).
  • A drive on the real roaster stopped 4:08 into batch #6: firmware 3.4.0 read a lease renewal that landed mid-check as 49 days old and cut the heater (stop code 7) 0.5 s after the server renewed it. Firmware 3.4.1 signs the lease's age and reads it before the clock; the server's stop reason now says when the roaster missed a renewal instead of "the server went quiet".
  • Firmware 3.4.1 drops the RoR limiter: on the firmware's 9 s RoR it cut the heater 19 times while the drive caught up after that stop (30 °C/min above 150 °C), fighting Profile drive. The over-temperature cutoffs, sensor checks, fan interlock and lease stay.
  • A server restart after an Autotune 2 run stored the firmware's last autotune again as a new run (its retained results message was compared with the newest run of any kind, now with the newest firmware run). Migration 039 removes the copies.
  • Autotune 2's hold-out check warned falsely on a drifting lag-dominant run (the real roaster from cold, 2026-10-27): fitted again without the last cycle, the main lag slid to 6000 s against the drift term. The hold-out now keeps the full fit's main lag.
  • The gain schedule editor: emptying a gain box sent null and the save failed with a server error. An empty Kd now saves as 0; an empty Kp or Ki says so and stops the save. A row typed over by hand no longer shows the autotune rule it came from ("edited").
  • First crack and dry end were counted from the session start where the roast time counts from CHARGE (History's FC, roast roasts, the Phases tab's shares and DTR); migration 036 corrects the stored times. The drying RoR starts at CHARGE.
  • Resuming a drive's setpoint or heater left the roaster in the mode pressed by hand, so BT stopped following while the drive showed live; it now restores the drive's mode.
  • Pausing a roast stopped its drive's lease renewal and the roaster cut its heater 10 s later; Pause stops the recording only.
  • A drive kept renewing its lease with no telemetry; it now lets the lease lapse after 8 s and stops at 20 s.
  • A stop could let a tick's command land after DROP's heater off and cooling fan; engaging a ghost on the server loop sent the integral it built while watching (up to 100 %); a gain change or a stale reading stepped the server PID's output.
  • A profile fan below 45 % reached the roaster, whose interlock then gated the heater.
  • A new CHARGE while the last roast still cooled could get that roast's fan-off.
  • Scheduled gains were not resent after the roaster rebooted; undo could store the schedule's RAM gains instead of the set stored before.
  • "Behind profile" (and any rule armed after the turning point) never armed on a cold charge, which has no turning point.
  • The MQTT consumer spun on a closed channel.

Security

  • Hardening with no visible change: the audit log masks secret settings (the webhook URL and notification tokens, one at a time or in a batch) and reads at most 1 MB of a body before auth; a notification test never sends a stored token to a server the test changed (type the token again to test another server); label assets are served sandboxed, so an uploaded SVG runs no script; an .alog nested more than 64 deep is refused instead of overflowing the stack.