Skip to content

Commit aeecaa2

Browse files
committed
FIX tcp component + add example file sender
1 parent 2fd80df commit aeecaa2

File tree

4 files changed

+137
-78
lines changed

4 files changed

+137
-78
lines changed

src/gh/components/DF_http_listener/code.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,46 @@ def RunScript(self,
1616
i_load: bool,
1717
i_ply_url: str):
1818

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)
19+
sc.sticky.setdefault('ply_url', None) #last url processed
20+
sc.sticky.setdefault('imported_geom', None) #geo imported from ply
21+
sc.sticky.setdefault('status_message','Idle') #status message on component
22+
sc.sticky.setdefault('prev_load', False) #previous state of toggle
23+
sc.sticky.setdefault('thread_running', False) #is an import thread active?
2424

2525
def _import_job(url):
26+
"""
27+
Background job:
28+
- Downloads the .ply file from the URL
29+
- Imports it into the active Rhino document
30+
- Extracts the new geometry (point cloud or mesh)
31+
- Cleans up the temporary file and document objects
32+
- Updates sticky state and status message
33+
"""
2634
try:
2735
if not url.lower().endswith('.ply'):
2836
raise ValueError("URL must end in .ply")
2937

3038
resp = requests.get(url, timeout=30)
3139
resp.raise_for_status()
40+
# save om temporary file
3241
fn = os.path.basename(url)
3342
tmp = os.path.join(tempfile.gettempdir(), fn)
3443
with open(tmp, 'wb') as f:
3544
f.write(resp.content)
3645

3746
doc = Rhino.RhinoDoc.ActiveDoc
47+
# recordd existing object IDs to detect new ones
3848
before_ids = {o.Id for o in doc.Objects}
3949

50+
# import PLY using Rhino's API
4051
opts = Rhino.FileIO.FilePlyReadOptions()
4152
ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts)
4253
if not ok:
4354
raise RuntimeError("Rhino.FilePly.Read failed")
4455

4556
after_ids = {o.Id for o in doc.Objects}
4657
new_ids = after_ids - before_ids
47-
58+
# get new pcd or mesh from document
4859
geom = None
4960
for guid in new_ids:
5061
g = doc.Objects.FindId(guid).Geometry
@@ -54,11 +65,12 @@ def _import_job(url):
5465
elif isinstance(g, rg.Mesh):
5566
geom = g.DuplicateMesh()
5667
break
57-
68+
# remove imported objects
5869
for guid in new_ids:
5970
doc.Objects.Delete(guid, True)
6071
doc.Views.Redraw()
6172

73+
# store new geometry
6274
sc.sticky['imported_geom'] = geom
6375
count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count
6476
if isinstance(geom, rg.PointCloud):
@@ -75,15 +87,18 @@ def _import_job(url):
7587
os.remove(tmp)
7688
except Exception:
7789
pass
90+
# mark thread as finished
7891
sc.sticky['thread_running'] = False
7992
ghenv.Component.ExpireSolution(True) # noqa: F821
8093

94+
# check if the URL input has changed
8195
if sc.sticky['ply_url'] != i_ply_url:
8296
sc.sticky['ply_url'] = i_ply_url
8397
sc.sticky['status_message'] = "URL changed. Press Load"
8498
sc.sticky['thread_running'] = False
85-
sc.sticky['prev_load'] = False
99+
sc.sticky['prev_load'] = False
86100

101+
# start importing if Load toggle is pressed and import thread is not already running
87102
if i_load and not sc.sticky['prev_load'] and not sc.sticky['thread_running']:
88103
sc.sticky['status_message'] = "Loading..."
89104
sc.sticky['thread_running'] = True
Lines changed: 75 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#! python3
2+
13
from ghpythonlib.componentbase import executingcomponent as component
24
import socket
35
import threading
@@ -7,42 +9,33 @@
79
import System.Drawing as sd
810

911

10-
class DFHTTPListener(component):
11-
def RunScript(self, i_load: bool, i_reset: bool, i_port: int, i_host: str):
12+
class DFTCPListener(component):
13+
def RunScript(self,
14+
i_start: bool,
15+
i_load: bool,
16+
i_stop: bool,
17+
i_port: int,
18+
i_host: str):
1219

1320
# Sticky defaults
14-
sc.sticky.setdefault('listen_addr', None)
1521
sc.sticky.setdefault('server_sock', None)
1622
sc.sticky.setdefault('server_started', False)
1723
sc.sticky.setdefault('cloud_buffer_raw', [])
1824
sc.sticky.setdefault('latest_cloud', None)
19-
sc.sticky.setdefault('status_message', "Waiting...")
25+
sc.sticky.setdefault('status_message', 'Waiting...')
26+
sc.sticky.setdefault('prev_start', False)
27+
sc.sticky.setdefault('prev_stop', False)
2028
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:
29-
old.close()
30-
except Exception:
31-
pass
32-
33-
sc.sticky['listen_addr'] = addr
34-
sc.sticky['server_sock'] = None
35-
sc.sticky['server_started'] = False
36-
sc.sticky['cloud_buffer_raw'] = []
37-
sc.sticky['latest_cloud'] = None
38-
sc.sticky['status_message'] = "Reset" if i_reset else f"Addr → {i_host}:{i_port}"
39-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
29+
sc.sticky.setdefault('client_socks', []) # Track client sockets
4030

4131
# Client handler
4232
def handle_client(conn):
33+
"""
34+
reads the incoming bytes from a single client socket and stores valid data in a shared buffer
35+
"""
4336
buf = b''
4437
with conn:
45-
while True:
38+
while sc.sticky.get('server_started', False):
4639
try:
4740
chunk = conn.recv(4096)
4841
if not chunk:
@@ -52,61 +45,77 @@ def handle_client(conn):
5245
line, buf = buf.split(b'\n', 1)
5346
try:
5447
raw = json.loads(line.decode())
55-
except Exception as e:
56-
sc.sticky['status_message'] = f"JSON error: {e}"
57-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
48+
except Exception:
5849
continue
59-
60-
if isinstance(raw, list) and all(isinstance(pt, list) and len(pt)==6 for pt in raw):
50+
if isinstance(raw, list) and all(isinstance(pt, list) and len(pt) == 6 for pt in raw):
6151
sc.sticky['cloud_buffer_raw'] = raw
62-
sc.sticky['status_message'] = f"Buffered {len(raw)} pts"
63-
else:
64-
sc.sticky['status_message'] = "Unexpected format"
65-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
66-
except Exception as e:
67-
sc.sticky['status_message'] = f"Socket error: {e}"
68-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
52+
except Exception:
6953
break
7054

71-
def accept_loop(srv_sock):
72-
while True:
73-
try:
74-
conn, _ = srv_sock.accept()
75-
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
76-
except Exception:
77-
break
55+
# thread to accept incoming connections
56+
def server_loop(sock):
57+
"""
58+
runs in its own thread, continuously calling accept() on the listening socket
59+
Each time a client connects, it launches a new thread running handle_client to deal with that connection
60+
"""
61+
try:
62+
conn, _ = sock.accept()
63+
handle_client(conn)
64+
except Exception:
65+
pass
7866

79-
# Start server
67+
# Start TCP server
8068
def start_server():
81-
try:
82-
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
83-
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
84-
srv.bind((i_host, i_port))
85-
srv.listen()
86-
sc.sticky['server_sock'] = srv
87-
sc.sticky['server_started'] = True
88-
sc.sticky['status_message'] = f"Listening on {i_host}:{i_port}"
89-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
90-
threading.Thread(target=accept_loop, args=(srv,), daemon=True).start()
91-
except Exception as e:
92-
sc.sticky['status_message'] = f"Server error: {e}"
93-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
69+
"""
70+
creates and binds a TCP socket on the given host/port, marks the server as started and then starts the accept_loop in a background thread
71+
"""
72+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
73+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
74+
sock.bind((i_host, i_port))
75+
sock.listen(1)
76+
sc.sticky['server_sock'] = sock
77+
sc.sticky['server_started'] = True
78+
sc.sticky['status_message'] = f'Listening on {i_host}:{i_port}'
79+
# Only accept one connection to keep it long-lived
80+
threading.Thread(target=server_loop, args=(sock,), daemon=True).start()
9481

