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:** + +![AFC Snapmaker Orca Sync](images/afc_snpamaker_orca.gif) + +**Tools re-mapping:** + +![AFC Tools Remapping](images/afc_tools.gif) + 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