Files
kyleandClaude Opus 4.8 54a01625ce Initial commit: CloudView generic networked point-cloud viewer
Standalone Open3D viewer that listens on a TCP/Unix socket and renders NDJSON point-cloud streams from any producer. Decoupled server/protocol/store layers (no Open3D dependency, testable headless) plus a lazy Open3D render loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:09:11 -07:00

114 lines
4.0 KiB
Python

"""Stream-socket server that feeds a CloudStore from NDJSON producers.
Listens on one or more addresses (TCP and/or Unix), accepts any number of
producers concurrently, and applies their messages to a shared store. All
networking is on background threads; the render loop reads the store separately.
"""
from __future__ import annotations
import logging
import os
import socket
import threading
from .protocol import LineDecoder, parse_address
from .store import CloudStore
logger = logging.getLogger(__name__)
class CloudServer:
def __init__(self, store: CloudStore, addresses: list[str]) -> None:
self._store = store
self._addresses = addresses
self._listeners: list[socket.socket] = []
self._unix_paths: list[str] = []
self._stop = threading.Event()
# Actual bound endpoints (resolves ephemeral ":0" ports for callers/tests).
self.bound: list[tuple[str, str, int | None]] = []
def start(self) -> None:
for addr in self._addresses:
kind, a, b = parse_address(addr)
if kind == "tcp":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((a, b))
host, port = sock.getsockname()[:2]
self.bound.append(("tcp", host, port))
else:
# Replace a stale socket file from a prior run.
if os.path.exists(a):
os.unlink(a)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(a)
self._unix_paths.append(a)
self.bound.append(("unix", a, None))
sock.listen(8)
sock.settimeout(0.5)
self._listeners.append(sock)
threading.Thread(target=self._accept_loop, args=(sock, addr),
name=f"accept[{addr}]", daemon=True).start()
logger.info("listening on %s", addr)
def _accept_loop(self, listener: socket.socket, addr: str) -> None:
while not self._stop.is_set():
try:
conn, _ = listener.accept()
except socket.timeout:
continue
except OSError:
break
threading.Thread(target=self._serve_client, args=(conn, addr),
name=f"client[{addr}]", daemon=True).start()
def _serve_client(self, conn: socket.socket, addr: str) -> None:
logger.info("producer connected on %s", addr)
conn.settimeout(1.0)
decoder = LineDecoder()
try:
while not self._stop.is_set():
try:
data = conn.recv(65536)
except socket.timeout:
continue
if not data:
break
for msg in decoder.feed(data):
self._apply(msg)
except Exception:
logger.exception("error serving producer on %s", addr)
finally:
conn.close()
logger.info("producer disconnected (%s)", addr)
def _apply(self, msg: dict) -> None:
kind = msg.get("type")
source = msg.get("source", "default")
if kind == "points":
self._store.add_points(source, msg.get("points", []),
msg.get("color"))
elif kind == "hello":
self._store.hello(source, msg.get("name"))
elif kind == "clear":
self._store.clear(source)
elif kind == "pose":
self._store.set_pose(source, msg.get("position", [0, 0, 0]),
msg.get("yaw"))
else:
logger.debug("ignoring unknown message type %r", kind)
def stop(self) -> None:
self._stop.set()
for sock in self._listeners:
try:
sock.close()
except OSError:
pass
for path in self._unix_paths:
try:
os.unlink(path)
except OSError:
pass