Architecture¶
The stack is split in two. The northbound side faces applications: it speaks CAMARA, holds the identity of assets and decides what each consumer may see. The southbound side faces the sources: each adapter speaks its source's language and translates it into one contract. A southbound component conforms to contracts it does not define. Vendor knowledge exists only in the adapters.
Services¶
| Service | Responsibility | Holds |
|---|---|---|
camara-gateway |
CAMARA retrieval and verification, the position stream, the profile's extension endpoints. Validates the JWT, resolves an asset to its capabilities, fuses them, applies the tenant scope. | the asset map |
positioning-engine |
For one positioning id: routes to the adapter, places each measurement in the venue frame, fuses, converts to WGS84. Broadcasts positions over a WebSocket. | the blueprint, the adapter registry |
wifi-adapter |
Multilaterates WiFi RSSI scans posted by an edge scanner. | the WiFi bindings (BSSIDs, calibration) |
vendor-adapter |
Translates a vendor's REST positioning API, described by a schema document. | the vendor schema |
synthetic-adapter |
Generates devices that walk inside the first room of the blueprint. Used for demonstrations and tests. | nothing |
placement-editor |
Operator tool to author the blueprint. Behind oauth2-proxy. |
nothing, it reads and writes the engine's blueprint |
location-app |
A CAMARA consumer. Talks to the gateway only. | nothing |
Each stored document has one service that holds it and serves it over HTTP. No service mounts another's file. Keycloak issues the tokens. The edge WiFi scanner runs outside the cluster and posts scans over the 5G data network.
Retrieving a position¶
sequenceDiagram
autonumber
participant App as CAMARA client
participant GW as camara-gateway
participant ENG as positioning-engine
participant AD as adapter
App->>GW: POST /location-retrieval/v0.5/retrieve { device.assetId, maxAge }
GW->>GW: validate JWT and role, resolve assetId to capabilities, check org
loop each capability (source, positioningId)
GW->>ENG: GET /position/{positioningId}?source={source}
ENG->>AD: GET /measurement/{positioningId}
AD-->>ENG: Measurement, or 404
ENG->>ENG: place in venue frame, fuse, convert to WGS84
ENG-->>GW: EnginePosition { establishedAt, current, … }, or 404
end
GW->>GW: fuse the capabilities, apply maxAge
GW-->>App: Location { lastLocationTime, area, … }
Routing in the engine. The gateway passes the capability's source, and
the engine asks the adapter registered under that name. When no adapter is
registered under it, for example while that adapter restarts, the engine
answers 503: the source is unavailable, and no other adapter is asked about
its device. A request without a source consults the optional DEVICE_MAP pins,
and otherwise asks every registered adapter and fuses the answers. An adapter
answers 404 for a device it does not serve. See
adapter registry.
Fusion at two levels. The engine fuses the measurements of one positioning
id, weighting each by confidence / accuracy². The gateway fuses the positions
of an asset's capabilities by inverse variance. A capability without a position
is skipped, so an asset stays located while one of its sources answers. See
fusion strategies.
Failures. Each hop waits less than its caller: the vendor-adapter waits its
schema's requestTimeout for the vendor, the engine waits the adapter's
declared responseTimeout, and the gateway waits 5 s for the engine. The
engine answers 404 only when the source answered without a fix. An adapter
that does not answer within its budget gives 504, and one that fails
otherwise gives 503. The gateway retries the engine once after 200 ms on a
5xx other than 504, or on a network error. An engine 404 becomes
422 UNABLE_TO_LOCATE, 504 becomes 504 TIMEOUT, 503 and an unreachable
engine become 503 UNAVAILABLE, any other persistent 5xx becomes 502. Positions are cached per
positioning id for LOCATION_CACHE_TTL_S (5 s by default).
The position stream¶
The engine broadcasts the positions it knows every WEBSOCKET_INTERVAL_MS
(500 ms by default) on /ws/positions, keyed by positioning id. It learns the
ids to broadcast from the adapters that serve GET /devices. A tick waits at
most one interval: a device whose source is slower joins the frame of the tick
in which its answer arrives, without holding back the others. The gateway
accepts clients on /positions/stream, opens one engine connection per client,
and turns each batch into asset events: it groups the ids by asset, fuses
multi-capability assets, drops ids with no asset and assets outside the
client's organisation. The token travels in the Sec-WebSocket-Protocol
header. The message format is published as
AsyncAPI.
Coordinate frame¶
The blueprint places each level in its parent: the floor plan in the world, each room in the floor plan, anchors and walls in the room. Every level uses the local frame of omlox:
- origin at the lower-left corner of the parent level,
- x in metres along the width (
width_m), - y in metres along the depth (
depth_m), - z in metres up, the height above the floor.
A room sits in its floor plan at its lower-left corner (x_m, y_m), rotated
by rotation_deg clockwise about its centre. The floor plan sits in the world
through its georef: the WGS84 position of its origin, azimuth_deg for the
bearing of +y from true north, and altitude_m for the height of the origin
above the WGS84 ellipsoid.
An adapter reports in a room, in the venue (the floor plan) or in WGS84. The
engine places every measurement in the venue frame before fusion and converts
the result to WGS84, so the gateway handles no geometry. altitude is the
origin's altitude_m plus the fused height, present only when both exist.
Without a georef the engine returns latitude and longitude 0 and logs a
warning. The editor and the location-app draw in screen axes and convert at
their edge. See georeferencing for how the georef is
surveyed.
Time¶
A position carries two times. The fix time is when the source computed it. The
established time is the latest moment the position is known to hold. The two
differ for a source that reports on motion: a still tag keeps an old fix, and
its later communications confirm it. Each adapter declares how its source
reports (reporting, reportingInterval), and the engine derives the
established time and whether the position is current from that declaration.
CAMARA lastLocationTime is the established time, and maxAge is judged on
it. See adapters and the
profile.
Relation to the 3GPP location architecture¶
In 3GPP an application reaches the network's location service through the NEF, which asks an LMF, which uses the radio access network:
The testbed runs Open5GS, which has no NEF or LMF. The stack takes their places:
| 3GPP role | Here | Note |
|---|---|---|
| Application | location-app, any CAMARA client |
|
| CAMARA gateway and NEF | camara-gateway |
CAMARA surface, identity, authorisation, fusion of an asset's capabilities |
| LMF, selection and hybrid combination | positioning-engine |
routes to a source, fuses, owns the venue georeference |
| LMF, one positioning method | each adapter | the engine cannot tell how a fix was produced. vendor-adapter bridges a solver that runs in the vendor's cloud |
| RAN and UE measurements | POST /ingest/wifi-scan on wifi-adapter |
the only path that carries raw measurements. The adapter contract carries computed fixes |
| AMF and SMF session state | not used | assets are addressed by assetId, not by subscriber or session |
The gateway and the engine can be split into separate NEF and LMF services without changing what a consumer sees.
Deployment¶
This repository builds the images. The companion repository
kelt holds the Kubernetes manifests and
runs the testbed. See deployment.