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 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)
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
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() }