95-
if not sc.sticky['server_started']:
82+
def stop_server():
83+
sock = sc.sticky.get('server_sock')
84+
if sock:
85+
try:
86+
sock.close()
87+
except Exception:
88+
pass
89+
sc.sticky['server_sock'] = None
90+
sc.sticky['server_started'] = False
91+
sc.sticky['cloud_buffer_raw'] = []
92+
sc.sticky['status_message'] = 'Stopped'
93+
94+
# Start or stop server based on inputs
95+
if i_start and not sc.sticky['prev_start']:
9696
start_server()
97+
if i_stop and not sc.sticky['prev_stop']:
98+
stop_server()
9799

100+
# Load buffered points into PointCloud
98101
if i_load and not sc.sticky['prev_load']:
99-
raw = sc.sticky['cloud_buffer_raw']
102+
raw = sc.sticky.get('cloud_buffer_raw', [])
100103
if raw:
101104
pc = rg.PointCloud()
102105
for x, y, z, r, g, b in raw:
103-
col = sd.Color.FromArgb(int(r), int(g), int(b))
104-
pc.Add(rg.Point3d(x, y, z), col)
105-
sc.sticky['latest_cloud'] = pc
106-
sc.sticky['status_message'] = f"Retrieved {pc.Count} pts"
106+
pc.Add(rg.Point3d(x, y, z), sd.Color.FromArgb(int(r), int(g), int(b)))
107+
sc.sticky['latest_cloud'] = pc
108+
sc.sticky['status_message'] = f'Retrieved {pc.Count} pts'
107109
else:
108-
sc.sticky['status_message'] = "No data buffered"
109-
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
110+
sc.sticky['status_message'] = 'No data buffered'
111+
112+
# Update previous states
113+
sc.sticky['prev_start'] = i_start
114+
sc.sticky['prev_stop'] = i_stop
110115
sc.sticky['prev_load'] = i_load
111116

112-
return [sc.sticky['latest_cloud']]
117+
# Update UI and output
118+
ghenv.Component.Message = sc.sticky['status_message'] # noqa: F821
119+
120+
o_cloud = sc.sticky['latest_cloud']
121+
return [o_cloud]

src/gh/components/DF_tcp_listener/metadata.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,22 @@
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 get a new PCD batch",
31+
"description": "Button to get the latest PCD from the buffer",
2032
"optional": true,
2133
"allowTreeAccess": true,
2234
"showTypeHints": true,
@@ -26,9 +38,9 @@
2638
"typeHintID": "bool"
2739
},
2840
{
29-
"name": "i_reset",
30-
"nickname": "i_reset",
31-
"description": "Reset the port",
41+
"name": "i_stop",
42+
"nickname": "i_stop",
43+
"description": "Stop the server and release the port",
3244
"optional": true,
3345
"allowTreeAccess": true,
3446
"showTypeHints": true,

src/gh/examples/simple_tcp_sender.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import socket
2+
import time
3+
import random
4+
import json
5+
6+
host = '127.0.0.1'
7+
port = 5000
8+
9+
10+
def random_colored_point():
11+
x, y, z = [round(random.uniform(-10, 10), 2) for _ in range(3)]
12+
r, g, b = [random.randint(0, 255) for _ in range(3)]
13+
return [x, y, z, r, g, b]
14+
15+
16+
with socket.create_connection((host, port)) as s:
17+
print("Connected to GH")
18+
while True:
19+
cloud = [random_colored_point() for _ in range(1000)]
20+
msg = json.dumps(cloud) + "\n"
21+
s.sendall(msg.encode())
22+
print("Sent cloud with", len(cloud), "colored points")
23+
time.sleep(1)

0 commit comments

Comments
 (0)