Skip to content

Commit 38aab63

Browse files
committed
ADD TCP Listener
1 parent 136e3dc commit 38aab63

File tree

7 files changed

+342
-5
lines changed

7 files changed

+342
-5
lines changed

src/gh/components/DF_http_listener/code.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ def _import_job(url):
2727
if not url.lower().endswith('.ply'):
2828
raise ValueError("URL must end in .ply")
2929

30-
resp = requests.get(url, timeout=30); resp.raise_for_status()
30+
resp = requests.get(url, timeout=30)
31+
resp.raise_for_status()
3132
fn = os.path.basename(url)
3233
tmp = os.path.join(tempfile.gettempdir(), fn)
3334
with open(tmp, 'wb') as f:
@@ -61,16 +62,19 @@ def _import_job(url):
6162
sc.sticky['imported_geom'] = geom
6263
count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count
6364
if isinstance(geom, rg.PointCloud):
64-
sc.sticky['status_message'] = f"Done: {count} points"
65-
else: sc.sticky['status_message'] = f"Done: {count} vertices"
65+
sc.sticky['status_message'] = f"Done: {count} points"
66+
else:
67+
sc.sticky['status_message'] = f"Done: {count} vertices"
6668
ghenv.Component.Message = sc.sticky.get('status_message')
6769

