diff --git a/examples/huecircle.py b/examples/huecircle.py index f4a63d8..8817f53 100644 --- a/examples/huecircle.py +++ b/examples/huecircle.py @@ -24,11 +24,11 @@ def circle_generator(): circle = circle_generator() -def callback(): +def callback(*args, **kwargs): return next(circle) if __name__ == '__main__': - p = Pyghthouse(UNAME, TOKEN, image_callback=callback) + p = Pyghthouse(UNAME, TOKEN, image_callback=callback, ignore_ssl_cert=True) print("Starting... use CTRL+C to stop.") p.start() diff --git a/examples/movingdot_remote_input.py b/examples/movingdot_remote_input.py new file mode 100644 index 0000000..5981c6f --- /dev/null +++ b/examples/movingdot_remote_input.py @@ -0,0 +1,37 @@ +from pyghthouse import Pyghthouse, VerbosityLevel, KeyEvent +from config import UNAME, TOKEN + + +def clip(val, min_val, max_val): + if val < min_val: + return min_val + if val > max_val: + return max_val + return val + + +def main_loop(): + x = 0 + y = 0 + p = Pyghthouse(UNAME, TOKEN, verbosity=VerbosityLevel.NONE, stream_remote_inputs=True) + p.start() + while True: + img = p.empty_image() + img[y][x] = [255, 255, 255] + p.set_image(img) + for e in p.get_all_events(): + if isinstance(e, KeyEvent) and e.down: + if e.code == 65: # A + x -= 1 + elif e.code == 68: # D + x += 1 + elif e.code == 87: # W + y -= 1 + elif e.code == 83: # S + y += 1 + x = clip(x, 0, 27) + y = clip(y, 0, 13) + + +if __name__ == '__main__': + main_loop() diff --git a/examples/noisefill.py b/examples/noisefill.py index ced1be6..bc94bcb 100644 --- a/examples/noisefill.py +++ b/examples/noisefill.py @@ -20,7 +20,12 @@ def image_gen(): g = image_gen() + +def callback(*args, **kwargs): + return next(g) + + if __name__ == '__main__': - p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) + p = Pyghthouse(UNAME, TOKEN, image_callback=callback, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() diff --git a/examples/rainbow.py b/examples/rainbow.py index d59b6c5..dfeb3a7 100644 --- a/examples/rainbow.py +++ b/examples/rainbow.py @@ -12,7 +12,7 @@ def rainbow_generator(): rainbow = rainbow_generator() -def callback(): +def callback(*args, **kwargs): return next(rainbow) diff --git a/examples/rgbfill.py b/examples/rgbfill.py index 97f8d81..975e708 100644 --- a/examples/rgbfill.py +++ b/examples/rgbfill.py @@ -22,7 +22,12 @@ def image_gen(): g = image_gen() + +def callback(*args, **kwargs): + return next(g) + + if __name__ == '__main__': - p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) + p = Pyghthouse(UNAME, TOKEN, image_callback=callback, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() diff --git a/examples/rgbscan.py b/examples/rgbscan.py index 28939fc..5ba9dba 100644 --- a/examples/rgbscan.py +++ b/examples/rgbscan.py @@ -18,7 +18,12 @@ def image_gen(): g = image_gen() + +def callback(*args, **kwargs): + return next(g) + + if __name__ == '__main__': - p = Pyghthouse(UNAME, TOKEN, image_callback=g.__next__, frame_rate=60) + p = Pyghthouse(UNAME, TOKEN, image_callback=callback, frame_rate=60) print("Starting... use CTRL+C to stop.") p.start() diff --git a/examples/twopoints.py b/examples/twopoints.py index ef446b0..542c024 100644 --- a/examples/twopoints.py +++ b/examples/twopoints.py @@ -47,7 +47,7 @@ def __init__(self): self.draw = ImageDraw.Draw(self.img) self.hue = 0.0 - def callback(self): + def callback(self, events): self.p1.update() self.p2.update() self.hue += COLOR_SPEED diff --git a/examples/twopoints_remote_input.py b/examples/twopoints_remote_input.py new file mode 100644 index 0000000..a4fa2f3 --- /dev/null +++ b/examples/twopoints_remote_input.py @@ -0,0 +1,94 @@ +from random import random + +import numpy as np +from PIL import Image, ImageDraw + +from pyghthouse.utils import from_hsv +from pyghthouse import Pyghthouse, KeyEvent + +from config import UNAME, TOKEN + +SPEED_FACTOR = 0.02 +COLOR_SPEED = 0.01 + + +class BouncyPoint: + + def __init__(self): + self.x = random() + self.y = random() + self.vx = SPEED_FACTOR * (random() + 0.5) + self.vy = SPEED_FACTOR * (random() + 0.5) + + def update(self): + self.x += self.vx + self.y += self.vy + + if self.x < 0: + self.vx *= -1 + self.x *= -1 + elif self.x > 1: + self.vx *= -1 + self.x = 2 - self.x + if self.y < 0: + self.vy *= -1 + self.y *= -1 + elif self.y > 1: + self.vy *= -1 + self.y = 2 - self.y + + +class ImageMaker: + + def __init__(self): + self.p1 = BouncyPoint() + self.p2 = BouncyPoint() + self.img = Image.new('RGB', (280, 140)) + self.draw = ImageDraw.Draw(self.img) + self.hue = 0.0 + self.pressed_keys = {'l': False, 'u': False, 'r': False, 'd': False, + 'L': False, 'U': False, 'R': False, 'D': False} + + def callback(self, events): + for e in filter(lambda x: x is not None and isinstance(x, KeyEvent), events): + if e.code == 37: + self.pressed_keys['l'] = e.down + if e.code == 38: + self.pressed_keys['u'] = e.down + if e.code == 39: + self.pressed_keys['r'] = e.down + if e.code == 40: + self.pressed_keys['d'] = e.down + if e.code == 65: + self.pressed_keys['L'] = e.down + if e.code == 87: + self.pressed_keys['U'] = e.down + if e.code == 68: + self.pressed_keys['R'] = e.down + if e.code == 83: + self.pressed_keys['D'] = e.down + + self.p1.vx = (self.pressed_keys['r'] - self.pressed_keys['l']) * SPEED_FACTOR + self.p1.vy = (self.pressed_keys['d'] - self.pressed_keys['u']) * SPEED_FACTOR + self.p2.vx = (self.pressed_keys['R'] - self.pressed_keys['L']) * SPEED_FACTOR + self.p2.vy = (self.pressed_keys['D'] - self.pressed_keys['U']) * SPEED_FACTOR + self.p1.update() + self.p2.update() + self.hue += COLOR_SPEED + self.draw.line([(self.p1.x * 280, self.p1.y * 140), (self.p2.x * 280, self.p2.y * 140)], width=10, + fill=tuple(from_hsv(self.hue, 1, 1))) + + output = self.img.copy() + draw2 = ImageDraw.Draw(output) + draw2.rectangle((self.p1.x * 280 - 6, self.p1.y * 140 - 6, self.p1.x * 280 + 6, self.p1.y * 140 + 6), + fill=(255, 255, 255), outline=(255, 255, 255)) + draw2.rectangle((self.p2.x * 280 - 6, self.p2.y * 140 - 6, self.p2.x * 280 + 6, self.p2.y * 140 + 6), + fill=(0, 0, 0), outline=(0, 0, 0)) + output.thumbnail((28, 14)) + return np.asarray(output) + + +if __name__ == '__main__': + i = ImageMaker() + p = Pyghthouse(UNAME, TOKEN, image_callback=i.callback, frame_rate=60, stream_remote_inputs=True) + p.start() diff --git a/pyghthouse/__init__.py b/pyghthouse/__init__.py index 23aa611..7ad15ac 100644 --- a/pyghthouse/__init__.py +++ b/pyghthouse/__init__.py @@ -1 +1,2 @@ from pyghthouse.ph import Pyghthouse, VerbosityLevel +from pyghthouse.event.events import KeyEvent diff --git a/pyghthouse/connection/wsconnector.py b/pyghthouse/connection/wsconnector.py index 7b22ef8..902edf7 100644 --- a/pyghthouse/connection/wsconnector.py +++ b/pyghthouse/connection/wsconnector.py @@ -18,7 +18,7 @@ def __next__(self): def __iter__(self): return self - def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ssl_cert=False): + def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ssl_cert=False, stream=False): self.username = username self.token = token self.address = address @@ -28,6 +28,7 @@ def __init__(self, username: str, token: str, address: str, on_msg=None, ignore_ self.reid = self.REID() self.running = False self.ignore_ssl_cert = ignore_ssl_cert + self.stream = stream setdefaulttimeout(60) def send(self, data): @@ -59,6 +60,16 @@ def _ready(self, ws): print(f"Connected to {self.address}.") self.running = True self.lock.release() + if self.stream: + packet_start_stream = { + 'REID': next(self.reid), + 'AUTH': {'USER': self.username, 'TOKEN': self.token}, + 'VERB': 'STREAM', + 'PATH': ['user', self.username, 'model'], + 'META': {}, + 'PAYL': {} + } + self.ws.send(packb(packet_start_stream, use_bin_type=True), opcode=ABNF.OPCODE_BINARY) def _handle_msg(self, ws, msg): if isinstance(msg, bytes): @@ -73,4 +84,4 @@ def construct_package(self, payload_data): 'PATH': ['user', self.username, 'model'], 'META': {}, 'PAYL': payload_data - } \ No newline at end of file + } diff --git a/pyghthouse/event/__init__.py b/pyghthouse/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyghthouse/event/event_manager.py b/pyghthouse/event/event_manager.py new file mode 100644 index 0000000..739c095 --- /dev/null +++ b/pyghthouse/event/event_manager.py @@ -0,0 +1,27 @@ +from .events import BaseEvent, KeyEvent +from queue import Queue, Empty + + +class EventManager: + queue: Queue[BaseEvent] + + def __init__(self): + self.queue = Queue(maxsize=0) + + def add_event(self, e: BaseEvent): + self.queue.put(e) + + def add_key_event(self, code: int, down: bool): + self.add_event(KeyEvent(code, down)) + + def get_event(self): + try: + return self.queue.get(block=False) + except Empty: + return None + + def get_all_events(self): + events = [] + while not self.queue.empty(): + events.append(self.queue.get(block=False)) + return events diff --git a/pyghthouse/event/events.py b/pyghthouse/event/events.py new file mode 100644 index 0000000..2fced83 --- /dev/null +++ b/pyghthouse/event/events.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from abc import ABC + + +class BaseEvent(ABC): + pass + + +@dataclass +class KeyEvent(BaseEvent): + code: int + down: bool diff --git a/pyghthouse/ph.py b/pyghthouse/ph.py index 5718b1e..017c3cf 100644 --- a/pyghthouse/ph.py +++ b/pyghthouse/ph.py @@ -7,6 +7,7 @@ from pyghthouse.data.canvas import PyghthouseCanvas from pyghthouse.connection.wsconnector import WSConnector +from pyghthouse.event.event_manager import EventManager class VerbosityLevel(Enum): @@ -43,9 +44,9 @@ class Pyghthouse: Rate in 1/sec at which frames (images) are automatically sent to the lighthouse server. Also determines how often the image_callback function is called. - image_callback: function (optional) - A function that takes no arguments and generates a valid lighthouse image (cf Image Format). If set, this - function is called before an image is sent and is used to determine said image. + image_callback: function List[Event] -> image (optional) + A function that takes an argument 'events' and generates a valid lighthouse image (cf Image Format). If set, + this function is called before an image is sent and is used to determine said image. The function is guaranteed to be called frame_rate times each second *unless* its execution takes longer than 1/frame_rate seconds, in which case execution will be slowed down accordingly. @@ -61,6 +62,10 @@ class Pyghthouse: pyghthouse.VerbosityLevel.ALL: Print all messages. + stream_remote_inputs: bool (optional, default: False) + Stream key events from the server. These can be retrieved via the get_event() or get_all_events() + methods and are passed to the callback method (if one exists). + Image Format ------------ Conceptually, each window of the highrise represents a pixel of a 28x14 RGB image. @@ -141,9 +146,10 @@ class Pyghthouse: class PHMessageHandler: - def __init__(self, verbosity=VerbosityLevel.WARN_ONCE): + def __init__(self, verbosity=VerbosityLevel.WARN_ONCE, event_manager=None): self.verbosity = verbosity self.warned_already = False + self.event_manager: EventManager = event_manager def reset(self): self.warned_already = False @@ -152,6 +158,16 @@ def handle(self, msg): if msg['RNUM'] == 200: if self.verbosity == VerbosityLevel.ALL: print(msg) + payload = msg['PAYL'] + if isinstance(payload, dict): + try: + key = payload['key'] + down = payload['dwn'] + if self.event_manager is not None: + self.event_manager.add_key_event(key, down) + except KeyError: + pass + elif self.verbosity == VerbosityLevel.WARN: self.print_warning(msg) elif self.verbosity == VerbosityLevel.WARN_ONCE and not self.warned_already: @@ -181,13 +197,13 @@ def run(self): sleep_time = self.parent.send_interval - (time() % self.parent.send_interval) sleep(sleep_time) if self.parent.image_callback is not None: - image_from_callback = self.parent.image_callback() + image_from_callback = self.parent.image_callback(events=self.parent.get_all_events()) self.parent.set_image(image_from_callback) self.parent.connector.send(self.parent.canvas.get_image_bytes()) def __init__(self, username: str, token: str, address: str = "wss://lighthouse.uni-kiel.de/websocket", frame_rate: float = 30.0, image_callback=None, verbosity=VerbosityLevel.WARN_ONCE, - ignore_ssl_cert=False): + ignore_ssl_cert=False, stream_remote_inputs=False): if frame_rate > 60.0 or frame_rate <= 0: raise ValueError("Frame rate must be greater than 0 and at most 60.") self.username = username @@ -196,11 +212,13 @@ def __init__(self, username: str, token: str, address: str = "wss://lighthouse.u self.send_interval = 1.0 / frame_rate self.image_callback = image_callback self.canvas = PyghthouseCanvas() - self.msg_handler = self.PHMessageHandler(verbosity) + self.event_manager = EventManager() + self.msg_handler = self.PHMessageHandler(verbosity, self.event_manager) self.connector = WSConnector(username, token, address, on_msg=self.msg_handler.handle, - ignore_ssl_cert=ignore_ssl_cert) + ignore_ssl_cert=ignore_ssl_cert, stream=stream_remote_inputs) self.config_lock = Lock() self.ph_thread = None + signal(SIGINT, self._handle_sigint) def connect(self): @@ -253,3 +271,9 @@ def set_frame_rate(self, frame_rate): def _handle_sigint(self, sig, frame): self.close() raise SystemExit(0) + + def get_event(self): + return self.event_manager.get_event() + + def get_all_events(self): + return self.event_manager.get_all_events() diff --git a/setup.py b/setup.py index d03c4e2..88584e0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='pyghthouse', - version='0.2.1', + version='0.3.0-alpha', packages=find_packages(where='.'), url='https://github.com/Musicted/pyghthouse', license='MIT',