Skip to content
Merged
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
20 changes: 18 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
**Printer Control** is a Macro Deck 3 out-of-process plugin (Windows, macOS and Linux) that connects Macro
Deck to 3D printers through the software that runs them: live printer state as variables and events,
printer control as actions, a print status widget, and the printer webcam as a video stream (SDK
3.0.0-beta.15). Each kind of printer software is a backend; OctoPrint is the only one shipped. A Moonraker
backend waits on the `feature/moonraker` branch until it has been tested on a real Klipper printer.
3.0.0-beta.15). Each kind of printer software is a backend: OctoPrint and Moonraker.
[README.md](README.md) is the user-facing guide; this file is the rule set for changing the code. Keep it
current when a rule stops matching reality.

Expand All @@ -26,6 +25,8 @@ src/PrinterControl/
IPrinterSetup.cs a backend's own setup steps after the address (signing in)
OctoPrint/ REST client, push frame parsing, sign-in (OctoPrintSetup: application
keys or a pasted key), OctoPrintConnection, GPIO Control outputs
Moonraker/ JSON-RPC websocket client, Klipper object state (MoonrakerStatus), setup
(trusted client or API key), MoonrakerConnection, power device outputs
ConfigFlow/ PrinterConfigFlow (name, address and webcam, recognizes the printer
software, then runs its IPrinterSetup), ConfigKeys, PrinterConfigReader
Actions/ PrinterAction (printer picker, error mapping, ConfirmAsync) + actions
Expand All @@ -36,6 +37,7 @@ src/PrinterControl/
tests/PrinterControl.Tests/
Support/FakeOctoPrint.cs a Kestrel fake of OctoPrint's REST API, application keys, GPIO Control
and push socket
Support/FakeMoonraker.cs a Kestrel fake of Moonraker's HTTP info endpoints and JSON-RPC websocket
Support/IFakePrinterServer.cs what the backend contract needs from a fake, FakeServerHost, NotAPrinterServer
Backends/BackendContract.cs the tests every backend passes against its fake; AllFakeServers
Backends/Example/ a minimal polling backend, its fake and contract tests: the template
Expand Down Expand Up @@ -97,6 +99,20 @@ backend type leaks into actions, variables, events or the UI.
- OctoPrint reports a cancel as `PrintCancelled` and again as `PrintFailed` with reason `cancelled`; only
the first becomes `print-cancelled` (`PushMessages.ReadEvent`).

