Example #1
0
    def connect(self, port=None, baudrate=None, profile=None):
        """
         Connects to a BVC printer. Ignores port, baudrate parameters.
         They are kept just for interface compatibility
        """
        if self._comm is not None:
            self._comm.close()

        self._comm = BeeCom(callbackObject=self,
                            printerProfileManager=self._printerProfileManager)
        self._comm.confirmConnection()

        bee_commands = self._comm.getCommandsInterface()

        # homes all axis
        if bee_commands is not None and bee_commands.isPrinting() is False:
            bee_commands.home()

        # selects the printer profile based on the connected printer name
        printer_name = self.get_printer_name()

        # converts the name to the id
        printer_id = None
        if printer_name is not None:
            printer_id = printer_name.lower().replace(' ', '')
        self._printerProfileManager.select(printer_id)

        # if the printer is printing or in shutdown mode selects the last selected file for print
        # and starts the progress monitor
        lastFile = settings().get(['lastPrintJobFile'])
        if lastFile is not None and (self.is_shutdown() or self.is_printing()
                                     or self.is_paused()):
            # Calls the select_file with the real previous PrintFileInformation object to recover the print status
            self.select_file(self._currentPrintJobFile, False)
            self._comm.startPrintStatusProgressMonitor()

        # gets current Filament profile data
        self._currentFilamentProfile = self.getSelectedFilamentProfile()

        # subscribes event handlers
        eventManager().subscribe(Events.PRINT_CANCELLED,
                                 self.on_print_cancelled)
        eventManager().subscribe(Events.PRINT_CANCELLED_DELETE_FILE,
                                 self.on_print_cancelled_delete_file)
        eventManager().subscribe(Events.PRINT_DONE, self.on_print_finished)
Example #2
0
    def disconnect(self):
        """
        Closes the connection to the printer.
        """
        super(BeePrinter, self).disconnect()

        # Instantiates the comm object just to allow for a state to be returned. This is a workaround
        # to allow to have the "Connecting..." string when the printer is connecting when the comm object is None...
        # This forces the printer to be used in auto-connect mode
        self._comm = BeeCom(callbackObject=self,
                            printerProfileManager=self._printerProfileManager)
        self._comm.confirmConnection()

        # Starts the connection monitor thread
        import threading
        bvc_conn_thread = threading.Thread(
            target=detect_bvc_printer_connection, args=(self.connect, ))
        bvc_conn_thread.daemon = True
        bvc_conn_thread.start()
Example #3
0
    def connect(self, port=None, baudrate=None, profile=None):
        """
         Connects to a BVC printer. Ignores port, baudrate parameters.
         They are kept just for interface compatibility
        """

        if self._comm is not None:
            self._comm.close()

        self._comm = BeeCom(callbackObject=self, printerProfileManager=self._printerProfileManager)
        self._comm.confirmConnection()

        bee_commands = self._comm.getCommandsInterface()

        # homes all axis
        if bee_commands is not None and bee_commands.isPrinting() is False:
            bee_commands.home()

        # selects the printer profile based on the connected printer name
        printer_name = self.get_printer_name()

        # converts the name to the id
        printer_id = None
        if printer_name is not None:
            printer_id = printer_name.lower().replace(' ', '')
        self._printerProfileManager.select(printer_id)

        # if the printer is printing or in shutdown mode selects the last selected file for print
        # and starts the progress monitor
        lastFile = settings().get(['lastPrintJobFile'])
        if lastFile is not None and (self.is_shutdown() or self.is_printing()):
            self.select_file(lastFile, False)

            self._comm.startPrintStatusProgressMonitor()

        # gets current Filament profile data
        self._currentFilamentProfile = self.getSelectedFilamentProfile()

        # subscribes the unselect_file function with the PRINT_FAILED event
        eventManager().subscribe(Events.PRINT_FAILED, self.on_print_cancelled)
