示例#1
0
    def StartTimelapse(self, octoprintPrinter, octoprintPrinterProfile,
                       ffmpegPath, g90InfluencesExtruder):
        self.Reset()

        self.OctoprintPrinter = octoprintPrinter
        self.OctoprintPrinterProfile = octoprintPrinterProfile
        self.FfMpegPath = ffmpegPath
        self.PrintStartTime = time.time()
        self.Snapshot = Snapshot(self.Settings.CurrentSnapshot())
        self.Gcode = SnapshotGcodeGenerator(self.Settings,
                                            octoprintPrinterProfile)
        self.Printer = Printer(self.Settings.CurrentPrinter())
        self.Rendering = Rendering(self.Settings.CurrentRendering())
        self.CaptureSnapshot = CaptureSnapshot(
            self.Settings, self.DataFolder, printStartTime=self.PrintStartTime)
        self.Position = Position(self.Settings, octoprintPrinterProfile,
                                 g90InfluencesExtruder)
        self.State = TimelapseState.WaitingForTrigger

        self.IsTestMode = self.Settings.CurrentDebugProfile().is_test_mode
        # create the triggers
        # If the gcode trigger is enabled, add it
        if (self.Snapshot.gcode_trigger_enabled):
            #Add the trigger to the list
            self.Triggers.append(GcodeTrigger(self.Settings))
        # If the layer trigger is enabled, add it
        if (self.Snapshot.layer_trigger_enabled):
            self.Triggers.append(LayerTrigger(self.Settings))
        # If the layer trigger is enabled, add it
        if (self.Snapshot.timer_trigger_enabled):
            self.Triggers.append(TimerTrigger(self.Settings))
示例#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_GetSnapshotGcode_ReturnCommands(self):
        # test with relative paths, absolute extruder coordinates, retract and z hop
        # use relative coordinates for stabilizations
        self.Settings.current_stabilization().x_type = "fixed_path"
        self.Settings.current_stabilization().x_fixed_path = "50,100"  # 125,250
        self.Settings.current_stabilization().x_fixed_path_loop = False
        self.Settings.current_stabilization().x_fixed_path_invert_loop = False
        self.Settings.current_stabilization().y_type = "fixed_path"
        self.Settings.current_stabilization().y_fixed_path = "50,100"  # 100,200
        self.Settings.current_stabilization().y_fixed_path_loop = False
        self.Settings.current_stabilization().y_fixed_path_invert_loop = False
        self.Settings.current_snapshot().retract_before_move = True
        snapshot_gcode_generator = SnapshotGcodeGenerator(
            self.Settings, self.create_octoprint_printer_profile())
        self.Extruder.is_retracted = lambda: True
        snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
            100, 50, 0, 3600, True, False, self.Extruder, 0.5, "SavedCommand")

        # verify the return commands
        self.assertEqual(snapshot_gcode.get_return_commands()[0], "G1 X100.000 Y50.000")
        self.assertEqual(snapshot_gcode.get_return_commands()[1], "G91")
        self.assertEqual(snapshot_gcode.get_return_commands()[2], "G1 F6000")
        self.assertEqual(snapshot_gcode.get_return_commands()[3], "G1 Z-0.500")
        self.assertEqual(snapshot_gcode.get_return_commands()[4], "G1 F3600")
        self.assertEqual(snapshot_gcode.get_return_commands()[5], "SAVEDCOMMAND")
        self.assertEqual(snapshot_gcode.get_return_commands()[6], "M400")
        self.assertEqual(snapshot_gcode.get_return_commands()[7], "M114")
示例#4
0
 def test_GetSnapshotGcode_Fixed_AbsoluteCoordintes_ExtruderRelative(self):
     """Test snapshot gcode in absolute coordinate system with relative extruder and fixed coordinate stabilization"""
     # adjust the settings for absolute position and create the snapshot gcode generator
     self.Settings.CurrentStabilization().x_type = "fixed_coordinate"
     self.Settings.CurrentStabilization().x_fixed_coordinate = 10
     self.Settings.CurrentStabilization().y_type = "fixed_coordinate"
     self.Settings.CurrentStabilization().y_fixed_coordinate = 20
     snapshotGcodeGenerator = SnapshotGcodeGenerator(
         self.Settings, self.CreateOctoprintPrinterProfile())
     self.Extruder.IsRetracted = True
     snapshotGcode = snapshotGcodeGenerator.CreateSnapshotGcode(
         0, 0, 0, False, True, self.Extruder, "SavedCommand")
     # verify the created gcode
     # this line should switch from absolute to relative for the ZHop
     self.assertTrue(snapshotGcode.GcodeCommands[0] == "G91")
     # this line should zhop by 0.500 mm
     self.assertTrue(snapshotGcode.GcodeCommands[1] == "G1 Z0.500 F7200")
     # this line should switch to absolute coordinates in prep for move
     self.assertTrue(snapshotGcode.GcodeCommands[2] == "G90")
     # this line should switch back to absolute coordinates
     self.assertTrue(
         snapshotGcode.GcodeCommands[3] == "G1 X10.000 Y20.000 F7200")
     # move back to the return position
     self.assertTrue(
         snapshotGcode.GcodeCommands[4] == "G1 X0.000 Y0.000 F7200")
     # change to relative coordinates
     self.assertTrue(snapshotGcode.GcodeCommands[5] == "G91")
     # this line should zhop by -0.500 mm
     self.assertTrue(snapshotGcode.GcodeCommands[6] == "G1 Z-0.500 F7200")
     # change back to the original coordinate system (absolute)
     self.assertTrue(snapshotGcode.GcodeCommands[7] == "G90")
     # the saved command
     self.assertTrue(snapshotGcode.GcodeCommands[8] == "SavedCommand")
     # verify the return commands
     self.assertTrue(
         snapshotGcode.ReturnCommands()[0] == "G1 X0.000 Y0.000 F7200")
     self.assertTrue(snapshotGcode.ReturnCommands()[1] == "G91")
     self.assertTrue(
         snapshotGcode.ReturnCommands()[2] == "G1 Z-0.500 F7200")
     self.assertTrue(snapshotGcode.ReturnCommands()[3] == "G90")
     self.assertTrue(snapshotGcode.ReturnCommands()[4] == "SavedCommand")
     # verify the snapshot commands
     self.assertTrue(snapshotGcode.SnapshotCommands()[0] == "G91")
     self.assertTrue(
         snapshotGcode.SnapshotCommands()[1] == "G1 Z0.500 F7200")
     self.assertTrue(snapshotGcode.SnapshotCommands()[2] == "G90")
     self.assertTrue(
         snapshotGcode.SnapshotCommands()[3] == "G1 X10.000 Y20.000 F7200")
     # verify the indexes of the generated gcode
     self.assertTrue(snapshotGcode.SnapshotIndex == 3)
     self.assertTrue(snapshotGcode.EndIndex() == 8)
     # verify the return coordinates
     self.assertTrue(snapshotGcode.ReturnX == 0)
     self.assertTrue(snapshotGcode.ReturnY == 0)
     self.assertTrue(snapshotGcode.ReturnZ == 0)
示例#5
0
    def test_GetSnapshotGcode_RelativePath_RelativeCoordinates_ExtruderAbsolute_ZHop_Retraction(
            self):
        # test with relative paths, absolute extruder coordinates, retract and z hop
        # use relative coordinates for stabilizations
        self.Settings.current_stabilization().x_type = "relative_path"
        self.Settings.current_stabilization(
        ).x_relative_path = "50,100"  # 125,250
        self.Settings.current_stabilization().x_relative_path_loop = False
        self.Settings.current_stabilization(
        ).x_relative_path_invert_loop = False
        self.Settings.current_stabilization().y_type = "relative_path"
        self.Settings.current_stabilization(
        ).y_relative_path = "50,100"  # 100,200
        self.Settings.current_stabilization().y_relative_path_loop = False
        self.Settings.current_stabilization(
        ).y_relative_path_invert_loop = False
        self.Settings.current_snapshot().retract_before_move = True
        snapshot_gcode_generator = SnapshotGcodeGenerator(
            self.Settings, self.create_octoprint_printer_profile())

        snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
            10, 10, 10, 3600, True, False, self.Extruder, 0.5, "SavedCommand")
        # verify the created gcode
        self.assertEqual(snapshot_gcode.GcodeCommands[0], "M83")
        self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 F4000")
        self.assertEqual(snapshot_gcode.GcodeCommands[2], "G1 E-2.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[3], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 Z0.500")
        self.assertEqual(snapshot_gcode.GcodeCommands[5], "G90")
        self.assertEqual(snapshot_gcode.GcodeCommands[6], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[7],
                         "G1 X125.000 Y100.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[8], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[9], "M114")
        self.assertEqual(snapshot_gcode.GcodeCommands[10],
                         "G1 X10.000 Y10.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[11], "G91")
        self.assertEqual(snapshot_gcode.GcodeCommands[12], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[13], "G1 Z-0.500")
        self.assertEqual(snapshot_gcode.GcodeCommands[14], "G1 F3000")
        self.assertEqual(snapshot_gcode.GcodeCommands[15], "G1 E2.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[16], "M82")
        self.assertEqual(snapshot_gcode.GcodeCommands[17], "G1 F3600")
        self.assertEqual(snapshot_gcode.GcodeCommands[18], "SAVEDCOMMAND")
        self.assertEqual(snapshot_gcode.GcodeCommands[19], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[20], "M114")

        # verify the indexes of the generated gcode
        self.assertTrue(snapshot_gcode.SnapshotIndex == 9)
        self.assertTrue(snapshot_gcode.end_index() == 20)
        # verify the return coordinates
        self.assertTrue(snapshot_gcode.ReturnX == 10)
        self.assertTrue(snapshot_gcode.ReturnY == 10)
        self.assertTrue(snapshot_gcode.ReturnZ == 10)
示例#6
0
    def test_GetSnapshotGcode_Relative_RelativeCoordinates_AbsoluteExtruder_ZhopTooHigh(
            self):
        """Test snapshot gcode with relative stabilization, relative coordinates, absolute extruder, z is too high to hop, no retraction"""

        # test with relative coordinates, absolute extruder coordinates, z hop impossible (current z height will not allow this since it puts things outside of the bounds)
        # use relative coordinates for stabilizations
        self.Settings.CurrentStabilization().x_type = "relative"
        self.Settings.CurrentStabilization().x_relative = 50  #125
        self.Settings.CurrentStabilization().y_type = "relative"
        self.Settings.CurrentStabilization().y_relative = 100  #200
        self.Settings.CurrentSnapshot().retract_before_move = False
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        # create
        snapshotGcode = snapshotGcodeGenerator.CreateSnapshotGcode(
            10, 10, 200, True, False, self.Extruder, "SavedCommand")
        # verify the created gcode
        # this line should switch to absolute coordinates in prep for move
        self.assertTrue(snapshotGcode.GcodeCommands[0] == "G90")
        # this line should switch back to absolute coordinates
        self.assertTrue(
            snapshotGcode.GcodeCommands[1] == "G1 X125.000 Y200.000 F7200")
        # move back to the return position
        self.assertTrue(
            snapshotGcode.GcodeCommands[2] == "G1 X10.000 Y10.000 F7200")
        # change to relative coordinates
        self.assertTrue(snapshotGcode.GcodeCommands[3] == "G91")
        # the saved command
        self.assertTrue(snapshotGcode.GcodeCommands[4] == "SavedCommand")
        # verify the return commands
        self.assertTrue(
            snapshotGcode.ReturnCommands()[0] == "G1 X10.000 Y10.000 F7200")
        self.assertTrue(snapshotGcode.ReturnCommands()[1] == "G91")
        self.assertTrue(snapshotGcode.ReturnCommands()[2] == "SavedCommand")
        # verify the snapshot commands
        self.assertTrue(snapshotGcode.SnapshotCommands()[0] == "G90")
        self.assertTrue(snapshotGcode.SnapshotCommands()[1] ==
                        "G1 X125.000 Y200.000 F7200")
        # verify the indexes of the generated gcode
        self.assertTrue(snapshotGcode.SnapshotIndex == 1)
        self.assertTrue(snapshotGcode.EndIndex() == 4)
        # verify the return coordinates
        self.assertTrue(snapshotGcode.ReturnX == 10)
        self.assertTrue(snapshotGcode.ReturnY == 10)
        self.assertTrue(snapshotGcode.ReturnZ == 200)
示例#7
0
    def test_GetSnapshotPosition_Absolute(self):
        """Test getting absolute snapshot positions for x and y"""
        # adjust the settings for absolute position and create the snapshot gcode generator
        self.Settings.CurrentStabilization().x_type = "fixed_coordinate"
        self.Settings.CurrentStabilization().x_fixed_coordinate = 10
        self.Settings.CurrentStabilization().y_type = "fixed_coordinate"
        self.Settings.CurrentStabilization().y_fixed_coordinate = 20
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())

        # get the coordinates and test
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 10 and coords["Y"] == 20)
        # get the coordinates and test
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 10 and coords["Y"] == 20)
        # get the coordinates and test
        coords = snapshotGcodeGenerator.GetSnapshotPosition(100, 100)
        self.assertTrue(coords["X"] == 10 and coords["Y"] == 20)
