armnet-runtime 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- armnet_runtime/__init__.py +66 -0
- armnet_runtime/_rerun_pb2.py +32 -0
- armnet_runtime/cli.py +217 -0
- armnet_runtime/context.py +974 -0
- armnet_runtime/decorator.py +71 -0
- armnet_runtime/env.py +51 -0
- armnet_runtime/lerobot/__init__.py +8 -0
- armnet_runtime/lerobot/_remote_protocol.py +261 -0
- armnet_runtime/lerobot/import_swap.py +85 -0
- armnet_runtime/lerobot/remote_arx5.py +147 -0
- armnet_runtime/lerobot/remote_camera.py +145 -0
- armnet_runtime/lerobot/remote_feetech.py +160 -0
- armnet_runtime/markers.py +51 -0
- armnet_runtime/rerun.py +461 -0
- armnet_runtime-0.1.0.dist-info/METADATA +53 -0
- armnet_runtime-0.1.0.dist-info/RECORD +18 -0
- armnet_runtime-0.1.0.dist-info/WHEEL +4 -0
- armnet_runtime-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""armnet runtime SDK \u2014 used inside customer containers on a cell.
|
|
2
|
+
|
|
3
|
+
Customer-facing surface: a ``@main`` decorator and a :class:`Context`. The
|
|
4
|
+
container's entrypoint is the ``armnet-runtime`` console script
|
|
5
|
+
(installed by this package); it loads the user's file, finds the
|
|
6
|
+
``@main``-decorated function, builds the context, and calls it.
|
|
7
|
+
|
|
8
|
+
Typical use::
|
|
9
|
+
|
|
10
|
+
from armnet_runtime import main, Context
|
|
11
|
+
|
|
12
|
+
@main
|
|
13
|
+
def run(ctx: Context):
|
|
14
|
+
seed = ctx.args.get("seed", 0)
|
|
15
|
+
ctx.report_progress("starting")
|
|
16
|
+
...
|
|
17
|
+
return {"success_rate": 1.0}
|
|
18
|
+
|
|
19
|
+
Wire types (``Embodiment``, ``Task``, ``JobSpec``, ``JobResult``, ...) are
|
|
20
|
+
re-exported from :mod:`armnet_core` for ergonomics, so customer code
|
|
21
|
+
never needs to know about the core package directly.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from armnet_core import (
|
|
25
|
+
Embodiment,
|
|
26
|
+
Job,
|
|
27
|
+
JobResult,
|
|
28
|
+
JobSpec,
|
|
29
|
+
JobStatus,
|
|
30
|
+
RemoteExecutionError,
|
|
31
|
+
Task,
|
|
32
|
+
TerminalStatus,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from armnet_runtime.context import (
|
|
36
|
+
BIMANUAL_SO101_EMBODIMENT,
|
|
37
|
+
SO101_EMBODIMENT,
|
|
38
|
+
Cell,
|
|
39
|
+
CompletionStatus,
|
|
40
|
+
Context,
|
|
41
|
+
ResetTimeoutException,
|
|
42
|
+
require_so101_embodiment,
|
|
43
|
+
)
|
|
44
|
+
from armnet_runtime.decorator import MainRegistrationError, main
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"BIMANUAL_SO101_EMBODIMENT",
|
|
48
|
+
"SO101_EMBODIMENT",
|
|
49
|
+
"Cell",
|
|
50
|
+
"CompletionStatus",
|
|
51
|
+
"Context",
|
|
52
|
+
"require_so101_embodiment",
|
|
53
|
+
"Embodiment",
|
|
54
|
+
"Job",
|
|
55
|
+
"JobResult",
|
|
56
|
+
"JobSpec",
|
|
57
|
+
"JobStatus",
|
|
58
|
+
"MainRegistrationError",
|
|
59
|
+
"RemoteExecutionError",
|
|
60
|
+
"ResetTimeoutException",
|
|
61
|
+
"Task",
|
|
62
|
+
"TerminalStatus",
|
|
63
|
+
"main",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: rerun.proto
|
|
4
|
+
# Protobuf Python Version: 5.26.1
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0brerun.proto\x12\x10remoterobo.rerun\"y\n\x05Image\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x31\n\x08\x65ncoding\x18\x02 \x01(\x0e\x32\x1f.remoterobo.rerun.ImageEncoding\x12\r\n\x05width\x18\x03 \x01(\r\x12\x0e\n\x06height\x18\x04 \x01(\r\x12\x10\n\x08\x63hannels\x18\x05 \x01(\r\"f\n\nRerunEntry\x12\x13\n\x0b\x65ntity_path\x18\x01 \x01(\t\x12\x10\n\x06scalar\x18\x02 \x01(\x01H\x00\x12(\n\x05image\x18\x03 \x01(\x0b\x32\x17.remoterobo.rerun.ImageH\x00\x42\x07\n\x05value\"\x82\x01\n\x0bRerunPacket\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x01\x12-\n\x07\x65ntries\x18\x03 \x03(\x0b\x32\x1c.remoterobo.rerun.RerunEntry\x12\x0e\n\x06obs_ts\x18\x04 \x01(\x01\x12\x11\n\tsubmit_ts\x18\x05 \x01(\x01*d\n\rImageEncoding\x12\x1e\n\x1aIMAGE_ENCODING_UNSPECIFIED\x10\x00\x12\x17\n\x13IMAGE_ENCODING_JPEG\x10\x01\x12\x1a\n\x16IMAGE_ENCODING_RAW_RGB\x10\x02\x62\x06proto3')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rerun_pb2', _globals)
|
|
22
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
23
|
+
DESCRIPTOR._loaded_options = None
|
|
24
|
+
_globals['_IMAGEENCODING']._serialized_start=393
|
|
25
|
+
_globals['_IMAGEENCODING']._serialized_end=493
|
|
26
|
+
_globals['_IMAGE']._serialized_start=33
|
|
27
|
+
_globals['_IMAGE']._serialized_end=154
|
|
28
|
+
_globals['_RERUNENTRY']._serialized_start=156
|
|
29
|
+
_globals['_RERUNENTRY']._serialized_end=258
|
|
30
|
+
_globals['_RERUNPACKET']._serialized_start=261
|
|
31
|
+
_globals['_RERUNPACKET']._serialized_end=391
|
|
32
|
+
# @@protoc_insertion_point(module_scope)
|
armnet_runtime/cli.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""``armnet-runtime`` console script.
|
|
2
|
+
|
|
3
|
+
This is the container entrypoint for any image built on top of the
|
|
4
|
+
armnet runtime SDK. The container's ``CMD`` looks like::
|
|
5
|
+
|
|
6
|
+
CMD ["armnet-runtime", "/app/hello.py"]
|
|
7
|
+
|
|
8
|
+
and this script:
|
|
9
|
+
|
|
10
|
+
1. (M3+) Performs the LeRobot import-system swap so customer code
|
|
11
|
+
transparently routes hardware operations through the safety-aware
|
|
12
|
+
robot connector. Currently a marker; nothing is swapped yet.
|
|
13
|
+
2. Adds the user script's directory to ``sys.path`` so it can import
|
|
14
|
+
sibling modules.
|
|
15
|
+
3. Imports the user script \u2014 which executes the ``@main`` decorator as
|
|
16
|
+
a side effect, registering the entry-point function.
|
|
17
|
+
4. Builds a :class:`~armnet_runtime.Context` from cell-injected env
|
|
18
|
+
vars.
|
|
19
|
+
5. Calls the registered function with the context.
|
|
20
|
+
6. Prints the function's return value with the
|
|
21
|
+
:data:`~armnet_runtime.markers.RESULT_MARKER_JSON` prefix; the
|
|
22
|
+
cell extracts that and surfaces it as
|
|
23
|
+
:attr:`~armnet_core.JobResult.return_value`.
|
|
24
|
+
7. Maps exceptions to a non-zero exit so the cell records the job as
|
|
25
|
+
``FAILED``; the traceback ends up in the captured stderr.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import importlib.util
|
|
32
|
+
import json
|
|
33
|
+
import sys
|
|
34
|
+
import traceback
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import TextIO
|
|
38
|
+
from typing import Sequence
|
|
39
|
+
|
|
40
|
+
from armnet_runtime.context import build_context
|
|
41
|
+
from armnet_runtime.decorator import registered_main
|
|
42
|
+
from armnet_runtime.markers import (
|
|
43
|
+
MAX_TRACEBACK_BYTES,
|
|
44
|
+
RESULT_MARKER,
|
|
45
|
+
RESULT_MARKER_JSON,
|
|
46
|
+
TRACEBACK_MARKER_JSON,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Exit codes. Keep stable \u2014 the cell may eventually distinguish
|
|
50
|
+
# runtime-CLI failures from user-code failures, e.g. by mapping these to
|
|
51
|
+
# JobResult.error.
|
|
52
|
+
EXIT_OK = 0
|
|
53
|
+
EXIT_USER_RAISED = 1
|
|
54
|
+
EXIT_USAGE = 2
|
|
55
|
+
EXIT_NO_MAIN = 3
|
|
56
|
+
EXIT_USER_IMPORT_FAILED = 4
|
|
57
|
+
EXIT_BAD_CONTEXT = 5
|
|
58
|
+
|
|
59
|
+
_USER_MODULE_NAME = "__armnet_user_main__"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _TimestampedStream:
|
|
63
|
+
"""Prefix every stdout/stderr line with a Unix timestamp."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, wrapped: TextIO) -> None:
|
|
66
|
+
self._wrapped = wrapped
|
|
67
|
+
self._at_line_start = True
|
|
68
|
+
|
|
69
|
+
def write(self, text: str) -> int:
|
|
70
|
+
written = 0
|
|
71
|
+
for chunk in text.splitlines(keepends=True):
|
|
72
|
+
if self._at_line_start and chunk:
|
|
73
|
+
prefix = f"[{datetime.now().isoformat(timespec='milliseconds')}] "
|
|
74
|
+
self._wrapped.write(prefix)
|
|
75
|
+
self._wrapped.write(chunk)
|
|
76
|
+
written += len(chunk)
|
|
77
|
+
self._at_line_start = chunk.endswith(("\n", "\r"))
|
|
78
|
+
return written
|
|
79
|
+
|
|
80
|
+
def flush(self) -> None:
|
|
81
|
+
self._wrapped.flush()
|
|
82
|
+
|
|
83
|
+
def isatty(self) -> bool:
|
|
84
|
+
return self._wrapped.isatty()
|
|
85
|
+
|
|
86
|
+
def __getattr__(self, name: str) -> object:
|
|
87
|
+
return getattr(self._wrapped, name)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _install_timestamped_streams() -> None:
|
|
91
|
+
sys.stdout = _TimestampedStream(sys.stdout) # type: ignore[assignment]
|
|
92
|
+
sys.stderr = _TimestampedStream(sys.stderr) # type: ignore[assignment]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
|
|
96
|
+
parser = argparse.ArgumentParser(
|
|
97
|
+
prog="armnet-runtime",
|
|
98
|
+
description="Run a armnet @main-decorated script inside a cell container.",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"script",
|
|
102
|
+
type=Path,
|
|
103
|
+
help="Path to the customer's Python script containing one @main-decorated function.",
|
|
104
|
+
)
|
|
105
|
+
return parser.parse_args(argv)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _load_user_script(script_path: Path) -> None:
|
|
109
|
+
"""Import the user's script; the @main side-effect populates the registry."""
|
|
110
|
+
|
|
111
|
+
if not script_path.is_file():
|
|
112
|
+
raise FileNotFoundError(f"script not found: {script_path}")
|
|
113
|
+
|
|
114
|
+
sys.path.insert(0, str(script_path.parent))
|
|
115
|
+
|
|
116
|
+
spec = importlib.util.spec_from_file_location(_USER_MODULE_NAME, script_path)
|
|
117
|
+
if spec is None or spec.loader is None:
|
|
118
|
+
raise ImportError(f"could not build import spec for {script_path}")
|
|
119
|
+
module = importlib.util.module_from_spec(spec)
|
|
120
|
+
sys.modules[_USER_MODULE_NAME] = module
|
|
121
|
+
spec.loader.exec_module(module)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _print_result(value: object) -> None:
|
|
125
|
+
"""Render the user's return value to stdout.
|
|
126
|
+
|
|
127
|
+
Two lines: a human-readable line for ergonomic logs, and a marker line
|
|
128
|
+
with JSON for the cell to parse into ``JobResult.return_value``.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
print(f"\n{RESULT_MARKER} {value!r}", flush=True)
|
|
132
|
+
try:
|
|
133
|
+
encoded = json.dumps(value, default=repr)
|
|
134
|
+
except (TypeError, ValueError):
|
|
135
|
+
encoded = json.dumps(repr(value))
|
|
136
|
+
print(f"{RESULT_MARKER_JSON}{encoded}", flush=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run(argv: Sequence[str] | None = None) -> int:
|
|
140
|
+
args = _parse_args(sys.argv[1:] if argv is None else list(argv))
|
|
141
|
+
_install_timestamped_streams()
|
|
142
|
+
|
|
143
|
+
# Install guarded LeRobot import replacement before loading user code so
|
|
144
|
+
# `from lerobot...` inside the script sees the remote Feetech proxy.
|
|
145
|
+
from armnet_runtime.lerobot import install_lerobot_remote_mode
|
|
146
|
+
|
|
147
|
+
install_lerobot_remote_mode()
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
_load_user_script(args.script)
|
|
151
|
+
except Exception:
|
|
152
|
+
print("armnet-runtime: failed to import user script:", file=sys.stderr)
|
|
153
|
+
traceback.print_exc()
|
|
154
|
+
# An import failure is still a Python traceback in user code (think
|
|
155
|
+
# syntax error in `hello.py`); surface it the same way as a runtime
|
|
156
|
+
# raise so the cell lifts it onto JobResult.traceback.
|
|
157
|
+
_print_traceback_marker(traceback.format_exc())
|
|
158
|
+
return EXIT_USER_IMPORT_FAILED
|
|
159
|
+
|
|
160
|
+
fn = registered_main()
|
|
161
|
+
if fn is None:
|
|
162
|
+
print(
|
|
163
|
+
f"armnet-runtime: no @main-decorated function found in {args.script}. "
|
|
164
|
+
"Decorate exactly one function with `@main` to mark it as the entry point.",
|
|
165
|
+
file=sys.stderr,
|
|
166
|
+
)
|
|
167
|
+
return EXIT_NO_MAIN
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
ctx = build_context()
|
|
171
|
+
except Exception as exc: # noqa: BLE001
|
|
172
|
+
print(f"armnet-runtime: failed to build context: {exc}", file=sys.stderr)
|
|
173
|
+
return EXIT_BAD_CONTEXT
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
result = fn(ctx)
|
|
177
|
+
except SystemExit:
|
|
178
|
+
# Customer code called sys.exit explicitly \u2014 let that propagate.
|
|
179
|
+
raise
|
|
180
|
+
except Exception:
|
|
181
|
+
# Stderr: full, human-readable for `docker logs` users.
|
|
182
|
+
print(
|
|
183
|
+
f"armnet-runtime: @main function raised; "
|
|
184
|
+
f"job {ctx.job_id} will be marked FAILED.",
|
|
185
|
+
file=sys.stderr,
|
|
186
|
+
)
|
|
187
|
+
traceback.print_exc()
|
|
188
|
+
# Stdout: structured marker the cell parses into JobResult.traceback.
|
|
189
|
+
_print_traceback_marker(traceback.format_exc())
|
|
190
|
+
return EXIT_USER_RAISED
|
|
191
|
+
|
|
192
|
+
if result is not None:
|
|
193
|
+
_print_result(result)
|
|
194
|
+
return EXIT_OK
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _print_traceback_marker(tb_text: str) -> None:
|
|
198
|
+
"""Emit the `[armnet:traceback]:json` marker on stdout.
|
|
199
|
+
|
|
200
|
+
Truncates at :data:`MAX_TRACEBACK_BYTES` of UTF-8 to keep NATS payloads
|
|
201
|
+
bounded. The full untruncated traceback is still in stderr (printed by
|
|
202
|
+
``traceback.print_exc()`` above), so power users on the cell side can
|
|
203
|
+
read ``JobResult.stderr`` if the truncated head isn't enough.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
encoded = tb_text.encode("utf-8")
|
|
207
|
+
if len(encoded) > MAX_TRACEBACK_BYTES:
|
|
208
|
+
# Truncate on a UTF-8 boundary, then append a marker visible inside
|
|
209
|
+
# the truncated payload itself.
|
|
210
|
+
head = encoded[:MAX_TRACEBACK_BYTES].decode("utf-8", errors="ignore")
|
|
211
|
+
tb_text = head + "\n... [traceback truncated; full version in stderr]"
|
|
212
|
+
|
|
213
|
+
print(f"{TRACEBACK_MARKER_JSON}{json.dumps(tb_text)}", flush=True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
sys.exit(run())
|