# Include servo hardware definition separately to allow for automatic upgrade
[include blobifier_hw.cfg]

##########################################################################################

# Sample config to be used in conjunction with Blobifier Purge Tray, Bucket & Nozzle 
# Scrubber mod. Created by Dendrowen (dendrowen on Discord). The Macro is based on a 
# version, and Nozzle Scrubber is made by Hernsl (hernsl#8860 on Discord). The device is 
# designed around a Voron V2.4 300mm, but should work for 250mm and 350mm too. This 
# version only supports the assembly on the rear-left of the bed. If you decide to change 
# that, please consider contributing to the project by creating a pull request with the 
# needed changes.

# IMPORTANT: The rear-left part of your bed becomes unusable by this mod because the 
#            toolhead needs to lower down to 0. Be sure not to use the left-rear 130x35mm.

# The goals of this combination of devices is to dispose of purged filament during a 
# multicolored print without the need of a purge block and without the flurries of
# filament poops consuming your entire 3D printer room. The Blobifier achieves that by
# purging onto a retractable tray which causes the filament to turn into a tiny blob 
# rather then a large spiral. This keeps the waste relatively small. The bucket should be
# able to account for up to 200 filament swaps (for the 300mm V2).

# The Blobifier uses some room at the back-left side of your printer, depending on your
# printer limits and positions. (usually max_pos.y - toolhead_y and brush_start + 
# brush_width + toolhead_x). If you do place objects within this region, Blobifier will
# skip purging automatically. It does this by extending the EXCLUDE_OBJECT_* macro's, so
# make sure you have exclude objects enabled in your slicer.

# If your using Blobifier in conjunction with the filament cutter on the stealthburner
# toolhead, you can place the pin at max_pos.y - 7 (e.g., max pos y is 307, place it at
# 300). The pin will then poke through the cavity in your toolhead. (Be careful with 
# manually moving the toolhead. I have broken many filament cutter pins)

# It is advised to use the start_gcode from Happy Hare. Then you will be able to fully 
# and efficiently use this mod. Check the Happy Hare document at gcode_preprocessing.md 
# in the Happy Hare github for more details.

###################################### DISCLAIMER ########################################

# You, and you alone, are responsible for the correct execution of these macros and 
# gcodes. Any damage that may occur to your machine remains your responsibility. 
# Especially when executing this macro for the first few times, keep an eye on your 
# printer and the 
# emergency stop.

##########################################################################################

##########################################################################################
# Main macro. Usually you should only need to call this one or place it in the Happy Hare
# _MMU_POST_LOAD macro using the variable_user_post_load_extension:
#
# variable_user_post_load_extension : `BLOBIFIER`
#
# Notes on parameters:
# PURGE_LENGTH=[float] (optional) The length to purge. If omitted (default) it will check
#                      the purge_volumes matrix or variable_purge_length. This can be used
#                      to override and for testing.
#
[gcode_macro BLOBIFIER]
# These parameters define your filament purging.
# Note that the control of retraction is set in 'mmu_macro_vars.cfg' which can be increased
# if you experience excessive oozing.
variable_purge_spd: 400                 # Speed, in mm/min, of the purge.
variable_purge_temp_min: 200            # Minimum nozzle purge temperature.
variable_toolhead_x: 70                 # From the nozzle to the left of your toolhead
variable_toolhead_y: 50                 # From the nozzle to the front of your toolhead

# This macro will prevent a gcode movement downward while 'blobbing' if there might be a
# print in the way (e.g. You print something large and need the area where Blobifier does
# its... 'business'). However, at low heights (or at print start) this might not be
# desireable. You can force a 'safe descend' with this variable. Keep in mind that the 
# height of the print is an estimation based on previous heights and certain assumptions
# so it might be wise to include a safety margin of 0.2mm
variable_force_safe_descend_height_until: 1.0 

# Adjust this so that your nozzle scrubs within the brush. Be careful not to go too low!
# Start out with a high value (like, 6) and go
# down from there.
variable_brush_top:             6

# These parameters define your scrubbing, travel speeds, safe z clearance and how many
# times you want to wipe. Update as necessary.
variable_clearance_z:           2          # When traveling, but not cleaning, the
                                           #   clearance along the z-axis between nozzle
                                           #   and brush.
variable_wipe_qty:              2          # Number of complete (A complete wipe: left,
                                           #   right, left OR right, left, right) wipes.
variable_travel_spd_xy:     10000          # Travel (not cleaning) speed along x and
                                           #   y-axis in mm/min.
variable_travel_spd_z:       1000          # Travel (not cleaning) speed along z axis
                                           #   in mm/min.
variable_wipe_spd_xy: 10000          # Nozzle wipe speed in mm/min.

# The acceleration to use when using the brush action. If set to 0, it uses the already 
# set acceleration. However, in some cases this is not desirable for the last motion 
# could be an 'outer contour' acceleration which is usually lower.
variable_brush_accel: 0

# Blobifier sends the toolhead to the maximum y position during purge operations and
# minimum x position during shake operations. This can cause issues when skew correction 
# is set up. If you have skew correction enabled and get 'move out of range' errors 
# regarding blobifier while skew is enabled, try increasing this value. Keep the 
# adjustments small though! (0.1mm - 0.5mm) and increase it until it works.
variable_skew_correction: 0.1