示例#8
0
    def test_GetSnapshotPosition_BedRelative(self):
        """Test getting bed relative snapshot positions for x and y"""
        # adjust the settings for absolute position and create the snapshot gcode generator
        self.Settings.current_stabilization().x_type = "relative"
        self.Settings.current_stabilization().x_relative = 0
        self.Settings.current_stabilization().y_type = "relative"
        self.Settings.current_stabilization().y_relative = 100
        snapshot_gcode_generator = SnapshotGcodeGenerator(
            self.Settings, self.create_octoprint_printer_profile())

        # get the coordinates and test
        coords = snapshot_gcode_generator.get_snapshot_position(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
        # get the coordinates and test
        coords = snapshot_gcode_generator.get_snapshot_position(1, 1)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
        # get the coordinates and test
        coords = snapshot_gcode_generator.get_snapshot_position(100, 100)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
示例#9
0
 def StartTimelapse(self, octoprintPrinter, octoprintPrinterProfile,
                    ffmpegPath, g90InfluencesExtruder):
     self._reset()
     self.HasSentInitialStatus = False
     self.OctoprintPrinter = octoprintPrinter
     self.OctoprintPrinterProfile = octoprintPrinterProfile
     self.FfMpegPath = ffmpegPath
     self.PrintStartTime = time.time()
     self.Snapshot = Snapshot(self.Settings.CurrentSnapshot())
     self.Gcode = SnapshotGcodeGenerator(self.Settings,
                                         octoprintPrinterProfile)
     self.Printer = Printer(self.Settings.CurrentPrinter())
     self.Rendering = Rendering(self.Settings.CurrentRendering())
     self.CaptureSnapshot = CaptureSnapshot(
         self.Settings, self.DataFolder, printStartTime=self.PrintStartTime)
     self.Position = Position(self.Settings, octoprintPrinterProfile,
                              g90InfluencesExtruder)
     self.State = TimelapseState.WaitingForTrigger
     self.IsTestMode = self.Settings.CurrentDebugProfile().is_test_mode
     self.Triggers.Create()
     # send an initial state message
     self._onTimelapseStart()
示例#10
0
    def test_GetSnapshotGcode_Relative_RelativeCoordinates_AbsoluteExtruder_ZhopTooHigh(
            self):
        """Test snapshot gcode with relative stabilization, relative coordinates, absolute extruder, z is too high to
        hop, no retraction """

        # test with relative coordinates, absolute extruder coordinates, z hop impossible (current z height will not
        # allow this since it puts things outside of the bounds) use relative coordinates for stabilizations
        self.Settings.current_stabilization().x_type = "relative"
        self.Settings.current_stabilization().x_relative = 50  # 125
        self.Settings.current_stabilization().y_type = "relative"
        self.Settings.current_stabilization().y_relative = 100  # 200
        self.Settings.current_snapshot().retract_before_move = False
        snapshot_gcode_generator = SnapshotGcodeGenerator(
            self.Settings, self.create_octoprint_printer_profile())
        # create
        snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
            10, 10, 200, 3600, True, False, self.Extruder, 0.5, "SavedCommand")
        # verify the created gcode
        self.assertEqual(snapshot_gcode.GcodeCommands[0], "G90")
        self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[2],
                         "G1 X125.000 Y200.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[3], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[4], "M114")
        self.assertEqual(snapshot_gcode.GcodeCommands[5], "G1 X10.000 Y10.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[6], "G91")
        self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 F3600")
        self.assertEqual(snapshot_gcode.GcodeCommands[8], "SAVEDCOMMAND")
        self.assertEqual(snapshot_gcode.GcodeCommands[9], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[10], "M114")

        # verify the indexes of the generated gcode
        self.assertTrue(snapshot_gcode.SnapshotIndex == 4)
        self.assertTrue(snapshot_gcode.end_index() == 10)
        # verify the return coordinates
        self.assertTrue(snapshot_gcode.ReturnX == 10)
        self.assertTrue(snapshot_gcode.ReturnY == 10)
        self.assertTrue(snapshot_gcode.ReturnZ == 200)
示例#11
0
 def test_GetSnapshotGcode_Fixed_AbsoluteCoordintes_ExtruderRelative(self):
     """Test snapshot gcode in absolute coordinate system with relative extruder and fixed coordinate
     stabilization """
     # adjust the settings for absolute position and create the snapshot gcode generator
     self.Settings.current_stabilization().x_type = "fixed_coordinate"
     self.Settings.current_stabilization().x_fixed_coordinate = 10
     self.Settings.current_stabilization().y_type = "fixed_coordinate"
     self.Settings.current_stabilization().y_fixed_coordinate = 20
     snapshot_gcode_generator = SnapshotGcodeGenerator(
         self.Settings, self.create_octoprint_printer_profile())
     self.Extruder.is_retracted = lambda: True
     snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
         0, 0, 0, 3600, False, True, self.Extruder, 0.5, "SavedCommand")
     # verify the created gcode
     self.assertEqual(snapshot_gcode.GcodeCommands[0], "G91")
     self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 F6000")
     self.assertEqual(snapshot_gcode.GcodeCommands[2], "G1 Z0.500")
     self.assertEqual(snapshot_gcode.GcodeCommands[3], "G90")
     self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 F6000")
     self.assertEqual(snapshot_gcode.GcodeCommands[5], "G1 X10.000 Y20.000")
     self.assertEqual(snapshot_gcode.GcodeCommands[6], "M400")
     self.assertEqual(snapshot_gcode.GcodeCommands[7], "M114")
     self.assertEqual(snapshot_gcode.GcodeCommands[8], "G1 X0.000 Y0.000")
     self.assertEqual(snapshot_gcode.GcodeCommands[9], "G91")
     self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 F6000")
     self.assertEqual(snapshot_gcode.GcodeCommands[11], "G1 Z-0.500")
     self.assertEqual(snapshot_gcode.GcodeCommands[12], "G90")
     self.assertEqual(snapshot_gcode.GcodeCommands[13], "G1 F3600")
     self.assertEqual(snapshot_gcode.GcodeCommands[14], "SAVEDCOMMAND")
     self.assertEqual(snapshot_gcode.GcodeCommands[15], "M400")
     self.assertEqual(snapshot_gcode.GcodeCommands[16], "M114")
     # verify the indexes of the generated gcode
     self.assertEqual(snapshot_gcode.SnapshotIndex, 7)
     self.assertEqual(snapshot_gcode.end_index(), 16)
     # verify the return coordinates
     self.assertEqual(snapshot_gcode.ReturnX, 0)
     self.assertEqual(snapshot_gcode.ReturnY, 0)
     self.assertEqual(snapshot_gcode.ReturnZ, 0)
示例#12
0
    def test_GetSnapshotGcode_Fixed_AbsoluteCoordintes_ExtruderRelative(self):
        """Test snapshot gcode in absolute coordinate system with relative extruder and fixed coordinate
        stabilization """
        # adjust the settings for absolute position and create the snapshot gcode generator
        self.Settings.current_stabilization().x_type = "fixed_coordinate"
        self.Settings.current_stabilization().x_fixed_coordinate = 10
        self.Settings.current_stabilization().y_type = "fixed_coordinate"
        self.Settings.current_stabilization().y_fixed_coordinate = 20
        snapshot_gcode_generator = SnapshotGcodeGenerator(
            self.Settings, self.create_octoprint_printer_profile())
        self.Extruder.is_retracted = lambda: True

        self.Position.update(Commands.parse("G90"))
        self.Position.update(Commands.parse("M83"))
        self.Position.update(Commands.parse("G28"))
        self.Position.update(Commands.parse("G0 X95 Y95 Z0.2 F3600"))
        parsed_command = Commands.parse("G0 X100 Y100")
        self.Position.update(parsed_command)
        snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
            self.Position, None, parsed_command)
        gcode_commands = snapshot_gcode.snapshot_gcode()

        # verify the created gcodegcode_commands
        self.assertEqual(gcode_commands[0], "G1 E-4.00000 F4800")
        self.assertEqual(gcode_commands[1], "G1 X10.000 Y20.000 F10800")
        self.assertEqual(gcode_commands[2], "G1 X100.000 Y100.000")
        self.assertEqual(gcode_commands[3], "G1 E4.00000 F3000")
        self.assertEqual(gcode_commands[4], "G1 F3600")
        self.assertEqual(gcode_commands[5], parsed_command.gcode)

        # verify the return coordinates
        self.assertEqual(snapshot_gcode.ReturnX, 100)
        self.assertEqual(snapshot_gcode.ReturnY, 100)
        self.assertEqual(snapshot_gcode.ReturnZ, 0.2)

        self.assertEqual(snapshot_gcode.X, 10)
        self.assertEqual(snapshot_gcode.Y, 20)
        self.assertEqual(snapshot_gcode.Z, None)
