def test_GetSnapshotGcode_ReturnCommands(self): # test with relative paths, absolute extruder coordinates, retract and z hop # use relative coordinates for stabilizations self.Settings.current_stabilization().x_type = "fixed_path" self.Settings.current_stabilization().x_fixed_path = "50,100" # 125,250 self.Settings.current_stabilization().x_fixed_path_loop = False self.Settings.current_stabilization().x_fixed_path_invert_loop = False self.Settings.current_stabilization().y_type = "fixed_path" self.Settings.current_stabilization().y_fixed_path = "50,100" # 100,200 self.Settings.current_stabilization().y_fixed_path_loop = False self.Settings.current_stabilization().y_fixed_path_invert_loop = False self.Settings.current_snapshot().retract_before_move = True snapshot_gcode_generator = SnapshotGcodeGenerator( self.Settings, self.create_octoprint_printer_profile()) self.Extruder.is_retracted = lambda: True snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( 100, 50, 0, 3600, True, False, self.Extruder, 0.5, "SavedCommand") # verify the return commands self.assertEqual(snapshot_gcode.get_return_commands()[0], "G1 X100.000 Y50.000") self.assertEqual(snapshot_gcode.get_return_commands()[1], "G91") self.assertEqual(snapshot_gcode.get_return_commands()[2], "G1 F6000") self.assertEqual(snapshot_gcode.get_return_commands()[3], "G1 Z-0.500") self.assertEqual(snapshot_gcode.get_return_commands()[4], "G1 F3600") self.assertEqual(snapshot_gcode.get_return_commands()[5], "SAVEDCOMMAND") self.assertEqual(snapshot_gcode.get_return_commands()[6], "M400") self.assertEqual(snapshot_gcode.get_return_commands()[7], "M114")
def test_GetSnapshotGcode_RelativePath_RelativeCoordinates_ExtruderAbsolute_ZHop_Retraction( self): # test with relative paths, absolute extruder coordinates, retract and z hop # use relative coordinates for stabilizations self.Settings.current_stabilization().x_type = "relative_path" self.Settings.current_stabilization( ).x_relative_path = "50,100" # 125,250 self.Settings.current_stabilization().x_relative_path_loop = False self.Settings.current_stabilization( ).x_relative_path_invert_loop = False self.Settings.current_stabilization().y_type = "relative_path" self.Settings.current_stabilization( ).y_relative_path = "50,100" # 100,200 self.Settings.current_stabilization().y_relative_path_loop = False self.Settings.current_stabilization( ).y_relative_path_invert_loop = False self.Settings.current_snapshot().retract_before_move = True snapshot_gcode_generator = SnapshotGcodeGenerator( self.Settings, self.create_octoprint_printer_profile()) snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( 10, 10, 10, 3600, True, False, self.Extruder, 0.5, "SavedCommand") # verify the created gcode self.assertEqual(snapshot_gcode.GcodeCommands[0], "M83") self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 F4000") self.assertEqual(snapshot_gcode.GcodeCommands[2], "G1 E-2.000") self.assertEqual(snapshot_gcode.GcodeCommands[3], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 Z0.500") self.assertEqual(snapshot_gcode.GcodeCommands[5], "G90") self.assertEqual(snapshot_gcode.GcodeCommands[6], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 X125.000 Y100.000") self.assertEqual(snapshot_gcode.GcodeCommands[8], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[9], "M114") self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 X10.000 Y10.000") self.assertEqual(snapshot_gcode.GcodeCommands[11], "G91") self.assertEqual(snapshot_gcode.GcodeCommands[12], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[13], "G1 Z-0.500") self.assertEqual(snapshot_gcode.GcodeCommands[14], "G1 F3000") self.assertEqual(snapshot_gcode.GcodeCommands[15], "G1 E2.000") self.assertEqual(snapshot_gcode.GcodeCommands[16], "M82") self.assertEqual(snapshot_gcode.GcodeCommands[17], "G1 F3600") self.assertEqual(snapshot_gcode.GcodeCommands[18], "SAVEDCOMMAND") self.assertEqual(snapshot_gcode.GcodeCommands[19], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[20], "M114") # verify the indexes of the generated gcode self.assertTrue(snapshot_gcode.SnapshotIndex == 9) self.assertTrue(snapshot_gcode.end_index() == 20) # verify the return coordinates self.assertTrue(snapshot_gcode.ReturnX == 10) self.assertTrue(snapshot_gcode.ReturnY == 10) self.assertTrue(snapshot_gcode.ReturnZ == 10)
def test_GetSnapshotGcode_Fixed_AbsoluteCoordintes_ExtruderRelative(self): """Test snapshot gcode in absolute coordinate system with relative extruder and fixed coordinate stabilization """ # adjust the settings for absolute position and create the snapshot gcode generator self.Settings.current_stabilization().x_type = "fixed_coordinate" self.Settings.current_stabilization().x_fixed_coordinate = 10 self.Settings.current_stabilization().y_type = "fixed_coordinate" self.Settings.current_stabilization().y_fixed_coordinate = 20 snapshot_gcode_generator = SnapshotGcodeGenerator( self.Settings, self.create_octoprint_printer_profile()) self.Extruder.is_retracted = lambda: True self.Position.update(Commands.parse("G90")) self.Position.update(Commands.parse("M83")) self.Position.update(Commands.parse("G28")) self.Position.update(Commands.parse("G0 X95 Y95 Z0.2 F3600")) parsed_command = Commands.parse("G0 X100 Y100") self.Position.update(parsed_command) snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( self.Position, None, parsed_command) gcode_commands = snapshot_gcode.snapshot_gcode() # verify the created gcodegcode_commands self.assertEqual(gcode_commands[0], "G1 E-4.00000 F4800") self.assertEqual(gcode_commands[1], "G1 X10.000 Y20.000 F10800") self.assertEqual(gcode_commands[2], "G1 X100.000 Y100.000") self.assertEqual(gcode_commands[3], "G1 E4.00000 F3000") self.assertEqual(gcode_commands[4], "G1 F3600") self.assertEqual(gcode_commands[5], parsed_command.gcode) # verify the return coordinates self.assertEqual(snapshot_gcode.ReturnX, 100) self.assertEqual(snapshot_gcode.ReturnY, 100) self.assertEqual(snapshot_gcode.ReturnZ, 0.2) self.assertEqual(snapshot_gcode.X, 10) self.assertEqual(snapshot_gcode.Y, 20) self.assertEqual(snapshot_gcode.Z, None)
def test_GetSnapshotGcode_Fixed_AbsoluteCoordintes_ExtruderRelative(self): """Test snapshot gcode in absolute coordinate system with relative extruder and fixed coordinate stabilization """ # adjust the settings for absolute position and create the snapshot gcode generator self.Settings.current_stabilization().x_type = "fixed_coordinate" self.Settings.current_stabilization().x_fixed_coordinate = 10 self.Settings.current_stabilization().y_type = "fixed_coordinate" self.Settings.current_stabilization().y_fixed_coordinate = 20 snapshot_gcode_generator = SnapshotGcodeGenerator( self.Settings, self.create_octoprint_printer_profile()) self.Extruder.is_retracted = lambda: True snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( 0, 0, 0, 3600, False, True, self.Extruder, 0.5, "SavedCommand") # verify the created gcode self.assertEqual(snapshot_gcode.GcodeCommands[0], "G91") self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[2], "G1 Z0.500") self.assertEqual(snapshot_gcode.GcodeCommands[3], "G90") self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[5], "G1 X10.000 Y20.000") self.assertEqual(snapshot_gcode.GcodeCommands[6], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[7], "M114") self.assertEqual(snapshot_gcode.GcodeCommands[8], "G1 X0.000 Y0.000") self.assertEqual(snapshot_gcode.GcodeCommands[9], "G91") self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[11], "G1 Z-0.500") self.assertEqual(snapshot_gcode.GcodeCommands[12], "G90") self.assertEqual(snapshot_gcode.GcodeCommands[13], "G1 F3600") self.assertEqual(snapshot_gcode.GcodeCommands[14], "SAVEDCOMMAND") self.assertEqual(snapshot_gcode.GcodeCommands[15], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[16], "M114") # verify the indexes of the generated gcode self.assertEqual(snapshot_gcode.SnapshotIndex, 7) self.assertEqual(snapshot_gcode.end_index(), 16) # verify the return coordinates self.assertEqual(snapshot_gcode.ReturnX, 0) self.assertEqual(snapshot_gcode.ReturnY, 0) self.assertEqual(snapshot_gcode.ReturnZ, 0)
def test_GetSnapshotGcode_Relative_RelativeCoordinates_AbsoluteExtruder_ZhopTooHigh( self): """Test snapshot gcode with relative stabilization, relative coordinates, absolute extruder, z is too high to hop, no retraction """ # test with relative coordinates, absolute extruder coordinates, z hop impossible (current z height will not # allow this since it puts things outside of the bounds) use relative coordinates for stabilizations self.Settings.current_stabilization().x_type = "relative" self.Settings.current_stabilization().x_relative = 50 # 125 self.Settings.current_stabilization().y_type = "relative" self.Settings.current_stabilization().y_relative = 100 # 200 self.Settings.current_snapshot().retract_before_move = False snapshot_gcode_generator = SnapshotGcodeGenerator( self.Settings, self.create_octoprint_printer_profile()) # create snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( 10, 10, 200, 3600, True, False, self.Extruder, 0.5, "SavedCommand") # verify the created gcode self.assertEqual(snapshot_gcode.GcodeCommands[0], "G90") self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[2], "G1 X125.000 Y200.000") self.assertEqual(snapshot_gcode.GcodeCommands[3], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[4], "M114") self.assertEqual(snapshot_gcode.GcodeCommands[5], "G1 X10.000 Y10.000") self.assertEqual(snapshot_gcode.GcodeCommands[6], "G91") self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 F3600") self.assertEqual(snapshot_gcode.GcodeCommands[8], "SAVEDCOMMAND") self.assertEqual(snapshot_gcode.GcodeCommands[9], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[10], "M114") # verify the indexes of the generated gcode self.assertTrue(snapshot_gcode.SnapshotIndex == 4) self.assertTrue(snapshot_gcode.end_index() == 10) # verify the return coordinates self.assertTrue(snapshot_gcode.ReturnX == 10) self.assertTrue(snapshot_gcode.ReturnY == 10) self.assertTrue(snapshot_gcode.ReturnZ == 200)
class Timelapse(object): def __init__( self, settings, octoprint_printer, data_folder, timelapse_folder, on_print_started=None, on_print_start_failed=None, on_snapshot_start=None, on_snapshot_end=None, on_render_start=None, on_render_end=None, on_timelapse_stopping=None, on_timelapse_stopped=None, on_state_changed=None, on_timelapse_start=None, on_snapshot_position_error=None, on_position_error=None): # config variables - These don't change even after a reset self.DataFolder = data_folder self.Settings = settings # type: OctolapseSettings self.OctoprintPrinter = octoprint_printer self.DefaultTimelapseDirectory = timelapse_folder self.OnPrintStartCallback = on_print_started self.OnPrintStartFailedCallback = on_print_start_failed self.OnRenderStartCallback = on_render_start self.OnRenderEndCallback = on_render_end self.OnSnapshotStartCallback = on_snapshot_start self.OnSnapshotCompleteCallback = on_snapshot_end self.TimelapseStoppingCallback = on_timelapse_stopping self.TimelapseStoppedCallback = on_timelapse_stopped self.OnStateChangedCallback = on_state_changed self.OnTimelapseStartCallback = on_timelapse_start self.OnSnapshotPositionErrorCallback = on_snapshot_position_error self.OnPositionErrorCallback = on_position_error self.Responses = Responses() # Used to decode responses from the 3d printer self.Commands = Commands() # used to parse and generate gcode self.Triggers = None self.RenderingJobs = set() self.PrintEndStatus = "Unknown" # Settings that may be different after StartTimelapse is called self.OctoprintPrinterProfile = None self.PrintStartTime = None self.FfMpegPath = None self.Snapshot = None self.Gcode = None self.Printer = None self.CaptureSnapshot = None self.Position = None self.HasSentInitialStatus = False self.Rendering = None self.State = TimelapseState.Idle self.IsTestMode = False # State Tracking that should only be reset when starting a timelapse self.SnapshotCount = 0 self.HasBeenStopped = False self.TimelapseStopRequested = False self.SavedCommand = None self.SecondsAddedByOctolapse = 0 # State tracking variables self.RequiresLocationDetectionAfterHome = False # fetch position private variables self._position_payload = None self._position_timeout = 600.0 self._position_signal = threading.Event() self._position_signal.set() # get snapshot async private variables self._snapshot_success = False # It shouldn't take more than 5 seconds to take a snapshot! self._snapshot_timeout = 5.0 self._snapshot_signal = threading.Event() self._snapshot_signal.set() self.CurrentProfiles = {} self.CurrentFileLine = 0 self._reset() def start_timelapse( self, settings, octoprint_printer_profile, ffmpeg_path, g90_influences_extruder): # we must supply the settings first! Else reset won't work properly. self._reset() # in case the settings have been destroyed and recreated self.Settings = settings # time tracking - how much time did we add to the print? self.SnapshotCount = 0 self.SecondsAddedByOctolapse = 0 self.HasSentInitialStatus = False self.RequiresLocationDetectionAfterHome = False self.OctoprintPrinterProfile = octoprint_printer_profile self.FfMpegPath = ffmpeg_path self.PrintStartTime = time.time() self.Snapshot = Snapshot(self.Settings.current_snapshot()) self.Gcode = SnapshotGcodeGenerator( self.Settings, octoprint_printer_profile) self.Printer = Printer(self.Settings.current_printer()) self.Rendering = Rendering(self.Settings.current_rendering()) self.CaptureSnapshot = CaptureSnapshot( self.Settings, self.DataFolder, print_start_time=self.PrintStartTime) self.Position = Position( self.Settings, octoprint_printer_profile, g90_influences_extruder) self.State = TimelapseState.WaitingForTrigger self.IsTestMode = self.Settings.current_debug_profile().is_test_mode self.Triggers = Triggers(self.Settings) self.Triggers.create() # take a snapshot of the current settings for use in the Octolapse Tab self.CurrentProfiles = self.Settings.get_profiles_dict() # send an initial state message self._on_timelapse_start() def on_position_received(self, payload): if self.State != TimelapseState.Idle: self._position_payload = payload self._position_signal.set() def get_position_async(self, start_gcode=None, timeout=None): if timeout is None: timeout = self._position_timeout self.Settings.current_debug_profile().log_print_state_change("Octolapse is requesting a position.") # Warning, we can only request one position at a time! if not self._position_signal.is_set(): self.Settings.current_debug_profile().log_warning( "Warning: A position request has already been made, clearing existing signal." ) self._position_signal.clear() # build the staret commands commands_to_send = ["M400", "M114"] # send any code that is to be run before the position request if start_gcode is not None and len(start_gcode) > 0: commands_to_send = start_gcode + commands_to_send self.OctoprintPrinter.commands(commands_to_send) event_is_set = self._position_signal.wait(timeout) if not event_is_set: # we ran into a timeout while waiting for a fresh position # set the position signal self._snapshot_signal.set() self.Settings.current_debug_profile().log_warning( "Warning: A timeout occurred while requesting the current position!." ) return None return self._position_payload def _on_snapshot_success(self, *args, **kwargs): # Increment the number of snapshots received self.SnapshotCount += 1 self._snapshot_success = True self._snapshot_signal.set() def _on_snapshot_fail(self, *args, **kwargs): reason = args[0] message = "Failed to download the snapshot. Reason: {0}".format( reason) self.Settings.current_debug_profile().log_snapshot_download(message) self._snapshot_success = False self.SnapshotError = message self._snapshot_signal.set() def _take_snapshot_async(self): snapshot_async_payload = { "success": False, "error": "" } if not self._snapshot_signal.is_set(): self.Settings.current_debug_profile().log_warning( "Warning: A snapshot request has already been made, clearing existing signal." ) # only clear signal and send a new M114 if we haven't already done that from another thread self._snapshot_signal.clear() # start the snapshot self.Settings.current_debug_profile().log_snapshot_download("Taking a snapshot.") try: self.CaptureSnapshot.snap( utility.get_currently_printing_filename(self.OctoprintPrinter), self.SnapshotCount, on_success=self._on_snapshot_success, on_fail=self._on_snapshot_fail ) event_is_set = self._snapshot_signal.wait(self._snapshot_timeout) if not event_is_set: # we ran into a timeout while waiting for a fresh position snapshot_async_payload["success"] = False snapshot_async_payload["error"] = \ "Snapshot timed out in {0} seconds.".format(self._snapshot_timeout) self._snapshot_signal.set() else: snapshot_async_payload["success"] = True except Exception as e: self.Settings.current_debug_profile().log_exception(e) snapshot_async_payload["success"] = False snapshot_async_payload["error"] = "An unexpected error was encountered while taking a snapshot" return snapshot_async_payload def _take_timelapse_snapshot(self, trigger, triggering_command): timelapse_snapshot_payload = { "snapshot_position": None, "return_position": None, "snapshot_gcode": None, "snapshot_payload": None, "current_snapshot_time": 0, "total_snapshot_time": 0, "success": False, "error": "" } try: # create the GCode for the timelapse and store it snapshot_gcode = self.Gcode.create_snapshot_gcode( self.Position, trigger, triggering_command ) # save the gcode fo the payload timelapse_snapshot_payload["snapshot_gcode"] = snapshot_gcode assert (isinstance(snapshot_gcode, SnapshotGcode)) self.Settings.current_debug_profile().log_snapshot_gcode( "Sending snapshot start gcode and snapshot commands.") # park the printhead in the snapshot position and wait for the movement to complete snapshot_position = self.get_position_async( start_gcode=snapshot_gcode.StartGcode + snapshot_gcode.SnapshotCommands ) timelapse_snapshot_payload["snapshot_position"] = snapshot_position # record the snapshot start time snapshot_start_time = time.time() # take the snapshot snapshot_async_payload = self._take_snapshot_async() timelapse_snapshot_payload["snapshot_payload"] = snapshot_async_payload # calculate the snapshot time snapshot_time = time.time() - snapshot_start_time # record the return movement start time return_start_time = time.time() self.Settings.current_debug_profile().log_snapshot_gcode("Sending snapshot return gcode.") # return the printhead to the start position return_position = self.get_position_async( start_gcode=snapshot_gcode.ReturnCommands ) # Note that sending the EndGccode via the end_gcode parameter allows us to execute additional # gcode and still know when the ReturnCommands were completed. Hopefully this will reduce delays. timelapse_snapshot_payload["return_position"] = return_position self.Settings.current_debug_profile().log_snapshot_gcode("Sending snapshot end gcode.") # send the end gcode end_position = self.get_position_async( start_gcode=snapshot_gcode.EndGcode ) # calculate the return movement time return_time = time.time() - return_start_time # Note that sending the EndGccode via the end_gcode parameter allows us to execute additional # gcode and still know when the ReturnCommands were completed. Hopefully this will reduce delays. timelapse_snapshot_payload["end_position"] = end_position # calculate the total snapshot time # Note that we use 2 * return_time as opposed to snapshot_travel_time + return_time. # This is so we can avoid sending an M400 until after we've lifted and hopped current_snapshot_time = snapshot_time + 2 * return_time self.SecondsAddedByOctolapse += current_snapshot_time timelapse_snapshot_payload["current_snapshot_time"] = current_snapshot_time timelapse_snapshot_payload["total_snapshot_time"] = self.SecondsAddedByOctolapse # we've completed the procedure, set success timelapse_snapshot_payload["success"] = True except Exception as e: self.Settings.current_debug_profile().log_exception(e) timelapse_snapshot_payload["error"] = "An unexpected error was encountered while running the timelapse " \ "snapshot procedure. " return timelapse_snapshot_payload def alter_gcode_for_trigger(self, command): # We don't want to send the snapshot command to the printer, or any of # the SupporessedSavedCommands (gcode.py) if command is None or command == (None,) or command in self.Commands.SuppressedSavedCommands: # this will suppress the command since it won't be added to our snapshot commands list return None # adjust the triggering command for test mode command = self.Commands.get_test_mode_command_string(command) if command == "": return None # send the triggering command return command # public functions def to_state_dict(self): try: position_dict = None position_state_dict = None extruder_dict = None trigger_state = None if self.Settings is not None: if self.Settings.show_position_changes and self.Position is not None: position_dict = self.Position.to_position_dict() if self.Settings.show_position_state_changes and self.Position is not None: position_state_dict = self.Position.to_state_dict() if self.Settings.show_extruder_state_changes and self.Position is not None: extruder_dict = self.Position.Extruder.to_dict() if self.Settings.show_trigger_state_changes and self.Triggers is not None: trigger_state = { "Name": self.Triggers.Name, "Triggers": self.Triggers.state_to_list() } state_dict = { "Extruder": extruder_dict, "Position": position_dict, "PositionState": position_state_dict, "TriggerState": trigger_state } return state_dict except Exception as e: self.Settings.CurrentDebugProfile().log_exception(e) # if we're here, we've reached and logged an error. return { "Extruder": None, "Position": None, "PositionState": None, "TriggerState": None } def stop_snapshots(self, message=None, error=False): self.State = TimelapseState.WaitingToRender if self.TimelapseStoppedCallback is not None: timelapse_stopped_callback_thread = threading.Thread( target=self.TimelapseStoppedCallback, args=[message, error] ) timelapse_stopped_callback_thread.daemon = True timelapse_stopped_callback_thread.start() return True def on_print_failed(self): if self.State != TimelapseState.Idle: self.end_timelapse("FAILED") def on_print_disconnecting(self): if self.State != TimelapseState.Idle: self.end_timelapse("DISCONNECTING") def on_print_disconnected(self): if self.State != TimelapseState.Idle: self.end_timelapse("DISCONNECTED") def on_print_canceled(self): if self.State != TimelapseState.Idle: self.end_timelapse("CANCELED") def on_print_completed(self): if self.State != TimelapseState.Idle: self.end_timelapse("COMPLETED") def end_timelapse(self, print_status): self.PrintEndStatus = print_status try: if self.State in [ TimelapseState.WaitingForTrigger, TimelapseState.WaitingToRender, TimelapseState.WaitingToEndTimelapse ]: if not self._render_timelapse(self.PrintEndStatus): if self.OnRenderEndCallback is not None: payload = RenderingCallbackArgs( "Could not start timelapse job.", -1, "unknown", "unknown", "unknown", "unknown", "unknown", "unknown", False, 0, 0, "RENDER") render_end_callback_thread = threading.Thread( target=self.OnRenderEndCallback, args=[payload] ) render_end_callback_thread.daemon = True render_end_callback_thread.start() self._reset() elif self.State != TimelapseState.Idle: self.State = TimelapseState.WaitingToEndTimelapse except Exception as e: self.Settings.current_debug_profile().log_exception(e) def on_print_paused(self): try: if self.State == TimelapseState.Idle: return elif self.State < TimelapseState.WaitingToRender: self.Settings.current_debug_profile().log_print_state_change("Print Paused.") self.Triggers.pause() except Exception as e: self.Settings.current_debug_profile().log_exception(e) def on_print_resumed(self): try: if self.State == TimelapseState.Idle: return elif self.State < TimelapseState.WaitingToRender: self.Triggers.resume() except Exception as e: self.Settings.current_debug_profile().log_exception(e) def is_timelapse_active(self): if ( self.Settings is None or self.State in [TimelapseState.Idle, TimelapseState.Initializing, TimelapseState.WaitingToRender] or self.Triggers is None or self.Triggers.count() < 1 ): return False return True def get_is_rendering(self): return len(self.RenderingJobs) > 0 def on_print_start(self, tags): self.OnPrintStartCallback(tags) def on_print_start_failed(self, message): self.OnPrintStartFailedCallback(message) def on_gcode_queuing(self, cmd, cmd_type, gcode, tags): # detect print start if ( self.Settings.is_octolapse_enabled and self.State == TimelapseState.Idle and {'trigger:comm.start_print', 'fileline:1'} <= tags and self.OctoprintPrinter.is_printing() ): if self.OctoprintPrinter.set_job_on_hold(True): try: self.State = TimelapseState.Initializing self.Settings.current_debug_profile().log_print_state_change( "Print Start Detected. Command: {0}, Tags:{1}".format(cmd, tags) ) # call the synchronous callback on_print_start self.on_print_start(tags) if self.State == TimelapseState.WaitingForTrigger: # set the current line to 0 so that the plugin checks for line 1 below after startup. self.CurrentFileLine = 0 else: # if we're not in the waiting for trigger state, there is a problem self.on_print_start_failed( "Unable to start timelapse, failed to initialize. Print start failed." ) finally: self.OctoprintPrinter.set_job_on_hold(False) else: self.on_print_start_failed( "Unabled to start timelapse, failed to acquire a job lock. Print start failed." ) # if the timelapse is not active, exit without changing any gcode if not self.is_timelapse_active(): return # check the current line number if {'source:file'} in tags: # this line is from the file, advance! self.CurrentFileLine += 1 if "fileline:{0}".format(self.CurrentFileLine) not in tags: actual_file_line = "unknown" for tag in tags: if len(tag) > 9 and tag.startswith("fileline:"): actual_file_line = tag[9:] message = "File line number {0} was expected, but {1} was received!".format( self.CurrentFileLine + 1, actual_file_line ) self.Settings.current_debug_profile().log_error(message) self.stop_snapshots(message, True) try: self.Settings.current_debug_profile().log_gcode_queuing( "Queuing Command: Command Type:{0}, gcode:{1}, cmd: {2}, tags: {3}".format(cmd_type, gcode, cmd, tags)) # update the position tracker so that we know where all of the axis are. # We will need this later when generating snapshot gcode so that we can return to the previous # position cmd = cmd.upper().strip() # create our state change dictionaries position_change_dict = None position_state_change_dict = None extruder_change_dict = None trigger_change_list = None self.Position.update(cmd) # capture any changes, if necessary, to the position, position state and extruder state # Note: We'll get the trigger state later if (self.Settings.show_position_changes and (self.Position.has_position_changed() or not self.HasSentInitialStatus)): position_change_dict = self.Position.to_position_dict() if (self.Settings.show_position_state_changes and (self.Position.has_state_changed() or not self.HasSentInitialStatus)): position_state_change_dict = self.Position.to_state_dict() if (self.Settings.show_extruder_state_changes and (self.Position.Extruder.has_changed() or not self.HasSentInitialStatus)): extruder_change_dict = self.Position.Extruder.to_dict() # get the position state in case it has changed # if there has been a position or extruder state change, inform any listener is_snapshot_gcode_command = self._is_snapshot_command(cmd) # make sure we're not using inches is_metric = self.Position.is_metric() if is_metric is None and self.Position.has_position_error(): self.stop_snapshots( "The printer profile requires an explicit G21 command before any position altering/setting " "commands, including any home commands. Stopping timelapse, but continuing the print. ", error=True ) elif not is_metric and self.Position.has_position_error(): if self.Printer.units_default == "inches": self.stop_snapshots( "The printer profile uses 'inches' as the default unit of measurement. In order to use " "Octolapse, a G21 command must come before any position altering/setting commands, including" " any home commands. Stopping timelapse, but continuing the print. ", error=True ) else: self.stop_snapshots( "The gcode file contains a G20 command (set units to inches), which Octolapse does not " "support. Stopping timelapse, but continuing the print.", error=True ) elif self.Position.has_position_error(0): self._on_position_error() # check to see if we've just completed a home command elif (self.State == TimelapseState.WaitingForTrigger and (self.Position.requires_location_detection(1)) and self.OctoprintPrinter.is_printing()): self.State = TimelapseState.AcquiringLocation def acquire_position_async(post_position_command): try: self.Settings.current_debug_profile().log_print_state_change( "A position altering command has been detected. Fetching and updating position. " "Position Command: {0}".format(post_position_command)) # Undo the last position update, we will be resending the command self.Position.undo_update() current_position = self.get_position_async() if current_position is None: self.PrintEndStatus = "POSITION_TIMEOUT" self.State = TimelapseState.WaitingToEndTimelapse self.Settings.current_debug_profile().log_print_state_change( "Unable to acquire a position.") else: # update position self.Position.update_position( x=current_position["x"], y=current_position["y"], z=current_position["z"], e=current_position["e"], force=True, calculate_changes=True) # adjust the triggering command if post_position_command is not None and post_position_command != (None,): post_position_command = self.Commands.get_test_mode_command_string( post_position_command ) if post_position_command != "": self.Settings.current_debug_profile().log_print_state_change( "Sending saved command - {0}.".format(post_position_command)) # send the triggering command self.OctoprintPrinter.commands(post_position_command) # set the state if self.State == TimelapseState.AcquiringLocation: self.State = TimelapseState.WaitingForTrigger self.Settings.current_debug_profile().log_print_state_change("Position Acquired") finally: try: if self.State == TimelapseState.WaitingToEndTimelapse: self.stop_snapshots( "A timeout occurred when attempting to acquire the printer's current position." "The current timeout is {0} seconds. Stopping timelapse.".format( self._position_timeout ), True ) finally: self.OctoprintPrinter.set_job_on_hold(False) if self.OctoprintPrinter.set_job_on_hold(True): thread = threading.Thread(target=acquire_position_async, args=[cmd]) thread.daemon = True thread.start() return None, elif (self.State == TimelapseState.WaitingForTrigger and self.OctoprintPrinter.is_printing() and not self.Position.has_position_error(0)): self.Triggers.update(self.Position, cmd) # If our triggers have changed, update our dict if self.Settings.show_trigger_state_changes and self.Triggers.has_changed(): trigger_change_list = self.Triggers.changes_to_list() _first_triggering = self.get_first_triggering() if _first_triggering: # set the state self.State = TimelapseState.TakingSnapshot def take_snapshot_async(triggering_command, trigger): timelapse_snapshot_payload = None try: self.Settings.current_debug_profile().log_snapshot_download( "About to take a snapshot. Triggering Command: {0}".format( triggering_command)) if self.OnSnapshotStartCallback is not None: snapshot_callback_thread = threading.Thread(target=self.OnSnapshotStartCallback) snapshot_callback_thread.daemon = True snapshot_callback_thread.start() # Undo the last position update, we're not going to be using it! self.Position.undo_update() # take the snapshot timelapse_snapshot_payload = self._take_timelapse_snapshot(trigger, triggering_command) self.Settings.current_debug_profile().log_snapshot_download("The snapshot has completed") finally: # set the state if self.State == TimelapseState.TakingSnapshot: self.State = TimelapseState.WaitingForTrigger timelapse_ended = False try: if self.State == TimelapseState.WaitingToEndTimelapse: timelapse_ended = False self.stop_snapshots( "A timeout occurred when attempting to take a snapshot." "The current timeout is {0} seconds. Stopping timelapse.".format( self._snapshot_timeout), True ) finally: self.OctoprintPrinter.set_job_on_hold(False) if not timelapse_ended: # notify that we're finished, but only if we haven't just stopped the timelapse. self._on_trigger_snapshot_complete(timelapse_snapshot_payload) if self.OctoprintPrinter.set_job_on_hold(True): thread = threading.Thread(target=take_snapshot_async, args=[cmd, _first_triggering]) thread.daemon = True thread.start() return None, elif self.State == TimelapseState.TakingSnapshot: # Don't do anything further to any commands unless we are # taking a timelapse , or if octolapse paused the print. # suppress any commands we don't, under any cirumstances, # to execute while we're taking a snapshot if cmd in self.Commands.SuppressedSnapshotGcodeCommands: cmd = None, # suppress the command if is_snapshot_gcode_command: # in all cases do not return the snapshot command to the printer. # It is NOT a real gcode and could cause errors. cmd = None, # notify any callbacks self._on_state_changed( position_change_dict, position_state_change_dict, extruder_change_dict, trigger_change_list) self.HasSentInitialStatus = True if cmd != (None,): cmd = self._get_command_for_octoprint(cmd) except: e = sys.exc_info()[0] self.Settings.current_debug_profile().log_exception(e) raise return cmd def get_first_triggering(self): try: # make sure we're in a state that could want to check for triggers if not self.State == TimelapseState.WaitingForTrigger: return False # see if the PREVIOUS command triggered (that means current gcode gets sent if the trigger[0] # is triggering first_trigger = self.Triggers.get_first_triggering(0, Triggers.TRIGGER_TYPE_IN_PATH) if first_trigger: self.Settings.current_debug_profile().log_triggering("An in-path snapshot is triggering") return first_trigger first_trigger = self.Triggers.get_first_triggering(1, Triggers.TRIGGER_TYPE_DEFAULT) if first_trigger: # We're triggering self.Settings.current_debug_profile().log_triggering("A snapshot is triggering") return first_trigger except Exception as e: self.Settings.current_debug_profile().log_exception(e) # no need to re-raise here, the trigger just won't happen return False def on_gcode_sent(self, cmd, cmd_type, gcode, tags): self.Settings.current_debug_profile().log_gcode_sent( "Sent to printer: Command Type:{0}, gcode:{1}, cmd: {2}, tags: {3}".format(cmd_type, gcode, cmd, tags)) def on_gcode_received(self, comm, line, *args, **kwargs): self.Settings.current_debug_profile().log_gcode_received( "Received from printer: line:{0}".format(line) ) return line # internal functions #################### def _get_command_for_octoprint(self, cmd): if cmd is None or cmd == (None,): return cmd if self.IsTestMode and self.State >= TimelapseState.WaitingForTrigger: return self.Commands.alter_for_test_mode(cmd) # if we were given a list, return it. if isinstance(cmd, list): return cmd # if we were given a command return None (don't change the command at all) return None def _on_state_changed( self, position_change_dict, position_state_change_dict, extruder_change_dict, trigger_change_list): """Notifies any callbacks about any changes contained in the dictionaries. If you send a dict here the client will get a message, so check the settings to see if they are subscribed to notifications before populating the dictinaries!""" trigger_changes_dict = None try: # Notify any callbacks if (self.OnStateChangedCallback is not None and (position_change_dict is not None or position_state_change_dict is not None or extruder_change_dict is not None or trigger_change_list is not None)): if trigger_change_list is not None and len(trigger_change_list) > 0: trigger_changes_dict = { "Name": self.Triggers.Name, "Triggers": trigger_change_list } change_dict = { "Extruder": extruder_change_dict, "Position": position_change_dict, "PositionState": position_state_change_dict, "TriggerState": trigger_changes_dict } if ( change_dict["Extruder"] is not None or change_dict["Position"] is not None or change_dict["PositionState"] is not None or change_dict["TriggerState"] is not None ): state_changed_callback_thread = threading.Thread( target=self.OnStateChangedCallback, args=[change_dict] ) state_changed_callback_thread.daemon = True state_changed_callback_thread.start() except Exception as e: # no need to re-raise, callbacks won't be notified, however. self.Settings.current_debug_profile().log_exception(e) def _is_snapshot_command(self, command): command_name = get_gcode_from_string(command) snapshot_command_name = get_gcode_from_string(self.Printer.snapshot_command) return command_name == snapshot_command_name def _is_trigger_waiting(self): # make sure we're in a state that could want to check for triggers if not self.State == TimelapseState.WaitingForTrigger: return None # Loop through all of the active currentTriggers waiting_trigger = self.Triggers.get_first_waiting() if waiting_trigger is not None: return True return False def _on_position_error(self): message = self.Position.position_error(0) self.Settings.current_debug_profile().log_error(message) if self.OnPositionErrorCallback is not None: position_error_callback_thread = threading.Thread( target=self.OnPositionErrorCallback, args=[message] ) position_error_callback_thread.daemon = True position_error_callback_thread.start() def _on_trigger_snapshot_complete(self, snapshot_payload): if self.OnSnapshotCompleteCallback is not None: payload = { "success": snapshot_payload["success"], "error": snapshot_payload["error"], "snapshot_count": self.SnapshotCount, "total_snapshot_time": snapshot_payload["total_snapshot_time"], "current_snapshot_time": snapshot_payload["total_snapshot_time"] } if self.OnSnapshotCompleteCallback is not None: snapshot_complete_callback_thread = threading.Thread( target=self.OnSnapshotCompleteCallback, args=[payload] ) snapshot_complete_callback_thread.daemon = True snapshot_complete_callback_thread.start() def _render_timelapse(self, print_end_state): # make sure we have a non null TimelapseSettings object. We may have terminated the timelapse for some reason if self.Rendering is not None and self.Rendering.enabled: self.Settings.current_debug_profile().log_render_start("Started Rendering Timelapse") # we are rendering, set the state before starting the rendering job. timelapse_render_job = Render( self.Settings, self.Snapshot, self.Rendering, self.DataFolder, self.DefaultTimelapseDirectory, self.FfMpegPath, 1, time_added=self.SecondsAddedByOctolapse, on_render_start=self._on_render_start, on_render_fail=self._on_render_fail, on_render_success=self._on_render_success, on_render_complete=self.on_render_complete, on_after_sync_fail=self._on_synchronize_rendering_fail, on_after_sync_success=self._on_synchronize_rendering_complete, on_complete=self._on_render_end ) job_id = "TimelapseRenderJob_{0}".format(str(uuid.uuid4())) self.RenderingJobs.add(job_id) try: timelapse_render_job.process(job_id, utility.get_currently_printing_filename( self.OctoprintPrinter), self.PrintStartTime, time.time(), print_end_state) return True except Exception as e: self.Settings.current_debug_profile().log_exception(e) self.RenderingJobs.remove(job_id) return False def _on_render_start(self, *args, **kwargs): job_id = args[0] self.Settings.current_debug_profile().log_render_start( "Started rendering/synchronizing the timelapse. JobId: {0}".format(job_id)) payload = args[1] # notify the caller if self.OnRenderStartCallback is not None: render_start_complete_callback_thread = threading.Thread( target=self.OnRenderStartCallback, args=[payload] ) render_start_complete_callback_thread.daemon = True render_start_complete_callback_thread.start() def _on_render_fail(self, *args, **kwargs): job_id = args[0] self.Settings.current_debug_profile().log_render_fail( "The timelapse rendering failed. JobId: {0}".format(job_id)) def _on_render_success(self, *args, **kwargs): job_id = args[0] self.Settings.current_debug_profile().log_render_complete( "Rendering completed successfully. JobId: {0}".format(job_id)) # payload = args[1] def on_render_complete(self, *args, **kwargs): job_id = args[0] # payload = args[1] self.Settings.current_debug_profile().log_render_complete( "Completed rendering the timelapse. JobId: {0}".format(job_id)) def _on_synchronize_rendering_fail(self, *args, **kwargs): job_id = args[0] payload = args[1] self.Settings.current_debug_profile().log_render_sync( "Synchronization with the default timelapse plugin failed." " JobId: {0}, Reason: {1}".format(job_id, payload.Reason) ) def _on_synchronize_rendering_complete(self, *args, **kwargs): job_id = args[0] # payload = args[1] self.Settings.current_debug_profile().log_render_sync( "Synchronization with the default timelapse plugin was successful. JobId: {0}".format(job_id)) def _on_render_end(self, *args, **kwargs): job_id = args[0] payload = args[1] self.Settings.current_debug_profile().log_render_complete("Completed rendering. JobId: {0}".format(job_id)) assert (isinstance(payload, RenderingCallbackArgs)) # Remove job from list. If it is not there, raise an exception. self.RenderingJobs.remove(job_id) if payload.ErrorType is not None: if self.Snapshot.cleanup_after_render_fail: self.CaptureSnapshot.clean_snapshots(utility.get_snapshot_temp_directory(self.DataFolder)) else: if self.Snapshot.cleanup_after_render_complete: self.CaptureSnapshot.clean_snapshots(utility.get_snapshot_temp_directory(self.DataFolder)) if self.OnRenderEndCallback is not None: render_end_complete_callback_thread = threading.Thread( target=self.OnRenderEndCallback, args=[payload] ) render_end_complete_callback_thread.daemon = True render_end_complete_callback_thread.start() def _on_timelapse_start(self): if self.OnTimelapseStartCallback is None: return self.OnTimelapseStartCallback() def _reset(self): self.State = TimelapseState.Idle self.CurrentFileLine = 0 self.HasSentInitialStatus = False if self.Triggers is not None: self.Triggers.reset() self.CommandIndex = -1 self.PrintStartTime = None self.SnapshotGcodes = None self.SavedCommand = None self.PositionRequestAttempts = 0 self.IsTestMode = False self.ReturnPositionReceivedTime = None # A list of callbacks who want to be informed when a timelapse ends self.TimelapseStopRequested = False self._snapshot_success = False self.SnapshotError = "" self.HasBeenStopped = False self.CurrentProfiles = { "printer": "", "stabilization": "", "snapshot": "", "rendering": "", "camera": "", "debug_profile": "" } # fetch position private variables self._position_payload = None self._position_signal.set() # get snapshot async private variables self._snapshot_signal.set() def _reset_snapshot(self): self.State = TimelapseState.WaitingForTrigger self.CommandIndex = -1 self.SnapshotGcodes = None self.SavedCommand = None self.PositionRequestAttempts = 0 self._snapshot_success = False self.SnapshotError = ""
def test_GetSnapshotGcode_FixedPath_RelativeCoordinates_ExtruderAbsolute_ZHop_AlreadyRetracted(self): # test with relative paths, absolute extruder coordinates, retract and z hop # use relative coordinates for stabilizations self.Settings.current_stabilization().x_type = "fixed_path" self.Settings.current_stabilization().x_fixed_path = "50,100" # 125,250 self.Settings.current_stabilization().x_fixed_path_loop = False self.Settings.current_stabilization().x_fixed_path_invert_loop = False self.Settings.current_stabilization().y_type = "fixed_path" self.Settings.current_stabilization().y_fixed_path = "50,100" # 100,200 self.Settings.current_stabilization().y_fixed_path_loop = False self.Settings.current_stabilization().y_fixed_path_invert_loop = False self.Settings.current_snapshot().retract_before_move = True snapshot_gcode_generator = SnapshotGcodeGenerator( self.Settings, self.create_octoprint_printer_profile()) self.Extruder.is_retracted = lambda: True snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( 100, 50, 0, 3600, True, False, self.Extruder, 0.5, "SavedCommand") # verify the created gcode self.assertEqual(snapshot_gcode.GcodeCommands[0], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 Z0.500") self.assertEqual(snapshot_gcode.GcodeCommands[2], "G90") self.assertEqual(snapshot_gcode.GcodeCommands[3], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 X50.000 Y50.000") self.assertEqual(snapshot_gcode.GcodeCommands[5], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[6], "M114") self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 X100.000 Y50.000") self.assertEqual(snapshot_gcode.GcodeCommands[8], "G91") self.assertEqual(snapshot_gcode.GcodeCommands[9], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 Z-0.500") self.assertEqual(snapshot_gcode.GcodeCommands[11], "G1 F3600") self.assertEqual(snapshot_gcode.GcodeCommands[12], "SAVEDCOMMAND") self.assertEqual(snapshot_gcode.GcodeCommands[13], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[14], "M114") # verify the indexes of the generated gcode self.assertEqual(snapshot_gcode.SnapshotIndex, 6) self.assertEqual(snapshot_gcode.end_index(), 14) # verify the return coordinates self.assertEqual(snapshot_gcode.ReturnX, 100) self.assertEqual(snapshot_gcode.ReturnY, 50) self.assertEqual(snapshot_gcode.ReturnZ, 0) # Get the next coordinate in the path snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode( 101, 51, 0, 3600, True, False, self.Extruder, 0.5, "SavedCommand") # verify the created gcode self.assertEqual(snapshot_gcode.GcodeCommands[0], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 Z0.500") self.assertEqual(snapshot_gcode.GcodeCommands[2], "G90") self.assertEqual(snapshot_gcode.GcodeCommands[3], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 X100.000 Y100.000") self.assertEqual(snapshot_gcode.GcodeCommands[5], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[6], "M114") self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 X101.000 Y51.000") self.assertEqual(snapshot_gcode.GcodeCommands[8], "G91") self.assertEqual(snapshot_gcode.GcodeCommands[9], "G1 F6000") self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 Z-0.500") self.assertEqual(snapshot_gcode.GcodeCommands[11], "G1 F3600") self.assertEqual(snapshot_gcode.GcodeCommands[12], "SAVEDCOMMAND") self.assertEqual(snapshot_gcode.GcodeCommands[13], "M400") self.assertEqual(snapshot_gcode.GcodeCommands[14], "M114") # verify the indexes of the generated gcode self.assertEqual(snapshot_gcode.SnapshotIndex, 6) self.assertEqual(snapshot_gcode.end_index(), 14) # verify the return coordinates self.assertEqual(snapshot_gcode.ReturnX, 101) self.assertEqual(snapshot_gcode.ReturnY, 51) self.assertEqual(snapshot_gcode.ReturnZ, 0)
class Timelapse(object): def __init__( self, settings, octoprint_printer, data_folder, timelapse_folder, on_print_started=None, on_print_start_failed=None, on_snapshot_start=None, on_snapshot_end=None, on_render_start=None, on_render_end=None, on_timelapse_stopping=None, on_timelapse_stopped=None, on_state_changed=None, on_timelapse_start=None, on_timelapse_end=None, on_snapshot_position_error=None, on_position_error=None, on_plugin_message_sent=None): # config variables - These don't change even after a reset self.DataFolder = data_folder self.Settings = settings # type: OctolapseSettings self.OctoprintPrinter = octoprint_printer self.DefaultTimelapseDirectory = timelapse_folder self.OnPrintStartCallback = on_print_started self.OnPrintStartFailedCallback = on_print_start_failed self.OnRenderStartCallback = on_render_start self.OnRenderEndCallback = on_render_end self.OnSnapshotStartCallback = on_snapshot_start self.OnSnapshotCompleteCallback = on_snapshot_end self.TimelapseStoppingCallback = on_timelapse_stopping self.TimelapseStoppedCallback = on_timelapse_stopped self.OnStateChangedCallback = on_state_changed self.OnTimelapseStartCallback = on_timelapse_start self.OnTimelapseEndCallback = on_timelapse_end self.OnSnapshotPositionErrorCallback = on_snapshot_position_error self.OnPositionErrorCallback = on_position_error self.OnPluginMessageSentCallback = on_plugin_message_sent self.Commands = Commands() # used to parse and generate gcode self.Triggers = None self.PrintEndStatus = "Unknown" self.LastStateChangeMessageTime = None self.StateChangeMessageThread = None # Settings that may be different after StartTimelapse is called self.OctoprintPrinterProfile = None self.PrintStartTime = None self.FfMpegPath = None self.Snapshot = None self.Gcode = None self.Printer = None self.CaptureSnapshot = None self.Position = None self.Rendering = None self.State = TimelapseState.Idle self.IsTestMode = False # State Tracking that should only be reset when starting a timelapse self.SnapshotCount = 0 self.HasBeenStopped = False self.TimelapseStopRequested = False self.SavedCommand = None self.SecondsAddedByOctolapse = 0 # State tracking variables self.RequiresLocationDetectionAfterHome = False # fetch position private variables self._position_payload = None self._position_timeout_long = 600.0 self._position_timeout_short = 10.0 self._position_signal = threading.Event() self._position_signal.set() # get snapshot async private variables self._snapshot_success = False # It shouldn't take more than 5 seconds to take a snapshot! self._snapshot_timeout = 5.0 self._snapshot_signal = threading.Event() self._snapshot_signal.set() self._most_recent_snapshot_payload = None self.CurrentProfiles = {} self.CurrentFileLine = 0 # snapshot thread queue self._snapshot_task_queue = Queue(maxsize=1) self._rendering_task_queue = Queue(maxsize=1) self._reset() def start_timelapse( self, settings, octoprint_printer_profile, ffmpeg_path, g90_influences_extruder): # we must supply the settings first! Else reset won't work properly. self._reset() # in case the settings have been destroyed and recreated self.Settings = settings # time tracking - how much time did we add to the print? self.SnapshotCount = 0 self.SecondsAddedByOctolapse = 0 self.RequiresLocationDetectionAfterHome = False self.OctoprintPrinterProfile = octoprint_printer_profile self.FfMpegPath = ffmpeg_path self.PrintStartTime = time.time() self.Snapshot = Snapshot(self.Settings.current_snapshot()) self.Gcode = SnapshotGcodeGenerator( self.Settings, octoprint_printer_profile) self.Printer = Printer(self.Settings.current_printer()) self.Rendering = Rendering(self.Settings.current_rendering()) self.CaptureSnapshot = CaptureSnapshot( self.Settings, self.DataFolder, print_start_time=self.PrintStartTime) self.Position = Position( self.Settings, octoprint_printer_profile, g90_influences_extruder) self.State = TimelapseState.WaitingForTrigger self.IsTestMode = self.Settings.current_debug_profile().is_test_mode self.Triggers = Triggers(self.Settings) self.Triggers.create() # take a snapshot of the current settings for use in the Octolapse Tab self.CurrentProfiles = self.Settings.get_profiles_dict() # send an initial state message self._on_timelapse_start() def on_position_received(self, payload): if self.State != TimelapseState.Idle: self._position_payload = payload self._position_signal.set() def send_snapshot_gcode_array(self, gcode_array): self.OctoprintPrinter.commands(gcode_array, tags={"snapshot_gcode"}) def get_position_async(self, start_gcode=None, timeout=None): if timeout is None: timeout = self._position_timeout_long self.Settings.current_debug_profile().log_print_state_change("Octolapse is requesting a position.") # Warning, we can only request one position at a time! if self._position_signal.is_set(): self._position_signal.clear() # build the staret commands commands_to_send = ["M400", "M114"] # send any code that is to be run before the position request if start_gcode is not None and len(start_gcode) > 0: commands_to_send = start_gcode + commands_to_send self.send_snapshot_gcode_array(commands_to_send) event_is_set = self._position_signal.wait(timeout) if not event_is_set: # we ran into a timeout while waiting for a fresh position # set the position signal self._snapshot_signal.set() self.Settings.current_debug_profile().log_warning( "Warning: A timeout occurred while requesting the current position!." ) return None return self._position_payload def _on_snapshot_success(self, *args, **kwargs): # Increment the number of snapshots received self.SnapshotCount += 1 self._snapshot_success = True self._snapshot_signal.set() def _on_snapshot_fail(self, *args, **kwargs): reason = args[0] message = "Failed to download the snapshot. Reason: {0}".format( reason) self.Settings.current_debug_profile().log_snapshot_download(message) self._snapshot_success = False self.SnapshotError = message self._snapshot_signal.set() def _on_snapshot_complete(self, *args, **kwargs): self.Settings.current_debug_profile().log_snapshot_download("Snapshot download complete.") def _take_snapshot_async(self): snapshot_async_payload = { "success": False, "error": "Waiting on thread to signal, aborting" } if self._snapshot_signal.is_set(): # only clear signal and send a new M114 if we haven't already done that from another thread self._snapshot_signal.clear() # start the snapshot self.Settings.current_debug_profile().log_snapshot_download("Taking a snapshot.") snapshot_guid = str(uuid.uuid4()) snapshot_job = self.CaptureSnapshot.create_snapshot_job( utility.get_currently_printing_filename(self.OctoprintPrinter), self.SnapshotCount, snapshot_guid, self._snapshot_task_queue, on_success=self._on_snapshot_success, on_fail=self._on_snapshot_fail, on_complete=self._on_snapshot_complete ) self._snapshot_task_queue.put(snapshot_guid) snapshot_thread = threading.Thread(target=snapshot_job) snapshot_thread.daemon = True snapshot_thread.start() event_is_set = self._snapshot_signal.wait(self._snapshot_timeout) if not event_is_set: # we ran into a timeout while waiting for a fresh position snapshot_async_payload["success"] = False snapshot_async_payload["error"] = \ "Snapshot timed out in {0} seconds.".format(self._snapshot_timeout) self._snapshot_signal.set() else: snapshot_async_payload["success"] = True return snapshot_async_payload def _take_timelapse_snapshot( self, trigger, command_string, cmd, parameters, triggering_command_position, triggering_extruder_position ): timelapse_snapshot_payload = { "snapshot_position": None, "return_position": None, "snapshot_gcode": None, "snapshot_payload": None, "current_snapshot_time": 0, "total_snapshot_time": 0, "success": False, "error": "" } try: show_real_snapshot_time = self.Settings.show_real_snapshot_time # create the GCode for the timelapse and store it snapshot_gcode = self.Gcode.create_snapshot_gcode( self.Position, trigger, command_string, cmd, parameters, triggering_command_position, triggering_extruder_position ) # save the gcode fo the payload timelapse_snapshot_payload["snapshot_gcode"] = snapshot_gcode if snapshot_gcode is None: self.Settings.current_debug_profile().log_warning( "No snapshot gcode was generated." ) return timelapse_snapshot_payload assert (isinstance(snapshot_gcode, SnapshotGcode)) if not show_real_snapshot_time: gcodes_to_send = snapshot_gcode.StartGcode + snapshot_gcode.SnapshotCommands if len(gcodes_to_send) > 0: self.Settings.current_debug_profile().log_snapshot_gcode( "Sending snapshot start gcode and snapshot commands.") snapshot_position = self.get_position_async( start_gcode=snapshot_gcode.StartGcode + snapshot_gcode.SnapshotCommands ) else: self.Settings.current_debug_profile().log_snapshot_gcode( "Sending snapshot start gcode.") # send start commands and zhop/retract if they exist if len(snapshot_gcode.StartGcode) > 0: start_position = self.get_position_async(start_gcode=snapshot_gcode.StartGcode) # Todo: Handle start_position = None # park the printhead in the snapshot position and wait for the movement to complete snapshot_start_time = time.time() if len(snapshot_gcode.SnapshotCommands) > 0: self.Settings.current_debug_profile().log_snapshot_gcode("Sending snapshot commands.") snapshot_position = self.get_position_async( start_gcode=snapshot_gcode.SnapshotCommands, timeout=self._position_timeout_short ) # record the snapshot position timelapse_snapshot_payload["snapshot_position"] = snapshot_position # by now we should be ready to take a snapshot snapshot_async_payload = self._take_snapshot_async() timelapse_snapshot_payload["snapshot_payload"] = snapshot_async_payload if not show_real_snapshot_time: # return the printhead to the start position gcode_to_send = snapshot_gcode.ReturnCommands + snapshot_gcode.EndGcode if len (gcode_to_send) > 0: self.Settings.current_debug_profile().log_snapshot_gcode("Sending snapshot return and end gcode.") self.send_snapshot_gcode_array(gcode_to_send) else: if len(snapshot_gcode.ReturnCommands) > 0: self.Settings.current_debug_profile().log_snapshot_gcode("Sending return gcode.") return_position = self.get_position_async( start_gcode=snapshot_gcode.ReturnCommands, timeout=self._position_timeout_short ) timelapse_snapshot_payload["return_position"] = return_position # calculate the total snapshot time snapshot_end_time = time.time() snapshot_time = snapshot_end_time - snapshot_start_time self.SecondsAddedByOctolapse += snapshot_time timelapse_snapshot_payload["current_snapshot_time"] = snapshot_time timelapse_snapshot_payload["total_snapshot_time"] = self.SecondsAddedByOctolapse if len(snapshot_gcode.EndGcode) > 0: self.Settings.current_debug_profile().log_snapshot_gcode("Sending end gcode.") self.send_snapshot_gcode_array(snapshot_gcode.EndGcode) # we've completed the procedure, set success timelapse_snapshot_payload["success"] = True except Exception as e: self.Settings.current_debug_profile().log_exception(e) timelapse_snapshot_payload["error"] = "An unexpected error was encountered while running the timelapse " \ "snapshot procedure. " return timelapse_snapshot_payload # public functions def to_state_dict(self): try: position_dict = None position_state_dict = None extruder_dict = None trigger_state = None if self.Settings is not None: if self.Settings.show_position_changes and self.Position is not None: position_dict = self.Position.to_position_dict() if self.Settings.show_position_state_changes and self.Position is not None: position_state_dict = self.Position.to_state_dict() if self.Settings.show_extruder_state_changes and self.Position is not None: extruder_dict = self.Position.Extruder.to_dict() if self.Settings.show_trigger_state_changes and self.Triggers is not None: trigger_state = { "Name": self.Triggers.Name, "Triggers": self.Triggers.state_to_list() } state_dict = { "Extruder": extruder_dict, "Position": position_dict, "PositionState": position_state_dict, "TriggerState": trigger_state } return state_dict except Exception as e: self.Settings.CurrentDebugProfile().log_exception(e) # if we're here, we've reached and logged an error. return { "Extruder": None, "Position": None, "PositionState": None, "TriggerState": None } def stop_snapshots(self, message=None, error=False): self.State = TimelapseState.WaitingToRender if self.TimelapseStoppedCallback is not None: timelapse_stopped_callback_thread = threading.Thread( target=self.TimelapseStoppedCallback, args=[message, error] ) timelapse_stopped_callback_thread.daemon = True timelapse_stopped_callback_thread.start() return True def on_print_failed(self): if self.State != TimelapseState.Idle: self.end_timelapse("FAILED") def on_print_disconnecting(self): if self.State != TimelapseState.Idle: self.end_timelapse("DISCONNECTING") def on_print_disconnected(self): if self.State != TimelapseState.Idle: self.end_timelapse("DISCONNECTED") def on_print_canceled(self): if self.State != TimelapseState.Idle: self.end_timelapse("CANCELED") def on_print_completed(self): if self.State != TimelapseState.Idle: self.end_timelapse("COMPLETED") def end_timelapse(self, print_status): self.PrintEndStatus = print_status try: if self.PrintStartTime is None: self._reset() elif self.PrintStartTime is not None and self.State in [ TimelapseState.WaitingForTrigger, TimelapseState.WaitingToRender, TimelapseState.WaitingToEndTimelapse ]: if not self._render_timelapse(self.PrintEndStatus): if self.OnRenderEndCallback is not None: payload = RenderingCallbackArgs( "Could not start timelapse job.", -1, "unknown", "unknown", "unknown", "unknown", "unknown", "unknown", False, 0, 0, True, "timelapse_start", "The render_start function returned false" ) render_end_callback_thread = threading.Thread( target=self.OnRenderEndCallback, args=[payload] ) render_end_callback_thread.daemon = True render_end_callback_thread.start() self._reset() if self.State != TimelapseState.Idle: self.State = TimelapseState.WaitingToEndTimelapse except Exception as e: self.Settings.current_debug_profile().log_exception(e) if self.OnTimelapseEndCallback is not None: self.OnTimelapseEndCallback() def on_print_paused(self): try: if self.State == TimelapseState.Idle: return elif self.State < TimelapseState.WaitingToRender: self.Settings.current_debug_profile().log_print_state_change("Print Paused.") self.Triggers.pause() except Exception as e: self.Settings.current_debug_profile().log_exception(e) def on_print_resumed(self): try: if self.State == TimelapseState.Idle: return elif self.State < TimelapseState.WaitingToRender: self.Triggers.resume() except Exception as e: self.Settings.current_debug_profile().log_exception(e) def is_timelapse_active(self): if ( self.Settings is None or self.State in [TimelapseState.Idle, TimelapseState.Initializing, TimelapseState.WaitingToRender] or self.OctoprintPrinter.get_state_id() == "CANCELLING" or self.Triggers is None or self.Triggers.count() < 1 ): return False return True def get_is_rendering(self): return self._rendering_task_queue.qsize() > 0 def on_print_start(self, tags): self.OnPrintStartCallback(tags) def on_print_start_failed(self, message): self.OnPrintStartFailedCallback(message) def on_gcode_queuing(self, command_string, cmd_type, gcode, tags): self.detect_timelapse_start(command_string, tags) # if the timelapse is not active, exit without changing any gcode if not self.is_timelapse_active(): return self.check_current_line_number(tags) # update the position tracker so that we know where all of the axis are. # We will need this later when generating snapshot gcode so that we can return to the previous # position is_snapshot_gcode_command = self._is_snapshot_command(command_string) try: self.Settings.current_debug_profile().log_gcode_queuing( "Queuing Command: Command Type:{0}, gcode:{1}, cmd: {2}, tags: {3}".format( cmd_type, gcode, command_string, tags ) ) try: cmd, parameters = Commands.parse(command_string) except ValueError as e: message = "An error was thrown by the gcode parser, stopping timelapse. Details: {0}".format(str(e)) self.Settings.current_debug_profile().log_warning( message ) self.stop_snapshots(message, True) return None # get the position state in case it has changed # if there has been a position or extruder state change, inform any listener if cmd is not None and not is_snapshot_gcode_command: # create our state change dictionaries self.Position.update(command_string, cmd, parameters) # if this code is snapshot gcode, simply return it to the printer. if {'plugin:octolapse', 'snapshot_gcode'}.issubset(tags): return None if not self.check_for_non_metric_errors(): if self.Position.has_position_error(0): # There are position errors, report them! self._on_position_error() elif (self.State == TimelapseState.WaitingForTrigger and (self.Position.requires_location_detection(1)) and self.OctoprintPrinter.is_printing()): if 'source:script' in tags: # warn user self._send_plugin_message_async( "warning", "Octolapse could not acquire a position while sending" " OctoPrint scripts (settings=>GCODE Scripts)." " A fix is in the works, but a modification to OctoPrint itself" " is required. For now please move your start gcode into the" " actual gcode file.") else: self.State = TimelapseState.AcquiringLocation if self.OctoprintPrinter.set_job_on_hold(True): thread = threading.Thread(target=self.acquire_position, args=[command_string, cmd, parameters]) thread.daemon = True thread.start() return None, elif (self.State == TimelapseState.WaitingForTrigger and self.OctoprintPrinter.is_printing() and not self.Position.has_position_error(0)): # update the triggers with the current position self.Triggers.update(self.Position, command_string) # see if at least one trigger is triggering _first_triggering = self.get_first_triggering() if _first_triggering: if 'source:script' in tags: # warn user self._send_plugin_message_async( "warning", "Octolapse could not take a snapshot while sending" " OctoLapse scripts (settings=>GCODE Scripts)." " A fix is in the works, but a modification to OctoPrint itself" " is required. For now please move your start gcode into the" " actual gcode file." ) else: # We are triggering, take a snapshot self.State = TimelapseState.TakingSnapshot # pause any timer triggers that are enabled self.Triggers.pause() # get the job lock if self.OctoprintPrinter.set_job_on_hold(True): # take the snapshot on a new thread thread = threading.Thread( target=self.acquire_snapshot, args=[command_string, cmd, parameters, _first_triggering] ) thread.daemon = True thread.start() # suppress the current command, we'll send it later return None, elif self.State == TimelapseState.TakingSnapshot: # Don't do anything further to any commands unless we are # taking a timelapse , or if octolapse paused the print. # suppress any commands we don't, under any cirumstances, # to execute while we're taking a snapshot if cmd in self.Commands.SuppressedSnapshotGcodeCommands: command_string = None, # suppress the command if is_snapshot_gcode_command: # in all cases do not return the snapshot command to the printer. # It is NOT a real gcode and could cause errors. command_string = None, except Exception as e: self.Settings.current_debug_profile().log_exception(e) raise # notify any callbacks self._send_state_changed_message() # do any post processing for test mode if command_string != (None,): command_string = self._get_command_for_octoprint(command_string, cmd,parameters) return command_string def detect_timelapse_start(self, cmd, tags): # detect print start if ( self.Settings.is_octolapse_enabled and self.State == TimelapseState.Idle and {'trigger:comm.start_print', 'trigger:comm.reset_line_numbers'} <= tags and # ({'trigger:comm.start_print', 'fileline:1'} <= tags or {'script:beforePrintStarted', 'trigger:comm.send_gcode_script'} <= tags) and self.OctoprintPrinter.is_printing() ): if self.OctoprintPrinter.set_job_on_hold(True): try: self.State = TimelapseState.Initializing self.Settings.current_debug_profile().log_print_state_change( "Print Start Detected. Command: {0}, Tags:{1}".format(cmd, tags) ) # call the synchronous callback on_print_start self.on_print_start(tags) if self.State == TimelapseState.WaitingForTrigger: # set the current line to 0 so that the plugin checks for line 1 below after startup. self.CurrentFileLine = 0 finally: self.OctoprintPrinter.set_job_on_hold(False) else: self.on_print_start_failed( "Unable to start timelapse, failed to acquire a job lock. Print start failed." ) def check_current_line_number(self, tags): # check the current line number if {'source:file'} in tags: # this line is from the file, advance! self.CurrentFileLine += 1 if "fileline:{0}".format(self.CurrentFileLine) not in tags: actual_file_line = "unknown" for tag in tags: if len(tag) > 9 and tag.startswith("fileline:"): actual_file_line = tag[9:] message = "File line number {0} was expected, but {1} was received!".format( self.CurrentFileLine + 1, actual_file_line ) self.Settings.current_debug_profile().log_error(message) self.stop_snapshots(message, True) def check_for_non_metric_errors(self): # make sure we're not using inches is_metric = self.Position.is_metric() has_error = False error_message = "" if is_metric is None and self.Position.has_position_error(): has_error = True error_message = "The printer profile requires an explicit G21 command before any position " \ "altering/setting commands, including any home commands. Stopping timelapse, " \ "but continuing the print. " elif not is_metric and self.Position.has_position_error(): has_error = True if self.Printer.units_default == "inches": error_message = "The printer profile uses 'inches' as the default unit of measurement. In order to" \ " use Octolapse, a G21 command must come before any position altering/setting commands, including" \ " any home commands. Stopping timelapse, but continuing the print. " else: error_message = "The gcode file contains a G20 command (set units to inches), which Octolapse " \ "does not support. Stopping timelapse, but continuing the print." if has_error: self.stop_snapshots(error_message,has_error) return has_error def get_first_triggering(self): try: # make sure we're in a state that could want to check for triggers if not self.State == TimelapseState.WaitingForTrigger: return False # see if the PREVIOUS command triggered (that means current gcode gets sent if the trigger[0] # is triggering first_trigger = self.Triggers.get_first_triggering(0, Triggers.TRIGGER_TYPE_IN_PATH) if first_trigger: self.Settings.current_debug_profile().log_triggering("An in-path snapshot is triggering") return first_trigger first_trigger = self.Triggers.get_first_triggering(1, Triggers.TRIGGER_TYPE_DEFAULT) if first_trigger: # We're triggering self.Settings.current_debug_profile().log_triggering("A snapshot is triggering") return first_trigger except Exception as e: self.Settings.current_debug_profile().log_exception(e) # no need to re-raise here, the trigger just won't happen return False def acquire_position(self, command_string, cmd, parameters): try: self.Settings.current_debug_profile().log_print_state_change( "A position altering command has been detected. Fetching and updating position. " "Position Command: {0}".format(cmd)) # Undo the last position update, we will be resending the command self.Position.undo_update() current_position = self.get_position_async() if current_position is None: self.PrintEndStatus = "POSITION_TIMEOUT" self.State = TimelapseState.WaitingToEndTimelapse self.Settings.current_debug_profile().log_print_state_change( "Unable to acquire a position.") else: # update position self.Position.update_position( x=current_position["x"], y=current_position["y"], z=current_position["z"], e=current_position["e"], force=True, calculate_changes=True) # adjust the triggering command if self.IsTestMode: gcode = self.Commands.alter_for_test_mode(command_string, cmd, parameters, return_string=True) else: gcode = command_string if gcode != "": self.Settings.current_debug_profile().log_print_state_change( "Sending triggering command for position acquisition - {0}.".format(gcode)) # send the triggering command self.send_snapshot_gcode_array([gcode]) # set the state if self.State == TimelapseState.AcquiringLocation: self.State = TimelapseState.WaitingForTrigger self.Settings.current_debug_profile().log_print_state_change("Position Acquired") finally: self.OctoprintPrinter.set_job_on_hold(False) def acquire_snapshot(self, command_string, cmd, parameters, trigger): try: self.Settings.current_debug_profile().log_snapshot_download( "About to take a snapshot. Triggering Command: {0}".format(cmd)) if self.OnSnapshotStartCallback is not None: snapshot_callback_thread = threading.Thread(target=self.OnSnapshotStartCallback) snapshot_callback_thread.daemon = True snapshot_callback_thread.start() # Capture and undo the last position update, we're not going to be using it! triggering_command_position, triggering_extruder_position = self.Position.undo_update() # take the snapshot # Todo: We probably don't need the payload here. self._most_recent_snapshot_payload = self._take_timelapse_snapshot( trigger, command_string, cmd, parameters, triggering_command_position, triggering_extruder_position ) self.Settings.current_debug_profile().log_snapshot_download("The snapshot has completed") finally: # set the state if self.State == TimelapseState.TakingSnapshot: self.State = TimelapseState.WaitingForTrigger self.Triggers.resume() self.OctoprintPrinter.set_job_on_hold(False) # notify that we're finished, but only if we haven't just stopped the timelapse. if self._most_recent_snapshot_payload is not None: # send a copy of the dict in case it gets changed by threads. self._on_trigger_snapshot_complete(self._most_recent_snapshot_payload.copy()) def on_gcode_sent(self, cmd, cmd_type, gcode, tags): self.Settings.current_debug_profile().log_gcode_sent( "Sent to printer: Command Type:{0}, gcode:{1}, cmd: {2}, tags: {3}".format(cmd_type, gcode, cmd, tags)) def on_gcode_received(self, comm, line, *args, **kwargs): self.Settings.current_debug_profile().log_gcode_received( "Received from printer: line:{0}".format(line) ) return line # internal functions #################### def _get_command_for_octoprint(self, command_string, cmd, parameters): if command_string is None or command_string == (None,): return command_string if self.IsTestMode and self.State >= TimelapseState.WaitingForTrigger: return self.Commands.alter_for_test_mode(command_string, cmd, parameters) # if we were given a list, return it. if isinstance(command_string, list): return command_string # if we were given a command return None (don't change the command at all) return None def _send_state_changed_message(self): """Notifies any callbacks about any changes contained in the dictionaries. If you send a dict here the client will get a message, so check the settings to see if they are subscribed to notifications before populating the dictinaries!""" delay_seconds = 0 # if another thread is trying to send the message, stop it if self.StateChangeMessageThread is not None and self.StateChangeMessageThread.isAlive(): self.StateChangeMessageThread.cancel() if self.LastStateChangeMessageTime is not None: # do not send more than 1 per second time_since_last_update = time.time() - self.LastStateChangeMessageTime if time_since_last_update < 1: delay_seconds = 1-time_since_last_update if delay_seconds < 0: delay_seconds = 0 try: # Notify any callbacks if self.OnStateChangedCallback is not None: def send_change_message(): trigger_change_list = None position_change_dict = None position_state_change_dict = None extruder_change_dict = None trigger_changes_dict = None # Get the changes if self.Settings.show_trigger_state_changes: trigger_change_list = self.Triggers.state_to_list() if self.Settings.show_position_changes: position_change_dict = self.Position.to_position_dict() if self.Settings.show_position_state_changes: position_state_change_dict = self.Position.to_state_dict() if self.Settings.show_extruder_state_changes: extruder_change_dict = self.Position.Extruder.to_dict() # if there are any state changes, send them if ( position_change_dict is not None or position_state_change_dict is not None or extruder_change_dict is not None or trigger_change_list is not None ): if trigger_change_list is not None and len(trigger_change_list) > 0: trigger_changes_dict = { "Name": self.Triggers.Name, "Triggers": trigger_change_list } change_dict = { "Extruder": extruder_change_dict, "Position": position_change_dict, "PositionState": position_state_change_dict, "TriggerState": trigger_changes_dict } if ( change_dict["Extruder"] is not None or change_dict["Position"] is not None or change_dict["PositionState"] is not None or change_dict["TriggerState"] is not None ): self.OnStateChangedCallback(change_dict) self.LastStateChangeMessageTime = time.time() # Send a delayed message self.StateChangeMessageThread = threading.Timer( delay_seconds, send_change_message ) self.StateChangeMessageThread.daemon = True self.StateChangeMessageThread.start() except Exception as e: # no need to re-raise, callbacks won't be notified, however. self.Settings.current_debug_profile().log_exception(e) def _send_plugin_message(self, message_type, message): self.OnPluginMessageSentCallback(message_type, message) def _send_plugin_message_async(self, message_type, message): warning_thread = threading.Thread(target=self._send_plugin_message, args=[message_type, message]) warning_thread.daemon = True warning_thread.start() def _is_snapshot_command(self, command_string): return command_string == self.Printer.snapshot_command def _is_trigger_waiting(self): # make sure we're in a state that could want to check for triggers if not self.State == TimelapseState.WaitingForTrigger: return None # Loop through all of the active currentTriggers waiting_trigger = self.Triggers.get_first_waiting() if waiting_trigger is not None: return True return False def _on_position_error(self): message = self.Position.position_error(0) self.Settings.current_debug_profile().log_error(message) if self.OnPositionErrorCallback is not None: position_error_callback_thread = threading.Thread( target=self.OnPositionErrorCallback, args=[message] ) position_error_callback_thread.daemon = True position_error_callback_thread.start() def _on_trigger_snapshot_complete(self, snapshot_payload): if self.OnSnapshotCompleteCallback is not None: payload = { "success": snapshot_payload["success"], "error": snapshot_payload["error"], "snapshot_count": self.SnapshotCount, "total_snapshot_time": snapshot_payload["total_snapshot_time"], "current_snapshot_time": snapshot_payload["total_snapshot_time"] } if self.OnSnapshotCompleteCallback is not None: snapshot_complete_callback_thread = threading.Thread( target=self.OnSnapshotCompleteCallback, args=[payload] ) snapshot_complete_callback_thread.daemon = True snapshot_complete_callback_thread.start() def _render_timelapse(self, print_end_state): def _render_timelapse_async(render_job_id, timelapse_render_job): try: snapshot_thread = threading.Thread(target=timelapse_render_job, args=[]) snapshot_thread.daemon = True num_snapshot_tasks = self._snapshot_task_queue.qsize() if num_snapshot_tasks > 0: self.Settings.current_debug_profile().log_render_start("Started Rendering Timelapse.") else: self.Settings.current_debug_profile().log_render_start( "Waiting for {0} snapshot threads to complete".format( self._snapshot_task_queue.qsize())) self._snapshot_task_queue.join() self.Settings.current_debug_profile().log_render_start( "All snapshot tasks have completed, rendering timelapse" ) # we are rendering, set the state before starting the rendering job. self._rendering_task_queue.put(render_job_id) snapshot_thread.start() except Exception as e: self.Settings.current_debug_profile().log_exception(e) self._rendering_task_queue.get() self._rendering_task_queue.task_done() # make sure we have a non null TimelapseSettings object. We may have terminated the timelapse for some reason if self.Rendering is not None and self.Rendering.enabled: job_id = "TimelapseRenderJob_{0}".format(str(uuid.uuid4())) job = Render.create_render_job( self.Settings, self.Snapshot, self.Rendering, self.DataFolder, self.DefaultTimelapseDirectory, self.FfMpegPath, 1, self._rendering_task_queue, job_id, utility.get_currently_printing_filename(self.OctoprintPrinter), self.PrintStartTime, time.time(), print_end_state, self.SecondsAddedByOctolapse, self._on_render_start, self._on_render_end ) rendering_thread = threading.Thread(target=_render_timelapse_async, args=[job_id, job]) rendering_thread.daemon = True rendering_thread.start() return True return False def _on_render_start(self, *args, **kwargs): job_id = args[0] self.Settings.current_debug_profile().log_render_start( "Started rendering/synchronizing the timelapse. JobId: {0}".format(job_id)) payload = args[1] # notify the caller if self.OnRenderStartCallback is not None: render_start_complete_callback_thread = threading.Thread( target=self.OnRenderStartCallback, args=[payload] ) render_start_complete_callback_thread.daemon = True render_start_complete_callback_thread.start() def _on_render_end(self, *args, **kwargs): job_id = args[0] payload = args[1] self.Settings.current_debug_profile().log_render_complete("Completed rendering. JobId: {0}".format(job_id)) assert (isinstance(payload, RenderingCallbackArgs)) if not payload.HasError and self.Snapshot.cleanup_after_render_fail: self.CaptureSnapshot.clean_snapshots(utility.get_snapshot_temp_directory(self.DataFolder)) elif self.Snapshot.cleanup_after_render_complete: self.CaptureSnapshot.clean_snapshots(utility.get_snapshot_temp_directory(self.DataFolder)) if self.OnRenderEndCallback is not None: render_end_complete_callback_thread = threading.Thread( target=self.OnRenderEndCallback, args=[payload] ) render_end_complete_callback_thread.daemon = True render_end_complete_callback_thread.start() def _on_timelapse_start(self): if self.OnTimelapseStartCallback is None: return self.OnTimelapseStartCallback() def _reset(self): self.State = TimelapseState.Idle self.CurrentFileLine = 0 if self.Triggers is not None: self.Triggers.reset() self.CommandIndex = -1 self.LastStateChangeMessageTime = None self.PrintStartTime = None self.SnapshotGcodes = None self.SavedCommand = None self.PositionRequestAttempts = 0 self.IsTestMode = False self.ReturnPositionReceivedTime = None # A list of callbacks who want to be informed when a timelapse ends self.TimelapseStopRequested = False self._snapshot_success = False self.SnapshotError = "" self.HasBeenStopped = False self.CurrentProfiles = { "printer": "", "stabilization": "", "snapshot": "", "rendering": "", "camera": "", "debug_profile": "" } # fetch position private variables self._position_payload = None self._position_signal.set() # get snapshot async private variables self._snapshot_signal.set() def _reset_snapshot(self): self.State = TimelapseState.WaitingForTrigger self.CommandIndex = -1 self.SnapshotGcodes = None self.SavedCommand = None self.PositionRequestAttempts = 0 self._snapshot_success = False self.SnapshotError = ""