0d32f32db0
Замкнутый контур "поток -> CV -> механика": товары идут по конвейеру с шагом 700 мм, класс определяется стереопайплайном во время движения, пушер и плуг реагируют физически. Состав: * control_test/ - ячейка и CV. run_sorting_cv.py + cv_worker.py (два процесса, потому что torch внутри Isaac роняет сцену), cell.py (физика лент, плуга, пушера), measure_plane.py (замер габаритов), README.md и .memory.md с замерами, проблемами и ловушками * robozon_sorter/ - модули симуляции, scripts/ - утилиты, scene/ - сцены * assets/ - меши товаров, плуг, объекты Objaverse Бейзлайн CV: DEFOM-Stereo vitl, вход 480, iters 24, кроп зоны осмотра, без сегментации. На потоке 700 мм - классы 8/9, габариты MAE 32.8 мм, 469 мс на товар при такте 700 мс. Веса моделей (4.5 ГБ) и пропсы конвейера NVIDIA (274 МБ) не включены - источники и команды скачивания в MODELS.md. Выход прогонов (captures/, runtime/) не включён: воспроизводится. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
317 lines
12 KiB
Python
317 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Send Python code to a running Isaac Sim instance via the python_server TCP socket.
|
|
|
|
Usage:
|
|
# Inline code
|
|
python isaacsim_send.py 'print("hello")'
|
|
|
|
# From stdin (pipe or heredoc)
|
|
echo 'print("hello")' | python isaacsim_send.py
|
|
|
|
# Send a .py file
|
|
python isaacsim_send.py --file path/to/script.py
|
|
|
|
# Send a file with injected variables
|
|
python isaacsim_send.py --file script.py --arg output_path=/tmp/shot.png --arg width=1920
|
|
|
|
# Custom host/port/timeout
|
|
python isaacsim_send.py --host 127.0.0.1 --port 8226 --timeout 120 'print("hello")'
|
|
|
|
# Raw JSON output (default is formatted human-readable)
|
|
python isaacsim_send.py --raw 'print("hello")'
|
|
|
|
# --file scripts are isolated by default (wrapped in a function scope).
|
|
# Use --context for persistent named namespaces:
|
|
python isaacsim_send.py --context recording --file setup.py
|
|
python isaacsim_send.py --context recording 'print(my_var)'
|
|
|
|
# JSON envelope mode: named context, injected args, per-request timeout
|
|
python isaacsim_send.py --context my_session --execution-timeout 30 'x = 1'
|
|
python isaacsim_send.py --args-json '{"x": 42}' 'print(x)'
|
|
|
|
# Fire-and-forget: get immediate ACK and task_id
|
|
python isaacsim_send.py --fire-and-forget 'import time; time.sleep(5)'
|
|
|
|
# Introspection queries
|
|
python isaacsim_send.py --introspect status
|
|
python isaacsim_send.py --introspect contexts
|
|
python isaacsim_send.py --introspect tasks
|
|
|
|
Exit codes:
|
|
0 - Execution succeeded (status: "ok")
|
|
1 - Execution failed (status: "error") or connection error
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
|
|
|
|
async def send_and_receive(host: str, port: int, source: str, timeout: float = 60.0) -> dict:
|
|
"""Send Python source code or a JSON envelope and return the parsed JSON response."""
|
|
reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=timeout)
|
|
writer.write(source.encode())
|
|
writer.write_eof()
|
|
data = await asyncio.wait_for(reader.read(), timeout=timeout)
|
|
writer.close()
|
|
return json.loads(data.decode())
|
|
|
|
|
|
def _inject_args(source: str, args: list[str]) -> str:
|
|
"""Prepend variable assignments for --arg key=value pairs."""
|
|
if not args:
|
|
return source
|
|
lines = []
|
|
for arg in args:
|
|
key, _, value = arg.partition("=")
|
|
key = key.strip()
|
|
value = value.strip()
|
|
try:
|
|
parsed = eval(value, {"__builtins__": {}}) # noqa: S307
|
|
lines.append(f"{key} = {repr(parsed)}")
|
|
except Exception:
|
|
lines.append(f'{key} = "{value}"')
|
|
return "\n".join(lines) + "\n" + source
|
|
|
|
|
|
def _wrap_isolated(source: str, args: list[str]) -> str:
|
|
"""Wrap script in an async function scope to isolate from executor state.
|
|
|
|
The python_server executor shares global state between connections.
|
|
Wrapping in a function prevents variable leakage between calls.
|
|
Top-level `await` expressions are supported inside the wrapper.
|
|
"""
|
|
# Build argument assignments
|
|
arg_lines = []
|
|
for arg in args:
|
|
key, _, value = arg.partition("=")
|
|
key = key.strip()
|
|
value = value.strip()
|
|
try:
|
|
parsed = eval(value, {"__builtins__": {}}) # noqa: S307
|
|
arg_lines.append(f" {key} = {repr(parsed)}")
|
|
except Exception:
|
|
arg_lines.append(f' {key} = "{value}"')
|
|
|
|
# Indent the source
|
|
indented = "\n".join(" " + line for line in source.splitlines())
|
|
|
|
parts = ["async def _isolated_script():"]
|
|
if arg_lines:
|
|
parts.extend(arg_lines)
|
|
parts.append(indented)
|
|
parts.append("")
|
|
parts.append("await _isolated_script()")
|
|
|
|
return "\n".join(parts)
|
|
|
|
|
|
def _parse_args_kv(arg_list: list[str]) -> dict:
|
|
"""Convert a list of ``key=value`` strings to a dict with type inference."""
|
|
result = {}
|
|
for arg in arg_list:
|
|
key, _, value = arg.partition("=")
|
|
key = key.strip()
|
|
value = value.strip()
|
|
try:
|
|
result[key] = eval(value, {"__builtins__": {}}) # noqa: S307
|
|
except Exception:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _needs_envelope(args: argparse.Namespace) -> bool:
|
|
"""Return whether any flag requires the JSON envelope format."""
|
|
return bool(
|
|
args.json_envelope
|
|
or args.context
|
|
or args.fire_and_forget
|
|
or args.execution_timeout is not None
|
|
or args.args_json
|
|
or args.introspect
|
|
)
|
|
|
|
|
|
async def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Send Python code to Isaac Sim python_server")
|
|
parser.add_argument("code", nargs="?", help="Python code to execute (reads stdin if omitted)")
|
|
parser.add_argument("--file", "-f", help="Path to a Python file to send instead of inline code")
|
|
parser.add_argument(
|
|
"--arg", "-a", action="append", default=[], help="Inject key=value as a global variable (use with --file)"
|
|
)
|
|
parser.add_argument("--host", default="127.0.0.1", help="Server host (default: 127.0.0.1)")
|
|
parser.add_argument("--port", type=int, default=8226, help="Server port (default: 8226)")
|
|
parser.add_argument("--timeout", type=float, default=60.0, help="Client TCP timeout in seconds (default: 60)")
|
|
parser.add_argument("--raw", action="store_true", help="Print raw JSON instead of formatted output")
|
|
parser.add_argument(
|
|
"--no-isolate",
|
|
action="store_true",
|
|
help="Don't wrap --file scripts in isolated scope (use for state persistence)",
|
|
)
|
|
|
|
# JSON envelope options
|
|
envelope_group = parser.add_argument_group("JSON envelope options")
|
|
envelope_group.add_argument("--json-envelope", "-j", action="store_true", help="Force JSON envelope mode")
|
|
envelope_group.add_argument(
|
|
"--context", "-c", help="Named execution context (creates or reuses a persistent namespace)"
|
|
)
|
|
envelope_group.add_argument(
|
|
"--fire-and-forget",
|
|
"--ff",
|
|
action="store_true",
|
|
dest="fire_and_forget",
|
|
help="Fire-and-forget mode: receive immediate ACK with task_id, code runs in background",
|
|
)
|
|
envelope_group.add_argument(
|
|
"--execution-timeout",
|
|
"-t",
|
|
type=float,
|
|
metavar="SECONDS",
|
|
dest="execution_timeout",
|
|
help="Per-request server-side execution timeout (0 = no limit; different from --timeout)",
|
|
)
|
|
envelope_group.add_argument(
|
|
"--args-json",
|
|
metavar="JSON",
|
|
help="Inject variables as a JSON object string, e.g. '{\"x\": 42}'",
|
|
)
|
|
envelope_group.add_argument(
|
|
"--introspect",
|
|
metavar="COMMAND",
|
|
help=(
|
|
"Run an introspection query instead of executing code. "
|
|
"Commands: status, contexts, context, tasks, task, delete_context"
|
|
),
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Handle introspection queries
|
|
if args.introspect:
|
|
envelope: dict = {"introspect": args.introspect}
|
|
if args.context:
|
|
envelope["context"] = args.context
|
|
if args.code:
|
|
# Allow passing a task_id as a positional argument for 'task' queries
|
|
envelope["task_id"] = args.code
|
|
source = json.dumps(envelope)
|
|
try:
|
|
result = await send_and_receive(args.host, args.port, source, args.timeout)
|
|
except ConnectionRefusedError:
|
|
print(f"Error: Cannot connect to Isaac Sim at {args.host}:{args.port}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except asyncio.TimeoutError:
|
|
print(f"Error: Timeout after {args.timeout}s waiting for response", file=sys.stderr)
|
|
sys.exit(1)
|
|
if args.raw:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
if result.get("status") == "ok":
|
|
print(json.dumps(result.get("result"), indent=2))
|
|
else:
|
|
print(f"ERROR: {result.get('result', result)}", file=sys.stderr)
|
|
sys.exit(1)
|
|
return
|
|
|
|
# Build code string
|
|
if args.file:
|
|
with open(args.file) as f:
|
|
source_code = f.read()
|
|
if args.no_isolate or _needs_envelope(args):
|
|
source_code = _inject_args(source_code, args.arg)
|
|
else:
|
|
source_code = _wrap_isolated(source_code, args.arg)
|
|
elif args.code:
|
|
source_code = args.code
|
|
if not _needs_envelope(args):
|
|
source_code = _inject_args(source_code, args.arg)
|
|
else:
|
|
source_code = sys.stdin.read()
|
|
|
|
if not source_code.strip():
|
|
print("Error: No code provided", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Build the final request: JSON envelope or raw Python
|
|
if _needs_envelope(args):
|
|
envelope = {"code": source_code}
|
|
|
|
if args.context:
|
|
envelope["context"] = args.context
|
|
if args.fire_and_forget:
|
|
envelope["fire_and_forget"] = True
|
|
if args.execution_timeout is not None:
|
|
envelope["timeout"] = args.execution_timeout
|
|
|
|
# Merge args from --arg key=value pairs and --args-json
|
|
merged_args: dict = _parse_args_kv(args.arg)
|
|
if args.args_json:
|
|
try:
|
|
json_args = json.loads(args.args_json)
|
|
if isinstance(json_args, dict):
|
|
merged_args.update(json_args)
|
|
else:
|
|
print("Error: --args-json must be a JSON object", file=sys.stderr)
|
|
sys.exit(1)
|
|
except json.JSONDecodeError as exc:
|
|
print(f"Error: Invalid --args-json: {exc}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if merged_args:
|
|
envelope["args"] = merged_args
|
|
|
|
source = json.dumps(envelope)
|
|
else:
|
|
source = source_code
|
|
|
|
try:
|
|
result = await send_and_receive(args.host, args.port, source, args.timeout)
|
|
except ConnectionRefusedError:
|
|
print(f"Error: Cannot connect to Isaac Sim at {args.host}:{args.port}", file=sys.stderr)
|
|
print("Make sure Isaac Sim is running with isaacsim.code_editor.python_server enabled.", file=sys.stderr)
|
|
sys.exit(1)
|
|
except asyncio.TimeoutError:
|
|
print(f"Error: Timeout after {args.timeout}s waiting for response", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if args.raw:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
# Fire-and-forget: show task_id prominently
|
|
if result.get("fire_and_forget"):
|
|
print(f"Task submitted. task_id: {result.get('task_id')}")
|
|
sys.exit(0)
|
|
|
|
output = result.get("output", "")
|
|
if output:
|
|
print(output)
|
|
if "result" in result and result["result"] is not None:
|
|
print(f"=> {result['result']}")
|
|
if result.get("status") == "error":
|
|
print(f"\nERROR [{result.get('ename', '?')}]: {result.get('evalue', '?')}", file=sys.stderr)
|
|
for tb in result.get("traceback", []):
|
|
print(tb, file=sys.stderr)
|
|
|
|
if result.get("elapsed_seconds") is not None:
|
|
print(f"(elapsed: {result['elapsed_seconds']:.2f}s)", file=sys.stderr)
|
|
|
|
sys.exit(0 if result.get("status") == "ok" else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|