Example #4
0
class BeePrinter(Printer):
    """
    BVC implementation of the :class:`PrinterInterface`. Manages the communication layer object and registers
    itself with it as a callback to react to changes on the communication layer.
    """
    TMP_FILE_MARKER = '__tmp-scn'

    def __init__(self, fileManager, analysisQueue, printerProfileManager):
        super(BeePrinter, self).__init__(fileManager, analysisQueue,
                                         printerProfileManager)
        self._estimatedTime = None
        self._elapsedTime = None
        self._numberLines = None
        self._executedLines = None
        self._currentFeedRate = None
        self._runningCalibrationTest = False
        self._insufficientFilamentForCurrent = False

        # Initializes the slicing manager for filament profile information
        self._slicingManager = SlicingManager(
            settings().getBaseFolder("slicingProfiles"), printerProfileManager)
        self._slicingManager.reload_slicers()
        self._currentFilamentProfile = None

        # We must keep a copy of the _currentFile variable (from the comm layer) to allow the situation of
        # disconnecting the printer and maintaining any selected file information after a reconnect is done
        self._currentPrintJobFile = None

    def connect(self, port=None, baudrate=None, profile=None):
        """
         Connects to a BVC printer. Ignores port, baudrate parameters.
         They are kept just for interface compatibility
        """
        if self._comm is not None:
            self._comm.close()

        self._comm = BeeCom(callbackObject=self,
                            printerProfileManager=self._printerProfileManager)
        self._comm.confirmConnection()

        bee_commands = self._comm.getCommandsInterface()

        # homes all axis
        if bee_commands is not None and bee_commands.isPrinting() is False:
            bee_commands.home()

        # selects the printer profile based on the connected printer name
        printer_name = self.get_printer_name()

        # converts the name to the id
        printer_id = None
        if printer_name is not None:
            printer_id = printer_name.lower().replace(' ', '')
        self._printerProfileManager.select(printer_id)

        # if the printer is printing or in shutdown mode selects the last selected file for print
        # and starts the progress monitor
        lastFile = settings().get(['lastPrintJobFile'])
        if lastFile is not None and (self.is_shutdown() or self.is_printing()
                                     or self.is_paused()):
            # Calls the select_file with the real previous PrintFileInformation object to recover the print status
            self.select_file(self._currentPrintJobFile, False)
            self._comm.startPrintStatusProgressMonitor()

        # gets current Filament profile data
        self._currentFilamentProfile = self.getSelectedFilamentProfile()

        # subscribes event handlers
        eventManager().subscribe(Events.PRINT_CANCELLED,
                                 self.on_print_cancelled)
        eventManager().subscribe(Events.PRINT_CANCELLED_DELETE_FILE,
                                 self.on_print_cancelled_delete_file)
        eventManager().subscribe(Events.PRINT_DONE, self.on_print_finished)

    def disconnect(self):
        """
        Closes the connection to the printer.
        """
        super(BeePrinter, self).disconnect()

        # Instantiates the comm object just to allow for a state to be returned. This is a workaround
        # to allow to have the "Connecting..." string when the printer is connecting when the comm object is None...
        # This forces the printer to be used in auto-connect mode
        self._comm = BeeCom(callbackObject=self,
                            printerProfileManager=self._printerProfileManager)
        self._comm.confirmConnection()

        # Starts the connection monitor thread
        import threading
        bvc_conn_thread = threading.Thread(
            target=detect_bvc_printer_connection, args=(self.connect, ))
        bvc_conn_thread.daemon = True
        bvc_conn_thread.start()

    def select_file(self, path, sd, printAfterSelect=False, pos=None):

        if self._comm is None:
            self._logger.info(
                "Cannot load file: printer not connected or currently busy")
            return

        # special case where we want to recover the file information after a disconnect/connect during a print job
        if path is None:
            return  # In case the server was restarted during connection break-up and path variable is passed empty from the connect method
        if isinstance(path, PrintingFileInformation):
            self._comm._currentFile = path
            return

        recovery_data = self._fileManager.get_recovery_data()
        if recovery_data:
            # clean up recovery data if we just selected a different file than is logged in that
            expected_origin = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
            actual_origin = recovery_data.get("origin", None)
            actual_path = recovery_data.get("path", None)

            if actual_origin is None or actual_path is None or actual_origin != expected_origin or actual_path != path:
                self._fileManager.delete_recovery_data()

        self._printAfterSelect = printAfterSelect
        self._posAfterSelect = pos
        self._comm.selectFile(
            "/" + path
            if sd and not settings().getBoolean(["feature", "sdRelativePath"])
            else path, sd)

        if not self._comm.isPrinting() and not self._comm.isShutdown():
            self._setProgressData(completion=0)
            self._setCurrentZ(None)

        # saves the path to the selected file
        settings().set(['lastPrintJobFile'], path)
        settings().save()

    # # # # # # # # # # # # # # # # # # # # # # #
    ############# PRINTER ACTIONS ###############
    # # # # # # # # # # # # # # # # # # # # # # #
    def start_print(self, pos=None):
        """
        Starts a new print job
        :param pos:
        :return:
        """
        super(BeePrinter, self).start_print(pos)

        # saves the current PrintFileInformation object
        self._currentPrintJobFile = self._comm.getCurrentFile()

        # sends usage statistics
        self._sendUsageStatistics('start')

    def cancel_print(self):
        """
         Cancels the current print job.
        """
        if self._comm is None:
            return

        self._comm.cancelPrint()

        # reset progress, height, print time
        self._setCurrentZ(None)
        self._setProgressData()
        self._currentPrintJobFile = None

        # mark print as failure
        if self._selectedFile is not None:
            self._fileManager.log_print(
                FileDestinations.SDCARD if self._selectedFile["sd"] else
                FileDestinations.LOCAL, self._selectedFile["filename"],
                time.time(), self._comm.getPrintTime(), False,
                self._printerProfileManager.get_current_or_default()["id"])
            payload = {
                "file": self._selectedFile["filename"],
                "origin": FileDestinations.LOCAL
            }
            if self._selectedFile["sd"]:
                payload["origin"] = FileDestinations.SDCARD

            if BeePrinter.TMP_FILE_MARKER in self._selectedFile["filename"]:
                eventManager().fire(Events.PRINT_CANCELLED_DELETE_FILE,
                                    payload)
            else:
                eventManager().fire(Events.PRINT_CANCELLED, payload)

            eventManager().fire(Events.PRINT_FAILED, payload)

    def jog(self, axis, amount):
        """
        Jogs the tool a selected amount in the axis chosen

        :param axis:
        :param amount:
        :return:
        """
        if not isinstance(axis, (str, unicode)):
            raise ValueError("axis must be a string: {axis}".format(axis=axis))

        axis = axis.lower()
        if not axis in PrinterInterface.valid_axes:
            raise ValueError("axis must be any of {axes}: {axis}".format(
                axes=", ".join(PrinterInterface.valid_axes), axis=axis))
        if not isinstance(amount, (int, long, float)):
            raise ValueError("amount must be a valid number: {amount}".format(
                amount=amount))

        printer_profile = self._printerProfileManager.get_current_or_default()

        # if the feed rate was manually set uses it
        if self._currentFeedRate is not None:
            movement_speed = self._currentFeedRate * 60
        else:
            movement_speed = printer_profile["axes"][axis]["speed"]

        bee_commands = self._comm.getCommandsInterface()

        if axis == 'x':
            bee_commands.move(amount, 0, 0, None, movement_speed)
        elif axis == 'y':
            bee_commands.move(0, amount, 0, None, movement_speed)
        elif axis == 'z':
            bee_commands.move(0, 0, amount, None, movement_speed)

    def home(self, axes):
        """
        Moves the select axes to their home position
        :param axes:
        :return:
        """
        if not isinstance(axes, (list, tuple)):
            if isinstance(axes, (str, unicode)):
                axes = [axes]
            else:
                raise ValueError(
                    "axes is neither a list nor a string: {axes}".format(
                        axes=axes))

        validated_axes = filter(lambda x: x in PrinterInterface.valid_axes,
                                map(lambda x: x.lower(), axes))
        if len(axes) != len(validated_axes):
            raise ValueError(
                "axes contains invalid axes: {axes}".format(axes=axes))

        bee_commands = self._comm.getCommandsInterface()

        if 'z' in axes:
            bee_commands.homeZ()
        elif 'x' in axes and 'y' in axes:
            bee_commands.homeXY()

    def extrude(self, amount):
        """
        Extrudes the defined amount
        :param amount:
        :return:
        """
        if not isinstance(amount, (int, long, float)):
            raise ValueError("amount must be a valid number: {amount}".format(
                amount=amount))

        printer_profile = self._printerProfileManager.get_current_or_default()
        extrusion_speed = printer_profile["axes"]["e"]["speed"]

        bee_commands = self._comm.getCommandsInterface()
        bee_commands.move(0, 0, 0, amount, extrusion_speed)

    def startHeating(self, targetTemperature=200):
        """
        Starts the heating procedure
        :param targetTemperature:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().startHeating(
                targetTemperature)
        except Exception as ex:
            self._logger.error(ex)

    def cancelHeating(self):
        """
        Cancels the heating procedure
        :return:
        """
        try:
            return self._comm.getCommandsInterface().cancelHeating()
        except Exception as ex:
            self._logger.error(ex)

    def heatingDone(self):
        """
        Runs the necessary commands after the heating operation is finished
        :return:
        """
        try:
            return self._comm.getCommandsInterface().goToLoadUnloadPos()
        except Exception as ex:
            self._logger.error(ex)

    def unload(self):
        """
        Unloads the filament from the printer
        :return:
        """
        try:
            return self._comm.getCommandsInterface().unload()
        except Exception as ex:
            self._logger.error(ex)

    def load(self):
        """
        Loads the filament to the printer
        :return:
        """
        try:
            return self._comm.getCommandsInterface().load()
        except Exception as ex:
            self._logger.error(ex)

    def setFilamentString(self, filamentStr):
        """
        Saves the filament reference string in the printer memory
        :param filamentStr:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().setFilamentString(
                filamentStr)
        except Exception as ex:
            self._logger.error(ex)

    def getSelectedFilamentProfile(self):
        """
        Gets the slicing profile for the currently selected filament in the printer
        Returns the first occurrence of filament name and printer. Ignores resolution and nozzle size.
        :return: Profile or None
        """
        try:
            filamentStr = self._comm.getCommandsInterface().getFilamentString()
            if not filamentStr:
                return None

            filamentNormalizedName = filamentStr.lower().replace(
                ' ', '_') + '_' + self.getPrinterName().lower()
            profiles = self._slicingManager.all_profiles_list(
                self._slicingManager.default_slicer)

            if len(profiles) > 0:
                for key, value in profiles.items():
                    if filamentNormalizedName in key:
                        filamentProfile = self._slicingManager.load_profile(
                            self._slicingManager.default_slicer,
                            key,
                            require_configured=False)
                        return filamentProfile

            return None
        except Exception as ex:
            self._logger.error(ex)

    def getFilamentString(self):
        """
        Gets the current filament reference string in the printer memory
        :return: string
        """
        try:
            return self._comm.getCommandsInterface().getFilamentString()
        except Exception as ex:
            self._logger.error(ex)

    def getFilamentInSpool(self):
        """
        Gets the current amount of filament left in spool
        :return: float filament amount in mm
        """
        try:
            filament = self._comm.getCommandsInterface().getFilamentInSpool()
            if filament < 0:
                # In case the value returned from the printer is not valid returns a high value to prevent false
                # positives of not enough filament available
                return 1000000.0

            return filament
        except Exception as ex:
            self._logger.error(ex)

    def getFilamentWeightInSpool(self):
        """
        Gets the current amount of filament left in spool
        :return: float filament amount in grams
        """
        try:
            filament_mm = self._comm.getCommandsInterface().getFilamentInSpool(
            )

            if filament_mm > 0:
                filament_cm = filament_mm / 10.0

                filament_diameter, filament_density = self._getFilamentSettings(
                )

                filament_radius = float(int(filament_diameter) / 10000.0) / 2.0
                filament_volume = filament_cm * (math.pi * filament_radius *
                                                 filament_radius)

                filament_weight = filament_volume * filament_density
                return round(filament_weight, 2)
            else:
                # In case the value returned from the printer is not valid returns a high value to prevent false
                # positives of not enough filament available
                return 1000.0
        except Exception as ex:
            self._logger.error(ex)

    def setFilamentInSpool(self, filamentInSpool):
        """
        Passes to the printer the amount of filament left in spool
        :param filamentInSpool: Amount of filament in grams
        :return: string Command return value
        """
        try:
            if filamentInSpool < 0:
                self._logger.error(
                    'Unable to set invalid filament weight: %s' %
                    filamentInSpool)
                return

            filament_diameter, filament_density = self._getFilamentSettings()

            filament_volume = filamentInSpool / filament_density
            filament_radius = float(int(filament_diameter) / 10000.0) / 2.0
            filament_cm = filament_volume / (math.pi * filament_radius *
                                             filament_radius)
            filament_mm = filament_cm * 10.0

            comm_return = self._comm.getCommandsInterface().setFilamentInSpool(
                filament_mm)

            # updates the current print job information with availability of filament
            self._checkSufficientFilamentForPrint()

            return comm_return
        except Exception as ex:
            self._logger.error(ex)

    def setNozzleSize(self, nozzleSize):
        """
        Saves the selected nozzle size
        :param nozzleSize:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().setNozzleSize(nozzleSize)
        except Exception as ex:
            self._logger.error(ex)

    def getNozzleSize(self):
        """
        Gets the current selected nozzle size in the printer memory
        :return: float
        """
        try:
            return self._comm.getCommandsInterface().getNozzleSize()
        except Exception as ex:
            self._logger.error(ex)

    def startCalibration(self, repeat=False):
        """
        Starts the calibration procedure
        :param repeat:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().startCalibration(
                repeat=repeat)
        except Exception as ex:
            self._logger.error(ex)

    def nextCalibrationStep(self):
        """
        Goes to the next calibration step
        :return:
        """
        try:
            return self._comm.getCommandsInterface().goToNextCalibrationPoint()
        except Exception as ex:
            self._logger.error(ex)

    def startCalibrationTest(self):
        """
        Starts the printer calibration test
        :return:
        """
        """
        TODO: For now we will hard-code a fixed string to fetch the calibration GCODE, since it is the same for all
        the "first version" printers. In the future this function call must use the printer name for dynamic fetch
        of the correct GCODE, using self._printerProfileManager.get_current_or_default()['name'] to get the current
        printer name
        """
        test_gcode = CalibrationGCoder.get_calibration_gcode(
            'BVC_BEETHEFIRST_V1')
        lines = test_gcode.split(',')

        file_path = os.path.join(settings().getBaseFolder("uploads"),
                                 'BEETHEFIRST_calib_test.gcode')
        calibtest_file = open(file_path, 'w')

        for line in lines:
            calibtest_file.write(line + '\n')

        calibtest_file.close()

        self.select_file(file_path, False)
        self.start_print()

        self._runningCalibrationTest = True

        return None

    def cancelCalibrationTest(self):
        """
        Cancels the running calibration test
        :return:
        """
        self.cancel_print()
        self._runningCalibrationTest = False

        return None

    def toggle_pause_print(self):
        """
        Pauses the current print job if it is currently running or resumes it if it is currently paused.
        """
        if self.is_printing():
            self.pause_print()
        elif self.is_paused() or self.is_shutdown():
            self.resume_print()

    def resume_print(self):
        """
        Resume the current printjob.
        """
        if self._comm is None:
            return

        if not self._comm.isPaused() and not self._comm.isShutdown():
            return

        self._comm.setPause(False)

    # # # # # # # # # # # # # # # # # # # # # # #
    ########  GETTER/SETTER FUNCTIONS  ##########
    # # # # # # # # # # # # # # # # # # # # # # #

    def getPrintProgress(self):
        """
        Gets the current progress of the print job
        :return:
        """
        if self._numberLines is not None and self._executedLines is not None and self._numberLines > 0:
            return float(self._executedLines) / float(self._numberLines)
        else:
            return -1

    def getPrintFilepos(self):
        """
        Gets the current position in file being printed
        :return:
        """
        if self._executedLines is not None:
            return self._executedLines
        else:
            return 0

    def getCurrentProfile(self):
        """
        Returns current printer profile
        :return:
        """
        if self._printerProfileManager is not None:
            return self._printerProfileManager.get_current_or_default()
        else:
            return None

    def getPrinterName(self):
        """
        Returns the name of the connected printer
        :return:
        """
        if self._comm is not None:
            return self._comm.getConnectedPrinterName()
        else:
            return None

    def feed_rate(self, factor):
        """
        Updates the feed rate factor
        :param factor:
        :return:
        """
        factor = self._convert_rate_value(factor, min=50, max=200)
        self._currentFeedRate = factor

    def get_current_temperature(self):
        """
        Returns the current extruder temperature
        :return:
        """
        try:
            return self._comm.getCommandsInterface().getNozzleTemperature()
        except Exception as ex:
            self._logger.error(ex)

    def isRunningCalibrationTest(self):
        """
        Updates the running calibration test flag
        :return:
        """
        return self._runningCalibrationTest

    def isValidNozzleSize(self, nozzleSize):
        """
        Checks if the passed nozzleSize value is valid
        :param nozzleSize:
        :return:
        """
        for k, v in settings().get(['nozzleTypes']).iteritems():
            if v['value'] == nozzleSize:
                return True

        return False

    def is_preparing_print(self):
        return self._comm is not None and self._comm.isPreparingPrint()

    def is_heating(self):
        return self._comm is not None and (self._comm.isHeating()
                                           or self._comm.isPreparingPrint())

    def is_shutdown(self):
        return self._comm is not None and self._comm.isShutdown()

    def get_state_string(self):
        """
         Returns a human readable string corresponding to the current communication state.
        """
        if self._comm is None:
            return "Connecting..."
        else:
            return self._comm.getStateString()

    def getCurrentFirmware(self):
        """
        Gets the current printer firmware version
        :return: string
        """
        if self._comm is not None and self._comm.getCommandsInterface(
        ) is not None:
            firmware_v = self._comm.getCommandsInterface().getFirmwareVersion()

            if firmware_v is not None:
                return firmware_v
            else:
                return 'Not available'
        else:
            return 'Not available'

    def printFromMemory(self):
        """
        Prints the file currently in the printer memory
        :param self:
        :return:
        """
        try:
            if self._comm is None:
                self._logger.info(
                    "Cannot print from memory: printer not connected or currently busy"
                )
                return

            # bypasses normal octoprint workflow to print from memory "special" file
            self._comm.selectFile('Memory File', False)

            self._setProgressData(completion=0)
            self._setCurrentZ(None)
            return self._comm.startPrint('from_memory')
        except Exception as ex:
            self._logger.error(ex)

    # # # # # # # # # # # # # # # # # # # # # # #
    ##########  CALLBACK FUNCTIONS  #############
    # # # # # # # # # # # # # # # # # # # # # # #
    def updateProgress(self, progressData):
        """
        Receives a progress data object from the BVC communication layer
        and updates the progress attributes

        :param progressData:
        :return:
        """
        if progressData is not None and self._selectedFile is not None:
            if 'Elapsed Time' in progressData:
                self._elapsedTime = progressData['Elapsed Time']
            if 'Estimated Time' in progressData:
                self._estimatedTime = progressData['Estimated Time']
            if 'Executed Lines' in progressData:
                self._executedLines = progressData['Executed Lines']
            if 'Lines' in progressData:
                self._numberLines = progressData['Lines']

    def on_comm_progress(self):
        """
         Callback method for the comm object, called upon any change in progress of the print job.
         Triggers storage of new values for printTime, printTimeLeft and the current progress.
        """
        if self._comm is not None:
            self._setProgressData(self.getPrintProgress(),
                                  self.getPrintFilepos(),
                                  self._comm.getPrintTime(),
                                  self._comm.getCleanedPrintTime())

            # If the status from the printer is no longer printing runs the post-print trigger
            if self.getPrintProgress() >= 1 \
                    and self._comm.getCommandsInterface().isPreparingOrPrinting() is False:

                # Runs the print finish communications callback
                self._comm.triggerPrintFinished()

                self._setProgressData()

                self._comm.getCommandsInterface().stopStatusMonitor()
                self._runningCalibrationTest = False

    def on_comm_file_selected(self, filename, filesize, sd):
        """
        Override callback function to allow for print halt when there is not enough filament
        :param filename:
        :param filesize:
        :param sd:
        :return:
        """
        self._setJobData(filename, filesize, sd)
        self._stateMonitor.set_state({
            "text": self.get_state_string(),
            "flags": self._getStateFlags()
        })

        # checks if the insufficient filament flag is true and halts the print process
        if self._insufficientFilamentForCurrent:
            self._printAfterSelect = False

        if self._printAfterSelect:
            self._printAfterSelect = False
            self.start_print(pos=self._posAfterSelect)

    def on_print_cancelled(self, event, payload):
        """
        Print cancelled callback for the EventManager.
        """
        self.unselect_file()

        # sends usage statistics to remote server
        self._sendUsageStatistics('cancel')

    def on_print_cancelled_delete_file(self, event, payload):
        """
        Print cancelled callback for the EventManager.
        """
        self._fileManager.remove_file(payload['origin'], payload['file'])
        self.on_print_cancelled(event, payload)

    def on_comm_state_change(self, state):
        """
        Callback method for the comm object, called if the connection state changes.
        """
        oldState = self._state

        # forward relevant state changes to gcode manager
        if oldState == BeeCom.STATE_PRINTING:
            self._analysisQueue.resume(
            )  # printing done, put those cpu cycles to good use

        elif state == BeeCom.STATE_PRINTING:
            self._analysisQueue.pause()  # do not analyse files while printing

        elif state == BeeCom.STATE_CLOSED or state == BeeCom.STATE_CLOSED_WITH_ERROR:
            if self._comm is not None:
                self._comm = None

        self._setState(state)

    def on_print_finished(self, event, payload):
        """
        Event listener to when a print job finishes
        :return:
        """
        if BeePrinter.TMP_FILE_MARKER in payload["file"]:
            self._fileManager.remove_file(payload['origin'], payload['file'])

        # unselects the current file
        self.unselect_file()

        # sends usage statistics
        self._sendUsageStatistics('stop')

    # # # # # # # # # # # # # # # # # # # # # # #
    ########### AUXILIARY FUNCTIONS #############
    # # # # # # # # # # # # # # # # # # # # # # #

    def _setJobData(self, filename, filesize, sd):
        super(BeePrinter, self)._setJobData(filename, filesize, sd)

        self._checkSufficientFilamentForPrint()

    def _getFilamentSettings(self):
        """
        Gets the necessary filament settings for weight/size conversions
        Returns tuple with (diameter,density)
        """
        # converts the amount of filament in grams to mm
        if self._currentFilamentProfile:
            # Fetches the first position filament_diameter from the filament data and converts to microns
            filament_diameter = self._currentFilamentProfile.data[
                'filament_diameter'][0] * 1000
            # TODO: The filament density should also be set based on profile data
            filament_density = 1.275  # default value
        else:
            filament_diameter = 1.75 * 1000  # default value in microns
            filament_density = 1.275  # default value

        return filament_diameter, filament_density

    def _checkSufficientFilamentForPrint(self):
        """
        Checks if the current print job has enough filament to complete. By updating the
        job setting, it will automatically update the interface through the web socket
        :return:
        """
        # Gets the current print job data
        state_data = self._stateMonitor.get_current_data()

        if state_data and state_data['job'] and state_data['job']['filament']:
            # gets the filament information for the filament weight to be used in the print job
            filament = state_data['job']['filament']
            # gets the current amount of filament left in printer
            current_filament_length = self.getFilamentInSpool()

            # Signals that there is not enough filament
            if "tool0" in filament:
                if filament["tool0"]['length'] > current_filament_length:
                    filament["tool0"]['insufficient'] = True
                    self._insufficientFilamentForCurrent = True
                else:
                    filament["tool0"]['insufficient'] = False
                    self._insufficientFilamentForCurrent = False

    def _setProgressData(self,
                         completion=None,
                         filepos=None,
                         printTime=None,
                         printTimeLeft=None):
        """
        Auxiliar method to control the print progress status data
        :param completion:
        :param filepos:
        :param printTime:
        :param printTimeLeft:
        :return:
        """
        estimatedTotalPrintTime = self._estimateTotalPrintTime(
            completion, printTimeLeft)
        totalPrintTime = estimatedTotalPrintTime

        if self._selectedFile and "estimatedPrintTime" in self._selectedFile \
                and self._selectedFile["estimatedPrintTime"]:

            statisticalTotalPrintTime = self._selectedFile[
                "estimatedPrintTime"]
            if completion and printTimeLeft:
                if estimatedTotalPrintTime is None:
                    totalPrintTime = statisticalTotalPrintTime
                else:
                    if completion < 0.5:
                        sub_progress = completion * 2
                    else:
                        sub_progress = 1.0
                    totalPrintTime = (
                        1 - sub_progress
                    ) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime

        self._progress = completion
        self._printTime = printTime
        self._printTimeLeft = totalPrintTime - printTimeLeft if (
            totalPrintTime is not None and printTimeLeft is not None) else None
        if printTime is None:
            self._elapsedTime = 0

        self._stateMonitor.set_progress({
            "completion":
            self._progress * 100 if self._progress is not None else None,
            "filepos":
            filepos,
            "printTime":
            int(self._elapsedTime *
                60) if self._elapsedTime is not None else None,
            "printTimeLeft":
            int(self._printTimeLeft)
            if self._printTimeLeft is not None else None
        })

        if completion:
            progress_int = int(completion * 100)
            if self._lastProgressReport != progress_int:
                self._lastProgressReport = progress_int
                self._reportPrintProgressToPlugins(progress_int)

    def _getStateFlags(self):
        return {
            "operational": self.is_operational(),
            "printing": self.is_printing(),
            "closedOrError": self.is_closed_or_error(),
            "error": self.is_error(),
            "paused": self.is_paused(),
            "ready": self.is_ready(),
            "sdReady": self.is_sd_ready(),
            "heating": self.is_heating(),
            "shutdown": self.is_shutdown()
        }

    def _sendUsageStatistics(self, operation):
        """
        Calls and external executable to send usage statistics to a remote cloud server
        :param operation: Supports 'start' (Start Print), 'cancel' (Cancel Print), 'stop' (Print finished) operations
        :return: true in case the operation was successfull or false if not
        """
        _logger = logging.getLogger()
        biExePath = settings().getBaseFolder('bi') + '/bi_azure'

        if operation != 'start' and operation != 'cancel' and operation != 'stop':
            return False

        if os.path.exists(biExePath) and os.path.isfile(biExePath):

            printerSN = self._comm.getConnectedPrinterSN()

            if printerSN is None:
                _logger.error(
                    "Could not get Printer Serial Number for statistics communication."
                )
                return False
            else:
                cmd = '%s %s %s' % (biExePath, str(printerSN), str(operation))
                _logger.info(u"Running %s" % cmd)

                import subprocess
                p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)

                (output, err) = p.communicate()

                p_status = p.wait()

                if p_status == 0 and 'IOTHUB_CLIENT_CONFIRMATION_OK' in output:
                    _logger.info(
                        u"Statistics sent to remote server. (Operation: %s)" %
                        operation)
                    return True
                else:
                    _logger.info(
                        u"Failed sending statistics to remote server. (Operation: %s)"
                        % operation)

        return False
