def test_LayerTrigger_ExtruderTriggerWait(self): """Test wait on extruder""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # home the axis and send another command to make sure the previous instruction was homed position.Update("G28") position.Update("PreviousHomed") trigger = LayerTrigger(self.Settings) trigger.RequireZHop = False # no zhop required trigger.HeightIncrement = 0 # Trigger on every layer change #Reset the extruder position.Extruder.Reset() position.Extruder.IsPrimed = False trigger.IsWaiting = False # Use on extruding start for this test. trigger.ExtruderTriggers = ExtruderTriggers(True,None,None,None,None,None,None,None,None,None) position.Extruder.IsExtrudingStart = False position.IsLayerChange = True trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == True) # update again with no change trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == True) # set the trigger and try again position.Extruder.IsExtrudingStart = True trigger.Update(position) self.assertTrue(trigger.IsTriggered == True) self.assertTrue(trigger.IsWaiting == False)
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 test_LayerTrigger_ExtruderTriggerWait(self): """Test wait on extruder""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) trigger = LayerTrigger(self.Settings) trigger.RequireZHop = False # no zhop required trigger.HeightIncrement = 0 # Trigger on every layer change # home the axis position.update("G28") # add the current state pos = position.get_position(0) state = position.Extruder.get_state(0) state.IsPrimed = False # Use on extruding start for this test. trigger.ExtruderTriggers = ExtruderTriggers(True, None, None, None, None, None, None, None, None, None) state.IsExtrudingStart = False pos.IsLayerChange = True trigger.update(position) self.assertFalse(trigger.is_triggered(0)) self.assertTrue(trigger.is_waiting(0)) # update again with no change trigger.update(position) self.assertFalse(trigger.is_triggered(0)) self.assertTrue(trigger.is_waiting(0)) # set the trigger and try again state.IsExtrudingStart = True trigger.update(position) self.assertTrue(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0))
def StartTimelapse(self, octoprintPrinter, octoprintPrinterProfile, ffmpegPath, g90InfluencesExtruder): self.Reset() self.OctoprintPrinter = octoprintPrinter self.OctoprintPrinterProfile = octoprintPrinterProfile self.FfMpegPath = ffmpegPath self.PrintStartTime = time.time() self.Snapshot = Snapshot(self.Settings.CurrentSnapshot()) self.Gcode = SnapshotGcodeGenerator(self.Settings, octoprintPrinterProfile) self.Printer = Printer(self.Settings.CurrentPrinter()) self.Rendering = Rendering(self.Settings.CurrentRendering()) self.CaptureSnapshot = CaptureSnapshot( self.Settings, self.DataFolder, printStartTime=self.PrintStartTime) self.Position = Position(self.Settings, octoprintPrinterProfile, g90InfluencesExtruder) self.State = TimelapseState.WaitingForTrigger self.IsTestMode = self.Settings.CurrentDebugProfile().is_test_mode # create the triggers # If the gcode trigger is enabled, add it if (self.Snapshot.gcode_trigger_enabled): #Add the trigger to the list self.Triggers.append(GcodeTrigger(self.Settings)) # If the layer trigger is enabled, add it if (self.Snapshot.layer_trigger_enabled): self.Triggers.append(LayerTrigger(self.Settings)) # If the layer trigger is enabled, add it if (self.Snapshot.timer_trigger_enabled): self.Triggers.append(TimerTrigger(self.Settings))
def setUp(self): self.Settings = OctolapseSettings(NamedTemporaryFile().name) self.OctoprintPrinterProfile = self.create_octoprint_printer_profile() printer = get_printer_profile() self.Settings.printers.update({printer["guid"]: Printer(printer=printer)}) self.Settings.profiles.current_printer_profile_guid = printer["guid"] self.Extruder = Extruder(self.Settings) self.Position = Position(self.Settings, self.OctoprintPrinterProfile, False)
def test_G92AbsoluteMovement(self): """Test the G92 command, move in absolute mode and test results.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # set homed axis, absolute coordinates, and set position position.Update("G28") position.Update("G90") position.Update("G1 x100 y200 z150") position.Update("G92 x10 y20 z30") self.assertTrue(position.X == 100) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 200) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 150) self.assertTrue(position.ZOffset == 120) # move to origin position.Update("G1 x-90 y-180 z-120") self.assertTrue(position.X == 0) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 0) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 0) self.assertTrue(position.ZOffset == 120) # move back position.Update("G1 x0 y0 z0") self.assertTrue(position.X == 90) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 180) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 120) self.assertTrue(position.ZOffset == 120)
def test_G92SetPosition(self): """Test the G92 command, settings the position.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # no homed axis position.Update("G92 x10 y20 z30") self.assertTrue(position.X is None) self.assertTrue(position.Y is None) self.assertTrue(position.Z is None) # set homed axis, absolute coordinates, and set position position.Update("G28") position.Update("G90") position.Update("G1 x100 y200 z150") position.Update("G92 x10 y20 z30") self.assertTrue(position.X == 100) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 200) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 150) self.assertTrue(position.ZOffset == 120) # Move to same position and retest position.Update("G1 x0 y0 z0") self.assertTrue(position.X == 90) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 180) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 120) self.assertTrue(position.ZOffset == 120) # Move and retest position.Update("G1 x-10 y10 z20") self.assertTrue(position.X == 80) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 190) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 140) self.assertTrue(position.ZOffset == 120) # G92 with no parameters position.Update("G92") self.assertTrue(position.X == 80) self.assertTrue(position.XOffset == 80) self.assertTrue(position.Y == 190) self.assertTrue(position.YOffset == 190) self.assertTrue(position.Z == 140) self.assertTrue(position.ZOffset == 140)
def test_reset(self): """Test init state.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # reset all initialized vars to something else position.update("G28") position.update("G0 X1 Y1 Z1") # reset position.reset() # test initial state self.assertEqual(len(position.Positions), 0) self.assertIsNone(position.SavedPosition)
def test_G90InfluencesExtruder_UpdatePosition(self): """Test G90 for machines where it influences the coordinate system of the extruder.""" position = Position(self.Settings, self.OctoprintPrinterProfile, True) # Make sure the axis is homed position.XHomed = True position.YHomed = True position.ZHomed = True # set absolute mode with G90 position.Update("g90;") # update the position to 10 (absolute) position.UpdatePosition(e=10) self.assertTrue(position.E == 10) # update the position to 10 again (absolute) to make sure we are in absolute coordinates. position.UpdatePosition(e=10) self.assertTrue(position.E == 10) # set relative mode with G90 position.Update("g91;") # update the position to 20 (relative) position.UpdatePosition(e=20) self.assertTrue(position.E == 30)
def test_IsAtCurrentPosition(self): #Received: x:119.91,y:113.34,z:2.1,e:0.0, Expected: x:119.9145519,y:113.33847,z:2.1 #G1 X119.915 Y113.338 F7200 position = Position(self.Settings, self.OctoprintPrinterProfile, False) position.Printer.printer_position_confirmation_tolerance = .0051 position.Update("g28") position.Update("G1 X119.915 Y113.338 Z2.1 F7200") self.assertTrue(position.IsAtCurrentPosition(119.91, 113.34, 2.1)) position.Update("g0 x120 y121 z2.1") self.assertTrue(position.IsAtPreviousPosition(119.91, 113.34, 2.1))
def test_UpdatePosition_force(self): """Test the UpdatePosition function with the force option set to true.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) position.UpdatePosition(x=0, y=0, z=0, e=0, force=True) self.assertTrue(position.X == 0) self.assertTrue(position.Y == 0) self.assertTrue(position.Z == 0) self.assertTrue(position.E == 0) position.UpdatePosition(x=1, y=2, z=3, e=4, force=True) self.assertTrue(position.X == 1) self.assertTrue(position.Y == 2) self.assertTrue(position.Z == 3) self.assertTrue(position.E == 4) position.UpdatePosition(x=None, y=None, z=None, e=None, force=True) self.assertTrue(position.X == 1) self.assertTrue(position.Y == 2) self.assertTrue(position.Z == 3) self.assertTrue(position.E == 4)
def test_TimerTrigger_ExtruderTriggerWait(self): """Test wait on extruder""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # home the axis position.Update("G28") trigger = TimerTrigger(self.Settings) trigger.RequireZHop = False # no zhop required trigger.IntervalSeconds = 1 #Reset the extruder position.Extruder.Reset() position.Extruder.IsPrimed = False trigger.IsWaiting = False # Use on extruding start for this test. trigger.ExtruderTriggers = ExtruderTriggers(True, None, None, None, None, None, None, None, None, None) position.Extruder.IsExtrudingStart = False trigger.TriggerStartTime = time.time() - 1.01 # will not wait or trigger because the previous position state was not homed trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # send another command and try again position.Update("PreviousPositionIsNowHomed") trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == True) # update again with no change trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == True) # set the trigger and try again position.Extruder.IsExtrudingStart = True trigger.Update(position) self.assertTrue(trigger.IsTriggered == True) self.assertTrue(trigger.IsWaiting == False)
def test_TimerTrigger_ExtruderTriggerWait(self): """Test wait on extruder""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # home the axis position.update("G28") trigger = TimerTrigger(self.Settings) trigger.RequireZHop = False # no zhop required trigger.IntervalSeconds = 1 # Use on extruding start for this test. trigger.ExtruderTriggers = ExtruderTriggers( True, None, None, None, None, None, None, None, None, None) # set the extruder trigger position.Extruder.get_state(0).IsExtrudingStart = True # will not wait or trigger because not enough time has elapsed trigger.update(position) self.assertFalse(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # add 1 second to the state and try again trigger.get_state(0).TriggerStartTime = time.time() - 1.01 # send another command and try again position.update("PreviousPositionIsNowHomed") # set the extruder trigger position.Extruder.get_state(0).IsExtrudingStart = True trigger.update(position) self.assertTrue(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0))
def StartTimelapse(self, octoprintPrinter, octoprintPrinterProfile, ffmpegPath, g90InfluencesExtruder): self._reset() self.HasSentInitialStatus = False self.OctoprintPrinter = octoprintPrinter self.OctoprintPrinterProfile = octoprintPrinterProfile self.FfMpegPath = ffmpegPath self.PrintStartTime = time.time() self.Snapshot = Snapshot(self.Settings.CurrentSnapshot()) self.Gcode = SnapshotGcodeGenerator(self.Settings, octoprintPrinterProfile) self.Printer = Printer(self.Settings.CurrentPrinter()) self.Rendering = Rendering(self.Settings.CurrentRendering()) self.CaptureSnapshot = CaptureSnapshot( self.Settings, self.DataFolder, printStartTime=self.PrintStartTime) self.Position = Position(self.Settings, octoprintPrinterProfile, g90InfluencesExtruder) self.State = TimelapseState.WaitingForTrigger self.IsTestMode = self.Settings.CurrentDebugProfile().is_test_mode self.Triggers.Create() # send an initial state message self._onTimelapseStart()
def test_TimerTrigger(self): """Test the timer trigger""" # use a short trigger time so that the test doesn't take too long self.Settings.CurrentSnapshot().timer_trigger_seconds = 2 position = Position(self.Settings, self.OctoprintPrinterProfile, False) trigger = TimerTrigger(self.Settings) trigger.ExtruderTriggers = ExtruderTriggers(None, None, None, None, None, None, None, None, None, None) #Ignore extruder trigger.RequireZHop = False # no zhop required trigger.HeightIncrement = 0 # Trigger on any height change # test initial state self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # set interval time to 0, send another command and test again (should not trigger, no homed axis) trigger.IntervalSeconds = 0 position.Update("g0 x0 y0 z.2 e1") trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # Home all axis and try again with interval seconds 1 - should not trigger since the timer will start after the home command trigger.IntervalSeconds = 2 position.Update("g28") trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # send another command and try again, should not trigger cause we haven't waited 2 seconds yet position.Update("g0 x0 y0 z.2 e1") trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # Set the last trigger time to 1 before the previous LastTrigger time(equal to interval seconds), should not trigger trigger.TriggerStartTime = time.time() - 1.01 position.Update("g0 x0 y0 z.2 e1") trigger.Update(position) self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # Set the last trigger time to 1 before the previous LastTrigger time(equal to interval seconds), should trigger trigger.TriggerStartTime = time.time() - 2.01 position.Update("g0 x0 y0 z.2 e1") trigger.Update(position) self.assertTrue(trigger.IsTriggered == True) self.assertTrue(trigger.IsWaiting == False)
def TestReset(self): """Test the reset function""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) trigger = LayerTrigger(self.Settings) # test initial state self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # set the flags to different valuse trigger.IsTriggered = True trigger.IsWaiting = True self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False) # test the reset state trigger.Reset() self.assertTrue(trigger.IsTriggered == False) self.assertTrue(trigger.IsWaiting == False)
def test_G92RelativeMovement(self): """Test the G92 command, move in relative mode and test results.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # set homed axis, relative coordinates, and set position position.Update("G28") position.Update("G91") position.Update("G1 x100 y200 z150") position.Update("G92 x10 y20 z30") self.assertTrue(position.X is None) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 200) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 150) self.assertTrue(position.ZOffset == 120) # move to origin position.Update("G1 x-100 y-200 z-150") self.assertTrue(position.X == 0) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 0) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 0) self.assertTrue(position.ZOffset == 120) # advance each axis position.Update("G1 x1 y2 z3") self.assertTrue(position.X == 1) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 2) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 3) self.assertTrue(position.ZOffset == 120) # advance again position.Update("G1 x1 y2 z3") self.assertTrue(position.X == 2) self.assertTrue(position.XOffset == 90) self.assertTrue(position.Y == 4) self.assertTrue(position.YOffset == 180) self.assertTrue(position.Z == 6) self.assertTrue(position.ZOffset == 120)
def test_PositionError(self): """Test the IsInBounds function to make sure the program will not attempt to operate after being told to move out of bounds.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # Initial test, should return false without any coordinates self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # home the axis and test position.Update("G28") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) #X axis tests # reset, home the axis and test again position.Reset() position.Update("G28") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move out of bounds min position.Update("G0 x-0.0001") self.assertTrue(position.HasPositionError) self.assertTrue(position.PositionError is not None) # move back in bounds position.Update("G0 x0.0") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move to middle position.Update("G0 x125") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move to max position.Update("G0 x250") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move out of bounds max position.Update("G0 x250.0001") self.assertTrue(position.HasPositionError) self.assertTrue(position.PositionError is not None) #Y axis tests # reset, home the axis and test again position.Reset() position.Update("G28") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move out of bounds min position.Update("G0 y-0.0001") self.assertTrue(position.HasPositionError) self.assertTrue(position.PositionError is not None) # move back in bounds position.Update("G0 y0.0") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move to middle position.Update("G0 y100") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move to max position.Update("G0 y200") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move out of bounds max position.Update("G0 y200.0001") self.assertTrue(position.HasPositionError) self.assertTrue(position.PositionError is not None) #Z axis tests # reset, home the axis and test again position.Reset() position.Update("G28") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move out of bounds min position.Update("G0 z-0.0001") self.assertTrue(position.HasPositionError) self.assertTrue(position.PositionError is not None) # move back in bounds position.Update("G0 z0.0") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move to middle position.Update("G0 z100") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move to max position.Update("G0 z200") self.assertTrue(not position.HasPositionError) self.assertTrue(position.PositionError is None) # move out of bounds max position.Update("G0 z200.0001") self.assertTrue(position.HasPositionError) self.assertTrue(position.PositionError is not None)
class Timelapse(object): def __init__(self, octolapseSettings, dataFolder, timelapseFolder, onSnapshotStart=None, onSnapshotEnd=None, onRenderStart=None, onRenderComplete=None, onRenderFail=None, onRenderSynchronizeFail=None, onRenderSynchronizeComplete=None, onRenderEnd=None, onTimelapseStopping=None, onTimelapseStopped=None, onStateChanged=None, onTimelapseStart=None, onSnapshotPositionError=None, onPositionError=None): # config variables - These don't change even after a reset self.Settings = octolapseSettings self.DataFolder = dataFolder self.DefaultTimelapseDirectory = timelapseFolder self.OnRenderStartCallback = onRenderStart self.OnRenderCompleteCallback = onRenderComplete self.OnRenderFailCallback = onRenderFail self.OnRenderingSynchronizeFailCallback = onRenderSynchronizeFail self.OnRenderingSynchronizeCompleteCallback = onRenderSynchronizeComplete self.OnRenderEndCallback = onRenderEnd self.OnSnapshotStartCallback = onSnapshotStart self.OnSnapshotCompleteCallback = onSnapshotEnd self.TimelapseStoppingCallback = onTimelapseStopping self.TimelapseStoppedCallback = onTimelapseStopped self.OnStateChangedCallback = onStateChanged self.OnTimelapseStartCallback = onTimelapseStart self.OnSnapshotPositionErrorCallback = onSnapshotPositionError self.OnPositionErrorCallback = onPositionError self.Responses = Responses( ) # Used to decode responses from the 3d printer self.Commands = Commands() # used to parse and generate gcode self.Triggers = Triggers(octolapseSettings) # Settings that may be different after StartTimelapse is called self.FfMpegPath = None self.Snapshot = None self.Gcode = None self.Printer = None self.CaptureSnapshot = None self.Position = None self.HasSentInitialStatus = False # State Tracking that should only be reset when starting a timelapse self.IsRendering = False self.HasBeenCancelled = False self.HasBeenStopped = False # State tracking variables self._reset() # public functions def StartTimelapse(self, octoprintPrinter, octoprintPrinterProfile, ffmpegPath, g90InfluencesExtruder): self._reset() self.HasSentInitialStatus = False self.OctoprintPrinter = octoprintPrinter self.OctoprintPrinterProfile = octoprintPrinterProfile self.FfMpegPath = ffmpegPath self.PrintStartTime = time.time() self.Snapshot = Snapshot(self.Settings.CurrentSnapshot()) self.Gcode = SnapshotGcodeGenerator(self.Settings, octoprintPrinterProfile) self.Printer = Printer(self.Settings.CurrentPrinter()) self.Rendering = Rendering(self.Settings.CurrentRendering()) self.CaptureSnapshot = CaptureSnapshot( self.Settings, self.DataFolder, printStartTime=self.PrintStartTime) self.Position = Position(self.Settings, octoprintPrinterProfile, g90InfluencesExtruder) self.State = TimelapseState.WaitingForTrigger self.IsTestMode = self.Settings.CurrentDebugProfile().is_test_mode self.Triggers.Create() # send an initial state message self._onTimelapseStart() def GetStateDict(self): try: positionDict = None positionStateDict = None extruderDict = None triggerState = None if (self.Settings.show_position_changes and self.Position is not None): positionDict = self.Position.ToPositionDict() if (self.Settings.show_position_state_changes and self.Position is not None): positionStateDict = self.Position.ToStateDict() if (self.Settings.show_extruder_state_changes and self.Position is not None): extruderDict = self.Position.Extruder.ToDict() if (self.Settings.show_trigger_state_changes): triggerState = { "Name": self.Triggers.Name, "Triggers": self.Triggers.StateToList() } stateDict = { "Extruder": extruderDict, "Position": positionDict, "PositionState": positionStateDict, "TriggerState": triggerState } return stateDict except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # if we're here, we've reached and logged an error. return { "Extruder": None, "Position": None, "PositionState": None, "TriggerState": None } def StopSnapshots(self): """Stops octolapse from taking any further snapshots. Any existing snapshots will render after the print is ends.""" # we don't need to end the timelapse if it hasn't started if (self.State == TimelapseState.WaitingForTrigger or self.TimelapseStopRequested): self.State = TimelapseState.WaitingToRender self.TimelapseStopRequested = False if (self.TimelapseStoppedCallback is not None): self.TimelapseStoppedCallback() return True # if we are here, we're delaying the request until after the snapshot self.TimelapseStopRequested = True if (self.TimelapseStoppingCallback is not None): self.TimelapseStoppingCallback() def EndTimelapse(self, cancelled=False, force=False): try: if (not self.State == TimelapseState.Idle): if (not force): if (self.State > TimelapseState.WaitingForTrigger and self.State < TimelapseState.WaitingToRender): if (cancelled): self.HasBeenCancelled = True else: self.HasBeenStopped = True return self._renderTimelapse() self._reset() except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) def PrintPaused(self): try: if (self.State == TimelapseState.Idle): return elif (self.State < TimelapseState.WaitingToRender): self.Settings.CurrentDebugProfile().LogPrintStateChange( "Print Paused.") self.Triggers.Pause() except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) def PrintResumed(self): try: if (self.State == TimelapseState.Idle): return elif (self.State < TimelapseState.WaitingToRender): self.Triggers.Resume() except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) def IsTimelapseActive(self): try: if (self.State == TimelapseState.Idle or self.State == TimelapseState.WaitingToRender or self.Triggers.Count() < 1): return False return True except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) return False def GcodeQueuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): try: self.Settings.CurrentDebugProfile().LogQueuingGcode( "Queuing Command: Command Type:{0}, gcode:{1}, cmd: {2}". format(cmd_type, gcode, cmd)) # 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 positionChangeDict = None positionStateChangeDict = None extruderChangeDict = None triggerChangeList = None self.Position.Update(cmd) if (self.Position.HasPositionError(0)): self._onPositionError() # capture any changes, if neccessary, 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.HasPositionChanged() or not self.HasSentInitialStatus)): positionChangeDict = self.Position.ToPositionDict() if (self.Settings.show_position_state_changes and (self.Position.HasStateChanged() or not self.HasSentInitialStatus)): positionStateChangeDict = self.Position.ToStateDict() if (self.Settings.show_extruder_state_changes and (self.Position.Extruder.HasChanged() or not self.HasSentInitialStatus)): extruderChangeDict = self.Position.Extruder.ToDict() # get the position state in case it has changed # if there has been a position or extruder state change, inform any listener isSnapshotGcodeCommand = self._isSnapshotCommand(cmd) # check to see if we've just completed a home command if (self.State == TimelapseState.WaitingForTrigger and self.Position.HasReceivedHomeCommand(1) and self.OctoprintPrinter.is_printing()): if (self.Printer.auto_detect_origin): self.State = TimelapseState.AcquiringHomeLocation if (self.IsTestMode): cmd = self.Commands.GetTestModeCommandString(cmd) self.SavedCommand = cmd cmd = None, self._pausePrint() elif (self.State == TimelapseState.WaitingForTrigger and self.OctoprintPrinter.is_printing() and not self.Position.HasPositionError(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.HasChanged()): triggerChangeList = self.Triggers.ChangesToList() if (self.GcodeQueuing_IsTriggering(cmd, isSnapshotGcodeCommand)): # Undo the last position update, we're not going to be using it! self.Position.UndoUpdate() # Store the current position (our previous position), since this will be our snapshot position self.Position.SavePosition() # we don't want to execute the current command. We have saved it for later. # but we don't want to send the snapshot command to the printer, or any of the SupporessedSavedCommands (gcode.py) if (isSnapshotGcodeCommand or cmd in self.Commands.SuppressedSavedCommands): self.SavedCommand = None # this will suppress the command since it won't be added to our snapshot commands list else: if (self.IsTestMode): cmd = self.Commands.GetTestModeCommandString(cmd) self.SavedCommand = cmd # this will cause the command to be added to the end of our snapshot commands # pause the printer to start the snapshot self.State = TimelapseState.RequestingReturnPosition # Pausing the print here will immediately trigger an M400 and a location request self._pausePrint() # send M400 and position request # send a notification to the client that the snapshot is starting if (self.OnSnapshotStartCallback is not None): self.OnSnapshotStartCallback() # suppress the command cmd = None, elif ((self.State > TimelapseState.WaitingForTrigger and self.State < TimelapseState.SendingReturnGcode) or (self.State in [ TimelapseState.AcquiringHomeLocation, TimelapseState.SendingSavedHomeLocationCommand ])): # 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 (isSnapshotGcodeCommand): # 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._onStateChanged(positionChangeDict, positionStateChangeDict, extruderChangeDict, triggerChangeList) self.HasSentInitialStatus = True if (cmd != None, ): return self._returnGcodeCommandToOctoprint(cmd) # if we are here we need to suppress the command except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) raise return cmd def GcodeQueuing_IsTriggering(self, cmd, isSnapshotGcodeCommand): try: # make sure we're in a state that could want to check for triggers if (not self.State == TimelapseState.WaitingForTrigger): return None currentTrigger = self.Triggers.GetFirstTriggering(0) if (currentTrigger is not None): #We're triggering self.Settings.CurrentDebugProfile().LogTriggering( "A snapshot is triggering") # notify any callbacks return True elif (self._isTriggerWaiting(cmd)): self.Settings.CurrentDebugProfile().LogTriggerWaitState( "Trigger is Waiting On Extruder.") except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # no need to re-raise here, the trigger just won't happen return False def GcodeSent(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): self.Settings.CurrentDebugProfile().LogSentGcode( "Sent to printer: Command Type:{0}, gcode:{1}, cmd: {2}".format( cmd_type, gcode, cmd)) def PositionReceived(self, payload): # if we cancelled the print, we don't want to do anything. if (self.HasBeenCancelled): self.EndTimelapse(force=True) return x = payload["x"] y = payload["y"] z = payload["z"] e = payload["e"] if (self.State == TimelapseState.AcquiringHomeLocation): self.Settings.CurrentDebugProfile().LogPrintStateChange( "Snapshot home position received by Octolapse.") self._positionReceived_Home(x, y, z, e) elif (self.State == TimelapseState.SendingSavedHomeLocationCommand): self.Settings.CurrentDebugProfile().LogPrintStateChange( "Snapshot home saved command position received by Octolapse.") self._positionReceived_HomeLocationSavedCommand(x, y, z, e) elif (self.State == TimelapseState.RequestingReturnPosition): self.Settings.CurrentDebugProfile().LogPrintStateChange( "Snapshot return position received by Octolapse.") self._positionReceived_Return(x, y, z, e) elif (self.State == TimelapseState.SendingSnapshotGcode): self.Settings.CurrentDebugProfile().LogPrintStateChange( "Snapshot position received by Octolapse.") self._positionReceived_Snapshot(x, y, z, e) elif (self.State == TimelapseState.SendingReturnGcode): self._positionReceived_ResumePrint(x, y, z, e) else: self.Settings.CurrentDebugProfile().LogPrintStateChange( "Position received by Octolapse while paused, but was declined." ) return False, "Declined - Incorrect State" # internal functions #################### def _returnGcodeCommandToOctoprint(self, cmd): if (cmd is None or cmd == (None, )): return cmd if (self.IsTestMode and self.State >= TimelapseState.WaitingForTrigger): return self.Commands.AlterCommandForTestMode(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 _onStateChanged(self, positionChangeDict, positionStateChangeDict, extruderChangeDict, triggerChangeList): """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!""" triggerChangesDict = None try: # Notify any callbacks if (self.OnStateChangedCallback is not None and (positionChangeDict is not None or positionStateChangeDict is not None or extruderChangeDict is not None or triggerChangeList is not None)): if (triggerChangeList is not None and len(triggerChangeList) > 0): triggerChangesDict = { "Name": self.Triggers.Name, "Triggers": triggerChangeList } changeDict = { "Extruder": extruderChangeDict, "Position": positionChangeDict, "PositionState": positionStateChangeDict, "TriggerState": triggerChangesDict } if (changeDict["Extruder"] is not None or changeDict["Position"] is not None or changeDict["PositionState"] is not None or changeDict["TriggerState"] is not None): self.OnStateChangedCallback(changeDict) except Exception as e: # no need to re-raise, callbacks won't be notified, however. self.Settings.CurrentDebugProfile().LogException(e) def _isSnapshotCommand(self, command): commandName = GetGcodeFromString(command) snapshotCommandName = GetGcodeFromString(self.Printer.snapshot_command) return commandName == snapshotCommandName def _isTriggerWaiting(self, cmd): # make sure we're in a state that could want to check for triggers if (not self.State == TimelapseState.WaitingForTrigger): return None isWaiting = False # Loop through all of the active currentTriggers waitingTrigger = self.Triggers.GetFirstWaiting() if (waitingTrigger is not None): return True return False def _positionReceived_Home(self, x, y, z, e): try: self.Position.UpdatePosition(x=x, y=y, z=z, e=e, force=True, calculateChanges=True) except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # we need to abandon the snapshot completely, reset and resume self.State = TimelapseState.SendingSavedHomeLocationCommand self.OctoprintPrinter.commands(self.SavedCommand) self.SavedCommand = "" self.OctoprintPrinter.commands("M400") self.OctoprintPrinter.commands("M114") def _positionReceived_HomeLocationSavedCommand(self, x, y, z, e): # just sent so we can resume after the commands were sent. # Todo: do this in the gcode sent function instead of sending an m400/m114 combo self.State = TimelapseState.WaitingForTrigger self._resumePrint() def _positionReceived_Return(self, x, y, z, e): try: self.ReturnPositionReceivedTime = time.time() #todo: Do we need to re-request the position like we do for the return? Maybe... printerTolerance = self.Printer.printer_position_confirmation_tolerance # If we are requesting a return position we have NOT yet executed the command that triggered the snapshot. # Because of this we need to compare the position we received to the previous position, not the current one. if (not self.Position.IsAtSavedPosition(x, y, z)): self.Settings.CurrentDebugProfile().LogWarning( "The snapshot return position recieved from the printer does not match the position expected by Octolapse. received (x:{0},y:{1},z:{2}), Expected (x:{3},y:{4},z:{5})" .format(x, y, z, self.Position.X(), self.Position.Y(), self.Position.Z())) self.Position.UpdatePosition(x=x, y=y, z=z, force=True) else: # return position information received self.Settings.CurrentDebugProfile().LogSnapshotPositionReturn( "Snapshot return position received - x:{0},y:{1},z:{2},e:{3}" .format(x, y, z, e)) # make sure the SnapshotCommandIndex = 0 # Todo: ensure this is unnecessary self.CommandIndex = 0 # create the GCode for the timelapse and store it isRelative = self.Position.IsRelative() isExtruderRelative = self.Position.IsExtruderRelative() self.SnapshotGcodes = self.Gcode.CreateSnapshotGcode( x, y, z, self.Position.F(), self.Position.IsRelative(), self.Position.IsExtruderRelative(), self.Position.Extruder, self.Position.DistanceToZLift(), savedCommand=self.SavedCommand) # make sure we acutally received gcode if (self.SnapshotGcodes is None): self._resetSnapshot() self._resumePrint() self._onSnapshotPositionError() return False, "Error - No Snapshot Gcode" elif (self.Gcode.HasSnapshotPositionErrors): # there is a position error, but gcode was generated. Just report it to the user. self._onSnapshotPositionError() self.State = TimelapseState.SendingSnapshotGcode snapshotCommands = self.SnapshotGcodes.SnapshotCommands() # send our commands to the printer # these commands will go through queuing, no reason to track position self.OctoprintPrinter.commands(snapshotCommands) except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # we need to abandon the snapshot completely, reset and resume self._resetSnapshot() self._resumePrint() def _positionReceived_Snapshot(self, x, y, z, e): try: # snapshot position information received self.Settings.CurrentDebugProfile().LogSnapshotPositionReturn( "Snapshot position received, checking position: Received: x:{0},y:{1},z:{2},e:{3}, Expected: x:{4},y:{5},z:{6}" .format(x, y, z, e, self.Position.X(), self.Position.Y(), self.Position.Z())) printerTolerance = self.Printer.printer_position_confirmation_tolerance # see if the CURRENT position is the same as the position we received from the printer # AND that it is equal to the snapshot position if (not self.Position.IsAtCurrentPosition(x, y, None)): self.Settings.CurrentDebugProfile().LogWarning( "The snapshot position is incorrect. Received: x:{0},y:{1},z:{2},e:{3}, Expected: x:{4},y:{5},z:{6}" .format(x, y, z, e, self.Position.X(), self.Position.Y(), self.Position.Z())) elif (not self.Position.IsAtCurrentPosition(self.SnapshotGcodes.X, self.SnapshotGcodes.Y, None, applyOffset=False) ): # our snapshot gcode will NOT be offset self.Settings.CurrentDebugProfile().LogError( "The snapshot gcode position is incorrect. x:{0},y:{1},z:{2},e:{3}, Expected: x:{4},y:{5},z:{6}" .format(x, y, z, e, self.SnapshotGcodes.X, self.SnapshotGcodes.Y, self.Position.Z())) self.Settings.CurrentDebugProfile().LogSnapshotPositionReturn( "The snapshot position is correct, taking snapshot.") self.State = TimelapseState.TakingSnapshot self._takeSnapshot() except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # our best bet of fixing things up here is just to return to the previous position. self._sendReturnCommands() def _onPositionError(self): message = self.Position.PositionError(0) self.Settings.CurrentDebugProfile().LogError(message) if (self.OnPositionErrorCallback is not None): self.OnPositionErrorCallback(message) def _onSnapshotPositionError(self): if (self.Printer.abort_out_of_bounds): message = "No snapshot gcode was created for this snapshot. Aborting this snapshot. Details: {0}".format( self.Gcode.SnapshotPositionErrors) else: message = "The snapshot position has been updated due to an out-of-bounds error. Details: {0}".format( self.Gcode.SnapshotPositionErrors) self.Settings.CurrentDebugProfile().LogError(message) if (self.OnSnapshotPositionErrorCallback is not None): self.OnSnapshotPositionErrorCallback(message) def _positionReceived_ResumePrint(self, x, y, z, e): try: if (not self.Position.IsAtCurrentPosition(x, y, None)): self.Settings.CurrentDebugProfile().LogError( "Save Command Position is incorrect. Received: x:{0},y:{1},z:{2},e:{3}, Expected: x:{4},y:{5},z:{6}" .format(x, y, z, e, self.Position.X(), self.Position.Y(), self.Position.Z())) else: self.Settings.CurrentDebugProfile( ).LogSnapshotPositionResumePrint( "Save Command Position is correct. Received: x:{0},y:{1},z:{2},e:{3}, Expected: x:{4},y:{5},z:{6}" .format(x, y, z, e, self.Position.X(), self.Position.Y(), self.Position.Z())) self.SecondsAddedByOctolapse += time.time( ) - self.ReturnPositionReceivedTime # before resetting the snapshot, see if it was a success snapshotSuccess = self.SnapshotSuccess snapshotError = self.SnapshotError # end the snapshot self._resetSnapshot() # If we've requested that the timelapse stop, stop it now if (self.TimelapseStopRequested): self.StopSnapshots() except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # do not re-raise, we are better off trying to resume the print here. self._resumePrint() self._onTriggerSnapshotComplete(snapshotSuccess, snapshotError) def _onTriggerSnapshotComplete(self, snapshotSuccess, snapshotError=""): if (self.OnSnapshotCompleteCallback is not None): payload = { "success": snapshotSuccess, "error": snapshotError, "snapshot_count": self.SnapshotCount, "seconds_added_by_octolapse": self.SecondsAddedByOctolapse } self.OnSnapshotCompleteCallback(payload) def _pausePrint(self): self.OctoprintPrinter.pause_print() def _resumePrint(self): self.OctoprintPrinter.resume_print() if (self.HasBeenStopped or self.HasBeenCancelled): self.EndTimelapse(force=True) def _takeSnapshot(self): self.Settings.CurrentDebugProfile().LogSnapshotDownload( "Taking Snapshot.") try: self.CaptureSnapshot.Snap(utility.CurrentlyPrintingFileName( self.OctoprintPrinter), self.SnapshotCount, onComplete=self._onSnapshotComplete, onSuccess=self._onSnapshotSuccess, onFail=self._onSnapshotFail) except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # try to recover by sending the return command self._sendReturnCommands() def _onSnapshotSuccess(self, *args, **kwargs): # Increment the number of snapshots received self.SnapshotCount += 1 self.SnapshotSuccess = True def _onSnapshotFail(self, *args, **kwargs): reason = args[0] message = "Failed to download the snapshot. Reason:{0}".format(reason) self.Settings.CurrentDebugProfile().LogSnapshotDownload(message) self.SnapshotSuccess = False self.SnapshotError = message def _onSnapshotComplete(self, *args, **kwargs): self.Settings.CurrentDebugProfile().LogSnapshotDownload( "Snapshot Completed.") self._sendReturnCommands() def _sendReturnCommands(self): try: # if the print has been cancelled, quit now. if (self.HasBeenCancelled): self.EndTimelapse(force=True) return # Expand the current command to include the return commands if (self.SnapshotGcodes is None): self.Settings.CurrentDebugProfile().LogError( "The snapshot gcode generator has no value.") self.EndTimelapse(force=True) return returnCommands = self.SnapshotGcodes.ReturnCommands() if (returnCommands is None): self.Settings.CurrentDebugProfile().LogError( "No return commands were generated!") ## How do we handle this? we probably need to cancel the print or something.... # Todo: What to do if no return commands are generated? We should never let this happen. Make sure this is true. self.EndTimelapse(force=True) return # set the state so that the final received position will trigger a resume. self.State = TimelapseState.SendingReturnGcode # these commands will go through queuing, no need to update the position self.OctoprintPrinter.commands(returnCommands) except Exception as e: self.Settings.CurrentDebugProfile().LogException(e) # need to re-raise, can't fix this here, but at least it will be logged # properly raise def _renderTimelapse(self): # make sure we have a non null TimelapseSettings object. We may have terminated the timelapse for some reason if (self.Rendering.enabled): self.Settings.CurrentDebugProfile().LogRenderStart( "Started Rendering Timelapse") # we are rendering, set the state before starting the rendering job. self.IsRendering = True timelapseRenderJob = Render( self.Settings, self.Snapshot, self.Rendering, self.DataFolder, self.DefaultTimelapseDirectory, self.FfMpegPath, 1, timeAdded=self.SecondsAddedByOctolapse, onRenderStart=self._onRenderStart, onRenderFail=self._onRenderFail, onRenderSuccess=self._onRenderSuccess, onRenderComplete=self._onRenderComplete, onAfterSyncFail=self._onSynchronizeRenderingFail, onAfterSycnSuccess=self._onSynchronizeRenderingComplete, onComplete=self._onRenderEnd) timelapseRenderJob.Process( utility.CurrentlyPrintingFileName(self.OctoprintPrinter), self.PrintStartTime, time.time()) return True return False def _onRenderStart(self, *args, **kwargs): self.Settings.CurrentDebugProfile().LogRenderStart( "Started rendering/synchronizing the timelapse.") finalFilename = args[0] baseFileName = args[1] willSync = args[2] snapshotCount = args[3] snapshotTimeSeconds = args[4] payload = dict(FinalFileName=finalFilename, WillSync=willSync, SnapshotCount=snapshotCount, SnapshotTimeSeconds=snapshotTimeSeconds) # notify the caller if (self.OnRenderStartCallback is not None): self.OnRenderStartCallback(payload) def _onRenderFail(self, *args, **kwargs): self.IsRendering = False self.Settings.CurrentDebugProfile().LogRenderFail( "The timelapse rendering failed.") #Notify Octoprint finalFilename = args[0] baseFileName = args[1] returnCode = args[2] reason = args[3] payload = dict(gcode="unknown", movie=finalFilename, movie_basename=baseFileName, returncode=returnCode, reason=reason) if (self.OnRenderFailCallback is not None): self.OnRenderFailCallback(payload) def _onRenderSuccess(self, *args, **kwargs): finalFilename = args[0] baseFileName = args[1] #TODO: Notify the user that the rendering is completed if we are not synchronizing with octoprint self.Settings.CurrentDebugProfile().LogRenderComplete( "Rendering completed successfully.") def _onRenderComplete(self, *args, **kwargs): self.IsRendering = False finalFileName = args[0] synchronize = args[1] self.Settings.CurrentDebugProfile().LogRenderComplete( "Completed rendering the timelapse.") if (self.OnRenderCompleteCallback is not None): self.OnRenderCompleteCallback() def _onSynchronizeRenderingFail(self, *args, **kwargs): finalFilename = args[0] baseFileName = args[1] # Notify the user of success and refresh the default timelapse control payload = dict( gcode="unknown", movie=finalFilename, movie_basename=baseFileName, reason= "Error copying the rendering to the Octoprint timelapse folder. If logging is enabled you can search for 'Synchronization Error' to find the error. Your timelapse is likely within the octolapse data folder." ) if (self.OnRenderingSynchronizeFailCallback is not None): self.OnRenderingSynchronizeFailCallback(payload) def _onSynchronizeRenderingComplete(self, *args, **kwargs): finalFilename = args[0] baseFileName = args[1] # Notify the user of success and refresh the default timelapse control payload = dict( gcode="unknown", movie=finalFilename, movie_basename=baseFileName, movie_prefix= "from Octolapse has been synchronized and is now available within the default timelapse plugin tab. Octolapse ", returncode=0, reason="See the octolapse log for details.") if (self.OnRenderingSynchronizeCompleteCallback is not None): self.OnRenderingSynchronizeCompleteCallback(payload) def _onRenderEnd(self, *args, **kwargs): self.IsRendering = False finalFileName = args[0] baseFileName = args[1] synchronize = args[2] success = args[3] self.Settings.CurrentDebugProfile().LogRenderComplete( "Completed rendering.") moviePrefix = "from Octolapse" if (not synchronize): moviePrefix = "from Octolapse. Your timelapse was NOT synchronized (see advanced rendering settings for details), but can be found in octolapse's data directory. A file browser will be added in a future release (hopefully)" payload = dict(movie=finalFileName, movie_basename=baseFileName, movie_prefix=moviePrefix, success=success) if (self.OnRenderEndCallback is not None): self.OnRenderEndCallback(payload) def _onTimelapseStart(self): if (self.OnTimelapseStartCallback is None): return self.OnTimelapseStartCallback() def _reset(self): self.State = TimelapseState.Idle self.HasSentInitialStatus = False self.Triggers.Reset() self.CommandIndex = -1 self.SnapshotCount = 0 self.PrintStartTime = None self.SnapshotGcodes = None self.SavedCommand = None self.PositionRequestAttempts = 0 self.IsTestMode = False # time tracking - how much time did we add to the print? self.SecondsAddedByOctolapse = 0 self.ReturnPositionReceivedTime = None # A list of callbacks who want to be informed when a timelapse ends self.TimelapseStopRequested = False self.SnapshotSuccess = False self.SnapshotError = "" self.HasBeenCancelled = False self.HasBeenStopped = False def _resetSnapshot(self): self.State = TimelapseState.WaitingForTrigger self.CommandIndex = -1 self.SnapshotGcodes = None self.SavedCommand = None self.PositionRequestAttempts = 0 self.SnapshotSuccess = False self.SnapshotError = ""
def test_GcodeTrigger(self): """Test the gcode triggers""" self.Settings.current_snapshot().gcode_trigger_require_zhop = False position = Position(self.Settings, self.OctoprintPrinterProfile, False) trigger = GcodeTrigger(self.Settings) # test initial state self.assertFalse(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # send a command that is NOT the snapshot command using the defaults trigger.update(position, "NotTheSnapshotCommand") self.assertFalse(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # send a command that is the snapshot command without the axis being homes trigger.update(position, "snap") self.assertFalse(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # reset, set relative extruder and absolute xyz, home the axis, and resend the snap command, should wait # since we require the home command to complete (sent to printer) before triggering position.update("M83") position.update("G90") position.update("G28") trigger.update(position, "snap") self.assertFalse(trigger.is_triggered(0)) self.assertTrue(trigger.is_waiting(0)) # try again, Snap is encountered, but it must be the previous command to trigger position.update("G0 X0 Y0 Z0 E1 F0") trigger.update(position, "G0 X0 Y0 Z0 E1 F0") self.assertTrue(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # try again, but this time set RequireZHop to true trigger.RequireZHop = True trigger.update(position, "snap") self.assertFalse(trigger.is_triggered(0)) self.assertTrue(trigger.is_waiting(0)) # send another command to see if we are still waiting trigger.update(position, "NotTheSnapshotCommand") self.assertFalse(trigger.is_triggered(0)) self.assertTrue(trigger.is_waiting(0)) # fake a zhop position.is_zhop = lambda x: True trigger.update(position, "NotTheSnapshotCommand") self.assertTrue(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # send a command that is NOT the snapshot command using the defaults trigger.update(position, "NotTheSnapshotCommand") self.assertFalse(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0)) # change the snapshot triggers and make sure they are working self.Settings.current_snapshot().gcode_trigger_require_zhop = None self.Settings.current_snapshot().gcode_trigger_on_extruding = True self.Settings.current_snapshot( ).gcode_trigger_on_extruding_start = None self.Settings.current_snapshot().gcode_trigger_on_primed = None self.Settings.current_snapshot().gcode_trigger_on_retracting = None self.Settings.current_snapshot( ).gcode_trigger_on_partially_retracted = None self.Settings.current_snapshot().gcode_trigger_on_retracted = None self.Settings.current_snapshot( ).gcode_trigger_on_detracting_start = None self.Settings.current_snapshot().gcode_trigger_on_detracting = None self.Settings.current_snapshot().gcode_trigger_on_detracted = None trigger = GcodeTrigger(self.Settings) # send a command that is the snapshot command using the defaults trigger.update(position, "snap") self.assertFalse(trigger.is_triggered(0)) self.assertTrue(trigger.is_waiting(0)) # change the extruder state and test # should not trigger because trigger tests the previous command position.update("G0 X0 Y0 Z0 E10 F0") trigger.update(position, "NotTheSnapshotCommand") self.assertTrue(trigger.is_triggered(0)) self.assertFalse(trigger.is_waiting(0))
def test_zHop(self): """Test zHop detection.""" # set zhop distance self.Settings.CurrentPrinter().z_hop = .5 position = Position(self.Settings, self.OctoprintPrinterProfile, False) # test initial state self.assertTrue(not position.IsZHop) # check without homed axis position.Update("G1 x0 y0 z0") self.assertTrue(not position.IsZHop) position.Update("G1 x0 y0 z0.5") self.assertTrue(not position.IsZHop) # Home axis, check again position.Update("G28") self.assertTrue(not position.IsZHop) # Position reports as NotHomed (misnomer, need to replace), needs to get coordinates position.Update("G1 x0 y0 z0") # Move up without extrude, this is not a zhop since we haven't extruded anything! position.Update("g0 z0.5") self.assertTrue(not position.IsZHop) # move back down to 0 and extrude position.Update("g0 z0 e1") self.assertTrue(not position.IsZHop) # Move up without extrude, this should trigger zhop start position.Update("g0 z0.5") self.assertTrue(position.IsZHop) # move below zhop threshold position.Update("g0 z0.3") self.assertTrue(position.IsZHop) # move right up to zhop without going over, we are within the rounding error position.Update("g0 z0.4999") self.assertTrue(position.IsZHop) # Extrude on z5 position.Update("g0 z0.5 e1") self.assertTrue(position.IsZHop) # partial z lift, , we are within the rounding error position.Update("g0 z0.9999") self.assertTrue(position.IsZHop) # zhop to 1 position.Update("g0 z1") self.assertTrue(position.IsZHop) # test with extrusion start at 1.5 position.Update("g0 z1.5 e1") self.assertTrue(position.IsZHop) # test with extrusion at 2 position.Update("g0 z2 e1") self.assertTrue(not position.IsZHop) #zhop position.Update("g0 z2.5 e0") self.assertTrue(position.IsZHop) # do not move extruder position.Update("no-command") self.assertTrue(position.IsZHop)
def test_ExtruderMovement(self): """Test the M82 and M83 command.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # test initial position self.assertTrue(position.E == 0) self.assertTrue(position.IsExtruderRelative == True) self.assertTrue(position.ERelative() == 0) # test movement position.Update("G0 E100") self.assertTrue(position.E == 100) self.assertTrue(position.ERelative() == 100) # switch to absolute movement position.Update("M82") self.assertTrue(position.IsExtruderRelative == False) self.assertTrue(position.E == 100) self.assertTrue(position.ERelative() == 0) # move to -25 position.Update("G0 E-25") self.assertTrue(position.E == -25) self.assertTrue(position.ERelative() == -125) # test movement to origin position.Update("G0 E0") self.assertTrue(position.E == 0) self.assertTrue(position.ERelative() == 25) # switch to relative position position.Update("M83") position.Update("G0 e1.1") self.assertTrue(position.E == 1.1) self.assertTrue(position.ERelative() == 1.1) # move and test position.Update("G0 e1.1") self.assertTrue(position.E == 2.2) self.assertTrue(position.ERelative() == 1.1) # move and test position.Update("G0 e-2.2") self.assertTrue(position.E == 0) self.assertTrue(position.ERelative() == -2.2)
def test_HeightAndLayerChanges(self): """Test the height and layer changes.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # test initial state self.assertTrue(position.Height is None) self.assertTrue(position.HeightPrevious == 0) self.assertTrue(position.Layer == 0) self.assertTrue(not position.IsLayerChange) # check without homed axis position.Update("G1 x0 y0 z0.20000 e1") self.assertTrue(position.Height is None) self.assertTrue(position.HeightPrevious is None) self.assertTrue(position.Layer == 0) self.assertTrue(not position.IsLayerChange) # set homed axis, absolute coordinates, and check height and layer position.Update("G28") self.assertTrue(position.Height is None) self.assertTrue(position.HeightPrevious is None) self.assertTrue(position.Layer == 0) self.assertTrue(not position.IsLayerChange) # move without extruding, height and layer should not change position.Update("G1 x100 y200 z150") self.assertTrue(position.Height is None) self.assertTrue(position.HeightPrevious is None) self.assertTrue(position.Layer == 0) self.assertTrue(not position.IsLayerChange) # move to origin, height and layer stuff should stay the same position.Update("G1 x0 y0 z0") self.assertTrue(position.Height is None) self.assertTrue(position.HeightPrevious is None) self.assertTrue(position.Layer == 0) self.assertTrue(not position.IsLayerChange) # extrude, height change! position.Update("G1 x0 y0 z0 e1") self.assertTrue(position.Height == 0) self.assertTrue(position.HeightPrevious == 0) self.assertTrue(position.Layer == 1) self.assertTrue(position.IsLayerChange) #extrude higher, update layer., this will get rounded to 0.2 position.Update("G1 x0 y0 z0.1999 e1") self.assertTrue(position.Height == 0.2) self.assertTrue(position.HeightPrevious == 0) self.assertTrue(position.Layer == 2) self.assertTrue(position.IsLayerChange) # extrude just slightly higher, but with rounding on the same layer position.Update("G1 x0 y0 z0.20000 e1") self.assertTrue(position.Height == .2) self.assertTrue(position.HeightPrevious == 0.2) self.assertTrue(position.Layer == 2) self.assertTrue(not position.IsLayerChange) # extrude again on same layer - Height Previous should now be updated, and IsLayerChange should be false position.Update("G1 x0 y0 z0.20000 e1") self.assertTrue(position.Height == .2) self.assertTrue(position.HeightPrevious == .2) self.assertTrue(position.Layer == 2) self.assertTrue(not position.IsLayerChange) # extrude again on same layer - No changes position.Update("G1 x0 y0 z0.20000 e1") self.assertTrue(position.Height == .2) self.assertTrue(position.HeightPrevious == .2) self.assertTrue(position.Layer == 2) self.assertTrue(not position.IsLayerChange) # extrude below the current layer - No changes position.Update("G1 x0 y0 z0.00000 e1") self.assertTrue(position.Height == .2) self.assertTrue(position.HeightPrevious == .2) self.assertTrue(position.Layer == 2) self.assertTrue(not position.IsLayerChange) # extrude up higher and change the height/layer. Should never happen, but it's an interesting test case position.Update("G1 x0 y0 z0.60000 e1") self.assertTrue(position.Height == .6) self.assertTrue(position.HeightPrevious == .2) self.assertTrue(position.Layer == 3) self.assertTrue(position.IsLayerChange) # extrude up again position.Update("G1 x0 y0 z0.65000 e1") self.assertTrue(position.Height == .65) self.assertTrue(position.HeightPrevious == .6) self.assertTrue(position.Layer == 4) self.assertTrue(position.IsLayerChange) # extrude on previous layer position.Update("G1 x0 y0 z0.60000 e1") self.assertTrue(position.Height == .65) self.assertTrue(position.HeightPrevious == .65) self.assertTrue(position.Layer == 4) self.assertTrue(not position.IsLayerChange) # extrude on previous layer again position.Update("G1 x0 y0 z0.60000 e1") self.assertTrue(position.Height == .65) self.assertTrue(position.HeightPrevious == .65) self.assertTrue(position.Layer == 4) self.assertTrue(not position.IsLayerChange) # move up but do not extrude position.Update("G1 x0 y0 z0.70000") self.assertTrue(position.Height == .65) self.assertTrue(position.HeightPrevious == .65) self.assertTrue(position.Layer == 4) self.assertTrue(not position.IsLayerChange) # move up but do not extrude a second time position.Update("G1 x0 y0 z0.80000") self.assertTrue(position.Height == .65) self.assertTrue(position.HeightPrevious == .65) self.assertTrue(position.Layer == 4) self.assertTrue(not position.IsLayerChange) # extrude at a different height position.Update("G1 x0 y0 z0.85000 e.001") self.assertTrue(position.Height == .85) self.assertTrue(position.HeightPrevious == .65) self.assertTrue(position.Layer == 5) self.assertTrue(position.IsLayerChange)
def test_PositionError(self): """Test the IsInBounds function to make sure the program will not attempt to operate after being told to move out of bounds. """ position = Position(self.Settings, self.OctoprintPrinterProfile, False) # Initial test, should return false without any coordinates self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # home the axis and test position.update("G28") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # X axis tests # reset, set relative extruder and absolute xyz, home the axis and test again position.reset() position.update("M83") position.update("G90") position.update("G28") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move out of bounds min position.update("G0 x-0.0001") self.assertTrue(position.has_position_error()) self.assertTrue(position.position_error() is not None) # move back in bounds position.update("G0 x0.0") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move to middle position.update("G0 x125") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move to max position.update("G0 x250") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move out of bounds max position.update("G0 x250.0001") self.assertTrue(position.has_position_error()) self.assertTrue(position.position_error() is not None) # Y axis tests # reset, set relative extruder and absolute xyz, home the axis and test again position.reset() position.update("M83") position.update("G90") position.update("G28") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move out of bounds min position.update("G0 y-0.0001") self.assertTrue(position.has_position_error()) self.assertTrue(position.position_error() is not None) # move back in bounds position.update("G0 y0.0") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move to middle position.update("G0 y100") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move to max position.update("G0 y200") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move out of bounds max position.update("G0 y200.0001") self.assertTrue(position.has_position_error()) self.assertTrue(position.position_error() is not None) # Z axis tests # reset, home the axis and test again # reset, set relative extruder and absolute xyz, home the axis and test again position.reset() position.update("M83") position.update("G90") position.update("G28") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move out of bounds min position.update("G0 z-0.0001") self.assertTrue(position.has_position_error()) self.assertTrue(position.position_error() is not None) # move back in bounds position.update("G0 z0.0") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move to middle position.update("G0 z100") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move to max position.update("G0 z200") self.assertFalse(position.has_position_error()) self.assertIsNone(position.position_error()) # move out of bounds max position.update("G0 z200.0001") self.assertTrue(position.has_position_error()) self.assertTrue(position.position_error() is not None)
def test_ExtruderMovement(self): """Test the M82 and M83 command.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile) # test initial position self.assertIsNone(position.e()) self.assertIsNone(position.is_extruder_relative()) self.assertIsNone(position.e_relative_pos(previous_pos)) # set extruder to relative coordinates position.update("M83") # test movement previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("G0 E100") self.assertEqual(position.e(), 100) # this is somewhat reversed from what we do in the position.py module # there we update the pos() object and compare to the current state, so # comparing the current state to the # previous will result in the opposite sign self.assertEqual(position.e_relative_pos(previous_pos), -100) # switch to absolute movement previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("M82") self.assertFalse(position.is_extruder_relative()) self.assertEqual(position.e(), 100) self.assertEqual(position.e_relative_pos(previous_pos), 0) # move to -25 previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("G0 E-25") self.assertEqual(position.e(), -25) self.assertEqual(position.e_relative_pos(previous_pos), 125) # test movement to origin previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("G0 E0") self.assertEqual(position.e(), 0) self.assertEqual(position.e_relative_pos(previous_pos), -25) # switch to relative position previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("M83") position.update("G0 e1.1") self.assertEqual(position.e(), 1.1) self.assertEqual(position.e_relative_pos(previous_pos), -1.1) # move and test previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("G0 e1.1") self.assertEqual(position.e(), 2.2) self.assertEqual(position.e_relative_pos(previous_pos), -1.1) # move and test previous_pos = Pos(self.Settings.current_printer(), self.OctoprintPrinterProfile, position.get_position()) position.update("G0 e-2.2") self.assertEqual(position.e(), 0) self.assertEqual(position.e_relative_pos(previous_pos), 2.2)
def test_Update(self): """Test the UpdatePosition function with the force option set to true.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # no homed axis position.Update("G1 x100 y200 z300") self.assertTrue(position.X is None) self.assertTrue(position.Y is None) self.assertTrue(position.Z is None) # set homed axis and update absolute position position.Update("G28") position.Update("G1 x100 y200 z150") self.assertTrue(position.X == 100) self.assertTrue(position.Y == 200) self.assertTrue(position.Z == 150) # move again and retest position.Update("G1 x101 y199 z151") self.assertTrue(position.X == 101) self.assertTrue(position.Y == 199) self.assertTrue(position.Z == 151) # switch to relative and update position position.Update("G91") position.Update("G1 x-1 y-1 z1.0") self.assertTrue(position.X == 100) self.assertTrue(position.Y == 198) self.assertTrue(position.Z == 152) # move again and retest position.Update("G1 x-99 y-196 z-149.0") self.assertTrue(position.X == 1) self.assertTrue(position.Y == 2) self.assertTrue(position.Z == 3) # go back to absolute and move to origin position.Update("G90") position.Update("G1 x0 y0 z0.0") self.assertTrue(position.X == 0) self.assertTrue(position.Y == 0) self.assertTrue(position.Z == 0)
def test_reset(self): """Test init state.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # reset all initialized vars to something else position.X = -1 position.XOffset = -1 position.XPrevious = -1 position.Y = -1 position.YOffset = -1 position.YPrevious = -1 position.Z = -1 position.ZOffset = -1 position.ZPrevious = -1 position.E = -1 position.EOffset = -1 position.EPrevious = -1 position.IsRelative = True position.IsExtruderRelative = False position.Height = -1 position.HeightPrevious = -1 position.ZDelta = -1 position.ZDeltaPrevious = -1 position.Layer = -1 position.IsLayerChange = True position.IsZHop = True position.XHomed = True position.YHomed = True position.ZHomed = True position.HasPositionError = True position.IsZHop = True position.PositionError = "Error!" # reset position.Reset() # test initial state self.assertTrue(position.X is None) self.assertTrue(position.XOffset == 0) self.assertTrue(position.XPrevious is None) self.assertTrue(position.Y is None) self.assertTrue(position.YOffset == 0) self.assertTrue(position.YPrevious is None) self.assertTrue(position.Z is None) self.assertTrue(position.ZOffset == 0) self.assertTrue(position.ZPrevious is None) self.assertTrue(position.E == 0) self.assertTrue(position.EOffset == 0) self.assertTrue(position.EPrevious == 0) self.assertTrue(position.IsRelative == False) self.assertTrue(position.IsExtruderRelative) self.assertTrue(position.Height is None) self.assertTrue(position.HeightPrevious == 0) self.assertTrue(position.ZDelta is None) self.assertTrue(position.ZDeltaPrevious is None) self.assertTrue(position.Layer == 0) self.assertTrue(not position.IsLayerChange) self.assertTrue(not position.IsZHop) self.assertTrue(not position.XHomed) self.assertTrue(not position.YHomed) self.assertTrue(not position.ZHomed) self.assertTrue(not position.HasPositionError) self.assertTrue(not position.IsZHop) self.assertTrue(position.PositionError is None)
def test_Home(self): """Test the home command. Make sure the position is set to 0,0,0 after the home.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) position.Update("G28") self.assertTrue(position.X == None) self.assertTrue(position.XHomed) self.assertTrue(position.Y == None) self.assertTrue(position.YHomed) self.assertTrue(position.ZHomed) self.assertTrue(position.Z == None) self.assertTrue(not position.HasHomedPosition()) position.Reset() position.Update("G28 X") self.assertTrue(position.X == None) self.assertTrue(position.XHomed) self.assertTrue(position.Y is None) self.assertTrue(not position.YHomed) self.assertTrue(position.Z is None) self.assertTrue(not position.ZHomed) self.assertTrue(not position.HasHomedPosition()) position.Reset() position.Update("G28 Y") self.assertTrue(position.X is None) self.assertTrue(not position.XHomed) self.assertTrue(position.Y is None) self.assertTrue(position.YHomed) self.assertTrue(position.Z is None) self.assertTrue(not position.ZHomed) self.assertTrue(not position.HasHomedPosition()) position.Reset() position.Update("G28 Z") self.assertTrue(position.X is None) self.assertTrue(not position.XHomed) self.assertTrue(position.Y is None) self.assertTrue(not position.YHomed) self.assertTrue(position.Z is None) self.assertTrue(position.ZHomed) self.assertTrue(not position.HasHomedPosition()) position.Reset() position.Update("G28 Z X Y") self.assertTrue(position.X is None) self.assertTrue(position.XHomed) self.assertTrue(position.Y is None) self.assertTrue(position.YHomed) self.assertTrue(position.Z is None) self.assertTrue(position.ZHomed) self.assertTrue(not position.HasHomedPosition()) position.Reset() position.Update("G28 W") self.assertTrue(position.X is None) self.assertTrue(position.XHomed) self.assertTrue(position.Y is None) self.assertTrue(position.YHomed) self.assertTrue(position.Z is None) self.assertTrue(position.ZHomed) self.assertTrue(not position.HasHomedPosition()) position.Reset() position.Update("g28") position.Update("g1 x0 y0 z0") # here we have seen the upded coordinates, but we do not know the position self.assertTrue(position.X == 0) self.assertTrue(position.XHomed) self.assertTrue(position.Y == 0) self.assertTrue(position.YHomed) self.assertTrue(position.Z == 0) self.assertTrue(position.ZHomed) self.assertTrue(not position.HasHomedPosition()) # give it another position, now we have homed axis with a known position position.Update("g1 x0 y0 z0") self.assertTrue(position.X == 0) self.assertTrue(position.XHomed) self.assertTrue(position.Y == 0) self.assertTrue(position.YHomed) self.assertTrue(position.Z == 0) self.assertTrue(position.ZHomed) self.assertTrue(position.HasHomedPosition())
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_UpdatePosition_noforce(self): """Test the UpdatePosition function with the force option set to true.""" position = Position(self.Settings, self.OctoprintPrinterProfile, False) # no homed axis position.UpdatePosition(x=0, y=0, z=0, e=0) self.assertTrue(position.X is None) self.assertTrue(position.Y is None) self.assertTrue(position.Z is None) self.assertTrue(position.E == 0) # set homed axis, test absolute position (default) position.XHomed = True position.YHomed = True position.ZHomed = True position.UpdatePosition(x=0, y=0, z=0) self.assertTrue(position.X == 0) self.assertTrue(position.Y == 0) self.assertTrue(position.Z == 0) self.assertTrue(position.E == 0) # update absolute position position.UpdatePosition(x=1, y=2, z=3) self.assertTrue(position.X == 1) self.assertTrue(position.Y == 2) self.assertTrue(position.Z == 3) self.assertTrue(position.E == 0) # set relative position position.IsRelative = True position.UpdatePosition(x=1, y=1, z=1) self.assertTrue(position.X == 2) self.assertTrue(position.Y == 3) self.assertTrue(position.Z == 4) self.assertTrue(position.E == 0) # set extruder absolute position.IsExtruderRelative = False position.UpdatePosition(e=100) self.assertTrue(position.X == 2) self.assertTrue(position.Y == 3) self.assertTrue(position.Z == 4) self.assertTrue(position.E == 100) position.UpdatePosition(e=-10) self.assertTrue(position.X == 2) self.assertTrue(position.Y == 3) self.assertTrue(position.Z == 4) self.assertTrue(position.E == -10) # set extruder relative position.IsExtruderRelative = True position.UpdatePosition(e=20) self.assertTrue(position.X == 2) self.assertTrue(position.Y == 3) self.assertTrue(position.Z == 4) self.assertTrue(position.E == 10) position.UpdatePosition(e=-1) self.assertTrue(position.X == 2) self.assertTrue(position.Y == 3) self.assertTrue(position.Z == 4) self.assertTrue(position.E == 9) position.UpdatePosition(x=1, y=2, z=3, e=4, force=True) self.assertTrue(position.X == 1) self.assertTrue(position.Y == 2) self.assertTrue(position.Z == 3) self.assertTrue(position.E == 4) position.UpdatePosition(x=None, y=None, z=None, e=None, force=True) self.assertTrue(position.X == 1) self.assertTrue(position.Y == 2) self.assertTrue(position.Z == 3) self.assertTrue(position.E == 4)