# These parameters define the size of the brush. Update as necessary. A visual reference
# is provided below.
#
#                  ←   brush_width   →
#                   _________________
#                  |                 |  ↑                Y position is acquired from your
#  brush_start (x) |                 | brush_depth       stepper_y position_max. Adjust
#                  |_________________|  ↓                your brush physically in Y so
#                          (y)                           that the nozzle scrubs within the
#                      brush_front                       brush.
# __________________________________________________________
#                     PRINTER FRONT
#
#
# Start location of the brush. Defaults for 250, 300 and 350mm are provided below.
# Uncomment as necessary
#variable_brush_start:          34  # For 250mm build
variable_brush_start:           67  # For 300mm build
#variable_brush_start:          84  # for 350mm build

# width of the brush
variable_brush_width: 35

# Location of where to purge. The tray is 15mm in length, so if you assemble it against 
# the side of the bed (default), 10mm is a good location
variable_purge_x: 10

# Height of the tray. If it's below your bed, give this a negative number equal to the 
# difference. If it's above your bed, give it a positive number. You can find this number 
# by homing, optional QGL or equivalent, and moving you toolhead above the tray, and 
# lowering it with the paper method. 
variable_tray_top: 0.7

# Servo angles for tray positions
variable_tray_angle_out: 0
variable_tray_angle_in: 180

# Increase this value if the servo doesn't have enough time to fully retract or extend
variable_dwell_time: 200

# ========================================================================================
# ==================== BLOB TUNING =======================================================
# ========================================================================================

# The following section defines how the purging sequence is executed. This is where you 
# tune the purging to create pretty blobs. Refer to the visual reference for a better 
# understanding. The visual is populated with example values. Below are some guides 
# provided to help with tuning.
#
#                          \_____________/
#                             |___|___|
#                                \_/            ______________  < End of third iteration.
#                                / \                                  HEIGHT:   3 x iteration_z_raise - (2 + 1) x iteration_z_change  (3 x 5 - 2 x 1.2 = 11.4)
#                               |   |                                 EXTRUDED: 3 x max_iteration_length                              (3 x 50 = 150)
#                              /     \          ______________  < End of second iteration.
#                             |       \                               HEIGHT:   2 x iteration_z_raise - 1 x iteration_z_change        (2 x 5 - 1 x 1.2 = 8.8)
#                            /         |                              EXTRUDED: 2 x max_iteration_length                              (2 x 50 = 100)
#                           |           \       ______________  < End of first iteration. 
#                          /             \                            HEIGHT:   1 x iteration_z_raise                                 (1 x 5 = 5)
#                         |               |                           EXTRUDED: 1 x max_iteration_length                              (1 x 50 = 50)
#___________               \             /      ______________  < Start height of the nozzle. default value: 1.5mm
#           |_______________\___________/_      ______________  < Bottom of the tray
#           |_____________________________|
#           |
# 
########################### BLOB TUNING ##############################
# +-------------------------------------+----------------------------+
# |  Filament sticks to the nozzle at   | Incr. purge start          |
# |    initial purge (first few mm)     |                            |
# +-------------------------------------+----------------------------+
# |  Filament scoots out from under     | Incr. temperature          |
# |  the nozzle at the first iteration  | Decr. z_raise              |
# |                                     | Incr. purge_length_maximum |
# +-------------------------------------+----------------------------+
# |  Filament scoots out from under the | Decr. purge_spd            |
# |  the nozzle at later iterations     | Decr. z_raise_exp          |
# |                                     | Decr. z_raise              |
# |                                     | Incr. purge_length_maximum |
# +-------------------------------------+----------------------------+
# |  Filament sticks to the nozzle at   | Incr. z_raise_exp          |
# |         later iterations            |     (Not above 1)          |
# +-------------------------------------+----------------------------+
#

# The height to raise the nozzle above the tray before purging. This allows any built up 
# pressure to escape before the purge.
variable_purge_start: 0.2

# The amount to raise Z
variable_z_raise: 12

# As the nozzle gets higher and the blob wider, the Z raise needs to be reduced, this
# follows the following formula: 
#            (extruded_amount/max_purge_length)^z_raise_exp * z_raise
# 1 is linear, below 1 will cause z to raise less quickly over time, above 1 will make it
# raise quicker over time. 0.85 is a good starting point and you should not have it above 1
variable_z_raise_exp: 0.85

# Lift the nozzle slightly after creating the blob te release pressure on the tray.
variable_eject_hop: 1.0

# Dwell time (ms) after purging and before cleaning to relieve pressure from the nozzle.
variable_pressure_release_time: 1000

# Set the part cooling fan speed. Disabling can help prevent the nozzle from cooling down 
# and stimulate flow, Enabling it can prevent blobs from sticking together. Values range 
# from 0 .. 1, or -1 if you don't want it changed.
#variable_part_cooling_fan: -1              # Leave it unchanged
#variable_part_cooling_fan:  0              # Disable the fan
variable_part_cooling_fan:  1               # Run it at full speed

