discovery plugin: let's try upnp
This commit is contained in:
parent
60e0ed911e
commit
428ea89f62
5 changed files with 179 additions and 28 deletions
|
|
@ -21,7 +21,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None):
|
|||
if plugin_folders is None:
|
||||
plugin_folders = (settings().getBaseFolder("plugins"), os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")))
|
||||
if plugin_types is None:
|
||||
plugin_types = [StartupPlugin, TemplatePlugin, SettingsPlugin, SimpleApiPlugin, AssetPlugin]
|
||||
plugin_types = [StartupPlugin, TemplatePlugin, SettingsPlugin, SimpleApiPlugin, AssetPlugin, BlueprintPlugin]
|
||||
|
||||
_instance = PluginManager(plugin_folders, plugin_types)
|
||||
else:
|
||||
|
|
@ -33,6 +33,22 @@ def plugin_settings(plugin_key, defaults=None):
|
|||
return PluginSettings(settings(), plugin_key, defaults=defaults)
|
||||
|
||||
|
||||
def call_plugin(types, method, args=None, kwargs=None, callback=None):
|
||||
if not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
if args is None:
|
||||
args = []
|
||||
if kwargs is None:
|
||||
kwargs = dict()
|
||||
|
||||
plugins = plugin_manager().get_implementations(*types)
|
||||
for name, plugin in plugins.items():
|
||||
if hasattr(plugin, method):
|
||||
result = getattr(plugin, method)(*args, **kwargs)
|
||||
if callback:
|
||||
callback(name, plugin, result)
|
||||
|
||||
|
||||
class PluginSettings(object):
|
||||
def __init__(self, settings, plugin_key, defaults=None):
|
||||
self.settings = settings
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ class SimpleApiPlugin(Plugin):
|
|||
return None
|
||||
|
||||
|
||||
class BlueprintPlugin(Plugin):
|
||||
def get_blueprint(self):
|
||||
return None
|
||||
|
||||
|
||||
class SettingsPlugin(TemplatePlugin):
|
||||
def on_settings_load(self):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
|
||||
import logging
|
||||
import os
|
||||
import flask
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
|
|
@ -20,13 +21,48 @@ default_settings = {
|
|||
s = octoprint.plugin.plugin_settings("discovery", defaults=default_settings)
|
||||
|
||||
|
||||
class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
||||
def get_uuid():
|
||||
import uuid
|
||||
return str(uuid.uuid4())
|
||||
UUID = get_uuid()
|
||||
del get_uuid
|
||||
|
||||
def get_instance_name():
|
||||
import socket
|
||||
return "OctoPrint instance on {}".format(socket.gethostname())
|
||||
INSTANCENAME = get_instance_name()
|
||||
del get_instance_name
|
||||
|
||||
|
||||
blueprint = flask.Blueprint("plugin.discovery", __name__, template_folder=os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates"))
|
||||
|
||||
@blueprint.route("/discovery.xml")
|
||||
def discovery():
|
||||
logging.getLogger("octoprint.plugins." + __name__).info("Rendering discovery.xml")
|
||||
response = flask.make_response(flask.render_template("discovery.jinja2",
|
||||
friendlyName=INSTANCENAME,
|
||||
manufacturer="OctoPrint",
|
||||
manufacturerUrl="http://www.octoprint.org",
|
||||
modelDescription="Some funny description",
|
||||
modelName="Some funny name",
|
||||
uuid=UUID,
|
||||
presentationUrl=flask.url_for("index", _external=True)))
|
||||
response.headers['Content-Type'] = 'application/xml'
|
||||
return response
|
||||
|
||||
|
||||
class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.types.BlueprintPlugin, octoprint.plugin.SettingsPlugin):
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger("octoprint.plugins." + __name__)
|
||||
|
||||
self.octoprint_sd_ref = None
|
||||
self.http_sd_ref = None
|
||||
|
||||
##~~ BlueprintPlugin API
|
||||
|
||||
def get_blueprint(self):
|
||||
return blueprint
|
||||
|
||||
##~~ TemplatePlugin API (part of SettingsPlugin)
|
||||
|
||||
def get_template_vars(self):
|
||||
|
|
@ -41,6 +77,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
|||
|
||||
def on_startup(self, host, port):
|
||||
self._bonjour_register(host, port)
|
||||
self._ssdp_register(port)
|
||||
|
||||
#~~ SettingsPlugin API
|
||||
|
||||
|
|
@ -66,7 +103,6 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
|||
|
||||
def _bonjour_register(self, host, port):
|
||||
import pybonjour
|
||||
import socket
|
||||
|
||||
def register_callback(sd_ref, flags, error_code, name, reg_type, domain):
|
||||
if error_code == pybonjour.kDNSServiceErr_NoError:
|
||||
|
|
@ -75,17 +111,8 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
|||
if s.getInt(["publicPort"]):
|
||||
port = s.getInt(["publicPort"])
|
||||
|
||||
prefix = s.globalGet(["server", "reverseProxy", "prefixFallback"])
|
||||
path = "/"
|
||||
if s.get(["pathPrefix"]):
|
||||
path = s.get(["pathPrefix"])
|
||||
elif prefix:
|
||||
path = prefix
|
||||
|
||||
name = "OctoPrint instance on {}".format(socket.gethostname())
|
||||
|
||||
self.octoprint_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name=name,
|
||||
name=INSTANCENAME,
|
||||
regtype='_octoprint._tcp',
|
||||
port=port,
|
||||
txtRecord=pybonjour.TXTRecord(self._create_octoprint_txt_record_dict()),
|
||||
|
|
@ -94,7 +121,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
|||
pybonjour.DNSServiceProcessResult(self.octoprint_sd_ref)
|
||||
|
||||
self.http_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name=name,
|
||||
name=INSTANCENAME,
|
||||
regtype='_http._tcp',
|
||||
port=port,
|
||||
txtRecord=pybonjour.TXTRecord(self._create_base_txt_record_dict()),
|
||||
|
|
@ -139,6 +166,101 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin):
|
|||
return entries
|
||||
|
||||
|
||||
def _ssdp_register(self, port):
|
||||
import threading
|
||||
self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, args=[port])
|
||||
self._ssdp_monitor_thread.daemon = True
|
||||
self._ssdp_monitor_thread.start()
|
||||
|
||||
def _ssdp_notify(self, port, alive=True):
|
||||
import socket
|
||||
|
||||
def interface_addresses(family=socket.AF_INET):
|
||||
for fam, _, _, _, sockaddr in socket.getaddrinfo('', None):
|
||||
if family == fam:
|
||||
yield sockaddr[0]
|
||||
|
||||
for addr in interface_addresses():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
sock.bind((addr, 0))
|
||||
|
||||
location = "http://{addr}:{port}/plugins/discovery/discovery.xml".format(addr=addr, port=port)
|
||||
|
||||
self.logger.info("Sending NOTIFY alive")
|
||||
notify_message = "".join(["NOTIFY * HTTP/1.1\r\n", "Server: Python/2.7\r\n", "Cache-Control: max-age=900\r\n", "Location: {location}\r\n", "NTS: {nts}\r\n", "NT: upnp:rootdevice\r\n", "USN: uuid:{uuid}::upnp:rootdevice\r\n", "Host: 239.255.255.250:1900\r\n\r\n"])
|
||||
message = notify_message.format(uuid=UUID, location=location, nts="ssdp:alive" if alive else "ssdp:byebye")
|
||||
for _ in xrange(2):
|
||||
sock.sendto(message, ("239.255.255.250", 1900))
|
||||
|
||||
try:
|
||||
sock.recv(1024)
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
def _ssdp_monitor(self, port):
|
||||
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
from httplib import HTTPResponse
|
||||
from StringIO import StringIO
|
||||
import socket
|
||||
import struct
|
||||
|
||||
socket.setdefaulttimeout(5)
|
||||
|
||||
location_message = "".join(["HTTP/1.1 200 OK\r\n", "ST: upnp:rootdevice\r\n", "USN: uuid:{uuid}::upnp:rootdevice\r\n", "Location: {location}\r\n", "Cache-Control: max-age=60\r\n\r\n"])
|
||||
|
||||
class Request(BaseHTTPRequestHandler):
|
||||
|
||||
def __init__(self, request_text):
|
||||
self.rfile = StringIO(request_text)
|
||||
self.raw_requestline = self.rfile.readline()
|
||||
self.error_code = self.error_message = None
|
||||
self.parse_request()
|
||||
|
||||
def send_error(self, code, message=None):
|
||||
self.error_code = code
|
||||
self.error_message = message
|
||||
|
||||
class Response(HTTPResponse):
|
||||
def __init__(self, response_text):
|
||||
self.fp = StringIO(response_text)
|
||||
self.debuglevel = 0
|
||||
self.strict = 0
|
||||
self.msg = None
|
||||
self._method = None
|
||||
self.begin()
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
sock.bind(('', 1900))
|
||||
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton('239.255.255.250') + socket.inet_aton('0.0.0.0'))
|
||||
|
||||
self.logger.info("Registered {} for SSDP".format(INSTANCENAME))
|
||||
|
||||
self._ssdp_notify(port, alive=True)
|
||||
|
||||
try:
|
||||
while (True):
|
||||
try:
|
||||
data, address = sock.recvfrom(4096)
|
||||
request = Request(data)
|
||||
if not request.error_code and request.command == "M-SEARCH" and request.path == "*" and (request.headers["ST"] == "upnp:rootdevice" or request.headers["ST"] == "ssdp:all") and request.headers["MAN"] == '"ssdp:discover"':
|
||||
message = location_message.format(uuid=UUID, location="http://192.168.1.3:5000/plugin/discovery/discovery.xml")
|
||||
sock.sendto(message, address)
|
||||
self.logger.info("Sent M-SEARCH reply for {path} and {st} to {address!r}".format(path=request.path, st=request.headers["ST"], address=address))
|
||||
except socket.timeout:
|
||||
self._ssdp_notify(port, alive=True)
|
||||
finally:
|
||||
try:
|
||||
sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
__plugin_name__ = "Discovery"
|
||||
__plugin_version__ = "0.1"
|
||||
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf"
|
||||
|
|
|
|||
|
|
@ -263,8 +263,14 @@ class Server():
|
|||
|
||||
from octoprint.server.api import api
|
||||
|
||||
# register API blueprint
|
||||
app.register_blueprint(api, url_prefix="/api")
|
||||
|
||||
# also register any blueprints defined in BlueprintPlugins
|
||||
octoprint.plugin.call_plugin(octoprint.plugin.types.BlueprintPlugin,
|
||||
"get_blueprint",
|
||||
callback=lambda name, _, blueprint: app.register_blueprint(blueprint, url_prefix="/plugin/{name}".format(name=name)))
|
||||
|
||||
self._router = SockJSRouter(self._createSocketConnection, "/sockjs")
|
||||
|
||||
upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"]))
|
||||
|
|
@ -295,9 +301,9 @@ class Server():
|
|||
observer.start()
|
||||
|
||||
# now it's the turn of the startup plugins
|
||||
startup_plugins = pluginManager.get_implementations(octoprint.plugin.StartupPlugin)
|
||||
for name, plugin in startup_plugins.items():
|
||||
plugin.on_startup(self._host, self._port)
|
||||
octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin,
|
||||
"on_startup",
|
||||
args=(self._host, self._port))
|
||||
|
||||
logger.info("Listening on http://%s:%d" % (self._host, self._port))
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -106,15 +106,17 @@ def getSettings():
|
|||
}
|
||||
}
|
||||
|
||||
settings_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
for name, plugin in settings_plugins.items():
|
||||
plugin_data = plugin.on_settings_load()
|
||||
if plugin_data:
|
||||
def process_plugin_result(name, plugin, result):
|
||||
if result:
|
||||
if not "plugins" in data:
|
||||
data["plugins"] = dict()
|
||||
if "__enabled" in plugin_data:
|
||||
del plugin_data["__enabled"]
|
||||
data["plugins"][name] = plugin_data
|
||||
if "__enabled" in result:
|
||||
del result["__enabled"]
|
||||
data["plugins"][name] = result
|
||||
|
||||
octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin,
|
||||
"on_settings_load",
|
||||
callback=process_plugin_result)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
|
@ -218,10 +220,10 @@ def setSettings():
|
|||
enabled = cura.get("enabled")
|
||||
s.setBoolean(["cura", "enabled"], enabled)
|
||||
|
||||
settings_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin)
|
||||
for name, plugin in settings_plugins.items():
|
||||
if "plugins" in data and name in data["plugins"]:
|
||||
plugin.on_settings_save(data["plugins"][name])
|
||||
octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin,
|
||||
"on_settings_save",
|
||||
args=(data["plugins"][name]))
|
||||
|
||||
|
||||
s.save()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue