Skip to content

Commit 4aafbf8

Browse files
committed
WIP FIX WS Receiver
1 parent 5cacef1 commit 4aafbf8

File tree

5 files changed

+192
-99
lines changed

5 files changed

+192
-99
lines changed
Lines changed: 108 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,118 @@
11
#! python3
22

33
from ghpythonlib.componentbase import executingcomponent as component
4-
import os
5-
import tempfile
6-
import requests
7-
import threading
8-
import Rhino
9-
import Rhino.Geometry as rg
4+
# import threading
5+
# import asyncio
6+
# import json
107
import scriptcontext as sc
8+
# import Rhino.Geometry as rg
9+
# import System.Drawing as sd
10+
#import websockets
1111

1212

13-
class DFHTTPListener(component):
14-
13+
class DFWSServerListener(component):
1514
def RunScript(self,
15+
i_start: bool,
1616
i_load: bool,
17-
i_ply_url: str):
18-
19-
sc.sticky.setdefault('ply_url', None)
20-
sc.sticky.setdefault('imported_geom', None)
21-
sc.sticky.setdefault('status_message','Idle')
17+
i_stop: bool,
18+
i_host: str,
19+
i_port: int): # Port to bind
20+
21+
# --- Persistent state across runs ---
22+
sc.sticky.setdefault('ws_thread', None)
23+
sc.sticky.setdefault('ws_loop', None)
24+
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')
28+
sc.sticky.setdefault('prev_start', False)
29+
sc.sticky.setdefault('prev_stop', False)
2230
sc.sticky.setdefault('prev_load', False)
23-
sc.sticky.setdefault('thread_running', False)
24-
25-
def _import_job(url):
26-
try:
27-
if not url.lower().endswith('.ply'):
28-
raise ValueError("URL must end in .ply")
29-
30-
resp = requests.get(url, timeout=30)
31-
resp.raise_for_status()
32-
fn = os.path.basename(url)
33-
tmp = os.path.join(tempfile.gettempdir(), fn)
34-
with open(tmp, 'wb') as f:
35-
f.write(resp.content)
36-
37-
doc = Rhino.RhinoDoc.ActiveDoc
38-
before_ids = {o.Id for o in doc.Objects}
39-
40-
opts = Rhino.FileIO.FilePlyReadOptions()
41-
ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts)
42-
if not ok:
43-
raise RuntimeError("Rhino.FilePly.Read failed")
44-
45-
after_ids = {o.Id for o in doc.Objects}
46-
new_ids = after_ids - before_ids
47-
48-
geom = None
49-
for guid in new_ids:
50-
g = doc.Objects.FindId(guid).Geometry
51-
if isinstance(g, rg.PointCloud):
52-
geom = g.Duplicate()
53-
break
54-
elif isinstance(g, rg.Mesh):
55-
geom = g.DuplicateMesh()
56-
break
57-
58-
for guid in new_ids:
59-
doc.Objects.Delete(guid, True)
60-
doc.Views.Redraw()
61-
62-
sc.sticky['imported_geom'] = geom
63-
count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count
64-
if isinstance(geom, rg.PointCloud):
65-
sc.sticky['status_message'] = f"Done: {count} points"
66-
else:
67-
sc.sticky['status_message'] = f"Done: {count} vertices"
68-
ghenv.Component.Message = sc.sticky.get('status_message') # noqa: F821
69-
70-
except Exception as e:
71-
sc.sticky['imported_geom'] = None
72-
sc.sticky['status_message'] = f"Error: {e}"
73-
finally:
74-
try:
75-
os.remove(tmp)
76-
except Exception:
77-
pass
78-
sc.sticky['thread_running'] = False
79-
ghenv.Component.ExpireSolution(True) # noqa: F821
80-
81-
if sc.sticky['ply_url'] != i_ply_url:
82-
sc.sticky['ply_url'] = i_ply_url
83-
sc.sticky['status_message'] = "URL changed. Press Load"
84-
sc.sticky['thread_running'] = False
85-
sc.sticky['prev_load'] = False
86-
87-
if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']:
88-
sc.sticky['status_message'] = "Loading..."
89-
sc.sticky['thread_running'] = True
90-
threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start()
91-
92-
sc.sticky['prev_load'] = i_load
93-
ghenv.Component.Message = sc.sticky.get('status_message', "") # noqa: F821
94-
95-
# output
96-
o_geometry = sc.sticky.get('imported_geom')
9731

