Asset registry¶
The asset map is the list of assets the gateway knows. An asset has no phone
number. It is named by an assetId the organisation chooses, such as
pkg-4471 or forklift-7, and carries one or more capabilities, each a way of
locating it. The gateway holds the map, serves it on GET /assets, replaces it
on PUT /assets, and persists it on its own volume.
The document¶
The contract is
schema/asset.schema.json,
version 4. The document is {"version": 4, "assets": [...]}.
| Asset field | Required | Content |
|---|---|---|
assetId |
yes | ^[A-Za-z0-9._:-]{1,128}$, the CAMARA device.assetId |
kind |
yes | the asset class, published as profile kind |
org |
yes | ^[a-z0-9-]{1,64}$, the organisation that owns the asset, matched against the token's org claim |
capabilities |
yes | one or more {source, positioningId} |
label |
no | a name for user interfaces |
metadata |
no | a free object, such as floor or bay |
| Capability field | Content |
|---|---|
source |
the name of the adapter that serves this capability |
positioningId |
^[A-Za-z0-9._:-]{1,128}$, the id that adapter knows the asset by |
A physical thing is a different id to each source, so each capability carries
its own positioningId. The seed in
dev/assets.json
has a single-capability pallet and a robot located by WiFi and UWB:
{
"version": 4,
"assets": [
{
"assetId": "pkg-4471",
"kind": "pallet",
"org": "acme",
"capabilities": [{ "source": "wittra", "positioningId": "wittra-tag-01" }],
"label": "Wittra tag 01",
"metadata": { "floor": 0, "note": "Timber bundle" }
},
{
"assetId": "robot-2",
"kind": "forklift",
"org": "acme",
"capabilities": [
{ "source": "wifi", "positioningId": "wifi-asset-02" },
{ "source": "wittra", "positioningId": "wittra-tag-02" }
],
"label": "Mobile robot 2"
}
]
}
For each capability of robot-2 the gateway asks the engine for the position
of that positioningId from that source, then fuses the answers into one
CAMARA Location (fusion strategies). The first
capability is the primary one. Device diagnostics and placement use it, and the
position stream and the asset details report its positioningId and source
beside the fused position.
Checks on write¶
PUT /assets refuses a document that breaks one of these rules, with 422:
| Code | Rule |
|---|---|
DUPLICATE_ASSET_ID |
an assetId appears once in the whole map, across organisations, since a request names the asset by assetId alone |
DUPLICATE_POSITIONING_ID |
a positioningId appears once in the whole map, since the position stream maps it back to one asset |
UNKNOWN_SOURCE |
a source is served by an adapter registered with the engine |
A source the stored map already uses is accepted, so the assets of an adapter
that is down stay writable. When the engine cannot be reached, the source check
is skipped. kind is not checked: it names the object, and the site chooses
it. The checks run on write only: a stored map
is not validated again when it is read.
Writing the map¶
PUT /assets replaces the whole map. Read it, change it, write it back:
curl -s -H "Authorization: Bearer $JWT" "$GW/assets" > assets.json
# edit assets.json
curl -s -X PUT -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
--data @assets.json "$GW/assets"
Only an operator token, which carries no org claim, writes. A token with an
org claim receives 403 PERMISSION_DENIED. The KELT dashboard writes the map
with its operator token.
The gateway keeps the map in ASSET_STORE_FILE (/app/data/assets.json),
which belongs on a persistent volume. When that file is absent at start, the
gateway copies ASSET_SEED_FILE (/app/config/assets.seed.json) into it once.
Without either, it starts with an empty map.
Onboarding discovered devices¶
An adapter that advertises the devices capability lists the devices its
source knows on GET /devices. The engine merges these lists, and the gateway
serves on GET /assets/discoverable the devices whose id is not yet a
positioningId in the map. The list spans every organisation, so only an
operator token reads it. The KELT Assets page offers each candidate for
onboarding: the candidate's id becomes a positioningId, its source the
capability's source, and the operator adds assetId, kind, org and
label. Nothing is onboarded without a PUT /assets.
| Candidate field | Content |
|---|---|
id, source |
the device id and the adapter that reports it |
origin |
inventory for a list the vendor maintains (vendor-adapter), observed for a device seen in recent traffic (wifi-adapter) |
role |
asset or infrastructure, when the source classifies its devices |
sourceClass |
the positioning technology, when the source states it |
deviceType, label |
as the source reports them |
lastCommunicationTime |
when the device last communicated with its source, RFC 3339 |
A device with role: infrastructure is a fixed sensor, such as a UWB anchor or
a gateway: it feeds positioning and is not an asset. A source that does not
classify leaves role out. The vendor-adapter classifies from its schema's
classify block
(the device list). The
synthetic-adapter reports its walking tags as assets and its anchors as
infrastructure.
Organisations¶
GET /assets returns only the assets of the token's org. For an asset of
another organisation, GET /assets/{assetId}/details and the CAMARA endpoints
answer 404, as for an asset that does not exist.
The map lists an organisation's assets, so a real map is never committed. The
repository holds only the placeholder dev/assets.json.