armnet-client API Reference¶
armnet_client
¶
armnet client SDK — submit jobs to the platform.
Top-level surface:
- :func:
execute— submit one job, block until done. - :func:
execute_many— submit many in parallel, stream results. - :func:
execute_local— run a@mainscript in the current Python process; bypasses Docker, NATS, and the orchestrator. - :class:
Image— build a Docker image with content-hash caching. - :class:
OrchestratorClient— lower-level HTTP wrapper.
Wire types are re-exported from :mod:armnet_core for ergonomics.
OrchestratorClient
¶
Synchronous client for the orchestrator HTTP API.
No auth in M0.5 (design doc explicitly defers it). When auth lands
we'll add an api_key arg here and inject it as a header.
Source code in client/src/armnet_client/client.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
close
¶
close() -> None
Source code in client/src/armnet_client/client.py
69 70 | |
submit
¶
submit(spec: JobSpec, *, allow_queue: bool = True) -> Job
POST /jobs — create a new job.
The returned :class:Job carries dispatched: True if it was sent to
a cell immediately, False if it was accepted but queued.
allow_queue (default True): when no matching cell is free, queue the
job (True) or have the orchestrator refuse it with HTTP 409 (False, for
callers that only make sense running immediately).
Source code in client/src/armnet_client/client.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
get
¶
get(job_id: str) -> Job
GET /jobs/{id} — fetch the current state of a job.
Source code in client/src/armnet_client/client.py
96 97 98 99 100 | |
list_jobs
¶
list_jobs(*, limit: int = 10) -> list[Job]
GET /jobs — the current user's most-recent jobs (newest first).
Source code in client/src/armnet_client/client.py
102 103 104 105 106 | |
get_status_transitions
¶
get_status_transitions(job_id: str) -> list[JobStatusTransition]
GET /jobs/{id}/status-transitions — a job's status-change audit trail.
Source code in client/src/armnet_client/client.py
108 109 110 111 112 | |
stop_job
¶
stop_job(job_id: str, *, reason: str = 'client requested stop') -> Job
POST /jobs/{id}/stop — request an immediate stop of a running job.
Used when the operator intentionally aborts (e.g. Ctrl-C): this cancels the job now rather than waiting out the orchestrator's log-disconnect grace window.
Source code in client/src/armnet_client/client.py
114 115 116 117 118 119 120 121 122 123 | |
get_cell_status
¶
get_cell_status(*, embodiment: str = DEFAULT_EMBODIMENT, task: Optional[str] = None) -> CellStatusSummary
GET /cells/status — fetch current availability for a task.
Source code in client/src/armnet_client/client.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
get_registry_credentials
¶
get_registry_credentials() -> RegistryCredentials
POST /registry/credentials — fetch fresh image-registry credentials.
Used by :meth:armnet_client.Image.push to get a short-lived
OAuth2 token for pushing customer images to the platform's
Artifact Registry repo. The orchestrator returns 503 if it isn't
configured for credential issuance (typical for local dev).
Source code in client/src/armnet_client/client.py
140 141 142 143 144 145 146 147 148 149 150 | |
whoami
¶
whoami() -> WhoAmI
GET /whoami — fetch the username for the current API key.
Source code in client/src/armnet_client/client.py
152 153 154 155 156 157 | |
list_secrets
¶
list_secrets() -> SecretList
GET /secrets — list secret names for the current user.
Source code in client/src/armnet_client/client.py
159 160 161 162 163 164 | |
create_secret
¶
create_secret(name: str, value: str) -> SecretInfo
POST /secrets — create or replace one secret value.
Source code in client/src/armnet_client/client.py
166 167 168 169 170 171 172 173 174 | |
delete_secret
¶
delete_secret(name: str) -> None
DELETE /secrets/{name} — delete one user secret if it exists.
Source code in client/src/armnet_client/client.py
176 177 178 179 180 | |
get_volume_credentials
¶
get_volume_credentials() -> VolumeCredentials
POST /volume/credentials — fetch short-lived GCS volume credentials.
Source code in client/src/armnet_client/client.py
182 183 184 185 186 187 | |
wait
¶
wait(job_id: str, poll_interval_seconds: float = 1.0, deadline_seconds: Optional[float] = None) -> Job
Poll GET /jobs/{id} until the job reaches a terminal state.
Returns the final :class:Job. Raises :class:TimeoutError if
deadline_seconds elapses first.
Source code in client/src/armnet_client/client.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
OrchestratorError
¶
Bases: RuntimeError
Raised when the orchestrator returns a non-2xx response.
status_code is the HTTP status returned by the orchestrator;
None if the request never got a response (e.g. connection refused).
Callers can branch on it to distinguish e.g. "endpoint not implemented"
(404) or "configured-but-unavailable" (503) from real failures.
Source code in client/src/armnet_client/client.py
34 35 36 37 38 39 40 41 42 43 44 45 | |
Image
dataclass
¶
A reference to a Docker image, either pre-existing or built locally.
Most customer code constructs one with :meth:build. To wrap an
existing registry-pushed image, use :meth:from_ref or just pass the
image string directly to :func:execute.
Source code in client/src/armnet_client/image.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
from_ref
classmethod
¶
from_ref(ref: str) -> 'Image'
Source code in client/src/armnet_client/image.py
98 99 100 | |
push
¶
push(*, if_possible: bool = False) -> 'Image'
Push this image to the platform's registry; return a new Image whose ref points at the registry.
Flow:
- Fetch short-lived OAuth2 credentials from the orchestrator's
POST /registry/credentialsendpoint (cached on disk; refreshed when within 5 minutes of expiry). docker login <registry> -u oauth2accesstoken --password-stdin.docker tag <local> <registry>/<namespace>/<local>.docker pushthe new tag.- Return
Image(ref=<full registry ref>).
Customers don't need any GCP credentials; the orchestrator does the IAM dance on their behalf via service-account impersonation.
For pure-local M0.5 dev where the orchestrator + cell share a
docker daemon and pushing isn't necessary, pass if_possible=True
— the method then logs a warning and returns self unchanged
if the orchestrator can't issue credentials, instead of raising.
Real docker push failures (network, auth, etc.) always raise.
Source code in client/src/armnet_client/image.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
build
classmethod
¶
build(dockerfile: Union[str, Path], name: str, context_dir: Union[str, Path, None] = None, force_rebuild: bool = False, platform: Optional[str] = None, build_contexts: Optional[Mapping[str, Union[str, Path]]] = None, cache_from: Optional[Iterable[str]] = None, cache_to: Optional[Iterable[str]] = None) -> 'Image'
Build a Docker image and return a content-addressed reference.
:param dockerfile: Path to the Dockerfile.
:param name: Image name (the part before the tag); used as a
human-readable prefix on the resulting tag.
:param context_dir: Build context directory. Defaults to the
Dockerfile's parent.
:param force_rebuild: If True, always invoke docker build even
when an image with the computed content-hash tag already
exists.
:param platform: Optional Docker platform, e.g. linux/arm64/v8 or
linux/amd64. Defaults to ARMNET_DOCKER_PLATFORM when
set, otherwise Docker's default platform for the build host.
:param build_contexts: Optional named build contexts forwarded to
docker build --build-context name=path. The Dockerfile can
then reference them via COPY --from=name .... Useful for
pulling in source trees that live outside the primary build
context (e.g. a sibling checkout on the build host). Contents
participate in the content hash so changes invalidate the cache.
:param cache_from: Optional BuildKit --cache-from specs (e.g.
["type=gha,scope=my-image"]). When any cache is requested the
build runs via docker buildx build --load so layers can be
reused across machines/CI runs. Falls back to a plain
docker build (no cache) if buildx or the cache backend is
unavailable, so enabling caching can never break a build.
:param cache_to: Optional BuildKit --cache-to specs (e.g.
["type=gha,scope=my-image,mode=max"]).
:returns: An :class:Image whose ref is
<name>:armnet-<short-hash>.
As a convenience for CI, setting ARMNET_DOCKER_BUILDX_CACHE=gha in
the environment enables GitHub-Actions layer caching (scoped per image
name) without passing cache_from / cache_to explicitly.
Source code in client/src/armnet_client/image.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
ImageBuildError
¶
Bases: RuntimeError
Raised when docker build fails.
Source code in client/src/armnet_client/image.py
64 65 | |
ImagePushError
¶
Bases: RuntimeError
Raised when docker login / docker tag / docker push fails.
Source code in client/src/armnet_client/image.py
68 69 | |
RegistryCredentialsUnavailableError
¶
Bases: RuntimeError
Raised when the orchestrator can't issue image-registry credentials.
This is the recoverable shape of an :meth:Image.push failure — the
orchestrator either doesn't have the endpoint (404), isn't configured
for credential issuance (503), or isn't reachable at all (connection
refused, typical for pure-local M0.5 dev). Callers can pass
if_possible=True to :meth:Image.push to log + fall back to the
local image ref instead of propagating this.
Source code in client/src/armnet_client/image.py
72 73 74 75 76 77 78 79 80 81 | |
LocalContext
dataclass
¶
Developer-facing context for local Docker execution.
This is intentionally smaller than RobotCellConfig. It describes the
runtime values a containerized program needs, without exposing operator-only
cell settings such as NATS, cell IDs, or completion model configuration.
Source code in client/src/armnet_client/local_container.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
camera_configs
class-attribute
instance-attribute
¶
camera_configs: Mapping[str, Any] = field(default_factory=dict)
calibration_dir
class-attribute
instance-attribute
¶
calibration_dir: Optional[Union[str, Path]] = None
calibration_file_path
class-attribute
instance-attribute
¶
calibration_file_path: Optional[Union[str, Path]] = None
language_instruction
class-attribute
instance-attribute
¶
language_instruction: Optional[str] = None
TeleoperatorConfig
dataclass
¶
How to connect to the local leader arm being used to teleoperate.
Mirrors what LeRobot needs to construct a teleoperator, plus the sample
rate. id is the LeRobot teleoperator id used to find calibration.
Source code in client/src/armnet_client/teleop_stream.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
execute_many
¶
execute_many(specs: Sequence[Mapping[str, Any]], *, max_parallel: int = 4) -> Iterator[JobResult]
Submit many jobs in parallel; yield :class:JobResult as each completes.
specs is a sequence of kwarg-dicts, each accepted by
:func:execute (e.g. [{"image": ..., "embodiment": ..., "task": ...,
"args": {...}}, ...]). Results are yielded in completion order, not
submission order — match them up via your spec's own identifying
field if you need to.
max_parallel caps the number of concurrent submissions. Backed by
a thread pool; safe to use from synchronous code.
Source code in client/src/armnet_client/execute.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
execute_local
¶
execute_local(main_module: Union[str, Path], *, args: Optional[Mapping[str, Any]] = None, port: Optional[str] = None, robot_id: Optional[str] = None, calibration_dir: Optional[Union[str, Path]] = None, calibration_file_path: Optional[Union[str, Path]] = None, camera_configs: Optional[Mapping[str, Any]] = None, embodiment: str = DEFAULT_EMBODIMENT, task: str = DEFAULT_TASK, timeout_seconds: int = 120, job_id: Optional[str] = None) -> JobResult
Run a @main-decorated script in the current Python process.
:param main_module: Path to the Python file containing exactly one
@main-decorated function.
:param args: Mapping passed through as ctx.args.
:param port: Value placed at ctx.cell.robot_port — typically the path
to a real local serial device, e.g. "/dev/ttyUSB0".
:param robot_id: Value placed at ctx.cell.robot_id — typically the
name of the robot, e.g. "praveen_so101".
:param embodiment: Recorded on ctx.embodiment (defaults to the
only embodiment supported in M0.5).
:param task: Recorded on ctx.task (defaults to the only task
supported in M0.5).
:param timeout_seconds: Recorded on ctx.timeout_seconds. Not
enforced by execute_local (see module docstring).
:param job_id: Optional explicit job id for ctx.job_id. Defaults
to a stable synthetic id derived from the script path.
:returns: A :class:JobResult with return_value set to whatever
@main returned, or error populated with a traceback if it
raised.
Source code in client/src/armnet_client/local.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
execute_local_container
¶
execute_local_container(*, image: Union[str, Image], local_context: Optional[LocalContext] = None, args: Optional[Mapping[str, Any]] = None, embodiment: str = DEFAULT_EMBODIMENT, task: str = DEFAULT_TASK, timeout_seconds: int = 120, cell_socket: Optional[str] = None, cell_endpoint: Optional[str] = None, robot_id: Optional[str] = None, calibration_dir: Optional[Union[str, Path]] = None, calibration_file_path: Optional[Union[str, Path]] = None, cell_config: Optional[RobotCellConfig] = None, cell_config_path: Optional[Union[str, Path]] = None, mount_calibration: bool = True, username: str = 'remoterobo-test', job_id: str = 'job_local_container', stream_output: bool = True, docker_gpus: Optional[str] = None, docker_runtime: Optional[str] = None, gpus: Optional[str] = None) -> JobResult
Run a runtime image locally through Docker.
This is the M2 local-dev path for robot examples: it validates the same container + armnet-runtime + Unix-socket route as remote execution, without going through NATS or the cloud orchestrator.
Source code in client/src/armnet_client/local_container.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
init_rerun
¶
init_rerun(application_id: str = 'armnet', *, spawn: bool = True, serve_web: bool = False, web_port: Optional[int] = None, grpc_port: Optional[int] = None, open_browser: bool = True, flush_tick_seconds: Optional[float] = None) -> Any
Initialize Rerun and start a viewer.
Thin convenience wrapper so orchestrate scripts don't need to import the
Rerun SDK directly. Raises a clear error if rerun-sdk isn't installed
(pip install 'armnet-client[viz]').
Viewer selection:
serve_web=Truealways hosts the web viewer over HTTP (open the printed URL in a browser; forward the port first if you're on a remote host).spawn=True(default) opens a native desktop window — but only when a display is available. On a headless machine (noDISPLAY/ Wayland, e.g. SSH or a GUI-less workstation) it automatically falls back to the web viewer instead of failing to open a window.
flush_tick_seconds sets Rerun's micro-batching flush interval (the
RERUN_FLUSH_TICK_SECS knob). A very small value (e.g. 0.002 = 2ms)
minimizes how long logged data waits before being sent to the viewer —
noticeably snappier for teleoperation. Left unset, Rerun's default applies.
Source code in client/src/armnet_client/rerun_stream.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
start_recording_key_listener
¶
start_recording_key_listener(events: 'queue.Queue[str]')
Bind LeRobot's dataset-recording keyboard shortcuts to events.
Starts a (non-suppressing) global pynput listener mapping Right Arrow →
next_episode, Left Arrow → rerecord_episode, Esc →
stop_recording; each press enqueues the event string for
:func:stream_job_teleop to forward to the cell. Esc does not tear
anything down client-side: the runtime ends the session on receipt and the
job result unblocks the orchestrate script, which then stops its streams.
Returns the started listener (call .stop() when done), or None when no
keyboard is available (e.g. headless Linux without $DISPLAY — the same
pynput limitation LeRobot's own recorder has).
Source code in client/src/armnet_client/teleop_stream.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
stream_job_teleop
¶
stream_job_teleop(job_id: str, stop: Event, config: TeleoperatorConfig, events: 'queue.Queue[str] | None' = None) -> None
Sample the local leader and push actions to job_id until stop is set.
Intended to run on a background thread (alongside the rerun stream). Returns quietly if no API key is configured, LeRobot is missing, or the leader/ websocket can't be reached, so teleop never crashes the orchestrate script.
events, when given, is a queue of recording-control event strings (see
:func:start_recording_key_listener); each is sent on the same websocket
as a :class:TeleopEvent frame. Unlike action frames — which are
freshest-wins and freely dropped — a queued event survives websocket
reconnects: it is re-queued on a failed send and retried.
Source code in client/src/armnet_client/teleop_stream.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |
armnet_client.execute
¶
Top-level job-submission API: execute and execute_many.
Both target the platform's remote orchestrator + cell infrastructure. In M0.5 the orchestrator and cell happen to run on localhost, but they are still "remote" from the SDK's perspective — the SDK speaks to them only through the orchestrator HTTP API.
The orchestrator URL is hard-coded into the SDK build (see
:mod:armnet_client._config); customers do not pass it. Set
DEBUG_ARMNET_ORCHESTRATOR_URI to override during development.
execute
¶
execute(*, image: Union[str, Image], embodiment: str, task: Optional[str] = None, args: Optional[Mapping[str, Any]] = None, secrets: Optional[Mapping[str, str]] = None, detach: bool = False, stream_logs: bool = True, use_rerun: bool = False, timeout_seconds: int = 120, allow_queue: bool = True) -> JobResult
Submit a single job to the platform and block until it terminates.
Returns the final :class:JobResult. Use result.return_value to
access whatever the customer's @main-decorated function returned;
use result.status/result.error for outcome.
The SDK polls the orchestrator until the job reaches a terminal state
or until timeout_seconds + 30 seconds have elapsed (the cell
enforces the actual timeout_seconds cap on the container; the
+30 here is just SDK-side polling slack).
Omit task to submit a task-less job: any cell of embodiment may run
it, regardless of the task it is configured for.
allow_queue (default True): if no matching cell is free when the job is
submitted, it is accepted and queued — this call then returns immediately
with a non-terminal JobResult (status submitted) rather than blocking,
since there may be no cell to run it for a while. Set allow_queue=False
for jobs that only make sense running right now (e.g. interactive
teleop/rerun): the orchestrator refuses to create the job (raising
:class:~armnet_client.client.OrchestratorError, HTTP 409) if it can't
be dispatched immediately.
Source code in client/src/armnet_client/execute.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
execute_many
¶
execute_many(specs: Sequence[Mapping[str, Any]], *, max_parallel: int = 4) -> Iterator[JobResult]
Submit many jobs in parallel; yield :class:JobResult as each completes.
specs is a sequence of kwarg-dicts, each accepted by
:func:execute (e.g. [{"image": ..., "embodiment": ..., "task": ...,
"args": {...}}, ...]). Results are yielded in completion order, not
submission order — match them up via your spec's own identifying
field if you need to.
max_parallel caps the number of concurrent submissions. Backed by
a thread pool; safe to use from synchronous code.
Source code in client/src/armnet_client/execute.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
armnet_client.image
¶
Programmatic Docker image building with content-hash caching.
The :class:Image class lets customer code declaratively build a Docker
image from a Dockerfile and pass the resulting reference to
:func:execute. Builds are content-addressed: the resulting image tag is
derived from a hash of the Dockerfile + every file in the build context,
so identical inputs short-circuit to the cached image without re-invoking
docker build.
Example::
from armnet_client import Image, execute
img = Image.build(
dockerfile="Dockerfile",
context_dir=".",
name="my-eval-runner",
)
result = execute(image=img, embodiment="lerobot/so-101",
task="assemble_block_tower", args={"seed": 0})
The cache lives in the local Docker daemon (we tag with a content hash). For M0.5 the cell runs against the same daemon so cached images are immediately available; for M1+ we'll add a registry push step.
ImageBuildError
¶
Bases: RuntimeError
Raised when docker build fails.
Source code in client/src/armnet_client/image.py
64 65 | |
ImagePushError
¶
Bases: RuntimeError
Raised when docker login / docker tag / docker push fails.
Source code in client/src/armnet_client/image.py
68 69 | |
RegistryCredentialsUnavailableError
¶
Bases: RuntimeError
Raised when the orchestrator can't issue image-registry credentials.
This is the recoverable shape of an :meth:Image.push failure — the
orchestrator either doesn't have the endpoint (404), isn't configured
for credential issuance (503), or isn't reachable at all (connection
refused, typical for pure-local M0.5 dev). Callers can pass
if_possible=True to :meth:Image.push to log + fall back to the
local image ref instead of propagating this.
Source code in client/src/armnet_client/image.py
72 73 74 75 76 77 78 79 80 81 | |
Image
dataclass
¶
A reference to a Docker image, either pre-existing or built locally.
Most customer code constructs one with :meth:build. To wrap an
existing registry-pushed image, use :meth:from_ref or just pass the
image string directly to :func:execute.
Source code in client/src/armnet_client/image.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
from_ref
classmethod
¶
from_ref(ref: str) -> 'Image'
Source code in client/src/armnet_client/image.py
98 99 100 | |
push
¶
push(*, if_possible: bool = False) -> 'Image'
Push this image to the platform's registry; return a new Image whose ref points at the registry.
Flow:
- Fetch short-lived OAuth2 credentials from the orchestrator's
POST /registry/credentialsendpoint (cached on disk; refreshed when within 5 minutes of expiry). docker login <registry> -u oauth2accesstoken --password-stdin.docker tag <local> <registry>/<namespace>/<local>.docker pushthe new tag.- Return
Image(ref=<full registry ref>).
Customers don't need any GCP credentials; the orchestrator does the IAM dance on their behalf via service-account impersonation.
For pure-local M0.5 dev where the orchestrator + cell share a
docker daemon and pushing isn't necessary, pass if_possible=True
— the method then logs a warning and returns self unchanged
if the orchestrator can't issue credentials, instead of raising.
Real docker push failures (network, auth, etc.) always raise.
Source code in client/src/armnet_client/image.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
build
classmethod
¶
build(dockerfile: Union[str, Path], name: str, context_dir: Union[str, Path, None] = None, force_rebuild: bool = False, platform: Optional[str] = None, build_contexts: Optional[Mapping[str, Union[str, Path]]] = None, cache_from: Optional[Iterable[str]] = None, cache_to: Optional[Iterable[str]] = None) -> 'Image'
Build a Docker image and return a content-addressed reference.
:param dockerfile: Path to the Dockerfile.
:param name: Image name (the part before the tag); used as a
human-readable prefix on the resulting tag.
:param context_dir: Build context directory. Defaults to the
Dockerfile's parent.
:param force_rebuild: If True, always invoke docker build even
when an image with the computed content-hash tag already
exists.
:param platform: Optional Docker platform, e.g. linux/arm64/v8 or
linux/amd64. Defaults to ARMNET_DOCKER_PLATFORM when
set, otherwise Docker's default platform for the build host.
:param build_contexts: Optional named build contexts forwarded to
docker build --build-context name=path. The Dockerfile can
then reference them via COPY --from=name .... Useful for
pulling in source trees that live outside the primary build
context (e.g. a sibling checkout on the build host). Contents
participate in the content hash so changes invalidate the cache.
:param cache_from: Optional BuildKit --cache-from specs (e.g.
["type=gha,scope=my-image"]). When any cache is requested the
build runs via docker buildx build --load so layers can be
reused across machines/CI runs. Falls back to a plain
docker build (no cache) if buildx or the cache backend is
unavailable, so enabling caching can never break a build.
:param cache_to: Optional BuildKit --cache-to specs (e.g.
["type=gha,scope=my-image,mode=max"]).
:returns: An :class:Image whose ref is
<name>:armnet-<short-hash>.
As a convenience for CI, setting ARMNET_DOCKER_BUILDX_CACHE=gha in
the environment enables GitHub-Actions layer caching (scoped per image
name) without passing cache_from / cache_to explicitly.
Source code in client/src/armnet_client/image.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
armnet_client.client
¶
HTTP client for the orchestrator.
Thin wrapper around the two M0.5 endpoints. Used internally by
:func:armnet_client.execute; exposed publicly for power users that
need fine-grained control (custom polling, fire-and-forget submissions,
etc.).
OrchestratorError
¶
Bases: RuntimeError
Raised when the orchestrator returns a non-2xx response.
status_code is the HTTP status returned by the orchestrator;
None if the request never got a response (e.g. connection refused).
Callers can branch on it to distinguish e.g. "endpoint not implemented"
(404) or "configured-but-unavailable" (503) from real failures.
Source code in client/src/armnet_client/client.py
34 35 36 37 38 39 40 41 42 43 44 45 | |
OrchestratorClient
¶
Synchronous client for the orchestrator HTTP API.
No auth in M0.5 (design doc explicitly defers it). When auth lands
we'll add an api_key arg here and inject it as a header.
Source code in client/src/armnet_client/client.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
close
¶
close() -> None
Source code in client/src/armnet_client/client.py
69 70 | |
submit
¶
submit(spec: JobSpec, *, allow_queue: bool = True) -> Job
POST /jobs — create a new job.
The returned :class:Job carries dispatched: True if it was sent to
a cell immediately, False if it was accepted but queued.
allow_queue (default True): when no matching cell is free, queue the
job (True) or have the orchestrator refuse it with HTTP 409 (False, for
callers that only make sense running immediately).
Source code in client/src/armnet_client/client.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
get
¶
get(job_id: str) -> Job
GET /jobs/{id} — fetch the current state of a job.
Source code in client/src/armnet_client/client.py
96 97 98 99 100 | |
list_jobs
¶
list_jobs(*, limit: int = 10) -> list[Job]
GET /jobs — the current user's most-recent jobs (newest first).
Source code in client/src/armnet_client/client.py
102 103 104 105 106 | |
get_status_transitions
¶
get_status_transitions(job_id: str) -> list[JobStatusTransition]
GET /jobs/{id}/status-transitions — a job's status-change audit trail.
Source code in client/src/armnet_client/client.py
108 109 110 111 112 | |
stop_job
¶
stop_job(job_id: str, *, reason: str = 'client requested stop') -> Job
POST /jobs/{id}/stop — request an immediate stop of a running job.
Used when the operator intentionally aborts (e.g. Ctrl-C): this cancels the job now rather than waiting out the orchestrator's log-disconnect grace window.
Source code in client/src/armnet_client/client.py
114 115 116 117 118 119 120 121 122 123 | |
get_cell_status
¶
get_cell_status(*, embodiment: str = DEFAULT_EMBODIMENT, task: Optional[str] = None) -> CellStatusSummary
GET /cells/status — fetch current availability for a task.
Source code in client/src/armnet_client/client.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
get_registry_credentials
¶
get_registry_credentials() -> RegistryCredentials
POST /registry/credentials — fetch fresh image-registry credentials.
Used by :meth:armnet_client.Image.push to get a short-lived
OAuth2 token for pushing customer images to the platform's
Artifact Registry repo. The orchestrator returns 503 if it isn't
configured for credential issuance (typical for local dev).
Source code in client/src/armnet_client/client.py
140 141 142 143 144 145 146 147 148 149 150 | |
whoami
¶
whoami() -> WhoAmI
GET /whoami — fetch the username for the current API key.
Source code in client/src/armnet_client/client.py
152 153 154 155 156 157 | |
list_secrets
¶
list_secrets() -> SecretList
GET /secrets — list secret names for the current user.
Source code in client/src/armnet_client/client.py
159 160 161 162 163 164 | |
create_secret
¶
create_secret(name: str, value: str) -> SecretInfo
POST /secrets — create or replace one secret value.
Source code in client/src/armnet_client/client.py
166 167 168 169 170 171 172 173 174 | |
delete_secret
¶
delete_secret(name: str) -> None
DELETE /secrets/{name} — delete one user secret if it exists.
Source code in client/src/armnet_client/client.py
176 177 178 179 180 | |
get_volume_credentials
¶
get_volume_credentials() -> VolumeCredentials
POST /volume/credentials — fetch short-lived GCS volume credentials.
Source code in client/src/armnet_client/client.py
182 183 184 185 186 187 | |
wait
¶
wait(job_id: str, poll_interval_seconds: float = 1.0, deadline_seconds: Optional[float] = None) -> Job
Poll GET /jobs/{id} until the job reaches a terminal state.
Returns the final :class:Job. Raises :class:TimeoutError if
deadline_seconds elapses first.
Source code in client/src/armnet_client/client.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
armnet_client.volume
¶
Local armnet volume helpers.
upload_to_local_volume
¶
upload_to_local_volume(*, username: str, local_path: str | Path, volume_path: str | Path, overwrite: bool = False) -> Path
Source code in client/src/armnet_client/volume.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | |
upload_to_cloud_volume
¶
upload_to_cloud_volume(*, credentials: VolumeCredentials, username: str, local_path: str | Path, volume_path: str | Path, overwrite: bool = False) -> list[str]
Source code in client/src/armnet_client/volume.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
download_from_cloud_volume
¶
download_from_cloud_volume(*, credentials: VolumeCredentials, username: str, volume_path: str | Path, local_path: str | Path) -> Path
Source code in client/src/armnet_client/volume.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
delete_from_cloud_volume
¶
delete_from_cloud_volume(*, credentials: VolumeCredentials, volume_path: str | Path) -> None
Source code in client/src/armnet_client/volume.py
104 105 106 107 108 109 110 111 112 | |
armnet_client.local
¶
execute_local — in-process execution of a @main script.
Faster-iteration counterpart to :func:execute: no Docker, no
orchestrator, no NATS. Loads the customer's Python file directly, finds
the @main-decorated function, builds a
:class:~armnet_runtime.Context with a user-supplied robot_port, and
calls it in the current process.
Returns the same :class:~armnet_core.JobResult shape as
:func:execute so downstream code (result.return_value,
result.status, etc.) is identical.
Notes for M0.5:
- The LeRobot import-system swap that
armnet-runtimewill perform in M3+ is intentionally not done here — the whole point ofexecute_localis to talk to a real local robot driver, not a safety-aware connector. - Stdout / stderr are not captured (they go to the user's terminal);
the corresponding :class:
JobResultfields are empty strings. timeout_secondsis recorded intoctx.timeout_secondsfor the function to honour, but is not enforced byexecute_localitself. Killing arbitrary in-process Python by deadline is hard to do cleanly without subprocesses; deferred until we need it.
execute_local
¶
execute_local(main_module: Union[str, Path], *, args: Optional[Mapping[str, Any]] = None, port: Optional[str] = None, robot_id: Optional[str] = None, calibration_dir: Optional[Union[str, Path]] = None, calibration_file_path: Optional[Union[str, Path]] = None, camera_configs: Optional[Mapping[str, Any]] = None, embodiment: str = DEFAULT_EMBODIMENT, task: str = DEFAULT_TASK, timeout_seconds: int = 120, job_id: Optional[str] = None) -> JobResult
Run a @main-decorated script in the current Python process.
:param main_module: Path to the Python file containing exactly one
@main-decorated function.
:param args: Mapping passed through as ctx.args.
:param port: Value placed at ctx.cell.robot_port — typically the path
to a real local serial device, e.g. "/dev/ttyUSB0".
:param robot_id: Value placed at ctx.cell.robot_id — typically the
name of the robot, e.g. "praveen_so101".
:param embodiment: Recorded on ctx.embodiment (defaults to the
only embodiment supported in M0.5).
:param task: Recorded on ctx.task (defaults to the only task
supported in M0.5).
:param timeout_seconds: Recorded on ctx.timeout_seconds. Not
enforced by execute_local (see module docstring).
:param job_id: Optional explicit job id for ctx.job_id. Defaults
to a stable synthetic id derived from the script path.
:returns: A :class:JobResult with return_value set to whatever
@main returned, or error populated with a traceback if it
raised.
Source code in client/src/armnet_client/local.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
armnet_client.local_container
¶
Docker-backed local execution for robot cell development.
LocalContext
dataclass
¶
Developer-facing context for local Docker execution.
This is intentionally smaller than RobotCellConfig. It describes the
runtime values a containerized program needs, without exposing operator-only
cell settings such as NATS, cell IDs, or completion model configuration.
Source code in client/src/armnet_client/local_container.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
camera_configs
class-attribute
instance-attribute
¶
camera_configs: Mapping[str, Any] = field(default_factory=dict)
calibration_dir
class-attribute
instance-attribute
¶
calibration_dir: Optional[Union[str, Path]] = None
calibration_file_path
class-attribute
instance-attribute
¶
calibration_file_path: Optional[Union[str, Path]] = None
language_instruction
class-attribute
instance-attribute
¶
language_instruction: Optional[str] = None
execute_local_container
¶
execute_local_container(*, image: Union[str, Image], local_context: Optional[LocalContext] = None, args: Optional[Mapping[str, Any]] = None, embodiment: str = DEFAULT_EMBODIMENT, task: str = DEFAULT_TASK, timeout_seconds: int = 120, cell_socket: Optional[str] = None, cell_endpoint: Optional[str] = None, robot_id: Optional[str] = None, calibration_dir: Optional[Union[str, Path]] = None, calibration_file_path: Optional[Union[str, Path]] = None, cell_config: Optional[RobotCellConfig] = None, cell_config_path: Optional[Union[str, Path]] = None, mount_calibration: bool = True, username: str = 'remoterobo-test', job_id: str = 'job_local_container', stream_output: bool = True, docker_gpus: Optional[str] = None, docker_runtime: Optional[str] = None, gpus: Optional[str] = None) -> JobResult
Run a runtime image locally through Docker.
This is the M2 local-dev path for robot examples: it validates the same container + armnet-runtime + Unix-socket route as remote execution, without going through NATS or the cloud orchestrator.
Source code in client/src/armnet_client/local_container.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
armnet_client.rerun_stream
¶
Client-side Rerun streaming.
Connects to the orchestrator's /jobs/{job_id}/rerun websocket, decodes the
binary RerunPacket frames the cell republished from the container's
ctx.log_rerun_data(...) calls, and replays each into the active Rerun
recording via rr.log(...).
Call :func:init_rerun (or rerun.init(...) directly) once in your
orchestrate script before submitting the job so the packets land in a live
viewer.
init_rerun
¶
init_rerun(application_id: str = 'armnet', *, spawn: bool = True, serve_web: bool = False, web_port: Optional[int] = None, grpc_port: Optional[int] = None, open_browser: bool = True, flush_tick_seconds: Optional[float] = None) -> Any
Initialize Rerun and start a viewer.
Thin convenience wrapper so orchestrate scripts don't need to import the
Rerun SDK directly. Raises a clear error if rerun-sdk isn't installed
(pip install 'armnet-client[viz]').
Viewer selection:
serve_web=Truealways hosts the web viewer over HTTP (open the printed URL in a browser; forward the port first if you're on a remote host).spawn=True(default) opens a native desktop window — but only when a display is available. On a headless machine (noDISPLAY/ Wayland, e.g. SSH or a GUI-less workstation) it automatically falls back to the web viewer instead of failing to open a window.
flush_tick_seconds sets Rerun's micro-batching flush interval (the
RERUN_FLUSH_TICK_SECS knob). A very small value (e.g. 0.002 = 2ms)
minimizes how long logged data waits before being sent to the viewer —
noticeably snappier for teleoperation. Left unset, Rerun's default applies.
Source code in client/src/armnet_client/rerun_stream.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
stream_job_rerun
¶
stream_job_rerun(job_id: str, stop: Event) -> None
Consume the rerun websocket for job_id until stop is set.
Intended to run on a background thread. Silently returns if no API key is configured, the websocket can't be reached, or the rerun SDK is missing, so visualization never blocks or fails the job itself.
Source code in client/src/armnet_client/rerun_stream.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
armnet_client.teleop_stream
¶
Client-side remote teleoperation.
Connects to a locally-attached LeRobot leader arm (e.g. an SO-101 leader),
samples its joint positions at a high rate, and pushes each reading to the
orchestrator's /jobs/{job_id}/teleop websocket. The orchestrator forwards
them to the job's cell, which keeps only the freshest action and serves it to
the runtime container (ctx.cell.get_teleop_action()), which drives the
remote follower.
Sampling faster than the robot's control rate (default 60Hz vs a ~20Hz robot loop) makes teleop feel snappier: the cell always has a near-current command, so a dropped or late frame doesn't force the follower to wait a full robot tick for the next one.
When recording a dataset (armnet-lerobot-record), discrete
recording-control events ride the same websocket: pass an events queue to
:func:stream_job_teleop and feed it from
:func:start_recording_key_listener, which binds LeRobot's standard dataset
recording shortcuts (Right Arrow → save episode and move on, Left Arrow →
discard and re-record, Esc → stop the session).
LeRobot is an optional dependency — install with armnet-client[teleop].
It is imported lazily so the rest of the client works without it.
TeleoperatorConfig
dataclass
¶
How to connect to the local leader arm being used to teleoperate.
Mirrors what LeRobot needs to construct a teleoperator, plus the sample
rate. id is the LeRobot teleoperator id used to find calibration.
Source code in client/src/armnet_client/teleop_stream.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
start_recording_key_listener
¶
start_recording_key_listener(events: 'queue.Queue[str]')
Bind LeRobot's dataset-recording keyboard shortcuts to events.
Starts a (non-suppressing) global pynput listener mapping Right Arrow →
next_episode, Left Arrow → rerecord_episode, Esc →
stop_recording; each press enqueues the event string for
:func:stream_job_teleop to forward to the cell. Esc does not tear
anything down client-side: the runtime ends the session on receipt and the
job result unblocks the orchestrate script, which then stops its streams.
Returns the started listener (call .stop() when done), or None when no
keyboard is available (e.g. headless Linux without $DISPLAY — the same
pynput limitation LeRobot's own recorder has).
Source code in client/src/armnet_client/teleop_stream.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
stream_job_teleop
¶
stream_job_teleop(job_id: str, stop: Event, config: TeleoperatorConfig, events: 'queue.Queue[str] | None' = None) -> None
Sample the local leader and push actions to job_id until stop is set.
Intended to run on a background thread (alongside the rerun stream). Returns quietly if no API key is configured, LeRobot is missing, or the leader/ websocket can't be reached, so teleop never crashes the orchestrate script.
events, when given, is a queue of recording-control event strings (see
:func:start_recording_key_listener); each is sent on the same websocket
as a :class:TeleopEvent frame. Unlike action frames — which are
freshest-wins and freely dropped — a queued event survives websocket
reconnects: it is re-queued on a failed send and retried.
Source code in client/src/armnet_client/teleop_stream.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |