diff --git a/CHANGELOG.md b/CHANGELOG.md index 4571e6b..a6d6a14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -202,7 +202,67 @@ gcode: ### Fixed - Fixed erroring out if a buffer in not configured +## [2024-12-02] + +### Added + +- Buffer_Ram_Sensor + - Enabling the buffer to be used as a ram sensor for loading and unloading filament + - see Buffer_Ram_Sensor doc for more information + +### Changed + +- Adjusted load and unload to account for ram sensor +- Adjusted Prep to account for ram sensor + +## [2024-12-03] + +### Added + +- The `install-afc.sh` script will now query the printer upon exit to see if it is actively printing. If it is not + printing, it will restart the `klipper` service. ## [2024-12-04] ### Fixed - Fixed issue with Turtleneck buffer pins not being assigned correctly when prompted during install +- Fixed issue with LEDs not showing the right color when error happened during PREP +- Changed error message when AFC.vars.unit lane showed loaded but AFC.vars.tool file didn't match +- Added logic so that user could change trsync value. To set value add the following into `[AFC]` section in AFC.cfg file: +`trsync_update: True` +Optional: +`trsync_timeout: 0.05` +`TRSYNC_SINGLE_MCU_TIMEOUT: 0.5` + +## [2024-12-07] + +### Updated +- When BT_TOOL_UNLOAD is used, spoolman active spool is set to None +- When spool is ejected from Box Turtle spoolman spool is removed from variables +- Activated espooler when user calls LANE_MOVE + +### Fixed +- Fixed places where gcode was not referencing AFC and would cause crashes + + +## [2024-12-09] + +### Added +- Added logic to pause print when filament goes past prep sensor. Verify that PAUSE macro move's toolhead off print when it's called. +======= +## [2024-12-08] + +### Added +- When updating the AFC software, the `install-afc.sh` script will now remove any instances of `[gcode_macro T#]` found in the `AFC_Macros.cfg` +file as the code now generates them automatically. + +## [2024-12-09] + +### Added +- **New Command: `CALIBRATE_AFC`** + Allows calibration of the hub position and Bowden length in the Automated Filament Changer (AFC) system. + Supports calibration for a specific lane or all lanes (`LANES` parameter). + Provides options for distance and tolerance during calibration: + - `DISTANCE=`: Optional distance parameter for lane movement during calibration (default is 25mm). + - `TOLERANCE=`: Optional tolerance for fine-tuning adjustments during calibration (default is 5mm). + - Bowden Calibration: Added functionality to calibrate Bowden length for individual lanes using the `BOWDEN` parameter. + diff --git a/README.md b/README.md index b87babf..6251351 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Additionally, review the following files for any changes that may be required: 1) `~/printer_data/config/AFC/AFC.cfg` 2) `~/printer_data/config/AFC/AFC_Macro_Vars.cfg` +Review information in [mandatory configuration changes](README.md#mandatory-configuration-changes-all) section + ## Installation & Configuration (Manual) To manually install and configure the plugin, you can use the following commands: @@ -121,12 +123,39 @@ Prior to operation, the following checks / updates **MUST** be made to your syst 1) Update the following values in the `~/printer_data/config/AFC/AFC.cfg` file: - - tool_stn - - tool_stn_unload - - afc_bowden_length - -2) If you are using any of the built-in macros, the variables in the `~/printer_data/config/AFC/AFC_Macro_Vars.cfg` file -must also be modified to match your configuration for your system. + - tool_stn: This value is the length from your toolhead sensor to nozzle + - tool_stn_unload: This value is the amount to unload from extruder when doing a filament change. + - afc_bowden_length: This value is the length from your hub to your toolhead sensor + +2) Verify that `pin_tool_start` is set to the correct pin for your toolhead sensor. If you are using an existing filament sensor as your toolhead sensor make sure you comment out the filament sensor section in your `printer.cfg` file. + +3) If you are using any of the built-in macros, the variables in the `~/printer_data/config/AFC/AFC_Macro_Vars.cfg` file +must also be modified to match your configuration for your system. + + Required variables to verify and update if necessary for the following default macros + - tool_cut: + - variable_retract_length + - variable_cut_direction + - variable_pin_loc_xy + - variable_pin_park_dist + - variable_cut_move_dist + - park: + - variable_park_loc_xy + - poop: + - variable_purge_loc_xy + - kick: + - variable_kick_start_loc + - variable_kick_direction + - wipe + - variable_brush_loc + - variable_y_brush + - form_tip + Variables to update for tip forming are in `~/printer_data/config/AFC/AFC.cfg` + - cooling_tube_position + - cooling_tube_length + +4) If you would like to use your own macro instead of the provided macros, make sure to update the command with your custom macro in `~/printer_data/config/AFC/AFC.cfg` + ex. If using custom park macro, change `park_cmd` from `AFC_PARK` to your macro name **Failure to update these values can result in damage to your system** @@ -140,12 +169,55 @@ led_buffer_trailing: 0,1,0,0 led_buffer_disable: 0,0,0,0.25 ``` +If using a hub that is not located in the box turtle the following value needs to be updated for each stepper + - dist_hub: This value the the length between the lanes extruder and the hub, this does not have to be exact and is better to figure the length and then minus about 40mm + +If using snappy hub cutter update the following values: + - cut: change to True + - cut_dist: update to the value that you would like to cut off the end, this may take some tuning to get right + +## Calibration + +The function `CALIBRATE_AFC` can be called in the console to calibrate distances. + +_distances will be calibrated to have ~1 short move after the move distance_ + +### Definitions + +- `dist_hub` for each lane is the distance from the load switch at the extruder to the hub +- `afc_bowden_length` is the distance from the hub to the toolhead sensor + +### Usage + +`CALIBRATE_AFC LANES= DISTANCE= TOLERANCE= BOWDEN=` + +_`DISTANCE` and `TOLERANCE` are optional. default distance 25mm, default tolerance 5mm_ + +- To calibrate all lanes and the bowden length all at once: + - `CALIBRATE_AFC LANES=all BOWDEN=` input which lane to be used to check `afc_bowden_length` +- To calibrate individual lanes + - `CALIBRATE_AFC LANES=` input the lane you would like to calibrate +- To calibrate just the bowden length: + - `CALIBRATE_AFC BOWDEN=` input which lane to be used to check `afc_bowden_length` + +__Values must be updated in AFC_Hardware.cfg after calibration__ + ## Troubleshooting Debug information about the respooler system can be found by visiting the following URL in your browser: `{ip address}/printer/objects/query?AFC` +### LEDs not displaying correct color +If your leds are not displaying the correct color update the following value under your `AFC_led` section in `~/printer_data/config/AFC/AFC_hardware.cfg` file. + - color_order: change to match the color order for you leds. Different color orders are: RGB, RGBW, GRB, GRBW + +### Filament pulling past extruder during unloads +During unloads if your filament retracts too much and goes past the lanes extruder then decrease your `afc_bowden_length` value in `~/printer_data/config/AFC/AFC.cfg` file + +### Timer too close (TTC) error +If you keep getting TTC errors start by adding the following to `AFC/AFC.cfg` file under `[AFC]` section +- `trsync_update: True` ## Removing Plugin diff --git a/config/AFC.cfg b/config/AFC.cfg index 518d1d1..bc32969 100644 --- a/config/AFC.cfg +++ b/config/AFC.cfg @@ -14,15 +14,15 @@ short_move_dis: 10 # Move distance for failsafe moves. Default is 1 #------- Pause/Resume --------------------------------------------------------------- #--=================================================================================- z_hop: 5 # Height to move up before and after a tool change completes -resume_speed: 350 # Speed mm/s of resume move. Set to 0 to use gcode speed -resume_z_speed: 100 # Speed mm/s of resume move in Z. Set to 0 to use gcode speed +resume_speed: 120 # Speed mm/s of resume move. Set to 0 to use gcode speed +resume_z_speed: 30 # Speed mm/s of resume move in Z. Set to 0 to use gcode speed #--=================================================================================- #------- Spoolman ------------------------------------------------------------------- #--=================================================================================- -# spoolman_ip: http://spoolman.local # Spoolman IP address. -# spoolman_port: 7912 # Spoolman port. +# spoolman_ip: localhost # Spoolman IP address. +# spoolman_port: 7912 # Spoolman port. #--=================================================================================- diff --git a/config/AFC_Macro_Vars.cfg b/config/AFC_Macro_Vars.cfg index d283748..734cf3c 100644 --- a/config/AFC_Macro_Vars.cfg +++ b/config/AFC_Macro_Vars.cfg @@ -10,12 +10,13 @@ gcode: # Leave Empty # Set this to match your AFC_stepper base naming in AFC_Hardware.cfg variable_stepper_name : 'leg' # [AFC_stepper leg1] [AFC_stepper leg2]... -variable_travel_speed : 350 # Speed mm/s -variable_z_travel_speed : 100 # Speed mm/s -variable_verbose : 1 # Console Output for macros - # 0 - None - # 1 - Limited - # 2 - All +variable_travel_speed : 120 # Speed mm/s +variable_z_travel_speed : 30 # Speed mm/s +variable_accel : 2000 # Move accel for all macro moves +variable_verbose : 1 # Console Output for macros + # 0 - None + # 1 - Limited + # 2 - All #--=================================================================================- @@ -25,6 +26,28 @@ variable_verbose : 1 # Console Output for macros description: Toolhead tip cutting macro configuration variables gcode: # Leave empty +# This should be the position of the toolhead where the cutter arm just +# lightly touches the depressor pin +variable_pin_loc_xy : -1, -1 # x,y coordinates of depressor pin + +# Accel during cut. This will overwrite the global accel for this macro. Set to 0 to use global accel +variable_cut_accel : 0 + +# Direction to make the cut move (left, right, front, back) +variable_cut_direction : "left" + +# This distance is used to move toolhead to cut filament +# and to create a small saftely distance that aids in generating momentum +variable_pin_park_dist : 6.0 # Distance in mm + +# Position of the toolhead when the cutter is fully compressed. +# Distnace the toolhead needs to travel to compress the cutter arm. +# To calculate this distance start at the pin_loc_xy position and move +# your toolhead till the cutter arm is completely compressed. Take 0.5mm off this distance +# as a buffer. +# Ex pin_loc_x : 9, 310 fully compressed at 0, 310 set cut_move_dist to 8.5 +variable_cut_move_dist : 8.5 # Distance in mm + # Speed related settings for tip cutting # Note that if the cut speed is too fast, the steppers can lose steps. # Therefore, for a cut: @@ -33,9 +56,9 @@ gcode: # Leave empty # - We then make a slow move for the actual cut to happen variable_cut_fast_move_speed : 32 # Speed mm/s variable_cut_slow_move_speed : 10 # Speed mm/s -variable_evacuate_speed : 150 # Speed mm/s +variable_evacuate_speed : 150 # Speed mm/s variable_cut_dwell_time : 50 # Time to dwell at the cut point in ms -variable_cut_fast_move_fraction : 0.85 # Fraction of the move that uses fast move 0.0 - 1.0 +variable_cut_fast_move_fraction : 0.85 # Fraction of the move that uses fast move 0.0 - 1.0 variable_extruder_move_speed : 25 # Speed mm/s for all extruder movement # If the toolhead returns to initial position after the cut is complete. @@ -49,28 +72,9 @@ variable_retract_length : 20 # This can help prevent cloggin of some toolheads by doing a quick tip from to reduce stringing variable_quick_tip_forming : False -# Direction to make the cut move (left, right, front, back) -variable_cut_direction : "left" - # Number of times to run the cut movement variable_cut_count : 2 -# This should be the position of the toolhead where the cutter arm just -# lightly touches the depressor pin -variable_pin_loc_xy : 9, 310 # x,y coordinates of depressor pin - -# This distance is used to move toolhead to cut filament -# and to create a small saftely distance that aids in generating momentum -variable_pin_park_dist : 6.0 # Distance in mm - -# Position of the toolhead when the cutter is fully compressed. -# Distnace the toolhead needs to travel to compress the cutter arm. -# To calculate this distance start at the pin_loc_xy position and move -# your toolhead till the cutter arm is completely compressed. Take 0.5mm off this distance -# as a buffer. -# Ex pin_loc_x : 9, 310 fully compressed at 0, 310 set cut_move_dist to 8.5 -variable_cut_move_dist : 8.5 # Distance in mm - # Retract length and speed after the cut so that the cutter can go back # into its origin position variable_rip_length : 1.0 # Distance in mm to retract to aid lever decompression (>= 0) @@ -95,7 +99,7 @@ variable_safe_margin_xy : 30, 30 # Approx toolhead width +5mm, heig description: Poop macro configuration variables gcode: # Leave empty # (x,y) Location of where to purge -variable_purge_loc_xy : 297, 290 +variable_purge_loc_xy : -1, -1 # Speed, in mm/s, of the purge. variable_purge_spd : 6.5 @@ -152,12 +156,15 @@ variable_purge_length_minimum : 60.999 description: Kick macro configuration variables gcode: # Leave empty # Location to move before kick -variable_kick_start_loc : 250, 300, 10 +variable_kick_start_loc : -1, -1, 10 variable_kick_z : 1.5 # Height to drop to for kick move variable_kick_speed : 150 # Speed of kick movement +# Accel of kick moves. This will overwrite the global accel for this macro. Set to 0 to use global accel +variable_kick_accel : 0 + # Direction to make the kick move (left, right, front, back) variable_kick_direction : "right" @@ -173,13 +180,13 @@ variable_z_after_kick : 10 # Height of z after kick move description: Brush macro configuration variables gcode: # Leave empty # Position of the center of the brush (Set z to -1 if you dont want a z move) -variable_brush_loc : 217, 310, -1 +variable_brush_loc : -1, -1, -1 # Speed of cleaning moves when brushing variable_brush_clean_speed : 150 -# Accel of cleaning moves when brushing -variable_brush_clean_accel : 1500 +# Accel of cleaning moves when brushing. This will overwrite the global accel for this macro. Set to 0 to use global accel +variable_brush_clean_accel : 0 # Total width in mm of the brush in the X direction variable_brush_width : 30 @@ -200,7 +207,7 @@ variable_brush_count : 4 # Number of passes to make on the brush. description: Park macro configuration variables gcode: # Leave empty # Position to park the toolhead -variable_park_loc_xy : 270, 310 +variable_park_loc_xy : -1, -1 # Height to raise Z when moving to park. Leave 0 to disable # If you want z_hop during toolchanges please set the value in the AFC.cfg diff --git a/config/macros/AFC_macros.cfg b/config/macros/AFC_macros.cfg index 4d447a5..e55f480 100644 --- a/config/macros/AFC_macros.cfg +++ b/config/macros/AFC_macros.cfg @@ -41,36 +41,4 @@ gcode: [gcode_macro BT_PREP] description: Run the AFC PREP sequence gcode: - PREP - -[gcode_macro T0] -description: Change to tool 0 -gcode: - {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} - {% set ln = stepper ~ "1" %} - - CHANGE_TOOL LANE={ln} - -[gcode_macro T1] -description: Change to tool 1 -gcode: - {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} - {% set ln = stepper ~ "2" %} - - CHANGE_TOOL LANE={ln} - -[gcode_macro T2] -description: Change to tool 2 -gcode: - {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} - {% set ln = stepper ~ "3" %} - - CHANGE_TOOL LANE={ln} - -[gcode_macro T3] -description: Change to tool 3 -gcode: - {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} - {% set ln = stepper ~ "4" %} - - CHANGE_TOOL LANE={ln} \ No newline at end of file + PREP \ No newline at end of file diff --git a/config/macros/Brush.cfg b/config/macros/Brush.cfg index dbe0354..d6ce042 100644 --- a/config/macros/Brush.cfg +++ b/config/macros/Brush.cfg @@ -2,6 +2,7 @@ description: Wipe the nozzle on the brush gcode: {% set gVars = printer['gcode_macro _AFC_GLOBAL_VARS'] %} + {% set accel = gVars['accel']|float %} {% set travel_speed = gVars['travel_speed'] * 60|float %} {% set z_travel_speed = gVars['z_travel_speed'] * 60|float %} {% set verbose = gVars['verbose']|int %} @@ -28,7 +29,15 @@ gcode: # Set the cleaning acceleration prior to any movement {% set saved_accel = printer.toolhead.max_accel %} - SET_VELOCITY_LIMIT ACCEL={brush_clean_accel} + + # Check if macro accel is defined + {% if brush_clean_accel > 0 %} + {% set selected_accel = brush_clean_accel %} + {% else %} + {% set selected_accel = accel %} + {% endif %} + + SET_VELOCITY_LIMIT ACCEL={selected_accel} {% if verbose > 0 %} RESPOND TYPE=command MSG='AFC_Brush: Clean Nozzle' diff --git a/config/macros/Cut.cfg b/config/macros/Cut.cfg index f0b5972..8ec5138 100644 --- a/config/macros/Cut.cfg +++ b/config/macros/Cut.cfg @@ -2,6 +2,7 @@ description: Cut filament by pressing the cutter on a pin gcode: {% set gVars = printer['gcode_macro _AFC_GLOBAL_VARS'] %} + {% set accel = gVars['accel']|float %} {% set travel_speed = gVars['travel_speed'] * 60|float %} {% set verbose = gVars['verbose']|int %} {% set vars = printer['gcode_macro _AFC_CUT_TIP_VARS'] %} @@ -11,6 +12,7 @@ gcode: {% set quick_tip_forming = vars['quick_tip_forming']|default(true)|lower == 'true' %} {% set rip_length = vars['rip_length']|float %} {% set cut_direction = vars['cut_direction']|default('')|lower %} + {% set cut_accel = vars['cut_accel']|float %} {% set pushback_length = vars['pushback_length']|float %} {% set pushback_dwell_time = vars['pushback_dwell_time']|int %} {% set extruder_move_speed = vars['extruder_move_speed'] * 60|float %} @@ -49,6 +51,18 @@ gcode: SAVE_GCODE_STATE NAME=_AFC_CUT + # Save current Accel + {% set saved_accel = printer.toolhead.max_accel %} + + # Check if macro accel is defined + {% if cut_accel > 0 %} + {% set selected_accel = cut_accel %} + {% else %} + {% set selected_accel = accel %} + {% endif %} + + SET_VELOCITY_LIMIT ACCEL={selected_accel} + {% set prev_pa = printer.extruder.pressure_advance %} # Save current PA SET_PRESSURE_ADVANCE ADVANCE=0 # Temporarily disable PA @@ -90,6 +104,7 @@ gcode: {% if verbose > 1 %} RESPOND TYPE=command MSG='AFC_Cut: Final Cut...' {% endif %} + # Do a rip on final cut pass _DO_CUT_MOTION PIN_PARK_X_LOC={pin_park_x_loc} PIN_PARK_Y_LOC={pin_park_y_loc} RIP_LENGTH={rip_length} @@ -109,6 +124,9 @@ gcode: SET_PRESSURE_ADVANCE ADVANCE={prev_pa} # Restore PA + # Reset acceleration values to what it was before + SET_VELOCITY_LIMIT ACCEL={saved_accel} + # Restore state and optionally position RESTORE_GCODE_STATE NAME=_AFC_CUT MOVE={1 if restore_position else 0} MOVE_SPEED={travel_speed} @@ -184,7 +202,9 @@ gcode: {% if rip_length > 0 %} G1 E-{rip_length} F{rip_speed} {% endif %} + G4 P200 G1 X{pin_park_x_loc} F{evacuate_speed} # Evacuate + G4 P200 {% endif %} {% elif cut_direction == "front" or cut_direction == "back" %} {% set fast_slow_transition_loc_y = pin_park_y_loc + location_factor[cut_direction].y * (cut_dist * cut_fast_move_fraction)| float %} @@ -199,7 +219,9 @@ gcode: {% if rip_length > 0 %} G1 E-{rip_length} F{rip_speed} {% endif %} + G4 P200 G1 Y{pin_park_y_loc} F{evacuate_speed} # Evacuate + G4 P200 {% endif %} {% else %} { action_raise_error("Invalid cut direction. Check the cut_direction in your AFC_Macro_Vars.cfg file!") } diff --git a/config/macros/Kick.cfg b/config/macros/Kick.cfg index 5f818fd..0a7ef36 100644 --- a/config/macros/Kick.cfg +++ b/config/macros/Kick.cfg @@ -1,16 +1,18 @@ [gcode_macro AFC_KICK] gcode: {% set gVars = printer['gcode_macro _AFC_GLOBAL_VARS'] %} + {% set accel = gVars['accel']|float %} {% set travel_speed = gVars['travel_speed'] * 60|float %} {% set z_travel_speed = gVars['z_travel_speed'] * 60|float %} {% set verbose = gVars['verbose']|int %} {% set vars = printer['gcode_macro _AFC_KICK_VARS'] %} {% set kick_start_x, kick_start_y, kick_start_z = vars.kick_start_loc|map('float') %} {% set kick_z = vars['kick_z']|float %} - {% set kick_direction = vars['kick_direction']|default('')|lower %} %} + {% set kick_direction = vars['kick_direction']|default('')|lower %} {% set kick_move_dist = vars['kick_move_dist']|float %} {% set z_after_kick = vars['z_after_kick']|float %} {% set kick_speed = vars['kick_speed'] * 60|float %} + {% set kick_accel = vars['kick_accel']|float %} # Get printer bounds to make sure none of our kick moves fall outside of them {% set x_max = printer.toolhead.axis_maximum.x %} @@ -18,6 +20,18 @@ gcode: {% set y_max = printer.toolhead.axis_maximum.y %} {% set y_min = printer.toolhead.axis_minimum.y %} + # Save current Accel + {% set saved_accel = printer.toolhead.max_accel %} + + # Check if macro accel is defined + {% if kick_accel > 0 %} + {% set selected_accel = kick_accel %} + {% else %} + {% set selected_accel = accel %} + {% endif %} + + SET_VELOCITY_LIMIT ACCEL={selected_accel} + {% if verbose > 0 %} RESPOND TYPE=command MSG='AFC_Kick: Starting Filament Kick' {% endif %} @@ -64,4 +78,7 @@ gcode: { action_raise_error("Error in kick movement. Check the directions in your AFC_Macro_Vars.cfg file!") } {% endif %} - G1 Z{z_after_kick} F{z_travel_speed} \ No newline at end of file + G1 Z{z_after_kick} F{z_travel_speed} + + # Reset acceleration values to what it was before + SET_VELOCITY_LIMIT ACCEL={saved_accel} \ No newline at end of file diff --git a/docs/Buffer_Ram_Sensor.md b/docs/Buffer_Ram_Sensor.md new file mode 100644 index 0000000..d420109 --- /dev/null +++ b/docs/Buffer_Ram_Sensor.md @@ -0,0 +1,49 @@ +# Armored Turtle Automated Filament Changer (AFC) Buffer Ram Sensor + +This file describes using a filament buffer as a ram sensor. This is part of the Armored Turtle Automated Filament Changer (AFC) project. + +## Overview + +Ram sensor is compatible with two types of buffers: [TurtleNeck](https://github.com/ArmoredTurtle/TurtleNeck), [TurtleNeck 2.0](https://github.com/ArmoredTurtle/TurtleNeck2.0). + +The filament loading and unloading use the two sensor design to evaluate the position of the filament. + +### Basic Functionality + +- During `TOOL_LOAD` filament will travel to buffer sensor and then execute the `afc_bowden_length` to the tool head + - If the buffer is expanded after the `afc_bowden_length` is complete then it will move forward with the tool load. + - If the buffer is not expanded after the `afc_bowden_length` then AFC will perform short moves until the buffer expands and the tool load will continue. + - After the `tool_stn` is complete the AFC will then pull back off the advance sensor, checking that it was loaded successfully and resetting the buffer. + +- During `TOOL_UNLOAD` AFC will perform the user specified macros (cut/tip shaping etc.). + - Once these macros are finished AFC will pull back to the trailing sensor to insure consistent position of the buffer. + - The rest of the unload will follow. + +## Configuration + +### Required Configuration + +Under `[AFC_extruder extruder]` section: + +- `pin_tool_start: buffer` + - By setting the `pin_tool_start` to `buffer` the ram sensor will be enabled. + +Under `[AFC_Buffer TN2]` +__advance_pin and trailing_pin must be defined__ + +- __advance_pin__: Pin for the advance sensor. +- __trailing_pin__: Pin for the trailing sensor. + +### Optional Configuration + +Under `[AFC_extruder extruder]` section: + +- `tool_max_load_checks: 4` can be set for the amount of times the AFC pulls back after load to come off the advance sensor. + - Default 4 +- `tool_max_unload_attempts: 2` can be set for the amount of repetitions AFC pulss back to trailing sensor on unload. + - Default 2 + +## Tuning + +- `afc_bowden_length` should be set so that on unload the filament comes just short of the hub sensor +- `tool_unload_stn` should be set so that on unload the filament clears the extruder \ No newline at end of file diff --git a/docs/command_reference.md b/docs/command_reference.md index 3d0acf3..6476416 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -5,6 +5,17 @@ The following commands are built-in the AFC-Klipper-Add-On and are available through the Klipper console. +### RESET_FAILURE +_Description_: This function clears the error state of the AFC system by setting the error state to False. +Usage: ``RESET_FAILURE`` +Example: ``RESET_FAILURE`` + +### AFC_RESUME +_Description_: This function clears the error state of the AFC system, sets the in_toolchange flag to False, +runs the resume script, and restores the toolhead position to the last saved position. +Usage: ``AFC_RESUME`` +Example: ``AFC_RESUME`` + ### AFC_STATUS _Description_: This function generates a status message for each unit and lane, indicating the preparation, loading, hub, and tool states. The status message is formatted with HTML tags for display. @@ -19,17 +30,6 @@ it uses the hub of the current lane. Usage: ``SET_BOWDEN_LENGTH HUB= LENGTH=`` Example: ``SET_BOWDEN_LENGTH HUB=Turtle_1 LENGTH=100`` -### CLEAR_ERROR -_Description_: This function clears the error state of the AFC system by setting the error state to False. -Usage: ``CLEAR_ERROR`` -Example: ``CLEAR_ERROR`` - -### AFC_RESUME -_Description_: This function clears the error state of the AFC system, sets the in_toolchange flag to False, -runs the resume script, and restores the toolhead position to the last saved position. -Usage: ``AFC_RESUME`` -Example: ``AFC_RESUME`` - ### HUB_CUT_TEST _Description_: This function tests the cutting sequence of the hub cutter for a specified lane. It retrieves the lane specified by the 'LANE' parameter, performs the hub cut, @@ -79,18 +79,14 @@ current lane and loading the new lane. Usage: ``CHANGE_TOOL LANE=`` Example: ``CHANGE_TOOL LANE=leg1`` -### SET_COLOR -_Description_: This function handles changing the color of a specified lane. It retrieves the lane -specified by the 'LANE' parameter and sets its color to the value provided by the 'COLOR' parameter. -Usage: ``SET_COLOR LANE= COLOR=`` -Example: ``SET_COLOR LANE=leg1 COLOR=FF0000`` - -### SET_SPOOLID -_Description_: This function handles setting the spool ID for a specified lane. It retrieves the lane -specified by the 'LANE' parameter and updates its spool ID, material, color, and weight -based on the information retrieved from the Spoolman API. -Usage: ``SET_SPOOLID LANE= SPOOL_ID=`` -Example: ``SET_SPOOLID LANE=leg1 SPOOL_ID=12345`` +### CALIBRATE_AFC +_Description_: This function performs the calibration of the hub and Bowden length for one or more lanes within an AFC +(Automated Filament Changer) system. The function uses precise movements to adjust the positions of the +steppers, check the state of the hubs and tools, and calculate distances for calibration based on the +user-provided input. If no specific lane is provided, the function defaults to notifying the user that no lane has been selected. The function also includes +the option to calibrate the Bowden length for a particular lane, if specified. +Usage: ``CALIBRATE_AFC LANES= DISTANCE= TOLERANCE= BOWDEN=`` +Example: `CALIBRATE_AFC LANES=all Bowden=leg1` ### SET_MULTIPLIER _Description_: This function handles the adjustment of the buffer multipliers for the turtleneck buffer. @@ -112,6 +108,31 @@ distance of the current AFC stepper motor. Usage: `QUERY_BUFFER BUFFER=` Example: `QUERY_BUFFER BUFFER=TN2` +### SET_MAP +_Description_: This function handles changing the GCODE tool change command for a Lane. +Usage: ``SET_MAP LANE= MAP=`` +Example: ``SET_MAP LANE=leg1 MAP=T1`` + +### SET_COLOR +_Description_: This function handles changing the color of a specified lane. It retrieves the lane +specified by the 'LANE' parameter and sets its color to the value provided by the 'COLOR' parameter. +Usage: ``SET_COLOR LANE= COLOR=`` +Example: ``SET_COLOR LANE=leg1 COLOR=FF0000`` + +### SET_SPOOLID +_Description_: This function handles setting the spool ID for a specified lane. It retrieves the lane +specified by the 'LANE' parameter and updates its spool ID, material, color, and weight +based on the information retrieved from the Spoolman API. +Usage: ``SET_SPOOLID LANE= SPOOL_ID=`` +Example: ``SET_SPOOLID LANE=leg1 SPOOL_ID=12345`` + +### SET_RUNOUT +_Description_: This function handles setting the runout lane (infanet spool) for a specified lane. It retrieves the lane +specified by the 'LANE' parameter and updates its the lane to use if filament is empty +based on the information retrieved from the Spoolman API. +Usage: ``SET_RUNOUT LANE= RUNOUT=`` +Example: ``SET_RUNOUT LANE=lane1 RUNOUT=lane4`` + ## AFC Macros The following macros are defined in the `config/macros/AFC_macros.cfg` file. diff --git a/extras/AFC.py b/extras/AFC.py index 5454d8a..5792f54 100644 --- a/extras/AFC.py +++ b/extras/AFC.py @@ -6,39 +6,40 @@ import json -try: - from urllib.request import urlopen -except: - # Python 2.7 support - from urllib2 import urlopen - - from configparser import Error as error class afc: def __init__(self, config): self.printer = config.get_printer() self.reactor = self.printer.get_reactor() - self.printer.register_event_handler("klippy:connect", - self.handle_connect) + self.printer.register_event_handler("klippy:connect",self.handle_connect) + self.SPOOL = self.printer.load_object(config,'AFC_spool') + self.ERROR = self.printer.load_object(config,'AFC_error') + self.IDLE = self.printer.load_object(config,'idle_timeout') self.gcode = self.printer.lookup_object('gcode') + self.gcode_move = self.printer.load_object(config, 'gcode_move') self.VarFile = config.get('VarFile') self.current = None - self.failure = False + self.error_state = False self.lanes = {} self.extruders = {} + self.tool_cmds={} self.afc_monitoring = False # tool position when tool change was requested self.change_tool_pos = None self.in_toolchange = False self.tool_start = None + + # Save/resume pos variables self.base_position = [0.0, 0.0, 0.0, 0.0] self.last_gcode_position = [0.0, 0.0, 0.0, 0.0] self.last_toolhead_position = [0.0, 0.0, 0.0, 0.0] self.homing_position = [0.0, 0.0, 0.0, 0.0] self.speed = 25. + self.absolute_coord = True + # SPOOLMAN self.spoolman_ip = config.get('spoolman_ip', None) self.spoolman_port = config.get('spoolman_port', None) @@ -48,8 +49,9 @@ def __init__(self, config): self.led_name = config.get('led_name') self.led_fault =config.get('led_fault','1,0,0,0') self.led_ready = config.get('led_ready','1,1,1,1') - self.led_not_ready = config.get('led_not_ready','1,0,0,0') - self.led_loading = config.get('led_loading','1,1,0,0') + self.led_not_ready = config.get('led_not_ready','1,1,0,0') + self.led_loading = config.get('led_loading','1,0,0,0') + self.led_prep_loaded = config.get('led_loading','1,1,0,0') self.led_unloading = config.get('led_unloading','1,1,.5,0') self.led_tool_loaded = config.get('led_tool_loaded','1,1,0,0') self.led_advancing = config.get('led_buffer_advancing','0,0,1,0') @@ -82,10 +84,49 @@ def __init__(self, config): self.short_moves_accel = config.getfloat("short_moves_accel", 400) self.short_move_dis = config.getfloat("short_move_dis", 10) self.tool_max_unload_attempts = config.getint('tool_max_unload_attempts', 2) + self.tool_max_load_checks = config.getint('tool_max_load_checks', 4) self.z_hop =config.getfloat("z_hop", 0) self.xy_resume =config.getboolean("xy_resume", False) self.resume_speed =config.getfloat("resume_speed", 0) self.resume_z_speed = config.getfloat("resume_z_speed", 0) + + self._update_trsync(config) + + self.VarFile = config.get('VarFile') + + # Get debug and cast to boolean + #self.debug = True == config.get('debug', 0) + self.debug = False + + def _update_trsync(self, config): + # Logic to update trsync values + update_trsync = config.getboolean("trsync_update", False) + if update_trsync: + try: + import mcu + trsync_value = config.getfloat("trsync_timeout", 0.05) + trsync_single_value = config.getfloat("trsync_single_timeout", 0.5) + self.gcode.respond_info("Applying TRSYNC update") + + # Making sure value exists as kalico(danger klipper) does not have TRSYNC_TIMEOUT value + if( hasattr(mcu, "TRSYNC_TIMEOUT")): mcu.TRSYNC_TIMEOUT = max(mcu.TRSYNC_TIMEOUT, trsync_value) + else : self.gcode.respond_info("TRSYNC_TIMEOUT does not exist in mcu file, not updating") + + if( hasattr(mcu, "TRSYNC_SINGLE_MCU_TIMEOUT")): mcu.TRSYNC_SINGLE_MCU_TIMEOUT = max(mcu.TRSYNC_SINGLE_MCU_TIMEOUT, trsync_single_value) + else : self.gcode.respond_info("TRSYNC_SINGLE_MCU_TIMEOUT does not exist in mcu file, not updating") + except Exception as e: + self.gcode.respond_info("Unable to update TRSYNC_TIMEOUT: {}".format(e)) + + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up the toolhead object + and assigns it to the instance variable `self.toolhead`. + """ + + self.toolhead = self.printer.lookup_object('toolhead') + + # GCODE REGISTERS self.gcode.register_command('HUB_LOAD', self.cmd_HUB_LOAD, desc=self.cmd_HUB_LOAD_help) self.gcode.register_command('LANE_UNLOAD', self.cmd_LANE_UNLOAD, desc=self.cmd_LANE_UNLOAD_help) self.gcode.register_command('TOOL_LOAD', self.cmd_TOOL_LOAD, desc=self.cmd_TOOL_LOAD_help) @@ -94,19 +135,8 @@ def __init__(self, config): self.gcode.register_command('LANE_MOVE', self.cmd_LANE_MOVE, desc=self.cmd_LANE_MOVE_help) self.gcode.register_command('TEST', self.cmd_TEST, desc=self.cmd_TEST_help) self.gcode.register_command('HUB_CUT_TEST', self.cmd_HUB_CUT_TEST, desc=self.cmd_HUB_CUT_TEST_help) - self.gcode.register_command('RESET_FAILURE', self.cmd_CLEAR_ERROR, desc=self.cmd_CLEAR_ERROR_help) - self.gcode.register_command('AFC_RESUME', self.cmd_AFC_RESUME, desc=self.cmd_AFC_RESUME_help) self.gcode.register_mux_command('SET_BOWDEN_LENGTH', 'AFC', None, self.cmd_SET_BOWDEN_LENGTH, desc=self.cmd_SET_BOWDEN_LENGTH_help) - self.gcode.register_mux_command('SET_COLOR',None,None, self.cmd_SET_COLOR, desc=self.cmd_SET_COLOR_help) - self.gcode.register_mux_command('SET_SPOOL_ID',None,None, self.cmd_SET_SPOOLID, desc=self.cmd_SET_SPOOLID_help) self.gcode.register_command('AFC_STATUS', self.cmd_AFC_STATUS, desc=self.cmd_AFC_STATUS_help) - self.VarFile = config.get('VarFile') - # Get debug and cast to boolean - #self.debug = True == config.get('debug', 0) - self.debug = False - - # Constant variable for renaming RESUME macro - self.AFC_RENAME_RESUME_NAME = '_AFC_RENAMED_RESUME_' cmd_AFC_STATUS_help = "Return current status of AFC" def cmd_AFC_STATUS(self, gcmd): @@ -132,7 +162,7 @@ def cmd_AFC_STATUS(self, gcmd): status_msg += '{} Status\n'.format(UNIT) # Create a dynamic format string that adjusts based on lane name length - header_format = '{:<{}} | Prep | Load | Hub | Tool |\n' + header_format = '{:<{}} | Prep | Load |\n' status_msg += header_format.format("LANE", max_lane_length) for LANE in self.lanes[UNIT].keys(): @@ -156,27 +186,20 @@ def cmd_AFC_STATUS(self, gcmd): else: lane_msg += '| xx |' if CUR_LANE.load_state == True: - lane_msg += ' <--> |' - else: - lane_msg += ' xx |' - - if self.current != None: - if self.current == CUR_LANE.name: - if CUR_HUB.state == True: - lane_msg += ' <-> |' - else: - lane_msg += ' xx |' - if CUR_EXTRUDER.tool_start_state == True: - lane_msg += ' <--> |\n' - else: - lane_msg += ' xx |\n' - else: - lane_msg += ' x |' - lane_msg += ' xx |\n' + lane_msg += ' <--> |\n' else: - lane_msg += ' x |' lane_msg += ' xx |\n' status_msg += lane_msg + if CUR_HUB.state == True: + status_msg += 'HUB: <->' + else: + status_msg += 'HUB: x' + if CUR_EXTRUDER.tool_start_state == True: + status_msg += ' Tool: <->' + else: + status_msg += ' Tool: x' + if CUR_EXTRUDER.tool_start == 'buffer': + status_msg += '\nRam sensor enabled' self.gcode.respond_raw(status_msg) cmd_SET_BOWDEN_LENGTH_help = "Helper to dynamically set length of bowden between hub and toolhead. Pass in HUB if using multiple box turtles" @@ -253,38 +276,30 @@ def cmd_LANE_MOVE(self, gcmd): lane = gcmd.get('LANE', None) distance = gcmd.get_float('DISTANCE', 0) CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) - CUR_LANE.move(distance, self.short_moves_speed, self.short_moves_accel) - - cmd_CLEAR_ERROR_help = "CLEAR STATUS ERROR" - def cmd_CLEAR_ERROR(self, gcmd): - """ - This function clears the error state of the AFC system by setting the error state to False. - - Usage: `CLEAR_ERROR` - Example: `CLEAR_ERROR` - - Args: - gcmd: The G-code command object containing the parameters for the command. - - Returns: - None - """ - self.set_error_state(False) + CUR_LANE.move(distance, self.short_moves_speed, self.short_moves_accel, True) def save_pos(self): # Only save previous location on the first toolchange call to keep an error state from overwriting the location if self.in_toolchange == False: - if self.failure == False: + if self.error_state == False: self.last_toolhead_position = self.toolhead.get_position() self.base_position = self.gcode_move.base_position self.last_gcode_position = self.gcode_move.last_position self.homing_position = self.gcode_move.homing_position self.speed = self.gcode_move.speed + self.absolute_coord = self.gcode_move.absolute_coord def restore_pos(self): + """ + restore_pos function restores the previous saved position, speed and coord type. The resume uses + the z_hop value to lift, move to previous x,y coords, then lower to saved z position. + """ newpos = self.toolhead.get_position() newpos[2] = self.last_gcode_position[2] + self.z_hop + # Restore absolute coords + self.gcode_move.absolute_coord = self.absolute_coord + speed = self.resume_speed if self.resume_speed > 0 else self.speed speedz = self.resume_z_speed if self.resume_z_speed > 0 else self.speed # Update GCODE STATE variables @@ -307,31 +322,6 @@ def restore_pos(self): newpos[2] = self.last_gcode_position[2] self.gcode_move.move_with_transform(newpos, speedz) - def pause_print(self): - if self.is_homed() and not self.is_paused(): - self.save_pos() - self.gcode.respond_info ('PAUSING') - self.gcode.run_script_from_command('PAUSE') - - def set_error_state(self, state): - # Only save position on first error state call - if state == True and self.failure == False: - self.save_pos() - self.failure = state - - def AFC_error(self, msg, pause=True): - # Handle AFC errors - self.gcode._respond_error( msg ) - if pause: self.pause_print() - - handle_lane_failure_help = "Get load errors, stop stepper and respond error" - def handle_lane_failure(self, CUR_LANE, message, pause=True): - # Disable the stepper for this lane - CUR_LANE.do_enable(False) - msg = (CUR_LANE.name.upper() + ' NOT READY' + message) - self.AFC_error(msg, pause) - self.afc_led(self.led_fault, CUR_LANE.led_index) - # Helper function to write variables to file. Prints with indents to make it more readable for users def save_vars(self): """ @@ -343,34 +333,6 @@ def save_vars(self): with open(self.VarFile+ '.tool', 'w') as f: f.write(json.dumps(self.extruders, indent=4)) - def handle_connect(self): - """ - Handle the connection event. - This function is called when the printer connects. It looks up the toolhead object - and assigns it to the instance variable `self.toolhead`. - """ - self.toolhead = self.printer.lookup_object('toolhead') - - cmd_AFC_RESUME_help = "Clear error state and restores position before resuming the print" - def cmd_AFC_RESUME(self, gcmd): - """ - This function clears the error state of the AFC system, sets the in_toolchange flag to False, - runs the resume script, and restores the toolhead position to the last saved position. - - Usage: `AFC_RESUME` - Example: `AFC_RESUME` - - Args: - gcmd: The G-code command object containing the parameters for the command. - - Returns: - None - """ - self.set_error_state(False) - self.in_toolchange = False - self.gcode.run_script_from_command(self.AFC_RENAME_RESUME_NAME) - self.restore_pos() - cmd_HUB_CUT_TEST_help = "Test the cutting sequence of the hub cutter, expects LANE=legN" def cmd_HUB_CUT_TEST(self, gcmd): """ @@ -418,13 +380,13 @@ def cmd_TEST(self, gcmd): """ lane = gcmd.get('LANE', None) if lane == None: - self.AFC_error('Must select LANE', False) + self.ERROR.AFC_error('Must select LANE', False) return self.gcode.respond_info('TEST ROUTINE') try: CUR_LANE = self.printer.lookup_object('AFC_stepper '+lane) - except error as e: - self.AFC_error(str(e), False) + except error: + self.ERROR.fix('could not find stepper ' + lane) #send to error handling return self.gcode.respond_info('Testing at full speed') CUR_LANE.assist(-1) @@ -506,6 +468,10 @@ def cmd_LANE_UNLOAD(self, gcmd): CUR_LANE = self.printer.lookup_object('AFC_stepper '+ lane) CUR_HUB = self.printer.lookup_object('AFC_hub '+ CUR_LANE.unit) if CUR_LANE.name != self.current: + # Setting status as ejecting so if filament is removed and de-activates the prep sensor while + # extruder motors are still running it does not trigger infinite spool or pause logic + # once user removes filament lanes status will go to None + CUR_LANE.status = 'ejecting' CUR_LANE.do_enable(True) if CUR_LANE.hub_load: CUR_LANE.move(CUR_LANE.dist_hub * -1, CUR_LANE.dist_hub_move_speed, CUR_LANE.dist_hub_move_accel, True if CUR_LANE.dist_hub > 200 else False) @@ -516,7 +482,10 @@ def cmd_LANE_UNLOAD(self, gcmd): CUR_LANE.do_enable(False) self.lanes[CUR_LANE.unit][CUR_LANE.name]['hub_loaded'] = CUR_LANE.hub_load self.save_vars() - CUR_LANE.status = None + + # Removing spool from vars since it was ejected + self.SPOOL.set_spoolID( CUR_LANE, "") + else: self.gcode.respond_info('LANE ' + CUR_LANE.name + ' IS TOOL LOADED') @@ -554,105 +523,141 @@ def TOOL_LOAD(self, CUR_LANE): CUR_LANE: The lane object to be loaded into the tool. Returns: - None + bool: True if load was successful, False if an error occurred. """ - if CUR_LANE == None: - return - # Try to get bypass filament sensor, if lookup fails default to None + if CUR_LANE is None: + # Exit early if no lane is provided. + return False + + # Check if the bypass filament sensor is triggered; abort loading if filament is already present. try: bypass = self.printer.lookup_object('filament_switch_sensor bypass').runout_helper - if bypass.filament_present == True: - return - except: bypass = None - self.failure = False + if bypass.filament_present: + self.gcode.respond_info("Filament loaded in bypass, not doing tool load") + return False + except: + bypass = None + + self.gcode.respond_info("Loading {}".format(CUR_LANE.name)) + + # Lookup extruder and hub objects associated with the lane. CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) CUR_HUB = self.printer.lookup_object('AFC_hub '+ CUR_LANE.unit) - extruder = self.toolhead.get_extruder() #Get extruder - self.heater = extruder.get_heater() #Get extruder heater + # Prepare extruder and heater. + extruder = self.toolhead.get_extruder() + self.heater = extruder.get_heater() + + # Set the lane status to 'loading' and activate the loading LED. CUR_LANE.status = 'loading' self.afc_led(self.led_loading, CUR_LANE.led_index) - if CUR_LANE.load_state == True and CUR_HUB.state == False: - if not self.heater.can_extrude: #Heat extruder if not at min temp - extruder = self.printer.lookup_object('toolhead').get_extruder() + + # Check if the lane is in a state ready to load and hub is clear. + if CUR_LANE.load_state and not CUR_HUB.state: + # Heat the extruder if it is below the minimum extrusion temperature. + if not self.heater.can_extrude: pheaters = self.printer.lookup_object('heaters') - wait = True if self.heater.target_temp <= self.heater.min_extrude_temp: - self.gcode.respond_info('Extruder below min_extrude_temp, heating to 5 degrees above min') - pheaters.set_temperature(extruder.get_heater(), self.heater.min_extrude_temp + 5, wait) + self.gcode.respond_info('Extruder below min_extrude_temp, heating to 5 degrees above min.') + pheaters.set_temperature(extruder.get_heater(), self.heater.min_extrude_temp + 5, wait=True) + + # Enable the lane for filament movement. CUR_LANE.do_enable(True) - if CUR_LANE.hub_load == False: - CUR_LANE.move(CUR_LANE.dist_hub, CUR_LANE.dist_hub_move_speed, CUR_LANE.dist_hub_move_accel, True if CUR_LANE.dist_hub > 200 else False) + + # Move filament to the hub if it's not already loaded there. + if not CUR_LANE.hub_load: + CUR_LANE.move(CUR_LANE.dist_hub, CUR_LANE.dist_hub_move_speed, CUR_LANE.dist_hub_move_accel, CUR_LANE.dist_hub > 200) + CUR_LANE.hub_load = True hub_attempts = 0 - while CUR_HUB.state == False: + + # Ensure filament moves past the hub. + while not CUR_HUB.state: if hub_attempts == 0: - CUR_LANE.move( CUR_HUB.move_dis, self.short_moves_speed, self.short_moves_accel) + CUR_LANE.move(CUR_HUB.move_dis, self.short_moves_speed, self.short_moves_accel) else: - CUR_LANE.move( self.short_move_dis, self.short_moves_speed, self.short_moves_accel) + CUR_LANE.move(self.short_move_dis, self.short_moves_speed, self.short_moves_accel) hub_attempts += 1 - #callout if filament doesn't go past hub during load if hub_attempts > 20: - self.failure = True - self.pause_print() - message = (' PAST HUB, CHECK FILAMENT PATH\n||=====||==>--||-----||\nTRG LOAD HUB TOOL') - self.handle_lane_failure(CUR_LANE, message) - return - CUR_LANE.move( CUR_HUB.afc_bowden_length, self.long_moves_speed, self.long_moves_accel, True) + message = ('PAST HUB, CHECK FILAMENT PATH\n||=====||==>--||-----||\nTRG LOAD HUB TOOL') + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + + # Move filament towards the toolhead. + CUR_LANE.move(CUR_HUB.afc_bowden_length, self.long_moves_speed, self.long_moves_accel, True) + + # Ensure filament reaches the toolhead. tool_attempts = 0 - if CUR_EXTRUDER.tool_start != None: - while CUR_EXTRUDER.tool_start_state == False: + if CUR_EXTRUDER.tool_start: + while not CUR_EXTRUDER.tool_start_state: tool_attempts += 1 - CUR_LANE.move( self.short_move_dis, CUR_EXTRUDER.tool_load_speed, self.long_moves_accel) - #callout if filament doesn't reach toolhead + CUR_LANE.move(self.short_move_dis, CUR_EXTRUDER.tool_load_speed, self.long_moves_accel) if tool_attempts > 20: - self.failure = True - message = (' FAILED TO LOAD ' + CUR_LANE.name.upper() + ' TO TOOL, CHECK FILAMENT PATH\n||=====||====||==>--||\nTRG LOAD HUB TOOL') - self.AFC_error(message) - self.set_error_state(True) - break + message = ('FAILED TO LOAD ' + CUR_LANE.name.upper() + ' TO TOOL, CHECK FILAMENT PATH\n||=====||====||==>--||\nTRG LOAD HUB TOOL') + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + + # Synchronize lane's extruder stepper and finalize tool loading. + CUR_LANE.status = 'Tooled' + CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) - if self.failure == False: + # Adjust tool position for loading. + pos = self.toolhead.get_position() + pos[3] += CUR_EXTRUDER.tool_stn + self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_load_speed) + self.toolhead.wait_moves() + + # Check if ramming is enabled, if it is go through ram load sequence. + # Lane will load until Advance sensor is True + # After the tool_stn distance the lane will retract off the sensor to confirm load and reset buffer + if CUR_EXTRUDER.tool_start == "buffer": + CUR_LANE.extruder_stepper.sync_to_extruder(None) + load_checks = 0 + while CUR_EXTRUDER.tool_start_state == True: + CUR_LANE.move( self.short_move_dis * -1, self.short_moves_speed, self.short_moves_accel ) + load_checks += 1 + self.reactor.pause(self.reactor.monotonic() + 0.1) + if load_checks > self.tool_max_load_checks: + msg = '' + msg += "Buffer did not become compressed after {} short moves.\n".format(self.tool_max_load_checks) + msg += "Tool may not be loaded" + self.gcode.respond_info("{}".format(msg)) + break CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) - CUR_LANE.status = 'Tooled' - pos = self.toolhead.get_position() - pos[3] += CUR_EXTRUDER.tool_stn - self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_load_speed) - self.toolhead.wait_moves() - self.printer.lookup_object('AFC_stepper ' + CUR_LANE.name).status = 'tool' - self.lanes[CUR_LANE.unit][CUR_LANE.name]['tool_loaded'] = True - - self.current = CUR_LANE.name - CUR_EXTRUDER.enable_buffer() - - self.afc_led(self.led_tool_loaded, CUR_LANE.led_index) - if self.poop: - self.gcode.run_script_from_command(self.poop_cmd) - if self.wipe: - self.gcode.run_script_from_command(self.wipe_cmd) - if self.kick: - self.gcode.run_script_from_command(self.kick_cmd) + # Update tool and lane status. + self.printer.lookup_object('AFC_stepper ' + CUR_LANE.name).status = 'tool' + self.lanes[CUR_LANE.unit][CUR_LANE.name]['tool_loaded'] = True + self.current = CUR_LANE.name + CUR_EXTRUDER.enable_buffer() + + # Activate the tool-loaded LED and handle filament operations if enabled. + self.afc_led(self.led_tool_loaded, CUR_LANE.led_index) + if self.poop: + self.gcode.run_script_from_command(self.poop_cmd) if self.wipe: self.gcode.run_script_from_command(self.wipe_cmd) - # Setting hub loaded outside of failure check since this could be true + if self.kick: + self.gcode.run_script_from_command(self.kick_cmd) + if self.wipe: + self.gcode.run_script_from_command(self.wipe_cmd) + + # Update lane and extruder state for tracking. self.lanes[CUR_LANE.unit][CUR_LANE.name]['hub_loaded'] = True self.extruders[CUR_LANE.extruder_name]['lane_loaded'] = CUR_LANE.name - self.set_active_spool(self.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id']) + self.SPOOL.set_active_spool(self.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id']) self.afc_led(self.led_tool_loaded, CUR_LANE.led_index) - self.save_vars() # Always save variables even if a failure happens - if self.failure == True: - self.pause_print() - self.afc_led(self.led_fault, CUR_LANE.led_index) + self.save_vars() else: - #callout if hub is triggered when trying to load - if CUR_HUB.state == True: - msg = ('HUB NOT CLEAR TRYING TO LOAD ' + CUR_LANE.name.upper() + '\n||-----||----|x|-----||\nTRG LOAD HUB TOOL') - self.AFC_error(msg) - self.afc_led(self.led_ready, CUR_LANE.led_index) - #callout if lane is not ready when trying to load - if CUR_LANE.load_state == False: - msg = (CUR_LANE.name.upper() + ' NOT READY' + '\n||==>--||----||-----||\nTRG LOAD HUB TOOL') - self.AFC_error(msg) - self.afc_led(self.led_not_ready, CUR_LANE.led_index) + # Handle errors if the hub is not clear or the lane is not ready for loading. + if CUR_HUB.state: + message = ('HUB NOT CLEAR TRYING TO LOAD ' + CUR_LANE.name.upper() + '\n||-----||----|x|-----||\nTRG LOAD HUB TOOL') + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + if not CUR_LANE.load_state: + message = (CUR_LANE.name.upper() + ' NOT READY\n||==>--||----||-----||\nTRG LOAD HUB TOOL') + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + + return True cmd_TOOL_UNLOAD_help = "Unload from tool head" def cmd_TOOL_UNLOAD(self, gcmd): @@ -678,6 +683,9 @@ def cmd_TOOL_UNLOAD(self, gcmd): CUR_LANE = self.printer.lookup_object('AFC_stepper '+ lane) self.TOOL_UNLOAD(CUR_LANE) + # User manually unloaded spool from toolhead, remove spool from active status + self.SPOOL.set_active_spool( None ) + def TOOL_UNLOAD(self, CUR_LANE): """ This function handles the unloading of a specified lane from the tool. It performs @@ -689,99 +697,160 @@ def TOOL_UNLOAD(self, CUR_LANE): CUR_LANE: The lane object to be unloaded from the tool. Returns: - None + bool: True if unloading was successful, False if an error occurred. """ - if CUR_LANE == None: - return + if CUR_LANE is None: + # If no lane is provided, exit the function early with a failure. + return False + + self.gcode.respond_info("Unloading {}".format(CUR_LANE.name)) + # Lookup current extruder and hub objects using the lane's information. CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) - CUR_HUB = self.printer.lookup_object('AFC_hub '+ CUR_LANE.unit) + CUR_HUB = self.printer.lookup_object('AFC_hub ' + CUR_LANE.unit) + + # Quick pull to prevent oozing. pos = self.toolhead.get_position() pos[3] -= 2 self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_unload_speed) self.toolhead.wait_moves() + + # Perform Z-hop to avoid collisions during unloading. pos[2] += self.z_hop self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_unload_speed) self.toolhead.wait_moves() - extruder = self.toolhead.get_extruder() #Get extruder - self.heater = extruder.get_heater() #Get extruder heater + + # Prepare the extruder and heater for unloading. + extruder = self.toolhead.get_extruder() + self.heater = extruder.get_heater() CUR_LANE.status = 'unloading' + # Disable the buffer if it's active. CUR_EXTRUDER.disable_buffer() + # Activate LED indicator for unloading. self.afc_led(self.led_unloading, CUR_LANE.led_index) - CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) - extruder = self.printer.lookup_object('toolhead').get_extruder() - pheaters = self.printer.lookup_object('heaters') + + if CUR_LANE.extruder_stepper.motion_queue != CUR_LANE.extruder_name: + # Synchronize the extruder stepper with the lane. + CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) + + # Check and set the extruder temperature if below the minimum. wait = True + pheaters = self.printer.lookup_object('heaters') if self.heater.target_temp <= self.heater.min_extrude_temp: - self.gcode.respond_info('Extruder below min_extrude_temp, heating to 5 degrees above min') + self.gcode.respond_info('Extruder below min_extrude_temp, heating to 5 degrees above min.') pheaters.set_temperature(extruder.get_heater(), self.heater.min_extrude_temp + 5, wait) + + # Enable the lane for unloading operations. CUR_LANE.do_enable(True) + + # Perform filament cutting and parking if specified. if self.tool_cut: self.gcode.run_script_from_command(self.tool_cut_cmd) if self.park: self.gcode.run_script_from_command(self.park_cmd) + + # Form filament tip if necessary. if self.form_tip: - if self.park: self.gcode.run_script_from_command(self.park_cmd) + if self.park: + self.gcode.run_script_from_command(self.park_cmd) if self.form_tip_cmd == "AFC": self.AFC_tip = self.printer.lookup_object('AFC_form_tip') self.AFC_tip.tip_form() else: self.gcode.run_script_from_command(self.form_tip_cmd) + + # Attempt to unload the filament from the extruder, retrying if needed. num_tries = 0 - while CUR_EXTRUDER.tool_start_state: - num_tries += 1 - if num_tries > self.tool_max_unload_attempts: - self.set_error_state(True) - msg = ('FAILED TO UNLOAD ' + CUR_LANE.name.upper() + '. FILAMENT STUCK IN TOOLHEAD.\n||=====||====||====|x|\nTRG LOAD HUB TOOL') - self.AFC_error(msg) - return + if CUR_EXTRUDER.tool_start == "buffer": + # if ramming is enabled, AFC will retract to collapse buffer before unloading + CUR_LANE.extruder_stepper.sync_to_extruder(None) + while CUR_EXTRUDER.buffer_trailing == False: + # attempt to return buffer to trailng pin + CUR_LANE.move( self.short_move_dis * -1, self.short_moves_speed, self.short_moves_accel ) + num_tries += 1 + self.reactor.pause(self.reactor.monotonic() + 0.1) + if num_tries > self.tool_max_unload_attempts: + msg = '' + msg += "Buffer did not become compressed after {} short moves.\n".format(self.tool_max_unload_attempts) + msg += "Increasing 'tool_max_unload_attempts' may improve loading reliablity" + self.gcode.respond_info("{}".format(msg)) + break + CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) pos = self.toolhead.get_position() - pos[3] += CUR_EXTRUDER.tool_stn_unload * -1 + pos[3] -= CUR_EXTRUDER.tool_stn_unload self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_unload_speed) self.toolhead.wait_moves() - if CUR_EXTRUDER.tool_sensor_after_extruder >0: + else: + while CUR_EXTRUDER.tool_start_state: + num_tries += 1 + if num_tries > self.tool_max_unload_attempts: + # Handle failure if the filament cannot be unloaded. + message = ('FAILED TO UNLOAD {}. FILAMENT STUCK IN TOOLHEAD.'.format(CUR_LANE.name.upper())) + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) + pos = self.toolhead.get_position() + pos[3] -= CUR_EXTRUDER.tool_stn_unload + self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_unload_speed) + self.toolhead.wait_moves() + + # Move filament past the sensor after the extruder, if applicable. + if CUR_EXTRUDER.tool_sensor_after_extruder > 0: pos = self.toolhead.get_position() - pos[3] += CUR_EXTRUDER.tool_sensor_after_extruder * -1 + pos[3] -= CUR_EXTRUDER.tool_sensor_after_extruder self.toolhead.manual_move(pos, CUR_EXTRUDER.tool_unload_speed) self.toolhead.wait_moves() + + # Synchronize and move filament out of the hub. CUR_LANE.extruder_stepper.sync_to_extruder(None) - CUR_LANE.move( CUR_HUB.afc_bowden_length * -1, self.long_moves_speed, self.long_moves_accel, True) + CUR_LANE.move(CUR_HUB.afc_bowden_length * -1, self.long_moves_speed, self.long_moves_accel, True) + + # Clear toolhead's loaded state for easier error handling later. + self.lanes[CUR_LANE.unit][CUR_LANE.name]['tool_loaded'] = False + self.lanes[CUR_LANE.unit][CUR_LANE.name]['hub_loaded'] = CUR_LANE.hub_load + self.extruders[CUR_LANE.extruder_name]['lane_loaded'] = '' + self.save_vars() + + # Ensure filament is fully cleared from the hub. num_tries = 0 - while CUR_HUB.state == True: + while CUR_HUB.state: CUR_LANE.move(self.short_move_dis * -1, self.short_moves_speed, self.short_moves_accel, True) num_tries += 1 - # callout if while unloading, filament doesn't move past HUB - if num_tries > (CUR_HUB.afc_bowden_length/self.short_move_dis): - self.set_error_state(True) - msg = (' HUB NOT CLEARING' + '\n||=====||====|x|-----||\nTRG LOAD HUB TOOL') - self.AFC_error(msg) - return - CUR_LANE.move( CUR_HUB.move_dis * -1, self.short_moves_speed, self.short_moves_accel) + if num_tries > (CUR_HUB.afc_bowden_length / self.short_move_dis): + # Handle failure if the filament doesn't clear the hub. + message = 'HUB NOT CLEARING' + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + + #Move to make sure hub path is clear based on the move_clear_dis var + CUR_LANE.move( CUR_HUB.hub_clear_move_dis * -1, self.short_moves_speed, self.short_moves_accel, True) + + # Cut filament at the hub, if configured. if CUR_HUB.cut: if CUR_HUB.cut_cmd == 'AFC': CUR_HUB.hub_cut(CUR_LANE) else: self.gcode.run_script_from_command(CUR_HUB.cut_cmd) - while CUR_HUB.state == True: + + # Confirm the hub is clear after the cut. + while CUR_HUB.state: CUR_LANE.move(self.short_move_dis * -1, self.short_moves_speed, self.short_moves_accel, True) num_tries += 1 - # callout if while unloading, filament doesn't move past HUB - if num_tries > (CUR_HUB.afc_bowden_length/self.short_move_dis): - self.set_error_state(True) - msg = (' HUB NOT CLEARING' + '\n||=====||====|x|-----||\nTRG LOAD HUB TOOL') - self.AFC_error(msg) - return + if num_tries > (CUR_HUB.afc_bowden_length / self.short_move_dis): + message = 'HUB NOT CLEARING' + self.ERROR.handle_lane_failure(CUR_LANE, message) + return False + + # Finalize unloading and reset lane state. CUR_LANE.hub_load = True - self.lanes[CUR_LANE.unit][CUR_LANE.name]['tool_loaded'] = False - self.lanes[CUR_LANE.unit][CUR_LANE.name]['hub_loaded'] = CUR_LANE.hub_load - self.extruders[CUR_LANE.extruder_name]['lane_loaded'] = '' - self.save_vars() self.afc_led(self.led_ready, CUR_LANE.led_index) CUR_LANE.status = None self.current = None CUR_LANE.do_enable(False) + return True + cmd_CHANGE_TOOL_help = "change filaments in tool head" def cmd_CHANGE_TOOL(self, gcmd): """ @@ -800,120 +869,97 @@ def cmd_CHANGE_TOOL(self, gcmd): Returns: None """ - lane = gcmd.get('LANE', None) - # Try to get bypass filament sensor, if lookup fails default to None + if not self.is_homed(): + self.ERROR.AFC_error("Please home printer before doing a toolchange", False) + return + + tmp = gcmd.get_commandline() + cmd = tmp.upper() + Tcmd = '' + if 'LANE' in cmd: + lane = gcmd.get('LANE', None) + for key in self.tool_cmds.keys(): + if self.tool_cmds[key].upper() == lane.upper(): + Tcmd = key + break + else: + Tcmd = cmd + + if Tcmd == '': + self.gcode.respond_info("I did not understand the change -- " +cmd) + return + + lane=self.tool_cmds[Tcmd] + # Check if the bypass filament sensor detects filament; if so, abort the tool change. try: bypass = self.printer.lookup_object('filament_switch_sensor bypass').runout_helper - if bypass.filament_present == True: + if bypass.filament_present: + self.gcode.respond_info("Filament loaded in bypass, not doing toolchange") return - except: bypass = None + except: + bypass = None + # If the requested lane is not the current lane, proceed with the tool change. if lane != self.current: - # Create save state + # Save the current toolhead position to allow restoration after the tool change. self.save_pos() - # Set in_toolchange flag so if there is a failure it doesnt overwrite the saved position + + # Set the in_toolchange flag to prevent overwriting the saved position during potential failures. self.in_toolchange = True + + # Lookup the lane object for the requested lane. CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) - if CUR_LANE._afc_prep_done == True: - self.gcode.respond_info(" Tool Change - " + str(self.current) + " -> " + lane) - if self.current != None: + # Check if the lane has completed the preparation process required for tool changes. + if CUR_LANE._afc_prep_done: + # Log the tool change operation for debugging or informational purposes. + self.gcode.respond_info("Tool Change - {} -> {}".format(self.current, lane)) + + # If a current lane is loaded, unload it first. + if self.current is not None: CUR_LANE = self.printer.lookup_object('AFC_stepper ' + self.current) - self.TOOL_UNLOAD(CUR_LANE) - if self.failure: + if not self.TOOL_UNLOAD(CUR_LANE): + # Abort if the unloading process fails. msg = (' UNLOAD ERROR NOT CLEARED') - self.AFC_error(msg) + self.ERROR.fix(msg) #send to error handling return + + # Switch to the new lane for loading. CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) - #CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) - self.TOOL_LOAD(CUR_LANE) - # Restore state - if self.failure == False: + + # Load the new lane and restore the toolhead position if successful. + if self.TOOL_LOAD(CUR_LANE) and not self.error_state: + self.gcode.respond_info("{} is now loaded in toolhead".format(lane)) self.restore_pos() self.in_toolchange = False - cmd_SET_COLOR_help = "change filaments color" - def cmd_SET_COLOR(self, gcmd): - """ - This function handles changing the color of a specified lane. It retrieves the lane - specified by the 'LANE' parameter and sets its color to the value provided by the 'COLOR' parameter. - - Usage: `SET_COLOR LANE= COLOR=` - Example: `SET_COLOR LANE=leg1 COLOR=FF0000` - - Args: - gcmd: The G-code command object containing the parameters for the command. - Expected parameters: - - LANE: The name of the lane whose color is to be changed. - - COLOR: The new color value in hexadecimal format (optional, defaults to '#000000'). - - Returns: - None - """ - lane = gcmd.get('LANE', None) - if lane == None: - self.gcode.respond_info("No LANE Defined") - return - color = gcmd.get('COLOR', '#000000') - CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) - CUR_LANE.color = '#' + color - self.lanes[CUR_LANE.unit][CUR_LANE.name]['color'] ='#'+ color - self.save_vars() - - def set_active_spool(self, ID): - webhooks = self.printer.lookup_object('webhooks') - if self.spoolman_ip != None: - if ID: - args = {'spool_id' : int(ID)} - try: - webhooks.call_remote_method("spoolman_set_active_spool", **args) - except self.printer.command_error: - self.gcode._respond_error("Error trying to set active spool") - else: - self.gcode.respond_info("Spool ID not set, cannot update spoolman with active spool") - - cmd_SET_SPOOLID_help = "change filaments ID" - def cmd_SET_SPOOLID(self, gcmd): - """ - This function handles setting the spool ID for a specified lane. It retrieves the lane - specified by the 'LANE' parameter and updates its spool ID, material, color, and weight - based on the information retrieved from the Spoolman API. - - Usage: `SET_SPOOLID LANE= SPOOL_ID=` - Example: `SET_SPOOLID LANE=leg1 SPOOL_ID=12345` - - Args: - gcmd: The G-code command object containing the parameters for the command. - Expected parameters: - - LANE: The name of the lane whose spool ID is to be set. - - SPOOL_ID: The new spool ID (optional, defaults to an empty string). + def get_filament_status(self, LANE): + if LANE.prep_state: + if LANE.load_state: + if self.extruders[LANE.extruder_name]['lane_loaded'] == LANE.name: + return 'In Tool:' + self.HexConvert(self.led_tool_loaded) + return "Ready:" + self.HexConvert(self.led_ready) + return 'Prep:' + self.HexConvert(self.led_prep_loaded) + return 'Not Ready:' + self.HexConvert(self.led_not_ready) + + def HexConvert(self,tmp): + led=tmp.split(',') + if float(led[0])>0: + led[0]=int(255*float(led[0])) + else: + led[0]=0 + if float(led[1])>0: + led[1]=int(255*float(led[1])) + else: + led[1]=0 + if float(led[2])>0: + led[2]=int(255*float(led[2])) + else: + led[2]=0 - Returns: - None - """ - if self.spoolman_ip !=None: - lane = gcmd.get('LANE', None) - if lane == None: - self.gcode.respond_info("No LANE Defined") - return - SpoolID = gcmd.get('SPOOL_ID', '') - CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) - if SpoolID !='': - url = 'http://' + self.spoolman_ip + ':'+ self.spoolman_port +"/api/v1/spool/" + SpoolID - result = json.load(urlopen(url)) - self.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id'] = SpoolID - self.lanes[CUR_LANE.unit][CUR_LANE.name]['material'] = result['filament']['material'] - self.lanes[CUR_LANE.unit][CUR_LANE.name]['color'] = '#' + result['filament']['color_hex'] - self.lanes[CUR_LANE.unit][CUR_LANE.name]['weight'] = result['remaining_weight'] - else: - self.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id'] = '' - self.lanes[CUR_LANE.unit][CUR_LANE.name]['material'] = '' - self.lanes[CUR_LANE.unit][CUR_LANE.name]['color'] = '' - self.lanes[CUR_LANE.unit][CUR_LANE.name]['weight'] = '' - self.save_vars() + return '#{:02x}{:02x}{:02x}'.format(*led) def get_status(self, eventtime): str = {} - numoflanes = 0 for UNIT in self.lanes.keys(): try: @@ -925,14 +971,21 @@ def get_status(self, eventtime): LANE=self.printer.lookup_object('AFC_stepper '+ NAME) str[UNIT][NAME]={} str[UNIT][NAME]['LANE'] = LANE.index - str[UNIT][NAME]['Command'] = LANE.gcode_cmd + str[UNIT][NAME]['map'] = LANE.map str[UNIT][NAME]['load'] = bool(LANE.load_state) str[UNIT][NAME]["prep"] =bool(LANE.prep_state) + str[UNIT][NAME]["tool_loaded"] = self.lanes[UNIT][NAME]['tool_loaded'] str[UNIT][NAME]["loaded_to_hub"] = self.lanes[UNIT][NAME]['hub_loaded'] str[UNIT][NAME]["material"]=self.lanes[UNIT][NAME]['material'] str[UNIT][NAME]["spool_id"]=self.lanes[UNIT][NAME]['spool_id'] str[UNIT][NAME]["color"]=self.lanes[UNIT][NAME]['color'] str[UNIT][NAME]["weight"]=self.lanes[UNIT][NAME]['weight'] + str[UNIT][NAME]["runout_lane"]=self.lanes[LANE.unit][LANE.name]['runout_lane'] + filiment_stat=self.get_filament_status(LANE).split(':') + str[UNIT][NAME]['filament_status']=filiment_stat[0] + self.lanes[UNIT][NAME]['filament_status']=filiment_stat[0] + str[UNIT][NAME]['filament_status_led']=filiment_stat[1] + self.lanes[UNIT][NAME]['filament_status_led']=filiment_stat[1] numoflanes +=1 str[UNIT]['system']={} str[UNIT]['system']['type'] = self.printer.lookup_object('AFC_hub '+ UNIT).type @@ -948,7 +1001,6 @@ def get_status(self, eventtime): str["system"]["extruders"]={} for EXTRUDE in self.extruders.keys(): - str["system"]["extruders"][EXTRUDE]={} CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + EXTRUDE) str["system"]["extruders"][EXTRUDE]['lane_loaded'] = self.extruders[LANE.extruder_name]['lane_loaded'] @@ -988,8 +1040,23 @@ def afc_led (self, status, idx=None): try: led = self.printer.lookup_object(afc_object) except: error_string = "Error: Cannot find [{}] in config, make sure led_index in config is correct for AFC_stepper {}".format(afc_object, idx.split(':')[-1]) - self.AFC_error( error_string) + self.gcode.respond_info( error_string) led.led_change(int(idx.split(':')[1]), status) + def TcmdAssign(self, CUR_LANE): + if CUR_LANE.map == 'NONE' : + for x in range(99): + cmd = 'T'+str(x) + if cmd not in self.tool_cmds: + self.lanes[CUR_LANE.unit][CUR_LANE.name]['map'] = cmd + CUR_LANE.map = cmd + break + self.tool_cmds[self.lanes[CUR_LANE.unit][CUR_LANE.name]['map']]=CUR_LANE.name + try: + self.gcode.register_command(self.lanes[CUR_LANE.unit][CUR_LANE.name]['map'], self.cmd_CHANGE_TOOL, desc=self.cmd_CHANGE_TOOL_help) + except: + self.gcode.respond_info("Error trying to map lane {lane} to {tool_macro}, please make sure there are no macros already setup for {tool_macro}".format(lane=[CUR_LANE.name], tool_macro=self.lanes[CUR_LANE.unit][CUR_LANE.name]['map']), ) + self.save_vars() + def load_config(config): return afc(config) diff --git a/extras/AFC_BoxTurtle.py b/extras/AFC_BoxTurtle.py new file mode 100644 index 0000000..cfe91d9 --- /dev/null +++ b/extras/AFC_BoxTurtle.py @@ -0,0 +1,265 @@ +class afcBoxTurtle: + def __init__(self, config): + self.printer = config.get_printer() + self.printer.register_event_handler("klippy:connect", self.handle_connect) + + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up AFC info + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + + firstLeg = '|_' + secondLeg = firstLeg + '|' + self.logo ='R _____ ____\n' + self.logo+='E / \ | o | \n' + self.logo+='A | |/ ___/ \n' + self.logo+='D |_________/ \n' + self.logo+='Y {first}{second} {first}{second}\n'.format(first=firstLeg, second=secondLeg) + + self.logo_error ='E _ _ _ _\n' + self.logo_error+='R |_|_|_|_|_|\n' + self.logo_error+='R | \____\n' + self.logo_error+='O | \ \n' + self.logo_error+='R | |\ X |\n' + self.logo_error+='! \_________/ |___|\n' + + self.AFC.gcode.register_mux_command('CALIBRATE_AFC', None, None, self.cmd_CALIBRATE_AFC, desc=self.cmd_CALIBRATE_AFC_help) + + def system_Test(self, UNIT, LANE, delay): + msg = '' + succeeded = True + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + LANE) + try: CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) + except: + error_string = 'Error: No config found for extruder: ' + CUR_LANE.extruder_name + ' in [AFC_stepper ' + CUR_LANE.name + ']. Please make sure [AFC_extruder ' + CUR_LANE.extruder_name + '] config exists in AFC_Hardware.cfg' + self.AFC.AFC_error(error_string, False) + return False + + # Run test reverse/forward on each lane + CUR_LANE.extruder_stepper.sync_to_extruder(None) + CUR_LANE.move( 5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + delay) + CUR_LANE.move( -5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + + if CUR_LANE.prep_state == False: + if CUR_LANE.load_state == False: + self.AFC.afc_led(self.AFC.led_not_ready, CUR_LANE.led_index) + msg += 'EMPTY READY FOR SPOOL' + else: + self.AFC.afc_led(self.AFC.led_fault, CUR_LANE.led_index) + CUR_LANE.status = None + msg +=" NOT READY" + CUR_LANE.do_enable(False) + msg = 'CHECK FILAMENT Prep: False - Load: True' + succeeded = False + + else: + CUR_LANE.hub_load = self.AFC.lanes[UNIT][LANE]['hub_loaded'] # Setting hub load state so it can be retained between restarts + self.AFC.afc_led(self.AFC.led_ready, CUR_LANE.led_index) + msg +="LOCKED" + if CUR_LANE.load_state == False: + msg +=" NOT LOADED" + self.AFC.afc_led(self.AFC.led_not_ready, CUR_LANE.led_index) + succeeded = False + else: + CUR_LANE.status = 'Loaded' + msg +=" AND LOADED" + + if self.AFC.lanes[UNIT][CUR_LANE.name]['tool_loaded']: + if CUR_EXTRUDER.tool_start_state == True or CUR_EXTRUDER.tool_start == "buffer": + if self.AFC.extruders[CUR_LANE.extruder_name]['lane_loaded'] == CUR_LANE.name: + CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) + msg +=" in ToolHead" + if CUR_EXTRUDER.tool_start == "buffer": + msg += "\n Ram sensor enabled, confirm tool is loaded" + self.AFC.SPOOL.set_active_spool(self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id']) + self.AFC.afc_led(self.AFC.led_tool_loaded, CUR_LANE.led_index) + if len(self.AFC.extruders) == 1: + self.AFC.current = CUR_LANE.name + CUR_EXTRUDER.enable_buffer() + else: + if CUR_EXTRUDER.tool_start_state == True: + msg +=" error in ToolHead. \nLane identified as loaded in AFC.vars.unit file\n but not identified as loaded in AFC.var.tool file" + succeeded = False + else: + lane_check=self.AFC.ERROR.fix('toolhead',CUR_LANE) #send to error handling + if not lane_check: + return False + + self.AFC.TcmdAssign(CUR_LANE) + CUR_LANE.do_enable(False) + self.AFC.gcode.respond_info( '{lane_name} tool cmd: {tcmd:3} {msg}'.format(lane_name=CUR_LANE.name.upper(), tcmd=CUR_LANE.map, msg=msg)) + CUR_LANE.set_afc_prep_done() + + return succeeded + + cmd_CALIBRATE_AFC_help = 'calibrate the dist hub for lane and then afc_bowden_length' + def cmd_CALIBRATE_AFC(self, gcmd): + """ + This function performs the calibration of the hub and Bowden length for one or more lanes within an AFC + (Automated Filament Changer) system. The function uses precise movements to adjust the positions of the + steppers, check the state of the hubs and tools, and calculate distances for calibration based on the + user-provided input. If no specific lane is provided, the function defaults to notifying the user that no lane has been selected. The function also includes + the option to calibrate the Bowden length for a particular lane, if specified. + + Usage:`CALIBRATE_AFC LANES= DISTANCE= TOLERANCE= BOWDEN=` + Examples: + - `CALIBRATE_AFC LANES=all Bowden=leg1` + - `CALIBRATE_AFC LANES=leg1 DISTANCE=30 TOLERANCE=3` (Calibrates lane 'leg1' with specific parameters) + - `CALIBRATE_AFC BOWDEN=leg1` (Calibrates the Bowden length for 'leg1') + + Args: + gcmd: The G-code command object containing the parameters for the command. + Parameters: + - LANES: Specifies the lane to calibrate. If not provided, calibrates no lanes. + - DISTANCE: The distance to move during calibration (optional, defaults to 25mm). + - TOLERANCE: The tolerance for fine adjustments during calibration (optional, defaults to 5mm). + - BOWDEN: Specifies the lane to perform Bowden length calibration (optional). + + Returns: + None + """ + dis = gcmd.get_float('DISTANCE', 25) + tol = gcmd.get_float('TOLERANCE', 5) + afc_bl = gcmd.get('BOWDEN', None) + short_dis = self.AFC.short_move_dis + lanes = gcmd.get('LANES', None) + + if self.AFC.current is not None: + self.AFC.gcode.respond_info('Tool must be unloaded to calibrate Bowden length') + return + + cal_msg = '' + + # Helper functions for movement and calibration + def calibrate_hub(CUR_LANE, CUR_HUB): + hub_pos = 0 + hub_pos = move_until_state(CUR_LANE, lambda: CUR_HUB.state, CUR_HUB.move_dis, tol, short_dis, hub_pos) + tuned_hub_pos = calc_position(CUR_LANE, lambda: CUR_HUB.state, hub_pos, short_dis, tol) + return tuned_hub_pos + + def move_until_state(lane, state, move_dis, tolerance, short_move, pos=0): + while state() == False: + lane.move(move_dis, self.AFC.short_moves_speed, self.AFC.short_moves_accel) + pos += move_dis + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + 0.1) + while state() == True: + lane.move(short_move * -1, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + pos -= short_move + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + 0.1) + while state() == False: + lane.move(tolerance, self.AFC.short_moves_speed, self.AFC.short_moves_accel) + pos += tolerance + return pos + + def calc_position(lane, state, pos, short_move, tolerance): + while state(): + lane.move(short_move * -1, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + pos -= short_move + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + 0.1) + while not state(): + lane.move(tolerance, self.AFC.short_moves_speed, self.AFC.short_moves_accel) + pos += tolerance + return pos + + # Determine if a specific lane is provided + if lanes is not None: + self.AFC.gcode.respond_info('Starting AFC distance Calibrations') + cal_msg += 'AFC Calibration distances +/-{}mm'.format(tol) + cal_msg += '\nUpdate values in AFC_Hardware.cfg' + if lanes != 'all': + lane_to_calibrate = None + # Search for the lane within the units + for UNIT in self.AFC.lanes.keys(): + if lanes in self.AFC.lanes[UNIT]: + lane_to_calibrate = lanes + break + if lane_to_calibrate is None: + self.AFC.gcode.respond_info('{} not found in any unit.'.format(lanes)) + return + + # Calibrate the specific lane + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane_to_calibrate) + CUR_HUB = self.printer.lookup_object('AFC_hub ' + CUR_LANE.unit) + + if CUR_HUB.state: + self.AFC.gcode.respond_info('Hub is not clear, check before calibration') + return + if not CUR_LANE.load_state: + self.AFC.gcode.respond_info('{} not loaded, load before calibration'.format(CUR_LANE.name.upper())) + return + + self.AFC.gcode.respond_info('Calibrating {}'.format(CUR_LANE.name.upper())) + # reset to extruder + calc_position(CUR_LANE, lambda: CUR_LANE.load_state, 0, short_dis, tol) + hub_pos = calibrate_hub(CUR_LANE, CUR_HUB) + if CUR_HUB.state: + CUR_LANE.move(CUR_HUB.move_dis * -1, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + cal_msg += '\n{} dist_hub: {}'.format(CUR_LANE.name.upper(), (hub_pos - short_dis)) + CUR_LANE.hub_load = True + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['hub_loaded'] = CUR_LANE.hub_load + else: + # Calibrate all lanes if no specific lane is provided + for UNIT in self.AFC.lanes.keys(): + for LANE in self.AFC.lanes[UNIT].keys(): + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + LANE) + CUR_HUB = self.printer.lookup_object('AFC_hub ' + CUR_LANE.unit) + + if CUR_HUB.state: + self.AFC.gcode.respond_info('Hub is not clear, check before calibration') + return + if not CUR_LANE.load_state: + self.AFC.gcode.respond_info('{} not loaded, skipping calibration'.format(CUR_LANE.name.upper())) + continue + + self.AFC.gcode.respond_info('Calibrating {}'.format(CUR_LANE.name.upper())) + # reset to extruder + calc_position(CUR_LANE, lambda: CUR_LANE.load_state, 0, short_dis, tol) + hub_pos = calibrate_hub(CUR_LANE, CUR_HUB) + if CUR_HUB.state: + CUR_LANE.move(CUR_HUB.move_dis * -1, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + cal_msg += '\n{} dist_hub: {}'.format(CUR_LANE.name.upper(), (hub_pos - short_dis)) + CUR_LANE.hub_load = True + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['hub_loaded'] = CUR_LANE.hub_load + else: + cal_msg +='No lanes selected to calibrate dist_hub' + + if afc_bl is not None: + if lanes is None: + self.AFC.gcode.respond_info('Starting AFC distance Calibrations') + cal_msg += 'AFC Calibration distances +/-{}mm'.format(tol) + cal_msg += '\nUpdate values in AFC_Hardware.cfg' + lane = afc_bl + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) + CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) + CUR_HUB = self.printer.lookup_object('AFC_hub ' + CUR_LANE.unit) + self.AFC.gcode.respond_info('Calibrating Bowden Length with {}'.format(CUR_LANE.name.upper())) + + move_until_state(CUR_LANE, lambda: CUR_HUB.state, CUR_HUB.move_dis, tol, short_dis) + + bow_pos = 0 + if CUR_EXTRUDER.tool_start: + while not CUR_EXTRUDER.tool_start_state: + CUR_LANE.move(dis, self.AFC.short_moves_speed, self.AFC.short_moves_accel) + bow_pos += dis + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + 0.1) + bow_pos = calc_position(CUR_LANE, lambda: CUR_EXTRUDER.tool_start_state, bow_pos, short_dis, tol) + CUR_LANE.move(bow_pos * -1, self.AFC.long_moves_speed, self.AFC.long_moves_accel, True) + calibrate_hub(CUR_LANE, CUR_HUB) + if CUR_HUB.state: + CUR_LANE.move(CUR_HUB.move_dis * -1, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + if CUR_EXTRUDER.tool_start == 'buffer': + cal_msg += '\n afc_bowden_length: {}'.format(bow_pos - (short_dis * 2)) + else: + cal_msg += '\n afc_bowden_length: {}'.format(bow_pos - short_dis) + else: + self.AFC.gcode.respond_info('CALIBRATE_AFC is not currently supported without tool start sensor') + + self.AFC.save_vars() + self.AFC.gcode.respond_info(cal_msg) + +def load_config(config): + return afcBoxTurtle(config) \ No newline at end of file diff --git a/extras/AFC_NightOwl.py b/extras/AFC_NightOwl.py new file mode 100644 index 0000000..461d30a --- /dev/null +++ b/extras/AFC_NightOwl.py @@ -0,0 +1,92 @@ +class afcNightOwl: + def __init__(self, config): + self.printer = config.get_printer() + self.printer.register_event_handler("klippy:connect", self.handle_connect) + + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up AFC info + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + + self.logo = 'Night Owl Ready' + self.logo ='R , ,\n' + self.logo+='E )\___/(\n' + self.logo+='A {(@)v(@)}\n' + self.logo+='D {|~~~|}\n' + self.logo+='Y {/^^^\}\n' + self.logo+='! `m-m`\n' + + self.logo_error = 'Night Owl Not Ready\n' + + def system_Test(self, UNIT, LANE, delay): + msg = '' + succeeded = True + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + LANE) + try: CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) + except: + error_string = 'Error: No config found for extruder: ' + CUR_LANE.extruder_name + ' in [AFC_stepper ' + CUR_LANE.name + ']. Please make sure [AFC_extruder ' + CUR_LANE.extruder_name + '] config exists in AFC_Hardware.cfg' + self.AFC.AFC_error(error_string, False) + return False + + # Run test reverse/forward on each lane + CUR_LANE.extruder_stepper.sync_to_extruder(None) + CUR_LANE.move( 5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + delay) + CUR_LANE.move( -5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + + if CUR_LANE.prep_state == False: + if CUR_LANE.load_state == False: + self.AFC.afc_led(self.AFC.led_not_ready, CUR_LANE.led_index) + msg += 'EMPTY READY FOR SPOOL' + else: + self.AFC.afc_led(self.AFC.led_fault, CUR_LANE.led_index) + CUR_LANE.status = None + msg +=" NOT READY" + CUR_LANE.do_enable(False) + msg = 'CHECK FILAMENT Prep: False - Load: True' + succeeded = False + + else: + CUR_LANE.hub_load = self.AFC.lanes[UNIT][LANE]['hub_loaded'] # Setting hub load state so it can be retained between restarts + self.AFC.afc_led(self.AFC.led_ready, CUR_LANE.led_index) + msg +="LOCKED" + if CUR_LANE.load_state == False: + msg +=" NOT LOADED" + self.AFC.afc_led(self.AFC.led_not_ready, CUR_LANE.led_index) + succeeded = False + else: + CUR_LANE.status = 'Loaded' + msg +=" AND LOADED" + + if self.AFC.lanes[UNIT][CUR_LANE.name]['tool_loaded']: + if CUR_EXTRUDER.tool_start_state == True or CUR_EXTRUDER.tool_start == "buffer": + if self.AFC.extruders[CUR_LANE.extruder_name]['lane_loaded'] == CUR_LANE.name: + CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) + msg +=" in ToolHead" + if CUR_EXTRUDER.tool_start == "buffer": + msg += "\n Ram sensor enabled, confirm tool is loaded" + self.AFC.SPOOL.set_active_spool(self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id']) + self.AFC.afc_led(self.AFC.led_tool_loaded, CUR_LANE.led_index) + if len(self.AFC.extruders) == 1: + self.AFC.current = CUR_LANE.name + CUR_EXTRUDER.enable_buffer() + else: + if CUR_EXTRUDER.tool_start_state == True: + msg +=" error in ToolHead. \nLane identified as loaded in AFC.vars.unit file\n but not identified as loaded in AFC.var.tool file" + succeeded = False + else: + lane_check=self.AFC.ERROR.fix('toolhead',CUR_LANE) #send to error handling + if not lane_check: + return False + + self.AFC.TcmdAssign(CUR_LANE) + CUR_LANE.do_enable(False) + self.AFC.gcode.respond_info( '{lane_name} tool cmd: {tcmd:3} {msg}'.format(lane_name=CUR_LANE.name.upper(), tcmd=CUR_LANE.map, msg=msg)) + CUR_LANE.set_afc_prep_done() + return succeeded + +def load_config(config): + return afcNightOwl(config) \ No newline at end of file diff --git a/extras/AFC_buffer.py b/extras/AFC_buffer.py index f3dd6ff..d34cb60 100644 --- a/extras/AFC_buffer.py +++ b/extras/AFC_buffer.py @@ -13,15 +13,17 @@ class AFCtrigger: def __init__(self, config): self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object('gcode') + self.AFC = self.printer.lookup_object('AFC') + self.reactor = self.AFC.reactor + self.gcode = self.AFC.gcode + self.name = config.get_name().split(' ')[-1] self.turtleneck = False self.belay = False self.last_state = False self.enable = False self.current = '' - self.AFC = self.printer.lookup_object('AFC') + self.debug = config.getboolean("debug", False) self.buttons = self.printer.load_object(config, "buttons") @@ -66,7 +68,6 @@ def __init__(self, config): raise error( msg ) self.printer.register_event_handler("klippy:ready", self._handle_ready) - self.gcode.register_mux_command("QUERY_BUFFER", "BUFFER", self.name, self.cmd_QUERY_BUFFER, desc=self.cmd_QUERY_BUFFER_help) # Belay Buffer @@ -160,27 +161,23 @@ def reset_multiplier(self): def advance_callback(self, eventime, state): if self.printer.state_message == 'Printer is ready' and self.enable: CUR_LANE = self.printer.lookup_object('AFC_stepper ' + self.AFC.current) - CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) - if CUR_EXTRUDER.tool_start_state: - if self.AFC.current != None and state: - CUR_LANE.assist(CUR_LANE.calculate_pwm_value(self.AFC.gcode_move.speed * (self.velocity / 10))) - self.reactor.pause(self.reactor.monotonic() + 1) - CUR_LANE.assist(0) - self.set_multiplier( self.multiplier_low ) - if self.debug: self.gcode.respond_info("Buffer Triggered State: Advanced") + if self.AFC.current != None and state: + CUR_LANE.assist(CUR_LANE.calculate_pwm_value(self.AFC.gcode_move.speed * (self.velocity / 10))) + self.reactor.pause(self.reactor.monotonic() + 1) + CUR_LANE.assist(0) + self.set_multiplier( self.multiplier_low ) + if self.debug: self.gcode.respond_info("Buffer Triggered State: Advanced") self.last_state = ADVANCE_STATE_NAME def trailing_callback(self, eventime, state): if self.printer.state_message == 'Printer is ready' and self.enable: CUR_LANE = self.printer.lookup_object('AFC_stepper ' + self.AFC.current) - CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) - if CUR_EXTRUDER.tool_start_state: - if self.AFC.current != None and state: - CUR_LANE.assist(CUR_LANE.calculate_pwm_value(self.AFC.gcode_move.speed * (self.velocity / 10))) - self.reactor.pause(self.reactor.monotonic() + 1) - CUR_LANE.assist(0) - self.set_multiplier( self.multiplier_high ) - if self.debug: self.gcode.respond_info("Buffer Triggered State: Trailing") + if self.AFC.current != None and state: + CUR_LANE.assist(CUR_LANE.calculate_pwm_value(self.AFC.gcode_move.speed * (self.velocity / 10))) + self.reactor.pause(self.reactor.monotonic() + 1) + CUR_LANE.assist(0) + self.set_multiplier( self.multiplier_high ) + if self.debug: self.gcode.respond_info("Buffer Triggered State: Trailing") self.last_state = TRAILING_STATE_NAME def buffer_status(self): diff --git a/extras/AFC_error.py b/extras/AFC_error.py new file mode 100644 index 0000000..9bc9304 --- /dev/null +++ b/extras/AFC_error.py @@ -0,0 +1,145 @@ + + +class afcError: + def __init__(self, config): + self.printer = config.get_printer() + self.printer.register_event_handler("klippy:connect", self.handle_connect) + self.errorLog= {} + self.pause= False + + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up AFC info + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + # Constant variable for renaming RESUME macro + self.AFC_RENAME_RESUME_NAME = '_AFC_RENAMED_RESUME_' + + self.AFC.gcode.register_command('RESET_FAILURE', self.cmd_RESET_FAILURE, desc=self.cmd_RESET_FAILURE_help) + self.AFC.gcode.register_command('AFC_RESUME', self.cmd_AFC_RESUME, desc=self.cmd_AFC_RESUME_help) + + def fix(self, problem, LANE=None): + self.pause= True + self.AFC = self.printer.lookup_object('AFC') + error_handled = False + if problem == None: + self.PauseUserIntervention('Paused for unknown error') + if problem=='toolhead': + error_handled = self.ToolHeadFix(LANE) + else: + self.PauseUserIntervention(problem) + if not error_handled: + self.AFC.afc_led(self.AFC.led_fault, LANE.led_index) + + return error_handled + + def ToolHeadFix(self, CUR_LANE): + CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) + if CUR_EXTRUDER.tool_start_state: #toolhead has filament + if self.AFC.extruders[CUR_LANE.extruder_name]['lane_loaded'] == CUR_LANE.name: #var has right lane loaded + if CUR_LANE.load_state == False: #Lane has filament + self.PauseUserIntervention('Filament not loaded in Lane') + else: + self.PauseUserIntervention('no error detected') + else: + self.PauseUserIntervention('laneloaded does not match extruder') + + else: #toolhead empty + if CUR_LANE.load_state == True: #Lane has filament + while CUR_LANE.load_state == True: # slowly back filament up to lane extruder + CUR_LANE.move(-5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + while CUR_LANE.load_state == False: # reload lane extruder + CUR_LANE.move(5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['tool_loaded'] = False + self.AFC.extruders[CUR_LANE.extruder_name]['lane_loaded']= '' + self.AFC.save_vars() + self.pause = False + return True + + else: + self.PauseUserIntervention('Filament not loaded in Lane') + + def PauseUserIntervention(self,message): + #pause for user intervention + self.AFC.gcode._respond_error(message) + if self.AFC.is_homed() and not self.AFC.is_paused(): + self.AFC.save_pos() + if self.pause: + self.pause_print() + + def pause_print(self): + """ + pause_print function verifies that the printer is homed and not currently paused before calling + the base pause command + """ + self.set_error_state( True ) + self.AFC.gcode.respond_info ('PAUSING') + self.AFC.gcode.run_script_from_command('PAUSE') + + def set_error_state(self, state): + # Only save position on first error state call + if state == True and self.AFC.error_state == False: + self.AFC.save_pos() + self.AFC.error_state = state + + def AFC_error(self, msg, pause=True): + # Handle AFC errors + self.AFC.gcode._respond_error( msg ) + if pause: self.pause_print() + + + cmd_RESET_FAILURE_help = "CLEAR STATUS ERROR" + def cmd_RESET_FAILURE(self, gcmd): + """ + This function clears the error state of the AFC system by setting the error state to False. + + Usage: `RESET_FAILURE` + Example: `RESET_FAILURE` + + Args: + gcmd: The G-code command object containing the parameters for the command. + + Returns: + None + """ + self.set_error_state(False) + self.pause = False + + cmd_AFC_RESUME_help = "Clear error state and restores position before resuming the print" + def cmd_AFC_RESUME(self, gcmd): + """ + This function clears the error state of the AFC system, sets the in_toolchange flag to False, + runs the resume script, and restores the toolhead position to the last saved position. + + Usage: `AFC_RESUME` + Example: `AFC_RESUME` + + Args: + gcmd: The G-code command object containing the parameters for the command. + + Returns: + None + """ + self.AFC.in_toolchange = False + self.AFC.gcode.run_script_from_command(self.AFC_RENAME_RESUME_NAME) + + #The only time our resume should restore position is if there was an error that caused the pause + if self.AFC.error_state: + self.set_error_state(False) + self.AFC.restore_pos() + self.pause = False + + handle_lane_failure_help = "Get load errors, stop stepper and respond error" + def handle_lane_failure(self, CUR_LANE, message, pause=True): + # Disable the stepper for this lane + CUR_LANE.do_enable(False) + CUR_LANE.status = 'Error' + msg = (CUR_LANE.name.upper() + ' NOT READY' + message) + self.AFC_error(msg, pause) + self.AFC.afc_led(self.AFC.led_fault, CUR_LANE.led_index) + +def load_config(config): + return afcError(config) + diff --git a/extras/AFC_extruder.py b/extras/AFC_extruder.py index 5bdef6a..30982ae 100644 --- a/extras/AFC_extruder.py +++ b/extras/AFC_extruder.py @@ -7,32 +7,65 @@ class AFCextruder: def __init__(self, config): self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() + self.printer.register_event_handler("klippy:connect", self.handle_connect) + self.name = config.get_name().split(' ')[-1] self.tool_stn = config.getfloat("tool_stn", 72) self.tool_stn_unload = config.getfloat("tool_stn_unload", 100) self.tool_sensor_after_extruder = config.getfloat("tool_sensor_after_extruder", 0) self.tool_unload_speed = config.getfloat("tool_unload_speed", 25) self.tool_load_speed = config.getfloat("tool_load_speed", 25) + ppins = self.printer.lookup_object('pins') + self.gcode = self.printer.lookup_object('gcode') + # BUFFER self.buffer_name = config.get('buffer', None) self.buffer = None - self.printer.register_event_handler("klippy:connect", self._handle_ready) buttons = self.printer.load_object(config, "buttons") self.tool_start = config.get('pin_tool_start', None) self.tool_end = config.get('pin_tool_end', None) + + # RAMMING + # Use buffer sensors for loading and unloading filament + if self.tool_start == "buffer": + self.r_enabled = False + b = config.getsection("AFC_buffer {}".format(self.buffer_name)) + ap = b.get('advance_pin', None) + tp = b.get('trailing_pin', None) + if ap is not None and tp is not None: + self.r_enabled = True + self.advance_pin = ap + self.trailing_pin = tp + pins = ap.strip("!^"), tp.strip("!^") + for pin_desc in pins: + ppins.allow_multi_use_pin(pin_desc) + else: + self.gcode.respond_info("advance_pin and trailing_pin must be defined to enable ram sensor") + if self.tool_start is not None: - self.tool_start_state = False - buttons.register_buttons([self.tool_start], self.tool_start_callback) + if self.tool_start == "buffer" and self.r_enabled == True: + self.tool_start_state = False + self.buffer_trailing = False + buttons.register_buttons([self.advance_pin], self.tool_start_callback) + buttons.register_buttons([self.trailing_pin], self.buffer_trailing_callback) + else: + self.tool_start_state = False + buttons.register_buttons([self.tool_start], self.tool_start_callback) if self.tool_end is not None: self.tool_end_state = False buttons.register_buttons([self.tool_end], self.tool_end_callback) - def tool_start_callback(self, eventtime, state): - self.tool_start_state = state - def tool_end_callback(self, eventtime, state): - self.tool_end_state = state + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up AFC info + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + self.reactor = self.AFC.reactor + + self.get_buffer() def get_buffer(self): """ @@ -43,8 +76,12 @@ def get_buffer(self): if self.buffer_name is not None: self.buffer = self.printer.lookup_object('AFC_buffer ' + self.buffer_name) - def _handle_ready(self): - self.get_buffer() + def tool_start_callback(self, eventtime, state): + self.tool_start_state = state + def buffer_trailing_callback(self, eventtime, state): + self.buffer_trailing = state + def tool_end_callback(self, eventtime, state): + self.tool_end_state = state def enable_buffer(self): """ diff --git a/extras/AFC_hub.py b/extras/AFC_hub.py index 955c797..2ecf073 100644 --- a/extras/AFC_hub.py +++ b/extras/AFC_hub.py @@ -1,17 +1,19 @@ -from . import AFC + +from configparser import Error as error class afc_hub: def __init__(self, config): - self.AFC = AFC.afc self.printer = config.get_printer() - self.name = config.get_name().split()[-1] - self.AFC = self.printer.lookup_object('AFC') - self.gcode = self.printer.lookup_object('gcode') - self.reactor = self.printer.get_reactor() + self.printer.register_event_handler("klippy:connect", self.handle_connect) - self.AFC = self.printer.lookup_object('AFC') + self.name = config.get_name().split()[-1] self.type = config.get('type', None) + try: + self.unit = self.printer.load_object(config, "AFC_{}".format(self.type.replace("_", ""))) + except: + raise error("{} not supported, please remove or fix correct type for AFC_hub in your configuration".format(self.type)) + # HUB Cut variables # Next two variables are used in AFC self.cut = config.getboolean("cut", False) @@ -26,6 +28,7 @@ def __init__(self, config): self.cut_confirm = config.getboolean("cut_confirm", 0) self.move_dis = config.getfloat("move_dis", 50) + self.hub_clear_move_dis = config.getfloat("hub_clear_move_dis", 10) self.afc_bowden_length = config.getfloat("afc_bowden_length", 900) self.config_bowden_length = self.afc_bowden_length # Used by SET_BOWDEN_LENGTH macro buttons = self.printer.load_object(config, "buttons") @@ -34,6 +37,17 @@ def __init__(self, config): self.state = False buttons.register_buttons([self.switch_pin], self.switch_pin_callback) + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up AFC info + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + self.gcode = self.AFC.gcode + self.reactor = self.AFC.reactor + + def switch_pin_callback(self, eventtime, state): self.state = state diff --git a/extras/AFC_prep.py b/extras/AFC_prep.py index 2c690ff..cd29c44 100644 --- a/extras/AFC_prep.py +++ b/extras/AFC_prep.py @@ -15,15 +15,23 @@ class afcPrep: def __init__(self, config): self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object('gcode') - self.gcode.register_command('PREP', self.PREP, desc=None) - self.enable = config.getboolean("enable", False) + self.printer.register_event_handler("klippy:connect", self.handle_connect) self.delay = config.getfloat('delay_time', 0.1, minval=0.0) + self.enable = config.getboolean("enable", False) + # Flag to set once resume rename as occured for the first time self.rename_occured = False + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up AFC info + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + self.AFC.gcode.register_command('PREP', self.PREP, desc=None) + def _rename_resume(self): """ Helper function to check if renaming RESUME macro has occured and renames RESUME. @@ -35,19 +43,18 @@ def _rename_resume(self): self.rename_occured = True # Renaming users Resume macro so that RESUME calls AFC_Resume function instead base_resume_name = "RESUME" - prev_cmd = self.gcode.register_command(base_resume_name, None) + prev_cmd = self.AFC.gcode.register_command(base_resume_name, None) if prev_cmd is not None: pdesc = "Renamed builtin of '%s'" % (base_resume_name,) - self.gcode.register_command(self.AFC.AFC_RENAME_RESUME_NAME, prev_cmd, desc=pdesc) + self.AFC.gcode.register_command(self.AFC.ERROR.AFC_RENAME_RESUME_NAME, prev_cmd, desc=pdesc) else: - self.gcode.respond_info("{}Existing command {} not found in gcode_macros{}".format("", base_resume_name, "",)) + self.AFC.gcode.respond_info("{}Existing command {} not found in gcode_macros{}".format("", base_resume_name, "",)) - self.gcode.register_command(base_resume_name, self.AFC.cmd_AFC_RESUME, desc=self.AFC.cmd_AFC_RESUME_help) + self.AFC.gcode.register_command(base_resume_name, self.AFC.ERROR.cmd_AFC_RESUME, desc=self.AFC.ERROR.cmd_AFC_RESUME_help) def PREP(self, gcmd): - self.AFC = self.printer.lookup_object('AFC') while self.printer.state_message != 'Printer is ready': - self.reactor.pause(self.reactor.monotonic() + 1) + self.AFC.reactor.pause(self.AFC.reactor.monotonic() + 1) self._rename_resume() @@ -61,7 +68,10 @@ def PREP(self, gcmd): self.AFC.extruders=json.load(open(self.AFC.VarFile + '.tool')) else: self.AFC.extruders={} + temp=[] + + self.AFC.tool_cmds={} for PO in self.printer.objects: if 'AFC_stepper' in PO and 'tmc' not in PO: LANE=self.printer.lookup_object(PO) @@ -75,21 +85,30 @@ def PREP(self, gcmd): self.AFC.lanes[LANE.unit][LANE.name]['spool_id']='' else: if self.AFC.spoolman_ip !=None and self.AFC.lanes[LANE.unit][LANE.name]['spool_id'] != '': - url = 'http://' + self.AFC.spoolman_ip + ':'+ self.AFC.spoolman_port +"/api/v1/spool/" + self.AFC.lanes[LANE.unit][LANE.name]['spool_id'] - result = json.load(urlopen(url)) - self.AFC.lanes[LANE.unit][LANE.name]['material'] = result['filament']['material'] - self.AFC.lanes[LANE.unit][LANE.name]['color'] = '#' + result['filament']['color_hex'] - if 'remaining_weight' in result: self.AFC.lanes[LANE.unit][LANE.name]['weight'] = result['remaining_weight'] + try: + url = 'http://' + self.AFC.spoolman_ip + ':'+ self.AFC.spoolman_port +"/api/v1/spool/" + self.AFC.lanes[LANE.unit][LANE.name]['spool_id'] + result = json.load(urlopen(url)) + self.AFC.lanes[LANE.unit][LANE.name]['material'] = result['filament']['material'] + self.AFC.lanes[LANE.unit][LANE.name]['color'] = '#' + result['filament']['color_hex'] + if 'remaining_weight' in result: self.AFC.lanes[LANE.unit][LANE.name]['weight'] = result['remaining_weight'] + except: + self.AFC.ERROR.AFC_error("Error when trying to get Spoolman data for ID:{}".format(self.AFC.lanes[LANE.unit][LANE.name]['spool_id']), False) if 'material' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['material']='' if 'color' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['color']='#000000' if 'weight' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['weight'] = 0 + if 'runout_lane' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['runout_lane']='NONE' + if 'map' not in self.AFC.lanes[LANE.unit][LANE.name] or self.AFC.lanes[LANE.unit][LANE.name]['map'] is None: + self.AFC.lanes[LANE.unit][LANE.name]['map'] = 'NONE' + else: + LANE.map = self.AFC.lanes[LANE.unit][LANE.name]['map'] + if LANE.map != 'NONE': + self.AFC.lanes[LANE.unit][LANE.name]['map'] = LANE.map + self.AFC.tool_cmds[LANE.map] = LANE.name - if 'command' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['command'] = LANE.gcode_cmd if 'index' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['index'] = LANE.index if 'tool_loaded' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['tool_loaded'] = False if 'hub_loaded' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['hub_loaded'] = False - if 'tool_loaded' not in self.AFC.lanes[LANE.unit][LANE.name]: self.AFC.lanes[LANE.unit][LANE.name]['tool_loaded'] = False tmp=[] for UNIT in self.AFC.lanes.keys(): @@ -99,9 +118,12 @@ def PREP(self, gcmd): if LANE not in temp: tmp.append(LANE) for erase in tmp: del self.AFC.lanes[UNIT][erase] - self.AFC.save_vars() - if len(self.AFC.lanes) >0: + + if self.enable == False: + self.AFC.gcode.respond_info('Prep Checks Disabled') + return + elif len(self.AFC.lanes) >0: for UNIT in self.AFC.lanes.keys(): logo='' logo_error = '' @@ -110,118 +132,37 @@ def PREP(self, gcmd): error_string = 'Error: Hub for ' + UNIT + ' not found in AFC_Hardware.cfg. Please add the [AFC_Hub ' + UNIT + '] config section.' self.AFC.AFC_error(error_string, False) return - self.gcode.respond_info(CUR_HUB.type + ' ' + UNIT +' Prepping lanes') - - if CUR_HUB.type == 'Box_Turtle': - firstLeg = '|_' - secondLeg = firstLeg + '|' - logo ='R _____ ____\n' - logo+='E / \ | o | \n' - logo+='A | |/ ___/ \n' - logo+='D |_________/ \n' - logo+='Y {first}{second} {first}{second}\n'.format(first=firstLeg, second=secondLeg) - logo+=' ' + UNIT + '\n' - - logo_error ='E _ _ _ _\n' - logo_error+='R |_|_|_|_|_|\n' - logo_error+='R | \____\n' - logo_error+='O | \ \n' - logo_error+='R | |\ X |\n' - logo_error+='! \_________/ |___|\n' - logo_error+=' ' + UNIT + '\n' - - if CUR_HUB.type == 'Night_Owl': - logo = 'Night Owl Ready' - logo_error = 'Night Owl Not Ready' - logo ='R , ,\n' - logo+='E )\___/(\n' - logo+='A {(@)v(@)}\n' - logo+='D {|~~~|}\n' - logo+='Y {/^^^\}\n' - logo+='! `m-m`\n' - logo+=' ' + UNIT + '\n' + self.AFC.gcode.respond_info(CUR_HUB.type + ' ' + UNIT +' Prepping lanes') + logo=CUR_HUB.unit.logo + logo+=' ' + UNIT + '\n' + logo_error=CUR_HUB.unit.logo_error + logo_error+=' ' + UNIT + '\n' + + LaneCheck = True for LANE in self.AFC.lanes[UNIT].keys(): - check_success = True - CUR_LANE = self.printer.lookup_object('AFC_stepper ' + LANE) - # Check each lane is assigned to a valid extruder - try: CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + CUR_LANE.extruder_name) - except: - error_string = 'Error: No config found for extruder: ' + CUR_LANE.extruder_name + ' in [AFC_stepper ' + CUR_LANE.name + ']. Please make sure [AFC_extruder ' + CUR_LANE.extruder_name + '] config exists in AFC_Hardware.cfg' - self.AFC.AFC_error(error_string, False) - check_success = False - break - - # Run test reverse/forward on each lane - if check_success == True: - CUR_LANE.extruder_stepper.sync_to_extruder(None) - CUR_LANE.move( 5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) - self.reactor.pause(self.reactor.monotonic() + self.delay) - CUR_LANE.move( -5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) - msg = '' - if CUR_LANE.prep_state == False: - if CUR_LANE.load_state == False: - self.AFC.afc_led(self.AFC.led_not_ready, CUR_LANE.led_index) - msg += 'EMPTY READY FOR SPOOL' - else: - CUR_LANE.status = None - msg +=" NOT READY" - CUR_LANE.do_enable(False) - msg = 'CHECK FILAMENT Prep: False - Load: True' - - elif CUR_LANE.prep_state == True: - CUR_LANE.hub_load = self.AFC.lanes[UNIT][LANE]['hub_loaded'] # Setting hub load state so it can be retained between restarts - self.AFC.afc_led(self.AFC.led_ready, CUR_LANE.led_index) - msg +="LOCKED" - if CUR_LANE.load_state == True: - CUR_LANE.status = 'Loaded' - msg +=" AND LOADED" - else: - msg +=" NOT LOADED" - if self.AFC.lanes[UNIT][CUR_LANE.name]['tool_loaded']: - if CUR_EXTRUDER.tool_start_state == True: - if CUR_LANE.prep_state == True and CUR_LANE.load_state == True: - CUR_LANE.extruder_stepper.sync_to_extruder(CUR_LANE.extruder_name) - msg +="\n in ToolHead" - self.AFC.set_active_spool(self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id']) - self.AFC.afc_led(self.AFC.led_tool_loaded, CUR_LANE.led_index) - if len(self.AFC.extruders) == 1: - self.AFC.current = CUR_LANE.name - CUR_EXTRUDER.enable_buffer() - else: - lane_check=self.error_tool_unload(CUR_LANE) - if lane_check != True: - check_success = False - else: - if CUR_EXTRUDER.tool_start_state == True: - if self.AFC.extruders[CUR_LANE.extruder_name]['lane_loaded'] == CUR_LANE.name: - msg +="\n error in ToolHead. Extruder loaded with no lane identified" - check_success = False - - CUR_LANE.do_enable(False) - self.gcode.respond_info(CUR_LANE.name.upper() + ' ' + msg) - CUR_LANE.set_afc_prep_done() - - for EXTRUDE in self.AFC.extruders.keys(): - CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + EXTRUDE) - if CUR_EXTRUDER.tool_start_state == True: - if not self.AFC.extruders[EXTRUDE]['lane_loaded']: - self.gcode.respond_info('Extruder loaded with out knowing Lane') - check_success = False - - if check_success == True: - self.gcode.respond_raw(logo) - else: - self.gcode.respond_raw(logo_error) - - def error_tool_unload(self, CUR_LANE): - self.gcode.respond_info('Error on filament trying to correct') - while CUR_LANE.load_state == True: - CUR_LANE.move(-5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) - while CUR_LANE.load_state == False: - CUR_LANE.move(5, self.AFC.short_moves_speed, self.AFC.short_moves_accel, True) - self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['tool_loaded'] = False - return True + if not CUR_HUB.unit.system_Test(UNIT,LANE, self.delay): + LaneCheck = False + + if LaneCheck: + self.AFC.gcode.respond_raw(logo) + else: + self.AFC.gcode.respond_raw(logo_error) + try: + bypass = self.printer.lookup_object('filament_switch_sensor bypass').runout_helper + if bypass.filament_present == True: + self.AFC.gcode.respond_info("Filament loaded in bypass, not doing toolchange") + except: bypass = None + + for EXTRUDE in self.AFC.extruders.keys(): + CUR_EXTRUDER = self.printer.lookup_object('AFC_extruder ' + EXTRUDE) + if CUR_EXTRUDER.tool_start_state == True and bypass != True: + if not self.AFC.extruders[EXTRUDE]['lane_loaded']: + self.AFC.gcode.respond_info("{} loaded with out identifying lane in AFC.vars.tool file".format(EXTRUDE)) + + # Defaulting to no active spool, putting at end so endpoint has time to register + if self.AFC.current is None: + self.AFC.SPOOL.set_active_spool( None ) def load_config(config): return afcPrep(config) diff --git a/extras/AFC_spool.py b/extras/AFC_spool.py new file mode 100644 index 0000000..1b54a47 --- /dev/null +++ b/extras/AFC_spool.py @@ -0,0 +1,192 @@ +import json +try: + from urllib.request import urlopen +except: + # Python 2.7 support + from urllib2 import urlopen + +class afcSpool: + def __init__(self, config): + self.printer = config.get_printer() + self.printer.register_event_handler("klippy:connect", self.handle_connect) + + def handle_connect(self): + """ + Handle the connection event. + This function is called when the printer connects. It looks up the AFC object + and assigns it to the instance variable `self.AFC`. + """ + self.AFC = self.printer.lookup_object('AFC') + self.ERROR = self.AFC.ERROR + self.reactor = self.AFC.reactor + self.gcode = self.AFC.gcode + + self.gcode.register_mux_command('SET_COLOR',None,None, self.cmd_SET_COLOR, desc=self.cmd_SET_COLOR_help) + self.gcode.register_mux_command('SET_SPOOL_ID',None,None, self.cmd_SET_SPOOLID, desc=self.cmd_SET_SPOOLID_help) + self.gcode.register_mux_command('SET_RUNOUT',None,None, self.cmd_SET_RUNOUT, desc=self.cmd_SET_RUNOUT_help) + self.gcode.register_mux_command('SET_MAP',None,None, self.cmd_SET_MAP, desc=self.cmd_SET_MAP_help) + + self.URL = 'http://{}:{}/api/v1/spool/'.format(self.AFC.spoolman_ip, self.AFC.spoolman_port) + + + cmd_SET_MAP_help = "change filaments color" + def cmd_SET_MAP(self, gcmd): + """ + This function handles changing the GCODE tool change command for a Lane. + + Usage: `SET_MAP LANE= MAP=` + Example: `SET_MAP LANE=leg1 MAP=T1` + + Args: + gcmd: The G-code command object containing the parameters for the command. + Expected parameters: + - LANE: The name of the lane whose color is to be changed. + - MAP: The new tool change gcode for lane (optional, defaults to None). + + Returns: + None + """ + lane = gcmd.get('LANE', None) + if lane == None: + self.gcode.respond_info("No LANE Defined") + return + map_cmd = gcmd.get('MAP', None) + lane_switch=self.AFC.tool_cmds[map_cmd] + self.gcode.respond_info("lane to switch is " + lane_switch) + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) + for UNIT_SERACH in self.AFC.lanes.keys(): + self.gcode.respond_info("looking for "+lane+" in " + UNIT_SERACH) + if lane in self.AFC.lanes[UNIT_SERACH]: + self.AFC.tool_cmds[map_cmd]=lane + map_switch=self.AFC.lanes[UNIT_SERACH][CUR_LANE.name]['map'] + self.AFC.lanes[UNIT_SERACH][CUR_LANE.name]['map']=map_cmd + CUR_LANE.map=map_cmd + + for UNIT_SERACH in self.AFC.lanes.keys(): + if lane_switch in self.AFC.lanes[UNIT_SERACH]: + SW_LANE = self.printer.lookup_object('AFC_stepper ' + lane_switch) + self.AFC.tool_cmds[map_switch]=lane_switch + self.AFC.lanes[UNIT_SERACH][lane_switch]['map']=map_switch + SW_LANE.map=map_switch + self.AFC.save_vars() + + cmd_SET_COLOR_help = "change filaments color" + def cmd_SET_COLOR(self, gcmd): + """ + This function handles changing the color of a specified lane. It retrieves the lane + specified by the 'LANE' parameter and sets its color to the value provided by the 'COLOR' parameter. + + Usage: `SET_COLOR LANE= COLOR=` + Example: `SET_COLOR LANE=leg1 COLOR=FF0000` + + Args: + gcmd: The G-code command object containing the parameters for the command. + Expected parameters: + - LANE: The name of the lane whose color is to be changed. + - COLOR: The new color value in hexadecimal format (optional, defaults to '#000000'). + + Returns: + None + """ + lane = gcmd.get('LANE', None) + if lane == None: + self.gcode.respond_info("No LANE Defined") + return + color = gcmd.get('COLOR', '#000000') + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) + CUR_LANE.color = '#' + color + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['color'] ='#'+ color + self.AFC.save_vars() + + def set_active_spool(self, ID): + webhooks = self.printer.lookup_object('webhooks') + if self.AFC.spoolman_ip != None: + if ID and ID is not None: + id = int(ID) + else: + id = None + + args = {'spool_id' : id } + try: + webhooks.call_remote_method("spoolman_set_active_spool", **args) + except self.printer.command_error as e: + self.gcode._respond_error("Error trying to set active spool \n{}".format(e)) + + cmd_SET_SPOOLID_help = "change filaments ID" + def cmd_SET_SPOOLID(self, gcmd): + """ + This function handles setting the spool ID for a specified lane. It retrieves the lane + specified by the 'LANE' parameter and updates its spool ID, material, color, and weight + based on the information retrieved from the Spoolman API. + + Usage: `SET_SPOOLID LANE= SPOOL_ID=` + Example: `SET_SPOOLID LANE=leg1 SPOOL_ID=12345` + + Args: + gcmd: The G-code command object containing the parameters for the command. + Expected parameters: + - LANE: The name of the lane whose spool ID is to be set. + - SPOOL_ID: The new spool ID (optional, defaults to an empty string). + + Returns: + None + """ + if self.AFC.spoolman_ip !=None: + lane = gcmd.get('LANE', None) + if lane == None: + self.gcode.respond_info("No LANE Defined") + return + SpoolID = gcmd.get('SPOOL_ID', '') + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) + self.set_spoolID(CUR_LANE, SpoolID) + + def set_spoolID(self, CUR_LANE, SpoolID): + if self.AFC.spoolman_ip !=None: + if SpoolID !='': + try: + url = "{}{}".format(self.URL, SpoolID) + result = json.load(urlopen(url)) + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id'] = SpoolID + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['material'] = result['filament']['material'] + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['color'] = '#' + result['filament']['color_hex'] + if 'remaining_weight' in result: self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['weight'] = result['remaining_weight'] + except: + self.AFC.ERROR.AFC_error("Error when trying to get Spoolman data for ID:{}".format(SpoolID), False) + else: + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['spool_id'] = '' + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['material'] = '' + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['color'] = '' + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['weight'] = '' + self.AFC.save_vars() + + cmd_SET_RUNOUT_help = "change filaments ID" + def cmd_SET_RUNOUT(self, gcmd): + """ + This function handles setting the runout lane (infanet spool) for a specified lane. It retrieves the lane + specified by the 'LANE' parameter and updates its the lane to use if filament is empty + based on the information retrieved from the Spoolman API. + + Usage: `SET_RUNOUT LANE= RUNOUT=` + Example: `SET_RUNOUT LANE=lane1 RUNOUT=lane4` + + Args: + gcmd: The G-code command object containing the parameters for the command. + Expected parameters: + - LANE: The name of the lane whose spool ID is to be set. + - RUNOUT: The lane to use if LANE runsout (optional, defaults to an empty string). + + Returns: + None + """ + lane = gcmd.get('LANE', None) + if lane == None: + self.gcode.respond_info("No LANE Defined") + return + runout = gcmd.get('RUNOUT', '') + CUR_LANE = self.printer.lookup_object('AFC_stepper ' + lane) + self.AFC.lanes[CUR_LANE.unit][CUR_LANE.name]['runout_lane'] = runout + self.AFC.save_vars() + self.gcode.respond_info("This is a feature WIP. Not functioning yet") + +def load_config(config): + return afcSpool(config) diff --git a/extras/AFC_stepper.py b/extras/AFC_stepper.py index ec1295d..dc19be6 100644 --- a/extras/AFC_stepper.py +++ b/extras/AFC_stepper.py @@ -46,13 +46,14 @@ def calc_move_time(dist, speed, accel): class AFCExtruderStepper: def __init__(self, config): - self.config = config self.printer = config.get_printer() + self.AFC = self.printer.lookup_object('AFC') + self.gcode = self.printer.lookup_object('gcode') self.name = config.get_name().split()[-1] self.reactor = self.printer.get_reactor() self.extruder_stepper = extruder.ExtruderStepper(config) self.extruder_name = config.get('extruder') - self.gcode_cmd = config.get('cmd',None) + self.map = config.get('cmd','NONE') self.motion_queue = None self.status = None @@ -65,7 +66,6 @@ def __init__(self, config): self.stepper_kinematics = ffi_main.gc( ffi_lib.cartesian_stepper_alloc(b'x'), ffi_lib.free) self.assist_activate=False - self.gcode = self.printer.lookup_object('gcode') # Units unit = config.get('unit', None) if unit != None: @@ -103,8 +103,6 @@ def __init__(self, config): self.afc_motor_fwd = AFC_assist.AFCassistMotor(config, 'fwd') if self.afc_motor_enb is not None: self.afc_motor_enb = AFC_assist.AFCassistMotor(config, 'enb') - self.AFC = self.printer.lookup_object('AFC') - self.gcode = self.printer.lookup_object('gcode') self.filament_diameter = config.getfloat("filament_diameter", 1.75) self.filament_density = config.getfloat("filament_density", 1.24) @@ -230,7 +228,7 @@ def prep_callback(self, eventtime, state): self.reactor.pause(self.reactor.monotonic() + 0.1) if x> 40: msg = (' FAILED TO LOAD, CHECK FILAMENT AT TRIGGER\n||==>--||----||------||\nTRG LOAD HUB TOOL') - self.AFC.AFC_error(msg) + self.AFC.AFC_error(msg, False) self.AFC.afc_led(self.AFC.led_fault, led) self.status='' break @@ -239,6 +237,20 @@ def prep_callback(self, eventtime, state): if self.load_state == True and self.prep_state == True: self.status = 'Loaded' self.AFC.afc_led(self.AFC.led_ready, led) + elif self.name == self.AFC.current and self.AFC.IDLE.state == 'Printing' and self.load_state and self.status != 'ejecting': + # Checking to make sure runout_lane is set and does not equal 'NONE' + if self.AFC.lanes[self.unit][self.name]['runout_lane'] and self.AFC.lanes[self.unit][self.name]['runout_lane'] != 'NONE': + self.status = None + self.AFC.afc_led(self.AFC.led_not_ready, led) + self.AFC.gcode.respond_info("Infinite Spool triggered for {}".format(self.name)) + empty_LANE = self.printer.lookup_object('AFC_stepper ' + self.AFC.current) + change_LANE = self.printer.lookup_object('AFC_stepper ' + self.AFC.lanes[self.unit][self.name]['runout_lane']) + self.gcode.run_script_from_command(change_LANE.map) + self.gcode.run_script_from_command('SET_MAP LANE=' + change_LANE.name + ' MAP=' + empty_LANE.map) + else: + # Pause print + self.AFC.gcode.respond_info("Runout triggered for lane {} and runout lane is not setup to switch to another lane".format(self.name)) + self.AFC.ERROR.pause_print() else: self.status = None self.AFC.afc_led(self.AFC.led_not_ready, led) diff --git a/include/auto_update_configs.sh b/include/auto_update_configs.sh index ddffb51..69311c5 100644 --- a/include/auto_update_configs.sh +++ b/include/auto_update_configs.sh @@ -7,6 +7,7 @@ function auto_update() { check_and_append_prep "${AFC_CONFIG_PATH}/AFC.cfg" + remove_t_macros # merge_configs "${AFC_CONFIG_PATH}/AFC_Hardware.cfg" "${AFC_PATH}/templates/AFC_Hardware-AFC.cfg" "${AFC_CONFIG_PATH}/AFC_Hardware-temp.cfg" # cleanup_blank_lines "${AFC_CONFIG_PATH}/AFC_Hardware-temp.cfg" # mv "${AFC_CONFIG_PATH}/AFC_Hardware-temp.cfg" "${AFC_CONFIG_PATH}/AFC_Hardware.cfg" diff --git a/include/check_commands.sh b/include/check_commands.sh index 08b31ed..f37dcd2 100644 --- a/include/check_commands.sh +++ b/include/check_commands.sh @@ -124,4 +124,31 @@ check_unzip() { print_msg ERROR " unzip command not found. Please install unzip and try again." exit 1 fi +} + +check_for_prereqs() { + if ! command -v jq &> /dev/null; then + print_msg INFO " jq is not installed. Installing jq..." + sudo apt-get update &> /dev/null + sudo apt-get install -y jq &> /dev/null + fi + if ! command -v crudini &> /dev/null; then + print_msg INFO " crudini is not installed. Installing crudini..." + sudo apt-get update &> /dev/null + sudo apt-get install -y crudini &> /dev/null + fi +} + +query_printer_status() { + local response + local state + + response=$(curl -s http://localhost/printer/objects/query?idle_timeout) + state=$(echo "$response" | jq -r '.result.status.idle_timeout.state') + + if [ "$state" == "Ready" ]; then + return 0 + else + return 1 + fi } \ No newline at end of file diff --git a/include/exit_routines.sh b/include/exit_routines.sh new file mode 100644 index 0000000..afcc465 --- /dev/null +++ b/include/exit_routines.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Armored Turtle Automated Filament Changer +# +# Copyright (C) 2024 Armored Turtle +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +exit_afc_install() { + echo "AFC_INSTALL_VERSION=$CURRENT_INSTALL_VERSION" > "${AFC_CONFIG_PATH}/.afc-version" + restart_klipper + exit 0 +} \ No newline at end of file diff --git a/include/menus.sh b/include/menus.sh index 1eebc1f..007e5e4 100644 --- a/include/menus.sh +++ b/include/menus.sh @@ -73,12 +73,14 @@ prompt_boolean() { } confirm_continue() { - # Function to prompt the user to press Y or y to continue. - # Exits the script if the user does not press Y or y. + # Function to prompt the user to press Y, y, or Enter to continue. + # Exits the script if the user does not press Y, y, or Enter. - read -p " Do you want to continue? (Y/y to continue): " -n 1 -r + read -p " Do you want to continue? (Y/n): " -n 1 -r echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then + if [[ -z $REPLY || $REPLY =~ ^[Yy]$ ]]; then + return + else print_msg ERROR " Operation aborted." exit 1 fi diff --git a/include/update_files.sh b/include/update_files.sh index cd88f85..a0725c5 100644 --- a/include/update_files.sh +++ b/include/update_files.sh @@ -206,3 +206,15 @@ replace_varfile_path() { sed -i "s|$old_path|$new_path|" "$file_path" fi } + +remove_t_macros() { + # Function to remove the T macros from the configuration file. + local t_macro + local t_macros + + t_macros=$(grep -o -E 'T[0-9]+' "${AFC_CONFIG_PATH}/macros/AFC_macros.cfg") + + for t_macro in $t_macros; do + crudini --del "${AFC_CONFIG_PATH}"/macros/AFC_macros.cfg "gcode_macro $t_macro" + done +} \ No newline at end of file diff --git a/include/utils.sh b/include/utils.sh index fb1f70c..32b8c9f 100644 --- a/include/utils.sh +++ b/include/utils.sh @@ -159,4 +159,12 @@ exclude_from_klipper_git() { echo "$relative_path" >> "$EXCLUDE_FILE" fi done -} \ No newline at end of file +} + +restart_klipper() { + if query_printer_status; then + restart_service klipper + else + print_msg ERROR " Printer is not ready, most likely printing. Ensure you restart Klipper when the printer is idle." + fi +} diff --git a/install-afc.sh b/install-afc.sh index 6368d64..fda7d46 100755 --- a/install-afc.sh +++ b/install-afc.sh @@ -173,6 +173,7 @@ if [ "$UNINSTALL" = "True" ]; then print_msg INFO " 1. Review your printer.cfg to ensure the AFC configuration is removed." print_msg INFO " 2. Remove any AFC configuration from your moonraker config." print_msg INFO " 3. Restart your Klipper service." + restart_klipper exit 0 fi @@ -180,6 +181,7 @@ fi clone_repo check_existing_install check_old_config_version +check_for_prereqs set_install_version_if_missing if [ "$FORCE_UPDATE_NO_VERSION" == "False" ]; then check_version_and_set_force_update @@ -211,7 +213,7 @@ if [ "$PRIOR_INSTALLATION" = "True" ] && [ "$FORCE_UPDATE" = False ]; then UPDATE_CONFIG=True else print_msg INFO " Skipping configuration update." - exit 0 + exit_afc_install fi fi fi @@ -222,8 +224,7 @@ if [ "$PRIOR_INSTALLATION" = "True" ] && [ "$AUTO_UPDATE_CONFIG" = True ]; then print_msg INFO " Auto updating configuration files..." print_msg INFO " Please review your configuration files for accuracy." auto_update - echo "AFC_INSTALL_VERSION=$CURRENT_INSTALL_VERSION" > "${AFC_CONFIG_PATH}/.afc-version" - exit 0 + exit_afc_install fi # If an existing installation is found, and we want to force run configuration changes. @@ -303,15 +304,13 @@ if [ "$PRIOR_INSTALLATION" = "False" ] || [ "$UPDATE_CONFIG" = "True" ]; then # Any additional configuration can be added here. check_and_append_prep "${AFC_CONFIG_PATH}/AFC.cfg" exclude_from_klipper_git + remove_t_macros # Update moonraker config update_moonraker_config print_msg INFO " Prior to starting Klipper, please review all files in the AFC directory to ensure they are correct." print_msg WARNING " This includes especially the AFC_Macro_Vars.cfg file and the pins in AFC_Hardware.cfg" - print_msg INFO " Once you have reviewed the files, restart Klipper to apply the changes." + exit_afc_install fi - -# Set the installed-version to the latest version run -echo "AFC_INSTALL_VERSION=$CURRENT_INSTALL_VERSION" > "${AFC_CONFIG_PATH}/.afc-version"