1- #! python3
2-
31from ghpythonlib .componentbase import executingcomponent as component
42import socket
53import threading
86import scriptcontext as sc
97import Rhino .Geometry as rg
108import System .Drawing as sd
9+ import Grasshopper
1110from diffCheck import df_gh_canvas_utils
1211
1312class DFTCPListener (component ):
1413 def __init__ (self ):
14+ super (DFTCPListener , self ).__init__ ()
1515 try :
1616 ghenv .Component .ExpireSolution (True ) # noqa: F821
1717 ghenv .Component .Attributes .PerformLayout () # noqa: F821
18- except NameError :
18+ except :
1919 pass
2020
2121 for idx , label in enumerate (("Start" , "Stop" , "Load" )):
22- df_gh_canvas_utils .add_button (
23- ghenv .Component , label , idx , x_offset = 60 ) # noqa: F821
22+ df_gh_canvas_utils .add_button (ghenv .Component , label , idx , x_offset = 60 ) # noqa: F821
2423 df_gh_canvas_utils .add_panel (ghenv .Component , "Host" , "127.0.0.1" , 3 , 60 , 20 ) # noqa: F821
2524 df_gh_canvas_utils .add_panel (ghenv .Component , "Port" , "5000" , 4 , 60 , 20 ) # noqa: F821
2625
27- def RunScript (self ,
28- i_start : bool ,
29- i_stop : bool ,
30- i_load : bool ,
31- i_host : str ,
32- i_port : int ):
33-
34- prefix = 'tcp'
26+ def RunScript (self , i_start , i_stop , i_load , i_host , i_port ):
27+ prefix = "tcp"
3528
29+ # ----------------------------
3630 # Sticky initialization
37- sc .sticky .setdefault (f'{ prefix } _server_sock' , None )
38- sc .sticky .setdefault (f'{ prefix } _server_started' , False )
39- sc .sticky .setdefault (f'{ prefix } _cloud_buffer_raw' , [])
40- sc .sticky .setdefault (f'{ prefix } _latest_cloud' , None )
41- sc .sticky .setdefault (f'{ prefix } _status_message' , 'Waiting..' )
42- sc .sticky .setdefault (f'{ prefix } _prev_start' , False )
43- sc .sticky .setdefault (f'{ prefix } _prev_stop' , False )
44- sc .sticky .setdefault (f'{ prefix } _prev_load' , False )
45-
46- # Client handler
47- def handle_client (conn : socket .socket ) -> None :
48- """
49- Reads the incoming bytes from a single TCP client socket and stores valid data in a shared buffer.
50-
51- :param conn: A socket object returned by `accept()` representing a live client connection.
52- The client is expected to send newline-delimited JSON-encoded data, where each
53- message is a list of 6D values: [x, y, z, r, g, b].
54-
55- :returns: None
56- """
57- buf = b''
31+ # ----------------------------
32+ sc .sticky .setdefault (prefix + "_server_sock" , None )
33+ sc .sticky .setdefault (prefix + "_server_started" , False )
34+ sc .sticky .setdefault (prefix + "_cloud_buffer_raw" , [])
35+ sc .sticky .setdefault (prefix + "_latest_cloud" , None )
36+ sc .sticky .setdefault (prefix + "_status_message" , "Waiting.." )
37+ sc .sticky .setdefault (prefix + "_prev_start" , False )
38+ sc .sticky .setdefault (prefix + "_prev_stop" , False )
39+ sc .sticky .setdefault (prefix + "_prev_load" , False )
40+
41+ # Receiving state
42+ sc .sticky .setdefault (prefix + "_is_receiving" , False )
43+ sc .sticky .setdefault (prefix + "_recv_bytes" , 0 )
44+
45+ # Loading state
46+ sc .sticky .setdefault (prefix + "_is_loading" , False )
47+ sc .sticky .setdefault (prefix + "_load_progress" , (0 , 0 )) # (done, total)
48+ sc .sticky .setdefault (prefix + "_load_started_at" , None )
49+ sc .sticky .setdefault (prefix + "_load_duration_s" , None )
50+
51+ # ----------------------------
52+ # Helper: schedule safe refresh on GH UI/solution thread
53+ # ----------------------------
54+ def request_expire (delay_ms = 200 , recompute = True ):
55+ try :
56+ comp = ghenv .Component # noqa: F821
57+ doc = comp .OnPingDocument ()
58+ if doc is None :
59+ return
60+
61+ def cb (_ ):
62+ try :
63+ comp .ExpireSolution (recompute )
64+ except :
65+ pass
66+
67+ doc .ScheduleSolution (int (delay_ms ), Grasshopper .Kernel .GH_Document .GH_ScheduleDelegate (cb ))
68+ except :
69+ pass
70+
71+ # ----------------------------
72+ # TCP receive thread
73+ # ----------------------------
74+ def handle_client (conn ):
75+ buf = b""
76+ sc .sticky [prefix + "_is_receiving" ] = True
77+ sc .sticky [prefix + "_recv_bytes" ] = 0
78+ sc .sticky [prefix + "_status_message" ] = "Client connected; receiving..."
79+ request_expire (0 , True )
80+
5881 with conn :
59- while sc .sticky .get (f' { prefix } _server_started' , False ):
82+ while sc .sticky .get (prefix + " _server_started" , False ):
6083 try :
61- chunk = conn .recv (4096 )
84+ chunk = conn .recv (65536 )
6285 if not chunk :
6386 break
87+ sc .sticky [prefix + "_recv_bytes" ] += len (chunk )
6488 buf += chunk
65- while b'\n ' in buf :
66- line , buf = buf .split (b'\n ' , 1 )
89+
90+ # Expect ONE message terminated by '\n'
91+ if b"\n " in buf :
92+ line , buf = buf .split (b"\n " , 1 )
93+
6794 try :
68- raw = json .loads (line .decode ())
69- except Exception :
70- continue
95+ raw = json .loads (line .decode ("utf-8" ))
96+ except Exception as e :
97+ sc .sticky [prefix + "_status_message" ] = "JSON error: {}" .format (repr (e ))
98+ request_expire (0 , True )
99+ break
100+
71101 if isinstance (raw , list ) and all (isinstance (pt , list ) and len (pt ) == 6 for pt in raw ):
72- sc .sticky [f'{ prefix } _cloud_buffer_raw' ] = raw
73- except Exception :
74- break
75- time .sleep (0.05 ) # sleep briefly to prevent CPU spin
102+ sc .sticky [prefix + "_cloud_buffer_raw" ] = raw
103+ sc .sticky [prefix + "_status_message" ] = "Buffered {} pts" .format (len (raw ))
104+ else :
105+ sc .sticky [prefix + "_status_message" ] = "Invalid payload (expected [[x,y,z,r,g,b],...])"
106+
107+ request_expire (0 , True )
108+ break
76109
77- # thread to accept incoming connections
78- def server_loop ( sock : socket . socket ) -> None :
79- """
80- Accepts a single client connection and starts a background thread to handle it.
110+ except Exception as e :
111+ sc . sticky [ prefix + "_status_message" ] = "Recv error: {}" . format ( repr ( e ))
112+ request_expire ( 0 , True )
113+ break
81114
82- :param sock: A bound and listening TCP socket created by start_server().
83- This socket will accept one incoming connection, then delegate it to handle_client().
115+ sc . sticky [ prefix + "_is_receiving" ] = False
116+ request_expire ( 0 , True )
84117
85- :returns: None. This runs as a background thread and blocks on accept().
86- """
118+ def server_loop (sock ):
87119 try :
88120 conn , _ = sock .accept ()
89121 handle_client (conn )
90- except Exception :
91- pass
92-
93- # Start TCP server
94- def start_server () -> None :
95- """
96- 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
122+ except Exception as e :
123+ sc .sticky [prefix + "_status_message" ] = "Accept error: {}" .format (repr (e ))
124+ request_expire (0 , True )
97125
98- :returns: None.
99- """
126+ def start_server ():
100127 sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
101128 sock .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
102- sock .bind ((i_host , i_port ))
129+ sock .bind ((i_host , int ( i_port ) ))
103130 sock .listen (1 )
104- sc .sticky [f'{ prefix } _server_sock' ] = sock
105- sc .sticky [f'{ prefix } _server_started' ] = True
106- sc .sticky [f'{ prefix } _status_message' ] = f'Listening on { i_host } :{ i_port } '
107- # Only accept one connection to keep it long-lived
108- threading .Thread (target = server_loop , args = (sock ,), daemon = True ).start ()
109131
110- def stop_server () -> None :
111- """
112- Stops the running TCP server by closing the listening socket and resetting internal state.
132+ sc .sticky [prefix + "_server_sock" ] = sock
133+ sc .sticky [prefix + "_server_started" ] = True
134+ sc .sticky [prefix + "_status_message" ] = "Listening on {}:{}" .format (i_host , i_port )
135+
136+ threading .Thread (target = server_loop , args = (sock ,), daemon = True ).start ()
137+ request_expire (0 , True )
113138
114- :returns: None.
115- """
116- sock = sc .sticky .get (f'{ prefix } _server_sock' )
139+ def stop_server ():
140+ sock = sc .sticky .get (prefix + "_server_sock" )
117141 if sock :
118142 try :
119143 sock .close ()
120- except Exception :
144+ except :
121145 pass
122- sc .sticky [f'{ prefix } _server_sock' ] = None
123- sc .sticky [f'{ prefix } _server_started' ] = False
124- sc .sticky [f'{ prefix } _cloud_buffer_raw' ] = []
125- sc .sticky [f'{ prefix } _status_message' ] = 'Stopped'
126146
127- # Start or stop server based on inputs
128- if i_start and not sc .sticky [f'{ prefix } _prev_start' ]:
147+ sc .sticky [prefix + "_server_sock" ] = None
148+ sc .sticky [prefix + "_server_started" ] = False
149+ sc .sticky [prefix + "_is_receiving" ] = False
150+ sc .sticky [prefix + "_is_loading" ] = False
151+ sc .sticky [prefix + "_cloud_buffer_raw" ] = []
152+ sc .sticky [prefix + "_status_message" ] = "Stopped"
153+ request_expire (0 , True )
154+
155+ # ----------------------------
156+ # Async load (build Rhino PointCloud)
157+ # ----------------------------
158+ def build_pointcloud_async (raw_snapshot ):
159+ try :
160+ sc .sticky [prefix + "_is_loading" ] = True
161+ sc .sticky [prefix + "_load_started_at" ] = time .time ()
162+ sc .sticky [prefix + "_load_duration_s" ] = None
163+
164+ total = len (raw_snapshot )
165+ sc .sticky [prefix + "_load_progress" ] = (0 , total )
166+ sc .sticky [prefix + "_status_message" ] = "Loading {} pts..." .format (total )
167+ request_expire (0 , True )
168+
169+ pc = rg .PointCloud ()
170+
171+ step = 25000 if total > 25000 else 5000
172+ last_ui = time .time ()
173+
174+ for i , pt in enumerate (raw_snapshot ):
175+ x , y , z , r , g , b = pt
176+ pc .Add (rg .Point3d (x , y , z ), sd .Color .FromArgb (int (r ), int (g ), int (b )))
177+
178+ if (i + 1 ) % step == 0 :
179+ sc .sticky [prefix + "_load_progress" ] = (i + 1 , total )
180+ now = time .time ()
181+ if now - last_ui > 0.3 :
182+ request_expire (0 , True )
183+ last_ui = now
184+
185+ sc .sticky [prefix + "_latest_cloud" ] = pc
186+
187+ dur = time .time () - sc .sticky [prefix + "_load_started_at" ]
188+ sc .sticky [prefix + "_load_duration_s" ] = dur
189+ sc .sticky [prefix + "_load_progress" ] = (total , total )
190+ sc .sticky [prefix + "_status_message" ] = "Loaded {} pts in {:.2f}s" .format (pc .Count , dur )
191+
192+ # Force final recompute so output updates immediately
193+ request_expire (0 , True )
194+
195+ except Exception as e :
196+ sc .sticky [prefix + "_status_message" ] = "Load error: {}" .format (repr (e ))
197+ request_expire (0 , True )
198+ finally :
199+ sc .sticky [prefix + "_is_loading" ] = False
200+ request_expire (0 , True )
201+
202+ # ----------------------------
203+ # UI: start/stop/load button edges
204+ # ----------------------------
205+ if i_start and not sc .sticky [prefix + "_prev_start" ]:
129206 start_server ()
130- if i_stop and not sc .sticky [f'{ prefix } _prev_stop' ]:
207+
208+ if i_stop and not sc .sticky [prefix + "_prev_stop" ]:
131209 stop_server ()
132210
133- # Load buffered points into Rhino PointCloud
134- if i_load and not sc .sticky [f'{ prefix } _prev_load' ]:
135- if not sc .sticky .get (f'{ prefix } _server_started' , False ):
136- sc .sticky [f'{ prefix } _status_message' ] = "Start Server First!"
211+ if i_load and not sc .sticky [prefix + "_prev_load" ]:
212+ if not sc .sticky .get (prefix + "_server_started" , False ):
213+ sc .sticky [prefix + "_status_message" ] = "Start Server First!"
214+ elif sc .sticky .get (prefix + "_is_loading" , False ):
215+ sc .sticky [prefix + "_status_message" ] = "Already loading..."
137216 else :
138- raw = sc .sticky .get (f' { prefix } _cloud_buffer_raw' , [])
217+ raw = sc .sticky .get (prefix + " _cloud_buffer_raw" , [])
139218 if raw :
140- pc = rg .PointCloud ()
141- for x , y , z , r , g , b in raw :
142- pc .Add (rg .Point3d (x , y , z ), sd .Color .FromArgb (int (r ), int (g ), int (b )))
143- sc .sticky [f'{ prefix } _latest_cloud' ] = pc
144- sc .sticky [f'{ prefix } _status_message' ] = f'Loaded pcd with { pc .Count } pts'
219+ raw_snapshot = list (raw ) # snapshot
220+ threading .Thread (target = build_pointcloud_async , args = (raw_snapshot ,), daemon = True ).start ()
145221 else :
146- sc .sticky [f'{ prefix } _status_message' ] = 'No data buffered'
222+ sc .sticky [prefix + "_status_message" ] = "No data buffered"
223+ request_expire (0 , True )
224+
225+ # ----------------------------
226+ # Live status while receiving/loading
227+ # ----------------------------
228+ if sc .sticky .get (prefix + "_is_receiving" , False ):
229+ b = sc .sticky .get (prefix + "_recv_bytes" , 0 )
230+ sc .sticky [prefix + "_status_message" ] = "Receiving... {:.1f} MB" .format (b / (1024.0 * 1024.0 ))
231+ request_expire (300 , True )
232+
233+ if sc .sticky .get (prefix + "_is_loading" , False ):
234+ done , total = sc .sticky .get (prefix + "_load_progress" , (0 , 0 ))
235+ if total :
236+ sc .sticky [prefix + "_status_message" ] = "Loading... {}/{} pts" .format (done , total )
237+ request_expire (300 , True )
147238
148239 # Update previous states
149- sc .sticky [f'{ prefix } _prev_start' ] = i_start
150- sc .sticky [f'{ prefix } _prev_stop' ] = i_stop
151- sc .sticky [f'{ prefix } _prev_load' ] = i_load
152-
153- # Update UI and output
154- ghenv .Component .Message = sc .sticky [f'{ prefix } _status_message' ] # noqa: F821
240+ sc .sticky [prefix + "_prev_start" ] = i_start
241+ sc .sticky [prefix + "_prev_stop" ] = i_stop
242+ sc .sticky [prefix + "_prev_load" ] = i_load
155243
156- o_cloud = sc .sticky [f'{ prefix } _latest_cloud' ]
157- return [o_cloud ]
244+ # Output
245+ ghenv .Component .Message = sc .sticky [prefix + "_status_message" ] # noqa: F821
246+ return [sc .sticky [prefix + "_latest_cloud" ]]
0 commit comments