# Define the part fan name if you are using a fan other than [fan]
# Applies to [fan_generic] or other fan definitons
# Example would be if you are using auxiliary fan control in Orcaslicer (https://github.com/SoftFever/OrcaSlicer/wiki/Auxiliary-fan)
# If you are unsure if you need this, then probably just leave it commented out.

#variable_fan_name: "fan_generic fan0"



# ========================================================================================
# ==================== PURGE LENGTH TUNING ===============================================
# ========================================================================================

# The absolute minimum to purge, even if you don't changed tools. This is to prime the 
# nozzle before printing
variable_purge_length_minimum: 30

# The maximum amount of filament (in mm¹) to purge in a single blob. Blobifier will 
# automatically purge multiple blobs if the purge amount exceeds this.
variable_purge_length_maximum: 150

# Default purge length to fall back on when neither the tool map purge_volumes or 
# parameter PURGE_LENGTH is set.
variable_purge_length: 150

# The slicer values often are a bit too wasteful. Tune it here to get optimal values. 
# 0.6 (60%) is a good starting point.
variable_purge_length_modifier: 0.6

# Fixed length of filament to add after the purge volume calculation. Happy Hare already
# shares info on the extra amount of filament to purge based on known residual filament,
# tip cutting fragment and initial retraction setting. However this setting can add a fixed
# amount on top on that if necessary although it is recommended to start with 0 and tune
# slicer purge matrix first.
# When should you alter this value:
#   INCREASE: When the dark to light swaps are good, but light to dark aren't.
#   DECREASE: When the light to dark swaps are good, but dark to light aren't. Don't 
#     forget to increase the purge_length_modifier
variable_purge_length_addition: 0

# ========================================================================================
# ==================== BUCKET ============================================================
# ========================================================================================

# Maximum number of blobs that fit in the bucket. Pauses the print if it exceeds this 
# number.
variable_max_blobs: 400
# Enable the bucket shaker. You need to have the shaker.stl installed
variable_enable_shaker: 1
# The number of back-and-forth motions of one shake
variable_bucket_shakes: 10
# During shaking acceleration can often be higher because you don't need to keep print 
# quality in mind. Higher acceleration helps better with dispersing the blobs.
variable_shake_accel: 10000

# The frequency at which to shake the bucket. A decimal value ranging from 0 to 1, where 0 
# is never, and 1 is every time. This way the shaking occurs more often as the bucket 
# fills up. Sensible values range from 0.75 to 0.95
variable_bucket_shake_frequency: 0.95

# Height of the shaker arm. If your hotend hits your tray during shaking, increase.
variable_shaker_arm_z: 2

