diff --git a/.gitignore b/.gitignore index ecfcd6f..e68deef 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist *.egg* .DS_Store *.zip +make.sh diff --git a/README.md b/README.md index 094563d..2834c8f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ # OctoPrint-FilamentReloaded +## Issue Tracking + +I have a day job, so when filing an issue please tag me @nickmitchko in the issue so I get an email. + +## Overview + [OctoPrint](http://octoprint.org/) plugin that integrates with a filament sensor hooked up to a Raspberry Pi GPIO pin and allows the filament spool to be changed during a print if the filament runs out. Future developments are planned to include multiple filament sensors and pop-ups. -Initial work based on the [Octoprint-Filament](https://github.com/MoonshineSG/Octoprint-Filament) plugin by MoonshineSG. +Initial work based on the [Octoprint-Filament](https://github.com/MoonshineSG/Octoprint-Filament) plugin by MoonshineSG and [Octoprint-Reloaded] (https://github.com/kontakt/Octoprint-Filament-Reloaded/archive/master.zip) plugin by kontakt. ## Required sensor @@ -12,13 +18,16 @@ Using this plugin requires a filament sensor. The code is set to use the Raspber This plugin is using the GPIO.BOARD numbering scheme, the pin being used needs to be selected by the physical pin number. +_A DIY guide is in planned for new sensor users_ + ## Features -* Configurable GPIO pin. +* Configurable GPIO pin (including the type of resistor on the pin) * Debounce noisy sensors. * Support normally open and normally closed sensors. * Execution of custom GCODE when out of filament detected. * Optionally pause print when out of filament. +* Icon in the nav bar to reflect filament detection status. (with check frequency in parameters) An API is available to check the filament sensor status via a GET method to `/plugin/filamentreload/status` which returns a JSON @@ -29,7 +38,7 @@ An API is available to check the filament sensor status via a GET method to `/pl ## Installation * Install via the bundled [Plugin Manager](https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager). -* Manually using this URL: https://github.com/kontakt/Octoprint-Filament-Reloaded/archive/master.zip +* Manually using this URL: https://github.com/nickmitchko/Octoprint-Filament-Reloaded/archive/master.zip ## Configuration diff --git a/octoprint_filamentreload/__init__.py b/octoprint_filamentreload/__init__.py index 31e0b30..10b8ab5 100644 --- a/octoprint_filamentreload/__init__.py +++ b/octoprint_filamentreload/__init__.py @@ -1,25 +1,65 @@ # coding=utf-8 from __future__ import absolute_import - import octoprint.plugin from octoprint.events import Events import RPi.GPIO as GPIO from time import sleep from flask import jsonify +from threading import Thread class FilamentReloadedPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.EventHandlerPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, - octoprint.plugin.BlueprintPlugin): + octoprint.plugin.AssetPlugin, + octoprint.plugin.BlueprintPlugin + ): + active = 0 + class filamentStatusWatcher(Thread): + + running = False + + def __init__(self): + Thread.__init__(self) + self.wCurrentState = -1 + + def populate(self, wPluginManager, wIdentifier ,wCheckRate, wLogger): + self._logger=wLogger + self.wPluginManager = wPluginManager + self.wIdentifier = wIdentifier + self.wCheckRate = wCheckRate + + def run(self): + self.running= True + while self.running==True: + self.updateIcon() + sleep(self.wCheckRate/1000) + + def stopWatch(self): + if self.running==True: + self.running=False + + def updateIcon(self): + if self.wCurrentState==0: + self._logger.debug("Thread: Update icon 0") + self.wPluginManager.send_plugin_message(self.wIdentifier, dict(filamentStatus="empty")) + elif self.wCurrentState==1: + self._logger.debug("Thread: Update icon 1") + self.wPluginManager.send_plugin_message(self.wIdentifier, dict(filamentStatus="present")) + elif self.wCurrentState==-1: + self._logger.debug("Thread: Update icon 2") + self.wPluginManager.send_plugin_message(self.wIdentifier, dict(filamentStatus="unknown")) + + filamentStatusWatcher = filamentStatusWatcher() def initialize(self): - self._logger.info("Running RPi.GPIO version '{0}'".format(GPIO.VERSION)) + self._logger.info( + "Running RPi.GPIO version '{0}'".format(GPIO.VERSION)) if GPIO.VERSION < "0.6": # Need at least 0.6 for edge detection raise Exception("RPi.GPIO must be greater than 0.6") GPIO.setwarnings(False) # Disable GPIO warnings - + self.pin_value = -1 # Cache the pin value when we detect out of filament @octoprint.plugin.BlueprintPlugin.route("/status", methods=["GET"]) def check_status(self): @@ -36,6 +76,10 @@ def pin(self): def bounce(self): return int(self._settings.get(["bounce"])) + @property + def checkrate(self): + return int(self._settings.get(["checkrate"])) + @property def switch(self): return int(self._settings.get(["switch"])) @@ -44,6 +88,10 @@ def switch(self): def mode(self): return int(self._settings.get(["mode"])) + @property + def pullup(self): + return int(self._settings.get(["pullup"])) + @property def no_filament_gcode(self): return str(self._settings.get(["no_filament_gcode"])).splitlines() @@ -52,6 +100,10 @@ def no_filament_gcode(self): def pause_print(self): return self._settings.get_boolean(["pause_print"]) + @property + def prevent_print(self): + return self._settings.get_boolean(["prevent_print"]) + @property def send_gcode_only_once(self): return self._settings.get_boolean(["send_gcode_only_once"]) @@ -65,10 +117,34 @@ def _setup_sensor(self): else: self._logger.info("Using BCM Mode") GPIO.setmode(GPIO.BCM) + self._logger.info("Filament Sensor active on GPIO Pin [%s]"%self.pin) - GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + if self.pullup == 0: + GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) + self._logger.info("Filament Sensor Pin uses pullup") + else: + GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + self._logger.info("Filament Sensor Pin uses pulldown") + + if self.filamentStatusWatcher.running == False: + self.filamentStatusWatcher.populate(self._plugin_manager, self._identifier, self.checkrate,self._logger) + self.filamentStatusWatcher.daemon = True + self.filamentStatusWatcher.start() + else: + self._logger.info("Setting new checkrate") + self.filamentStatusWatcher.wCheckRate = self.checkrate + self.no_filament()#to update the watcher's status + + GPIO.remove_event_detect(self.pin) + GPIO.add_event_detect( + self.pin, GPIO.BOTH, + callback=self.sensor_callback, + bouncetime=self.bounce + ) else: - self._logger.info("Pin not configured, won't work unless configured!") + self._logger.info( + "Pin not configured, won't work unless configured!") def on_after_startup(self): self._logger.info("Filament Sensor Reloaded started") @@ -76,13 +152,16 @@ def on_after_startup(self): def get_settings_defaults(self): return dict( - pin = -1, # Default is no pin - bounce = 250, # Debounce 250ms - switch = 0, # Normally Open - mode = 0, # Board Mode - no_filament_gcode = '', - pause_print = True, - send_gcode_only_once = False, # Default set to False for backward compatibility + pin=-1, # Default is no pin + bounce=250, # Debounce 250ms + switch=0, # Normally Open + mode=0, # Board Mode + pullup=0, # Pullup or Pull Down - default is Pull up + no_filament_gcode='', + pause_print=True, + prevent_print=True, + send_gcode_only_once=False, # Default set to False for backward compatibility + checkrate = 1500, #navbar icon check frequency ) def on_settings_save(self, data): @@ -96,54 +175,93 @@ def sensor_enabled(self): return self.pin != -1 def no_filament(self): - return GPIO.input(self.pin) != self.switch + nofilament = GPIO.input(self.pin) != self.switch + self.filamentStatusWatcher.wCurrentState= int(not(nofilament)) + return nofilament + + ##~~ AssetPlugin mixin + def get_assets(self): + return { + "js": ["js/filamentreload.js"], + "less": ["less/filamentreload.less"], + "css": ["css/filamentreload.css"] + } + # return dict(js=["js/filamentreload.js"],css=["css/filamentreload.css"],less=["less/psucontrol.less"],) + def get_template_configs(self): - return [dict(type="settings", custom_bindings=False)] + return [ + dict(type="navbar", custom_bindings=False), + dict(type="settings", custom_bindings=False) + ] def on_event(self, event, payload): # Early abort in case of out ot filament when start printing, as we # can't change with a cold nozzle - if event is Events.PRINT_STARTED and self.no_filament(): + if event is Events.PRINT_STARTED and self.no_filament() and self.prevent_print: self._logger.info("Printing aborted: no filament detected!") self._printer.cancel_print() + # Enable sensor if event in ( Events.PRINT_STARTED, Events.PRINT_RESUMED ): + if self.prevent_print and self.no_filament(): + self._logger.info( + "Printing paused: request to resume but no filament detected!") + self._printer.pause_print() self._logger.info("%s: Enabling filament sensor." % (event)) if self.sensor_enabled(): self.triggered = 0 # reset triggered state + self.active = 1 GPIO.remove_event_detect(self.pin) + self._logger.info("Filament present, print starting") GPIO.add_event_detect( - self.pin, GPIO.BOTH, - callback=self.sensor_callback, - bouncetime=self.bounce - ) + self.pin, GPIO.BOTH, + callback=self.sensor_callback, + bouncetime=self.bounce + ) # Disable sensor elif event in ( Events.PRINT_DONE, Events.PRINT_FAILED, Events.PRINT_CANCELLED, + Events.PRINT_PAUSED, Events.ERROR ): self._logger.info("%s: Disabling filament sensor." % (event)) - GPIO.remove_event_detect(self.pin) + self.active = 0 def sensor_callback(self, _): sleep(self.bounce/1000) + pin_triggered = GPIO.input(self.pin) - # If we have previously triggered a state change we are still out + self._logger.info("The value of the pin is {}. No filament = {} input = {}".format(pin_triggered, self.no_filament(), _)) + if not self.active: + self._logger.debug("Sensor callback but no active sensor.") + return + # If we have previously triggered a state change we are still out # of filament. Log it and wait on a print resume or a new print job. if self.sensor_triggered(): - self._logger.info("Sensor callback but no trigger state change.") - return + self._logger.info("Sensor callback with triggered set") + # + # Check to see if this is a spurious call back by the GPIO change system. We have cached the + # value of the sensor in self.pin_value. If they are the same then we simply return + if self.pin_value == pin_triggered: + self._logger.info("Looks like we had one spurious callback , nothing to do, return") + return + else: + self._logger.info("The pin is different lets process it.") - # Set the triggered flag to check next callback - self.triggered = 1 + self.pin_value = pin_triggered if self.no_filament(): + if self.triggered == 1: + self._logger.info("Waiting for filament...") + return + # Set the triggered flag to check next callback + self.triggered = 1 self._logger.info("Out of filament!") if self.send_gcode_only_once: self._logger.info("Sending GCODE only once...") @@ -157,8 +275,9 @@ def sensor_callback(self, _): self._logger.info("Sending out of filament GCODE") self._printer.commands(self.no_filament_gcode) else: - self._logger.info("Filament detected!") - + self._logger.debug("Filament detected!") + # Set the triggered flag to check next callbacks + self.triggered = 0 def get_update_information(self): return dict( @@ -168,17 +287,19 @@ def get_update_information(self): # version check: github repository type="github_release", - user="kontakt", + user="ssorgatem", repo="Octoprint-Filament-Reloaded", current=self._plugin_version, # update method: pip - pip="https://github.com/kontakt/Octoprint-Filament-Reloaded/archive/{target_version}.zip" - ) - ) + pip="https://github.com/ssorgatem/Octoprint-Filament-Reloaded/archive/{target_version}.zip" + )) + __plugin_name__ = "Filament Sensor Reloaded" -__plugin_version__ = "1.0.2" +__plugin_version__ = "1.4.0" +__plugin_pythoncompat__ = ">=2.7,<4" + def __plugin_load__(): global __plugin_implementation__ @@ -187,4 +308,4 @@ def __plugin_load__(): global __plugin_hooks__ __plugin_hooks__ = { "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information -} + } diff --git a/octoprint_filamentreload/static/css/filamentreload.css b/octoprint_filamentreload/static/css/filamentreload.css new file mode 100644 index 0000000..a849379 --- /dev/null +++ b/octoprint_filamentreload/static/css/filamentreload.css @@ -0,0 +1,3 @@ +#filament_indicator.present i{color:#00FF00} +#filament_indicator.empty i{color:#FF8C00} +#filament_indicator.unknown i{color:grey} diff --git a/octoprint_filamentreload/static/js/filamentreload.js b/octoprint_filamentreload/static/js/filamentreload.js new file mode 100644 index 0000000..3c6f0ed --- /dev/null +++ b/octoprint_filamentreload/static/js/filamentreload.js @@ -0,0 +1,34 @@ +$(function() { + function FilamentReloadedViewModel(parameters) { + var self = this; + + self.filamentIcon = $("#filament_indicator") + + + self.onDataUpdaterPluginMessage = function(plugin, data) { + if (plugin != "filamentreload") { + return; + } + self.updateIcon(data.filamentStatus); + + }; + + + self.updateIcon = function(data){ + self.filamentIcon.removeClass("present").removeClass("empty").removeClass("unknown"); + self.filamentIcon.addClass(data); + } + + + } + OCTOPRINT_VIEWMODELS.push([ + FilamentReloadedViewModel, + + // e.g. loginStateViewModel, settingsViewModel, ... + [ "settingsViewModel" ], + + // e.g. #settings_plugin_discordremote, #tab_plugin_octorant, ... + [ "#navbar_plugin_filament_indicator" ] + ]); + +}); diff --git a/octoprint_filamentreload/static/less/filamentreload.less b/octoprint_filamentreload/static/less/filamentreload.less new file mode 100644 index 0000000..f95ee70 --- /dev/null +++ b/octoprint_filamentreload/static/less/filamentreload.less @@ -0,0 +1,15 @@ +@filament_indicator_present: #00FF00; +@filament_indicator_empty: #FF8C00; +@filament_indicator_unknown: grey; + +#filament_indicator { + &.present { + color: @filament_indicator_present; + } + &.empty { + color: @filament_indicator_empty; + } + &.unknown { + color: @filament_indicator_unknown; + } +} diff --git a/octoprint_filamentreload/templates/filamentreload_navbar.jinja2 b/octoprint_filamentreload/templates/filamentreload_navbar.jinja2 new file mode 100644 index 0000000..eb1dcec --- /dev/null +++ b/octoprint_filamentreload/templates/filamentreload_navbar.jinja2 @@ -0,0 +1,3 @@ + + + diff --git a/octoprint_filamentreload/templates/filamentreload_settings.jinja2 b/octoprint_filamentreload/templates/filamentreload_settings.jinja2 index 35bdaf7..939d84b 100644 --- a/octoprint_filamentreload/templates/filamentreload_settings.jinja2 +++ b/octoprint_filamentreload/templates/filamentreload_settings.jinja2 @@ -13,6 +13,13 @@ ms +