Skip to content

Latest commit

 

History

History
1299 lines (1075 loc) · 57.8 KB

File metadata and controls

1299 lines (1075 loc) · 57.8 KB

ACE Pro Architecture Overview

System Overview

The ACE Pro is a multi-material filament management system for Klipper-based 3D printers. This implementation supports multiple ACE Pro units.

High-Level Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         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)    │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Core Components

1. AceManager (manager.py)

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/inactive
  • ace_global_enabled: ACE system master enable
  • Sensor Management: Manages shared sensors (toolhead, optional RDM), supporting both filament_switch_sensor and filament_tracker via FilamentTrackerAdapter
  • Smart Operations: smart_unload(), smart_load() with sensor-aware fallback
  • Tool Change Orchestration: perform_tool_change() coordinates unload/load across instances
  • Runout Detection: Creates RunoutMonitor to 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 restored

2. AceInstance (instance.py)

Primary 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 temperature

Key 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 toolhead

Sensor 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 data

This enables precise timing measurements for:

  • Retraction efficiency analysis
  • Detecting stuck filament (late sensor triggers)
  • Optimizing movement speeds
  • Diagnosing mechanical issues

3. EndlessSpool (endless_spool.py)

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:

  1. Finding matches (based on match mode)
  2. Executing swaps (tool changes)
  3. 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

⚠️ Safety: Unknown Material Handling:

  • 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

4. RunoutMonitor (runout_monitor.py)

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 gcode

State 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 counter

Runout 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=True and was_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_detection with threshold tangle_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

5. AceSerialManager (serial_manager.py)

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=None to 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 size

Key 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 enabled

6. PersistentState (persistent_state.py)

Primary Responsibilities:

  • Single access point for all saved_variables.cfg reads and writes
  • Deferred-flush strategy: set() updates RAM and marks dirty; disk write is deferred until flush()
  • Configurable persistence: set_and_save() obeys persistence_mode
    • deferred (default): behaves like set() (dirty-only until flush())
    • immediate: writes to disk right away (legacy behaviour)
  • Type-safe serialisation: handles bool, str, dict/list, int/float with correct Klipper SAVE_VARIABLE formatting

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 flushing

Usage 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 shutdown
  • ACE_FLUSH gcode command — on user request
  • _flush_if_idle() timer callback — background idle-time flush

7. Configuration (config.py)

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 dict

Key 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

8. Commands (commands.py)

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=False to 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_info request 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=RED or COLOR=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 slot

9. Macros (ace.cfg)

Key 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 continue

Data Flow

Tool Change Sequence

1. 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

Runout Detection Flow

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.

State Management

Global State (saved_variables.cfg)

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
# ... etc

Runtime State (AceManager)

toolchange_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 objects

Runtime State (AceInstance)

inventory: 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)

Inventory Slot Structure

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)
}

Slot Empty Transition Behavior

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

Configuration Example

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)

Debug Commands

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)

Moonraker lane_data Sync Architecture (Orca Filament Sync)

Purpose

This feature publishes ACE slot metadata into Moonraker's database namespace (lane_data) so Orca can pull filament lane info using its Moonraker adapter.

Module Documentation

  • extras/ace/moonraker_lane_sync.py
    • Implements MoonrakerLaneSyncAdapter.
    • Builds lane payload from all ACE instances and writes Moonraker DB items.
  • extras/ace/config.py
    • Adds moonraker_lane_sync_* settings.
  • extras/ace/manager.py
    • Creates adapter once during manager init.
    • Triggers sync on startup and whenever inventory persistence occurs.

Data Flow

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 Mapping & Payload Rules

  • Lane index: instance.tool_offset + local_slot.
  • DB key: lane{index+1} (lane1, lane2, ...).
  • Required payload fields:
    • lane (0-based string)
    • material
    • color (#RRGGBB)
  • Optional payload fields:
    • nozzle_temp
    • bed_temp
    • vendor (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_id is derived from sku when it is a numeric value; non-numeric SKUs are still published for slicer-side matching but won't become a spool_id.
  • Unknown/placeholder materials can be filtered or remapped via moonraker_lane_sync_unknown_material_mode (passthrough/empty/map) and its marker/map settings.

Config ([ace])

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

Test & Debug (Moonraker DB)

  1. Ensure moonraker_lane_sync_enabled: True in [ace] (default), then restart after changes.
  2. Read namespace content:
curl -s "http://127.0.0.1:7125/server/database/item?namespace=lane_data" | jq .

Expected:

  • .result.namespace is lane_data
  • .result.value contains lane1, 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[]'
  1. 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 // "-")"'
  1. 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"'
  1. 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 .
  1. 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
  done

Troubleshooting:

  • If namespace is empty, verify moonraker_lane_sync_enabled.
  • Trigger inventory-changing events (ACE_SET_SLOT, slot status change) or do FIRMWARE_RESTART.
  • Check Klipper logs for Moonraker lane sync unavailable warnings.