gcode:

  # ======================================================================================
  # ==================== RECORD STATE (INCL. FANS, SPEEDS, ETC...) =======================
  # ======================================================================================

  # General state
  SAVE_GCODE_STATE NAME=BLOBIFIER_state

  
  # ======================================================================================
  # ==================== CHECK HOMING STATUS =============================================
  # ======================================================================================
  
  {% if "xyz" not in printer.toolhead.homed_axes %}
    RESPOND MSG="BLOBIFIER: Not homed! Home xyz before blobbing"
  {% elif printer.quad_gantry_level and printer.quad_gantry_level.applied == False %}
    RESPOND MSG="BLOBIFIER: QGL not applied! run quad_gantry_level before blobbing"
  {% else %}
    
    # Part cooling fan
    {% if part_cooling_fan >= 0 %}
      {% set fan = fan_name|string %}
      # Save the part cooling fan speed to be enabled again later
      {% set backup_fan_speed = (printer[fan].speed if printer[fan] is defined else printer.fan.speed) %}
      # Set part cooling fan speed
      M106 S{part_cooling_fan * 255}
    {% endif %}

    # Set feedrate to 100% for correct speed purging
    {% set backup_feedrate = printer.gcode_move.speed_factor %}
    M220 S100

    # ======================================================================================
    # ==================== DEFINE BASIC VARIABLES ==========================================
    # ======================================================================================
    
    {% set sequence_vars = printer['gcode_macro _MMU_SEQUENCE_VARS'] %}
    {% set park_vars = printer['gcode_macro _MMU_PARK'] %}
    {% set filament_diameter = printer.configfile.config.extruder.filament_diameter|float %}
    {% set filament_cross_section = (filament_diameter/2) ** 2 * 3.1415 %}
    {% set from_tool = printer.mmu.last_tool %}
    {% set to_tool = printer.mmu.tool %}
    {% set bl_count = printer['gcode_macro _BLOBIFIER_COUNT'] %}
    {% set pos = printer.gcode_move.gcode_position %}
    {% set safe = printer['gcode_macro _BLOBIFIER_SAFE_DESCEND'] %}
    {% set ignore_safe = safe.print_height < force_safe_descend_height_until %}
    {% set restore_z = [printer['gcode_macro BLOBIFIER_PARK'].restore_z,pos.z]|max %}
    {% set pos_max = printer.toolhead.axis_maximum %}
    {% set position_y = pos_max.y - skew_correction %}

    # Get purge volumes from the slicer (if set up right. see 
    # https://github.com/moggieuk/Happy-Hare/wiki/Gcode-Preprocessing)
    {% set pv = printer.mmu.slicer_tool_map.purge_volumes %}
    
    # ======================================================================================
    # ==================== DETERMINE PURGE LENGTH ==========================================
    # ======================================================================================

    {% if params.PURGE_LENGTH %} # =============== PARAM PURGE LENGTH ======================
      {action_respond_info("BLOBIFIER: param PURGE_LENGTH provided")}
      {% set purge_len = params.PURGE_LENGTH|float %}
    {% elif from_tool == to_tool and to_tool >= 0 %} # ==== TOOL DIDN'T CHANGE =============
      {action_respond_info("BLOBIFIER: Tool didn't change (T%s > T%s), %s" % (from_tool, to_tool, "priming" if purge_length_minimum else "skipping"))}
      {% set purge_len = 0 %}

    {% elif pv %} # ============== FETCH FROM HAPPY HARE (LIKELY FROM SLICER) ==============
      {% if from_tool < 0 and to_tool >= 0%}
        {action_respond_info("BLOBIFIER: from tool unknown. Finding largest value for T? > T%d" % to_tool)}
        {% set purge_vol = pv|map(attribute=to_tool)|max %}
      {% elif to_tool < 0 %}
        {action_respond_info("BLOBIFIER: tool(s) unknown. Finding largest value")}
        {% set purge_vol = pv|map('max')|max %}
      {% else %}
        {% set purge_vol = pv[from_tool][to_tool]|float * purge_length_modifier %}
        {action_respond_info("BLOBIFIER: Swapped T%s > T%s" % (from_tool, to_tool))}
      {% endif %}
      {% set purge_len = purge_vol / filament_cross_section %}

      {% set purge_len = purge_len + printer.mmu.extruder_filament_remaining + park_vars.retracted_length + purge_length_addition %}

    {% else %} # ========================= USE CONFIG VARIABLE =============================
      {action_respond_info("BLOBIFIER: No toolmap or PURGE_LENGTH. Using default")}
      {% set purge_len = purge_length|float + printer.mmu.extruder_filament_remaining + park_vars.retracted_length %}
    {% endif %}

    # ==================================== APPLY PURGE MINIMUM =============================
    {% set purge_len = [purge_len,purge_length_minimum]|max|round(0, 'ceil')|int %}
    {action_respond_info("BLOBIFIER: Purging %dmm of filament" % (purge_len))}

    # ======================================================================================
    # ==================== PURGING SEQUENCE ================================================
    # ======================================================================================

    # Set to absolute positioning.
    G90

    # Check for purge length and purge if necessary.
    {% if purge_len|float > 0 %}

      # ====================================================================================
      # ==================== POSITIONING ===================================================
      # ====================================================================================
      
      # Retract the tray so it is not in the way
      BLOBIFIER_SERVO POS=in

      # Move to the assembly, first a bit more to the right (brush_start) to avoid a 
      # potential filametrix pin if it's not already on the same Y coordinate.
      {% if printer.toolhead.position.y != position_y %}
        G1 X{[brush_start - 20, 30]|max} Y{position_y} F{travel_spd_xy}
      {% endif %}

      # ====================================================================================
      # ==================== BUCKET SHAKE ==================================================
      # ====================================================================================
      
      {% if enable_shaker and (safe.shake or ignore_safe) %}
        {% if (bl_count.current_blobs + 1) >= bl_count.next_shake %}
          BLOBIFIER_SHAKE_BUCKET SHAKES={bucket_shakes}
          _BLOBIFIER_CALCULATE_NEXT_SHAKE
        {% endif %}
      {% endif %}
      
      # ====================================================================================
      # ==================== POSITIONING ON TRAY ===========================================
      # ====================================================================================
      {% if safe.tray or ignore_safe %}
        G1 Z{tray_top + purge_start} F{travel_spd_z}
      {% endif %}

      # Move over to the tray after z change (For cases when the tool is lower than the tray)
      G1 X{purge_x} F{travel_spd_xy}

      # Extend the tray
      BLOBIFIER_SERVO POS=out

      # ====================================================================================
      # ==================== HEAT HOTEND ===================================================
      # ====================================================================================
      
      {% if printer.extruder.temperature < purge_temp_min %}
        {% if printer.extruder.target < purge_temp_min %}
          M109 S{purge_temp_min}
        {% else %}
          TEMPERATURE_WAIT SENSOR=extruder MINIMUM={purge_temp_min}
        {% endif %}
      {% endif %}

      # ====================================================================================
      # ==================== START ITERATING ===============================================
      # ====================================================================================
      
      # Calculate total number of iterations based on the purge length and the max_iteration 
      # length.
      {% set blobs = (purge_len / purge_length_maximum)|round(0, 'ceil')|int %}
      {% set purge_per_blob = purge_len|float / blobs %}
      {% set retracts_per_blob = (purge_per_blob / 40)|round(0, 'ceil')|int %}
      {% set purge_per_retract = (purge_per_blob / retracts_per_blob)|int %}
      {% set pulses_per_retract = (purge_per_blob / retracts_per_blob / 5)|round(0, 'ceil')|int %}
      {% set pulses_per_blob = (purge_per_blob / 5)|round(0, 'ceil')|int %}
      {% set purge_per_pulse = purge_per_blob / pulses_per_blob %}
      {% set pulse_time_constant = purge_per_pulse * 0.95 / purge_spd / (purge_per_pulse * 0.95 / purge_spd + purge_per_pulse * 0.05 / 50) %}
      {% set pulse_duration = purge_per_pulse / purge_spd %}

      # Repeat the process until purge_len is reached
      {% for blob in range(blobs) %}
        RESPOND MSG={"'BLOBIFIER: Blob %d of %d (%.1fmm)'" % (blob + 1, blobs, purge_per_blob)}

        {% if safe.tray or ignore_safe %}
          G1 Z{tray_top + purge_start} F{travel_spd_z}
        {% endif %}

        # relative positioning
        G91 
        # relative extrusion
        M83

        # Purge filament in a pulsating motion to purge the filament quicker and better
        {% for pulse in range(pulses_per_blob) %}
          # Calculations to determine z-speed
          {% set purged_this_blob = pulse * purge_per_pulse %}
          {% set z_last_pos = purge_start + ((purged_this_blob)/purge_length_maximum)**z_raise_exp * z_raise %}
          {% set z_pos = purge_start + ((purged_this_blob + purge_per_pulse)/purge_length_maximum)**z_raise_exp * z_raise %}
          {% set z_up = z_pos - z_last_pos %}
          {% set speed = z_up / pulse_duration %}

          # Purge quickly
          G1 Z{z_up * pulse_time_constant} E{purge_per_pulse * 0.95} F{speed}
          # Purge a tiny bit slowly
          G1 Z{z_up * (1 - pulse_time_constant)} E{purge_per_pulse * 0.05} F{speed}

          # retract and unretract filament every now and then for thorough cleaning
          {% if pulse % pulses_per_retract == 0 and pulse > 0 %}
            G1 E-2 F1800
            G1 E2 F800
          {% endif %}
          
        {% endfor %}

        # Retract to match what Happy Hare is expecting
        G1 E-{park_vars.retracted_length} F{sequence_vars.retract_speed * 60}
        
        # ==================================================================================
        # ==================== DEPOSIT BLOB ================================================
        # ==================================================================================
        {% if safe.tray or ignore_safe %}
          # Raise z a bit to relieve pressure on the blob preventing it to go sideways
          G1 Z{eject_hop} F{travel_spd_z}
          # Retract the tray
          BLOBIFIER_SERVO POS=in
          # Move the toolhead down to purge_start height lowering the blob below the tray
          G90 # absolute positioning
          G1 Z{tray_top} F{travel_spd_z}
          # Extend the tray to 'cut off' the blob and prepare for the next blob
          BLOBIFIER_SERVO POS=out
          BLOBIFIER_SERVO POS=in
          BLOBIFIER_SERVO POS=out
          # Keep track of the # of blobs
          _BLOBIFIER_COUNT
        {% endif %}
      {% endfor %}
    {% endif %}
    {% if safe.tray or ignore_safe %}
      G1 Z{tray_top + 1} F{travel_spd_z}
      G4 P{pressure_release_time}
    {% endif %}
    {% if safe.brush or ignore_safe %}
      BLOBIFIER_CLEAN
    {% else %}
      G1 X{brush_start} F{travel_spd_xy}
    {% endif %}

    # ======================================================================================
    # ==================== RESTORE STATE ===================================================
    # ======================================================================================
    G90 # absolute positioning
    G1 Z{restore_z} F{travel_spd_z}
    
    {% if part_cooling_fan >= 0 %}
      # Reset part cooling fan if it was changed
      M106 S{(backup_fan_speed * 255)|int}
    {% endif %}
    
    M220 S{(backup_feedrate * 100)|int}
  {% endif %}

  # Retract the tray
  BLOBIFIER_SERVO POS=in
  
  RESTORE_GCODE_STATE NAME=BLOBIFIER_state 


##########################################################################################
# Wipes the nozzle on the brass brush
#
[gcode_macro BLOBIFIER_CLEAN]
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set pos_max = printer.toolhead.axis_maximum %}
  {% set position_y = pos_max.y - bl.skew_correction %}
  {% set original_accel = printer.toolhead.max_accel %}
  {% set original_minimum_cruise_ratio = printer.toolhead.minimum_cruise_ratio %}
  {% set pos = printer.gcode_move.gcode_position %}
  
  SAVE_GCODE_STATE NAME=BLOBIFIER_CLEAN_state

  G90
  
  {% if bl.brush_accel > 0 %}
    SET_VELOCITY_LIMIT ACCEL={bl.brush_accel} MINIMUM_CRUISE_RATIO=0.1
  {% endif %}

  {% if pos.z < bl.brush_top + bl.clearance_z %}
    G1 Z{bl.brush_top + bl.clearance_z} F{bl.travel_spd_z}
  {% endif %}
  G1 X{bl.brush_start} F{bl.travel_spd_xy}
  G1 Y{position_y}
  G1 Z{bl.brush_top + bl.clearance_z} F{bl.travel_spd_z}

  # Move nozzle down into brush.
  G1 Z{bl.brush_top} F{bl.travel_spd_z}

  SET_VELOCITY_LIMIT ACCEL={original_accel} MINIMUM_CRUISE_RATIO={original_minimum_cruise_ratio}
  
  # Perform wipe. Wipe direction based off bucket_pos for cool random scrubby routine.
  {% for wipes in range(1, (bl.wipe_qty + 1)) %}
     G1 X{bl.brush_start + bl.brush_width} F{bl.wipe_spd_xy}
     G1 X{bl.brush_start} F{bl.wipe_spd_xy}
  {% endfor %}

  # Move away from the brush, but not onto the tray or in front of the filametrix cutter pin
  G1 X{[bl.brush_start - 20, 30]|max} F{bl.travel_spd_xy}

  RESTORE_GCODE_STATE NAME=BLOBIFIER_CLEAN_state



##########################################################################################
# Park the nozzle on the tray to prevent oozing during filament swaps. Place this 
# extension in the post_form_tip extension in mmu_macro_vars.cfg:
#   variable_user_post_form_tip_extension: "BLOBIFIER_PARK"
#
[gcode_macro BLOBIFIER_PARK]
variable_restore_z: 0
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set pos = printer.gcode_move.gcode_position %}
  {% set safe = printer['gcode_macro _BLOBIFIER_SAFE_DESCEND'] %}
  {% set pos_max = printer.toolhead.axis_maximum %}
  {% set position_y = pos_max.y - bl.skew_correction %}

  SET_GCODE_VARIABLE MACRO=BLOBIFIER_PARK VARIABLE=restore_z VALUE={pos.z}

  SAVE_GCODE_STATE NAME=blobifier_park_state
  
  {% if "xyz" in printer.toolhead.homed_axes and printer.quad_gantry_level and printer.quad_gantry_level.applied %}
    G90

    # Retract the tray
    BLOBIFIER_SERVO POS=in

    G1 X{[bl.brush_start - 20, 30]|max} Y{position_y} F{bl.travel_spd_xy}
    {% if safe.tray or ignore_safe %}
      G1 Z{bl.tray_top} F{bl.travel_spd_z}
    {% endif %}
    G1 X{bl.purge_x} F{bl.travel_spd_xy}

    # Extend the tray
    BLOBIFIER_SERVO POS=out

  {% else %}
    RESPOND MSG="Please home (and QGL) before parking"
  {% endif %}

  RESTORE_GCODE_STATE NAME=blobifier_park_state

##########################################################################################
# Retract or extend the tray 
# POS=[in|out] Retractor extend the tray
#
[gcode_macro BLOBIFIER_SERVO]
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set pos = params.POS %}
  {% if pos == "in" %}
    SET_SERVO SERVO=blobifier ANGLE={bl.tray_angle_in}
    G4 P{bl.dwell_time}
  {% elif pos == "out" %}
    SET_SERVO SERVO=blobifier ANGLE={bl.tray_angle_out}
    G4 P{bl.dwell_time}
  {% else %}
    {action_respond_info("BLOBIFIER: provide POS=[in|out]")}
  {% endif %}
  SET_SERVO SERVO=blobifier WIDTH=0

##########################################################################################
# Define exclude objects for those who haven't already
#
[exclude_object]

##########################################################################################
# Overwrite the existing EXCLUDE_OBJECT_DEFINE to also check for safe descend.
#
[gcode_macro EXCLUDE_OBJECT_DEFINE]
rename_existing: _EXCLUDE_OBJECT_DEFINE
gcode:
  # only reset on the first object at the beginning of a print
  {% if printer.exclude_object.objects|length < 1 %}
    _BLOBIFIER_RESET_SAFE_DESCEND
  {% endif %}
  _EXCLUDE_OBJECT_DEFINE {rawparams}
  _BLOBIFIER_SAFE_DESCEND
  UPDATE_DELAYED_GCODE ID=BLOBIFIER_SHOW_SAFE_DESCEND DURATION=1
  
[delayed_gcode BLOBIFIER_SHOW_SAFE_DESCEND]
gcode:
  {% set safe = printer['gcode_macro _BLOBIFIER_SAFE_DESCEND'] %}
  {action_respond_info(
    "BLOBIFIER: Safe descend possible:\n - tray:  %s\n - brush: %s\n - shake: %s" % 
    (
      "yes" if safe.tray else "no",
      "yes" if safe.brush else "no",
      "yes" if safe.shake else "no"
    )
  )}

##########################################################################################
# Use the EXCLUDE_OBJECT_START gcode macro to record the current height
#
[gcode_macro EXCLUDE_OBJECT_START]
rename_existing: _EXCLUDE_OBJECT_START
gcode:
  _EXCLUDE_OBJECT_START {rawparams}
  {% if printer['gcode_macro _BLOBIFIER_SAFE_DESCEND'].first_layer %}
    SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=first_layer VALUE=False
    SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=print_height VALUE={printer['gcode_macro _BLOBIFIER_SAFE_DESCEND'].print_layer_height}
  {% else %}
    {% set pos = printer.gcode_move.gcode_position %}
    {% set last_height = printer['gcode_macro _BLOBIFIER_SAFE_DESCEND'].print_previous_height|float %}
    {% if pos.z > last_height %}
      {% set last_layer = (pos.z - last_height)|round(2) %}
      {% set print_height = (pos.z + last_layer)|round(2) %}
      SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=print_previous_height VALUE={pos.z}
      SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=print_height VALUE={print_height}
    {% endif %}
  {% endif %}

##########################################################################################
# Reset the safe descend variables.
#
[gcode_macro _BLOBIFIER_RESET_SAFE_DESCEND]
gcode:
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=tray VALUE=True
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=brush VALUE=True
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=shake VALUE=True
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=first_layer VALUE=True
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=print_height VALUE=0
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=print_previous_height VALUE=0

##########################################################################################
# Determine if it is safe to drop the toolhead (e.g. not hit a print)
#
[gcode_macro _BLOBIFIER_SAFE_DESCEND]
variable_tray: True # Assume it is safe
variable_brush: True
variable_shake: True
variable_first_layer: True
variable_print_height: 0
variable_print_previous_height: 0
variable_print_layer_height: 0.3
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set pos_max = printer.toolhead.axis_maximum %}
  {% set position_y = pos_max.y - bl.skew_correction %}
  {% set tray = [bl.purge_x + bl.toolhead_x, position_y - bl.toolhead_y] %}
  {% set brush = [bl.brush_start + bl.brush_width + bl.toolhead_x, position_y - bl.toolhead_y] %}
  {% set shake = [bl.purge_x + bl.toolhead_x, position_y - bl.toolhead_y - 4] %}
  {% set objects = printer.exclude_object.objects | map(attribute='polygon') %}

  {% for polygon in objects %}
    {% for point in polygon %}
      {% if point[0] < tray[0] and point[1] > tray[1] %}
        SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=tray VALUE=False
      {% endif %}
      {% if point[0] < brush[0] and point[1] > brush[1] %}
        SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=brush VALUE=False
      {% endif %}
      {% if point[0] < shake[0] and point[1] > shake[1] %}
        SET_GCODE_VARIABLE MACRO=_BLOBIFIER_SAFE_DESCEND VARIABLE=shake VALUE=False
      {% endif %}
    {% endfor %}
  {% endfor %}

##########################################################################################
# Increment the blob count with 1 and check if the bucket is full. Pause 
# the printer if it is.
#
[gcode_macro _BLOBIFIER_COUNT]
# Don't change these variables
variable_current_blobs: 0
variable_last_shake: 0
variable_next_shake: 0
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set count = printer['gcode_macro _BLOBIFIER_COUNT'] %}
  {% if current_blobs >= bl.max_blobs %}
    {action_respond_info("BLOBIFIER: Empty purge bucket!")}
    M117 Empty purge bucket!
    MMU_PAUSE MSG="Empty purge bucket!"
  {% else %}
    SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=current_blobs VALUE={current_blobs + 1}
    _BLOBIFIER_SAVE_STATE
    {action_respond_info(
      "BLOBIFIER: Blobs in bucket: %s/%s. Next shake @ %s" 
      % (current_blobs + 1, bl.max_blobs, next_shake)
    )}
  {% endif %}

##########################################################################################
# Reset the blob count to 0
#
[gcode_macro _BLOBIFIER_COUNT_RESET]
gcode:
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=current_blobs VALUE=0
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=last_shake VALUE=0
  _BLOBIFIER_SAVE_STATE
  
  _BLOBIFIER_CALCULATE_NEXT_SHAKE

##########################################################################################
# Shake the blob bucket to disperse the blobs
#
[gcode_macro BLOBIFIER_SHAKE_BUCKET]
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set count = printer['gcode_macro _BLOBIFIER_COUNT'] %}
  {% set original_accel = printer.toolhead.max_accel %}
  {% set original_minimum_cruise_ratio = printer.toolhead.minimum_cruise_ratio %}
  {% set position_x = bl.skew_correction %}

  {% if "xyz" not in printer.toolhead.homed_axes %}
    {action_raise_error("BLOBIFIER: Not homed. Home xyz first")}
  {% endif %}
  
  SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=last_shake VALUE={count.current_blobs}
  _BLOBIFIER_SAVE_STATE
  SAVE_GCODE_STATE NAME=shake_bucket
  
  M400
  M117 (^_^)

  G90
  {% set shakes = params.SHAKES|default(10)|int %}
  {% set pos_max = printer.toolhead.axis_maximum %}
  {% set position_y = pos_max.y - bl.skew_correction %}
  
  # move to save y if not already there
  {% if printer.toolhead.position.y != position_y %}
    G1 X{bl.brush_start} Y{position_y} F{bl.travel_spd_xy}
  {% endif %}

  # Retract the tray
  BLOBIFIER_SERVO POS=in

  # move up a bit to prevent oozing on base
  G1 Z{bl.shaker_arm_z} F{bl.travel_spd_z}
  # slide into the slot
  G1 X{position_x} F{bl.travel_spd_xy}

  M400
  M117 (+(+_+)+)

  SET_VELOCITY_LIMIT ACCEL={bl.shake_accel} MINIMUM_CRUISE_RATIO=0.1
  
  # Shake away!
  {% for shake in range(1, shakes) %}
     G1 Y{position_y - 4}
     G1 Y{position_y}
  {% endfor %}

  SET_VELOCITY_LIMIT ACCEL={original_accel} MINIMUM_CRUISE_RATIO={original_minimum_cruise_ratio}
  # move out of slot
  G1 X{bl.purge_x}

  M400
  M117 (X_x)

  RESTORE_GCODE_STATE NAME=shake_bucket  

##########################################################################################
# Calculate when the bucket should be shaken. 
#
[gcode_macro _BLOBIFIER_CALCULATE_NEXT_SHAKE]
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}
  {% set count = printer['gcode_macro _BLOBIFIER_COUNT'] %}

  {% set remaining_blobs = bl.max_blobs - count.last_shake %}
  {% set next_shake = (1 - bl.bucket_shake_frequency) * remaining_blobs + count.last_shake %}
  _BLOBIFIER_SAVE_STATE
  _BLOBIFIER_SET_NEXT_SHAKE VALUE={next_shake|int}

##########################################################################################
# Set when the bucket should be shaken next
# VALUE=[int] At what amount of blobs should it be shaken
#
[gcode_macro _BLOBIFIER_SET_NEXT_SHAKE]
gcode:
  {% if params.VALUE %}
    {% set next_shake = params.VALUE %}
    SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=next_shake VALUE={next_shake}
    _BLOBIFIER_SAVE_STATE
  {% else %}
    {action_respond_info("BLOBIFIER: Provide parameter VALUE=")}
  {% endif %}

##########################################################################################
# Some sanity checks
#
[delayed_gcode BLOBIFIER_INIT]
initial_duration: 5.0
gcode:
  _BLOBIFIER_INIT
  # Extend and retract the tray to test
  BLOBIFIER_SERVO POS=out
  BLOBIFIER_SERVO POS=in

[gcode_macro _BLOBIFIER_INIT]
gcode:
  {% set bl = printer['gcode_macro BLOBIFIER'] %}

  # Valid part cooling fan setting
  {% if bl.part_cooling_fan != -1 and (bl.part_cooling_fan < 0 or bl.part_cooling_fan > 1) %}
    {action_emergency_stop("BLOBIFIER: Value %f is invalid for variable part_cooling_fan. Either -1 or a value from 0 .. 1 is valid." % (bl.part_cooling_fan))}
  {% endif %}

  # Valid bucket shake frequency
  {% if bl.bucket_shake_frequency < 0 or bl.bucket_shake_frequency > 1 %}
    {action_emergency_stop("BLOBIFIER: Value %f is invalid for variable bucket_shake_frequency. Change it to a value between 0 .. 1" % (bl.bucket_shake_frequency))}
  {% endif %}  

  # Check if position is on 'next'
  {% if printer.mmu %}
    {% if printer['gcode_macro _MMU_SEQUENCE_VARS'].restore_xy_pos != 'next' %}
      {action_respond_info("BLOBIFIER: If not using a wipe tower, consider setting restore_xy_pos: 'next' in mmu_macro_vars.cfg")}
    {% endif %}
  {% endif %}

  # Check the z_raise variable for normal values
  {% if bl.z_raise < 3 %}
    {action_respond_info("BLOBIFIER: variable_z_raise: %f is very low. This is the value z raises in total on a single blob. Make sure the value is correct before continuing." % (bl.z_raise))}
  {% endif %}

  # Z raise exponent
  {% if bl.z_raise_exp > 1 or bl.z_raise_exp < 0.5 %}
    {action_respond_info("BLOBIFIER: variable_z_raise_exp has value: %f. This value is out of spec (0.5 ... 1.0)." % (bl.z_raise_exp))}
  {% endif %}

  # cap user defined accels at printer max_accel if greater
  {% if bl.shake_accel >  printer.configfile.config.printer.max_accel|int %}
     {action_respond_info("BLOBIFIER: variable_shake_accel has value: %d which is higher than your printer limit of %d. Reduce this if your printer skips steps." % (bl.shake_accel,printer.configfile.config.printer.max_accel|int))}
  {% endif %}
  {% if bl.brush_accel >  printer.configfile.config.printer.max_accel|int %}
     {action_respond_info("BLOBIFIER: variable_brush_accel has value: %d which is higher than your printer limit of %d. Reduce this if your printer skips steps." % (bl.brush_accel,printer.configfile.config.printer.max_accel|int))}
  {% endif %}

[delayed_gcode BLOBIFIER_LOAD_STATE]
initial_duration: 2.0 # Give it some time to boot up
gcode:
  {% set sv = printer.save_variables.variables.blobifier %}

  {% if sv %}
    # Restore state
    SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=last_shake VALUE={sv.last_shake}
    SET_GCODE_VARIABLE MACRO=_BLOBIFIER_COUNT VARIABLE=current_blobs VALUE={sv.current_blobs}
  {% endif %}
  _BLOBIFIER_CALCULATE_NEXT_SHAKE

[gcode_macro _BLOBIFIER_SAVE_STATE]
gcode:
  {% set count = printer['gcode_macro _BLOBIFIER_COUNT'] %}
  {% set sv = {'current_blobs': count.current_blobs, 'last_shake': count.last_shake} %}
  SAVE_VARIABLE VARIABLE=blobifier VALUE="{sv}"