-
Notifications
You must be signed in to change notification settings - Fork 3
receivers for http/tcp/web socket communication #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
eleniv3d
wants to merge
18
commits into
release/2.0.0
Choose a base branch
from
IO
base: release/2.0.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+956
−125
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
136e3dc
ADD Http Listener
eleniv3d 38aab63
ADD TCP Listener
eleniv3d 2fd80df
FIX lintering
eleniv3d aeecaa2
FIX tcp component + add example file sender
eleniv3d 5559ade
Merge pull request #148 from diffCheckOrg/patch_ws
eleniv3d 5cacef1
Merge branch 'main' into IO
eleniv3d 4aafbf8
WIP FIX WS Receiver
eleniv3d e09fb79
FIX websocket component
eleniv3d 46c6d6f
FIX light refactoring and input reorder
eleniv3d c546fec
ADD drop btns and panel with standard inputs similar to vizualization…
eleniv3d 8509138
FIX rename python file
eleniv3d 2588656
FIX remove repeated ws from stickys
eleniv3d 683b4e8
ADD warning if trying to load data from tcp/ws listener without start…
eleniv3d 27cad32
ADD sleep to prevent CPU spin
eleniv3d bd004f7
ADD doc strings
eleniv3d b383bc1
FIX add docstrings
eleniv3d 7f11249
FIX missing lines from PKG-INFO
eleniv3d 484810f
FIX RML.Warning
eleniv3d File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule eigen
updated
from 11fd34 to 81044e
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
#! python3 | ||
|
||
from ghpythonlib.componentbase import executingcomponent as component | ||
import os | ||
import tempfile | ||
import requests | ||
import threading | ||
import Rhino | ||
import Rhino.Geometry as rg | ||
import scriptcontext as sc | ||
from diffCheck import df_gh_canvas_utils | ||
|
||
|
||
class DFHTTPListener(component): | ||
|
||
def __init__(self): | ||
try: | ||
ghenv.Component.ExpireSolution(True) # noqa: F821 | ||
ghenv.Component.Attributes.PerformLayout() # noqa: F821 | ||
except NameError: | ||
pass | ||
|
||
df_gh_canvas_utils.add_button(ghenv.Component, "Load", 0, x_offset=60) # noqa: F821 | ||
df_gh_canvas_utils.add_panel(ghenv.Component, "Ply_url", "https://github.com/diffCheckOrg/diffCheck/raw/refs/heads/main/tests/test_data/cube_mesh.ply", 1, 60, 20) # noqa: F821 | ||
|
||
def RunScript(self, | ||
i_load: bool, | ||
i_ply_url: str): | ||
|
||
prefix = 'http' | ||
|
||
# initialize sticky variables | ||
sc.sticky.setdefault(f'{prefix}_ply_url', None) # last url processed | ||
sc.sticky.setdefault(f'{prefix}_imported_geom', None) # last geo imported from ply | ||
sc.sticky.setdefault(f'{prefix}_status_message', "Waiting..") # status message on component | ||
sc.sticky.setdefault(f'{prefix}_prev_load', False) # previous state of toggle | ||
sc.sticky.setdefault(f'{prefix}_thread_running', False) # is a background thread running? | ||
|
||
def _import_job(url: str) -> None: | ||
|
||
""" | ||
Downloads and imports a .ply file from a given URL in a background thread. | ||
Background job: | ||
- Downloads the .ply file from the URL | ||
- Imports it into the active Rhino document | ||
- Extracts the new geometry (point cloud or mesh) | ||
- Cleans up the temporary file and document objects | ||
- Updates sticky state and status message | ||
- Signals to GH that it should re-solve | ||
|
||
:param url: A string representing a direct URL to a .ply file (e.g. from GitHub or local server). | ||
The file must end with ".ply". | ||
:returns: None | ||
""" | ||
|
||
tmp = None | ||
try: | ||
if not url.lower().endswith('.ply'): | ||
raise ValueError("URL must end in .ply") | ||
|
||
resp = requests.get(url, timeout=30) | ||
resp.raise_for_status() | ||
# save om temporary file | ||
fn = os.path.basename(url) | ||
tmp = os.path.join(tempfile.gettempdir(), fn) | ||
with open(tmp, 'wb') as f: | ||
f.write(resp.content) | ||
|
||
doc = Rhino.RhinoDoc.ActiveDoc | ||
# recordd existing object IDs to detect new ones | ||
before_ids = {o.Id for o in doc.Objects} | ||
|
||
# import PLY using Rhino's API | ||
opts = Rhino.FileIO.FilePlyReadOptions() | ||
ok = Rhino.FileIO.FilePly.Read(tmp, doc, opts) | ||
if not ok: | ||
raise RuntimeError("Rhino.FilePly.Read failed") | ||
|
||
after_ids = {o.Id for o in doc.Objects} | ||
new_ids = after_ids - before_ids | ||
# get new pcd or mesh from document | ||
geom = None | ||
for guid in new_ids: | ||
g = doc.Objects.FindId(guid).Geometry | ||
if isinstance(g, rg.PointCloud): | ||
geom = g.Duplicate() | ||
break | ||
elif isinstance(g, rg.Mesh): | ||
geom = g.DuplicateMesh() | ||
break | ||
# remove imported objects | ||
for guid in new_ids: | ||
doc.Objects.Delete(guid, True) | ||
doc.Views.Redraw() | ||
|
||
# store new geometry | ||
sc.sticky[f'{prefix}_imported_geom'] = geom | ||
count = geom.Count if isinstance(geom, rg.PointCloud) else geom.Vertices.Count | ||
if isinstance(geom, rg.PointCloud): | ||
sc.sticky[f'{prefix}_status_message'] = f"Loaded pcd with {count} pts" | ||
else: | ||
sc.sticky[f'{prefix}_status_message'] = f"Loaded mesh wih {count} vertices" | ||
ghenv.Component.Message = sc.sticky.get(f'{prefix}_status_message') # noqa: F821 | ||
|
||
except Exception as e: | ||
sc.sticky[f'{prefix}_imported_geom'] = None | ||
sc.sticky[f'{prefix}_status_message'] = f"Error: {e}" | ||
finally: | ||
try: | ||
os.remove(tmp) | ||
except Exception: | ||
pass | ||
# mark thread as finished | ||
sc.sticky[f'{prefix}_thread_running'] = False | ||
ghenv.Component.ExpireSolution(True) # noqa: F821 | ||
|
||
# check if the URL input has changed | ||
if sc.sticky[f'{prefix}_ply_url'] != i_ply_url: | ||
sc.sticky[f'{prefix}_ply_url'] = i_ply_url | ||
sc.sticky[f'{prefix}_status_message'] = "URL changed. Press Load" | ||
sc.sticky[f'{prefix}_thread_running'] = False | ||
sc.sticky[f'{prefix}_prev_load'] = False | ||
|
||
# start importing if Load toggle is pressed and import thread is not already running | ||
if i_load and not sc.sticky[f'{prefix}_prev_load'] and not sc.sticky[f'{prefix}_thread_running']: | ||
sc.sticky[f'{prefix}_status_message'] = "Loading..." | ||
sc.sticky[f'{prefix}_thread_running'] = True | ||
threading.Thread(target=_import_job, args=(i_ply_url,), daemon=True).start() | ||
|
||
sc.sticky[f'{prefix}_prev_load'] = i_load | ||
ghenv.Component.Message = sc.sticky.get(f'{prefix}_status_message', "") # noqa: F821 | ||
|
||
# output | ||
o_geometry = sc.sticky.get(f'{prefix}_imported_geom') | ||
|
||
return [o_geometry] |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
{ | ||
"name": "DFHTTPListener", | ||
"nickname": "HTTPIn", | ||
"category": "diffCheck", | ||
"subcategory": "IO", | ||
"description": "This component reads a ply file from the internet.", | ||
"exposure": 4, | ||
"instanceGuid": "ca4b5c94-6c85-4bc5-87f0-132cc34c4536", | ||
"ghpython": { | ||
"hideOutput": true, | ||
"hideInput": true, | ||
"isAdvancedMode": true, | ||
"marshalOutGuids": true, | ||
"iconDisplay": 2, | ||
"inputParameters": [ | ||
{ | ||
"name": "i_load", | ||
"nickname": "i_load", | ||
"description": "Button to import ply from url.", | ||
"optional": true, | ||
"allowTreeAccess": true, | ||
"showTypeHints": true, | ||
"scriptParamAccess": "item", | ||
"wireDisplay": "default", | ||
"sourceCount": 0, | ||
"typeHintID": "bool" | ||
}, | ||
{ | ||
"name": "i_ply_url", | ||
"nickname": "i_ply_url", | ||
"description": "The url where to get the pointcloud", | ||
"optional": true, | ||
"allowTreeAccess": true, | ||
"showTypeHints": true, | ||
"scriptParamAccess": "item", | ||
"wireDisplay": "default", | ||
"sourceCount": 0, | ||
"typeHintID": "str" | ||
} | ||
], | ||
"outputParameters": [ | ||
{ | ||
"name": "o_geometry", | ||
"nickname": "o_geo", | ||
"description": "The mesh or pcd that was imported.", | ||
"optional": false, | ||
"sourceCount": 0, | ||
"graft": false | ||
} | ||
] | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
#! python3 | ||
|
||
from ghpythonlib.componentbase import executingcomponent as component | ||
import socket | ||
import threading | ||
import json | ||
import time | ||
import scriptcontext as sc | ||
import Rhino.Geometry as rg | ||
import System.Drawing as sd | ||
from diffCheck import df_gh_canvas_utils | ||
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML | ||
|
||
class DFTCPListener(component): | ||
def __init__(self): | ||
try: | ||
ghenv.Component.ExpireSolution(True) # noqa: F821 | ||
ghenv.Component.Attributes.PerformLayout() # noqa: F821 | ||
except NameError: | ||
pass | ||
|
||
for idx, label in enumerate(("Start", "Stop", "Load")): | ||
df_gh_canvas_utils.add_button( | ||
ghenv.Component, label, idx, x_offset=60) # noqa: F821 | ||
df_gh_canvas_utils.add_panel(ghenv.Component, "Host", "127.0.0.1", 3, 60, 20) # noqa: F821 | ||
df_gh_canvas_utils.add_panel(ghenv.Component, "Port", "5000", 4, 60, 20) # noqa: F821 | ||
|
||
def RunScript(self, | ||
i_start: bool, | ||
i_stop: bool, | ||
i_load: bool, | ||
i_host: str, | ||
i_port: int): | ||
|
||
prefix = 'tcp' | ||
|
||
# Sticky initialization | ||
sc.sticky.setdefault(f'{prefix}_server_sock', None) | ||
sc.sticky.setdefault(f'{prefix}_server_started', False) | ||
sc.sticky.setdefault(f'{prefix}_cloud_buffer_raw', []) | ||
sc.sticky.setdefault(f'{prefix}_latest_cloud', None) | ||
sc.sticky.setdefault(f'{prefix}_status_message', 'Waiting..') | ||
sc.sticky.setdefault(f'{prefix}_prev_start', False) | ||
sc.sticky.setdefault(f'{prefix}_prev_stop', False) | ||
sc.sticky.setdefault(f'{prefix}_prev_load', False) | ||
|
||
# Client handler | ||
def handle_client(conn: socket.socket) -> None: | ||
""" | ||
Reads the incoming bytes from a single TCP client socket and stores valid data in a shared buffer. | ||
|
||
:param conn: A socket object returned by `accept()` representing a live client connection. | ||
The client is expected to send newline-delimited JSON-encoded data, where each | ||
message is a list of 6D values: [x, y, z, r, g, b]. | ||
|
||
:returns: None | ||
""" | ||
buf = b'' | ||
with conn: | ||
while sc.sticky.get(f'{prefix}_server_started', False): | ||
try: | ||
chunk = conn.recv(4096) | ||
if not chunk: | ||
break | ||
buf += chunk | ||
while b'\n' in buf: | ||
line, buf = buf.split(b'\n', 1) | ||
try: | ||
raw = json.loads(line.decode()) | ||
except Exception: | ||
continue | ||
if isinstance(raw, list) and all(isinstance(pt, list) and len(pt) == 6 for pt in raw): | ||
sc.sticky[f'{prefix}_cloud_buffer_raw'] = raw | ||
except Exception: | ||
break | ||
time.sleep(0.05) # sleep briefly to prevent CPU spin | ||
|
||
# thread to accept incoming connections | ||
def server_loop(sock: socket.socket) -> None: | ||
""" | ||
Accepts a single client connection and starts a background thread to handle it. | ||
|
||
:param sock: A bound and listening TCP socket created by start_server(). | ||
This socket will accept one incoming connection, then delegate it to handle_client(). | ||
|
||
:returns: None. This runs as a background thread and blocks on accept(). | ||
""" | ||
try: | ||
conn, _ = sock.accept() | ||
handle_client(conn) | ||
except Exception: | ||
pass | ||
|
||
# Start TCP server | ||
def start_server() -> None: | ||
""" | ||
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 | ||
|
||
:returns: None. | ||
""" | ||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
sock.bind((i_host, i_port)) | ||
sock.listen(1) | ||
sc.sticky[f'{prefix}_server_sock'] = sock | ||
sc.sticky[f'{prefix}_server_started'] = True | ||
sc.sticky[f'{prefix}_status_message'] = f'Listening on {i_host}:{i_port}' | ||
# Only accept one connection to keep it long-lived | ||
threading.Thread(target=server_loop, args=(sock,), daemon=True).start() | ||
|
||
def stop_server() -> None: | ||
""" | ||
Stops the running TCP server by closing the listening socket and resetting internal state. | ||
|
||
:returns: None. | ||
""" | ||
sock = sc.sticky.get(f'{prefix}_server_sock') | ||
if sock: | ||
try: | ||
sock.close() | ||
except Exception: | ||
pass | ||
sc.sticky[f'{prefix}_server_sock'] = None | ||
sc.sticky[f'{prefix}_server_started'] = False | ||
sc.sticky[f'{prefix}_cloud_buffer_raw'] = [] | ||
sc.sticky[f'{prefix}_status_message'] = 'Stopped' | ||
|
||
# Start or stop server based on inputs | ||
if i_start and not sc.sticky[f'{prefix}_prev_start']: | ||
start_server() | ||
eleniv3d marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if i_stop and not sc.sticky[f'{prefix}_prev_stop']: | ||
stop_server() | ||
|
||
# Load buffered points into Rhino PointCloud | ||
if i_load and not sc.sticky[f'{prefix}_prev_load']: | ||
if not sc.sticky.get(f'{prefix}_server_started', False): | ||
self.AddRuntimeMessage(RML.Warning, | ||
"Please start server here before trying to send data from remote device.") | ||
sc.sticky[f'{prefix}_status_message'] = "Server not started" | ||
else: | ||
raw = sc.sticky.get(f'{prefix}_cloud_buffer_raw', []) | ||
if raw: | ||
pc = rg.PointCloud() | ||
for x, y, z, r, g, b in raw: | ||
pc.Add(rg.Point3d(x, y, z), sd.Color.FromArgb(int(r), int(g), int(b))) | ||
sc.sticky[f'{prefix}_latest_cloud'] = pc | ||
sc.sticky[f'{prefix}_status_message'] = f'Loaded pcd with {pc.Count} pts' | ||
else: | ||
sc.sticky[f'{prefix}_status_message'] = 'No data buffered' | ||
|
||
# Update previous states | ||
sc.sticky[f'{prefix}_prev_start'] = i_start | ||
sc.sticky[f'{prefix}_prev_stop'] = i_stop | ||
sc.sticky[f'{prefix}_prev_load'] = i_load | ||
|
||
# Update UI and output | ||
ghenv.Component.Message = sc.sticky[f'{prefix}_status_message'] # noqa: F821 | ||
|
||
o_cloud = sc.sticky[f'{prefix}_latest_cloud'] | ||
return [o_cloud] |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.