示例#13
0
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 = ""
示例#14
0
    def test_GetSnapshotPosition_AbsolutePath(self):
        """Test getting absolute path snapshot positions for x and y"""
        # adjust the settings for absolute position and create the snapshot gcode generator
        self.Settings.CurrentStabilization().x_type = "fixed_path"
        self.Settings.CurrentStabilization().x_fixed_path = "0,1,2,3,4,5"
        self.Settings.CurrentStabilization().y_type = "fixed_path"
        self.Settings.CurrentStabilization().y_fixed_path = "5,4,3,2,1,0"

        # test with no loop
        self.Settings.CurrentStabilization().x_fixed_path_loop = False
        self.Settings.CurrentStabilization().x_fixed_path_invert_loop = False
        self.Settings.CurrentStabilization().y_fixed_path_loop = False
        self.Settings.CurrentStabilization().y_fixed_path_invert_loop = False
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 5)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 1 and coords["Y"] == 4)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 0)
        self.assertTrue(coords["X"] == 2 and coords["Y"] == 3)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 3 and coords["Y"] == 2)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 1)
        self.assertTrue(coords["X"] == 4 and coords["Y"] == 1)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 5 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 5 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 5 and coords["Y"] == 0)

        # test with loop, no invert
        self.Settings.CurrentStabilization().x_fixed_path_loop = True
        self.Settings.CurrentStabilization().x_fixed_path_invert_loop = False
        self.Settings.CurrentStabilization().y_fixed_path_loop = True
        self.Settings.CurrentStabilization().y_fixed_path_invert_loop = False
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 5)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 1 and coords["Y"] == 4)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 0)
        self.assertTrue(coords["X"] == 2 and coords["Y"] == 3)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 3 and coords["Y"] == 2)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 1)
        self.assertTrue(coords["X"] == 4 and coords["Y"] == 1)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 5 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 5)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 1 and coords["Y"] == 4)

        # test with loop and invert
        self.Settings.CurrentStabilization().x_fixed_path_loop = True
        self.Settings.CurrentStabilization().x_fixed_path_invert_loop = True
        self.Settings.CurrentStabilization().y_fixed_path_loop = True
        self.Settings.CurrentStabilization().y_fixed_path_invert_loop = True
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 5)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 1 and coords["Y"] == 4)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 0)
        self.assertTrue(coords["X"] == 2 and coords["Y"] == 3)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 3 and coords["Y"] == 2)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 1)
        self.assertTrue(coords["X"] == 4 and coords["Y"] == 1)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 5 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 4 and coords["Y"] == 1)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 3 and coords["Y"] == 2)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 2 and coords["Y"] == 3)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 1 and coords["Y"] == 4)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 5)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 1 and coords["Y"] == 4)
示例#15
0
    def test_GetSnapshotGcode_FixedPath_RelativeCoordinates_ExtruderAbsolute_ZHop_AlreadyRetracted(
            self):
        # test with relative paths, absolute extruder coordinates, retract and z hop
        # use relative coordinates for stabilizations
        self.Settings.CurrentStabilization().x_type = "fixed_path"
        self.Settings.CurrentStabilization().x_fixed_path = "50,100"  #125,250
        self.Settings.CurrentStabilization().x_fixed_path_loop = False
        self.Settings.CurrentStabilization().x_fixed_path_invert_loop = False
        self.Settings.CurrentStabilization().y_type = "fixed_path"
        self.Settings.CurrentStabilization().y_fixed_path = "50,100"  #100,200
        self.Settings.CurrentStabilization().y_fixed_path_loop = False
        self.Settings.CurrentStabilization().y_fixed_path_invert_loop = False
        self.Settings.CurrentSnapshot().retract_before_move = True
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        self.Extruder.IsRetracted = True
        snapshotGcode = snapshotGcodeGenerator.CreateSnapshotGcode(
            100, 50, 0, True, False, self.Extruder, "SavedCommand")
        # verify the created gcode
        # this line should switch to absolute coordinates in prep for move
        self.assertTrue(snapshotGcode.GcodeCommands[0] == "G1 Z0.500 F7200")
        self.assertTrue(snapshotGcode.GcodeCommands[1] == "G90")
        self.assertTrue(
            snapshotGcode.GcodeCommands[2] == "G1 X50.000 Y50.000 F7200")
        # move back to the return position
        self.assertTrue(
            snapshotGcode.GcodeCommands[3] == "G1 X100.000 Y50.000 F7200")
        # change to relative coordinates
        self.assertTrue(snapshotGcode.GcodeCommands[4] == "G91")
        self.assertTrue(snapshotGcode.GcodeCommands[5] == "G1 Z-0.500 F7200")
        # the saved command
        self.assertTrue(snapshotGcode.GcodeCommands[6] == "SavedCommand")
        # verify the return commands
        self.assertTrue(
            snapshotGcode.ReturnCommands()[0] == "G1 X100.000 Y50.000 F7200")
        self.assertTrue(snapshotGcode.ReturnCommands()[1] == "G91")
        self.assertTrue(
            snapshotGcode.ReturnCommands()[2] == "G1 Z-0.500 F7200")
        self.assertTrue(snapshotGcode.ReturnCommands()[3] == "SavedCommand")
        # verify the snapshot commands
        self.assertTrue(
            snapshotGcode.SnapshotCommands()[0] == "G1 Z0.500 F7200")
        self.assertTrue(snapshotGcode.SnapshotCommands()[1] == "G90")
        self.assertTrue(
            snapshotGcode.SnapshotCommands()[2] == "G1 X50.000 Y50.000 F7200")

        # verify the indexes of the generated gcode
        self.assertTrue(snapshotGcode.SnapshotIndex == 2)
        self.assertTrue(snapshotGcode.EndIndex() == 6)
        # verify the return coordinates
        self.assertTrue(snapshotGcode.ReturnX == 100)
        self.assertTrue(snapshotGcode.ReturnY == 50)
        self.assertTrue(snapshotGcode.ReturnZ == 0)

        # Get the next coordinate in the path
        snapshotGcode = snapshotGcodeGenerator.CreateSnapshotGcode(
            101, 51, 0, True, False, self.Extruder, "SavedCommand")
        # verify the created gcode
        # this line should switch to absolute coordinates in prep for move
        self.assertTrue(snapshotGcode.GcodeCommands[0] == "G1 Z0.500 F7200")
        self.assertTrue(snapshotGcode.GcodeCommands[1] == "G90")
        self.assertTrue(
            snapshotGcode.GcodeCommands[2] == "G1 X100.000 Y100.000 F7200")
        # move back to the return position
        self.assertTrue(
            snapshotGcode.GcodeCommands[3] == "G1 X101.000 Y51.000 F7200")
        # change to relative coordinates
        self.assertTrue(snapshotGcode.GcodeCommands[4] == "G91")
        self.assertTrue(snapshotGcode.GcodeCommands[5] == "G1 Z-0.500 F7200")
        # the saved command
        self.assertTrue(snapshotGcode.GcodeCommands[6] == "SavedCommand")
        # verify the return commands
        self.assertTrue(
            snapshotGcode.ReturnCommands()[0] == "G1 X101.000 Y51.000 F7200")
        self.assertTrue(snapshotGcode.ReturnCommands()[1] == "G91")
        self.assertTrue(
            snapshotGcode.ReturnCommands()[2] == "G1 Z-0.500 F7200")
        self.assertTrue(snapshotGcode.ReturnCommands()[3] == "SavedCommand")
        # verify the snapshot commands
        self.assertTrue(
            snapshotGcode.SnapshotCommands()[0] == "G1 Z0.500 F7200")
        self.assertTrue(snapshotGcode.SnapshotCommands()[1] == "G90")
        self.assertTrue(snapshotGcode.SnapshotCommands()[2] ==
                        "G1 X100.000 Y100.000 F7200")
        # verify the indexes of the generated gcode
        self.assertTrue(snapshotGcode.SnapshotIndex == 2)
        self.assertTrue(snapshotGcode.EndIndex() == 6)
        # verify the return coordinates
        self.assertTrue(snapshotGcode.ReturnX == 101)
        self.assertTrue(snapshotGcode.ReturnY == 51)
        self.assertTrue(snapshotGcode.ReturnZ == 0)
        # test the second coordinate in the path
        snapshotGcode = snapshotGcodeGenerator.CreateSnapshotGcode(
            10, 10, 10, True, False, self.Extruder, "SavedCommand")
示例#16
0
class Timelapse(object):
    def __init__(self,
                 octolapseSettings,
                 dataFolder,
                 timelapseFolder,
                 onMovieRendering=None,
                 onMovieDone=None,
                 onMovieFailed=None):
        # config variables - These don't change even after a reset
        self.Settings = octolapseSettings
        self.DataFolder = dataFolder
        self.DefaultTimelapseDirectory = timelapseFolder
        self.OnMovieRenderingCallback = onMovieRendering
        self.OnMovieDoneCallback = onMovieDone
        self.OnMovieFailedCallback = onMovieFailed
        self.Responses = Responses(
        )  # Used to decode responses from the 3d printer
        self.Commands = Commands()  # used to parse and generate gcode

        # 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

        # State tracking variables
        self.Reset()

    def StartTimelapse(self, octoprintPrinter, octoprintPrinterProfile,
                       ffmpegPath, g90InfluencesExtruder):
        self.Reset()

        self.OctoprintPrinter = octoprintPrinter
        self.OctoprintPrinterProfile = octoprintPrinterProfile
        self.FfMpegPath = ffmpegPath
        self.PrintStartTime = time.time()
        self.Snapshot = Snapshot(self.Settings.CurrentSnapshot())
        self.Gcode = SnapshotGcodeGenerator(self.Settings,
                                            octoprintPrinterProfile)
        self.Printer = Printer(self.Settings.CurrentPrinter())
        self.Rendering = Rendering(self.Settings.CurrentRendering())
        self.CaptureSnapshot = CaptureSnapshot(
            self.Settings, self.DataFolder, printStartTime=self.PrintStartTime)
        self.Position = Position(self.Settings, octoprintPrinterProfile,
                                 g90InfluencesExtruder)
        self.State = TimelapseState.WaitingForTrigger

        self.IsTestMode = self.Settings.CurrentDebugProfile().is_test_mode
        # create the triggers
        # If the gcode trigger is enabled, add it
        if (self.Snapshot.gcode_trigger_enabled):
            #Add the trigger to the list
            self.Triggers.append(GcodeTrigger(self.Settings))
        # If the layer trigger is enabled, add it
        if (self.Snapshot.layer_trigger_enabled):
            self.Triggers.append(LayerTrigger(self.Settings))
        # If the layer trigger is enabled, add it
        if (self.Snapshot.timer_trigger_enabled):
            self.Triggers.append(TimerTrigger(self.Settings))

    def Reset(self):
        self.State = TimelapseState.Idle
        self.Triggers = []
        self.CommandIndex = -1
        self.SnapshotCount = 0
        self.PrintStartTime = None
        self.SnapshotGcodes = None
        self.SavedCommand = None
        self.PositionRequestAttempts = 0
        self.IsTestMode = False

    def ResetSnapshot(self):
        self.State = TimelapseState.WaitingForTrigger
        self.CommandIndex = -1
        self.SnapshotGcodes = None
        self.SavedCommand = None
        self.PositionRequestAttempts = 0

    def PrintPaused(self):
        if (self.State == TimelapseState.Idle):
            return
        elif (self.State == TimelapseState.WaitingForTrigger):
            self.Settings.CurrentDebugProfile().LogPrintStateChange(
                "Print Paused.")
            if (self.Triggers is not None):
                for trigger in self.Triggers:
                    if (type(trigger) == TimerTrigger):
                        trigger.Pause()

    def IsTimelapseActive(self):
        if (self.State == TimelapseState.Idle or len(self.Triggers) < 1):
            return False
        return True

    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 GcodeQueuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args,
                     **kwargs):
        self.Settings.CurrentDebugProfile().LogSentGcode(
            "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()
        self.Position.Update(cmd)
        isSnapshotGcodeCommand = self.IsSnapshotCommand(cmd)

        if (self.State == TimelapseState.WaitingForTrigger):
            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
                # get the start timelapse gcide
                startTimelapseGcode = self.Gcode.CreateSnapshotStartGcode(
                    self.Position.Z(), self.Position.F(),
                    self.Position.IsRelative(),
                    self.Position.IsExtruderRelative(), self.Position.Extruder)
                if (len(startTimelapseGcode.GcodeCommands) > 0):
                    for command in startTimelapseGcode.GcodeCommands:
                        self.Position.Update(command)
                    # Pausing the print here will immediately trigger an M400 and a location request
                    self.OctoprintPrinter.pause_print()
                    # This gcode will send after the pause
                    return startTimelapseGcode.GcodeCommands
                else:
                    return None,
        elif (self.State > TimelapseState.WaitingForTrigger
              and self.State < TimelapseState.SendingReturnGcode):
            # 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):
                return 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.
            return None,

        return self.ReturnGcodeCommandToOctoprint(cmd)

    def GcodeQueuing_IsTriggering(self, cmd, isSnapshotGcodeCommand):
        currentTrigger = self.IsTriggering(cmd)
        if (currentTrigger is not None):  #We're triggering
            self.Settings.CurrentDebugProfile().LogTriggering(
                "A snapshot is triggering")

            return True
        elif (self.IsTriggerWaiting(cmd)):
            self.Settings.CurrentDebugProfile().LogTriggerWaitState(
                "Trigger is Waiting On Extruder.")
        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 IsSnapshotCommand(self, command):
        commandName = GetGcodeFromString(command)
        snapshotCommandName = GetGcodeFromString(self.Printer.snapshot_command)
        return commandName == snapshotCommandName

    def IsTriggering(self, cmd):
        # make sure we're in a state that could want to check for triggers
        if (not self.State == TimelapseState.WaitingForTrigger):
            return None
        # check the command to see if it's a debug assrt

        # Loop through all of the active currentTriggers
        for currentTrigger in self.Triggers:
            # determine what type the current trigger is and update appropriately
            if (isinstance(currentTrigger, GcodeTrigger)):
                currentTrigger.Update(self.Position, cmd)
            elif (isinstance(currentTrigger, TimerTrigger)):
                currentTrigger.Update(self.Position)
            elif (isinstance(currentTrigger, LayerTrigger)):
                currentTrigger.Update(self.Position)
            # see if the current trigger is triggering, indicting that a snapshot should be taken
            if (currentTrigger.IsTriggered):
                # Make sure there are no position errors (unknown position, out of bounds, etc)
                if (not self.Position.HasPositionError):
                    #Triggering!
                    return currentTrigger
                else:
                    self.Settings.CurrentDebugProfile().LogTriggering(
                        "A position error prevented a snapshot trigger!")
        return None

    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
        for currentTrigger in self.Triggers:
            if (currentTrigger.IsWaiting):
                return True
        return False

    def PositionReceived(self, payload):
        x = payload["x"]
        y = payload["y"]
        z = payload["z"]
        e = payload["e"]

        if (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"

    def PositionReceived_Return(self, x, y, z, e):
        #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().LogSnapshotPositionReturn(
                "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()
        extruder = self.Position.Extruder
        self.SnapshotGcodes = self.Gcode.CreateSnapshotGcode(
            x, y, z, savedCommand=self.SavedCommand)
        # make sure we acutally received gcode
        if (self.SnapshotGcodes is None):
            self.Settings.CurrentDebugProfile().LogSnapshotGcode(
                "No snapshot gcode was created for this snapshot.  Aborting this snapshot."
            )
            self.EndSnapshot()
            return False, "Error - No Snapshot Gcode"

        self.State = TimelapseState.SendingSnapshotGcode
        # send our commands to the printer
        self.OctoprintPrinter.commands(self.SnapshotGcodes.SnapshotCommands())

    def PositionReceived_Snapshot(self, x, y, z, e):
        # 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.SnapshotGcodes.X, self.SnapshotGcodes.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().LogSnapshotPosition(
                "The snapshot position is incorrect.")

        elif (not self.Position.IsAtCurrentPosition(
                self.SnapshotGcodes.X,
                self.SnapshotGcodes.Y,
                None,
                applyOffset=False)):  # our snapshot gcode will NOT be offset
            self.Settings.CurrentDebugProfile().LogSnapshotPosition(
                "The snapshot position matched the expected position (x:{0} y:{1}), but the SnapshotGcode object coordinates don't match the expected position."
                .format(x, y))
        self.Settings.CurrentDebugProfile().LogSnapshotPositionReturn(
            "The snapshot position is correct, taking snapshot.")
        self.State = TimelapseState.TakingSnapshot
        self.TakeSnapshot()

    def PositionReceived_ResumePrint(self, x, y, z, e):
        if (not self.Position.IsAtCurrentPosition(x, y, None)):
            self.Settings.CurrentDebugProfile().LogSnapshotPositionResumePrint(
                "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.ResetSnapshot()
        self.OctoprintPrinter.resume_print()

    def EndSnapshot(self):
        # Cleans up the variables and resumes the print once the snapshot is finished, and the extruder is in the proper position
        # reset the snapshot variables
        self.ResetSnapshot()

    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:
            a = sys.exc_info(
            )  # Info about unknown error that caused exception.
            errorMessage = "    {0}".format(a)
            b = [str(p) for p in a]
            errorMessage += "\n    {0}".format(b)
            self.Settings.CurrentDebugProfile().LogSnapshotSave(
                'Unknown error detected:{0}'.format(errorMessage))

    def OnSnapshotSuccess(self, *args, **kwargs):
        # Increment the number of snapshots received
        self.SnapshotCount += 1
        # get the save path
        snapshotInfo = args[0]
        # get the current file name
        newSnapshotName = snapshotInfo.GetFullPath(self.SnapshotCount)
        self.Settings.CurrentDebugProfile().LogSnapshotSave(
            "Renaming snapshot {0} to {1}".format(
                snapshotInfo.GetTempFullPath(), newSnapshotName))
        # create the output directory if it does not exist
        try:
            path = os.path.dirname(newSnapshotName)
            if not os.path.exists(path):
                os.makedirs(path)
        except:
            type = sys.exc_info()[0]
            value = sys.exc_info()[1]
            self.Settings.CurrentDebugProfile().LogSnapshotSave(
                "An exception was thrown when trying to create a directory for the downloaded snapshot: {0}  , ExceptionType:{1}, Exception Value:{2}"
                .format(os.path.dirname(dir), type, value))
            return

        # rename the current file
        try:

            shutil.move(snapshotInfo.GetTempFullPath(), newSnapshotName)
        except:
            type = sys.exc_info()[0]
            value = sys.exc_info()[1]
            self.Settings.CurrentDebugProfile().LogSnapshotSave(
                "Could rename the snapshot {0} to {1}!   Error Type:{2}, Details:{3}"
                .format(snapshotInfo.GetTempFullPath(), newSnapshotName, type,
                        value))

    def OnSnapshotFail(self, *args, **kwargs):
        reason = args[0]
        self.Settings.CurrentDebugProfile().LogSnapshotDownload(
            "Failed to download the snapshot.  Reason:{0}".format(reason))

    def OnSnapshotComplete(self, *args, **kwargs):
        self.Settings.CurrentDebugProfile().LogSnapshotDownload(
            "Snapshot Completed.")
        self.SendReturnCommands()

    def SendReturnCommands(self):
        # Expand the current command to include the return commands
        snapshotCommands = self.SnapshotGcodes.ReturnCommands()
        # set the state so that the final received position will trigger a resume.
        self.State = TimelapseState.SendingReturnGcode
        self.OctoprintPrinter.commands(snapshotCommands)

    # RENDERING Functions and Events
    def EndTimelapse(self):
        if (self.State != TimelapseState.Idle):
            self.RenderTimelapse()
            self.Reset()

    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")
            timelapseRenderJob = Render(
                self.Settings,
                self.Snapshot,
                self.Rendering,
                self.DataFolder,
                self.DefaultTimelapseDirectory,
                self.FfMpegPath,
                1,
                onRenderStart=self.OnRenderStart,
                onRenderFail=self.OnRenderFail,
                onRenderSuccess=self.OnRenderSuccess,
                onRenderComplete=self.OnRenderComplete,
                onAfterSyncFail=self.OnSynchronizeRenderingFail,
                onAfterSycnSuccess=self.OnSynchronizeRenderingComplete,
                onComplete=self.OnRenderTimelapseJobComplete)
            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]
        #Notify Octoprint
        if (willSync):
            payload = dict(gcode="unknown",
                           movie=finalFilename,
                           movie_basename=baseFileName,
                           movie_prefix="from Octolapse")
        else:
            payload = dict(
                gcode="unknown",
                movie=finalFilename,
                movie_basename=baseFileName,
                movie_prefix=
                "from Octolapse.  This timelapse will NOT be synchronized with the default timelapse module.  Please see the Octolapse advanced rendering settings for details."
            )
        self.OnMovieRendering(payload)

    def OnRenderFail(self, *args, **kwargs):
        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)
        self.OnMovieFailed(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):
        finalFileName = args[0]
        synchronize = args[1]
        self.Settings.CurrentDebugProfile().LogRenderComplete(
            "Completed rendering the timelapse.")

    # Synchronize renderings with the default plugin
    def OnSynchronizeRenderingComplete(self, *args, **kwargs):
        self.Settings.CurrentDebugProfile().LogRenderComplete(
            "Synchronization with octoprint completed successfully.")

    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,
            movie_prefix=
            "from Octolapse.  Synchronization between octolapse and octoprint failed.  Your timelapse is likely within the octolapse data folder.  A file browser will be added in a future version (hopefully).",
            returncode=0,
            reason="See the octolapse log for details.")
        self.OnMovieFailed(payload)

    def OnRenderTimelapseJobComplete(self, *args, **kwargs):
        finalFileName = args[0]
        baseFileName = args[1]
        synchronize = args[2]
        self.Settings.CurrentDebugProfile().LogRenderComplete(
            "Completed rendering, ending timelapse.")
        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(gcode="unknown",
                       movie=finalFileName,
                       movie_basename=baseFileName,
                       movie_prefix=moviePrefix)
        self.OnMovieDone(payload)

    def OnMovieRendering(self, payload):
        """Called when a timelapse has started being rendered.  Calls any callbacks onMovieRendering callback set in the constructor."""
        if (self.OnMovieRenderingCallback is not None):
            self.OnMovieRenderingCallback(payload)

    def OnMovieDone(self, payload):
        """Called after a timelapse has been rendered.  Calls any callbacks onMovieRendered callback set in the constructor."""
        if (self.OnMovieDoneCallback is not None):
            self.OnMovieDoneCallback(payload)

    def OnMovieFailed(self, payload):
        """Called after a timelapse rendering attempt has failed.  Calls any callbacks onMovieFailed callback set in the constructor."""
        if (self.OnMovieFailedCallback is not None):
            self.OnMovieFailedCallback(payload)