**Moonraker.** Written against the documented API (https://moonraker.readthedocs.io) and tested on real
Klipper printers by a closed tester group.

- Everything after setup goes over the websocket (`MoonrakerRpc`); the API key is in the handshake.
Klippy's state comes from `server.info` and the `notify_klippy_*` notifications, not from the `webhooks`
object, which Moonraker documents as unreliable for that. While Klippy is ready, the printer objects come
from a subscription; `notify_status_update` only carries changed fields (`MoonrakerStatus.Merge`).
- `printer.gcode.script` answers only once the G-code ran, so G-code waits 1 s for a refusal and then
answers sent (`CallWithoutWaitingAsync`); a homing move would otherwise time out.
- Klipper has no job events: they follow from `print_stats.state` changing (`MoonrakerConnection.PrintEvent`).
- Heaters: `extruder`, `extruder1`, `heater_bed`, and a chamber as `heater_generic chamber` (settable) or
`temperature_sensor chamber` (read only) (`MoonrakerHeaters`).
- Power devices have no documented change notification, so they are read every 2 s while there are any.

**Actions.**

- Every action takes the `printer` dynamic choice first; empty means the only printer.
Expand Down
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Printer Control talks to the software that runs your printer. Supported today:
| Printer software | Notes |
| --- | --- |
| [OctoPrint](https://octoprint.org) | Everything below. Lights need the GPIO Control plugin. |
| [Moonraker](https://moonraker.readthedocs.io) (Klipper with Mainsail or Fluidd) | Everything below except connecting and disconnecting the printer, which Klipper does not do. Temperature presets are not read. Lights are Moonraker's power devices. *Start* and *Restart* print the last loaded file again, and *Print a file* can only start a print, not just load the file. |

The plugin is built so more printer software can be added; see
[Adding printer software](#adding-printer-software). This is an unofficial community plugin, not made or
Expand All @@ -22,14 +23,21 @@ Any number of printers can be set up, each with its own server.
1. Open the Printer Control integration in Macro Deck and add a printer.
2. Enter the address you open your printer's web interface at, for example `http://octopi.local`.
Printer Control recognizes the printer software there by itself.
3. Choose **Approve in OctoPrint**. OctoPrint shows an "Access Request" dialog for Macro Deck in its web
interface; choose **Allow** there, then continue in Macro Deck. If the application keys plugin is
turned off in your OctoPrint, choose **Paste an API key** instead and create a key in OctoPrint under
*Settings > Application Keys*.

Leave the name empty to use the printer's own name, the name set in OctoPrint (*Settings > Appearance*).
The advanced settings of the address step take a different webcam stream; those of OctoPrint's sign-in
step limit the approval to one OctoPrint user.
3. Sign in, depending on the printer software:
- **OctoPrint:** choose **Approve in OctoPrint**. OctoPrint shows an "Access Request" dialog for Macro
Deck in its web interface; choose **Allow** there, then continue in Macro Deck. If the application
keys plugin is turned off in your OctoPrint, choose **Paste an API key** instead and create a key in
OctoPrint under *Settings > Application Keys*.
- **Moonraker:** nothing to do when this computer is one of Moonraker's `trusted_clients` (in
`moonraker.conf`, often your whole local network). Otherwise paste Moonraker's API key, which
`curl http://localhost:7125/access/api_key` shows on the printer's host.

Leave the name empty to use the printer's own name: the name set in OctoPrint (*Settings > Appearance*),
or the host name of the machine running Klipper. The advanced settings of the address step take a
different webcam stream; those of OctoPrint's sign-in step limit the approval to one OctoPrint user.

Moonraker's webcam is the first enabled MJPEG webcam it lists; other kinds of streams (WebRTC, HLS)
cannot be shown, so enter an MJPEG stream address in the advanced settings for those.

## Widgets

Expand Down Expand Up @@ -115,14 +123,21 @@ widget and anywhere else Macro Deck shows video.
## Privacy

The plugin only talks to the printer servers you set up. While you add a printer, it asks the address you
entered which printer software runs there (`/api/version`), without credentials. After that:
entered which printer software runs there (`/api/version` for OctoPrint, `/server/info` for Moonraker),
without credentials. For OctoPrint:

- the OctoPrint REST API (`/api/...`) to read settings and files and to send your commands,
- OctoPrint's push socket (`/sockjs/websocket`) for live state,
- the application keys plugin (`/plugin/appkeys/...`) while you approve Macro Deck,
- the GPIO Control plugin (`/api/plugin/gpiocontrol`), every 2 seconds while OctoPrint lists GPIO outputs, and
when you use the switch output action.

For Moonraker:

- `/server/info` and `/printer/info` while you add the printer,
- Moonraker's websocket (`/websocket`) for everything else: live state, files, webcams, your commands, and
its power devices every 2 seconds while it has any.

The webcam is loaded by Macro Deck itself from the webcam address and relayed to your deck devices; it
does not pass through the plugin. The API key is stored in Macro Deck's encrypted secret store. The plugin
writes no files of its own and sends nothing anywhere else.
Expand Down
2 changes: 2 additions & 0 deletions src/PrinterControl/Backends/IPrinterBackend.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MacroDeck.Localization;
using Microsoft.Extensions.DependencyInjection;
using PrinterControl.Backends.Moonraker;
using PrinterControl.Backends.OctoPrint;
using PrinterControl.Core;
using Serilog;
Expand Down Expand Up @@ -37,6 +38,7 @@ internal static class PrinterBackendRegistration
public static IServiceCollection AddPrinterBackends(this IServiceCollection services)
{
services.AddSingleton<IPrinterBackend, OctoPrintBackend>();
services.AddSingleton<IPrinterBackend, MoonrakerBackend>();
services.AddSingleton<PrinterBackends>();
services.AddSingleton<PrinterRegistry>();
return services;
Expand Down
28 changes: 28 additions & 0 deletions src/PrinterControl/Backends/Moonraker/MoonrakerBackend.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using MacroDeck.Localization;
using PrinterControl.Core;
using Serilog;

namespace PrinterControl.Backends.Moonraker;

// Klipper's API server behind Mainsail and Fluidd.
// https://moonraker.readthedocs.io/en/latest/external_api/introduction/
internal sealed class MoonrakerBackend(IHttpClientFactory httpClients) : IPrinterBackend
{
public const string BackendId = "moonraker";

public string Id => BackendId;

public LocalizedText Name => LocalizedText.FromLiteral("Moonraker (Mainsail, Fluidd)");

// A trusted client needs no key.
public bool RequiresApiKey => false;

public async Task<bool> DetectAsync(Uri baseUri, CancellationToken cancellationToken) =>
MoonrakerHttp.IsMoonraker(await Http(baseUri).GetAsync("server/info", null, cancellationToken));

public IPrinterSetup CreateSetup(Uri baseUri, bool editing) => new MoonrakerSetup(Http(baseUri), editing);

public PrinterConnection Connect(PrinterConfig config, HttpClient http, ILogger logger) => new MoonrakerConnection(config, logger);

private MoonrakerHttp Http(Uri baseUri) => new(httpClients.CreateClient(PrinterRegistry.HttpClientName), baseUri);
}
Loading
Loading