|
1 | 1 | #! python3
|
2 | 2 |
|
3 | 3 | from ghpythonlib.componentbase import executingcomponent as component
|
4 |
| -# import threading |
5 |
| -# import asyncio |
6 |
| -# import json |
| 4 | +import threading |
| 5 | +import asyncio |
| 6 | +import json |
7 | 7 | import scriptcontext as sc
|
8 |
| -# import Rhino.Geometry as rg |
9 |
| -# import System.Drawing as sd |
10 |
| -#import websockets |
| 8 | +import Rhino.Geometry as rg |
| 9 | +import System.Drawing as sd |
| 10 | +from websockets.server import serve |
11 | 11 |
|
12 | 12 |
|
13 | 13 | class DFWSServerListener(component):
|
14 | 14 | def RunScript(self,
|
15 | 15 | i_start: bool,
|
16 |
| - i_load: bool, |
17 | 16 | i_stop: bool,
|
18 |
| - i_host: str, |
19 |
| - i_port: int): # Port to bind |
| 17 | + i_load: bool, |
| 18 | + i_port: int, |
| 19 | + i_host: str): |
20 | 20 |
|
21 |
| - # --- Persistent state across runs --- |
22 |
| - sc.sticky.setdefault('ws_thread', None) |
23 |
| - sc.sticky.setdefault('ws_loop', None) |
| 21 | + # ─── Persistent state across runs ──────────────────────────────────── |
24 | 22 | sc.sticky.setdefault('ws_server', None)
|
25 |
| - sc.sticky.setdefault('ws_buffer', []) |
26 |
| - sc.sticky.setdefault('ws_latest', None) |
27 |
| - sc.sticky.setdefault('status', 'Idle') |
| 23 | + sc.sticky.setdefault('ws_loop', None) |
| 24 | + sc.sticky.setdefault('ws_thread', None) |
| 25 | + sc.sticky.setdefault('last_pcd', None) |
| 26 | + sc.sticky.setdefault('loaded_pcd', None) |
| 27 | + sc.sticky.setdefault('ws_logs', []) |
| 28 | + sc.sticky.setdefault('ws_thread_started', False) |
28 | 29 | sc.sticky.setdefault('prev_start', False)
|
29 |
| - sc.sticky.setdefault('prev_stop', False) |
30 |
| - sc.sticky.setdefault('prev_load', False) |
31 |
| - |
32 |
| - # async def handler(ws, path): |
33 |
| - # """Receive JSON-encoded dicts and buffer valid points.""" |
34 |
| - # try: |
35 |
| - # async for msg in ws: |
36 |
| - # data = json.loads(msg) |
37 |
| - # if isinstance(data, dict) and {'x','y','z'}.issubset(data): |
38 |
| - # sc.sticky['ws_buffer'].append(data) |
39 |
| - # sc.sticky['status'] = f"Buffered {len(sc.sticky['ws_buffer'])} pts" |
40 |
| - # ghenv.Component.ExpireSolution(True) # noqa: F821 |
41 |
| - # except Exception: |
42 |
| - # pass |
43 |
| - |
44 |
| - # def server_thread(): |
45 |
| - # # Create and set a new event loop in this thread |
46 |
| - # loop = asyncio.new_event_loop() |
47 |
| - # sc.sticky['ws_loop'] = loop |
48 |
| - # asyncio.set_event_loop(loop) |
49 |
| - # try: |
50 |
| - # # Start the WebSocket server on this loop |
51 |
| - # start_srv = websockets.serve(handler, i_host, i_port) |
52 |
| - # server = loop.run_until_complete(start_srv) |
53 |
| - # sc.sticky['ws_server'] = server |
54 |
| - # sc.sticky['status'] = f"Listening ws://{i_host}:{i_port}" |
55 |
| - # ghenv.Component.ExpireSolution(True) # noqa: F821 |
56 |
| - # # Serve forever until stopped |
57 |
| - # loop.run_forever() |
58 |
| - # except Exception as ex: |
59 |
| - # sc.sticky['status'] = f"Server error: {type(ex).__name__}: {ex}" |
60 |
| - # ghenv.Component.ExpireSolution(True) # noqa: F821 |
61 |
| - # finally: |
62 |
| - # # Cleanup: wait for server to close then shutdown loop |
63 |
| - # srv = sc.sticky.get('ws_server') |
64 |
| - # if srv: |
65 |
| - # loop.run_until_complete(srv.wait_closed()) |
66 |
| - # loop.close() |
67 |
| - # sc.sticky['ws_loop'] = None |
68 |
| - # sc.sticky['ws_server'] = None |
69 |
| - |
70 |
| - # def start(): |
71 |
| - # # Begin server thread on rising edge |
72 |
| - # if sc.sticky['ws_thread'] is None: |
73 |
| - # sc.sticky['status'] = 'Starting WebSocket server...' |
74 |
| - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 |
75 |
| - # t = threading.Thread(target=server_thread, daemon=True) |
76 |
| - # sc.sticky['ws_thread'] = t |
77 |
| - # t.start() |
78 |
| - |
79 |
| - # def stop(): |
80 |
| - # # Signal server and loop to stop |
81 |
| - # server = sc.sticky.get('ws_server') |
82 |
| - # loop = sc.sticky.get('ws_loop') |
83 |
| - # if server and loop: |
84 |
| - # loop.call_soon_threadsafe(server.close) |
85 |
| - # loop.call_soon_threadsafe(loop.stop) |
86 |
| - # sc.sticky['status'] = 'Stopped' |
87 |
| - # sc.sticky['ws_buffer'] = [] |
88 |
| - # sc.sticky['ws_thread'] = None |
89 |
| - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 |
90 |
| - |
91 |
| - # # Handle toggles |
92 |
| - # if i_start and not sc.sticky['prev_start']: |
93 |
| - # start() |
94 |
| - # if i_stop and not sc.sticky['prev_stop']: |
95 |
| - # stop() |
96 |
| - # if i_load and not sc.sticky['prev_load']: |
97 |
| - # buf = sc.sticky['ws_buffer'] |
98 |
| - # if buf: |
99 |
| - # pc = rg.PointCloud() |
100 |
| - # for pt in buf: |
101 |
| - # pc.Add(rg.Point3d(pt['x'], pt['y'], pt['z']), sd.Color.White) |
102 |
| - # sc.sticky['ws_latest'] = pc |
103 |
| - # sc.sticky['status'] = f"Retrieved {pc.Count} pts" |
104 |
| - # sc.sticky['ws_buffer'] = [] |
105 |
| - # else: |
106 |
| - # sc.sticky['status'] = 'No data buffered' |
107 |
| - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 |
108 |
| - |
109 |
| - # # Update previous states |
110 |
| - # sc.sticky['prev_start'] = i_start |
111 |
| - # sc.sticky['prev_stop'] = i_stop |
112 |
| - # sc.sticky['prev_load'] = i_load |
113 |
| - |
114 |
| - # # Always update message |
115 |
| - # ghenv.Component.Message = sc.sticky['status'] # noqa: F821 |
116 |
| - |
117 |
| - # o_cloud = sc.sticky.get('ws_latest') |
118 |
| - # return [o_cloud] |
| 30 | + sc.sticky.setdefault('prev_stop', False) |
| 31 | + sc.sticky.setdefault('prev_load', False) |
| 32 | + |
| 33 | + logs = sc.sticky['ws_logs'] |
| 34 | + |
| 35 | + # ─── STOP server ──────────────────────────────────────────────────── |
| 36 | + if i_stop and sc.sticky.pop('ws_thread_started', False): |
| 37 | + server = sc.sticky.pop('ws_server', None) |
| 38 | + loop = sc.sticky.pop('ws_loop', None) |
| 39 | + if server and loop: |
| 40 | + try: |
| 41 | + server.close() |
| 42 | + asyncio.run_coroutine_threadsafe(server.wait_closed(), loop) |
| 43 | + logs.append("WebSocket server close initiated") |
| 44 | + except Exception as e: |
| 45 | + logs.append(f"Error closing server: {e}") |
| 46 | + sc.sticky['ws_thread'] = None |
| 47 | + logs.append("Cleared previous WebSocket server flag") |
| 48 | + ghenv.Component.ExpireSolution(True) # noqa: F821 |
| 49 | + |
| 50 | + # ─── START server ──────────────────────────────────────────────────── |
| 51 | + if i_start and not sc.sticky['ws_thread_started']: |
| 52 | + |
| 53 | + async def echo(ws, path): |
| 54 | + logs.append("[GH] Client connected") |
| 55 | + try: |
| 56 | + async for msg in ws: |
| 57 | + try: |
| 58 | + pcd = json.loads(msg) |
| 59 | + if isinstance(pcd, list) and all(isinstance(pt, (list, tuple)) and len(pt) == 6 for pt in pcd): |
| 60 | + sc.sticky['last_pcd'] = pcd |
| 61 | + logs.append(f"Received PCD with {len(pcd)} points") |
| 62 | + else: |
| 63 | + logs.append("Invalid PCD format") |
| 64 | + except Exception as inner: |
| 65 | + logs.append(f"PCD parse error: {inner}") |
| 66 | + except Exception as outer: |
| 67 | + logs.append(f"Handler crashed: {outer}") |
| 68 | + |
| 69 | + async def server_coro(): |
| 70 | + loop = asyncio.get_running_loop() |
| 71 | + sc.sticky['ws_loop'] = loop |
| 72 | + |
| 73 | + logs.append(f"server_coro starting on {i_host}:{i_port}") |
| 74 | + server = await serve(echo, i_host, i_port) |
| 75 | + sc.sticky['ws_server'] = server |
| 76 | + logs.append(f"Listening on ws://{i_host}:{i_port}") |
| 77 | + await server.wait_closed() |
| 78 | + logs.append("Server coroutine exited") |
| 79 | + |
| 80 | + def run_server(): |
| 81 | + try: |
| 82 | + asyncio.run(server_coro()) |
| 83 | + except Exception as ex: |
| 84 | + logs.append(f"WebSocket server ERROR: {ex}") |
| 85 | + |
| 86 | + t = threading.Thread(target=run_server, daemon=True) |
| 87 | + t.start() |
| 88 | + sc.sticky['ws_thread'] = t |
| 89 | + sc.sticky['ws_thread_started'] = True |
| 90 | + ghenv.Component.ExpireSolution(True) # noqa: F821 |
| 91 | + |
| 92 | + # ─── LOAD buffered PCD on i_load rising edge ───────────────────────── |
| 93 | + if i_load and not sc.sticky['prev_load']: |
| 94 | + sc.sticky['loaded_pcd'] = sc.sticky.get('last_pcd') |
| 95 | + cnt = len(sc.sticky['loaded_pcd']) if sc.sticky['loaded_pcd'] else 0 |
| 96 | + logs.append(f"Loaded pcd with {cnt} points") |
| 97 | + ghenv.Component.ExpireSolution(True) # noqa: F821 |
| 98 | + |
| 99 | + # ─── BUILD output PointCloud ──────────────────────────────────────── |
| 100 | + raw = sc.sticky.get('loaded_pcd') |
| 101 | + if isinstance(raw, list) and all(isinstance(pt, (list, tuple)) and len(pt) == 6 for pt in raw): |
| 102 | + pc = rg.PointCloud() |
| 103 | + for x, y, z, r, g, b in raw: |
| 104 | + pt = rg.Point3d(x, y, z) |
| 105 | + col = sd.Color.FromArgb(r, g, b) |
| 106 | + pc.Add(pt, col) |
| 107 | + o_cloud = pc |
| 108 | + else: |
| 109 | + o_cloud = None |
| 110 | + |
| 111 | + # ─── UPDATE UI message & return outputs ───────────────────────────── |
| 112 | + ghenv.Component.Message = logs[-1] if logs else 'Waiting' # noqa: F821 |
| 113 | + sc.sticky['prev_start'] = i_start |
| 114 | + sc.sticky['prev_stop'] = i_stop |
| 115 | + sc.sticky['prev_load'] = i_load |
| 116 | + |
| 117 | + |
| 118 | + return [o_cloud] |
0 commit comments