Сортировочная ячейка Isaac Sim: CV-пайплайн и меши товаров
Замкнутый контур "поток -> 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>
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user