The ACE Pro is a multi-material filament management system for Klipper-based 3D printers. This implementation supports multiple ACE Pro units.
┌─────────────────────────────────────────────────────────────────┐
│ Klipper Printer │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ AceManager │ │
│ │ - Coordinates multiple ACE instances │ │
│ │ - Manages global filament position state │ │
│ │ - Handles runout detection & monitoring │ │
│ │ │ Sensors: toolhead_sensor, return_module_sensor │ │
│ │ - Orchestrates tool changes (T0-Tn) │ │
│ └──┬────────────────────────────────────────────┬────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ AceInstance[0] │ │ AceInstance[1] │ │
│ │ Tools: T0-T3 │ │ Tools: T4-T7 │ │
│ │ ┌────────────────┐ │ │ ┌────────────────┐ │ │
│ │ │ Slot 0: PLA │ │ │ │ Slot 0: PETG │ │ │
│ │ │ Slot 1: ABS │ │ │ │ Slot 1: PLA │ │ │
│ │ │ Slot 2: PETG │ │ │ │ Slot 2: PLA │ │ │
│ │ │ Slot 3: Empty │ │ │ │ Slot 3: Nylon │ │ │
│ │ └────────────────┘ │ │ └────────────────┘ │ │
│ │ Serial: /dev/ttyACM0 | │ Serial: /dev/ttyACM1 │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ EndlessSpool Handler │ │
│ │ - Material/color (or "next") matching for runout swaps │ │
│ │ - Executes automatic tool swap when triggered │ │
│ │ - No sensor polling (RunoutMonitor handles detection) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Primary Responsibilities:
- One AceManager orchestrates all ACE instances
- Tool Mapping: Maps global tool indices (T0-T) to instance/slot pairs
- Instance 0: T0-T3 (slots 0-3)
- Instance 1: T4-T7 (slots 0-3)
- Instance 2: T8-T11 (slots 0-3)
- Instance 3: T12-T15 (slots 0-3)
- Instance N: ...
- Global State Management:
ace_filament_pos: Tracks filament position ("bowden", "splitter", "toolhead", "nozzle")
ace_current_index: Currently active tool (-1 = none)ace_endless_spool_enabled: Endless spool active/inactiveace_global_enabled: ACE system master enable- Sensor Management: Manages shared sensors (toolhead, optional RDM), supporting both
filament_switch_sensorandfilament_trackerviaFilamentTrackerAdapter - Smart Operations:
smart_unload(),smart_load()with sensor-aware fallback - Tool Change Orchestration:
perform_tool_change()coordinates unload/load across instances - Runout Detection: Creates
RunoutMonitorto poll sensors (50ms interval) and raise events
Toolchange Guard Decorator:
@toolchange_in_progress_guard
def perform_tool_change(self, current_tool, target_tool, is_endless_spool=False):
# Protected method - runout detection blocked during execution
...The decorator uses a depth counter (_toolchange_depth) to support nested toolchange calls. toolchange_in_progress remains True until the outermost call returns. The counter ensures the flag is safely cleared via finally even if an exception is raised at any nesting level.
Key Methods:
# Core Operations
smart_unload(tool_index) # Intelligent unload with fallback strategies
smart_load() # Load all non-empty slots to RDM sensor
perform_tool_change(current, target) # Complete tool change sequence
execute_coordinated_retraction(...) # Synchronized ACE + extruder retraction
# Startup Validation
_validate_startup_tool_state() # Clear stale persisted tool state if sensors show clear;
# currently disabled on startup (timing-sensitive, pending rewrite)
# Sensor Management
get_switch_state(sensor_name) # Query sensor state (debounced)
get_instant_switch_state(sensor_name) # Query sensor state without debounce (instant read)
is_filament_path_free() # Check if bowden path is clear (toolhead + RDM, debounced)
is_filament_path_free_instant() # Check if bowden path is clear (instant, no debounce)
has_rdm_sensor() # Check if RDM sensor is configured
# Toolhead Preparation
prepare_toolhead_for_filament_retraction(tool_index) # Heat and prepare for unload
check_and_wait_for_spool_ready(tool) # Wait for spool motor stability (with timeout)
# State Management
set_and_save_variable(varname, value) # Set and persist variable (obeys persistence_mode)
update_ace_support_active_state() # Sync ACE enable/disable state from output pin
# Runout Handling (via RunoutMonitor)
runout_monitor.start_monitoring() # Begin sensor polling
runout_monitor.stop_monitoring() # Stop sensor polling
set_runout_detection_active(active) # Enable/disable detection
# Connection Health Monitoring
_check_connection_health(eventtime) # Check all instances for stable connections
_handle_connection_issue(instances, time) # Pause print (if printing) and show dialog
_show_connection_issue_dialog(instances, is_printing) # Mainsail dialog with details
_close_connection_dialog() # Close dialog when connection restoredPrimary Responsibilities:
- Single Physical Unit: Manages one ACE Pro hardware unit (4 slots)
- Local Operations: Feed, retract, feed assist for its 4 slots
- Serial Communication: Via AceSerialManager (request/response protocol)
- Inventory Tracking: Per-slot metadata (material, color, temp, status)
- Toolhead Integration: Extruder moves, filament feeding to nozzle
Key Attributes:
instance_num: int # 0, 1, 2, 3...
tool_offset: int # First tool: 0, 4, 8, 12...
SLOT_COUNT = 4 # Fixed per ACE unit
inventory: List[Dict] # Per-slot: material, color, temp, status
serial_mgr: AceSerialManager # Communication handler
# Defaults for non-RFID spools (applied when slot becomes ready with no metadata)
DEFAULT_MATERIAL = "Unknown" # Won't match in endless spool exact/material modes
DEFAULT_COLOR = [0, 0, 0] # Black - default empty slot color
DEFAULT_TEMP = 225 # Safe middle-ground temperatureKey Methods:
# Feed/Retract Operations
_feed(slot, length, speed, callback) # Feed filament from slot (async)
_retract(slot, length, speed, on_retract_started, on_wait_for_ready)
# Retract filament to slot with callbacks
_stop_feed(slot) # Stop active feed operation
_stop_retract(slot) # Stop active retract operation
_feed_sync(slot, length, speed) # Synchronous feed with blocking wait
# Toolhead Operations
_feed_filament_into_toolhead(tool) # Load filament to nozzle (multi-stage)
_feed_filament_to_verification_sensor(slot) # Feed to RDM/toolhead sensor only
_smart_unload_slot(slot, length) # Unload with sensor validation and retry
rmd_triggered_unload_slot(...) # RDM-triggered unload with coordinated retraction
# Feed Assist
_enable_feed_assist(slot) # Auto-push filament on detection
_disable_feed_assist(slot) # Disable auto-push
_update_feed_assist(slot) # Update active feed assist slot
_get_current_feed_assist_index() # Query current feed assist slot
_on_ace_connect() # Mark feed assist for deferred restoration
_maybe_restore_pending_feed_assist() # Restore after first successful heartbeat
# Sensor Monitoring (New in 2024-12)
_make_sensor_trigger_monitor(sensor_type) # Create sensor state change monitor
# Returns: monitor function with timing data
# Serial Communication
send_request(request, callback) # Queue normal request
send_high_prio_request(request, callback) # Queue priority request
wait_ready(on_wait_cycle) # Block until ACE is ready (with optional callback)
is_ready() # Check if ACE is ready (non-blocking)
# Property Accessors
@property
manager # Get AceManager for this instance (via registry)
# Status & Inventory
get_status(eventtime) # Get ACE hardware status (copy)
reset_persistent_inventory() # Clear all slot metadata
reset_feed_assist_state() # Reset feed assist to disabled
# Utility
_change_retract_speed(slot, speed) # Dynamically adjust retract speed
_change_feed_speed(slot, speed) # Dynamically adjust feed speed
_wait_for_condition(condition_fn, timeout) # Generic blocking wait helper
dwell(delay, verbose) # Reactor-based sleep with timing info
_extruder_move(length, speed, wait) # Extruder motion via toolheadSensor Trigger Monitor (Advanced Feature):
The _make_sensor_trigger_monitor() creates a closure-based monitor for tracking sensor state changes during operations:
monitor = instance._make_sensor_trigger_monitor(SENSOR_TOOLHEAD)
# Use in retract operation
instance._retract(slot, length, speed, on_wait_for_ready=monitor)
# Query results
timing = monitor.get_timing() # Time to sensor trigger (seconds)
count = monitor.get_call_count() # Number of sensor polls
state = monitor.state_data # Raw state dataThis enables precise timing measurements for:
- Retraction efficiency analysis
- Detecting stuck filament (late sensor triggers)
- Optimizing movement speeds
- Diagnosing mechanical issues
Primary Responsibilities:
- Material Matching: Find matches across all slots
- Match Modes:
"exact"(default): Match material AND color"material": Match material only, ignore color"next": Take the first ready spool, ignoring material/color
- Automatic Swap: Execute tool change on runout (pause → swap → resume)
- Intelligent Fallback: Retry with next match if feed fails
- User Prompts: Show interactive Mainsail prompts on failures
Architecture Note:
Runout detection and pausing are handled by RunoutMonitor.
EndlessSpool focuses purely on:
- Finding matches (based on match mode)
- Executing swaps (tool changes)
- Handling swap failures with user feedback
Key Methods:
get_match_mode() # Get match mode ("exact", "material", "next") from saved_variables
find_exact_match(current_tool) # Search for a match across all slots (mode-aware search)
execute_swap(from_tool, to_tool) # Execute automatic tool swap with fallback
# Coordinates:
# - Pause print (if not already paused)
# - Mark old slot empty (status="empty", preserves
# color/material/temp, clears RFID fields)
# - Execute tool change (skip unload)
# - 1.5x purge on new tool when endless spool
# - Resume print automatically
# Max attempts: 3 (retry with next match on fail)
_show_swap_failed_prompt(...) # User prompt on failed swap
get_status() # Return endless spool status dict (currently empty)Match Mode Behavior:
Mode | Material | Color/RGB | Example
─────────┼──────────┼───────────┼────────────────────────
"exact" | Must | Must | PLA + RGB(255,0,0) → match only identical red PLA
"material"| Must | Any | PLA + any RGB → match any PLA regardless of color
"next" | Any | Any | First ready spool, ignore material and RGB
- Unknown materials will NEVER match each other
- Non-RFID spools without manual labels default to
material="Unknown" - Even if two slots both have
material="Unknown", they will NOT match - Rationale: "Unknown" means we don't know the actual material type
- Could be PLA (210°C), PETG (240°C), ABS (250°C), TPU (230°C), etc.
- Automatic swapping risks: wrong temperature, incompatible materials, print failure
- Solution: Always label non-RFID spools explicitly using
ACE_SET_SLOT - Example:
ACE_SET_SLOT T=0 MATERIAL="PLA" COLOR=RED TEMP=210 ACE_SET_SLOT T=4 MATERIAL="PLA" COLOR=BLUE TEMP=210 # Now "PLA" → "PLA" can safely match
Color Matching Details:
- In "exact" mode: RGB values must match exactly (e.g., R=255,G=0,B=0)
- In "material" mode: RGB ignored, only material name compared
- In "next" mode: Both material and RGB ignored
- RGB preserved during slot empty transitions for auto-restore
- RFID-tagged spools auto-update RGB when inserted
Swap Failure Retry Logic:
1. Try to feed from candidate_tool
2. On failure → Smart unload failed tool
3. Find next matching spool
4. Retry swap with new candidate
5. Max 3 attempts before giving up
6. Show prompt for user intervention
Primary Responsibilities:
- Filament Runout Detection: Monitor toolhead sensor during printing
- State Change Detection: Detect sensor present → absent transitions
- Print State Tracking: Know when printing is active (vs idle/paused)
- Runout Coordination: Trigger endless spool or show prompts
- Print Start Baseline: Re-initialize sensor baseline when print starts
- Optional Tangle Detection: Compare extruder motion vs RDM encoder to detect stuck spools
Architecture: RunoutMonitor is purely an observer - it does NOT change state directly. Instead:
- Detects runout events
- Calls
EndlessSpool.find_exact_match()to find a swap candidate - If match found: Calls
EndlessSpool.execute_swap() - If no match: Shows user prompt and pauses
Key Methods:
start_monitoring() # Start runout detection monitor loop
# Registers with reactor for periodic polling (50ms)
stop_monitoring() # Stop runout monitoring
# Unregisters timer, stops polling
set_detection_active(active: bool) # Enable/disable runout detection
# Can disable during maintenance
# Returns: new active state
_monitor_runout(eventtime) # Main monitoring loop (50ms interval)
# Responsibilities:
# - Get current print state (idle, paused, printing, etc.)
# - Get current tool index from saved_variables
# - Get toolhead sensor state from manager
# - Detect state changes (present → absent)
# - Guard: skip if toolchange in progress
# - Guard: skip if detection disabled
# - Detect print start, re-initialize baseline
# - On runout: call _handle_runout_detected()
# Returns: next callback time (eventtime + interval)
_show_runout_prompt(tool_index, instance_num, local_slot, material, color)
# Show Mainsail prompt for runout
# Displays: tool, instance, slot, material, color
# Buttons: RESUME, CANCEL_PRINT
_handle_runout_detected(tool_index) # Process detected runout
# 1. Set runout_handling_in_progress flag
# 2. Pause print
# 3. Check endless spool enabled?
# 4a. If disabled: Show runout prompt, wait for user
# 4b. If enabled: Find match, auto-swap, resume
# 5. Clear handling flag
_pause_for_runout() # Execute PAUSE command via gcodeState Tracking:
prev_toolhead_sensor_state # Last known sensor state (for transition detection)
last_printing_active # Was printing active last cycle?
last_print_state # Last raw print state ("idle", "printing", "paused")
runout_detection_active # Is runout detection enabled?
runout_handling_in_progress # Are we handling a runout now?
monitor_debug_counter # For periodic debug logging (~15 min interval)
runout_debounce_count # Consecutive absent readings required (config, default 3)
_runout_false_count # Current consecutive absent reading counterRunout Detection Logic:
Print State Check
├─ Not printing? → Skip detection
├─ Toolchange in progress? → Skip detection (guard)
├─ Detection disabled? → Skip (guard)
└─ Printing? Continue...
Sensor State Check
├─ First cycle (print just started)?
│ └─ Initialize baseline (record current sensor state)
├─ Sensor state same as previous?
│ └─ No transition detected → Skip
└─ Sensor state CHANGED?
├─ Is new state = TRIGGERED (filament present)?
│ └─ Reset debounce counter → Reset baseline → Skip
└─ Is new state = CLEAR (filament absent)?
├─ Increment debounce counter (_runout_false_count)
├─ Counter < runout_debounce_count?
│ └─ Not yet confirmed → Keep prev as True → Poll again (50ms)
└─ Counter >= runout_debounce_count?
└─ CONFIRMED RUNOUT! Reset counter → Call _handle_runout_detected()
Debounce:
The sensor reads raw (undebounced) filament_present from Klipper. To filter
transient glitches, runout_debounce_count consecutive absent readings are
required before confirming a runout (default 1 = no debounce; e.g. 3 would give
~150ms at the 50ms poll interval). The counter resets to 0 whenever the sensor
reads present again, or on any baseline reset (pause, stop, no active tool).
Print Start Detection:
- Detects:
is_printing=Trueandwas_printing_active=False - Action: Re-initialize sensor baseline to current state
- Purpose: Prevent false runout detection if print starts with wrong baseline
Tangle Detection (optional):
- Enabled via
[ace] tangle_detectionwith thresholdtangle_detection_length(default 15mm) - Every 0.25s compares extruder motion vs RDM encoder pulses while sensors still show filament
- If extruder moves beyond the threshold with no encoder movement, declares a spool tangle for intervention
Primary Responsibilities:
- Serial Communication: Connect/disconnect to ACE Pro hardware
- Request/Response Queue: Sliding window protocol (4 concurrent requests)
- CRC Validation: Frame integrity checking
- Port Detection: Automatic USB port discovery by topology
- Heartbeat: Periodic status updates (1 Hz)
Protocol:
- Binary frames with CRC-16
- Request ID tracking for callback dispatch (never resets on reconnect)
- High-priority queue for time-sensitive operations
- 5-second timeout with elapsed time logging
- Unsolicited messages logged with response ID and current request ID
Request ID Behavior:
- IDs start at 0 and increment indefinitely (no wraparound)
- IDs never reset on reconnect to prevent collisions with pending responses
- Callbacks registered per ID, dispatched on response arrival
Timeout Handling:
- Default timeout: 5.0 seconds (configurable via
timeout_s) - On timeout: Log "Request ID={rid} TIMEOUT after {elapsed:.1f}s"
- Callback invoked with
response=Noneto signal failure - In-flight request removed from tracking
Unsolicited Message Handling:
- Responses without matching callback logged as "UNSOLICITED"
- Log format: "UNSOLICITED (ID={response_id}, current_id={self._request_id}): {json}"
- Helps diagnose timeout vs late-arrival issues
- Not an error - ACE may respond slower than timeout window
Protocol Configuration:
DEFAULT_TIMEOUT_S = 5.0 # Request timeout
# ACE devices can take several seconds to respond
# Timeout logged with elapsed time
WINDOW_SIZE = 4 # Max concurrent in-flight requests
QUEUE_MAXSIZE = 1024 # Request queue sizeKey Methods:
# Connection Management
connect(port, baud) # Establish serial connection
# Flushes I/O buffers on connect
connect_to_ace(baud, delay) # Connect with delayed initialization
auto_connect(instance, baud) # Auto-detect and connect to ACE by instance
reconnect(delay) # Reconnect after disconnect
disconnect() # Close serial connection
is_connected() # Check connection status
# Port Detection
find_com_port(device_name, instance) # Auto-detect ACE port by USB topology
# Request Management
send_request(request, callback) # Queue normal request
send_high_prio_request(req, cb) # Queue priority request (skip queue)
has_pending_requests() # Check if requests are queued
get_pending_request() # Get next request from queue
clear_queues() # Clear all pending requests
# Heartbeat & Status
set_heartbeat_callback(callback) # Register status update callback
set_on_connect_callback(callback) # Register callback for successful (re)connection
start_heartbeat() # Start periodic status requests (1Hz)
stop_heartbeat() # Stop heartbeat
_send_heartbeat_request() # Internal heartbeat implementation
# Connection Stability
is_connection_stable() # Check if connected and not in reconnect loop
get_connection_status() # Get detailed status dict:
# connected: bool - currently connected
# stable: bool - connected 30s+ and <6 reconnects in 180s
# recent_reconnects: int - reconnects in last 180s
# time_connected: float - seconds since last connect
# Connection stability ensures robust operation:
# - Feed assist restoration deferred until first successful heartbeat
# - This prevents send failures during initial connection negotiation
# - Reconnect timestamps only track actual failed attempts (not initial connection)
# Stability Constants (in __init__):
# INSTABILITY_WINDOW = 180.0 # Look at reconnects in last 3 minutes
# INSTABILITY_THRESHOLD = 6 # 6+ reconnects in window = unstable
# STABILITY_GRACE_PERIOD = 30.0 # Must stay connected 30s to be "stable"
# RECONNECT_BACKOFF_MIN = 5.0 # Initial retry delay
# RECONNECT_BACKOFF_MAX = 30.0 # Maximum retry delay (cyclic)
# RECONNECT_BACKOFF_FACTOR = 1.5 # Multiply delay on each failure
# Protocol & Frame Handling
_calc_crc(buffer) # Calculate CRC-16 for frame
_send_frame(request) # Send binary frame with CRC
_reader(eventtime) # Timer callback: read frames, parse, dispatch
# Logs unsolicited messages with response ID and current_id
_writer(eventtime) # Timer callback: send requests, handle timeouts
# Timeout logging: "Request ID={rid} TIMEOUT after {elapsed:.1f}s"
dispatch_response(response) # Route response to callback
# Returns (callback, was_solicited) tuple
# ACE Enable/Disable Support
enable_ace_pro() # Enable reconnection attempts
disable_ace_pro() # Disable reconnection attempts
is_ace_pro_enabled() # Check if ACE Pro is enabledPrimary Responsibilities:
- Single access point for all
saved_variables.cfgreads and writes - Deferred-flush strategy:
set()updates RAM and marks dirty; disk write is deferred untilflush() - Configurable persistence:
set_and_save()obeyspersistence_modedeferred(default): behaves likeset()(dirty-only untilflush())immediate: writes to disk right away (legacy behaviour)
- Type-safe serialisation: handles
bool,str,dict/list,int/floatwith correct KlipperSAVE_VARIABLEformatting
Design Rationale:
Using set() in time-critical paths (toolchanges, mid-print callbacks) avoids blocking
Klipper's single-threaded reactor with synchronous configparser.write().
set_and_save() defaults to the same deferred behaviour (safer mid-print) unless
persistence_mode=immediate is set in config. flush() is called at safe moments
(print end, disconnect) to persist all dirty variables.
Key Methods:
# Read
get(varname, default=None) # Read a variable (always fresh from Klipper)
get_all() # Return full variables dict (live reference)
# Write — in-memory only (deferred persist)
set(varname, value) # Update in RAM, mark dirty; disk write deferred to flush()
# Write — in-memory + mode-controlled disk write
set_and_save(varname, value) # Update RAM and either defer or write immediately based on
# persistence_mode (default deferred, immediate if configured)
# Persist dirty variables
flush() # Write all dirty variables to disk; clears dirty set
# Safe to call when nothing is dirty (no-op)
# Property
has_pending # True if any dirty variables await flushingUsage Pattern:
state = PersistentState(printer, gcode)
# Read (always fresh)
tool = state.get("ace_current_index", -1)
# In-memory + deferred (time-critical paths: toolchanges, mid-print)
state.set("ace_filament_pos", "bowden")
# In-memory + optional immediate disk (depends on persistence_mode)
state.set_and_save("ace_current_index", 2)
# Persist all deferred writes (e.g. at print end or disconnect)
state.flush()Where flushed:
_handle_disconnect()in AceManager — on Klipper shutdownACE_FLUSHgcode command — on user request_flush_if_idle()timer callback — background idle-time flush
Global State & Constants:
# Filament Position States
FILAMENT_STATE_BOWDEN = "bowden" # In bowden tube before RDM/4-in-1 splitter (unloaded)
FILAMENT_STATE_SPLITTER = "splitter" # In RDM, so possible loaded in splitter
FILAMENT_STATE_TOOLHEAD = "toolhead" # At toolhead sensor
FILAMENT_STATE_NOZZLE = "nozzle" # In hotend/nozzle
# Sensor Names
SENSOR_TOOLHEAD = 'toolhead_sensor'
SENSOR_RDM = 'return_module'
# Slots per ACE unit (fixed)
SLOTS_PER_ACE = 4
# Retry configuration for unload/load operations
UNLOAD_RETRY_ATTEMPTS = 3 # Number of retry attempts for unload
UNLOAD_RETRY_DELAY = 0.5 # Seconds between retry attempts
UNLOAD_INITIAL_LENGTH = 50 # mm for first retract attempt
UNLOAD_SPEED_MULTIPLIERS = [1.0, 0.7, 0.4] # Speed scale factor per retry attempt
# Max retries for ACE feed/retract operations
MAX_RETRIES = 6
# RFID hardware state codes (from ACE status responses)
RFID_STATE_NO_INFO = 0 # No RFID tag / information absent
RFID_STATE_FAILED = 1 # Tag detection failed
RFID_STATE_IDENTIFIED = 2 # Tag identified successfully
RFID_STATE_IDENTIFYING = 3 # Identification currently in progress
RFID_INVENTORY_SYNC_ENABLED = True # Default: auto-sync RFID data to slot inventory
# Registry (populated at runtime)
ACE_INSTANCES = {} # instance_num → AceInstance
INSTANCE_MANAGERS = {} # instance_num → AceManager
# Runtime globals for purge override (None = use per-instance config)
GLOBAL_PURGE_LENGTH = None # Override purge length globally (mm)
GLOBAL_PURGE_SPEED = None # Override purge speed globally (mm/min)
# Per-instance overridable config parameter names (support "value,inst:override" syntax)
OVERRIDABLE_PARAMS = [
"feed_speed", "retract_speed", "total_max_feeding_length",
"toolchange_load_length", "incremental_feeding_length",
"incremental_feeding_speed", "heartbeat_interval", "max_dryer_temperature",
]Helper Functions:
# Tool Mapping
get_tool_offset(instance_num) # → instance_num * 4
get_instance_from_tool(tool_index) # T7 → instance 1
get_local_slot(tool_index, instance) # T7, instance 1 → slot 3
get_ace_instance_and_slot_for_tool(tool) # T7 → (instance_obj, slot 3)
# Configuration Parsing
parse_instance_number(name) # "ace 2" → 2
parse_instance_config(config_value, instance, param) # "60,1:80" → 80 for instance 1
# Supports per-instance overrides
# Inventory Management
create_empty_inventory_slot() # Create empty slot dict
create_inventory(slot_count) # Create full inventory array
create_status_dict(slot_count) # Create ACE status dictKey Config Options (read by read_ace_config()):
| Key | Default | Description |
|---|---|---|
ace_count |
1 | Number of ACE Pro units |
baud |
115200 | Serial baud rate |
parkposition_to_toolhead_length |
1000 | Distance park → nozzle (mm) |
parkposition_to_rdm_length |
150 | Distance park → RDM (mm) |
toolhead_retraction_speed |
10 | Retraction speed at toolhead (mm/s) |
toolhead_retraction_length |
40 | Retraction length at toolhead (mm) |
toolhead_full_purge_length |
22 | Purge length for full load (mm) |
toolhead_slow_loading_speed |
5 | Slow feed speed near sensor (mm/s) |
extruder_feeding_length |
1 | Extruder shove length (mm) |
extruder_feeding_speed |
5 | Extruder shove speed (mm/s) |
default_color_change_purge_length |
50 | Default purge length for color change (mm) |
default_color_change_purge_speed |
400 | Default purge speed (mm/min) |
purge_max_chunk_length |
300 | Max chunk size per purge command (mm) |
pre_cut_retract_length |
2 | Safety retract before cutter (mm) |
timeout_multiplier |
2 | Multiplier applied to ACE request timeouts |
rfid_inventory_sync_enabled |
True | Auto-sync RFID data to inventory |
rfid_temp_mode |
"average" |
RFID temp calculation: "average", "min", or "max" |
feed_assist_active_after_ace_connect |
True | Restore feed assist after reconnect |
runout_debounce_count |
1 | Consecutive absent reads before confirming runout |
tangle_detection |
False | Enable encoder-based tangle detection |
tangle_detection_length |
15.0 | Extruder distance (mm) without encoder motion → tangle |
ace_connection_supervision |
True | Monitor connections; pause and alert on instability |
moonraker_lane_sync_enabled |
True | Sync slot metadata to Moonraker lane_data namespace |
moonraker_lane_sync_unknown_material_mode |
empty |
How to publish placeholder materials: passthrough/empty/map |
moonraker_lane_sync_unknown_material_markers |
???,unknown,n/a,none |
Values treated as “unknown” for mapping/empty |
moonraker_lane_sync_unknown_material_map_to |
"" | Target material when mode=map |
status_debug_logging |
False | Verbose logging of ACE status update callbacks |
persistence_mode |
deferred |
deferred makes set_and_save deferred; immediate writes instantly |
purge_multiplier |
1.0 | Scale factor for all purge operations |
toolchange_load_length |
3000 | Feed length for tool change load (mm) |
feed_speed |
60 | Default feed speed (mm/s); per-instance overridable |
retract_speed |
50 | Default retract speed (mm/s); per-instance overridable |
incremental_feeding_length |
50 | Feed segment length (mm); per-instance overridable |
incremental_feeding_speed |
30 | Feed segment speed (mm/s); per-instance overridable |
heartbeat_interval |
1.0 | Heartbeat polling interval (s); per-instance overridable |
max_dryer_temperature |
60 | Dryer temperature cap (°C); per-instance overridable |
GCode Command Handlers:
All commands are table-driven and globally registered. Commands use flexible parameter resolution:
Core Operations:
ACE_GET_STATUS [INSTANCE=<n>|TOOL=<n>] [VERBOSE=1]
# Query ACE hardware status
# Without INSTANCE/TOOL: all instances
# VERBOSE=1: detailed output (all fields)
# VERBOSE=0 (default): compact JSON
ACE_RECONNECT [INSTANCE=<n>] # Reconnect serial connection(s)
# Without INSTANCE: reconnect all instances
ACE_FEED [T=<tool>|INSTANCE=<n> INDEX=<n>] LENGTH=<mm> [SPEED=<mm/s>]
# Feed filament from slot
ACE_STOP_FEED [T=<tool>|INSTANCE=<n> INDEX=<n>]
# Stop active feed
ACE_RETRACT [T=<tool>|INSTANCE=<n> INDEX=<n>] LENGTH=<mm> [SPEED=<mm/s>]
# Retract filament to slot
ACE_STOP_RETRACT [T=<tool>|INSTANCE=<n> INDEX=<n>]
# Stop active retract
Tool Change & Loading:
ACE_SMART_UNLOAD [TOOL=<n>] # Intelligent unload with fallback strategies
# Tries current, then other slots, then cross-instance
ACE_SMART_LOAD # Load all non-empty slots to verification sensor (toolhead)
ACE_CHANGE_TOOL TOOL=<n> # Execute tool change T<n>
# TOOL=-1: unload current tool
ACE_FULL_UNLOAD [TOOL=<n>|TOOL=ALL] # Full unload until slot empty
# TOOL=ALL: unload all non-empty slots
# No TOOL: unload current tool
# Clears tool index on success
Inventory Management:
ACE_SET_SLOT [T=<tool>|INSTANCE=<n> INDEX=<n>] COLOR=<name>|R,G,B MATERIAL=<name> TEMP=<°C>
or EMPTY=1 # Set slot metadata or clear
# COLOR can be named (e.g. RED, BLUE) or R,G,B
ACE_QUERY_SLOTS [INSTANCE=<n>] [VERBOSE=1] # Query slots with RFID details
# Without INSTANCE: all instances
# VERBOSE=1: Show all RFID fields
# Format: Table with columns:
# [#] T# | Status | RFID | SKU | Brand | Material | RGB | Temp | Extruder | Bed
# Example: "[1] T1 | ready | RFID | AHPLBK-101 | Anycubic | PLA | RGB(255,0,0) | 210°C | 190-230°C | 50-60°C"
# Empty slots: "-----" status, "---" for missing fields
ACE_SAVE_INVENTORY [INSTANCE=<n>] # Persist inventory to saved_variables
# If INSTANCE specified, saves that instance
ACE_RESET_PERSISTENT_INVENTORY INSTANCE=<n>
# Clear all slot metadata for instance
ACE_RESET_ACTIVE_TOOLHEAD INSTANCE=<n> # Reset active tool index to -1
Feed Assist Control:
ACE_ENABLE_FEED_ASSIST [T=<tool>|INSTANCE=<n> INDEX=<n>]
# Enable auto-push filament on detection
ACE_DISABLE_FEED_ASSIST [T=<tool>|INSTANCE=<n> INDEX=<n>]
# Disable auto-push
ACE_SET_FEED_SPEED [T=<tool>|INSTANCE=<n> INDEX=<n>] SPEED=<mm/s>
# Dynamically adjust feed speed
ACE_SET_RETRACT_SPEED [T=<tool>|INSTANCE=<n> INDEX=<n>] SPEED=<mm/s>
# Dynamically adjust retract speed
Endless Spool:
ACE_ENABLE_ENDLESS_SPOOL # Enable auto-swap on runout
ACE_DISABLE_ENDLESS_SPOOL # Disable auto-swap
ACE_ENDLESS_SPOOL_STATUS # Query endless spool configuration
ACE_SET_ENDLESS_SPOOL_MODE MODE=exact|material|next
# Set match mode:
# "exact": match material AND color (default)
# "material": match material only
# "next": use next ready slot (ignore material/color)
ACE_GET_ENDLESS_SPOOL_MODE # Query current match mode
RFID Inventory Sync:
ACE_ENABLE_RFID_SYNC [INSTANCE=<n>] # Enable auto-sync RFID to inventory
# When enabled, RFID data auto-updates slot metadata
# Updates: material, color (RGB), temp, diameter, brand, etc.
# Slot marked with rfid=True when data present
ACE_DISABLE_RFID_SYNC [INSTANCE=<n>] # Disable auto-sync
# Manual ACE_SET_SLOT commands still work
ACE_RFID_SYNC_STATUS [INSTANCE=<n>] # Query RFID sync status
# Shows enabled/disabled state per instance
RFID Query Behavior:
- RFID tags are queried automatically when state transitions from
saved_rfid=Falseto RFID detected - On (re)connect: All slots are queried unconditionally to catch spool changes during disconnect
- No re-query for already-detected tags (prevents duplicate queries)
- Query triggers:
get_filament_inforequest to ACE firmware - Data update: Via callback, updates inventory with material/color/temp/brand/SKU/temps
RFID Color Handling:
- RFID tags provide RGB color values (0-255 range)
- Color auto-synced to inventory when RFID sync enabled
- Empty slots default to RGB(0,0,0) - black
- Manual color override:
ACE_SET_SLOT T=0 COLOR=REDorCOLOR=255,0,0 - Named colors: RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, WHITE, BLACK, GRAY
- RGB values preserved when slot becomes empty (for auto-restore)
Dryer Control:
ACE_START_DRYING [INSTANCE=<n>] TEMP=<°C> [DURATION=<min>]
# Start filament drying (default 240 min)
ACE_STOP_DRYING [INSTANCE=<n>] # Stop drying
Configuration & Purge:
ACE_SET_PURGE_AMOUNT PURGELENGTH=<mm> PURGESPEED=<mm/min> [INSTANCE=<n>]
# Set purge parameters for tool changes
Lifecycle Hooks:
_ACE_HANDLE_PRINT_END # Called at print end (cleanup sequence)
Debug & Testing Commands:
ACE_GET_CURRENT_INDEX # Query currently loaded tool index
ACE_GET_CONNECTION_STATUS # Show connection status for all instances
# Reports: connected, stable, recent reconnects
ACE_DEBUG_SENSORS # Print all sensor states
# (toolhead, RDM, path-free status)
ACE_DEBUG_STATE # Print manager & instance state
# (tool mapping, filament position)
ACE_DEBUG INSTANCE=<n> METHOD=<name> [PARAMS=<json>]
# Send raw debug request to hardware
ACE_DEBUG_CHECK_SPOOL_READY TOOL=<n> # Test spool ready check
# Verifies slot is ready and available
ACE_DEBUG_INJECT_SENSOR_STATE TOOLHEAD=0|1 RDM=0|1 or RESET=1
# Inject sensor state (testing)
ACE_DEBUG_SET_CURRENT_INDEX [TOOL=<n>] # Override saved tool index
# TOOL=-1: no tool loaded (default)
# Useful for correcting stale state after
# manual filament removal while powered off
ACE_DEBUG_SET_FILAMENT_STATE [STATE=bowden|splitter|toolhead|nozzle]
# Override saved filament position
# Omit STATE= to query current value
# Case-insensitive
ACE_FLUSH # Persist any pending dirty variables to disk
# (normally deferred to print end / disconnect)
ACE_SHOW_INSTANCE_CONFIG [INSTANCE=<n>] # Display resolved config for instance(s)
# Without INSTANCE: compare all instances
Tool Selection (Dynamic):
T<0-N> # Per-tool commands (auto-registered)
# Count depends on ace_count:
# ace_count=1: T0-T3
# ace_count=2: T0-T7
# ace_count=3: T0-T11
# ace_count=4: T0-T15
Command Resolution Priority:
def ace_get_instance(gcmd):
# Priority:
# 1. INSTANCE=<n> parameter (explicit instance)
# 2. T=<tool> or TOOL=<tool> parameter (map tool to instance)
# 3. Fallback to instance 0 if neither specified
def ace_get_instance_and_slot(gcmd):
# Resolves both instance and slot:
# 1. T=<tool> parameter → instance + slot
# 2. INSTANCE=<n> INDEX=<n> parameters → explicit slotKey Macros:
[gcode_macro _ACE_PRE_TOOLCHANGE]
# Pre-toolchange preparation:
# - Z-hop for safety
# - Ensure homed
# - Heat to appropriate temperature
# - Move to throw position (if heating needed during print)
[gcode_macro _ACE_POST_TOOLCHANGE]
# Post-toolchange finalization:
# - Purge new filament
# - Wipe nozzle
# - Restore temperature
# - Resume moves
[gcode_macro CUT_TIP]
# Cut filament at cutter (Kobra 3 Combo):
# - CRITICAL: Z-lift BEFORE Y movement (prevents collision)
# - Uses G91 (relative) for Z-lift to avoid absolute position issues
# - Move to cutter position (X=0, Y=260)
# - Multiple extruder jabs to ensure clean cut (-2mm/+2mm cycles)
# - Move to flush position after cut
# - Safety: M400 waits ensure moves complete before next operation
#
# Bug Fix (2024-12-07): Added G91/G90 Z-lift sequence to prevent
# toolhead collision with cutter arm during print toolchanges
[gcode_macro RESUME]
# Resume after pause:
# - Check filament position
# - Reload tool only if needed (filament at splitter/bowden)
# - Restore position and continue1. User Command: T3
↓
2. AceManager.perform_tool_change(current=-1, target=3)
↓
3. _ACE_PRE_TOOLCHANGE macro
- Z-hop
- Heat to target temp
- Move to throw position (if heating needed)
↓
4. Unload Current Tool (if any)
- AceManager.smart_unload(current_tool)
- Cut filament (CUT_TIP macro)
- Retract to bowden
- Validate sensors clear
↓
5. Load Target Tool
- Find instance managing T3 (instance 0)
- Check spool ready
- Feed from slot 3 → toolhead sensor
- Feed toolhead sensor → nozzle
- Update ace_filament_pos = "nozzle"
↓
6. _ACE_POST_TOOLCHANGE macro
- Purge filament
- Wipe nozzle
- Update state
↓
7. Set ace_current_index = 3
1. Toolhead Sensor Triggers (filament absent)
↓
2. RunoutMonitor._monitor_runout() (50ms interval)
- Detects state change (present → absent)
- Debounce: requires N consecutive absent readings (default 3 ≈ 150ms)
- Guards: not during toolchange, printing active, detection enabled
- Tracks previous sensor state for transition detection
↓
3. RunoutMonitor._handle_runout_detected(tool_index)
- Sets runout_handling_in_progress flag
- Resets sensor baseline to prevent repeated triggers
↓
4. RunoutMonitor._pause_for_runout()
- Execute PAUSE command (Klipper pause macro)
↓
5. Show Interactive Mainsail Prompt
- Display runout details (instance, slot, material, color)
- Buttons: RESUME, CANCEL_PRINT
↓
6. Check Endless Spool Enabled
- Query ace_endless_spool_enabled from saved_variables
↓
7a. If Endless Spool DISABLED:
- Stay paused, wait for user to refill spool
- User must click RESUME after refilling
↓
7b. If Endless Spool ENABLED:
- EndlessSpool.find_exact_match(tool_index) (mode-aware: exact/material/next)
- Search all instances according to match mode
↓
8a. If NO MATCH Found:
- Stay paused, prompt remains visible
- User must refill or load matching material
↓
8b. If MATCH Found:
- Close prompt automatically
- EndlessSpool.execute_swap(from_tool, to_tool)
- Mark old slot empty (status="empty", preserves color/RGB/material/temp)
- Execute tool change with is_endless_spool=True
- Skip unload (already empty), perform 1.5x purge
- Resume print automatically
↓
9. Finally: Clear runout_handling_in_progress flag
**Note on Color Preservation:**
RGB values are preserved when a slot becomes empty, allowing the system to
restore previous settings if the same spool is reinserted. This also enables
endless spool matching based on the previous spool's color/material metadata.
ace_filament_pos: str # "bowden" | "splitter" | "toolhead" | "nozzle"
ace_current_index: int # Currently loaded tool (-1 = none)
ace_endless_spool_enabled: bool # Endless spool active
ace_endless_spool_match_mode: str # Match mode: "exact" | "material" | "next"
ace_global_enabled: bool # ACE system enabled
# Per-instance inventory (persisted)
ace_inventory_0: List[Dict] # Instance 0 slots
ace_inventory_1: List[Dict] # Instance 1 slots
# ... etctoolchange_in_progress: bool # Tool change active (blocks runout)
runout_detection_active: bool # Runout monitoring enabled
prev_toolhead_sensor_state: bool # For detecting state changes
last_printing_state: bool # Track print start/stop
sensors: Dict[str, Sensor] # Sensor objectsinventory: List[Dict] # Slot metadata (runtime copy)
_feed_assist_index: int # Current feed assist slot (-1 = none)
_pending_feed_assist_restore: int # Slot pending restoration after reconnect (-1 = none)
_info: Dict # ACE hardware status
serial_mgr: AceSerialManager # Communication handler
feed_assist_active_after_ace_connect: bool # Restore feed assist on reconnect (config)Each slot in the inventory contains:
{
"status": str, # "ready" | "empty" - hardware state
"color": List[int], # [R, G, B] - preserved when empty
"material": str, # e.g. "PLA" - preserved when empty
"temp": int, # Print temperature - preserved when empty
"rfid": bool, # True if data came from RFID tag - cleared when empty
# Optional RFID fields (cleared when slot becomes empty):
"extruder_temp": Dict, # {"min": int, "max": int}
"hotbed_temp": Dict, # {"min": int, "max": int}
"diameter": float, # Filament diameter in mm
"sku": str, # Spool SKU
"brand": str, # Brand name
"total": int, # Total spool length (mm)
"current": int, # Remaining length (mm)
}When a slot transitions from ready to empty (runout, manual EMPTY=1, etc.):
| Field | Behavior | Reason |
|---|---|---|
status |
Set to "empty" |
Hardware reports no filament |
color |
Preserved (RGB) | Allows auto-restore if same spool reinserted |
material |
Preserved | Allows auto-restore if same spool reinserted |
temp |
Preserved | Allows auto-restore if same spool reinserted |
rfid |
Set to False |
No RFID tag present |
extruder_temp |
Cleared | RFID data no longer valid |
hotbed_temp |
Cleared | RFID data no longer valid |
diameter |
Cleared | RFID data no longer valid |
sku, brand, etc. |
Cleared | RFID data no longer valid |
Display Behavior:
- Empty slots show
-----for status and material in ACE_QUERY_SLOTS output - RGB color preserved (displays as RGB(r,g,b) even when empty)
- Temperature shows 0°C when empty (temp field preserved but not active)
- RFID indicator shows
[----]when no RFID tag present
Rationale: Core metadata (color/RGB, material, temp) is preserved so that if the same
spool is reinserted, the slot auto-restores to ready with its previous settings.
RFID-specific fields are cleared because they only apply when an RFID-tagged spool
is physically present.
Auto-Restore on Spool Swap:
1. Slot 0: status=ready, material=PLA, color=RGB(255,0,0), temp=210
2. Runout detected → status=empty (material/color/temp preserved)
3. User inserts NEW spool → ACE hardware detects filament
4a. IF RFID present: All fields updated from RFID tag (material, RGB, temp, etc.)
4b. IF NO RFID: Slot restores to ready with preserved material/color/temp
5. Endless spool can match based on preserved metadata
This example is just for reference; check printer_KS1.cfg / printer_K3.cfg for live values.
[ace]
ace_count: 1
baud: 115200
# Tube Lengths
parkposition_to_toolhead_length: 800
parkposition_to_rdm_length: 150
toolchange_load_length: 2000
# Feeding Speeds
feed_speed: 60
retract_speed: 50
incremental_feeding_length: 100
incremental_feeding_speed: 60
extruder_feeding_length: 10
extruder_feeding_speed: 8
toolhead_slow_loading_speed: 5
toolhead_full_purge_length: 85
# Purge Settings
default_color_change_purge_length: 50
default_color_change_purge_speed: 300
purge_max_chunk_length: 250
purge_multiplier: 1.0
# Safety & Misc
total_max_feeding_length: 2600
pre_cut_retract_length: 2
heartbeat_interval: 1.0
max_dryer_temperature: 55
feed_assist_active_after_ace_connect: True # Restore feed assist after ACE reconnect (deferred until first successful heartbeat)
runout_debounce_count: 3 # Consecutive absent sensor readings before confirming runout (default 1 = no debounce)ACE_DEBUG_SENSORS # Check sensor states
ACE_DEBUG_STATE # Check manager state
ACE_GET_STATUS INSTANCE=0 # Query ACE hardware (compact JSON)
ACE_GET_STATUS INSTANCE=0 VERBOSE=1 # Query ACE hardware (detailed output)This feature publishes ACE slot metadata into Moonraker's database namespace
(lane_data) so Orca can pull filament lane info using its Moonraker adapter.
extras/ace/moonraker_lane_sync.py- Implements
MoonrakerLaneSyncAdapter. - Builds lane payload from all ACE instances and writes Moonraker DB items.
- Implements
extras/ace/config.py- Adds
moonraker_lane_sync_*settings.
- Adds
extras/ace/manager.py- Creates adapter once during manager init.
- Triggers sync on startup and whenever inventory persistence occurs.
ACE heartbeat/status response
-> AceInstance._status_update_callback()
-> inventory changed?
-> manager._sync_inventory_to_persistent(instance_num)
-> SAVE_VARIABLE (existing inventory persistence)
-> manager._sync_moonraker_lane_data(...)
-> MoonrakerLaneSyncAdapter.sync_now(...)
-> GET existing namespace
-> POST changed lane keys
-> DELETE stale lane keys
Additionally, manager does a forced sync on klippy:ready to populate the
initial lane_data snapshot.
- Lane index:
instance.tool_offset + local_slot. - DB key:
lane{index+1}(lane1,lane2, ...). - Required payload fields:
lane(0-based string)materialcolor(#RRGGBB)
- Optional payload fields:
nozzle_tempbed_tempvendor(RFID brand/manufacturer when present)sku(RFID SKU/part number)spool_id
- Empty slots are still published with the same lane index and empty
material/color. spool_idis derived fromskuwhen it is a numeric value; non-numeric SKUs are still published for slicer-side matching but won't become aspool_id.- Unknown/placeholder materials can be filtered or remapped via
moonraker_lane_sync_unknown_material_mode(passthrough/empty/map) and its marker/map settings.
moonraker_lane_sync_enabled: True # default on (set False to disable Moonraker writes)
moonraker_lane_sync_url: http://127.0.0.1:7125
moonraker_lane_sync_namespace: lane_data
moonraker_lane_sync_api_key: # optional
moonraker_lane_sync_timeout: 2.0
moonraker_lane_sync_unknown_material_mode: passthrough # passthrough|empty|map
moonraker_lane_sync_unknown_material_markers: ???,unknown,n/a,none
moonraker_lane_sync_unknown_material_map_to: PLA # used when mode=map- Ensure
moonraker_lane_sync_enabled: Truein[ace](default), then restart after changes. - Read namespace content:
curl -s "http://127.0.0.1:7125/server/database/item?namespace=lane_data" | jq .Expected:
.result.namespaceislane_data.result.valuecontainslane1,lane2, ... entries
Check just the keys to spot stray entries:
curl -s "http://127.0.0.1:7125/server/database/item?namespace=lane_data" \
| jq -r '.result.value | keys[]'- Human-readable lane summary:
curl -s "http://127.0.0.1:7125/server/database/item?namespace=lane_data" \
| jq -r '.result.value | to_entries[] | "\(.key): T\(.value.lane) material=\(.value.material // "") color=\(.value.color // "") nozzle=\(.value.nozzle_temp // "-") bed=\(.value.bed_temp // "-")"'- Watch updates while changing slots (
ACE_SET_SLOT, RFID updates, load/unload):
watch -n1 'curl -s "http://127.0.0.1:7125/server/database/item?namespace=lane_data" | jq ".result.value"'- If Moonraker requires API key:
curl -s -H "X-Api-Key: YOUR_KEY" \
"http://127.0.0.1:7125/server/database/item?namespace=lane_data" | jq .- Cleanup (stray/stale keys):
- Delete a single key safely (handles spaces/quotes):
curl -s -X DELETE --get \
--data-urlencode "namespace=lane_data" \
--data-urlencode "key=lane7" \
http://127.0.0.1:7125/server/database/item- Delete all keys in the namespace:
curl -s "http://127.0.0.1:7125/server/database/item?namespace=lane_data" \
| jq -r '.result.value | keys[]' \
| while IFS= read -r key; do
curl -s -X DELETE --get \
--data-urlencode "namespace=lane_data" \
--data-urlencode "key=${key}" \
http://127.0.0.1:7125/server/database/item >/dev/null
doneTroubleshooting:
- If namespace is empty, verify
moonraker_lane_sync_enabled. - Trigger inventory-changing events (
ACE_SET_SLOT, slot status change) or doFIRMWARE_RESTART. - Check Klipper logs for
Moonraker lane sync unavailablewarnings.