diff --git a/.editorconfig b/.editorconfig index 82c8e05..df59181 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,8 @@ insert_final_newline = true trim_trailing_whitespace = true [**.py] -indent_style = tab +indent_style = space +indent_size = 4 [**.js] indent_style = space diff --git a/README.md b/README.md index 62779ac..14207be 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Install via the bundled [Plugin Manager](https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager) or manually using this URL: - https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective/archive/master.zip + https://github.com/TheSpaghettiDetective/TheSpaghettiDetective/archive/master.zip **TODO:** Describe how to install your plugin, if more needs to be done than just installing it via pip or through the plugin manager. diff --git a/extras/README.txt b/extras/README.txt index 08c6ea8..c51c1b7 100644 --- a/extras/README.txt +++ b/extras/README.txt @@ -1,6 +1,6 @@ Currently Cookiecutter generates the following helpful extras to this folder: -TheSpaghettiDetective.md +thespaghettidetective.md Data file for plugins.octoprint.org. Fill in the missing TODOs once your plugin is ready for release and file a PR as described at http://plugins.octoprint.org/help/registering/ to get it published. diff --git a/extras/TheSpaghettiDetective.md b/extras/TheSpaghettiDetective.md index 4a76ee4..a1e2bf0 100644 --- a/extras/TheSpaghettiDetective.md +++ b/extras/TheSpaghettiDetective.md @@ -1,18 +1,18 @@ --- layout: plugin -id: TheSpaghettiDetective +id: thespaghettidetective title: TheSpaghettiDetective -description: OctoPrint plugin for The Spaghetti Detective -author: Kenneth Jiang +description: TODO +author: The Spaghetti Detective license: AGPLv3 # TODO date: today's date in format YYYY-MM-DD, e.g. 2015-04-21 -homepage: https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective -source: https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective -archive: https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective/archive/master.zip +homepage: https://github.com/TheSpaghettiDetective/TheSpaghettiDetective +source: https://github.com/TheSpaghettiDetective/TheSpaghettiDetective +archive: https://github.com/TheSpaghettiDetective/TheSpaghettiDetective/archive/master.zip # TODO # Set this to true if your plugin uses the dependency_links setup parameter to include @@ -86,4 +86,4 @@ compatibility: --- **TODO**: Longer description of your plugin, configuration examples etc. This part will be visible on the page at -http://plugins.octoprint.org/plugin/TheSpaghettiDetective/ +http://plugins.octoprint.org/plugin/thespaghettidetective/ diff --git a/octoprint_thespaghettidetective/__init__.py b/octoprint_thespaghettidetective/__init__.py index f0b9f5b..2733169 100644 --- a/octoprint_thespaghettidetective/__init__.py +++ b/octoprint_thespaghettidetective/__init__.py @@ -4,6 +4,9 @@ import threading import os, sys, time import requests +import backoff + +from .webcam_capture import capture_jpeg ### (Don't forget to remove me) # This is a basic skeleton for your plugin's __init__.py. You probably want to adjust the class name of your plugin @@ -17,70 +20,125 @@ _logger = logging.getLogger(__name__) +POLL_INTERVAL_SECONDS = 30 class TheSpaghettiDetectivePlugin(octoprint.plugin.SettingsPlugin, - octoprint.plugin.StartupPlugin, - octoprint.plugin.EventHandlerPlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.TemplatePlugin): - - def get_template_configs(self): - return [ - dict(type="settings", custom_bindings=False) - ] - - ##~~ SettingsPlugin mixin - - def get_settings_defaults(self): - return dict( - endpoint_prefix="https://app.gofab.xyz/" - ) - - ##~~ AssetPlugin mixin - - def get_assets(self): - # Define your plugin's asset files to automatically include in the - # core UI here. - return dict( - js=["js/TheSpaghettiDetective.js"], - css=["css/TheSpaghettiDetective.css"], - less=["less/TheSpaghettiDetective.less"] - ) - - ##~~ Softwareupdate hook - - def get_update_information(self): - # Define the configuration for your plugin to use with the Software Update - # Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update - # for details. - return dict( - TheSpaghettiDetective=dict( - displayName="TheSpaghettiDetective Plugin", - displayVersion=self._plugin_version, - - # version check: github repository - type="github_release", - user="kennethjiang", - repo="OctoPrint-TheSpaghettiDetective", - current=self._plugin_version, - - # update method: pip - pip="https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective/archive/{target_version}.zip" - ) - ) + octoprint.plugin.StartupPlugin, + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.AssetPlugin, + octoprint.plugin.TemplatePlugin): + + def get_template_configs(self): + return [ + dict(type="settings", custom_bindings=False) + ] + + ##~~ SettingsPlugin mixin + + def get_settings_defaults(self): + return dict( + endpoint_prefix='' + ) + + ##~~ AssetPlugin mixin + + def get_assets(self): + # Define your plugin's asset files to automatically include in the + # core UI here. + return dict( + js=["js/TheSpaghettiDetective.js"], + css=["css/TheSpaghettiDetective.css"], + less=["less/TheSpaghettiDetective.less"] + ) + + ##~~ Softwareupdate hook + + def get_update_information(self): + # Define the configuration for your plugin to use with the Software Update + # Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update + # for details. + return dict( + TheSpaghettiDetective=dict( + displayName="TheSpaghettiDetective Plugin", + displayVersion=self._plugin_version, + + # version check: github repository + type="github_release", + user="kennethjiang", + repo="OctoPrint-TheSpaghettiDetective", + current=self._plugin_version, + + # update method: pip + pip="https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective/archive/{target_version}.zip" + ) + ) + + ##~~Startup Plugin + + def on_after_startup(self): + main_thread = threading.Thread(target=self.main_loop) + main_thread.daemon = True + main_thread.start() + + + ## Private methods + + def octoprint_data(self): + data = self._printer.get_current_data() + data['temperatures'] = self._printer.get_current_temperatures() + data['octoprint_port'] = self._octoprint_port + data['octoprint_ip'] = self._octoprint_ip + return data + + def main_loop(self): + last_poll = 0 + while True: + print(self._settings) + print(self._settings.get(["endpoint_prefix"])) + if not self._settings.get(["endpoint_prefix"]) or not self._settings.get(["auth_token"]): + next + + if last_poll < time.time() - POLL_INTERVAL_SECONDS: + last_poll = time.time() + print("here!") + #self.post_jpg_to_endpoint() + + time.sleep(1) + + @backoff.on_exception(backoff.expo, Exception, max_value=240) + def post_jpg_to_endpoint(self): + endpoint_prefix = self._settings.get(["endpoint_prefix"]).strip() + if endpoint_prefix.endswith('/'): + endpoint_prefix = endpoint_prefix[:-1] + + endpoint = endpoint_prefix + '/dev/predict' + + files = {'image': capture_jpeg(self._settings.global_get(["webcam"]))} + + resp = requests.post( endpoint, files=files) + resp.raise_for_status() + for command in resp.json(): + if command["command"] == "print": + self.download_and_print(command["data"]["file_url"], command["data"]["file_name"]) + if command["command"] == "cancel": + self._printer.cancel_print() + if command["command"] == "pause": + self._printer.pause_print() + if command["command"] == "resume": + self._printer.resume_print() # If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py # ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that # can be overwritten via __plugin_xyz__ control properties. See the documentation for that. -__plugin_name__ = "TheSpaghettiDetective Plugin" +__plugin_name__ = "The Spaghetti Detective" def __plugin_load__(): - global __plugin_implementation__ - __plugin_implementation__ = TheSpaghettiDetectivePlugin() + global __plugin_implementation__ + __plugin_implementation__ = TheSpaghettiDetectivePlugin() - global __plugin_hooks__ - __plugin_hooks__ = { - "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information - } + global __plugin_hooks__ + __plugin_hooks__ = { + "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information + } diff --git a/octoprint_thespaghettidetective/static/js/TheSpaghettiDetective.js b/octoprint_thespaghettidetective/static/js/TheSpaghettiDetective.js index 4f8a331..e14d27a 100644 --- a/octoprint_thespaghettidetective/static/js/TheSpaghettiDetective.js +++ b/octoprint_thespaghettidetective/static/js/TheSpaghettiDetective.js @@ -1,7 +1,7 @@ /* * View model for TheSpaghettiDetective * - * Author: Kenneth Jiang + * Author: The Spaghetti Detective * License: AGPLv3 */ $(function() { @@ -23,7 +23,7 @@ $(function() { construct: ThespaghettidetectiveViewModel, // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ... dependencies: [ /* "loginStateViewModel", "settingsViewModel" */ ], - // Elements to bind to, e.g. #settings_plugin_TheSpaghettiDetective, #tab_plugin_TheSpaghettiDetective, ... + // Elements to bind to, e.g. #settings_plugin_thespaghettidetective, #tab_plugin_thespaghettidetective, ... elements: [ /* ... */ ] }); }); diff --git a/octoprint_thespaghettidetective/templates/thespaghettidetective_settings.jinja2 b/octoprint_thespaghettidetective/templates/thespaghettidetective_settings.jinja2 index 08b0e05..eb6733f 100644 --- a/octoprint_thespaghettidetective/templates/thespaghettidetective_settings.jinja2 +++ b/octoprint_thespaghettidetective/templates/thespaghettidetective_settings.jinja2 @@ -1,9 +1,14 @@
- +
- + +
+
+
+ +
+
- diff --git a/octoprint_thespaghettidetective/webcam_capture.py b/octoprint_thespaghettidetective/webcam_capture.py new file mode 100644 index 0000000..0f7ed2d --- /dev/null +++ b/octoprint_thespaghettidetective/webcam_capture.py @@ -0,0 +1,62 @@ +# coding=utf-8 +from __future__ import absolute_import +from datetime import datetime, timedelta +import time +import logging +import StringIO +import re +import urllib2 +from urlparse import urlparse +from contextlib import closing +import requests +import backoff + +_logger = logging.getLogger(__name__) + +@backoff.on_exception(backoff.expo, Exception, max_value=1200) +@backoff.on_predicate(backoff.expo, max_value=1200) +def capture_jpeg(settings): + snapshot_url = settings.get("snapshot", '').strip() + if snapshot_url: + if not urlparse(snapshot_url).scheme: + snapshot_url = "http://localhost/" + re.sub(r"^\/", "", snapshot_url) + + with closing(urllib2.urlopen(snapshot_url)) as res: + jpg = res.read() + return "--boundarydonotcross\r\nContent-Type: image/jpeg\r\nContent-Length: {0}\r\n\r\n{1}\r\n".format(len(jpg), jpg) + + else: + stream_url = settings.get("stream", "/webcam/?action=stream").strip() + if not urlparse(stream_url).scheme: + stream_url = "http://localhost/" + re.sub(r"^\/", "", stream_url) + + with closing(urllib2.urlopen(stream_url)) as res: + chunker = MjpegStreamChunker() + + while True: + data = res.readline() + mjpg = chunker.findMjpegChunk(data) + if mjpg: + res.close() + return mjpg + + +class MjpegStreamChunker: + + def __init__(self): + self.boundary = None + self.current_chunk = StringIO.StringIO() + + # Return: mjpeg chunk if found + # None: in the middle of the chunk + def findMjpegChunk(self, line): + if not self.boundary: # The first time endOfChunk should be called with 'boundary' text as input + self.boundary = line + self.current_chunk.write(line) + return None + + if len(line) == len(self.boundary) and line == self.boundary: # start of next chunk + return self.current_chunk.getvalue() + + self.current_chunk.write(line) + return None diff --git a/setup.py b/setup.py index ee17351..6d9565e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ ### Do not forget to adjust the following variables to your own plugin. # The plugin's identifier, has to be unique -plugin_identifier = "TheSpaghettiDetective" +plugin_identifier = "thespaghettidetective" # The plugin's python package, should be "octoprint_", has to be unique plugin_package = "octoprint_thespaghettidetective" @@ -18,22 +18,22 @@ # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module -plugin_description = """OctoPrint plugin for The Spaghetti Detective""" +plugin_description = """TODO""" # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module -plugin_author = "Kenneth Jiang" +plugin_author = "The Spaghetti Detective" # The plugin's author's mail address. -plugin_author_email = "k@thespaghettidetective.com" +plugin_author_email = "admin@thespaghettidetective.com" # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module -plugin_url = "https://github.com/TheSpaghettiDetective/OctoPrint-TheSpaghettiDetective" +plugin_url = "https://github.com/TheSpaghettiDetective/TheSpaghettiDetective" # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module plugin_license = "AGPLv3" # Any additional requirements besides OctoPrint should be listed here -plugin_requires = [] +plugin_requires = ["OctoPrint>=1.3.1", "backoff>=1.4.3"] ### -------------------------------------------------------------------------------------------------------------------- ### More advanced options that you usually shouldn't have to touch follow after this point