Skip to content

feat: Web based firmware updates - #1966

Merged
pedrolamas merged 24 commits into
fluidd-core:developfrom
mjonuschat:feat/firmware-updates
Nov 9, 2026
Merged

pedrolamas merged 24 commits into
fluidd-core:developfrom
mjonuschat:feat/firmware-updates

Conversation

@mjonuschat

@mjonuschat mjonuschat commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a Firmware Updates card to Settings, driven by the aldis Moonraker agent. aldis finds every MCU Klipper is configured with, compares each one's firmware to its target (the running host, or for FOCI, Beacon and Cartographer MCUs, their release or image), and builds and flashes the outdated ones. The agent exposes a small API over server.extensions.request.

What Fluidd gains:

  • Agent plumbing that isn't specific to aldis. Fluidd reads connected agents from server.extensions.list and keeps them current with Moonraker's connected and disconnected events. A registry in Globals routes notify_agent_event by agent name and fans out Klippy state changes. Adding another agent takes one registry row.
  • A props-driven UpdatingDialog, shared by Software Updates and Firmware Updates. Software Updates changes in one way. A run that finishes with zero response lines now closes the dialog instead of leaving an empty one open. Nothing else changes.
  • A firmware store module and the card. The module keeps the agent's run as the agent reports it, adds live update_response events to it, and reconciles status snapshots of the same run, so a reloaded page or a reconnecting agent picks up a run in progress. Its run and payload-guard logic lives in pure, tested helpers. Buttons follow the agent's actions, and a rejected update shows the agent's own error message. MCUs the agent cannot flash, and states Fluidd doesn't know, show a chip with no action. An unsupported api_version disables updates and shows an alert explaining why.
  • An "Enable notifications" switch, off by default, and a sidebar entry.

The agent ships in aldis v0.3.0. aldis setup --agent installs the service, and the API is api_version 1. The card renders only when the agent is connected or aldis is listed in Moonraker's available services, so users without aldis see no change.

Test plan

  • pnpm run lint, type-check, test, circular-check, build
  • Unit tests for the run helpers, payload guards, error messages, and the store mutations. The actions and getters are not covered directly: Fluidd's split tsconfig cannot type-check specs that import them, so their logic sits in the tested helpers.
  • Verified on a Raspberry Pi running Klipper with USB, CAN, and Linux host MCUs. Cases covered: agent stopped and started, update one MCU, update all, a flash that fails at bootloader entry, restart_pending, printing and paused, and the notification switch.

Demo Video

aldis-fluidd-demo.mp4

Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
Signed-off-by: Morton Jonuschat <mjonuschat@gmail.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Bundle size report (gzip)

Chunk Base Head Δ
assets/index-*.js 172.0 kB 174.1 kB +2.2 kB
assets/Settings-*.js 23.7 kB 25.0 kB +1.2 kB
assets/en-*.js 15.8 kB 16.3 kB +438 B
assets/socketActions-*.js 39.8 kB 39.9 kB +143 B
assets/AppSettingsNav-*.js 817 B 852 B +35 B
assets/Dashboard-*.js 63.1 kB 63.1 kB +7 B
assets/JobQueueCard-*.js 4.5 kB 4.5 kB -6 B
assets/AppNamedSlider-*.js 1.8 kB 1.8 kB -2 B
assets/GcodePreviewCard-*.js 14.0 kB 14.0 kB -2 B
assets/MacroCategorySettings-*.js 2.0 kB 2.0 kB -2 B
assets/Tune-*.js 3.5 kB 3.5 kB -2 B
sw.js 11.2 kB 11.2 kB -2 B
assets/BeaconCard-*.js 4.4 kB 4.4 kB -1 B
assets/ConsoleCard-*.js 2.0 kB 2.0 kB -1 B
assets/Diagnostics-*.js 15.9 kB 15.9 kB -1 B
assets/DiskUsageCard-*.js 3.2 kB 3.2 kB -1 B
assets/FileSystem-*.js 32.9 kB 32.9 kB +1 B
assets/GcodePreview-*.js 368 B 367 B -1 B
assets/History-*.js 4.5 kB 4.5 kB -1 B
assets/Icons-*.js 530 B 529 B -1 B
assets/index-*.css 26.4 kB 26.4 kB -1 B
assets/Jobs-*.js 499 B 500 B +1 B
assets/NotFound-*.js 440 B 441 B +1 B
assets/Timelapse-*.js 1.9 kB 1.9 kB -1 B
assets/TimelapseRenderSettingsDialog-*.js 1.8 kB 1.8 kB +1 B
Total 2.9 MB 2.9 MB +4.0 kB

120 chunks compared, 25 changed. Sizes are gzip, matching what nginx serves.

