Skip to content

Commit e09fb79

Browse files
committed
FIX websocket component
1 parent 4aafbf8 commit e09fb79

File tree

10 files changed

+139
-152
lines changed

10 files changed

+139
-152
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ repos:
3434
types-requests==2.31.0,
3535
numpy==2.0.1,
3636
pytest==8.3.1,
37+
websockets>=10.4,
3738
types-setuptools>=71.1.0.20240818
3839
]
3940
args: [--config=pyproject.toml]

deps/eigen

Submodule eigen updated from 171bd08 to ac6955e

deps/pybind11

Submodule pybind11 updated 101 files

environment.yml

48 Bytes
Binary file not shown.
Lines changed: 105 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,118 @@
11
#! python3
22

33
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
77
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
1111

1212

1313
class DFWSServerListener(component):
1414
def RunScript(self,
1515
i_start: bool,
16-
i_load: bool,
1716
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):
2020

21-
# --- Persistent state across runs ---
22-
sc.sticky.setdefault('ws_thread', None)
23-
sc.sticky.setdefault('ws_loop', None)
21+
# ─── Persistent state across runs ────────────────────────────────────
2422
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)
2829
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]

src/gh/diffCheck/diffCheck.egg-info/PKG-INFO

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Metadata-Version: 2.4
1+
Metadata-Version: 2.1
22
Name: diffCheck
33
Version: 1.3.0
44
Summary: DiffCheck is a package to check the differences between two timber structures
@@ -11,14 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
1111
Description-Content-Type: text/markdown
1212
Requires-Dist: numpy
1313
Requires-Dist: pybind11>=2.5.0
14-
Dynamic: author
15-
Dynamic: author-email
16-
Dynamic: classifier
17-
Dynamic: description
18-
Dynamic: description-content-type
19-
Dynamic: home-page
20-
Dynamic: requires-dist
21-
Dynamic: summary
14+
Requires-Dist: websockets>=10.4
2215

2316
# DiffCheck: CAD-Scan comparison
2417

src/gh/diffCheck/diffCheck.egg-info/SOURCES.txt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ diffCheck/df_joint_detector.py
99
diffCheck/df_transformations.py
1010
diffCheck/df_util.py
1111
diffCheck/df_visualization.py
12-
diffCheck/diffcheck_bindings.cp312-win_amd64.pyd
1312
diffCheck.egg-info/PKG-INFO
1413
diffCheck.egg-info/SOURCES.txt
1514
diffCheck.egg-info/dependency_links.txt
1615
diffCheck.egg-info/requires.txt
17-
diffCheck.egg-info/top_level.txt
18-
diffCheck/dlls/Open3D.dll
19-
diffCheck/dlls/diffCheck.dll
16+
diffCheck.egg-info/top_level.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
numpy
22
pybind11>=2.5.0
3+
websockets>=10.4

src/gh/diffCheck/setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
packages=find_packages(),
99
install_requires=[
1010
"numpy",
11-
"pybind11>=2.5.0"
11+
"pybind11>=2.5.0",
12+
"websockets>=10.4"
1213
# other dependencies...
1314
],
1415
description="DiffCheck is a package to check the differences between two timber structures",

src/gh/examples/simple_ws_sender.py

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,31 @@
1-
# import asyncio
2-
# import websockets
3-
# import json
4-
# import random
1+
import asyncio
2+
import websockets
3+
import random
4+
import json
55

6-
URI = "ws://127.0.0.1:8765" # match i_host and i_port on your GH component
7-
SEND_INTERVAL = 2.0 # seconds between sends
86

7+
def random_colored_point():
8+
x, y, z = [round(random.uniform(-10, 10), 2) for _ in range(3)]
9+
r, g, b = [random.randint(0, 255) for _ in range(3)]
10+
return [x, y, z, r, g, b]
911

10-
# async def send_points(uri):
11-
# """
12-
# Connects once to the WebSocket server and then
13-
# sends a random point dict every SEND_INTERVAL seconds.
14-
# """
15-
# async with websockets.connect(uri) as ws:
16-
# print(f"Connected to {uri}")
17-
# while True:
18-
# # Generate a random point
19-
# pt = {
20-
# "x": random.uniform(0, 10),
21-
# "y": random.uniform(0, 10),
22-
# "z": random.uniform(0, 10),
23-
# }
24-
# msg = json.dumps(pt)
25-
# await ws.send(msg)
26-
# print(f"Sent point: {pt}")
27-
# await asyncio.sleep(SEND_INTERVAL)
2812

13+
async def send_pointcloud(host="127.0.0.1", port=8765):
14+
uri = f"ws://{host}:{port}"
15+
print(f"Connecting to {uri}…")
16+
try:
17+
async with websockets.connect(uri) as ws:
18+
counter = 0
19+
while True:
20+
counter += 1
21+
# generate and send 1 000 random points
22+
pcd = [random_colored_point() for _ in range(1000)]
23+
await ws.send(json.dumps(pcd))
24+
print(f"[{counter}] Sent PCD with {len(pcd)} points")
25+
await asyncio.sleep(5)
2926

30-
# def main():
31-
# try:
32-
# asyncio.run(send_points(URI))
33-
# except KeyboardInterrupt:
34-
# print("\nSender interrupted and exiting.")
27+
except Exception as e:
28+
print(f"Connection error: {e}")
3529

36-
# if __name__ == "__main__":
37-
# main()
30+
if __name__ == "__main__":
31+
asyncio.run(send_pointcloud(host="127.0.0.1", port=9000))

0 commit comments

Comments
 (0)