98-
return [o_geometry]
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]
-651 Bytes
Loading

src/gh/components/DF_websocket_listener/metadata.json

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
11
{
2-
"name": "DFWEBSOCKETListener",
2+
"name": "DFWSListener",
33
"nickname": "WSIn",
44
"category": "diffCheck",
55
"subcategory": "IO",
6-
"description": "This component reads a ply file from the internet.",
6+
"description": "This component receives a pcd via websocket connection.",
77
"exposure": 4,
8-
"instanceGuid": "909d9a4f-2698-4fbf-8dcb-2005f51e047f",
8+
"instanceGuid": "4e87cc43-8f9f-4f8f-a63a-49f76229db3e",
99
"ghpython": {
1010
"hideOutput": true,
1111
"hideInput": true,
1212
"isAdvancedMode": true,
1313
"marshalOutGuids": true,
1414
"iconDisplay": 2,
1515
"inputParameters": [
16+
{
17+
"name": "i_start",
18+
"nickname": "i_start",
19+
"description": "Button to start the TCP server",
20+
"optional": true,
21+
"allowTreeAccess": true,
22+
"showTypeHints": true,
23+
"scriptParamAccess": "item",
24+
"wireDisplay": "default",
25+
"sourceCount": 0,
26+
"typeHintID": "bool"
27+
},
1628
{
1729
"name": "i_load",
1830
"nickname": "i_load",
19-
"description": "Button to import ply from url.",
31+
"description": "Button to get the latest PCD from the buffer",
32+
"optional": true,
33+
"allowTreeAccess": true,
34+
"showTypeHints": true,
35+
"scriptParamAccess": "item",
36+
"wireDisplay": "default",
37+
"sourceCount": 0,
38+
"typeHintID": "bool"
39+
},
40+
{
41+
"name": "i_stop",
42+
"nickname": "i_stop",
43+
"description": "Stop the server and release the port",
2044
"optional": true,
2145
"allowTreeAccess": true,
2246
"showTypeHints": true,
@@ -26,9 +50,21 @@
2650
"typeHintID": "bool"
2751
},
2852
{
29-
"name": "i_ply_url",
30-
"nickname": "i_ply_url",
31-
"description": "The url where to get the pointcloud",
53+
"name": "i_port",
54+
"nickname": "i_port",
55+
"description": "The port for the connection",
56+
"optional": true,
57+
"allowTreeAccess": true,
58+
"showTypeHints": true,
59+
"scriptParamAccess": "item",
60+
"wireDisplay": "default",
61+
"sourceCount": 0,
62+
"typeHintID": "int"
63+
},
64+
{
65+
"name": "i_host",
66+
"nickname": "i_host",
67+
"description": "The host for the connection",
3268
"optional": true,
3369
"allowTreeAccess": true,
3470
"showTypeHints": true,
@@ -40,9 +76,9 @@
4076
],
4177
"outputParameters": [
4278
{
43-
"name": "o_geometry",
44-
"nickname": "o_geo",
45-
"description": "The mesh or pcd that was imported.",
79+
"name": "o_cloud",
80+
"nickname": "o_cloud",
81+
"description": "The pcd that was received.",
4682
"optional": false,
4783
"sourceCount": 0,
4884
"graft": false

src/gh/examples/simple_tcp_sender.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ def random_colored_point():
2020
msg = json.dumps(cloud) + "\n"
2121
s.sendall(msg.encode())
2222
print("Sent cloud with", len(cloud), "colored points")
23-
time.sleep(1)
23+
time.sleep(1)

src/gh/examples/simple_ws_sender.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# import asyncio
2+
# import websockets
3+
# import json
4+
# import random
5+
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
8+
9+
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)
28+
29+
30+
# def main():
31+
# try:
32+
# asyncio.run(send_points(URI))
33+
# except KeyboardInterrupt:
34+
# print("\nSender interrupted and exiting.")
35+
36+
# if __name__ == "__main__":
37+
# main()

0 commit comments

Comments
 (0)