Example #5
0
class BeePrinter(Printer):
    """
    BVC implementation of the :class:`PrinterInterface`. Manages the communication layer object and registers
    itself with it as a callback to react to changes on the communication layer.
    """

    def __init__(self, fileManager, analysisQueue, printerProfileManager):
        super(BeePrinter, self).__init__(fileManager, analysisQueue, printerProfileManager)
        self._estimatedTime = None
        self._elapsedTime = None
        self._numberLines = None
        self._executedLines = None
        self._currentFeedRate = None
        self._runningCalibrationTest = False
        self._insufficientFilamentForCurrent = False

        # Initializes the slicing manager for filament profile information
        self._slicingManager = SlicingManager(settings().getBaseFolder("slicingProfiles"), printerProfileManager)
        self._slicingManager.reload_slicers()
        self._currentFilamentProfile = None

    def connect(self, port=None, baudrate=None, profile=None):
        """
         Connects to a BVC printer. Ignores port, baudrate parameters.
         They are kept just for interface compatibility
        """

        if self._comm is not None:
            self._comm.close()

        self._comm = BeeCom(callbackObject=self, printerProfileManager=self._printerProfileManager)
        self._comm.confirmConnection()

        bee_commands = self._comm.getCommandsInterface()

        # homes all axis
        if bee_commands is not None and bee_commands.isPrinting() is False:
            bee_commands.home()

        # selects the printer profile based on the connected printer name
        printer_name = self.get_printer_name()

        # converts the name to the id
        printer_id = None
        if printer_name is not None:
            printer_id = printer_name.lower().replace(' ', '')
        self._printerProfileManager.select(printer_id)

        # if the printer is printing or in shutdown mode selects the last selected file for print
        # and starts the progress monitor
        lastFile = settings().get(['lastPrintJobFile'])
        if lastFile is not None and (self.is_shutdown() or self.is_printing()):
            self.select_file(lastFile, False)

            self._comm.startPrintStatusProgressMonitor()

        # gets current Filament profile data
        self._currentFilamentProfile = self.getSelectedFilamentProfile()

        # subscribes the unselect_file function with the PRINT_FAILED event
        eventManager().subscribe(Events.PRINT_FAILED, self.on_print_cancelled)


    def disconnect(self):
        """
        Closes the connection to the printer.
        """
        super(BeePrinter, self).disconnect()

        # Starts the connection monitor thread
        import threading
        bvc_conn_thread = threading.Thread(target=detect_bvc_printer_connection, args=(self.connect, ))
        bvc_conn_thread.daemon = True
        bvc_conn_thread.start()


    def select_file(self, path, sd, printAfterSelect=False, pos=None):

        if self._comm is None:
            self._logger.info("Cannot load file: printer not connected or currently busy")
            return

        recovery_data = self._fileManager.get_recovery_data()
        if recovery_data:
            # clean up recovery data if we just selected a different file than is logged in that
            expected_origin = FileDestinations.SDCARD if sd else FileDestinations.LOCAL
            actual_origin = recovery_data.get("origin", None)
            actual_path = recovery_data.get("path", None)

            if actual_origin is None or actual_path is None or actual_origin != expected_origin or actual_path != path:
                self._fileManager.delete_recovery_data()

        self._printAfterSelect = printAfterSelect
        self._posAfterSelect = pos
        self._comm.selectFile("/" + path if sd and not settings().getBoolean(["feature", "sdRelativePath"]) else path, sd)

        if not self._comm.isPrinting() and not self._comm.isShutdown():
            self._setProgressData(completion=0)
            self._setCurrentZ(None)

        # saves the path to the selected file
        settings().set(['lastPrintJobFile'], path)
        settings().save()

    # # # # # # # # # # # # # # # # # # # # # # #
    ############# PRINTER ACTIONS ###############
    # # # # # # # # # # # # # # # # # # # # # # #
    def cancel_print(self):
        """
         Cancels the current print job.
        """
        super(BeePrinter, self).cancel_print()

        # waits a bit before un-selecting the file
        import time
        time.sleep(2)
        self.unselect_file()


    def jog(self, axis, amount):
        """
        Jogs the tool a selected amount in the axis chosen

        :param axis:
        :param amount:
        :return:
        """
        if not isinstance(axis, (str, unicode)):
            raise ValueError("axis must be a string: {axis}".format(axis=axis))

        axis = axis.lower()
        if not axis in PrinterInterface.valid_axes:
            raise ValueError("axis must be any of {axes}: {axis}".format(axes=", ".join(PrinterInterface.valid_axes), axis=axis))
        if not isinstance(amount, (int, long, float)):
            raise ValueError("amount must be a valid number: {amount}".format(amount=amount))

        printer_profile = self._printerProfileManager.get_current_or_default()

        # if the feed rate was manually set uses it
        if self._currentFeedRate is not None:
            movement_speed = self._currentFeedRate * 60
        else:
            movement_speed = printer_profile["axes"][axis]["speed"]

        bee_commands = self._comm.getCommandsInterface()

        if axis == 'x':
            bee_commands.move(amount, 0, 0, None, movement_speed)
        elif axis == 'y':
            bee_commands.move(0, amount, 0, None, movement_speed)
        elif axis == 'z':
            bee_commands.move(0, 0, amount, None, movement_speed)


    def home(self, axes):
        """
        Moves the select axes to their home position
        :param axes:
        :return:
        """
        if not isinstance(axes, (list, tuple)):
            if isinstance(axes, (str, unicode)):
                axes = [axes]
            else:
                raise ValueError("axes is neither a list nor a string: {axes}".format(axes=axes))

        validated_axes = filter(lambda x: x in PrinterInterface.valid_axes, map(lambda x: x.lower(), axes))
        if len(axes) != len(validated_axes):
            raise ValueError("axes contains invalid axes: {axes}".format(axes=axes))

        bee_commands = self._comm.getCommandsInterface()

        if 'z' in axes:
            bee_commands.homeZ()
        elif 'x' in axes and 'y' in axes:
            bee_commands.homeXY()


    def extrude(self, amount):
        """
        Extrudes the defined amount
        :param amount:
        :return:
        """
        if not isinstance(amount, (int, long, float)):
            raise ValueError("amount must be a valid number: {amount}".format(amount=amount))

        printer_profile = self._printerProfileManager.get_current_or_default()
        extrusion_speed = printer_profile["axes"]["e"]["speed"]

        bee_commands = self._comm.getCommandsInterface()
        bee_commands.move(0, 0, 0, amount, extrusion_speed)


    def startHeating(self, targetTemperature=200):
        """
        Starts the heating procedure
        :param targetTemperature:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().startHeating(targetTemperature)
        except Exception as ex:
            self._logger.error(ex)


    def cancelHeating(self):
        """
        Cancels the heating procedure
        :return:
        """
        try:
            return self._comm.getCommandsInterface().cancelHeating()
        except Exception as ex:
            self._logger.error(ex)


    def heatingDone(self):
        """
        Runs the necessary commands after the heating operation is finished
        :return:
        """
        try:
            return self._comm.getCommandsInterface().goToLoadUnloadPos()
        except Exception as ex:
            self._logger.error(ex)


    def unload(self):
        """
        Unloads the filament from the printer
        :return:
        """
        try:
            return self._comm.getCommandsInterface().unload()
        except Exception as ex:
            self._logger.error(ex)


    def load(self):
        """
        Loads the filament to the printer
        :return:
        """
        try:
            return self._comm.getCommandsInterface().load()
        except Exception as ex:
            self._logger.error(ex)


    def setFilamentString(self, filamentStr):
        """
        Saves the filament reference string in the printer memory
        :param filamentStr:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().setFilamentString(filamentStr)
        except Exception as ex:
            self._logger.error(ex)


    def getSelectedFilamentProfile(self):
        """
        Gets the slicing profile for the currently selected filament in the printer
        Returns the first occurrence of filament name and printer. Ignores resolution and nozzle size.
        :return: Profile or None
        """
        try:
            filamentStr = self._comm.getCommandsInterface().getFilamentString()
            if not filamentStr:
                return None

            filamentNormalizedName = filamentStr.lower().replace(' ', '_') + '_' + self.getPrinterName().lower()
            profiles = self._slicingManager.all_profiles_list(self._slicingManager.default_slicer)

            if len(profiles) > 0:
                for key,value in profiles.items():
                    if filamentNormalizedName in key:
                        filamentProfile = self._slicingManager.load_profile(self._slicingManager.default_slicer, key, require_configured=False)
                        return filamentProfile

            return None
        except Exception as ex:
            self._logger.error(ex)


    def getFilamentString(self):
        """
        Gets the current filament reference string in the printer memory
        :return: string
        """
        try:
            return self._comm.getCommandsInterface().getFilamentString()
        except Exception as ex:
            self._logger.error(ex)


    def getFilamentInSpool(self):
        """
        Gets the current amount of filament left in spool
        :return: float filament amount in mm
        """
        try:
            filament = self._comm.getCommandsInterface().getFilamentInSpool()
            if filament < 0:
                # In case the value returned from the printer is not valid returns a high value to prevent false
                # positives of not enough filament available
                return 1000000.0

            return filament
        except Exception as ex:
            self._logger.error(ex)


    def getFilamentWeightInSpool(self):
        """
        Gets the current amount of filament left in spool
        :return: float filament amount in grams
        """
        try:
            filament_mm = self._comm.getCommandsInterface().getFilamentInSpool()

            if filament_mm > 0:
                filament_cm = filament_mm / 10.0

                filament_diameter, filament_density = self._getFilamentSettings()

                filament_radius = float(int(filament_diameter) / 10000.0) / 2.0
                filament_volume = filament_cm * (math.pi * filament_radius * filament_radius)

                filament_weight = filament_volume * filament_density
                return round(filament_weight, 2)
            else:
                # In case the value returned from the printer is not valid returns a high value to prevent false
                # positives of not enough filament available
                return 1000.0
        except Exception as ex:
            self._logger.error(ex)


    def setFilamentInSpool(self, filamentInSpool):
        """
        Passes to the printer the amount of filament left in spool
        :param filamentInSpool: Amount of filament in grams
        :return: string Command return value
        """
        try:
            if filamentInSpool < 0:
                self._logger.error('Unable to set invalid filament weight: %s' % filamentInSpool)
                return

            filament_diameter, filament_density = self._getFilamentSettings()

            filament_volume = filamentInSpool / filament_density
            filament_radius = float(int(filament_diameter) / 10000.0) / 2.0
            filament_cm = filament_volume / (math.pi * filament_radius * filament_radius)
            filament_mm = filament_cm * 10.0

            comm_return = self._comm.getCommandsInterface().setFilamentInSpool(filament_mm)

            # updates the current print job information with availability of filament
            self._checkSufficientFilamentForPrint()

            return comm_return
        except Exception as ex:
            self._logger.error(ex)


    def setNozzleSize(self, nozzleSize):
        """
        Saves the selected nozzle size
        :param nozzleSize:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().setNozzleSize(nozzleSize)
        except Exception as ex:
            self._logger.error(ex)


    def getNozzleSize(self):
        """
        Gets the current selected nozzle size in the printer memory
        :return: float
        """
        try:
            return self._comm.getCommandsInterface().getNozzleSize()
        except Exception as ex:
            self._logger.error(ex)


    def startCalibration(self, repeat=False):
        """
        Starts the calibration procedure
        :param repeat:
        :return:
        """
        try:
            return self._comm.getCommandsInterface().startCalibration(repeat=repeat)
        except Exception as ex:
            self._logger.error(ex)


    def nextCalibrationStep(self):
        """
        Goes to the next calibration step
        :return:
        """
        try:
            return self._comm.getCommandsInterface().goToNextCalibrationPoint()
        except Exception as ex:
            self._logger.error(ex)


    def startCalibrationTest(self):
        """
        Starts the printer calibration test
        :return:
        """
        test_gcode = CalibrationGCoder.get_calibration_gcode(self._printerProfileManager.get_current_or_default()['name'])
        lines = test_gcode.split(',')

        file_path = os.path.join(settings().getBaseFolder("uploads"), 'BEETHEFIRST_calib_test.gcode')
        calibtest_file = open(file_path, 'w')

        for line in lines:
            calibtest_file.write(line + '\n')

        calibtest_file.close()

        self.select_file(file_path, False)
        self.start_print()

        self._runningCalibrationTest = True

        return None


    def cancelCalibrationTest(self):
        """
        Cancels the running calibration test
        :return:
        """
        self.cancel_print()
        self._runningCalibrationTest = False

        return None


    def toggle_pause_print(self):
        """
        Pauses the current print job if it is currently running or resumes it if it is currently paused.
        """
        if self.is_printing():
            self.pause_print()
        elif self.is_paused() or self.is_shutdown():
            self.resume_print()


    def resume_print(self):
        """
        Resume the current printjob.
        """
        if self._comm is None:
            return

        if not self._comm.isPaused() and not self._comm.isShutdown():
            return

        self._comm.setPause(False)

    # # # # # # # # # # # # # # # # # # # # # # #
    ########  GETTER/SETTER FUNCTIONS  ##########
    # # # # # # # # # # # # # # # # # # # # # # #

    def getPrintProgress(self):
        """
        Gets the current progress of the print job
        :return:
        """
        if self._numberLines is not None and self._executedLines is not None and self._numberLines > 0:
            return float(self._executedLines) / float(self._numberLines)
        else:
            return -1


    def getPrintFilepos(self):
        """
        Gets the current position in file being printed
        :return:
        """
        if self._executedLines is not None:
            return self._executedLines
        else:
            return 0


    def getCurrentProfile(self):
        """
        Returns current printer profile
        :return:
        """
        if self._printerProfileManager is not None:
            return self._printerProfileManager.get_current_or_default()
        else:
            return None


    def getPrinterName(self):
        """
        Returns the name of the connected printer
        :return:
        """
        if self._comm is not None:
            return self._comm.getConnectedPrinterName()
        else:
            return None


    def feed_rate(self, factor):
        """
        Updates the feed rate factor
        :param factor:
        :return:
        """
        factor = self._convert_rate_value(factor, min=50, max=200)
        self._currentFeedRate = factor


    def get_current_temperature(self):
        """
        Returns the current extruder temperature
        :return:
        """
        try:
            return self._comm.getCommandsInterface().getNozzleTemperature()
        except Exception as ex:
            self._logger.error(ex)


    def isRunningCalibrationTest(self):
        """
        Updates the running calibration test flag
        :return:
        """
        return self._runningCalibrationTest


    def isValidNozzleSize(self, nozzleSize):
        """
        Checks if the passed nozzleSize value is valid
        :param nozzleSize:
        :return:
        """
        for k,v in settings().get(['nozzleTypes']).iteritems():
            if v['value'] == nozzleSize:
                return True

        return False


    def is_preparing_print(self):
        return self._comm is not None and self._comm.isPreparingPrint()


    def is_heating(self):
        return self._comm is not None and (self._comm.isHeating() or self._comm.isPreparingPrint())


    def is_shutdown(self):
        return self._comm is not None and self._comm.isShutdown()


    def get_state_string(self):
        """
         Returns a human readable string corresponding to the current communication state.
        """
        if self._comm is None:
            return "Connecting..."
        else:
            return self._comm.getStateString()


    def getCurrentFirmware(self):
        """
        Gets the current printer firmware version
        :return: string
        """
        if self._comm is not None:
            firmware_v = self._comm.getCommandsInterface().getFirmwareVersion()

            if firmware_v is not None:
                return firmware_v
            else:
                return 'Not available'
        else:
            return 'Not available'


    # # # # # # # # # # # # # # # # # # # # # # #
    ##########  CALLBACK FUNCTIONS  #############
    # # # # # # # # # # # # # # # # # # # # # # #
    def updateProgress(self, progressData):
        """
        Receives a progress data object from the BVC communication layer
        and updates the progress attributes

        :param progressData:
        :return:
        """
        if progressData is not None and self._selectedFile is not None:
            if 'Elapsed Time' in progressData:
                self._elapsedTime = progressData['Elapsed Time']
            if 'Estimated Time' in progressData:
                self._estimatedTime = progressData['Estimated Time']
            if 'Executed Lines' in progressData:
                self._executedLines = progressData['Executed Lines']
            if 'Lines' in progressData:
                self._numberLines = progressData['Lines']


    def on_comm_progress(self):
        """
         Callback method for the comm object, called upon any change in progress of the print job.
         Triggers storage of new values for printTime, printTimeLeft and the current progress.
        """
        if self._comm is not None:
            self._setProgressData(self.getPrintProgress(), self.getPrintFilepos(),
                                  self._comm.getPrintTime(), self._comm.getCleanedPrintTime())

            # If the status from the printer is no longer printing runs the post-print trigger
            if self.getPrintProgress() >= 1 \
                    and self._comm.getCommandsInterface().isPreparingOrPrinting() is False:

                # Runs the print finish communications callback
                self._comm.triggerPrintFinished()

                self._comm.getCommandsInterface().stopStatusMonitor()
                self._runningCalibrationTest = False


    def on_comm_file_selected(self, filename, filesize, sd):
        """
        Override callback function to allow for print halt when there is not enough filament
        :param filename:
        :param filesize:
        :param sd:
        :return:
        """
        self._setJobData(filename, filesize, sd)
        self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})

        # checks if the insufficient filament flag is true and halts the print process
        if self._insufficientFilamentForCurrent:
            self._printAfterSelect = False

        if self._printAfterSelect:
            self._printAfterSelect = False
            self.start_print(pos=self._posAfterSelect)

    def on_print_cancelled(self, event, payload):
        """
        Print cancelled callback for the EventManager.
        """
        self.unselect_file()

    def on_comm_state_change(self, state):
        """
        Callback method for the comm object, called if the connection state changes.
        """
        oldState = self._state

        # forward relevant state changes to gcode manager
        if oldState == BeeCom.STATE_PRINTING:
            self._analysisQueue.resume()  # printing done, put those cpu cycles to good use

        elif state == BeeCom.STATE_PRINTING:
            self._analysisQueue.pause()  # do not analyse files while printing

        elif state == BeeCom.STATE_CLOSED or state == BeeCom.STATE_CLOSED_WITH_ERROR:
            if self._comm is not None:
                self._comm = None

        self._setState(state)

    # # # # # # # # # # # # # # # # # # # # # # #
    ########### AUXILIARY FUNCTIONS #############
    # # # # # # # # # # # # # # # # # # # # # # #

    def _setJobData(self, filename, filesize, sd):
        super(BeePrinter, self)._setJobData(filename, filesize, sd)

        self._checkSufficientFilamentForPrint()


    def _getFilamentSettings(self):
        """
        Gets the necessary filament settings for weight/size conversions
        Returns tuple with (diameter,density)
        """
        # converts the amount of filament in grams to mm
        if self._currentFilamentProfile:
            # Fetches the first position filament_diameter from the filament data and converts to microns
            filament_diameter = self._currentFilamentProfile.data['filament_diameter'][0] * 1000
            # TODO: The filament density should also be set based on profile data
            filament_density = 1.275  # default value
        else:
            filament_diameter = 1.75 * 1000  # default value in microns
            filament_density = 1.275  # default value

        return filament_diameter, filament_density

    def _checkSufficientFilamentForPrint(self):
        """
        Checks if the current print job has enough filament to complete. By updating the
        job setting, it will automatically update the interface through the web socket
        :return:
        """
        # Gets the current print job data
        state_data = self._stateMonitor.get_current_data()

        if state_data and state_data['job'] and state_data['job']['filament']:
            # gets the filament information for the filament weight to be used in the print job
            filament = state_data['job']['filament']
            # gets the current amount of filament left in printer
            current_filament_length = self.getFilamentInSpool()

            # Signals that there is not enough filament
            if "tool0" in filament:
                if filament["tool0"]['length'] > current_filament_length:
                    filament["tool0"]['insufficient'] = True
                    self._insufficientFilamentForCurrent = True
                else:
                    filament["tool0"]['insufficient'] = False
                    self._insufficientFilamentForCurrent = False

    def _setProgressData(self, completion=None, filepos=None, printTime=None, printTimeLeft=None):
        """
        Auxiliar method to control the print progress status data
        :param completion:
        :param filepos:
        :param printTime:
        :param printTimeLeft:
        :return:
        """
        estimatedTotalPrintTime = self._estimateTotalPrintTime(completion, printTimeLeft)
        totalPrintTime = estimatedTotalPrintTime

        if self._selectedFile and "estimatedPrintTime" in self._selectedFile \
                and self._selectedFile["estimatedPrintTime"]:

            statisticalTotalPrintTime = self._selectedFile["estimatedPrintTime"]
            if completion and printTimeLeft:
                if estimatedTotalPrintTime is None:
                    totalPrintTime = statisticalTotalPrintTime
                else:
                    if completion < 0.5:
                        sub_progress = completion * 2
                    else:
                        sub_progress = 1.0
                    totalPrintTime = (1 - sub_progress) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime

        self._progress = completion
        self._printTime = printTime
        self._printTimeLeft = totalPrintTime - printTimeLeft if (totalPrintTime is not None and printTimeLeft is not None) else None

        self._stateMonitor.set_progress({
            "completion": self._progress * 100 if self._progress is not None else None,
            "filepos": filepos,
            "printTime": int(self._elapsedTime * 60) if self._elapsedTime is not None else None,
            "printTimeLeft": int(self._printTimeLeft) if self._printTimeLeft is not None else None
        })

        if completion:
            progress_int = int(completion * 100)
            if self._lastProgressReport != progress_int:
                self._lastProgressReport = progress_int
                self._reportPrintProgressToPlugins(progress_int)

    def _getStateFlags(self):
        return {
            "operational": self.is_operational(),
            "printing": self.is_printing(),
            "closedOrError": self.is_closed_or_error(),
            "error": self.is_error(),
            "paused": self.is_paused(),
            "ready": self.is_ready(),
            "sdReady": self.is_sd_ready(),
            "heating": self.is_heating(),
            "shutdown": self.is_shutdown()
        }