discovery plugin: let's try upnp

This commit is contained in:
Gina Häußge 2014-09-07 00:53:54 +02:00
parent 60e0ed911e
commit 428ea89f62
5 changed files with 179 additions and 28 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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:

View file

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