class SmoothieHost(App): is_connected = BooleanProperty(False) status = StringProperty("Not Connected") wpos = ListProperty([0, 0, 0]) mpos = ListProperty([0, 0, 0, 0, 0, 0]) fr = NumericProperty(0) frr = NumericProperty(0) fro = NumericProperty(100) sr = NumericProperty(0) lp = NumericProperty(0) is_inch = BooleanProperty(False) is_spindle_on = BooleanProperty(False) is_abs = BooleanProperty(True) is_desktop = NumericProperty(0) is_cnc = BooleanProperty(False) tab_top = BooleanProperty(False) main_window = ObjectProperty() gcode_file = StringProperty() is_show_camera = BooleanProperty(False) is_spindle_camera = BooleanProperty(False) manual_tool_change = BooleanProperty(False) is_v2 = BooleanProperty(True) wait_on_m0 = BooleanProperty(False) # Factory.register('Comms', cls=Comms) def __init__(self, **kwargs): super(SmoothieHost, self).__init__(**kwargs) if len(sys.argv) > 1: # override com port self.use_com_port = sys.argv[1] else: self.use_com_port = None self.webserver = False self._blanked = False self.blank_timeout = 0 self.last_touch_time = 0 self.camera_url = None self.loaded_modules = [] self.secs = 0 self.fast_stream = False self.last_probe = {'X': 0, 'Y': 0, 'Z': 0, 'status': False} self.tool_scripts = ToolScripts() self.desktop_changed = False def build_config(self, config): config.setdefaults( 'General', { 'last_gcode_path': os.path.expanduser("~"), 'last_print_file': '', 'serial_port': 'serial:///dev/ttyACM0', 'report_rate': '1.0', 'blank_timeout': '0', 'manual_tool_change': 'false', 'wait_on_m0': 'false', 'fast_stream': 'false', 'v2': 'false', 'is_spindle_camera': 'false' }) config.setdefaults( 'UI', { 'display_type': "RPI Touch", 'cnc': 'false', 'tab_top': 'false', 'screen_size': 'auto', 'screen_pos': 'auto', 'filechooser': 'default' }) config.setdefaults( 'Extruder', { 'last_bed_temp': '60', 'last_hotend_temp': '185', 'length': '20', 'speed': '300', 'hotend_presets': '185 (PLA), 230 (ABS)', 'bed_presets': '60 (PLA), 110 (ABS)' }) config.setdefaults('Jog', {'xy_feedrate': '3000'}) config.setdefaults( 'Web', { 'webserver': 'false', 'show_video': 'false', 'camera_url': 'http://localhost:8080/?action=snapshot' }) def build_settings(self, settings): jsondata = """ [ { "type": "title", "title": "UI Settings" }, { "type": "options", "title": "Desktop Layout", "desc": "Select Display layout, RPI is for 7in touch screen layout", "section": "UI", "key": "display_type", "options": ["RPI Touch", "Small Desktop", "Large Desktop", "Wide Desktop", "RPI Full Screen"] }, { "type": "bool", "title": "CNC layout", "desc": "Turn on for a CNC layout, otherwise it is a 3D printer Layout", "section": "UI", "key": "cnc" }, { "type": "bool", "title": "Tabs on top", "desc": "TABS are on top of the screen", "section": "UI", "key": "tab_top" }, { "type": "options", "title": "File Chooser", "desc": "Which filechooser to use in desktop mode", "section": "UI", "key": "filechooser", "options": ["default", "wx", "zenity", "kdialog"] }, { "type": "title", "title": "General Settings" }, { "type": "numeric", "title": "Report rate", "desc": "Rate in seconds to query for status from Smoothie", "section": "General", "key": "report_rate" }, { "type": "numeric", "title": "Blank Timeout", "desc": "Inactive timeout in seconds before screen will blank", "section": "General", "key": "blank_timeout" }, { "type": "bool", "title": "Manual Tool change", "desc": "On M6 let user do a manual tool change", "section": "General", "key": "manual_tool_change" }, { "type": "bool", "title": "Wait on M0", "desc": "On M0 popup a dialog and pause until it is dismissed", "section": "General", "key": "wait_on_m0" }, { "type": "bool", "title": "Spindle Camera", "desc": "Enable the spindle camera screen", "section": "General", "key": "is_spindle_camera" }, { "type": "bool", "title": "Version 2 Smoothie", "desc": "Select for version 2 smoothie", "section": "General", "key": "v2" }, { "type": "bool", "title": "Fast Stream", "desc": "Allow fast stream for laser over network", "section": "General", "key": "fast_stream" }, { "type": "title", "title": "Web Settings" }, { "type": "bool", "title": "Web Server", "desc": "Turn on Web server to remotely check progress", "section": "Web", "key": "webserver" }, { "type": "bool", "title": "Show Video", "desc": "Display mjpeg video in web progress", "section": "Web", "key": "show_video" }, { "type": "string", "title": "Camera URL", "desc": "URL for camera stream", "section": "Web", "key": "camera_url" }, { "type": "title", "title": "Extruder Settings" }, { "type": "string", "title": "Hotend Presets", "desc": "Set the comma separated presets for the hotend temps", "section": "Extruder", "key": "hotend_presets" }, { "type": "string", "title": "Bed Presets", "desc": "Set the comma separated presets for the bed temps", "section": "Extruder", "key": "bed_presets" } ] """ settings.add_json_panel('SmooPie application', self.config, data=jsondata) def on_config_change(self, config, section, key, value): # print("config changed: {} - {}: {}".format(section, key, value)) token = (section, key) if token == ('UI', 'cnc'): self.is_cnc = value == "1" elif token == ('UI', 'display_type'): self.desktop_changed = True self.main_window.display("NOTICE: Restart is needed") elif token == ('UI', 'tab_top'): self.tab_top = value == "1" elif token == ('Extruder', 'hotend_presets'): self.main_window.ids.extruder.ids.set_hotend_temp.values = value.split( ',') elif token == ('Extruder', 'bed_presets'): self.main_window.ids.extruder.ids.set_bed_temp.values = value.split( ',') elif token == ('General', 'blank_timeout'): self.blank_timeout = float(value) elif token == ('General', 'manual_tool_change'): self.manual_tool_change = value == '1' elif token == ('General', 'wait_on_m0'): self.wait_on_m0 = value == '1' elif token == ('General', 'v2'): self.is_v2 = value == '1' elif token == ('Web', 'camera_url'): self.camera_url = value else: self.main_window.display("NOTICE: Restart is needed") def on_stop(self): # The Kivy event loop is about to stop, stop the async main loop self.comms.stop() # stop the aysnc loop if self.is_webserver: self.webserver.stop() if self.blank_timeout > 0: # unblank if blanked self.unblank_screen() # stop any loaded modules for m in self.loaded_modules: m.stop() def on_start(self): # in case we added something to the defaults, make sure they are written to the ini file self.config.update_config('smoothiehost.ini') def window_request_close(self, win): if self.desktop_changed: # if the desktop changed we reset the window size and pos self.config.set('UI', 'screen_size', 'auto') self.config.set('UI', 'screen_pos', 'auto') self.config.write() elif self.is_desktop == 2 or self.is_desktop == 3: # Window.size is automatically adjusted for density, must divide by density when saving size self.config.set( 'UI', 'screen_size', "{}x{}".format(int(Window.size[0] / Metrics.density), int(Window.size[1] / Metrics.density))) self.config.set('UI', 'screen_pos', "{},{}".format(Window.top, Window.left)) Logger.info( 'close: Window.size: {}, Window.top: {}, Window.left: {}'. format(Window.size, Window.top, Window.left)) self.config.write() return False def build(self): lt = self.config.get('UI', 'display_type') dtlut = { "RPI Touch": 0, "Small Desktop": 1, "Large Desktop": 2, "Wide Desktop": 3, "RPI Full Screen": 4 } self.is_desktop = dtlut.get(lt, 0) # load the layouts for the desktop screen if self.is_desktop == 1: Builder.load_file('desktop.kv') Window.size = (1024, 768) elif self.is_desktop == 2 or self.is_desktop == 3 or self.is_desktop == 4: Builder.load_file('desktop_large.kv' if self.is_desktop == 2 else 'desktop_wide.kv') if self.is_desktop != 4: # because rpi_egl does not like to be told the size s = self.config.get('UI', 'screen_size') if s == 'auto': Window.size = (1280, 1024) if self.is_desktop == 2 else (1280, 800) elif 'x' in s: (w, h) = s.split('x') Window.size = (int(w), int(h)) p = self.config.get('UI', 'screen_pos') if p != 'auto' and ',' in p: (t, l) = p.split(',') Window.top = int(t) Window.left = int(l) Window.bind(on_request_close=self.window_request_close) else: self.is_desktop = 0 # load the layouts for rpi 7" touch screen Builder.load_file('rpi.kv') self.is_cnc = self.config.getboolean('UI', 'cnc') self.tab_top = self.config.getboolean('UI', 'tab_top') self.is_webserver = self.config.getboolean('Web', 'webserver') self.is_show_camera = self.config.getboolean('Web', 'show_video') self.is_spindle_camera = self.config.getboolean( 'General', 'is_spindle_camera') self.manual_tool_change = self.config.getboolean( 'General', 'manual_tool_change') self.wait_on_m0 = self.config.getboolean('General', 'wait_on_m0') self.is_v2 = self.config.getboolean('General', 'v2') self.comms = Comms(App.get_running_app(), self.config.getfloat('General', 'report_rate')) self.gcode_file = self.config.get('General', 'last_print_file') self.sm = ScreenManager() ms = MainScreen(name='main') self.main_window = ms.ids.main_window self.sm.add_widget(ms) self.sm.add_widget(GcodeViewerScreen(name='viewer', comms=self.comms)) self.config_editor = ConfigEditor(name='config_editor') self.sm.add_widget(self.config_editor) self.gcode_help = GcodeHelp(name='gcode_help') self.sm.add_widget(self.gcode_help) if self.is_desktop == 0: self.text_editor = TextEditor(name='text_editor') self.sm.add_widget(self.text_editor) self.blank_timeout = self.config.getint('General', 'blank_timeout') Logger.info("SmoothieHost: screen blank set for {} seconds".format( self.blank_timeout)) self.sm.bind(on_touch_down=self._on_touch) Clock.schedule_interval(self._every_second, 1) # select the file chooser to use # select which one we want from config filechooser = self.config.get('UI', 'filechooser') if self.is_desktop > 0: if filechooser != 'default': NativeFileChooser.type_name = filechooser Factory.register('filechooser', cls=NativeFileChooser) try: f = Factory.filechooser() except Exception: Logger.error( "SmoothieHost: can't use selected file chooser: {}". format(filechooser)) Factory.unregister('filechooser') Factory.register('filechooser', cls=FileDialog) else: # use Kivy filechooser Factory.register('filechooser', cls=FileDialog) # we want to capture arrow keys Window.bind(on_key_down=self._on_keyboard_down) else: # use Kivy filechooser Factory.register('filechooser', cls=FileDialog) # setup for cnc or 3d printer if self.is_cnc: if self.is_desktop < 3: # remove Extruder panel from tabpanel and tab self.main_window.ids.tabs.remove_widget( self.main_window.ids.tabs.extruder_tab) # if not CNC mode then do not show the ZABC buttons in jogrose if not self.is_cnc: self.main_window.ids.tabs.jog_rose.jogrosemain.remove_widget( self.main_window.ids.tabs.jog_rose.abc_panel) if self.is_webserver: self.webserver = ProgressServer() self.webserver.start(self, 8000) if self.is_show_camera: self.camera_url = self.config.get('Web', 'camera_url') self.sm.add_widget(CameraScreen(name='web cam')) self.main_window.tools_menu.add_widget( ActionButton(text='Web Cam', on_press=lambda x: self._show_web_cam())) if self.is_spindle_camera: if self.is_desktop in [0, 4]: try: self.sm.add_widget(SpindleCamera(name='spindle camera')) except Exception as err: self.main_window.display( 'ERROR: failed to load spindle camera. Check logs') Logger.error( 'Main: spindle camera exception: {}'.format(err)) self.main_window.tools_menu.add_widget( ActionButton(text='Spindle Cam', on_press=lambda x: self._show_spindle_cam())) # load any modules specified in config self._load_modules() if self.blank_timeout > 0: # unblank if blanked self.unblank_screen() return self.sm def _show_spindle_cam(self): if self.is_desktop in [0, 4]: self.sm.current = "spindle camera" else: # we run it as a separate program so it is in its own window subprocess.Popen(['python3', 'spindle_camera.py'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def _show_web_cam(self): self.sm.current = "web cam" def _on_keyboard_down(self, instance, key, scancode, codepoint, modifiers): # print("key: {}, scancode: {}, codepoint: {}, modifiers: {}".format(key, scancode, codepoint, modifiers)) # control uses finer move, shift uses coarse move v = 0.1 if len(modifiers) == 1: if modifiers[0] == 'ctrl': v = 0.01 elif modifiers[0] == 'shift': v = 1 choices = { 273: "Y{}".format(v), 275: "X{}".format(v), 274: "Y{}".format(-v), 276: "X{}".format(-v), 280: "Z{}".format(v), 281: "Z{}".format(-v) } s = choices.get(key, None) if s is not None: self.comms.write('$J {}\n'.format(s)) return True # handle command history if in desktop mode if self.is_desktop > 0: if v == 0.01: # it is a control key if codepoint == 'p': # get previous history by finding all the recently sent commands history = [ x['text'] for x in self.main_window.ids.log_window.data if x['text'].startswith('<< ') ] if history: last = history.pop() self.main_window.ids.entry.text = last[3:] elif codepoint == 'n': # get next history pass elif codepoint == 'c': # clear console self.main_window.ids.log_window.data = [] return False def command_input(self, s): if s.startswith('!'): # shell command send to unix shell self.main_window.display('> {}'.format(s)) try: p = subprocess.Popen(s[1:], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True) result, err = p.communicate() for l in result.splitlines(): self.main_window.display(l) for l in err.splitlines(): self.main_window.display(l) if p.returncode != 0: self.main_window.display('returncode: {}'.format( p.returncode)) except Exception as err: self.main_window.display('> command exception: {}'.format(err)) elif s == '?': self.gcode_help.populate() self.sm.current = 'gcode_help' else: self.main_window.display('<< {}'.format(s)) self.comms.write('{}\n'.format(s)) # when we hit enter it refocuses the the input def _refocus_text_input(self, *args): Clock.schedule_once(self._refocus_it) def _refocus_it(self, *args): self.main_window.ids.entry.focus = True def _load_modules(self): if not self.config.has_section('modules'): return try: for key in self.config['modules']: Logger.info("load_modules: loading module {}".format(key)) mod = importlib.import_module('modules.{}'.format(key)) if mod.start(self.config['modules'][key]): Logger.info("load_modules: loaded module {}".format(key)) self.loaded_modules.append(mod) else: Logger.info( "load_modules: module {} failed to start".format(key)) except Exception: Logger.warn("load_modules: exception: {}".format( traceback.format_exc())) def _every_second(self, dt): ''' called every second ''' self.secs += 1 if self.blank_timeout > 0 and not self.main_window.is_printing: self.last_touch_time += 1 if self.last_touch_time >= self.blank_timeout: self.last_touch_time = 0 self.blank_screen() def blank_screen(self): try: with open('/sys/class/backlight/rpi_backlight/bl_power', 'w') as f: f.write('1\n') self._blanked = True except Exception: Logger.warning("SmoothieHost: unable to blank screen") def unblank_screen(self): try: with open('/sys/class/backlight/rpi_backlight/bl_power', 'w') as f: f.write('0\n') except Exception: pass def _on_touch(self, a, b): self.last_touch_time = 0 if self._blanked: self._blanked = False self.unblank_screen() return True return False