@thijstriemstra

thijstriemstra commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

driven by the aldis Moonraker agent.

Very cool project I had not heard about yet! ps. what does the name aldis stand for?

@mjonuschat

Copy link
Copy Markdown
Contributor Author

driven by the aldis Moonraker agent.

Very cool project I had not heard about yet! ps. what does the name aldis stand for?

It’s named after the Aldis lamp, the signal lamp ships use to flash messages to each other. I thought it was fitting in the whole maritime theme

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
pedrolamas and others added 6 commits October 21, 2026 22:18
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>

# Conflicts:
#	src/globals.ts
#	src/store/socket/actions.ts
…dates

Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>

# Conflicts:
#	AGENTS.md
#	src/store/extensions/actions.ts
Drop MoonrakerAgent's dispatch and disconnectDispatch, along with
extensions/initAgents: agents now react to their own connected and
disconnected events (live or synthesized) in eventDispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Drop isAgentCustomEvent: once connected and disconnected are handled,
the payload already narrows to AgentCustomEvent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
The agent list stays current through notify_agent_event and the
relist on every connection, so extensions/refresh is gone and the
firmware refresh button is disabled while aldis is not connected.

Drop registeredAgents and MoonrakerAgent, restoring findAgent's inline
type with the optional klippyDispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
…dates

Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>

# Conflicts:
#	src/components/widgets/console/ConsoleItem.vue
@pedrolamas

Copy link
Copy Markdown
Member

Hi @mjonuschat, thank you for opening this Pull Request.

I have been doing some refactoring around this area, and I do intend to merge it to Fluidd "soon"!

Today I actually tested it with my printer, and I'm not going to lie: I was holding my breath while I was doing this (SKR-2 connected via serial and flashed with Katapult)... but it worked perfectly!!

Image

This is VERY impressive!!!

@pedrolamas pedrolamas added the FR - Enhancement New feature or request label Nov 8, 2026
pedrolamas and others added 7 commits November 8, 2026 19:59
- Store the agent's Aldis.Run as-is, with pending, logRunId and
  dismissedRunId alongside; result, message and log are getters
- Reconcile status snapshots only against the same run
- Refresh ignores calls while one is in flight; retries and the
  post-run refresh fetch status directly
- Show the agent's message for relayed agent errors
- Label outdated MCUs with the agent's message
- Add isAvailable getter and move agent event predicates to util

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
A run marked lost on agent disconnect now takes any snapshot of the
same run, so a run the agent kept going after reconnecting shows as
running again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
- Return ConsoleLogEntry from formatResponse, dropping the unread
  FirmwareUpdateResponse type
- Share one isUpdatable check between the store and FirmwareStatus
- Drop api version checks made redundant by unsupportedStatus
- Freeze the stored status and run, typed as Readonly, so Vue does
  not deep-observe them
- Read statusError from state instead of a wrapper getter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
- Mark a lost run explicitly with lostRunId instead of inferring it
  from a missing result; a progress event resumes the run
- Refresh on Klippy state changes even while a refresh is in flight
- Validate the status blocker and the run id returned by update
- Label unknown transports by type and hide empty status tooltips

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
- Replace lostRunId with a runLost flag that setRunLost sets together
  with the lost run, so no commit order needs keeping
- Build the run straight from the event in applyEvent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
@pedrolamas pedrolamas added this to the 1.37.8 milestone Nov 9, 2026

@pedrolamas pedrolamas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Cursor code review — 3 findings across 2 files.

These inline comments were posted from Cursor.

Comment thread src/store/firmware/actions.ts Resolved
Comment thread src/store/firmware/mutations.ts Resolved
Comment thread src/store/firmware/actions.ts Resolved
A socket drop reset the firmware module while onAgentDisconnected
awaited, clearing the run before it could be marked lost. The module
is no longer reset on drop, as the synthesized disconnected event
already clears its live state, and the run check now runs first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.

🟡 Changes recommended

The change still has unresolved review findings—most notably the dialog close path bypasses the open setter so the parent never receives a close event, alongside a too-permissive status type guard and inconsistent state immutability in setRunStarted.

3 open issues
What changed in this PR

This PR adds web-based firmware updates to Fluidd by integrating with the aldis Moonraker agent, introducing a new Firmware Updates Settings UI + Vuex module, and extending the agent plumbing so additional agents can be supported via a registry entry. It also refactors the update log dialog to be reusable across software and firmware update flows.

Changes:

  • Add a new firmware/ Vuex module (state/actions/getters/mutations + tested helpers) driven by server.extensions.request for aldis status, runs, and live update events.
  • Add Firmware Updates UI in Settings (status chips/actions, notifications toggle, navigation entry) and document the third-party integration.
  • Generalize agent event handling (connected/disconnected + Klippy-state fanout) and make UpdatingDialog props-driven for reuse.

