コード例 #1
0
	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)
コード例 #2
0
    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()
コード例 #3
0
    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))
コード例 #4
0
ファイル: timelapse.py プロジェクト: nbun/Octolapse
    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))
コード例 #5
0
 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)
コード例 #6
0
    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)
コード例 #7
0
    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)
コード例 #8
0
    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)
コード例 #9
0
    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)
コード例 #10
0
 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))
コード例 #11
0
    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)
コード例 #12
0
    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)
コード例 #13
0
    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))
コード例 #14
0
ファイル: timelapse.py プロジェクト: ppessi/Octolapse
 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()
コード例 #15
0
    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)
コード例 #16
0
	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)
コード例 #17
0
    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)
コード例 #18
0
    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)
コード例 #19
0
ファイル: timelapse.py プロジェクト: ppessi/Octolapse
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 = ""
コード例 #20
0
    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))
コード例 #21
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)
コード例 #22
0
    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)
コード例 #23
0
    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)
コード例 #24
0
    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)
コード例 #25
0
    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)
コード例 #26
0
    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)
コード例 #27
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)
コード例 #28
0
    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())
コード例 #29
0
class Timelapse(object):

    def __init__(
            self, settings, octoprint_printer, data_folder, timelapse_folder,
            on_print_started=None, on_print_start_failed=None,
            on_snapshot_start=None, on_snapshot_end=None,
            on_render_start=None, on_render_end=None,
            on_timelapse_stopping=None, on_timelapse_stopped=None,
            on_state_changed=None, on_timelapse_start=None,
            on_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 = ""
コード例 #30
0
    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)