Every topic and message this component publishes or accepts, and the CLI flags it runs under.
Addressing follows the Unified Namespace: ecv1/{device}/{component}/{instance}/{class}[/channel].
{device}— the resolved Thing name (the lasthierarchylevel, or-tdirectly).{component}—image-processor, set bycomponent.token. It is a separate identifier from the Greengrass component name (com.mbreissi.edgecommons.ImageProcessor), which never appears on the wire.{instance}— the route id for everything a route publishes; absent on the component-scope surfaces (thestatekeepalive,metric, and the component command inbox).
Every message is the EdgeCommons protobuf envelope {header, identity, tags, body}. A subscriber
that prints raw payloads sees bytes; decode them with the library
(edgecommons.messaging.message.Message.from_bytes) or use ec-uns-cmd for commands.
| Class | Message | Direction | Topic | Reply |
|---|---|---|---|---|
app |
ImageInferenceResult |
component → bus | ecv1/{device}/image-processor/{routeId}/app/inference/result |
— |
app |
ImageInferenceResult |
component → requester | the request's reply_to, correlated |
— |
app |
trigger request | bus → component | whatever source.subscribe names |
the bounded result, when the request carries reply_to |
app |
ImageCaptured |
bus → component | ecv1/{device}/camera-adapter/{instance}/app/image/captured |
— |
data |
SouthboundSignalUpdate |
component → bus | ecv1/{device}/image-processor/{routeId}/data/{signalId} |
— |
evt |
operator conditions | component → bus | ecv1/{device}/image-processor/{routeId}/evt/{severity}/{type} |
— |
cmd |
the verbs below | bus → component | ecv1/{device}/image-processor/{instance}/cmd/{verb} |
{ok, result} or {ok, error} |
cmd |
sb/capture-status |
component → camera | ecv1/{device}/camera-adapter/{instance}/cmd/sb/capture-status |
the camera's paged answer |
metric |
the eight groups | component → bus (automatic) | ecv1/{device}/image-processor/metric/{group} |
— |
state |
keepalive | component → bus (automatic) | ecv1/{device}/image-processor/state |
— |
state, metric, cfg, and log are library-owned reserved classes: a component publish
directly to them is rejected.
Header name ImageInferenceResult, version 1.0.
schemas/inference-result.schema.json is the contract; the component validates every body against
it before the message is prepared.
This is the authoritative, cleanup-gating output. It is prepared once, its exact bytes stored in
the ledger, and published with positive transport confirmation; the input is archived, deleted, or
quarantined only after that. A crash between transport confirmation and the local acknowledgement
may publish a duplicate, so consumers deduplicate on inferenceId.
{
"schemaVersion": "1.0",
"inferenceId": "blvuf6ru3i6jzmmksre6hkgccm",
"routeId": "clearance-cam-01",
"status": "SUCCEEDED",
"source": {
"kind": "spool",
"captureId": "cap_0001",
"cameraId": "cam-01",
"relativePath": "2026/08/23/cap-0001.png",
"bytes": 112,
"sha256": "1ce61926…",
"correlationId": "corr_cap_0001",
"capturedAtMs": 1787486400512
},
"model": {
"id": "synthetic-anomaly-scalar",
"version": "1.0.0",
"digest": "sha256:4a87394f…",
"runtime": "onnxruntime",
"providers": ["CPUExecutionProvider"],
"gpu": null,
"transformVersion": "1"
},
"decision": { "outcome": "CLEAR", "pass": true, "confidence": 0.0, "threshold": 0.05, "rule": "pass" },
"outputs": {
"family": "anomaly",
"anomaly": { "score": 0.0, "threshold": 0.05, "anomalous": false, "direction": "higherIsAnomalous" },
"truncated": false
},
"timingsMs": { "queue": 58.0, "modelLoad": 51.2, "preprocess": 11.7, "inference": 0.1, "postprocess": 23.5, "total": 93.5 },
"artifacts": {
"evidenceId": "blvuf6ru3i6jzmmksre6hkgccm",
"localRelativePath": "cap-0001.png.inference.json",
"sha256": "ad659011…",
"bytes": 1805
}
}| Field | Meaning |
|---|---|
inferenceId |
The stable identity of this inference, derived from the route, the capture or source digest, and the model digest. A retry reuses it; reprocessing under a different model digest produces a different one. |
status |
SUCCEEDED carries decision and outputs; FAILED carries error, and its decision is never CLEAR. |
source.kind |
spool (discovered in a watched root), inline (carried in a trigger message), or reference (named by a trigger message and resolved under fileRoot). |
source.sha256 |
The verified digest of the bytes the executor cell checked before it decoded anything. |
model.providers |
The session's actual provider assignment, in the order the runtime reports it — never the configured preference. |
model.gpu |
{deviceId, class}, or null on a CPU session. |
decision.outcome |
CLEAR (the rules evaluated and passed), HOLD (they did not pass, or could not be evaluated), FAIL (the inference itself failed). |
outputs |
Only the declared task family's collection is populated. Masks and tensors are never published; segmentation reports pixel counts and regions. |
outputs.truncated |
Whether collections were bounded to fit the message budget. When it is true, artifacts names the sidecar holding the full result. |
artifacts |
The evidence sidecar this result belongs to, present whenever the route writes one. |
error |
{code, message, class} on a failure. class is transient, permanent, or contaminating. A permanent failure repeats on every attempt; code says whether the image or the deployment caused it, and that is what decides the completion action. |
A failed result is still published, because a consumer that hears nothing cannot tell a held image from a component that stopped:
{
"schemaVersion": "1.0",
"inferenceId": "…",
"status": "FAILED",
"decision": { "outcome": "HOLD", "pass": false, "confidence": null, "threshold": null, "rule": "DECODE_FAILED" },
"error": { "code": "DECODE_FAILED", "message": "the image is truncated", "class": "permanent" },
"…": "…"
}A trigger route accepts one image per message, in one of two forms.
| Body | Handling |
|---|---|
An inline image: an opaque binary body, or a structured body whose image field is bytes |
Bounded by the core envelope's 64 KiB binary-body cap. The bytes are hashed, written into processor-owned staging under a digest-derived name, and from then on the job is an ordinary file job. |
A file reference: {"relativePath": "…", "sha256": "…", "bytes": n} |
relativePath resolves under the route's fileRoot with containment enforced, and sha256 and bytes are verified against the file before the job is admitted. |
Anything that is neither is refused with a stable reason rather than guessed at:
MALFORMED_BODY, EMPTY_BODY, INLINE_TOO_LARGE, NO_FILE_ROOT, SIZE_MISMATCH,
DIGEST_MISMATCH, CHANGED_DURING_READ, or a path-containment code.
When the request envelope carries reply_to, the route publishes the bounded result summary there
under the request's correlation id, in addition to the normal outputs.
Each configured outputs.decisionSignals entry publishes a SouthboundSignalUpdate on
ecv1/{device}/image-processor/{routeId}/data/{signalId}, with value read out of the committed
result body by JSONPath:
{
"signal": { "id": "line-clearance/pass" },
"samples": [{ "value": true, "quality": "GOOD", "serverTs": "2026-08-23T12:13:05.710038Z" }]
}The mirror is best effort and never cleanup-gating. A path that resolves nothing publishes nothing,
a path that resolves a document rather than a scalar publishes nothing, and a failed publication is
counted rather than retried. A reading derived from a FAILED result carries quality: BAD.
Severity derives the channel, so the topic and the body can never disagree. A condition that is a state rather than an occurrence is raised and cleared as an alarm, and only on a transition.
| Type | Severity | When |
|---|---|---|
model-staging-failed |
critical | A bundle could not be fetched, verified, extracted, or interpreted. |
model-warmup-failed |
critical | A staged bundle failed its golden warmup, so no route switched to it. |
model-activated |
info | A route switched to a new model generation. |
executor-unavailable |
critical (alarm) | No healthy executor cell can serve the routes that need one. |
executor-recycled |
warning | A cell was drained and restarted. |
route-degraded |
critical (alarm) | An enabled, unpaused route cannot execute. context.reason names the cause: the staging, warmup, or activation failure of its model when it has one, otherwise the route's most recent condition. |
queue-age-exceeded |
warning (alarm) | The oldest queued job passed scheduler.queueAgeWarningSecs. |
publish-backlog |
warning (alarm) | The outbox is approaching publish.outboxCapacity. |
publish-exhausted |
critical | A result spent publish.maxAttempts. The input is retained for an operator retry. |
evidence-failed |
critical | The evidence sidecar could not be installed, so nothing was committed. |
cleanup-failed |
critical | A completion action failed. The job is CLEANUP_FAILED, never success. |
input-rejected |
warning | An input can never be admitted as it stands. |
inference-failed |
critical | An inference ended without a result. Its decision is HOLD. |
disk-pressure |
warning (alarm) | The state or cache filesystem is low on free space. |
gpu-pressure |
warning (alarm) | A device cannot admit a model a route needs. |
Success is not an event. Every context value is a bounded scalar: no image bytes, no tensors, no credentials, and no unbounded model output reach this class.
A request is a cmd envelope on ecv1/{device}/image-processor/{instance}/cmd/{verb} whose
header.name equals the verb. A request carrying header.reply_to gets a reply there under its
correlation id: {"ok": true, "result": {…}} or
{"ok": false, "error": {"code": …, "message": …}}. A request without reply_to is
fire-and-forget.
ec-uns-cmd builds the envelope for you:
ec-uns-cmd --device smoke-device --component image-processor get-queue --body '{"max": 20}'
ec-uns-cmd --device smoke-device --component image-processor --instance clearance-cam-01 pauseping, describe, reload-config, get-configuration, and status. status answers with this
component's per-route connectivity — the same sample the state keepalive pushes, so a pulled
answer can never disagree with a pushed one:
{
"status": "RUNNING",
"uptimeSecs": 82,
"instances": [
{
"instance": "clearance-cam-01",
"connected": true,
"state": "ONLINE",
"detail": "/var/spool/camera-adapter/cam-01",
"attributes": {
"desiredGeneration": "sha256:4a87394f…",
"activeGeneration": "sha256:4a87394f…",
"sourceReachable": true,
"executorHealthy": true,
"queued": 0,
"oldestAgeSecs": 0.0,
"paused": false
}
}
]
}state is ONLINE, STAGING (configuration is ahead of the running generation; the route keeps
serving the last known good model), DEGRADED (it cannot decide right now), or DISABLED.
| Verb | Scope | Request body | Reply |
|---|---|---|---|
get-models |
component | {cursor?, max?} |
{models: [{id, version, digest, uri, staged, warmed, warmupSamples, loadMs, deviceMiB, activeRoutes, stagingRoutes, rollback, error}], nextCursor, total} |
get-queue |
both | {route?, states?, cursor?, max?} |
{route, jobs: [{inferenceId, route, state, attempts, source, model, lastError}], nextCursor, counts, scheduler} |
trigger-rescan |
both | {route?} |
{route, discovered} |
preload-model |
component | {id?, digest?} |
deferred: {id, version, digest, staged, warmed, routesSwitched} |
evict-model |
component | {digest} |
{evicted, digest, cells, reason}; a leased generation is CONFLICT |
reload-model-catalog |
component | {} |
deferred: {routesSwitched, requeued, collected, models} |
set-route-activation-override |
instance | {enabled} — true, false, or null to clear |
{route, configured, override, effective} |
retry-publication |
both | {route?, inferenceId?} |
deferred: {returned, published} |
retry-cleanup |
both | {route?, inferenceId?} |
deferred: {repaired, stillFailed} |
reconcile |
both | {route?} |
deferred: {reconciled, counts} |
pause |
both | {route?} |
{paused: true, routes} |
resume |
both | {route?} |
{paused: false, routes} |
A cell loads one model at a time, so preload-model refuses a load while the routes are busy and
the command returns the failure. Retry it when the routes are idle or paused.
reload-model-catalog re-verifies every configured model against the cache. A route whose entry
holds up is a route whose configuration no longer blocks work, so the jobs it stranded in
BLOCKED_CONFIGURATION return to READY and run again; requeued counts them.
A component-scope verb addressed to an instance is refused before the handler runs, and so is an instance-scope verb that names no route on a component with several. A slow verb takes a deferred reply rather than blocking the inbox: the request is accepted immediately and the reply arrives when the work finishes, within the core's 31-minute bound.
Pagination is an opaque cursor and a max (default 100, ceiling 500). A reply never grows with
how busy the component has been.
| Code | Meaning |
|---|---|
BAD_ARGS |
The arguments are unusable: a max that is not a positive integer, a cursor that is not a string, a state name that is not one, an enabled that is not a boolean or null, or a verb addressed to the wrong scope. |
NOT_FOUND |
The request names a route or a model this component does not have. |
CONFLICT |
The request is well formed but cannot be honoured now — an eviction of a generation still leased by draining work. |
OPERATION_FAILED |
A deferred operation ran and failed. The message carries the stable code from the subsystem, such as DIGEST_MISMATCH or WARMUP_FAILED. |
UNKNOWN_VERB, HANDLER_ERROR, RELOAD_FAILED, NO_CONFIG |
The library's own codes. |
metric and state are automatic and library-owned. See
Reference — Metrics for the eight groups and their measures; the state keepalive
carries the same instances[] array status returns.
| Flag | Values | Notes |
|---|---|---|
--platform |
GREENGRASS | HOST | KUBERNETES | auto |
Default auto. |
--transport |
MQTT [path] | IPC |
HOST and Kubernetes use MQTT; the path is the messaging config. |
-c/--config |
FILE <path> | ENV | GG_CONFIG | SHADOW | CONFIG_COMPONENT | CONFIGMAP |
Default from the platform profile. |
-t/--thing |
<name> |
IoT Thing name; the {device} token of every UNS topic. |