basic fixes for the server

This commit is contained in:
Yeicor
2024-03-02 18:10:49 +01:00
parent beb7ee6ef5
commit 2ff9ac9e7e
4 changed files with 39 additions and 29 deletions

View File

@@ -43,13 +43,16 @@ export class NetworkManager extends EventTarget {
ws.onmessage = (event) => { ws.onmessage = (event) => {
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
console.debug("WebSocket message", 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) => { ws.onerror = (event) => {
console.error("WebSocket error", event); console.error("WebSocket error", event);
} }
ws.onclose = () => { ws.onclose = () => {
//console.trace("WebSocket closed, reconnecting very soon"); console.debug("WebSocket closed, reconnecting very soon");
setTimeout(() => this.monitorWebSocket(url), settings.checkServerEveryMs); setTimeout(() => this.monitorWebSocket(url), settings.checkServerEveryMs);
} }
} }

View File

@@ -2,11 +2,11 @@
export const settings = { export const settings = {
preloadModels: [ preloadModels: [
// @ts-ignore // @ts-ignore
new URL('../../assets/fox.glb', import.meta.url).href, // new URL('../../assets/fox.glb', import.meta.url).href,
// @ts-ignore // @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 // 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 */ displayLoadingEveryMs: 1000, /* How often to display partially loaded models */
checkServerEveryMs: 100, /* How often to check for a new server */ checkServerEveryMs: 100, /* How often to check for a new server */

View File

@@ -4,7 +4,6 @@ import os
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from build123d import * from build123d import *
from build123d import Shape
def build_logo() -> TopoDS_Shape: def build_logo() -> TopoDS_Shape:
@@ -21,7 +20,6 @@ def build_logo() -> TopoDS_Shape:
return logo_obj.part.wrapped return logo_obj.part.wrapped
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)

View File

@@ -4,7 +4,7 @@ import os
import signal import signal
import sys import sys
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass
from threading import Thread from threading import Thread
from typing import Optional, Dict, Union from typing import Optional, Dict, Union
@@ -12,7 +12,7 @@ import aiohttp_cors
from OCP.TopoDS import TopoDS_Shape from OCP.TopoDS import TopoDS_Shape
from aiohttp import web from aiohttp import web
from build123d import Shape, Axis from build123d import Shape, Axis
from dataclasses_json import dataclass_json, config from dataclasses_json import dataclass_json
from mylogger import logger from mylogger import logger
from pubsub import BufferedPubSub 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""" """Name of the object. Should be unique unless you want to overwrite the previous object"""
hash: str hash: str
"""Hash of the object, to detect changes without rebuilding the object""" """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)""" """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)""" """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 # noinspection PyUnusedLocal
async def _index_handler(request: web.Request) -> web.Response: async def _index_handler(request: web.Request) -> web.Response:
@@ -47,7 +60,7 @@ class Server:
runner: web.AppRunner runner: web.AppRunner
thread: Optional[Thread] = None thread: Optional[Thread] = None
do_shutdown = asyncio.Event() do_shutdown = asyncio.Event()
show_events = BufferedPubSub[UpdatesApiData]() show_events = BufferedPubSub[UpdatesApiFullData]()
object_events: Dict[str, BufferedPubSub[bytes]] = {} object_events: Dict[str, BufferedPubSub[bytes]] = {}
object_events_lock = asyncio.Lock() object_events_lock = asyncio.Lock()
@@ -134,6 +147,7 @@ class Server:
subscription = self.show_events.subscribe() subscription = self.show_events.subscribe()
try: try:
async for data in subscription: async for data in subscription:
logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
await ws.send_str(data.to_json()) await ws.send_str(data.to_json())
finally: finally:
@@ -141,13 +155,15 @@ class Server:
# Start sending updates to the client automatically # Start sending updates to the client automatically
send_task = asyncio.create_task(_send_api_updates()) send_task = asyncio.create_task(_send_api_updates())
receive_task = asyncio.create_task(ws.receive())
try: try:
logger.debug('Client connected: %s', request.remote) logger.debug('Client connected: %s', request.remote)
# Wait for the client to close the connection (or send a message) # Wait for the client to close the connection (or send a message)
await ws.receive() done, pending = await asyncio.wait([send_task, receive_task], return_when=asyncio.FIRST_COMPLETED)
finally:
# Make sure to stop sending updates to the client and close the connection # 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() await ws.close()
logger.debug('Client disconnected: %s', request.remote) logger.debug('Client disconnected: %s', request.remote)
@@ -159,7 +175,7 @@ class Server:
kwargs=None): kwargs=None):
name = name or f'object_{self.obj_counter}' name = name or f'object_{self.obj_counter}'
self.obj_counter += 1 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) self.show_events.publish_nowait(precomputed_info)
logger.info('show_object(%s, %s) took %.3f seconds', name, hash, time.time() - start) logger.info('show_object(%s, %s) took %.3f seconds', name, hash, time.time() - start)
return precomputed_info return precomputed_info
@@ -198,6 +214,7 @@ class Server:
# Build123D & CadQuery # Build123D & CadQuery
while 'wrapped' in dir(obj) and not isinstance(obj, TopoDS_Shape): while 'wrapped' in dir(obj) and not isinstance(obj, TopoDS_Shape):
obj = obj.wrapped obj = obj.wrapped
# TODO: Support locations (drawn as axes)
if not isinstance(obj, TopoDS_Shape): if not isinstance(obj, TopoDS_Shape):
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)') 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: async def _api_object(self, request: web.Request) -> web.Response:
"""Returns the object file with the matching name, building it if necessary.""" """Returns the object file with the matching name, building it if necessary."""
# Export the object (or fail if not found) # Export the object (or fail if not found)
exported_glb = await self.export(request.match_info['name']) 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 # Wrap the GLB in a response and return it
response.body = exported_glb response = web.Response(body=exported_glb)
finally: response.content_type = 'model/gltf-binary'
# Close the response (if not an error) response.headers['Content-Disposition'] = f'attachment; filename="{request.match_info["name"]}.glb"'
if response.prepared:
await response.write_eof()
return response return response
async def export(self, name: str) -> bytes: async def export(self, name: str) -> bytes:
@@ -238,6 +246,7 @@ class Server:
async for data in subscription: async for data in subscription:
if data.name == name: if data.name == name:
obj = data.obj obj = data.obj
kwargs = data.kwargs
found = True # Required because obj could be None found = True # Required because obj could be None
break break
finally: finally: