From 7b3d329f3b69c99d1d45c2948faeb488472edd67 Mon Sep 17 00:00:00 2001 From: electric-monk Date: Mon, 30 Dec 2019 22:06:36 -0800 Subject: [PATCH] Initial upload --- decoder.py | 64 ++++++++ downloadassets.sh | 4 + link.py | 91 ++++++++++++ protocol.py | 367 ++++++++++++++++++++++++++++++++++++++++++++++ server.py | 192 ++++++++++++++++++++++++ teslabox.py | 109 ++++++++++++++ 6 files changed, 827 insertions(+) create mode 100644 decoder.py create mode 100644 downloadassets.sh create mode 100644 link.py create mode 100644 protocol.py create mode 100644 server.py create mode 100644 teslabox.py diff --git a/decoder.py b/decoder.py new file mode 100644 index 0000000..722e0b5 --- /dev/null +++ b/decoder.py @@ -0,0 +1,64 @@ + +# "Autobox" dongle driver for HTML 'streaming' +# Created by Colin Munro, December 2019 +# See README.md for more information + +"""Simple utility code to decode an h264 stream to a series of PNGs.""" + +import subprocess, threading, os, fcntl + +class Decoder: + class _Thread(threading.Thread): + def __init__(self, owner): + super().__init__() + self.owner = owner + self.running = threading.Event() + self.shutdown = False + + def run(self): + png_header = bytearray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + captured_data = b'' + checked = 0 + while not self.shutdown: + data = self.owner.child.stdout.read(1024000) + if data is None or not len(data): + self.running.clear() + self.running.wait(timeout=0.1) + continue + captured_data += data + first_header = captured_data.find(png_header) + if first_header == -1: + continue + if first_header != 0: + captured_data = captured_data[first_header:] + while True: + second_header = captured_data.find(png_header, checked) + if second_header == -1: + checked = len(captured_data) - len(png_header) + break + png = captured_data[:second_header] + captured_data = captured_data[second_header:] + checked = len(png_header) + self.owner.on_frame(png) + + def __init__(self): + self.child = subprocess.Popen(["ffmpeg", "-threads", "4", "-i", "-", "-vf", "fps=7", "-c:v", "png", "-f", "image2pipe", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=1) + fd = self.child.stdout.fileno() + fl = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + self.thread = self._Thread(self) + self.thread.start() + + def stop(self): + self.child.terminate() + self.thread.shutdown = True + self.thread.join() + + def send(self, data): + self.child.stdin.write(data) + self.child.stdin.flush() + self.thread.running.set() + + def on_frame(self, png): + """Callback for when a frame is received [called from a worker thread].""" + pass diff --git a/downloadassets.sh b/downloadassets.sh new file mode 100644 index 0000000..4609a6f --- /dev/null +++ b/downloadassets.sh @@ -0,0 +1,4 @@ +#!/bin/bash +curl "http://121.40.123.198:8080/AutoKit/AutoKit.apk" > AutoKit.apk +unzip AutoKit.apk 'assets/*' + diff --git a/link.py b/link.py new file mode 100644 index 0000000..b49431f --- /dev/null +++ b/link.py @@ -0,0 +1,91 @@ + +# "Autobox" dongle driver for HTML 'streaming' +# Created by Colin Munro, December 2019 +# See README.md for more information + +"""Dongle USB connection code.""" + +import usb.core +import usb.util +import threading +import protocol + +class Connection: + idVendor = 0x1314 + idProduct = 0x1520 + + def __init__(self): + self._device = usb.core.find(idVendor = self.idVendor, idProduct = self.idProduct) + if self._device is None: + raise RuntimeError("Couldn't find USB device") + self._device.reset() + self._device.set_configuration() + self._interface = self._device.get_active_configuration()[(0,0)] + self._ep_in = usb.util.find_descriptor(self._interface, custom_match = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN) + if self._ep_in is None: + raise RuntimeError("Couldn't find input endpoint") + self._ep_in.clear_halt() + self._ep_out = usb.util.find_descriptor(self._interface, custom_match = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT) + if self._ep_out is None: + raise RuntimeError("Couldn't find output endpoint") + self._ep_out.clear_halt() + self._out_locker = threading.Lock() + self._run = True + self._thread = threading.Thread(target=self._read_thread) + self._thread.start() + + def send_message(self, message): + data = message.serialise() + while not self._out_locker.acquire(): + pass + try: + self._ep_out.write(data[:message.headersize]) + self._ep_out.write(data[message.headersize:]) + finally: + self._out_locker.release() + + def send_multiple(self, messages): + for x in messages: + self.send_message(x) + + def stop(self): + self._run = False + self._thread.join() + + def on_message(self, message): + """Handle message from dongle [called from another thread]""" + pass + + def on_error(self, error): + """Handle exception on dongle read thread [called from another thread]""" + self._run = False + + def _read_thread(self): + while self._run: + try: + data = self._ep_in.read(protocol.Message.headersize) + except usb.core.USBError as e: + if e.errno != 110: # Timeout + self.on_error(e) + continue + if len(data) == protocol.Message.headersize: + header = protocol.Message() + header.deserialise(data) + needlen = len(header._data()) + if needlen: + try: + msg = header.upgrade(self._ep_in.read(needlen)) + except usb.core.USBError as e: + self._threaderror(e) + continue + else: + msg = header + try: + self.on_message(msg) + except Exception as e: + self.on_error(e) + continue + else: + print(f"R> Bad data: {data}") + +Error = usb.core.USBError diff --git a/protocol.py b/protocol.py new file mode 100644 index 0000000..d2e1dfd --- /dev/null +++ b/protocol.py @@ -0,0 +1,367 @@ + +# "Autobox" dongle driver for HTML 'streaming' +# Created by Colin Munro, December 2019 +# See README.md for more information + +"""Dongle communications protocol implementation.""" + +import struct +from enum import IntEnum + +def _setenum(enum, val): + try: + return enum(val) + except ValueError: + return val + +class Message: + """Base dongle message, indicating message size and type.""" + magic = 0x55aa55aa + headersize = 4 * 4 + + @classmethod + def _allmessages(cls): + """Get a dictionary mapping message types to messages.""" + msgs = {} + for x in cls.__subclasses__(): + if hasattr(x, 'msgtype'): + msgs[x.msgtype] = x + msgs.update(x._allmessages()) + return msgs + + def upgrade(self, bodydata): + """Convert a message containing only its header to the concrete message type (if known).""" + try: + upd=self._allmessages()[self.type]() + upd._setdata(bodydata) + upd._check_type() + except KeyError: + upd=Unknown(self.type) + upd._setdata(bodydata) + return upd + + def __init__(self, type=-1): + if type == -1 and hasattr(self, "msgtype"): + self.type = self.msgtype + else: + self.type = type + + def serialise(self): + data = self._data() + return struct.pack(" 16: + raise "String too long" + return SendFile(filename, s.encode('ascii')) + +def _send_int(filename, i): + return SendFile(filename, struct.pack(" + +TeslaCarPlay + + + + + + + +""".encode('utf-8')) + + def get_stream(self): + self.stream = Queue() + temp_data = self.owner.streamdata + i=0 + for x in temp_data: + self.wfile.write(x) + i+=len(x) + print(f"<>") + self.owner.streams.append(self) + try: + while True: + chunk=self.stream.get(True, None) + self.wfile.write(chunk) + print(f"<>") + + finally: + self.owner.streams.remove(self) + + def get_ping(self): + self.wfile.write(self.owner.on_get_snapshot()) + + def do_touch(self, json): + self.owner.on_touch(json["type"], json["x"], json["y"]) + self.wfile.write(simplejson.dumps({"ok": True}).encode('utf-8')) + + pages = { + "/": ("text/html; charset=utf-8", get_index), + "/stream": ("video/H264", get_stream), + "/snapshot": ("image/png", get_ping), + } + + posts = { + "/touch": do_touch, + } + + def do_GET(self): + self.close_connection = True + urldata = urllib.parse.urlparse(self.path) + getter = self.pages.get(urldata.path, None) + if getter is None: + self.send_error(404, "Invalid path") + return + self.send_response(200) + self.send_header("Content-type", getter[0]) + self.end_headers() + try: + getter[1](self) + except (BrokenPipeError, ConnectionResetError): + pass + + def do_POST(self): + self.close_connection = True + urldata = urllib.parse.urlparse(self.path) + poster = self.posts.get(urldata.path, None) + if poster is None: + self.send_error(404, "Invalid path") + return + self.send_response(200) + self.send_header("Content-type", "text/json") + self.end_headers() + content_len = int(self.headers.get('Content-length', 0)) + poster(self, simplejson.loads(self.rfile.read(content_len))) + + def on_touch(self, type, x, y): + """Callback for when a touch is received from the web browser [called from a web server thread].""" + pass + + def on_get_snapshot(self): + """Callback for when a new PNG is required [called from a web server thread].""" + return b'' diff --git a/teslabox.py b/teslabox.py new file mode 100644 index 0000000..bdf8f22 --- /dev/null +++ b/teslabox.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 + +# "Autobox" dongle driver for HTML 'streaming' - test application +# Created by Colin Munro, December 2019 +# See README.md for more information + +"""Implementation to stream PNGs over a webpage that responds with touches that are relayed back to the dongle for Tesla experimental purposes.""" +import decoder +import server +import link +import protocol +from threading import Thread +import time + +class Teslabox: + class _Server(server.Server): + def __init__(self, owner): + self._owner = owner + super().__init__() + def on_touch(self, type, x, y): + if self._owner.connection is None: + return + if True: + msg = protocol.Touch() + types = {"down": protocol.Touch.Action.Down, "up": protocol.Touch.Action.Up, "move": protocol.Touch.Action.Move} + msg.action = types[type] + msg.x = int(x*10000/800) + msg.y = int(y*10000/600) + else: + types = {"down": protocol.MultiTouch.Touch.Action.Down, "up": protocol.MultiTouch.Touch.Action.Up, "move": protocol.MultiTouch.Touch.Action.Move} + msg = protocol.MultiTouch() + tch = protocol.MultiTouch.Touch() + tch.x = int(x) + tch.y = int(y) + tch.action = types[type] + msg.touches.append(tch) + self._owner.connection.send_message(msg) + def on_get_snapshot(self): + return self._owner._frame + class _Decoder(decoder.Decoder): + def __init__(self, owner): + super().__init__() + self._owner = owner + def on_frame(self, png): + self._owner._frame = png + class _Connection(link.Connection): + def __init__(self, owner): + super().__init__() + self._owner = owner + def on_message(self, message): + if isinstance(message, protocol.Open): + if not self._owner.started: + self._owner._connected() + self.send_multiple(protocol.opened_info) + elif isinstance(message, protocol.VideoData): + self._owner.decoder.send(message.data) + def on_error(self, error): + self._owner._disconnect() + def __init__(self): + self._disconnect() + self.server = self._Server(self) + self.decoder = self._Decoder(self) + self.heartbeat = Thread(target=self._heartbeat_thread) + self.heartbeat.start() + def _connected(self): + print("Connected!") + self.started = True + self.decoder.stop() + self.decoder = self._Decoder(self) + def _disconnect(self): + if hasattr(self, "connection"): + if self.connection is None: + return + print("Lost USB device") + self._frame = b'' + self.connection = None + self.started = False + def _heartbeat_thread(self): + while True: + try: + self.connection.send_message(protocol.Heartbeat()) + except link.Error: + self._disconnect() + except: + pass + time.sleep(protocol.Heartbeat.lifecycle) + def run(self): + while True: + # First task: look for USB device + while self.connection is None: + try: + self.connection = self._Connection(self) + except Exception as e: + pass + print("Found USB device...") + # Second task: transmit startup info + try: + while not self.started: + self.connection.send_multiple(protocol.startup_info) + time.sleep(1) + except: + self._disconnect() + print("Connection started!") + # Third task: idle while connected + while self.started: + time.sleep(1) + +if __name__ == "__main__": + Teslabox().run()