Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions docs/afc-lite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# AFC-Lite Stub Implementation

**EXPERIMENTAL**: This feature is experimental and may be removed at any point without notice.

## Overview

Thin compatibility layer simulating the [ArmoredTurtle AFC-Klipper-Add-On](https://github.com/ArmoredTurtle/AFC-Klipper-Add-On/) to enable AFC UI panels in Fluidd/Mainsail. This is a status reporting stub only and does not implement actual AFC hardware control.

## Why This Exists

To provide an ability to manage U1 extruders via `Fluidd/Mainsail` interface.

## What It Provides

**Status Integration:**

- AFC-compatible status endpoints for Fluidd/Mainsail UI
- Maps U1's 4 extruders to AFC lanes (E0-E3)
- Displays filament information from `print_task_config`

**Macros:**

- `CHANGE_TOOL` - wraps `AUTO_FEEDING`
- `LANE_UNLOAD` - wraps `AUTO_FEEDING UNLOAD=1`
- `SET_COLOR` - wraps `SET_PRINT_FILAMENT_CONFIG`
- `SET_MATERIAL` - wraps `SET_PRINT_FILAMENT_CONFIG`
- `SET_MAP` - wraps `SET_PRINT_EXTRUDER_MAP`

## Limitations

**Not Supported:**

- Spoolman integration (planned for future release)
- Filament tracking and databases
- Runout lane configuration
- Mapping single extruder to multiple logical tools
- AFC hardware (hubs, buffers, physical devices)

**Technical Constraints:**

- None of the AFC.cfg configuration settings apply to AFC-Lite
- Changing (color, material, etc.) the RFID loaded filament via AFC is not supported and will result in error
- Changing weight is not supported, the weight is locked to 1000 g
- Changing runout lane is not supported
- All filament operations use U1's native `AUTO_FEEDING`
- Status reporting only, no actual AFC control

## Enabling/Disabling

Enable via Fluidd/Mainsail settings under **Tweaks > AFC Stub**, or manually:

```bash
ln -sf /usr/local/share/firmware-config/tweaks/klipper/afc.cfg \
/oem/printer_data/config/extended/klipper/afc.cfg
/etc/init.d/S60klipper restart
```

To disable:

```bash
rm /oem/printer_data/config/extended/klipper/afc.cfg
/etc/init.d/S60klipper restart
```

## Examples

**Snapmaker Orca synchronization:**

![AFC Snapmaker Orca Sync](images/afc_snpamaker_orca.gif)

**Tools re-mapping:**

![AFC Tools Remapping](images/afc_tools.gif)

Binary file added docs/images/afc_snpamaker_orca.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/afc_tools.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Heavily expanded firmware with extensive features and customization. Includes al
- [Camera Support](camera_support.md) - Hardware-accelerated camera stack with WebRTC streaming for internal and USB cameras
- [Klipper and Moonraker Custom Includes](klipper_includes.md) - Add custom configuration files via Fluidd/Mainsail
- [Klipper Tweaks](tweaks.md) - Experimental TMC driver optimizations and [object processing for adaptive mesh](tweaks.md#object-processing-for-adaptive-mesh) (firmware-config only)
- [AFC-Lite Stub](afc-lite.md) - Experimental AFC UI compatibility layer for Fluidd/Mainsail (may be removed)
- [RFID Filament Tag Support](rfid_support.md) - NTAG213/215/216 support for OpenSpool format
- [Remote Screen](remote_screen.md) - View and control printer screen remotely via web browser
- [Monitoring](monitoring.md) - Integration with Prometheus, Home Assistant, DataDog, and other monitoring systems
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
class AFCState:
IDLE = "idle"
LOADING = "loading"
UNLOADING = "unloading"
TOOL_CHANGE = "tool_change"
ERROR = "error"

class AFC:
def __init__(self, config):
self.printer = config.get_printer()
config.get("enabled", "True")
self.gcode = self.printer.lookup_object("gcode")

self.units = {}
self.lanes = {}

self.printer.register_event_handler("klippy:connect", self._handle_connect)

def _handle_connect(self):
for name, obj in self.printer.lookup_objects("AFC_unit"):
unit_name = name.replace("AFC_", "", 1)
self.units[unit_name] = obj

for name, obj in self.printer.lookup_objects("AFC_lane"):
lane_name = name.split(None, 1)[1] if " " in name else name
self.lanes[lane_name] = obj

def _get_current_lane(self):
try:
toolhead = self.printer.lookup_object('toolhead')
current_extruder = toolhead.extruder.name
for lane_name, lane in self.lanes.items():
if lane.extruder == current_extruder:
return lane_name
except:
pass
return None

def get_status(self, eventtime=None):
str = {}
str['current_load'] = None
str['current_lane'] = self._get_current_lane()
str['next_lane'] = None
str['current_state'] = AFCState.IDLE
str["current_toolchange"] = 0
str["number_of_toolchanges"] = 0
str['spoolman'] = True
str["td1_present"] = False
str["lane_data_enabled"] = False
str['error_state'] = False
str["bypass_state"] = False
str["quiet_mode"] = False
str["position_saved"] = False

str['units'] = list(self.units.keys())
str['lanes'] = list(self.lanes.keys())
str["extruders"] = []
str["hubs"] = []
str["buffers"] = []
str["message"] = ""
str["led_state"] = ""
return str

def load_config(config):
return AFC(config)
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import json
import logging
import os

class AFCLaneState:
EMPTY = "empty"
LOADING = "loading"
LOADED = "loaded"
UNLOADING = "unloading"
TOOL_LOADED = "tool_loaded"
ERROR = "error"

class AFCLane:
def __init__(self, config):
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
self.name = config.get_name().replace("AFC_lane ", "", 1)

self.unit_name = config.get("unit", "")
self.lane_index = config.getint("lane", 0)
self.extruder_name = config.get("extruder", None)
self.toolhead_sensor_name = config.get("toolhead_sensor", None)
self.filament_feed_name = config.get("filament_feed", None)

self.print_task_config = None
self.toolhead_sensor = None
self.filament_feed = None

self.printer.register_event_handler("klippy:connect", self._handle_connect)

def _handle_connect(self):
try:
self.print_task_config = self.printer.lookup_object("print_task_config")
except:
pass

if self.filament_feed_name:
try:
self.filament_feed = self.printer.lookup_object(self.filament_feed_name)
except:
pass

if self.toolhead_sensor_name:
try:
self.toolhead_sensor = self.printer.lookup_object(self.toolhead_sensor_name)
except:
pass

def _get_state(self, eventtime=None):
"""Get filament info from print_task_config based on lane index"""
if not self.print_task_config:
return {}

state = {
'loaded': False,
'tool_loaded': False,
'vendor': 'NONE',
'type': 'NONE',
'subtype': 'NONE',
'color': 'FFFFFFFF',
'map': f"T{self.lane_index}",
'runout_lane': 'NONE',
}

try:
status = self.print_task_config.get_status(eventtime)
state['loaded'] = dict(enumerate(status.get('filament_exist', []))).get(self.lane_index, False)
state['vendor'] = dict(enumerate(status.get('filament_vendor', []))).get(self.lane_index, 'NONE')
state['type'] = dict(enumerate(status.get('filament_type', []))).get(self.lane_index, 'NONE')
state['subtype'] = dict(enumerate(status.get('filament_sub_type', []))).get(self.lane_index, 'NONE')
state['color'] = dict(enumerate(status.get('filament_color_rgba', []))).get(self.lane_index, 'FFFFFFFF')
if status.get('auto_replenish_filament', False):
state['runout_lane'] = 'AUTO'

tool_to_extruder = dict(enumerate(status.get('extruder_map_table', [])))
for tool_idx, extruder_idx in tool_to_extruder.items():
if extruder_idx == self.lane_index:
# TODO: AFC only supports a single tool mapped
state['map'] = f"T{tool_idx}"
break
except:
pass

try:
status = self.toolhead_sensor.get_status(eventtime)
state['tool_loaded'] = status.get('filament_detected', True)
except:
state['tool_loaded'] = state['loaded']

return state

def get_status(self, eventtime=None):
response = {}

state = self._get_state(eventtime)

response['name'] = self.name
response['unit'] = self.unit_name
response['lane'] = self.lane_index
response['extruder'] = self.extruder_name
response['map'] = state.get('map', f"T{self.lane_index}")
response['load'] = state.get('loaded', False)
response['prep'] = state.get('loaded', False)
response['tool_loaded'] = state.get('tool_loaded', response['load'])
response['loaded_to_hub'] = False
response['material'] = state.get('type', 'NONE')
response['spool_id'] = None
response['color'] = f"#{state.get('color', 'FFFFFFFF')[:6]}" # RGB only, ignore alpha
response['weight'] = 1000 # AFC doesn't track weight
response['runout_lane'] = state.get('runout_lane', '?')
response['filament_status'] = 'unknown'
response['filament_status_led'] = 'gray'
response['status'] = AFCLaneState.EMPTY
return response

def load_config_prefix(config):
return AFCLane(config)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class AFCUnit:
def __init__(self, config):
# Prevent klipper error
config.get("enabled", True)

self.printer = config.get_printer()
self.fullname = config.get_name()
self.name = self.fullname.split()[-1]

self.lanes = {}
self.printer.register_event_handler("klippy:connect", self._handle_connect)

def _handle_connect(self):
for _, obj in self.printer.lookup_objects("AFC_lane"):
if hasattr(obj, 'unit_name') and obj.unit_name == self.name:
self.lanes[obj.name] = obj

def get_status(self, eventtime=None):
response = {}
response['lanes'] = [lane.name for lane in self.lanes.values()]
response["extruders"] = []
response["hubs"] = []
response["buffers"] = []
return response

def load_config_prefix(config):
return AFCUnit(config)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
settings:
tweaks:
label: Tweaks
items:
afc:
label: 'AFC Lite via Fluidd/Mainsail (<a href="http://snapmakeru1-extended-firmware.pages.dev/afc-lite.md" target="_blank">link</a>)'
get_cmd:
- bash
- -c
- test -f /oem/printer_data/config/extended/klipper/afc.cfg && echo "enabled" || echo "disabled"
options:
enabled:
label: Enabled
confirm: 'Enable AFC Lite component? This adds support for multi-lane filament management with CHANGE_TOOL and LANE_UNLOAD macros. See <a href="http://snapmakeru1-extended-firmware.pages.dev/afc-lite.md" target="_blank">documentation</a> for details.'
cmd:
- bash
- -xc
- |
ln -sf /usr/local/share/firmware-config/tweaks/klipper/afc.cfg /oem/printer_data/config/extended/klipper/afc.cfg &&
echo "AFC enabled. Restarting Klipper..." &&
/etc/init.d/S60klipper restart
disabled:
label: Disabled
cmd:
- bash
- -xc
- |
rm -vf /oem/printer_data/config/extended/klipper/afc.cfg &&
echo "AFC disabled. Restarting Klipper..." &&
/etc/init.d/S60klipper restart
default: disabled
Loading