This pull request requires changes.
Unresolved review comments:

  • (Moderate) UpdatingDialog.vue: closing via direct invokedDialog mutation bypasses the open setter/close event; close via this.open = false or emit close instead.
  • (Moderate) firmware/helpers.ts: tighten isStatusResponse guard so host.klippy_state / host.klippy_message are validated as strings to prevent malformed payloads leaking into UI.
  • (Moderate) firmware/mutations.ts: setRunStarted assigns a mutable run (despite Readonly expectation) and does not clear runLost for a new run, risking incorrect reconciliation.
  • (Nit) firmware/mutations.ts: freeze the run payload in setRunStarted to keep state consistency with setRun/setStatus.
File Summary
src/​views/​Settings.vue Renders the Firmware Updates Settings section when available.
src/​util/​is-agent-event.ts Adds type guards for agent connected/disconnected events.
src/​typings/​aldis.d.ts Defines typings for the aldis agent API payloads.
src/​store/​types.ts Registers the firmware Vuex module type in root store typings.
src/​store/​socket/​actions.ts Fans out Klippy disconnect events to agent Klippy-state handlers.
src/​store/​server/​actions.ts Detects Klippy state changes and notifies agent handlers.
src/​store/​index.ts Registers the firmware Vuex module in the store.
src/​store/​firmware/​types.ts Defines firmware module state shape and related types.
src/​store/​firmware/​state.ts Implements firmware module initial state.
src/​store/​firmware/​mutations.ts Implements firmware module state transitions (run/status tracking).
src/​store/​firmware/​index.ts Wires up the firmware Vuex module (state/getters/actions/mutations).
src/​store/​firmware/​helpers.ts Pure helpers for run reconciliation, payload guards, and error extraction.
src/​store/​firmware/​getters.ts Derived state for availability/support/busy status and UI-facing data.
src/​store/​firmware/​actions.ts Refresh/update flows, agent event handling, notifications, reconnect behavior.
src/​store/​firmware/​__tests__/​mutations.spec.ts Unit tests for firmware mutations and state transitions.
src/​store/​firmware/​__tests__/​helpers.spec.ts Unit tests for helper logic and payload guards.
src/​store/​extensions/​actions.ts Centralizes agent event guards and adds Klippy-state fanout to agents.
src/​store/​console/​types.ts Adds ConsoleLogEntry typing for generic log rendering.
src/​store/​config/​types.ts Adds enableFirmwareNotifications UI setting.
src/​store/​config/​state.ts Defaults firmware notifications setting to disabled.
src/​locales/​en.yaml Adds i18n strings for firmware update UI and notifications.
src/​globals.ts Registers the aldis agent and adds firmware-related wait keys/hooks.
src/​components/​widgets/​console/​ConsoleItem.vue Broadens console item rendering to support generic log entries.
src/​components/​widgets/​console/​ConsoleBrowser.vue Broadens console browser to accept generic log entry lists.
src/​components/​settings/​FirmwareStatus.vue Adds per-MCU status chip/action UI component.
src/​components/​settings/​FirmwareSettings.vue Adds Firmware Updates Settings card UI and interactions.
src/​components/​layout/​AppSettingsNav.vue Adds Firmware Updates entry to the Settings navigation/sidebar.
src/​components/​common/​UpdatingDialog.vue Refactors UpdatingDialog to be props-driven and reusable across flows.
src/​App.vue Wires two UpdatingDialog instances (software + firmware) and close handling.
src/​api/​socketActions.ts Adds aldisStatus / aldisUpdate wrappers via server.extensions.request.
docs/​docs/​features/​third-party-integrations.md Documents the aldis integration and where the UI appears.
AGENTS.md Updates agent plumbing documentation to include Klippy-state dispatch behavior.

Review effort: Balanced

Comment thread src/store/firmware/helpers.ts Resolved
Comment thread src/store/firmware/mutations.ts Resolved
- UpdatingDialog closes through its open setter, so the parent's
  close handler runs when an update ends with nothing to show
- isStatusResponse checks the host fields
- setRunStarted freezes the run, like setRun

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.

🟢 Approval recommended

The change set appears broadly complete and well-scoped—adding the new firmware update UI, Vuex module wiring, and supporting tests/documentation with no explicitly recorded unresolved blockers in the provided context.

No open issues 🎉

3 resolved since last review

Review effort: Balanced

@pedrolamas
pedrolamas merged commit 569b0ee into fluidd-core:develop Nov 9, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FR - Enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants