# Timelapse klipper macro definition
#
# Copyright (C) 2021 Christoph Frei <fryakatkop@gmail.com>
# Copyright (C) 2021 Alex Zellner <alexander.zellner@googlemail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license
#
# Macro version 1.15
#

##### DO NOT CHANGE ANY MACRO!!! #####

##########################################################################
#                                                                        #
# GET_TIMELAPSE_SETUP: Print the Timelapse setup to console              #
#                                                                        #
##########################################################################

[gcode_macro GET_TIMELAPSE_SETUP]
description: Print the Timelapse setup
gcode: 
  {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
  {% set output_txt = ["Timelapse Setup:"] %}
  {% set _dummy = output_txt.append("enable: %s" % tl.enable) %}
  {% set _dummy = output_txt.append("park: %s" % tl.park.enable) %}
  {% if tl.park.enable %}
    {% set _dummy = output_txt.append("park position: %s time: %s s" % (tl.park.pos, tl.park.time)) %}
    {% set _dummy = output_txt.append("park cord x:%s y:%s dz:%s" % (tl.park.coord.x, tl.park.coord.y, tl.park.coord.dz)) %}
    {% set _dummy = output_txt.append("travel speed: %s mm/s" % tl.speed.travel) %}
  {% endif %}
  {% set _dummy = output_txt.append("fw_retract: %s" % tl.extruder.fw_retract) %}
  {% if not tl.extruder.fw_retract %}
    {% set _dummy = output_txt.append("retract: %s mm speed: %s mm/s" % (tl.extruder.retract, tl.speed.retract)) %}
    {% set _dummy = output_txt.append("extrude: %s mm speed: %s mm/s" % (tl.extruder.extrude, tl.speed.extrude)) %}
  {% endif %}
  {% set _dummy = output_txt.append("verbose: %s" % tl.verbose) %}
  {action_respond_info(output_txt|join("\n"))}

################################################################################################
#                                                                                              #
# Use _SET_TIMELAPSE_SETUP [ENABLE=value] [VERBOSE=value] [PARK_ENABLE=value] [PARK_POS=value] #
#                          [PARK_TIME=value] [CUSTOM_POS_X=value] [CUSTOM_POS_Y=value]         #
#                          [CUSTOM_POS_DZ=value][TRAVEL_SPEED=value] [RETRACT_SPEED=value]     #
#                          [EXTRUDE_SPEED=value] [EXTRUDE_DISTANCE=value]                      #
#                          [RETRACT_DISTANCE=value] [FW_RETRACT=value]                         #
#                                                                                              #
################################################################################################

[gcode_macro _SET_TIMELAPSE_SETUP]
description: Set user parameters for timelapse
gcode:
  {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
  ##### get min and max bed size #####
  {% set min = printer.toolhead.axis_minimum %}
  {% set max = printer.toolhead.axis_maximum %}
  {% set round_bed = True if printer.configfile.settings.printer.kinematics is in ['delta','polar','rotary_delta','winch'] 
                else False %}
  {% set park = {'min'   : {'x': (min.x / 1.42)|round(3) if round_bed else min.x|round(3),
                            'y': (min.y / 1.42)|round(3) if round_bed else min.y|round(3)},
                 'max'   : {'x': (max.x / 1.42)|round(3) if round_bed else max.x|round(3),
                            'y': (max.y / 1.42)|round(3) if round_bed else max.y|round(3)},
                 'center': {'x': (max.x-(max.x-min.x)/2)|round(3),
                            'y': (max.y-(max.y-min.y)/2)|round(3)}} %}
  ##### set new values #####
  {% if params.ENABLE %}
    {% if params.ENABLE|lower is in ['true', 'false'] %}
      SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=enable VALUE={True if params.ENABLE|lower == 'true' else False}
    {% else %}
      {action_raise_error("ENABLE=%s not supported. Allowed values are [True, False]" % params.ENABLE|capitalize)}
    {% endif %}
  {% endif %}
  {% if params.VERBOSE %}
    {% if params.VERBOSE|lower is in ['true', 'false'] %}
      SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=verbose VALUE={True if params.VERBOSE|lower == 'true' else False}
    {% else %}
      {action_raise_error("VERBOSE=%s not supported. Allowed values are [True, False]" % params.VERBOSE|capitalize)}
    {% endif %}
  {% endif %}
  {% if params.CUSTOM_POS_X %}
    {% if params.CUSTOM_POS_X|float >= min.x and params.CUSTOM_POS_X|float <= max.x %}
      {% set _dummy = tl.park.custom.update({'x':params.CUSTOM_POS_X|float|round(3)}) %}
    {% else %}
      {action_raise_error("CUSTOM_POS_X=%s must be within [%s - %s]" % (params.CUSTOM_POS_X, min.x, max.x))}
    {% endif %}
  {% endif %}
  {% if params.CUSTOM_POS_Y %}
    {% if params.CUSTOM_POS_Y|float >= min.y and params.CUSTOM_POS_Y|float <= max.y %}
      {% set _dummy = tl.park.custom.update({'y':params.CUSTOM_POS_Y|float|round(3)}) %}
    {% else %}
      {action_raise_error("CUSTOM_POS_Y=%s must be within [%s - %s]" % (params.CUSTOM_POS_Y, min.y, max.y))}
    {% endif %}
  {% endif %}
  {% if params.CUSTOM_POS_DZ %}
    {% if params.CUSTOM_POS_DZ|float >= min.z and params.CUSTOM_POS_DZ|float <= max.z %}
      {% set _dummy = tl.park.custom.update({'dz':params.CUSTOM_POS_DZ|float|round(3)}) %}
    {% else %}
      {action_raise_error("CUSTOM_POS_DZ=%s must be within [%s - %s]" % (params.CUSTOM_POS_DZ, min.z, max.z))}
    {% endif %}
  {% endif %}
  {% if params.PARK_ENABLE %}
    {% if params.PARK_ENABLE|lower is in ['true', 'false'] %}
      {% set _dummy = tl.park.update({'enable':True if params.PARK_ENABLE|lower == 'true' else False}) %}
    {% else %}
      {action_raise_error("PARK_ENABLE=%s not supported. Allowed values are [True, False]" % params.PARK_ENABLE|capitalize)}
    {% endif %}
  {% endif %}
  {% if params.PARK_POS %}
    {% if params.PARK_POS|lower is in ['center','front_left','front_right','back_left','back_right','custom','x_only','y_only'] %}
      {% set dic = {'center'      : {'x': park.center.x   , 'y': park.center.y   , 'dz': 1                },
                    'front_left'  : {'x': park.min.x      , 'y': park.min.y      , 'dz': 0                },
                    'front_right' : {'x': park.max.x      , 'y': park.min.y      , 'dz': 0                },
                    'back_left'   : {'x': park.min.x      , 'y': park.max.y      , 'dz': 0                },
                    'back_right'  : {'x': park.max.x      , 'y': park.max.y      , 'dz': 0                },
                    'custom'      : {'x': tl.park.custom.x, 'y': tl.park.custom.y, 'dz': tl.park.custom.dz},
                    'x_only'      : {'x': tl.park.custom.x, 'y': 'none'          , 'dz': tl.park.custom.dz},
                    'y_only'      : {'x': 'none'          , 'y': tl.park.custom.y, 'dz': tl.park.custom.dz}} %}
      {% set _dummy = tl.park.update({'pos':params.PARK_POS|lower}) %}
      {% set _dummy = tl.park.update({'coord':dic[tl.park.pos]}) %}
    {% else %}
      {action_raise_error("PARK_POS=%s not supported. Allowed values are [CENTER, FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, CUSTOM, X_ONLY, Y_ONLY]"
                          % params.PARK_POS|upper)}
    {% endif %}
  {% endif %}
  {% if params.PARK_TIME %}
    {% if params.PARK_TIME|float >= 0.0 %}
      {% set _dummy = tl.park.update({'time':params.PARK_TIME|float|round(3)}) %}
    {% else %}
      {action_raise_error("PARK_TIME=%s must be a positive number" % params.PARK_TIME)}
    {% endif %}
  {% endif %}
  SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=park VALUE="{tl.park}"
  {% if params.TRAVEL_SPEED %}
    {% if params.TRAVEL_SPEED|float > 0.0 %}
      {% set _dummy = tl.speed.update({'travel':params.TRAVEL_SPEED|float|round(3)}) %}
    {% else %}
      {action_raise_error("TRAVEL_SPEED=%s must be larger than 0" % params.TRAVEL_SPEED)}
    {% endif %}
  {% endif %}
  {% if params.RETRACT_SPEED %}
    {% if params.RETRACT_SPEED|float > 0.0 %}
      {% set _dummy = tl.speed.update({'retract':params.RETRACT_SPEED|float|round(3)}) %}
    {% else %}
      {action_raise_error("RETRACT_SPEED=%s must be larger than 0" % params.RETRACT_SPEED)}
    {% endif %}
  {% endif %}
  {% if params.EXTRUDE_SPEED %}
    {% if params.EXTRUDE_SPEED|float > 0.0 %}
      {% set _dummy = tl.speed.update({'extrude':params.EXTRUDE_SPEED|float|round(3)}) %}
    {% else %}
      {action_raise_error("EXTRUDE_SPEED=%s must be larger than 0" % params.EXTRUDE_SPEED)}
    {% endif %}
  {% endif %}
  SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=speed VALUE="{tl.speed}"
  {% if params.EXTRUDE_DISTANCE %}
    {% if params.EXTRUDE_DISTANCE|float >= 0.0 %}
      {% set _dummy = tl.extruder.update({'extrude':params.EXTRUDE_DISTANCE|float|round(3)}) %}
    {% else %}
      {action_raise_error("EXTRUDE_DISTANCE=%s must be specified as positiv number" % params.EXTRUDE_DISTANCE)}
    {% endif %}
  {% endif %}
  {% if params.RETRACT_DISTANCE %}
    {% if params.RETRACT_DISTANCE|float >= 0.0 %}
      {% set _dummy = tl.extruder.update({'retract':params.RETRACT_DISTANCE|float|round(3)}) %}
    {% else %}
      {action_raise_error("RETRACT_DISTANCE=%s must be specified as positiv number" % params.RETRACT_DISTANCE)}
    {% endif %}
  {% endif %}
  {% if params.FW_RETRACT %}
    {% if params.FW_RETRACT|lower is in ['true', 'false'] %}
      {% if 'firmware_retraction' in printer.configfile.settings %}
        {% set _dummy = tl.extruder.update({'fw_retract': True if params.FW_RETRACT|lower == 'true' else False}) %}
      {% else %}
        {% set _dummy = tl.extruder.update({'fw_retract':False}) %}
        {% if params.FW_RETRACT|capitalize == 'True' %}
          {action_raise_error("[firmware_retraction] not defined in printer.cfg. Can not enable fw_retract")}
        {% endif %}
      {% endif %}
    {% else %}
      {action_raise_error("FW_RETRACT=%s not supported. Allowed values are [True, False]" % params.FW_RETRACT|capitalize)}
    {% endif %}
  {% endif %}
  SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=extruder VALUE="{tl.extruder}"
  {% if printer.configfile.settings['gcode_macro pause'] is defined %}
    {% set _dummy = tl.macro.update({'pause': printer.configfile.settings['gcode_macro pause'].rename_existing}) %}
  {% endif %}
  {% if printer.configfile.settings['gcode_macro resume'] is defined %}
    {% set _dummy = tl.macro.update({'resume': printer.configfile.settings['gcode_macro resume'].rename_existing}) %}
  {% endif %}
  SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=macro VALUE="{tl.macro}"

##########################################################################
#                                                                        #
# TIMELAPSE_TAKE_FRAME: take the next picture                            #
#                                                                        #
##########################################################################

######################### definition #########################
## enable: enable or disable the next frame. Valid inputs: [True, False]
## takingframe: internal use. Valid inputs: [True, False]
##
## park.enable: enable or disable to park the head while taking a picture. Valid inputs: [True, False]
## park.pos   : used position for parking. Valid inputs: [center, front_left, front_right, back_left, back_right, custom, x_only, y_only]
## park.time  : used for the debug macro. Time in s
## park.custom.x, park.custom.y: coordinates of the custom parkposition. Unit [mm]
## park.custom.dz              : custom z hop for the picture. Unit [mm]
## park.coord : internal use
##
## extruder.fw_retract: enable disable fw retraction [True,False]
## extruder.extrude   : filament extruded at the end of park. Unit [mm]
## extruder.retract   : filament retract at the start of park. Unit [mm]
##
## speed.travel : used speed for travel from and to the park positon. Unit: [mm/min]
## speed.retract: used speed for retract [mm/min]
## speed.extrude: used speed for extrude [mm/min]
##
## verbose: Enable mesage output of TIMELAPSE_TAKE_FRAME
##
## check_time: time when the status of the taken picture is checked. Default 0.5 sec
##
## restore.absolute.coordinates: internal use
## restore.absolute.extrude    : internal use
## restore.speed               : internal use
## restore.e                   : internal use
## restore.factor.speed        : internal use
## restore.factor.extrude      : internal use
##
## macro.pause  : internal use
## macro.resume : internal use
##
## is_paused: internal use
###############################################################
[gcode_macro TIMELAPSE_TAKE_FRAME]
description: Take Timelapse shoot
variable_enable: False
variable_takingframe: False
variable_park: {'enable': False,
                'pos'   : 'center',
                'time'  : 0.1,
                'custom': {'x': 0, 'y': 0, 'dz': 0},
                'coord' : {'x': 0, 'y': 0, 'dz': 0}}
variable_extruder: {'fw_retract': False,
                    'retract': 1.0,
                    'extrude': 1.0}
variable_speed: {'travel': 100,
                 'retract': 15,
                 'extrude': 15}
variable_verbose: True
variable_check_time: 0.5
variable_restore: {'absolute': {'coordinates': True, 'extrude': True}, 'speed': 1500, 'e':0, 'factor': {'speed': 1.0, 'extrude': 1.0}}
variable_macro: {'pause': 'PAUSE', 'resume': 'RESUME'}
variable_is_paused: False
gcode:
  {% set hyperlapse = True if params.HYPERLAPSE and params.HYPERLAPSE|lower =='true' else False %}
  {% if enable %}
    {% if (hyperlapse and printer['gcode_macro HYPERLAPSE'].run) or
          (not hyperlapse and not printer['gcode_macro HYPERLAPSE'].run) %}
      {% if park.enable %}
        {% set pos = {'x': 'X' + park.coord.x|string if park.pos != 'y_only' else '', 
                      'y': 'Y' + park.coord.y|string if park.pos != 'x_only' else '', 
                      'z': 'Z'+ [printer.gcode_move.gcode_position.z + park.coord.dz, printer.toolhead.axis_maximum.z]|min|string} %}
        {% set restore = {'absolute': {'coordinates': printer.gcode_move.absolute_coordinates,
                                       'extrude'    : printer.gcode_move.absolute_extrude},
                          'speed'   : printer.gcode_move.speed,
                          'e'       : printer.gcode_move.gcode_position.e,
                          'factor'  : {'speed'  : printer.gcode_move.speed_factor,
                                       'extrude': printer.gcode_move.extrude_factor}} %}
        SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=restore VALUE="{restore}"
        {% if not printer[printer.toolhead.extruder].can_extrude %}
          {% if verbose %}{action_respond_info("Timelapse: Warning, minimum extruder temperature not reached!")}{% endif %}
        {% else %}
          {% if extruder.fw_retract %}
            G10
          {% else %}
            M83     ; insure relative extrusion
            G0 E-{extruder.retract} F{speed.retract * 60}
          {% endif %}
        {% endif %}
        SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=is_paused VALUE=True 
        {macro.pause}            ; execute the klipper PAUSE command
        SET_GCODE_OFFSET X=0 Y=0 ; this will insure that the head parks always at the same position in a multi setup
        G90                      ; insure absolute move
        {% if "xyz" not in printer.toolhead.homed_axes %}
          {% if verbose %}{action_respond_info("Timelapse: Warning, axis not homed yet!")}{% endif %}
        {% else %}
          G0 {pos.x} {pos.y} {pos.z} F{speed.travel * 60}
        {% endif %}
        SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=takingframe VALUE=True
        UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_TAKE_FRAME DURATION={check_time}
        M400
      {% endif %}
      _TIMELAPSE_NEW_FRAME HYPERLAPSE={hyperlapse}
    {% endif %}    
  {% else %}
    {% if verbose %}{action_respond_info("Timelapse: disabled, take frame ignored")}{% endif %}
  {% endif %}

[gcode_macro _TIMELAPSE_NEW_FRAME]
description: action call for timelapse shoot. must be a seperate macro
gcode:  
 {action_call_remote_method("timelapse_newframe", 
                            macropark=printer['gcode_macro TIMELAPSE_TAKE_FRAME'].park, 
                            hyperlapse=params.HYPERLAPSE)}

[delayed_gcode _WAIT_TIMELAPSE_TAKE_FRAME]
gcode:
  {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
  {% set factor = {'speed': printer.gcode_move.speed_factor, 'extrude': printer.gcode_move.extrude_factor} %}
  {% if tl.takingframe %}
    UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_TAKE_FRAME DURATION={tl.check_time}
  {% else %}
    {tl.macro.resume} VELOCITY={tl.speed.travel} ; execute the klipper RESUME command
    SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=is_paused VALUE=False
    {% if not printer[printer.toolhead.extruder].can_extrude %}
        {action_respond_info("Timelapse: Warning minimum extruder temperature not reached!")}
    {% else %}
      {% if tl.extruder.fw_retract %}
        G11
      {% else %}
        G0 E{tl.extruder.extrude} F{tl.speed.extrude * 60}
        G0 F{tl.restore.speed}
        {% if tl.restore.absolute.extrude %} 
          M82
          G92 E{tl.restore.e}
        {% endif %}
      {% endif %}
    {% endif %}
    {% if tl.restore.factor.speed   != factor.speed   %} M220 S{(factor.speed*100)|round(0)}   {% endif %}
    {% if tl.restore.factor.extrude != factor.extrude %} M221 S{(factor.extrude*100)|round(0)} {% endif %}
    {% if not tl.restore.absolute.coordinates %} G91 {% endif %}
  {% endif %}

####################################################################################################
#                                                                                                  #
# HYPERLAPSE: Starts or stops a Hyperlapse video                                                   #
# Usage: HYPERLAPSE ACTION=START [CYCLE=time] starts a hyperlapse with cycle time (default 30 sec) #
#        HYPERLAPSE ACTION=STOP stops the hyperlapse recording                                     # 
#                                                                                                  #
####################################################################################################

######################### definition #########################
## cycle: cycle time in seconds
## run: internal use [True/False]
###############################################################
[gcode_macro HYPERLAPSE]
description: Start/Stop a hyperlapse recording
variable_cycle: 0
variable_run: False
gcode:
  {% set cycle = params.CYCLE|default(30)|int %}
  {% if params.ACTION and params.ACTION|lower == 'start' %}
    {action_respond_info("Hyperlapse: frames started (Cycle %d sec)" % cycle)}
    SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=run VALUE=True
    SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=cycle VALUE={cycle}
    UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION={cycle}
    TIMELAPSE_TAKE_FRAME HYPERLAPSE=True
  {% elif params.ACTION and params.ACTION|lower == 'stop' %}
    {% if run %}{action_respond_info("Hyperlapse: frames stopped")}{% endif %}
    SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=run VALUE=False
    UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION=0
  {% else %}
    {action_raise_error("Hyperlapse: No valid input parameter
                         Use: 
                         - HYPERLAPSE ACTION=START [CYCLE=time]
                         - HYPERLAPSE ACTION=STOP")}
  {% endif %}

[delayed_gcode _HYPERLAPSE_LOOP]
gcode:
  UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION={printer["gcode_macro HYPERLAPSE"].cycle}
  TIMELAPSE_TAKE_FRAME HYPERLAPSE=True

##########################################################################
#                                                                        #
# TIMELAPSE_RENDER: Render the video at print end                        #
#                                                                        #
##########################################################################

######################### definition #########################
## render: internal use. Valid inputs: [True, False]
## run_identifier: internal use. Valid input [0 .. 3]
###############################################################
[gcode_macro TIMELAPSE_RENDER]
description: Render Timelapse video and wait for the result
variable_render: False
variable_run_identifier: 0
gcode:
  {action_respond_info("Timelapse: Rendering started")}
  {action_call_remote_method("timelapse_render", byrendermacro="True")}
  SET_GCODE_VARIABLE MACRO=TIMELAPSE_RENDER VARIABLE=render VALUE=True
  {printer.configfile.settings['gcode_macro pause'].rename_existing} ; execute the klipper PAUSE command
  UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_RENDER DURATION=0.5

[delayed_gcode _WAIT_TIMELAPSE_RENDER]
gcode:
  {% set ri = printer['gcode_macro TIMELAPSE_RENDER'].run_identifier % 4 %}
  SET_GCODE_VARIABLE MACRO=TIMELAPSE_RENDER VARIABLE=run_identifier VALUE={ri + 1}
  {% if printer['gcode_macro TIMELAPSE_RENDER'].render %}
    M117 Rendering {['-','\\','|','/'][ri]}
    UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_RENDER DURATION=0.5
  {% else %}
    {action_respond_info("Timelapse: Rendering finished")}
    M117
    {printer.configfile.settings['gcode_macro resume'].rename_existing} ; execute the klipper RESUME command
  {% endif %}

##########################################################################
#                                                                        #
# TEST_STREAM_DELAY: Helper macro to find stream and park delay          #
#                                                                        #
##########################################################################

[gcode_macro TEST_STREAM_DELAY]
description: Helper macro to find stream and park delay
gcode:
  {% set min = printer.toolhead.axis_minimum %}
  {% set max = printer.toolhead.axis_maximum %}
  {% set act = printer.toolhead.position %}
  {% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
  {% if act.z > 5.0 %}
    G0 X{min.x + 5.0} F{tl.speed.travel|int * 60}
    G0 X{(max.x-min.x)/2}
    G4 P{tl.park.time|float * 1000}
    _TIMELAPSE_NEW_FRAME HYPERLAPSE=FALSE
    G0 X{max.x - 5.0}
  {% else %}
    {action_raise_error("Toolhead z %.3f to low. Please place head above z = 5.0" % act.z)}
  {% endif %}
