From 7b7fbde1fe718acd1efd6de0430ee52b01dd94fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laure=CE=B7t?= Date: Sun, 31 Oct 2021 22:17:50 +0100 Subject: [PATCH] feat: websocket server feat: emulator (virtualcam) feat: basic web client --- .gitignore | 1 + src/client.html | 67 +++++++++++++++++++++++++++ src/emulator.py | 67 +++++++++++++++++++++++++++ src/server.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 src/client.html create mode 100644 src/emulator.py create mode 100644 src/server.py diff --git a/.gitignore b/.gitignore index 60e1da6..8fe4e99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ roms/ .venv/ .mypy_cache/ +states/ diff --git a/src/client.html b/src/client.html new file mode 100644 index 0000000..ad3b84e --- /dev/null +++ b/src/client.html @@ -0,0 +1,67 @@ + + + + + Telecommande + + + + + + + + + + + + + + + + + + diff --git a/src/emulator.py b/src/emulator.py new file mode 100644 index 0000000..254fd11 --- /dev/null +++ b/src/emulator.py @@ -0,0 +1,67 @@ +import asyncio +import logging + +import mgba.core +import mgba.image +import mgba.log +import numpy as np +import pyvirtualcam +import websockets + +WIDTH = 240 +HEIGHT = 160 +URI: str = "ws://127.0.0.1:6789/" +KEYMAP: dict[str, int] = { + "a": 0, + "b": 1, + "select": 2, + "start": 3, + "right": 4, + "left": 5, + "up": 6, + "down": 7, + "r": 8, + "l": 9, +} + +core = mgba.core.load_path("roms/pokemon.gba") +screen = mgba.image.Image(WIDTH, HEIGHT) +core.set_video_buffer(screen) +core.reset() +mgba.log.silence() + +logging.basicConfig(level=logging.DEBUG) + + +async def main(): + with pyvirtualcam.Camera(width=WIDTH, height=HEIGHT, fps=60) as cam: + logging.debug(f"Using virtual camera: {cam.device}") + async with websockets.connect(URI) as websocket: + await websocket.send('{"auth":"password"}') + logging.debug(f"connected to: {websocket}") + test = core.save_raw_state() + + while True: + + if core.frame_counter % 30 == 0: + await websocket.send('{"action":"get"}') + response = await websocket.recv() + if response in KEYMAP: + key = KEYMAP[response] + core.set_keys(key) + logging.debug(f"pressing: {key}") + + # with open("pokemon.state", "w") as f: + # f.write(test) + core.load_raw_state(test) + print(str(test)) + + core.run_frame() + core.clear_keys(*KEYMAP.values()) + + frame = np.array(screen.to_pil().convert("RGB"), np.uint8) + cam.send(frame) + cam.sleep_until_next_frame() + + +asyncio.run(main()) diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..616a488 --- /dev/null +++ b/src/server.py @@ -0,0 +1,119 @@ +import asyncio +import json +import logging +import time +from typing import Any + +import websockets + +logging.basicConfig(level=logging.DEBUG) + + +class User: + websocket: Any = None + last_message: float = time.time() + has_voted: bool = False + + def register(self, websocket: Any): + self.websocket = websocket + USERS.add(self) + logging.debug( + f"user registered: {self}", + ) + + def unregister(self): + # self.websocket.close() + USERS.remove(self) + logging.debug( + f"user unregistered: {self}", + ) + + def __str__(self) -> str: + return f"{self.websocket.remote_address} ({self.websocket.id})" + + +USERS: set[User] = set() +EMULATOR: Any = None +PASSWORD: str = "password" + +VOTES: dict[str, int] = { + "a": 0, + "b": 0, + "select": 0, + "start": 0, + "right": 0, + "left": 0, + "up": 0, + "down": 0, + "r": 0, + "l": 0, +} + + +def clear_votes(): + for key in VOTES.keys(): + VOTES[key] = 0 + for user in USERS: + user.has_voted = False + logging.info(f"votes cleared: {VOTES}") + + +def next_move(): + if any(VOTES.values()): + return max(VOTES, key=VOTES.get) + else: + return "null" + + +async def handler(websocket, path): + try: + global EMULATOR + + # Register user + user = User() + user.register(websocket) + + # Manage received messages + async for message in websocket: + data = json.loads(message) + + if "auth" in data: + auth: str = data["auth"] + logging.debug(f"auth received: {auth}") + if not EMULATOR and auth == PASSWORD: + EMULATOR = websocket + user.unregister() + logging.debug(f"emulator authenticated: {EMULATOR}") + + if "action" in data: + action: str = data["action"] + + if action in VOTES: + VOTES[action] += 1 + user.last_message = time.time() + user.has_voted = True + logging.debug(f"key received: {action} ({VOTES[action]}), from {user}") + + elif action == "get": + if EMULATOR and websocket == EMULATOR: + move = next_move() + await websocket.send(move) + logging.info(f"vote sent: {move}") + clear_votes() + else: + logging.error(f"user is not EMULATOR: {user}") + + else: + logging.error(f"unsupported action: {data}") + finally: + # Unregister user + user.unregister() + + +async def main(): + async with websockets.serve(handler, "localhost", 6789): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main())