From 2ff9ac9e7e32241e69a77ac99c08f46bf1bfd3c7 Mon Sep 17 00:00:00 2001 From: Yeicor <4929005+Yeicor@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:10:49 +0100 Subject: [PATCH] basic fixes for the server --- src/misc/network.ts | 7 ++++-- src/misc/settings.ts | 6 ++--- yacv_server/logo.py | 2 -- yacv_server/server.py | 53 +++++++++++++++++++++++++------------------ 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/misc/network.ts b/src/misc/network.ts index 57d2c24..f0d7ce0 100644 --- a/src/misc/network.ts +++ b/src/misc/network.ts @@ -43,13 +43,16 @@ export class NetworkManager extends EventTarget { ws.onmessage = (event) => { let data = JSON.parse(event.data); console.debug("WebSocket message", data); - this.foundModel(data.name, data.hash, data.url); + let urlObj = new URL(url); + urlObj.protocol = urlObj.protocol === "ws:" ? "http:" : "https:"; + urlObj.searchParams.set("api_object", data.name); + this.foundModel(data.name, data.hash, urlObj.toString()); }; ws.onerror = (event) => { console.error("WebSocket error", event); } ws.onclose = () => { - //console.trace("WebSocket closed, reconnecting very soon"); + console.debug("WebSocket closed, reconnecting very soon"); setTimeout(() => this.monitorWebSocket(url), settings.checkServerEveryMs); } } diff --git a/src/misc/settings.ts b/src/misc/settings.ts index 023a769..737b918 100644 --- a/src/misc/settings.ts +++ b/src/misc/settings.ts @@ -2,11 +2,11 @@ export const settings = { preloadModels: [ // @ts-ignore - new URL('../../assets/fox.glb', import.meta.url).href, + // new URL('../../assets/fox.glb', import.meta.url).href, // @ts-ignore - new URL('../../assets/logo.glb', import.meta.url).href, + // new URL('../../assets/logo.glb', import.meta.url).href, // Websocket URLs automatically listen for new models from the python backend - // "ws://localhost:32323/" + "ws://192.168.1.132:32323/" ], displayLoadingEveryMs: 1000, /* How often to display partially loaded models */ checkServerEveryMs: 100, /* How often to check for a new server */ diff --git a/yacv_server/logo.py b/yacv_server/logo.py index 1da3743..ee4a64c 100644 --- a/yacv_server/logo.py +++ b/yacv_server/logo.py @@ -4,7 +4,6 @@ import os from OCP.TopoDS import TopoDS_Shape from build123d import * -from build123d import Shape def build_logo() -> TopoDS_Shape: @@ -21,7 +20,6 @@ def build_logo() -> TopoDS_Shape: return logo_obj.part.wrapped - if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) diff --git a/yacv_server/server.py b/yacv_server/server.py index b16788e..87d9d45 100644 --- a/yacv_server/server.py +++ b/yacv_server/server.py @@ -4,7 +4,7 @@ import os import signal import sys import time -from dataclasses import dataclass, field +from dataclasses import dataclass from threading import Thread from typing import Optional, Dict, Union @@ -12,7 +12,7 @@ import aiohttp_cors from OCP.TopoDS import TopoDS_Shape from aiohttp import web from build123d import Shape, Axis -from dataclasses_json import dataclass_json, config +from dataclasses_json import dataclass_json from mylogger import logger from pubsub import BufferedPubSub @@ -31,11 +31,24 @@ class UpdatesApiData: """Name of the object. Should be unique unless you want to overwrite the previous object""" hash: str """Hash of the object, to detect changes without rebuilding the object""" - obj: Optional[TopoDS_Shape] = field(default=None, metadata=config(exclude=lambda obj: True)) + + +class UpdatesApiFullData(UpdatesApiData): + obj: Optional[TopoDS_Shape] """The OCCT object, if any (not serialized)""" - kwargs: Optional[Dict[str, any]] = field(default=None, metadata=config(exclude=lambda obj: True)) + kwargs: Optional[Dict[str, any]] """The show_object options, if any (not serialized)""" + def __init__(self, name: str, hash: str, obj: Optional[TopoDS_Shape] = None, + kwargs: Optional[Dict[str, any]] = None): + self.name = name + self.hash = hash + self.obj = obj + self.kwargs = kwargs + + def to_json(self) -> str: + return super().to_json() + # noinspection PyUnusedLocal async def _index_handler(request: web.Request) -> web.Response: @@ -47,7 +60,7 @@ class Server: runner: web.AppRunner thread: Optional[Thread] = None do_shutdown = asyncio.Event() - show_events = BufferedPubSub[UpdatesApiData]() + show_events = BufferedPubSub[UpdatesApiFullData]() object_events: Dict[str, BufferedPubSub[bytes]] = {} object_events_lock = asyncio.Lock() @@ -134,6 +147,7 @@ class Server: subscription = self.show_events.subscribe() try: async for data in subscription: + logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data) # noinspection PyUnresolvedReferences await ws.send_str(data.to_json()) finally: @@ -141,13 +155,15 @@ class Server: # Start sending updates to the client automatically send_task = asyncio.create_task(_send_api_updates()) + receive_task = asyncio.create_task(ws.receive()) try: logger.debug('Client connected: %s', request.remote) # Wait for the client to close the connection (or send a message) - await ws.receive() - finally: + done, pending = await asyncio.wait([send_task, receive_task], return_when=asyncio.FIRST_COMPLETED) # Make sure to stop sending updates to the client and close the connection - send_task.cancel() + for task in pending: + task.cancel() + finally: await ws.close() logger.debug('Client disconnected: %s', request.remote) @@ -159,7 +175,7 @@ class Server: kwargs=None): name = name or f'object_{self.obj_counter}' self.obj_counter += 1 - precomputed_info = UpdatesApiData(name=name, hash=hash, obj=obj, kwargs=kwargs or {}) + precomputed_info = UpdatesApiFullData(name=name, hash=hash, obj=obj, kwargs=kwargs or {}) self.show_events.publish_nowait(precomputed_info) logger.info('show_object(%s, %s) took %.3f seconds', name, hash, time.time() - start) return precomputed_info @@ -198,6 +214,7 @@ class Server: # Build123D & CadQuery while 'wrapped' in dir(obj) and not isinstance(obj, TopoDS_Shape): obj = obj.wrapped + # TODO: Support locations (drawn as axes) if not isinstance(obj, TopoDS_Shape): raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)') @@ -208,22 +225,13 @@ class Server: async def _api_object(self, request: web.Request) -> web.Response: """Returns the object file with the matching name, building it if necessary.""" - # Export the object (or fail if not found) exported_glb = await self.export(request.match_info['name']) - response = web.Response() - try: - # Create a new stream response with custom content type and headers - response.content_type = 'model/gltf-binary' - response.headers['Content-Disposition'] = f'attachment; filename="{request.match_info["name"]}.glb"' - await response.prepare(request) - # Stream the export data to the response - response.body = exported_glb - finally: - # Close the response (if not an error) - if response.prepared: - await response.write_eof() + # Wrap the GLB in a response and return it + response = web.Response(body=exported_glb) + response.content_type = 'model/gltf-binary' + response.headers['Content-Disposition'] = f'attachment; filename="{request.match_info["name"]}.glb"' return response async def export(self, name: str) -> bytes: @@ -238,6 +246,7 @@ class Server: async for data in subscription: if data.name == name: obj = data.obj + kwargs = data.kwargs found = True # Required because obj could be None break finally: