mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-05-09 00:08:34 +02:00
[PR #1185] [Feature] Add Macro and Command Terminal #1178
Labels
No labels
A1
automated
automated
bug
bug
Closed due to inactivity
contrib
dependencies
dependencies
duplicate
enhancement
feedback
hold
invalid
Notes
P1S
pull-request
security
ThumbsUp
user-report
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/bambuddy-maziggy-1#1178
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/maziggy/bambuddy/pull/1185
Author: @Keybored02
Created: 5/1/2026
Status: 🔄 Open
Base:
dev← Head:feature/macro📝 Commits (10+)
d92cb90Add Macros66d9e82Add Terminal view372f55fImprove multiline macro handling142bb96Gcode reference creation56d175dTerminal placement rework1176c72Gcode whitelist update4aac7a5Add .cfg file supporta8867e9Macro editor updatesc571ca6Add system functions and persistent vars37f3424Merge branch 'dev' into feature/macro📊 Changes
44 files changed (+6277 additions, -8 deletions)
View changed files
➕
backend/app/api/routes/macros.py(+319 -0)📝
backend/app/api/routes/printers.py(+4 -4)📝
backend/app/api/routes/webhook.py(+45 -0)📝
backend/app/core/auth.py(+1 -0)📝
backend/app/core/config.py(+1 -0)📝
backend/app/core/database.py(+73 -0)📝
backend/app/core/permissions.py(+17 -0)📝
backend/app/main.py(+20 -0)📝
backend/app/models/__init__.py(+6 -0)📝
backend/app/models/api_key.py(+1 -0)➕
backend/app/models/macro.py(+68 -0)➕
backend/app/models/macro_var.py(+34 -0)➕
backend/app/schemas/macro.py(+109 -0)📝
backend/app/services/archive.py(+41 -0)➕
backend/app/services/gcode_whitelist.py(+116 -0)➕
backend/app/services/macro_cfg_parser.py(+199 -0)➕
backend/app/services/macro_cfg_watcher.py(+161 -0)➕
backend/app/services/macro_files.py(+83 -0)➕
backend/app/services/macro_functions.py(+221 -0)➕
backend/app/services/macro_integrations/__init__.py(+3 -0)...and 24 more files
📄 Description
Description
This PR introduces a complete macro scripting system for Bambuddy, enabling users to automate printer actions — filament drying, print control, notifications, spool assignments, and more — through Jinja2 template scripts stored as plain
.cfgfiles on disk.Related Issue
Fixes #1139
Documentation
Companion docs PRs (delete lines that don't apply):
Pick one:
Type of Change
Changes Made
File-first storage model
Macros live in Klipper-style
.cfgfiles (e.g.data/macros/my_macros.cfg) rather than as DB text blobs. A single.cfgfile can hold multiple[macro name]blocks. The DB stores only metadata (name, trigger type, cron expression, linked printer) and a foreign key to theMacroCfgFilerecord — the actual script body is always read from disk at render time.This means macros can be version-controlled, edited externally with any text editor, and shared between Bambuddy instances by copying files.
Three-layer execution pipeline
SandboxedEnvironment(no arbitrary Python execution). Context variables (printer,ams,queue,vars,assignments) are injected from live MQTT state and DB queries.is_whitelisted()checks the first token against a curated allowlist. Unknown tokens are rejected with a log warning, never silently dropped.Registry-based system commands
System commands (
AMS_DRYING,NOTIFY,WAIT,SET_VAR, etc.) are registered via a@macro_functiondecorator on plain async coroutines inmacro_integrations/*.py. The registry auto-discovers all files in that package at startup — adding a new command requires only a new decorated function, with zero changes to any other file.Each function receives a typed
FunctionContext(flags dict, printer_id, run_id, log callable) and returns aFunctionResult(ok, message, value). Thevaluefield is used by context_var functions that inject data into the Jinja2 render namespace.New files
Backend
backend/app/models/macro.pyMacroCfgFile,Macro,MacroRunSQLAlchemy 2.0 modelsbackend/app/models/macro_var.pyMacroVar— persistent key/value store for macro scriptsbackend/app/schemas/macro.pybackend/app/api/routes/macros.pybackend/app/services/macro_runner.pybackend/app/services/macro_functions.py@macro_functiondecorator, registry,FunctionContext,FunctionResultbackend/app/services/macro_files.py.cfgfiles with cross-platform path-traversal guardbackend/app/services/macro_cfg_parser.py[macro name]blocks from.cfgtext; extracts config headersbackend/app/services/macro_cfg_watcher.pybackend/app/services/gcode_whitelist.pyis_whitelisted()predicatebackend/app/services/macro_integrations/printer.pyPRINTER_PAUSE/RESUME/STOP,AMS_DRYING,WAIT_FOR_TEMPbackend/app/services/macro_integrations/notify.pyNOTIFY,WAITbackend/app/services/macro_integrations/printer_extended.pyCLEAR_HMS_ERRORS,PRINT_QUEUE_ADDbackend/app/services/macro_integrations/assignments.pyASSIGN_SPOOL,UNASSIGN_SPOOL,assignmentscontext varbackend/app/services/macro_integrations/vars.pySET_VAR,DELETE_VAR,varscontext varFrontend
frontend/src/pages/MacrosPage.tsxfrontend/src/components/CfgFileEditor.tsx.cfgfiles with syntax hints panelDetailed feature breakdown
.cfgfile format (Klipper-style)Config headers (
description:,trigger:,cron:,printer:) are parsed before the script body. Everything after the first non-header line is the Jinja2 body. Duplicate block names are caught at parse time and surfaced inMacroCfgFile.parse_error.Jinja2 context variables
printerprinter_manageramsqueuePrintQueueItemrowsvarsMacroVarrows (global + macro-scoped)assignmentsSpoolAssignmentrows for the target printerContext variables are populated by context_var functions in the registry — the same
@macro_functionmechanism, withcontext_var="vars"instead of a command name. They run eagerly before each Jinja2 render.System commands
All commands return structured
FunctionResult(ok, message). On failure,ok=Falseand the log line is prefixed witherror:so the UI can highlight it in red.Printer control (
allowed_in_embed=False— cannot run from G-code embeds):PRINTER_PAUSE / RESUME / STOP— wraps the printer client pause/resume/stop methods; distinguishes "not connected" from "command rejected by firmware"AMS_DRYING --ams=N --temp=T --duration=H— validates ams 0-3, temp 20-90C, duration 1-12h; callssend_drying_command()WAIT_FOR_TEMP --target=T [--tolerance=D] [--max_wait=S]— polls nozzle temp every 2s; validates target 0-350CNotifications / timing (
allowed_in_embed=True):NOTIFY --message="..."— dispatches through the configured notification providers; distinguishes "no providers configured" (ok, no-op) from "dispatch error"WAIT --seconds=N— async sleep; validates N > 0Extended printer (
allowed_in_embed=False):CLEAR_HMS_ERRORS— sends HMS clear command; reports "no errors to clear" vs "cleared N errors"PRINT_QUEUE_ADD --file_id=N [--plate=N]— validates file_id > 0, plate >= 1; adds item to print queueSpool assignments (
ASSIGN/UNASSIGN allowed_in_embed=False):ASSIGN_SPOOL --spool_id=N --ams=A --tray=T— upsertsSpoolAssignment, captures fingerprint color/type from live MQTT state, broadcastsspool_assignment_changedvia WebSocketUNASSIGN_SPOOL --ams=A --tray=T— deletes assignment, broadcasts same eventassignmentscontext var — injects current assignments as a list of dictsPersistent variables (
allowed_in_embed=True):SET_VAR --key=K --value=V [--ttl=S] [--scope=global|macro]— upsertsMacroVar; value is JSON-encoded (numbers, bools, lists all work); optional TTL for auto-expiryDELETE_VAR --key=K [--scope=...]— removes the varvarscontext var — merges global and macro-scoped vars; scoped vars shadow globals with the same key; expired rows are excluded at read timeExecution engine details
Log buffering (
_LogBuffer): Instead of one DB write per log line (N+1 problem), log writes are batched in memory and flushed toMacroRun.logevery 10 lines (or on explicit flush at end of run). This keeps SQLite happy during long macro runs.G-code safety (
_preflight()):GCODE_WHITELISTG0/G1with XY coordinates — XY movement viagcode_lineis unsafe on Bambu firmware becauseG91is ignored for XY, making coordinates always absolute and risking toolhead crashes. Z-only moves are allowed.G28,M104,M140, etc.) while printer is inRUNNINGstateConcurrency:
run_macro()usesasyncio.create_task()and registers the task inself._running_tasks[run_id].cancel_run(run_id)callstask.cancel(). All DB I/O uses freshasync_session()per call — no shared sessions across async tasks.Cron scheduler:
start_scheduler()launches a background asyncio task that wakes every 60s. It usescroniter.match()to check each schedule macro against the current UTC time. Alast_fired: dict[int, datetime]guard prevents double-fires within the same minute window.Sub-macro calls: The Jinja2 context includes a
run_macro("name")callable. It resolves the name to aMacroDB record, reads and renders the.cfgbody, and executes it inline within the parent run log. Acall_stack: set[str]is threaded through recursive calls; a repeated name raises an error to prevent infinite recursion.Terminal exec (
POST /macros/exec): Single-line execution for the in-app terminal. Invokes a full macro by name using therun: macro_nameprefix — this avoids the ambiguity of a macro named e.g.G28shadowing the actual G-code token. Plain G-code lines go directly through_dispatch_gcode().Path-traversal protection
macro_files._safe_path()usesPath.relative_to(macros_dir.resolve())rather than a string prefix check. The string-prefix approach broke on Windows becausePath.resolve()returns backslash-separated paths while the old code appended a forward-slash sentinel, causing all valid filenames to be incorrectly rejected with a 500 error.Permissions
Five new permissions added to the
Permissionenum and wired intoDEFAULT_GROUPS:macros:readmacros:createmacros:updatemacros:deletemacros:runOperators can read the macro list and trigger runs but cannot create or modify macros.
Terminal session inheritance fix
Opening the G-code terminal from the Printers page previously created an isolated browsing context (
window.openwithnoopener) that had no auth token, forcing the user to log in again. Fixed by appending?token=<current_token>to the popup URL —AuthContextalready reads and applies this query parameter on load.Test plan
macro_cfg_files,macros,macro_runs,macro_varstables createdGET /api/v1/macros/gcode-whitelistreturns a sorted list of stringsGET /api/v1/macros/functionsreturns all registered system commands with args.cfgfile via UI; verify file appears on disk atdata/macros/*.cfgG28; verifyMacroRun.status = successand log contains the dispatched lineG999); verify it is blocked with anerror:log lineAMS_DRYING --ams=0 --temp=65 --duration=4; verifysend_drying_command()called in logsWAIT --seconds=2; verify ~2s delay visible in run logSET_VAR --key=x --value=42then a macro using{{ vars.x }}; verify value injectedASSIGN_SPOOL --spool_id=1 --ams=0 --tray=0; verifySpoolAssignmentrow and WebSocket eventPOST /macros/runs/{id}/cancel; verify task cancelled and status updated* * * * *; wait 60s; verify auto-run createdrun: macro_nameto invoke a macro; typeG28directly to dispatch G-code/macrosin frontend; create, edit, run a macro end-to-end; watch run history poll update../../../etc/passwd; verify 400 rejectedScreenshots
Testing
Checklist
Additional Notes
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.