6870
except Exception as e:
6971
sc.sticky['imported_geom'] = None
7072
sc.sticky['status_message'] = f"Error: {e}"
7173
finally:
72-
try: os.remove(tmp)
73-
except: pass
74+
try:
75+
os.remove(tmp)
76+
except:
77+
pass
7478
sc.sticky['thread_running'] = False
7579
ghenv.Component.ExpireSolution(True)
7680

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from ghpythonlib.componentbase import executingcomponent as component
2+
import socket
3+
import threading
4+
import json
5+
import scriptcontext as sc
6+
import Rhino.Geometry as rg
7+
import System.Drawing as sd
8+
9+
10+
class DFHTTPListener(component):
11+
def RunScript(self, i_load: bool, i_reset: bool, i_port: int, i_host: str):
12+
13+
# Sticky defaults
14+
sc.sticky.setdefault('listen_addr', None)
15+
sc.sticky.setdefault('server_sock', None)
16+
sc.sticky.setdefault('server_started', False)
17+
sc.sticky.setdefault('cloud_buffer_raw', [])
18+
sc.sticky.setdefault('latest_cloud', None)
19+
sc.sticky.setdefault('status_message', "Waiting...")
20+
sc.sticky.setdefault('prev_load', False)
21+
22+
# Handle Reset or host/port change
23+
addr = (i_host, i_port)
24+
if i_reset or sc.sticky['listen_addr'] != addr:
25+
# close old socket if any
26+
old = sc.sticky.get('server_sock')
27+
try:
28+
if old: old.close()
29+
except: pass
30+
31+
sc.sticky['listen_addr'] = addr
32+
sc.sticky['server_sock'] = None
33+
sc.sticky['server_started'] = False
34+
sc.sticky['cloud_buffer_raw'] = []
35+
sc.sticky['latest_cloud'] = None
36+
sc.sticky['status_message'] = "Reset" if i_reset else f"Addr → {i_host}:{i_port}"
37+
ghenv.Component.Message = sc.sticky['status_message']
38+
39+
# Client handler
40+
def handle_client(conn):
41+
buf = b''
42+
with conn:
43+
while True:
44+
try:
45+
chunk = conn.recv(4096)
46+
if not chunk:
47+
break
48+
buf += chunk
49+
while b'\n' in buf:
50+
line, buf = buf.split(b'\n', 1)
51+
try:
52+
raw = json.loads(line.decode())
53+
except Exception as e:
54+
sc.sticky['status_message'] = f"JSON error: {e}"
55+
ghenv.Component.Message = sc.sticky['status_message']
56+
continue
57+
58+
if isinstance(raw, list) and all(isinstance(pt, list) and len(pt)==6 for pt in raw):
59+
sc.sticky['cloud_buffer_raw'] = raw
60+
sc.sticky['status_message'] = f"Buffered {len(raw)} pts"
61+
else:
62+
sc.sticky['status_message'] = "Unexpected format"
63+
ghenv.Component.Message = sc.sticky['status_message']
64+
except Exception as e:
65+
sc.sticky['status_message'] = f"Socket error: {e}"
66+
ghenv.Component.Message = sc.sticky['status_message']
67+
break
68+
69+
def accept_loop(srv_sock):
70+
while True:
71+
try:
72+
conn, _ = srv_sock.accept()
73+
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
74+
except:
75+
break
76+
77+
#Start server
78+
def start_server():
79+
try:
80+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
81+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
82+
srv.bind((i_host, i_port))
83+
srv.listen()
84+
sc.sticky['server_sock'] = srv
85+
sc.sticky['server_started'] = True
86+
sc.sticky['status_message'] = f"Listening on {i_host}:{i_port}"
87+
ghenv.Component.Message = sc.sticky['status_message']
88+
threading.Thread(target=accept_loop, args=(srv,), daemon=True).start()
89+
except Exception as e:
90+
sc.sticky['status_message'] = f"Server error: {e}"
91+
ghenv.Component.Message = sc.sticky['status_message']
92+
93+
if not sc.sticky['server_started']:
94+
start_server()
95+
96+
if i_load and not sc.sticky['prev_load']:
97+
raw = sc.sticky['cloud_buffer_raw']
98+
if raw:
99+
pc = rg.PointCloud()
100+
for x, y, z, r, g, b in raw:
101+
col = sd.Color.FromArgb(int(r), int(g), int(b))
102+
pc.Add(rg.Point3d(x, y, z), col)
103+
sc.sticky['latest_cloud'] = pc
104+
sc.sticky['status_message'] = f"Retrieved {pc.Count} pts"
105+
else:
106+
sc.sticky['status_message'] = "No data buffered"
107+
ghenv.Component.Message = sc.sticky['status_message']
108+
109+
sc.sticky['prev_load'] = i_load
110+
111+
return [sc.sticky['latest_cloud']]
4.39 KB
Loading
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"name": "DFTCPListener",
3+
"nickname": "TCPIn",
4+
"category": "diffCheck",
5+
"subcategory": "IO",
6+
"description": "This component get data from a tcp sender",
7+
"exposure": 4,
8+
"instanceGuid": "61a9cc27-864d-4892-bd39-5d97dbccbefb",
9+
"ghpython": {
10+
"hideOutput": true,
11+
"hideInput": true,
12+
"isAdvancedMode": true,
13+
"marshalOutGuids": true,
14+
"iconDisplay": 2,
15+
"inputParameters": [
16+
{
17+
"name": "i_load",
18+
"nickname": "i_load",
19+
"description": "Button to get a new PCD batch",
20+
"optional": true,
21+
"allowTreeAccess": true,
22+
"showTypeHints": true,
23+
"scriptParamAccess": "item",
24+
"wireDisplay": "default",
25+
"sourceCount": 0,
26+
"typeHintID": "bool"
27+
},
28+
{
29+
"name": "i_reset",
30+
"nickname": "i_reset",
31+
"description": "Reset the port",
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_port",
42+
"nickname": "i_port",
43+
"description": "The port for the connection",
44+
"optional": true,
45+
"allowTreeAccess": true,
46+
"showTypeHints": true,
47+
"scriptParamAccess": "item",
48+
"wireDisplay": "default",
49+
"sourceCount": 0,
50+
"typeHintID": "int"
51+
},
52+
{
53+
"name": "i_host",
54+
"nickname": "i_host",
55+
"description": "The host for the connection",
56+
"optional": true,
57+
"allowTreeAccess": true,
58+
"showTypeHints": true,
59+
"scriptParamAccess": "item",
60+
"wireDisplay": "default",
61+
"sourceCount": 0,
62+
"typeHintID": "str"
63+
}
64+
],
65+
"outputParameters": [
66+
{
67+
"name": "o_cloud",
68+
"nickname": "o_cloud",
69+
"description": "The pcd that was received.",
70+
"optional": false,
71+
"sourceCount": 0,
72+
"graft": false
73+
}
74+
]
75+
}
76+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#! python3
2+
3+
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
10+
import scriptcontext as sc
11+
12+
13+
class DFHTTPListener(component):
14+
15+
def RunScript(self,
16+
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')
22+
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); resp.raise_for_status()
31+
fn = os.path.basename(url)
32+
tmp = os.path.join(tempfile.gettempdir(), fn)
33+
with open(tmp, 'wb') as f:
34+
f.write(resp.content)
35+
36+
doc = Rhino.RhinoDoc.ActiveDoc
37+
before_ids = {o.Id for o in doc.Objects}
38+
39+
opts = Rhino.FileIO.FilePlyReadOptions()
40+
ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts)
41+
if not ok:
42+
raise RuntimeError("Rhino.FilePly.Read failed")
43+
44+
after_ids = {o.Id for o in doc.Objects}
45+
new_ids = after_ids - before_ids
46+
47+
geom = None
48+
for guid in new_ids:
49+
g = doc.Objects.FindId(guid).Geometry
50+
if isinstance(g, rg.PointCloud):
51+
geom = g.Duplicate()
52+
break
53+
elif isinstance(g, rg.Mesh):
54+
geom = g.DuplicateMesh()
55+
break
56+
57+
for guid in new_ids:
58+
doc.Objects.Delete(guid, True)
59+
doc.Views.Redraw()
60+
61+
sc.sticky['imported_geom'] = geom
62+
count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count
63+
if isinstance(geom, rg.PointCloud):
64+
sc.sticky['status_message'] = f"Done: {count} points"
65+
else: sc.sticky['status_message'] = f"Done: {count} vertices"
66+
ghenv.Component.Message = sc.sticky.get('status_message')
67+
68+
except Exception as e:
69+
sc.sticky['imported_geom'] = None
70+
sc.sticky['status_message'] = f"Error: {e}"
71+
finally:
72+
try: os.remove(tmp)
73+
except: pass
74+
sc.sticky['thread_running'] = False
75+
ghenv.Component.ExpireSolution(True)
76+
77+
if sc.sticky['ply_url'] != i_ply_url:
78+
sc.sticky['ply_url'] = i_ply_url
79+
sc.sticky['status_message'] = "URL changed. Press Load"
80+
sc.sticky['thread_running'] = False
81+
sc.sticky['prev_load'] = False
82+
83+
if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']:
84+
sc.sticky['status_message'] = "Loading..."
85+
sc.sticky['thread_running'] = True
86+
threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start()
87+
88+
sc.sticky['prev_load'] = i_load
89+
ghenv.Component.Message = sc.sticky.get('status_message', "")
90+
91+
# output
92+
o_geometry = sc.sticky.get('imported_geom')
93+
94+
return [o_geometry]
4.24 KB
Loading
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "DFWEBSOCKETListener",
3+
"nickname": "WSIn",
4+
"category": "diffCheck",
5+
"subcategory": "IO",
6+
"description": "This component reads a ply file from the internet.",
7+
"exposure": 4,
8+
"instanceGuid": "909d9a4f-2698-4fbf-8dcb-2005f51e047f",
9+
"ghpython": {
10+
"hideOutput": true,
11+
"hideInput": true,
12+
"isAdvancedMode": true,
13+
"marshalOutGuids": true,
14+
"iconDisplay": 2,
15+
"inputParameters": [
16+
{
17+
"name": "i_load",
18+
"nickname": "i_load",
19+
"description": "Button to import ply from url.",
20+
"optional": true,
21+
"allowTreeAccess": true,
22+
"showTypeHints": true,
23+
"scriptParamAccess": "item",
24+
"wireDisplay": "default",
25+
"sourceCount": 0,
26+
"typeHintID": "bool"
27+
},
28+
{
29+
"name": "i_ply_url",
30+
"nickname": "i_ply_url",
31+
"description": "The url where to get the pointcloud",
32+
"optional": true,
33+
"allowTreeAccess": true,
34+
"showTypeHints": true,
35+
"scriptParamAccess": "item",
36+
"wireDisplay": "default",
37+
"sourceCount": 0,
38+
"typeHintID": "str"
39+
}
40+
],
41+
"outputParameters": [
42+
{
43+
"name": "o_geometry",
44+
"nickname": "o_geo",
45+
"description": "The mesh or pcd that was imported.",
46+
"optional": false,
47+
"sourceCount": 0,
48+
"graft": false
49+
}
50+
]
51+
}
52+
}

0 commit comments

Comments
 (0)