diff --git a/examples/labs/orientation_controller/index.html b/examples/labs/orientation_controller/index.html new file mode 100644 index 000000000..9def4177a --- /dev/null +++ b/examples/labs/orientation_controller/index.html @@ -0,0 +1,26 @@ +{% extends "base-controller.html" %} + +{% block main %} + + +
+ {% if g.show_left_joystick %} + + {% endif %} + + +
+ +
+ +
+{% endblock %} diff --git a/examples/labs/orientation_controller/main.py b/examples/labs/orientation_controller/main.py new file mode 100644 index 000000000..95b67c763 --- /dev/null +++ b/examples/labs/orientation_controller/main.py @@ -0,0 +1,44 @@ +import os + +from pitop import Camera, DriveController, Pitop +from pitop.labs import RoverWebController + +# to access device orientation sensors, the webpage must be accessed over ssl, +# so ensure that an ssl cert exists for this +if not os.path.exists("cert.pem") or not os.path.exists("key.pem"): + print("Generating self-signed ssl cert") + os.system( + 'openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -nodes -subj "/O=pi-top"' + ) + +rover = Pitop() +rover.add_component(DriveController()) +rover.add_component(Camera()) + + +def device_orientation(data): + print(data) + x = data.get("x", 0) + y = data.get("y", 0) + + if abs(x) < 5: + x = 0 + x = x * -0.1 + + if abs(y) < 5: + y = 0 + y = y * 0.1 + + print(x, y) + rover.drive.robot_move(y, x) + + +rover_controller = RoverWebController( + get_frame=rover.camera.get_frame, + drive=rover.drive, + message_handlers={"device_orientation": device_orientation}, + cert="cert.pem", + key="key.pem", +) + +rover_controller.serve_forever() diff --git a/pitop/labs/web/blueprints/messaging/__init__.py b/pitop/labs/web/blueprints/messaging/__init__.py index 6b570bff2..d015922c1 100644 --- a/pitop/labs/web/blueprints/messaging/__init__.py +++ b/pitop/labs/web/blueprints/messaging/__init__.py @@ -64,6 +64,10 @@ def send(response_message): if message: handle_message(message, send) + disconnect_handler = message_handlers.get("disconnect") + if disconnect_handler: + disconnect_handler() + del self.sockets[id] def register(self, app, options, *args): diff --git a/pitop/labs/web/blueprints/rover/__init__.py b/pitop/labs/web/blueprints/rover/__init__.py index 8fafc18ef..72ccf394a 100644 --- a/pitop/labs/web/blueprints/rover/__init__.py +++ b/pitop/labs/web/blueprints/rover/__init__.py @@ -49,6 +49,13 @@ def right_joystick(data): message_handlers["right_joystick"] = right_joystick + if message_handlers.get("disconnect") is None: + + def disconnect(): + drive.robot_move(0, 0) + + message_handlers["disconnect"] = disconnect + self.controller_blueprint = ControllerBlueprint( get_frame=get_frame, message_handlers=message_handlers ) diff --git a/pitop/labs/web/blueprints/rover/templates/base-rover.html b/pitop/labs/web/blueprints/rover/templates/base-rover.html index 57c8638cb..fdfb537fd 100644 --- a/pitop/labs/web/blueprints/rover/templates/base-rover.html +++ b/pitop/labs/web/blueprints/rover/templates/base-rover.html @@ -7,7 +7,6 @@ {% if g.show_left_joystick %} {% endif %} diff --git a/pitop/labs/web/blueprints/webcomponents/templates/setup-webcomponents.html b/pitop/labs/web/blueprints/webcomponents/templates/setup-webcomponents.html index 243c438ac..67ca32e84 100644 --- a/pitop/labs/web/blueprints/webcomponents/templates/setup-webcomponents.html +++ b/pitop/labs/web/blueprints/webcomponents/templates/setup-webcomponents.html @@ -1,2 +1,3 @@ + diff --git a/pitop/labs/web/blueprints/webcomponents/webcomponents/orientation-component.js b/pitop/labs/web/blueprints/webcomponents/webcomponents/orientation-component.js new file mode 100644 index 000000000..412ff0c87 --- /dev/null +++ b/pitop/labs/web/blueprints/webcomponents/webcomponents/orientation-component.js @@ -0,0 +1,138 @@ +class DeviceOrientation extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + if (!this.connected) { + this.connected = true; + this.setup(); + } + } + + disconnectedCallback() { + this.disable(); + this.connected = false; + } + + setup = async () => { + this.attachShadow({mode: 'open'}); + this.wrapper = document.createElement('div'); + this.shadowRoot.append(this.wrapper); + + this.header = document.createElement('h3'); + this.header.textContent = 'Device Orientation Control'; + this.wrapper.appendChild(this.header); + + if (typeof DeviceOrientationEvent === 'undefined') { + this.header.textContent = 'Device Orientation Not Supported On This Device!'; + return; + } + + if (typeof DeviceOrientationEvent.requestPermission === 'function') { + this.permissionButton = document.createElement('button'); + this.permissionButton.setAttribute('type','button'); + this.permissionButton.textContent = 'Allow sensor access'; + this.permissionButton.addEventListener('click', this.requestPermission); + this.wrapper.appendChild(this.permissionButton); + return; + } + + this.showEnable(); + } + + requestPermission = async () => { + const permissionState = await DeviceOrientationEvent.requestPermission() + return permissionState === 'granted' ? this.permissionGranted() : this.permissionDenied(); + } + + permissionGranted = () => { + this.showEnable(); + this.permissionButton.remove(); + } + + permissionDenied = () => { + this.header.textContent = 'Device Orientation Permission Denied!'; + } + + showEnable = () => { + this.enabled = document.createElement('input'); + this.enabled.setAttribute('type','checkbox'); + this.enabled.setAttribute('id','enabled'); + this.wrapper.appendChild(this.enabled); + + this.enabledLabel = document.createElement('label'); + this.enabledLabel.setAttribute('for','enabled'); + this.enabledLabel.innerText = 'Disabled'; + this.wrapper.appendChild(this.enabledLabel); + + this.enabled.addEventListener('change', (event) => { + if (event.currentTarget.checked) { + this.enable(); + } else { + this.disable(); + } + }) + } + + enable = () => { + window.addEventListener('deviceorientation', this.handleOrientationEvent); + // deviceorientation events don't fire when window blurs, so reset controls + window.addEventListener('blur', this.orientationReset); + + this.showOrientationDisplay(); + this.enabledLabel.innerText = 'Enabled'; + } + + disable = () => { + this.orientationReset(); + + window.removeEventListener('deviceorientation', this.handleOrientationEvent); + window.removeEventListener('blur', this.orientationReset); + + this.hideOrientationDisplay(); + this.enabledLabel.innerText = 'Disabled'; + } + + orientationReset = () => { + this.handleOrientationEvent({ alpha: 0, beta: 0, gamma: 0 }); + }; + + handleOrientationEvent = (event) => { + const x = event.beta; // landscape left right + const y = event.gamma; // landscape forward back + const z = event.alpha; // landscape rotation + + this.updateOrientationDisplay(x, y, z); + + eval(` + const data = ${JSON.stringify({ x, y, z })}; + ${this.getAttribute('onchange')} + `); + } + + showOrientationDisplay = () => { + this.x = document.createElement('p'); + this.y = document.createElement('p'); + this.z = document.createElement('p'); + this.wrapper.appendChild(this.x); + this.wrapper.appendChild(this.y); + this.wrapper.appendChild(this.z); + + this.updateOrientationDisplay(0, 0, 0) + } + + hideOrientationDisplay = () => { + this.x.remove(); + this.y.remove(); + this.z.remove(); + } + + updateOrientationDisplay = (x, y, z) => { + this.x.textContent = 'x: ' + x; + this.y.textContent = 'y: ' + y; + this.z.textContent = 'z: ' + z; + } +} + +window.customElements.define('orientation-component', DeviceOrientation); diff --git a/pitop/labs/web/webcontroller.py b/pitop/labs/web/webcontroller.py index ba2664621..3dfcaad06 100644 --- a/pitop/labs/web/webcontroller.py +++ b/pitop/labs/web/webcontroller.py @@ -24,6 +24,8 @@ def __init__( pan_tilt=None, message_handlers={}, blueprints=[], + cert=None, + key=None, **kwargs ): self.rover_blueprint = RoverControllerBlueprint( @@ -34,7 +36,11 @@ def __init__( ) WebServer.__init__( - self, blueprints=[self.rover_blueprint] + blueprints, **kwargs + self, + blueprints=[self.rover_blueprint] + blueprints, + cert=cert, + key=key, + **kwargs ) def broadcast(self, message): diff --git a/pitop/labs/web/webserver.py b/pitop/labs/web/webserver.py index b09afaedf..9577f1e14 100644 --- a/pitop/labs/web/webserver.py +++ b/pitop/labs/web/webserver.py @@ -48,32 +48,47 @@ def add_header(req): class WebServer(WSGIServer): - def __init__(self, port=8070, app=create_app(), blueprints=[BaseBlueprint()]): + def __init__( + self, + port=8070, + app=create_app(), + blueprints=[BaseBlueprint()], + cert=None, + key=None, + ): self.port = port self.app = app self.sockets = Sockets(app) + self.ssl = cert and key is not None with self.app.app_context(): for blueprint in blueprints: self.app.register_blueprint(blueprint, sockets=self.sockets) WSGIServer.__init__( - self, ("0.0.0.0", port), self.app, handler_class=WebSocketHandler + self, + ("0.0.0.0", port), + self.app, + handler_class=WebSocketHandler, + certfile=cert, + keyfile=key, ) def _log_address(self): + protocol = "http" if not self.ssl else "https" + ip_addresses = list() for interface in ("wlan0", "ptusb0", "lo", "en0"): ip_address = get_internal_ip(interface) - if is_url("http://" + ip_address): + if is_url(f"{protocol}://{ip_address}"): ip_addresses.append(ip_address) print("WebServer is listening at:") if len(ip_addresses) > 0: for ip_address in ip_addresses: - print(f"\t- http://{ip_address}:{self.port}/") + print(f"\t- {protocol}://{ip_address}:{self.port}/") else: - print(f"\t- http://localhost:{self.port}/ (on same device)") + print(f"\t- {protocol}://localhost:{self.port}/ (on same device)") def start(self): self._log_address()