diff --git a/docs/afc-lite.md b/docs/afc-lite.md
new file mode 100644
index 00000000..f2fc309f
--- /dev/null
+++ b/docs/afc-lite.md
@@ -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:**
+
+
+
+**Tools re-mapping:**
+
+
+
diff --git a/docs/images/afc_snpamaker_orca.gif b/docs/images/afc_snpamaker_orca.gif
new file mode 100644
index 00000000..09fbae8a
Binary files /dev/null and b/docs/images/afc_snpamaker_orca.gif differ
diff --git a/docs/images/afc_tools.gif b/docs/images/afc_tools.gif
new file mode 100644
index 00000000..7ead0ea7
Binary files /dev/null and b/docs/images/afc_tools.gif differ
diff --git a/docs/index.md b/docs/index.md
index 52712ddf..d701d327 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC.py b/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC.py
new file mode 100644
index 00000000..caf35ab1
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC.py
@@ -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)
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC_lane.py b/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC_lane.py
new file mode 100644
index 00000000..c41e3393
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC_lane.py
@@ -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)
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC_unit.py b/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC_unit.py
new file mode 100644
index 00000000..4d668333
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/root/home/lava/klipper/klippy/extras/AFC_unit.py
@@ -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)
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/root/usr/local/share/firmware-config/functions/23_settings_tweaks_afc.yaml b/overlays/firmware-extended/23-klipper-afc-lite/root/usr/local/share/firmware-config/functions/23_settings_tweaks_afc.yaml
new file mode 100644
index 00000000..122b8621
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/root/usr/local/share/firmware-config/functions/23_settings_tweaks_afc.yaml
@@ -0,0 +1,31 @@
+settings:
+ tweaks:
+ label: Tweaks
+ items:
+ afc:
+ label: 'AFC Lite via Fluidd/Mainsail (link)'
+ 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 documentation 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
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/root/usr/local/share/firmware-config/tweaks/klipper/afc.cfg b/overlays/firmware-extended/23-klipper-afc-lite/root/usr/local/share/firmware-config/tweaks/klipper/afc.cfg
new file mode 100644
index 00000000..6f725e62
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/root/usr/local/share/firmware-config/tweaks/klipper/afc.cfg
@@ -0,0 +1,107 @@
+# AFC (Automated Filament Changer) Configuration and Macros
+
+[AFC]
+enabled: True
+
+[AFC_unit U1]
+enabled: True
+
+[AFC_lane E0]
+unit: U1
+lane: 0
+extruder: extruder
+toolhead_sensor: filament_motion_sensor e0_filament
+
+[AFC_lane E1]
+unit: U1
+lane: 1
+extruder: extruder1
+toolhead_sensor: filament_motion_sensor e1_filament
+
+[AFC_lane E2]
+unit: U1
+lane: 2
+extruder: extruder2
+toolhead_sensor: filament_motion_sensor e2_filament
+
+[AFC_lane E3]
+unit: U1
+lane: 3
+extruder: extruder3
+toolhead_sensor: filament_motion_sensor e3_filament
+
+[gcode_macro SET_COLOR]
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set color = params.COLOR|default('FFFFFFFF') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ SET_PRINT_FILAMENT_CONFIG CONFIG_EXTRUDER='{index}' FILAMENT_COLOR_RGBA='{color}'
+
+[gcode_macro SET_MATERIAL]
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set material = params.MATERIAL|default('PLA') %}
+ {% set subtype = params.SUBTYPE|default('') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ {% set vendor = printer.print_task_config.filament_vendor[index|int]|default('NONE') %}
+ {% if vendor == 'NONE' %}{% set vendor = 'Generic' %}{% endif %}
+ SET_PRINT_FILAMENT_CONFIG CONFIG_EXTRUDER='{index}' FILAMENT_TYPE='{material}' FILAMENT_SUBTYPE='{subtype}' VENDOR='{vendor}'
+
+[gcode_macro SET_VENDOR]
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set vendor = params.VENDOR|default('NONE') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ SET_PRINT_FILAMENT_CONFIG CONFIG_EXTRUDER='{index}' VENDOR='{vendor}'
+
+[gcode_macro SET_MAP]
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set new_map = params.MAP|default('T0') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ {% set current_map = printer['AFC_lane ' + lane].map %}
+ {% set new_logical = new_map[1:] if new_map.startswith('T') else '0' %}
+ {% set current_logical = current_map[1:] if current_map.startswith('T') else '0' %}
+
+ # Get the extruder_map_table to find what's currently mapped to new_logical
+ {% set map_table = printer.print_task_config.extruder_map_table %}
+ {% set old_channel = map_table[new_logical|int] if new_logical|int < map_table|length else index %}
+
+ # Swap: set new_logical -> index and current_logical -> old_channel
+ SET_PRINT_EXTRUDER_MAP CONFIG_EXTRUDER='{new_logical}' MAP_EXTRUDER='{index}'
+ SET_PRINT_EXTRUDER_MAP CONFIG_EXTRUDER='{current_logical}' MAP_EXTRUDER='{old_channel}'
+
+[gcode_macro SET_SPOOL_ID]
+gcode:
+ { action_raise_error("SET_SPOOL_ID is not supported.") }
+
+[gcode_macro SET_WEIGHT]
+gcode:
+ { action_raise_error("SET_WEIGHT is not supported.") }
+
+[gcode_macro CHANGE_TOOL]
+description: Change tool/lane with automatic load/unload
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ M118 Changing to {lane} (index {index})...
+ AUTO_FEEDING EXTRUDER={index} LOAD=1
+ M118 Tool change to {lane} complete
+
+[gcode_macro LANE_UNLOAD]
+description: Unload filament for specified lane
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ M118 Unloading {lane} (lane {index})...
+ AUTO_FEEDING EXTRUDER={index} UNLOAD=1
+ M118 Unload {lane} complete
+
+[gcode_macro TOOL_UNLOAD]
+description: Unload filament for specified tool
+gcode:
+ {% set lane = params.LANE|default('E0') %}
+ {% set index = printer['AFC_lane ' + lane].lane %}
+ M118 Unloading {lane} (lane {index})...
+ AUTO_FEEDING EXTRUDER={index} UNLOAD=1
+ M118 Unload {lane} complete
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/test/printer.cfg b/overlays/firmware-extended/23-klipper-afc-lite/test/printer.cfg
new file mode 100644
index 00000000..d759dc8c
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/test/printer.cfg
@@ -0,0 +1,14 @@
+[mcu]
+serial: /tmp/klipper_host_mcu
+
+[printer]
+kinematics: none
+max_velocity: 1
+max_accel: 1
+max_logical_extruder_num: 32
+max_physical_extruder_num: 4
+
+[print_task_config]
+
+# Load AFC configuration and macros
+[include ../root/usr/local/share/firmware-config/tweaks/klipper/afc.cfg]
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/test/restart.sh b/overlays/firmware-extended/23-klipper-afc-lite/test/restart.sh
new file mode 100755
index 00000000..507eac20
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/test/restart.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+ROOT_DIR="$(dirname "$(realpath "$0")")/.."
+
+if [[ $# -ne 1 ]]; then
+ echo "usage: $0 "
+ exit 1
+fi
+
+set -xeo pipefail
+
+scp -r "$ROOT_DIR/root/." "$1":/
+ssh -t "$1" /etc/init.d/S60klipper restart
diff --git a/overlays/firmware-extended/23-klipper-afc-lite/test/run.sh b/overlays/firmware-extended/23-klipper-afc-lite/test/run.sh
new file mode 100755
index 00000000..1c066c80
--- /dev/null
+++ b/overlays/firmware-extended/23-klipper-afc-lite/test/run.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+ROOT_DIR="$(dirname "$(realpath "$0")")/.."
+
+if [[ $# -ne 1 ]]; then
+ echo "usage: $0 "
+ exit 1
+fi
+
+set -xeo pipefail
+
+scp -r "$ROOT_DIR/root/." "$1":/
+scp "$ROOT_DIR/../../../scripts/dev/run-klipper.sh" "$ROOT_DIR/test/printer.cfg" "$1":/tmp/
+ssh -t "$1" /tmp/run-klipper.sh /tmp/printer.cfg