示例#17
0
    def test_GetSnapshotGcode_FixedPath_RelativeCoordinates_ExtruderAbsolute_ZHop_AlreadyRetracted(self):
        # test with relative paths, absolute extruder coordinates, retract and z hop
        # use relative coordinates for stabilizations
        self.Settings.current_stabilization().x_type = "fixed_path"
        self.Settings.current_stabilization().x_fixed_path = "50,100"  # 125,250
        self.Settings.current_stabilization().x_fixed_path_loop = False
        self.Settings.current_stabilization().x_fixed_path_invert_loop = False
        self.Settings.current_stabilization().y_type = "fixed_path"
        self.Settings.current_stabilization().y_fixed_path = "50,100"  # 100,200
        self.Settings.current_stabilization().y_fixed_path_loop = False
        self.Settings.current_stabilization().y_fixed_path_invert_loop = False
        self.Settings.current_snapshot().retract_before_move = True
        snapshot_gcode_generator = SnapshotGcodeGenerator(
            self.Settings, self.create_octoprint_printer_profile())
        self.Extruder.is_retracted = lambda: True
        snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
            100, 50, 0, 3600, True, False, self.Extruder, 0.5, "SavedCommand")

        # verify the created gcode
        self.assertEqual(snapshot_gcode.GcodeCommands[0], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 Z0.500")
        self.assertEqual(snapshot_gcode.GcodeCommands[2], "G90")
        self.assertEqual(snapshot_gcode.GcodeCommands[3], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 X50.000 Y50.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[5], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[6], "M114")
        self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 X100.000 Y50.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[8], "G91")
        self.assertEqual(snapshot_gcode.GcodeCommands[9], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 Z-0.500")
        self.assertEqual(snapshot_gcode.GcodeCommands[11], "G1 F3600")
        self.assertEqual(snapshot_gcode.GcodeCommands[12], "SAVEDCOMMAND")
        self.assertEqual(snapshot_gcode.GcodeCommands[13], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[14], "M114")

        # verify the indexes of the generated gcode
        self.assertEqual(snapshot_gcode.SnapshotIndex, 6)
        self.assertEqual(snapshot_gcode.end_index(), 14)
        # verify the return coordinates
        self.assertEqual(snapshot_gcode.ReturnX, 100)
        self.assertEqual(snapshot_gcode.ReturnY, 50)
        self.assertEqual(snapshot_gcode.ReturnZ, 0)

        # Get the next coordinate in the path
        snapshot_gcode = snapshot_gcode_generator.create_snapshot_gcode(
            101, 51, 0, 3600, True, False, self.Extruder, 0.5, "SavedCommand")
        # verify the created gcode
        self.assertEqual(snapshot_gcode.GcodeCommands[0], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[1], "G1 Z0.500")
        self.assertEqual(snapshot_gcode.GcodeCommands[2], "G90")
        self.assertEqual(snapshot_gcode.GcodeCommands[3], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[4], "G1 X100.000 Y100.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[5], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[6], "M114")
        self.assertEqual(snapshot_gcode.GcodeCommands[7], "G1 X101.000 Y51.000")
        self.assertEqual(snapshot_gcode.GcodeCommands[8], "G91")
        self.assertEqual(snapshot_gcode.GcodeCommands[9], "G1 F6000")
        self.assertEqual(snapshot_gcode.GcodeCommands[10], "G1 Z-0.500")
        self.assertEqual(snapshot_gcode.GcodeCommands[11], "G1 F3600")
        self.assertEqual(snapshot_gcode.GcodeCommands[12], "SAVEDCOMMAND")
        self.assertEqual(snapshot_gcode.GcodeCommands[13], "M400")
        self.assertEqual(snapshot_gcode.GcodeCommands[14], "M114")

        # verify the indexes of the generated gcode
        self.assertEqual(snapshot_gcode.SnapshotIndex, 6)
        self.assertEqual(snapshot_gcode.end_index(), 14)
        # verify the return coordinates
        self.assertEqual(snapshot_gcode.ReturnX, 101)
        self.assertEqual(snapshot_gcode.ReturnY, 51)
        self.assertEqual(snapshot_gcode.ReturnZ, 0)
示例#18
0
class Timelapse(object):

    def __init__(
            self, settings, octoprint_printer, data_folder, timelapse_folder,
            on_print_started=None, on_print_start_failed=None,
            on_snapshot_start=None, on_snapshot_end=None,
            on_render_start=None, on_render_end=None,
            on_timelapse_stopping=None, on_timelapse_stopped=None,
            on_state_changed=None, on_timelapse_start=None, on_timelapse_end=None,
            on_snapshot_position_error=None, on_position_error=None, on_plugin_message_sent=None):
        # config variables - These don't change even after a reset
        self.DataFolder = data_folder
        self.Settings = settings  # type: OctolapseSettings
        self.OctoprintPrinter = octoprint_printer
        self.DefaultTimelapseDirectory = timelapse_folder
        self.OnPrintStartCallback = on_print_started
        self.OnPrintStartFailedCallback = on_print_start_failed
        self.OnRenderStartCallback = on_render_start
        self.OnRenderEndCallback = on_render_end
        self.OnSnapshotStartCallback = on_snapshot_start
        self.OnSnapshotCompleteCallback = on_snapshot_end
        self.TimelapseStoppingCallback = on_timelapse_stopping
        self.TimelapseStoppedCallback = on_timelapse_stopped
        self.OnStateChangedCallback = on_state_changed
        self.OnTimelapseStartCallback = on_timelapse_start
        self.OnTimelapseEndCallback = on_timelapse_end
        self.OnSnapshotPositionErrorCallback = on_snapshot_position_error
        self.OnPositionErrorCallback = on_position_error
        self.OnPluginMessageSentCallback = on_plugin_message_sent
        self.Commands = Commands()  # used to parse and generate gcode
        self.Triggers = None
        self.PrintEndStatus = "Unknown"
        self.LastStateChangeMessageTime = None
        self.StateChangeMessageThread = None
        # Settings that may be different after StartTimelapse is called

        self.OctoprintPrinterProfile = None
        self.PrintStartTime = None
        self.FfMpegPath = None
        self.Snapshot = None
        self.Gcode = None
        self.Printer = None
        self.CaptureSnapshot = None
        self.Position = None
        self.Rendering = None
        self.State = TimelapseState.Idle
        self.IsTestMode = False
        # State Tracking that should only be reset when starting a timelapse
        self.SnapshotCount = 0

        self.HasBeenStopped = False
        self.TimelapseStopRequested = False
        self.SavedCommand = None
        self.SecondsAddedByOctolapse = 0
        # State tracking variables
        self.RequiresLocationDetectionAfterHome = False

        # fetch position private variables
        self._position_payload = None
        self._position_timeout_long = 600.0
        self._position_timeout_short = 10.0
        self._position_signal = threading.Event()
        self._position_signal.set()

        # get snapshot async private variables
        self._snapshot_success = False
        # It shouldn't take more than 5 seconds to take a snapshot!
        self._snapshot_timeout = 5.0
        self._snapshot_signal = threading.Event()
        self._snapshot_signal.set()
        self._most_recent_snapshot_payload = None

        self.CurrentProfiles = {}
        self.CurrentFileLine = 0

        # snapshot thread queue
        self._snapshot_task_queue = Queue(maxsize=1)
        self._rendering_task_queue = Queue(maxsize=1)
        self._reset()

    def start_timelapse(
            self, settings, octoprint_printer_profile, ffmpeg_path, g90_influences_extruder):
        # we must supply the settings first!  Else reset won't work properly.
        self._reset()
        # in case the settings have been destroyed and recreated
        self.Settings = settings
        # time tracking - how much time did we add to the print?
        self.SnapshotCount = 0
        self.SecondsAddedByOctolapse = 0
        self.RequiresLocationDetectionAfterHome = False
        self.OctoprintPrinterProfile = octoprint_printer_profile
        self.FfMpegPath = ffmpeg_path
        self.PrintStartTime = time.time()
        self.Snapshot = Snapshot(self.Settings.current_snapshot())
        self.Gcode = SnapshotGcodeGenerator(
            self.Settings, octoprint_printer_profile)
        self.Printer = Printer(self.Settings.current_printer())
        self.Rendering = Rendering(self.Settings.current_rendering())
        self.CaptureSnapshot = CaptureSnapshot(
            self.Settings, self.DataFolder, print_start_time=self.PrintStartTime)
        self.Position = Position(
            self.Settings, octoprint_printer_profile, g90_influences_extruder)
        self.State = TimelapseState.WaitingForTrigger
        self.IsTestMode = self.Settings.current_debug_profile().is_test_mode
        self.Triggers = Triggers(self.Settings)
        self.Triggers.create()

        # take a snapshot of the current settings for use in the Octolapse Tab
        self.CurrentProfiles = self.Settings.get_profiles_dict()

        # send an initial state message
        self._on_timelapse_start()

    def on_position_received(self, payload):
        if self.State != TimelapseState.Idle:
            self._position_payload = payload
            self._position_signal.set()

    def send_snapshot_gcode_array(self, gcode_array):
        self.OctoprintPrinter.commands(gcode_array, tags={"snapshot_gcode"})

    def get_position_async(self, start_gcode=None, timeout=None):
        if timeout is None:
            timeout = self._position_timeout_long

        self.Settings.current_debug_profile().log_print_state_change("Octolapse is requesting a position.")

        # Warning, we can only request one position at a time!
        if self._position_signal.is_set():
            self._position_signal.clear()

            # build the staret commands
            commands_to_send = ["M400", "M114"]
            # send any code that is to be run before the position request
            if start_gcode is not None and len(start_gcode) > 0:
                commands_to_send = start_gcode + commands_to_send

            self.send_snapshot_gcode_array(commands_to_send)

        event_is_set = self._position_signal.wait(timeout)

        if not event_is_set:
            # we ran into a timeout while waiting for a fresh position
            # set the position signal
            self._snapshot_signal.set()
            self.Settings.current_debug_profile().log_warning(
                "Warning:  A timeout occurred while requesting the current position!."
            )

            return None

        return self._position_payload

    def _on_snapshot_success(self, *args, **kwargs):
        # Increment the number of snapshots received
        self.SnapshotCount += 1
        self._snapshot_success = True
        self._snapshot_signal.set()

    def _on_snapshot_fail(self, *args, **kwargs):
        reason = args[0]
        message = "Failed to download the snapshot.  Reason: {0}".format(
            reason)

        self.Settings.current_debug_profile().log_snapshot_download(message)
        self._snapshot_success = False
        self.SnapshotError = message
        self._snapshot_signal.set()

    def _on_snapshot_complete(self, *args, **kwargs):
        self.Settings.current_debug_profile().log_snapshot_download("Snapshot download complete.")

    def _take_snapshot_async(self):
        snapshot_async_payload = {
            "success": False,
            "error": "Waiting on thread to signal, aborting"
        }

        if self._snapshot_signal.is_set():
            # only clear signal and send a new M114 if we haven't already done that from another thread
            self._snapshot_signal.clear()
            # start the snapshot
            self.Settings.current_debug_profile().log_snapshot_download("Taking a snapshot.")

            snapshot_guid = str(uuid.uuid4())
            snapshot_job = self.CaptureSnapshot.create_snapshot_job(
                utility.get_currently_printing_filename(self.OctoprintPrinter),
                self.SnapshotCount,
                snapshot_guid,
                self._snapshot_task_queue,
                on_success=self._on_snapshot_success,
                on_fail=self._on_snapshot_fail,
                on_complete=self._on_snapshot_complete
            )
            self._snapshot_task_queue.put(snapshot_guid)
            snapshot_thread = threading.Thread(target=snapshot_job)
            snapshot_thread.daemon = True
            snapshot_thread.start()

        event_is_set = self._snapshot_signal.wait(self._snapshot_timeout)
        if not event_is_set:
            # we ran into a timeout while waiting for a fresh position
            snapshot_async_payload["success"] = False
            snapshot_async_payload["error"] = \
                "Snapshot timed out in {0} seconds.".format(self._snapshot_timeout)
            self._snapshot_signal.set()
        else:
            snapshot_async_payload["success"] = True

        return snapshot_async_payload

    def _take_timelapse_snapshot(
        self, trigger, command_string, cmd, parameters, triggering_command_position, triggering_extruder_position
    ):
        timelapse_snapshot_payload = {
            "snapshot_position": None,
            "return_position": None,
            "snapshot_gcode": None,
            "snapshot_payload": None,
            "current_snapshot_time": 0,
            "total_snapshot_time": 0,
            "success": False,
            "error": ""
        }
        try:

            show_real_snapshot_time = self.Settings.show_real_snapshot_time
            # create the GCode for the timelapse and store it
            snapshot_gcode = self.Gcode.create_snapshot_gcode(
                self.Position,
                trigger,
                command_string,
                cmd,
                parameters,
                triggering_command_position,
                triggering_extruder_position
            )
            # save the gcode fo the payload
            timelapse_snapshot_payload["snapshot_gcode"] = snapshot_gcode
            if snapshot_gcode is None:
                self.Settings.current_debug_profile().log_warning(
                    "No snapshot gcode was generated."
                )
                return timelapse_snapshot_payload

            assert (isinstance(snapshot_gcode, SnapshotGcode))

            if not show_real_snapshot_time:
                gcodes_to_send = snapshot_gcode.StartGcode + snapshot_gcode.SnapshotCommands
                if len(gcodes_to_send) > 0:
                    self.Settings.current_debug_profile().log_snapshot_gcode(
                        "Sending snapshot start gcode and snapshot commands.")
                    snapshot_position = self.get_position_async(
                        start_gcode=snapshot_gcode.StartGcode + snapshot_gcode.SnapshotCommands
                    )
            else:
                self.Settings.current_debug_profile().log_snapshot_gcode(
                    "Sending snapshot start gcode.")
                # send start commands and zhop/retract if they exist
                if len(snapshot_gcode.StartGcode) > 0:
                    start_position = self.get_position_async(start_gcode=snapshot_gcode.StartGcode)
                    # Todo: Handle start_position = None

                # park the printhead in the snapshot position and wait for the movement to complete
                snapshot_start_time = time.time()
                if len(snapshot_gcode.SnapshotCommands) > 0:
                    self.Settings.current_debug_profile().log_snapshot_gcode("Sending snapshot commands.")
                    snapshot_position = self.get_position_async(
                        start_gcode=snapshot_gcode.SnapshotCommands, timeout=self._position_timeout_short
                    )

            # record the snapshot position
            timelapse_snapshot_payload["snapshot_position"] = snapshot_position
            # by now we should be ready to take a snapshot
            snapshot_async_payload = self._take_snapshot_async()
            timelapse_snapshot_payload["snapshot_payload"] = snapshot_async_payload

            if not show_real_snapshot_time:
                # return the printhead to the start position
                gcode_to_send = snapshot_gcode.ReturnCommands + snapshot_gcode.EndGcode
                if len (gcode_to_send) > 0:
                    self.Settings.current_debug_profile().log_snapshot_gcode("Sending snapshot return and end gcode.")
                    self.send_snapshot_gcode_array(gcode_to_send)
            else:

                if len(snapshot_gcode.ReturnCommands) > 0:
                    self.Settings.current_debug_profile().log_snapshot_gcode("Sending return gcode.")
                    return_position = self.get_position_async(
                        start_gcode=snapshot_gcode.ReturnCommands, timeout=self._position_timeout_short
                    )
                    timelapse_snapshot_payload["return_position"] = return_position

                # calculate the total snapshot time
                snapshot_end_time = time.time()
                snapshot_time = snapshot_end_time - snapshot_start_time
                self.SecondsAddedByOctolapse += snapshot_time
                timelapse_snapshot_payload["current_snapshot_time"] = snapshot_time
                timelapse_snapshot_payload["total_snapshot_time"] = self.SecondsAddedByOctolapse

                if len(snapshot_gcode.EndGcode) > 0:
                    self.Settings.current_debug_profile().log_snapshot_gcode("Sending end gcode.")
                    self.send_snapshot_gcode_array(snapshot_gcode.EndGcode)

            # we've completed the procedure, set success
            timelapse_snapshot_payload["success"] = True

        except Exception as e:
            self.Settings.current_debug_profile().log_exception(e)
            timelapse_snapshot_payload["error"] = "An unexpected error was encountered while running the timelapse " \
                                                  "snapshot procedure. "

        return timelapse_snapshot_payload

    # public functions
    def to_state_dict(self):
        try:

            position_dict = None
            position_state_dict = None
            extruder_dict = None
            trigger_state = None
            if self.Settings is not None:

                if self.Settings.show_position_changes and self.Position is not None:
                    position_dict = self.Position.to_position_dict()
                if self.Settings.show_position_state_changes and self.Position is not None:
                    position_state_dict = self.Position.to_state_dict()
                if self.Settings.show_extruder_state_changes and self.Position is not None:
                    extruder_dict = self.Position.Extruder.to_dict()
                if self.Settings.show_trigger_state_changes and self.Triggers is not None:
                    trigger_state = {
                        "Name": self.Triggers.Name,
                        "Triggers": self.Triggers.state_to_list()
                    }
            state_dict = {
                "Extruder": extruder_dict,
                "Position": position_dict,
                "PositionState": position_state_dict,
                "TriggerState": trigger_state
            }
            return state_dict
        except Exception as e:
            self.Settings.CurrentDebugProfile().log_exception(e)
        # if we're here, we've reached and logged an error.
        return {
            "Extruder": None,
            "Position": None,
            "PositionState": None,
            "TriggerState": None
        }

    def stop_snapshots(self, message=None, error=False):
        self.State = TimelapseState.WaitingToRender
        if self.TimelapseStoppedCallback is not None:
            timelapse_stopped_callback_thread = threading.Thread(
                target=self.TimelapseStoppedCallback, args=[message, error]
            )
            timelapse_stopped_callback_thread.daemon = True
            timelapse_stopped_callback_thread.start()
        return True

    def on_print_failed(self):
        if self.State != TimelapseState.Idle:
            self.end_timelapse("FAILED")

    def on_print_disconnecting(self):
        if self.State != TimelapseState.Idle:
            self.end_timelapse("DISCONNECTING")

    def on_print_disconnected(self):
        if self.State != TimelapseState.Idle:
            self.end_timelapse("DISCONNECTED")

    def on_print_canceled(self):
        if self.State != TimelapseState.Idle:
            self.end_timelapse("CANCELED")

    def on_print_completed(self):
        if self.State != TimelapseState.Idle:
            self.end_timelapse("COMPLETED")

    def end_timelapse(self, print_status):
        self.PrintEndStatus = print_status
        try:
            if self.PrintStartTime is None:
                self._reset()
            elif self.PrintStartTime is not None and self.State in [
                TimelapseState.WaitingForTrigger, TimelapseState.WaitingToRender, TimelapseState.WaitingToEndTimelapse
            ]:

                if not self._render_timelapse(self.PrintEndStatus):
                    if self.OnRenderEndCallback is not None:
                        payload = RenderingCallbackArgs(
                            "Could not start timelapse job.",
                            -1,
                            "unknown",
                            "unknown",
                            "unknown",
                            "unknown",
                            "unknown",
                            "unknown",
                            False,
                            0,
                            0,
                            True,
                            "timelapse_start",
                            "The render_start function returned false"
                        )

                        render_end_callback_thread = threading.Thread(
                            target=self.OnRenderEndCallback, args=[payload]
                        )
                        render_end_callback_thread.daemon = True
                        render_end_callback_thread.start()
                self._reset()
            if self.State != TimelapseState.Idle:
                self.State = TimelapseState.WaitingToEndTimelapse

        except Exception as e:
            self.Settings.current_debug_profile().log_exception(e)

        if self.OnTimelapseEndCallback is not None:
            self.OnTimelapseEndCallback()

    def on_print_paused(self):
        try:
            if self.State == TimelapseState.Idle:
                return
            elif self.State < TimelapseState.WaitingToRender:
                self.Settings.current_debug_profile().log_print_state_change("Print Paused.")
                self.Triggers.pause()
        except Exception as e:
            self.Settings.current_debug_profile().log_exception(e)

    def on_print_resumed(self):
        try:
            if self.State == TimelapseState.Idle:
                return
            elif self.State < TimelapseState.WaitingToRender:
                self.Triggers.resume()
        except Exception as e:
            self.Settings.current_debug_profile().log_exception(e)

    def is_timelapse_active(self):
        if (
            self.Settings is None
            or self.State in [TimelapseState.Idle, TimelapseState.Initializing, TimelapseState.WaitingToRender]
            or self.OctoprintPrinter.get_state_id() == "CANCELLING"
            or self.Triggers is None
            or self.Triggers.count() < 1
        ):
            return False
        return True

    def get_is_rendering(self):
        return self._rendering_task_queue.qsize() > 0

    def on_print_start(self, tags):
        self.OnPrintStartCallback(tags)

    def on_print_start_failed(self, message):
        self.OnPrintStartFailedCallback(message)

    def on_gcode_queuing(self, command_string, cmd_type, gcode, tags):

        self.detect_timelapse_start(command_string, tags)

        # if the timelapse is not active, exit without changing any gcode
        if not self.is_timelapse_active():
            return

        self.check_current_line_number(tags)

        # update the position tracker so that we know where all of the axis are.
        # We will need this later when generating snapshot gcode so that we can return to the previous
        # position
        is_snapshot_gcode_command = self._is_snapshot_command(command_string)

        try:
            self.Settings.current_debug_profile().log_gcode_queuing(
                "Queuing Command: Command Type:{0}, gcode:{1}, cmd: {2}, tags: {3}".format(
                    cmd_type, gcode, command_string, tags
                )
            )

            try:
                cmd, parameters = Commands.parse(command_string)
            except ValueError as e:
                message = "An error was thrown by the gcode parser, stopping timelapse.  Details: {0}".format(str(e))
                self.Settings.current_debug_profile().log_warning(
                    message
                )
                self.stop_snapshots(message, True)
                return None

            # get the position state in case it has changed
            # if there has been a position or extruder state change, inform any listener

            if cmd is not None and not is_snapshot_gcode_command:
                # create our state change dictionaries
                self.Position.update(command_string, cmd, parameters)

            # if this code is snapshot gcode, simply return it to the printer.
            if {'plugin:octolapse', 'snapshot_gcode'}.issubset(tags):
                return None

            if not self.check_for_non_metric_errors():
                if self.Position.has_position_error(0):
                    # There are position errors, report them!
                    self._on_position_error()
                elif (self.State == TimelapseState.WaitingForTrigger
                        and (self.Position.requires_location_detection(1)) and self.OctoprintPrinter.is_printing()):

                    if 'source:script' in tags:
                        # warn user
                        self._send_plugin_message_async(
                            "warning",
                            "Octolapse could not acquire a position while sending"
                            " OctoPrint scripts (settings=>GCODE Scripts)."
                            " A fix is in the works, but a modification to OctoPrint itself"
                            " is required.  For now please move your start gcode into the"
                            " actual gcode file.")
                    else:
                        self.State = TimelapseState.AcquiringLocation

                        if self.OctoprintPrinter.set_job_on_hold(True):
                            thread = threading.Thread(target=self.acquire_position, args=[command_string, cmd, parameters])
                            thread.daemon = True
                            thread.start()
                            return None,
                elif (self.State == TimelapseState.WaitingForTrigger
                      and self.OctoprintPrinter.is_printing()
                      and not self.Position.has_position_error(0)):
                    # update the triggers with the current position
                    self.Triggers.update(self.Position, command_string)

                    # see if at least one trigger is triggering
                    _first_triggering = self.get_first_triggering()

                    if _first_triggering:
                        if 'source:script' in tags:
                            # warn user
                            self._send_plugin_message_async(
                                "warning",
                                "Octolapse could not take a snapshot while sending"
                                " OctoLapse scripts (settings=>GCODE Scripts)."
                                " A fix is in the works, but a modification to OctoPrint itself"
                                " is required.  For now please move your start gcode into the"
                                " actual gcode file."
                            )
                        else:
                            # We are triggering, take a snapshot
                            self.State = TimelapseState.TakingSnapshot
                            # pause any timer triggers that are enabled
                            self.Triggers.pause()

                            # get the job lock
                            if self.OctoprintPrinter.set_job_on_hold(True):
                                # take the snapshot on a new thread
                                thread = threading.Thread(
                                    target=self.acquire_snapshot, args=[command_string, cmd, parameters, _first_triggering]
                                )
                                thread.daemon = True
                                thread.start()
                                # suppress the current command, we'll send it later
                                return None,

                elif self.State == TimelapseState.TakingSnapshot:
                    # Don't do anything further to any commands unless we are
                    # taking a timelapse , or if octolapse paused the print.
                    # suppress any commands we don't, under any cirumstances,
                    # to execute while we're taking a snapshot

                    if cmd in self.Commands.SuppressedSnapshotGcodeCommands:
                        command_string = None,  # suppress the command

            if is_snapshot_gcode_command:
                # in all cases do not return the snapshot command to the printer.
                # It is NOT a real gcode and could cause errors.
                command_string = None,

        except Exception as e:
            self.Settings.current_debug_profile().log_exception(e)
            raise

        # notify any callbacks
        self._send_state_changed_message()

        # do any post processing for test mode
        if command_string != (None,):
            command_string = self._get_command_for_octoprint(command_string, cmd,parameters)
        return command_string

    def detect_timelapse_start(self, cmd, tags):
        # detect print start
        if (
            self.Settings.is_octolapse_enabled and
            self.State == TimelapseState.Idle and
            {'trigger:comm.start_print', 'trigger:comm.reset_line_numbers'} <= tags and
            #  ({'trigger:comm.start_print', 'fileline:1'} <= tags or {'script:beforePrintStarted', 'trigger:comm.send_gcode_script'} <= tags) and
            self.OctoprintPrinter.is_printing()
        ):
            if self.OctoprintPrinter.set_job_on_hold(True):
                try:
                    self.State = TimelapseState.Initializing

                    self.Settings.current_debug_profile().log_print_state_change(
                        "Print Start Detected.  Command: {0}, Tags:{1}".format(cmd, tags)
                    )
                    # call the synchronous callback on_print_start
                    self.on_print_start(tags)

                    if self.State == TimelapseState.WaitingForTrigger:
                        # set the current line to 0 so that the plugin checks for line 1 below after startup.
                        self.CurrentFileLine = 0
                finally:
                    self.OctoprintPrinter.set_job_on_hold(False)
            else:
                self.on_print_start_failed(
                    "Unable to start timelapse, failed to acquire a job lock.  Print start failed."
                )

    def check_current_line_number(self, tags):
        # check the current line number
        if {'source:file'} in tags:
            # this line is from the file, advance!
            self.CurrentFileLine += 1
            if "fileline:{0}".format(self.CurrentFileLine) not in tags:
                actual_file_line = "unknown"
                for tag in tags:
                    if len(tag) > 9 and tag.startswith("fileline:"):
                        actual_file_line = tag[9:]
                message = "File line number {0} was expected, but {1} was received!".format(
                    self.CurrentFileLine + 1,
                    actual_file_line
                )
                self.Settings.current_debug_profile().log_error(message)
                self.stop_snapshots(message, True)

    def check_for_non_metric_errors(self):
        # make sure we're not using inches
        is_metric = self.Position.is_metric()
        has_error = False
        error_message = ""
        if is_metric is None and self.Position.has_position_error():
            has_error = True
            error_message = "The printer profile requires an explicit G21 command before any position " \
                            "altering/setting commands, including any home commands.  Stopping timelapse, " \
                            "but continuing the print. "

        elif not is_metric and self.Position.has_position_error():
            has_error = True
            if self.Printer.units_default == "inches":
                error_message = "The printer profile uses 'inches' as the default unit of measurement.  In order to" \
                    " use Octolapse, a G21 command must come before any position altering/setting commands, including" \
                    " any home commands.  Stopping timelapse, but continuing the print. "
            else:
                error_message = "The gcode file contains a G20 command (set units to inches), which Octolapse " \
                    "does not support.  Stopping timelapse, but continuing the print."

        if has_error:
            self.stop_snapshots(error_message,has_error)

        return has_error

    def get_first_triggering(self):
        try:
            # make sure we're in a state that could want to check for triggers
            if not self.State == TimelapseState.WaitingForTrigger:
                return False
            # see if the PREVIOUS command triggered (that means current gcode gets sent if the trigger[0]
            # is triggering
            first_trigger = self.Triggers.get_first_triggering(0, Triggers.TRIGGER_TYPE_IN_PATH)

            if first_trigger:
                self.Settings.current_debug_profile().log_triggering("An in-path snapshot is triggering")
                return first_trigger

            first_trigger = self.Triggers.get_first_triggering(1, Triggers.TRIGGER_TYPE_DEFAULT)
            if first_trigger:  # We're triggering
                self.Settings.current_debug_profile().log_triggering("A snapshot is triggering")
                return first_trigger
        except Exception as e:
            self.Settings.current_debug_profile().log_exception(e)
            # no need to re-raise here, the trigger just won't happen
        return False

    def acquire_position(self, command_string, cmd, parameters):
        try:
            self.Settings.current_debug_profile().log_print_state_change(
                "A position altering command has been detected.  Fetching and updating position.  "
                "Position Command: {0}".format(cmd))
            # Undo the last position update, we will be resending the command
            self.Position.undo_update()
            current_position = self.get_position_async()

            if current_position is None:
                self.PrintEndStatus = "POSITION_TIMEOUT"
                self.State = TimelapseState.WaitingToEndTimelapse
                self.Settings.current_debug_profile().log_print_state_change(
                    "Unable to acquire a position.")
            else:
                # update position
                self.Position.update_position(
                    x=current_position["x"],
                    y=current_position["y"],
                    z=current_position["z"],
                    e=current_position["e"],
                    force=True,
                    calculate_changes=True)

            # adjust the triggering command
            if self.IsTestMode:
                gcode = self.Commands.alter_for_test_mode(command_string, cmd, parameters, return_string=True)
            else:
                gcode = command_string

            if gcode != "":
                self.Settings.current_debug_profile().log_print_state_change(
                    "Sending triggering command for position acquisition - {0}.".format(gcode))
                # send the triggering command
                self.send_snapshot_gcode_array([gcode])

            # set the state
            if self.State == TimelapseState.AcquiringLocation:
                self.State = TimelapseState.WaitingForTrigger

            self.Settings.current_debug_profile().log_print_state_change("Position Acquired")

        finally:
            self.OctoprintPrinter.set_job_on_hold(False)

    def acquire_snapshot(self, command_string, cmd, parameters, trigger):
        try:
            self.Settings.current_debug_profile().log_snapshot_download(
                "About to take a snapshot.  Triggering Command: {0}".format(cmd))
            if self.OnSnapshotStartCallback is not None:
                snapshot_callback_thread = threading.Thread(target=self.OnSnapshotStartCallback)
                snapshot_callback_thread.daemon = True
                snapshot_callback_thread.start()

            # Capture and undo the last position update, we're not going to be using it!
            triggering_command_position, triggering_extruder_position = self.Position.undo_update()

            # take the snapshot
            # Todo:  We probably don't need the payload here.
            self._most_recent_snapshot_payload = self._take_timelapse_snapshot(
                trigger, command_string, cmd, parameters, triggering_command_position, triggering_extruder_position
            )
            self.Settings.current_debug_profile().log_snapshot_download("The snapshot has completed")
        finally:

            # set the state
            if self.State == TimelapseState.TakingSnapshot:
                self.State = TimelapseState.WaitingForTrigger
            self.Triggers.resume()
            self.OctoprintPrinter.set_job_on_hold(False)
            # notify that we're finished, but only if we haven't just stopped the timelapse.
            if self._most_recent_snapshot_payload is not None:
                # send a copy of the dict in case it gets changed by threads.
                self._on_trigger_snapshot_complete(self._most_recent_snapshot_payload.copy())

    def on_gcode_sent(self, cmd, cmd_type, gcode, tags):
        self.Settings.current_debug_profile().log_gcode_sent(
            "Sent to printer: Command Type:{0}, gcode:{1}, cmd: {2}, tags: {3}".format(cmd_type, gcode, cmd, tags))

    def on_gcode_received(self, comm, line, *args, **kwargs):
        self.Settings.current_debug_profile().log_gcode_received(
            "Received from printer: line:{0}".format(line)
        )
        return line

    # internal functions
    ####################
    def _get_command_for_octoprint(self, command_string, cmd, parameters):
        if command_string is None or command_string == (None,):
            return command_string

        if self.IsTestMode and self.State >= TimelapseState.WaitingForTrigger:
            return self.Commands.alter_for_test_mode(command_string, cmd, parameters)
        # if we were given a list, return it.
        if isinstance(command_string, list):
            return command_string
        # if we were given a command return None (don't change the command at all)
        return None

    def _send_state_changed_message(self):
        """Notifies any callbacks about any changes contained in the dictionaries.
        If you send a dict here the client will get a message, so check the
        settings to see if they are subscribed to notifications before populating the dictinaries!"""

        delay_seconds = 0
        # if another thread is trying to send the message, stop it
        if self.StateChangeMessageThread is not None and self.StateChangeMessageThread.isAlive():
            self.StateChangeMessageThread.cancel()

        if self.LastStateChangeMessageTime is not None:
            # do not send more than 1 per second
            time_since_last_update = time.time() - self.LastStateChangeMessageTime
            if time_since_last_update < 1:
                delay_seconds = 1-time_since_last_update
                if delay_seconds < 0:
                    delay_seconds = 0

        try:
            # Notify any callbacks
            if self.OnStateChangedCallback is not None:

                    def send_change_message():
                        trigger_change_list = None
                        position_change_dict = None
                        position_state_change_dict = None
                        extruder_change_dict = None
                        trigger_changes_dict = None

                        # Get the changes
                        if self.Settings.show_trigger_state_changes:
                            trigger_change_list = self.Triggers.state_to_list()
                        if self.Settings.show_position_changes:
                            position_change_dict = self.Position.to_position_dict()
                        if self.Settings.show_position_state_changes:
                            position_state_change_dict = self.Position.to_state_dict()
                        if self.Settings.show_extruder_state_changes:
                            extruder_change_dict = self.Position.Extruder.to_dict()

                        # if there are any state changes, send them
                        if (
                            position_change_dict is not None
                            or position_state_change_dict is not None
                            or extruder_change_dict is not None
                            or trigger_change_list is not None
                        ):
                            if trigger_change_list is not None and len(trigger_change_list) > 0:
                                trigger_changes_dict = {
                                    "Name": self.Triggers.Name,
                                    "Triggers": trigger_change_list
                                }
                        change_dict = {
                            "Extruder": extruder_change_dict,
                            "Position": position_change_dict,
                            "PositionState": position_state_change_dict,
                            "TriggerState": trigger_changes_dict
                        }

                        if (
                            change_dict["Extruder"] is not None
                            or change_dict["Position"] is not None
                            or change_dict["PositionState"] is not None
                            or change_dict["TriggerState"] is not None
                        ):
                            self.OnStateChangedCallback(change_dict)
                            self.LastStateChangeMessageTime = time.time()

                    # Send a delayed message
                    self.StateChangeMessageThread = threading.Timer(
                        delay_seconds,
                        send_change_message
                    )
                    self.StateChangeMessageThread.daemon = True
                    self.StateChangeMessageThread.start()

        except Exception as e:
            # no need to re-raise, callbacks won't be notified, however.
            self.Settings.current_debug_profile().log_exception(e)

    def _send_plugin_message(self, message_type, message):
        self.OnPluginMessageSentCallback(message_type, message)

    def _send_plugin_message_async(self, message_type, message):
        warning_thread = threading.Thread(target=self._send_plugin_message, args=[message_type, message])
        warning_thread.daemon = True
        warning_thread.start()

    def _is_snapshot_command(self, command_string):
        return command_string == self.Printer.snapshot_command

    def _is_trigger_waiting(self):
        # make sure we're in a state that could want to check for triggers
        if not self.State == TimelapseState.WaitingForTrigger:
            return None
        # Loop through all of the active currentTriggers
        waiting_trigger = self.Triggers.get_first_waiting()
        if waiting_trigger is not None:
            return True
        return False

    def _on_position_error(self):
        message = self.Position.position_error(0)
        self.Settings.current_debug_profile().log_error(message)
        if self.OnPositionErrorCallback is not None:
            position_error_callback_thread = threading.Thread(
                target=self.OnPositionErrorCallback, args=[message]
            )
            position_error_callback_thread.daemon = True
            position_error_callback_thread.start()

    def _on_trigger_snapshot_complete(self, snapshot_payload):

        if self.OnSnapshotCompleteCallback is not None:
            payload = {
                "success": snapshot_payload["success"],
                "error": snapshot_payload["error"],
                "snapshot_count": self.SnapshotCount,
                "total_snapshot_time": snapshot_payload["total_snapshot_time"],
                "current_snapshot_time": snapshot_payload["total_snapshot_time"]
            }
            if self.OnSnapshotCompleteCallback is not None:
                snapshot_complete_callback_thread = threading.Thread(
                    target=self.OnSnapshotCompleteCallback, args=[payload]
                )
                snapshot_complete_callback_thread.daemon = True
                snapshot_complete_callback_thread.start()

    def _render_timelapse(self, print_end_state):

        def _render_timelapse_async(render_job_id, timelapse_render_job):

            try:
                snapshot_thread = threading.Thread(target=timelapse_render_job, args=[])
                snapshot_thread.daemon = True
                num_snapshot_tasks = self._snapshot_task_queue.qsize()
                if num_snapshot_tasks > 0:
                    self.Settings.current_debug_profile().log_render_start("Started Rendering Timelapse.")
                else:
                    self.Settings.current_debug_profile().log_render_start(
                        "Waiting for {0} snapshot threads to complete".format(
                            self._snapshot_task_queue.qsize()))
                    self._snapshot_task_queue.join()
                    self.Settings.current_debug_profile().log_render_start(
                        "All snapshot tasks have completed, rendering timelapse"
                    )
                # we are rendering, set the state before starting the rendering job.
                self._rendering_task_queue.put(render_job_id)
                snapshot_thread.start()
            except Exception as e:
                self.Settings.current_debug_profile().log_exception(e)
                self._rendering_task_queue.get()
                self._rendering_task_queue.task_done()

        # make sure we have a non null TimelapseSettings object.  We may have terminated the timelapse for some reason
        if self.Rendering is not None and self.Rendering.enabled:
            job_id = "TimelapseRenderJob_{0}".format(str(uuid.uuid4()))
            job = Render.create_render_job(
                self.Settings,
                self.Snapshot,
                self.Rendering,
                self.DataFolder,
                self.DefaultTimelapseDirectory,
                self.FfMpegPath,
                1,
                self._rendering_task_queue,
                job_id,
                utility.get_currently_printing_filename(self.OctoprintPrinter),
                self.PrintStartTime,
                time.time(),
                print_end_state,
                self.SecondsAddedByOctolapse,
                self._on_render_start,
                self._on_render_end
            )
            rendering_thread = threading.Thread(target=_render_timelapse_async, args=[job_id, job])
            rendering_thread.daemon = True
            rendering_thread.start()
            return True
        return False

    def _on_render_start(self, *args, **kwargs):
        job_id = args[0]
        self.Settings.current_debug_profile().log_render_start(
            "Started rendering/synchronizing the timelapse. JobId: {0}".format(job_id))
        payload = args[1]
        # notify the caller
        if self.OnRenderStartCallback is not None:
            render_start_complete_callback_thread = threading.Thread(
                target=self.OnRenderStartCallback, args=[payload]
            )
            render_start_complete_callback_thread.daemon = True
            render_start_complete_callback_thread.start()

    def _on_render_end(self, *args, **kwargs):
        job_id = args[0]
        payload = args[1]

        self.Settings.current_debug_profile().log_render_complete("Completed rendering. JobId: {0}".format(job_id))
        assert (isinstance(payload, RenderingCallbackArgs))

        if not payload.HasError and self.Snapshot.cleanup_after_render_fail:
            self.CaptureSnapshot.clean_snapshots(utility.get_snapshot_temp_directory(self.DataFolder))
        elif self.Snapshot.cleanup_after_render_complete:
            self.CaptureSnapshot.clean_snapshots(utility.get_snapshot_temp_directory(self.DataFolder))

        if self.OnRenderEndCallback is not None:
            render_end_complete_callback_thread = threading.Thread(
                target=self.OnRenderEndCallback, args=[payload]
            )
            render_end_complete_callback_thread.daemon = True
            render_end_complete_callback_thread.start()

    def _on_timelapse_start(self):
        if self.OnTimelapseStartCallback is None:
            return
        self.OnTimelapseStartCallback()

    def _reset(self):
        self.State = TimelapseState.Idle
        self.CurrentFileLine = 0
        if self.Triggers is not None:
            self.Triggers.reset()
        self.CommandIndex = -1

        self.LastStateChangeMessageTime = None
        self.PrintStartTime = None
        self.SnapshotGcodes = None
        self.SavedCommand = None
        self.PositionRequestAttempts = 0
        self.IsTestMode = False

        self.ReturnPositionReceivedTime = None
        # A list of callbacks who want to be informed when a timelapse ends
        self.TimelapseStopRequested = False
        self._snapshot_success = False
        self.SnapshotError = ""
        self.HasBeenStopped = False
        self.CurrentProfiles = {
            "printer": "",
            "stabilization": "",
            "snapshot": "",
            "rendering": "",
            "camera": "",
            "debug_profile": ""
        }
        # fetch position private variables
        self._position_payload = None
        self._position_signal.set()

        # get snapshot async private variables
        self._snapshot_signal.set()


    def _reset_snapshot(self):
        self.State = TimelapseState.WaitingForTrigger
        self.CommandIndex = -1
        self.SnapshotGcodes = None
        self.SavedCommand = None
        self.PositionRequestAttempts = 0
        self._snapshot_success = False
        self.SnapshotError = ""
示例#19
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 = ""
示例#20
0
    def test_GetSnapshotPosition_BedRelativePath(self):
        """Test getting bed relative path snapshot positions for x and y"""
        # adjust the settings for absolute position and create the snapshot gcode generator
        self.Settings.CurrentStabilization().x_type = "relative_path"
        self.Settings.CurrentStabilization().x_relative_path = "0,25,50,75,100"
        self.Settings.CurrentStabilization().y_type = "relative_path"
        self.Settings.CurrentStabilization().y_relative_path = "100,75,50,25,0"

        # test with no loop
        self.Settings.CurrentStabilization().x_relative_path_loop = False
        self.Settings.CurrentStabilization(
        ).x_relative_path_invert_loop = False
        self.Settings.CurrentStabilization().y_relative_path_loop = False
        self.Settings.CurrentStabilization(
        ).y_relative_path_invert_loop = False
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 62.5 and coords["Y"] == 150)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 0)
        self.assertTrue(coords["X"] == 125 and coords["Y"] == 100)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 187.5 and coords["Y"] == 50)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 1)
        self.assertTrue(coords["X"] == 250 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 250 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 250 and coords["Y"] == 0)

        # test with loop, no invert
        self.Settings.CurrentStabilization().x_relative_path_loop = True
        self.Settings.CurrentStabilization(
        ).x_relative_path_invert_loop = False
        self.Settings.CurrentStabilization().y_relative_path_loop = True
        self.Settings.CurrentStabilization(
        ).y_relative_path_invert_loop = False
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 62.5 and coords["Y"] == 150)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 0)
        self.assertTrue(coords["X"] == 125 and coords["Y"] == 100)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 187.5 and coords["Y"] == 50)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 1)
        self.assertTrue(coords["X"] == 250 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 62.5 and coords["Y"] == 150)

        # test with loop and invert
        self.Settings.CurrentStabilization().x_relative_path_loop = True
        self.Settings.CurrentStabilization().x_relative_path_invert_loop = True
        self.Settings.CurrentStabilization().y_relative_path_loop = True
        self.Settings.CurrentStabilization().y_relative_path_invert_loop = True
        snapshotGcodeGenerator = SnapshotGcodeGenerator(
            self.Settings, self.CreateOctoprintPrinterProfile())
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 0 and coords["Y"] == 200)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 62.5 and coords["Y"] == 150)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 0)
        self.assertTrue(coords["X"] == 125 and coords["Y"] == 100)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(1, 1)
        self.assertTrue(coords["X"] == 187.5 and coords["Y"] == 50)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 1)
        self.assertTrue(coords["X"] == 250 and coords["Y"] == 0)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 187.5 and coords["Y"] == 50)
        coords = snapshotGcodeGenerator.GetSnapshotPosition(0, 0)
        self.assertTrue(coords["X"] == 125 and coords["Y"] == 100)