Skip to content

arcnode-io/ems-device-api

Repository files navigation

EMS Device API 🌳

📖 About

The API manages the canonical DTM (Device Topology Manifest) per ADR-002 §7 — a flat dict of devices keyed by snake_case slug, each with an optional parent: device_id pointer. Hierarchy depth is unbounded; modules contain racks, racks contain BMS/inverter/cells, etc. Clients fetch GET /topology and walk parent chains client-side.

The AsyncAPI v3 spec generated by this service is the single contract consumed by ems-industrial-gateway, ems-line-controller, and ems-hmi. Topic shape, payload schemas, unit vocabulary, and versioning are governed by ems/topic_structure_adr.md.

Diagrams

rectangle "compute_module_1\n(parent: null)" as cm
rectangle "grid_module_1\n(parent: null)" as gm
rectangle "bess_module_1\n(parent: null)" as bm
rectangle "gpu_node_1\n(parent: compute_module_1)" as n1
rectangle "cdu_1\n(parent: compute_module_1)" as cdu
rectangle "switchgear_1\n(parent: grid_module_1)" as sw
rectangle "bess_rack_1\n(parent: bess_module_1)" as br
rectangle "bess_cell_1\n(parent: bess_rack_1)" as bc

cm -d-> n1
cm -d-> cdu
gm -d-> sw
bm -d-> br
br -d-> bc

note right of bm
  Parent chains arbitrary depth.
  Clients fetch GET /topology and
  walk device.parent client-side.
end note

Topology Creation Flow

participant client
participant device_api
database typeorm_db

client -> device_api: POST /topology
device_api -> typeorm_db: save device sitemap
device_api -> device_api: generate AsyncAPI v3 spec\n(topics + schemas + x-* protocol bindings)
device_api -> typeorm_db: save spec
device_api -> client: topology created

Spec and Topology Consumers

participant device_api
participant industrial_gateway
participant ems_hmi

device_api -> industrial_gateway: GET /asyncapi\n(topics + x-modbus, x-snmp, x-connection)
device_api -> ems_hmi: GET /asyncapi\n(topics + message schemas)
device_api -> ems_hmi: GET /topology\n(devices + buses[])

GET /asyncapi and GET /topology serve different purposes for the HMI:

  • GET /asyncapi — messaging contract: measurement/command vocabulary per device, payload schemas, x-severity on enum values, enum:[...] constraint on each enum channel's value field (sourced from the class values: keys), poll rates. Used for TypeScript codegen at build time and topic subscription at runtime.
  • GET /topology — device catalog: the full DTM (devices + buses[]). Used to populate the module browser, derive module type for routing (bess_module.*/modules/bess/:id), and render the SLD from buses[].

The gateway only needs /asyncapi. The HMI needs both.

Build-time vs runtime

The spec serves two purposes at different times:

  • Build-time: Message schemas (e.g. BooleanReading, FloatReading, CommandPayload) are stable across all deployments. The HMI runs @asyncapi/modelina against the spec to generate TypeScript interfaces. These are compiled into the app. The gateway does the same for Rust structs.
  • Runtime: Topic paths (e.g. arcnode/{deployment_uuid}/{module_id}/{device_uuid}) are deployment-specific — they depend on which modules and devices are in the DTM. These cannot be compiled in. Both gateway and HMI fetch GET /asyncapi at startup and resolve topic paths dynamically.

Message types are compiled. Topic paths are fetched.

When the topology changes at runtime (device added, sensor goes offline, DTM re-provisioned), device-api publishes to system/topology-changed. Both the gateway and HMI subscribe to this topic — on receipt they re-fetch GET /asyncapi and diff against their current subscriptions.

Day-1 Boot

Per ADR-003, the API fetches a DTM from S3-compatible storage at startup. One mechanism, four deployment recipes — only the endpoint URL changes.

Cloud (CFN + ECS Fargate)

Set in CFN parameters or task env:

bootDtmS3Url: s3://arcnode-artifacts/deployments/<deployment_id>/dtm.json
s3EndpointUrl: ~  # null → real AWS S3

Task IAM role grants s3:GetObject on the artifact bucket.

On-prem ISO appliance

Set:

bootDtmS3Url: s3://arcnode-artifacts/deployments/<deployment_id>/dtm.json
s3EndpointUrl: http://minio:9000

minio is already a daemon in the on-prem stack (per ems/readme.md On-Prem Deployment). Bake-time scripts populate the bucket before first boot.

Dev (docker-compose)

LocalStack at port 4566. Compose file should include LocalStack alongside Postgres:

services:
  localstack:
    image: localstack/localstack:3
    environment:
      SERVICES: s3
    ports:
      - "4566:4566"

Upload a sample DTM:

aws --endpoint-url=http://localhost:4566 s3 cp sample-dtm.json s3://arcnode-artifacts/deployments/sample/dtm.json

cfg.yml's local: block points s3EndpointUrl: http://localhost:4566 and bootDtmS3Url: ~ by default. Set the URL to enable the seed.

Tests / CI

Leave bootDtmS3Url: ~. Service boots empty; tests POST DTMs explicitly via tests/topology.test.ts patterns.

Behavior matrix

bootDtmS3Url Topology table Result
set empty fetch + seed
set populated fetch + skip seed (don't overwrite operator changes)
null empty graceful empty start
null populated graceful empty start

Any fetch/parse/validation/catalog error when URL is set → fatal exit.

Local Development Setup

The device_templates/ directory is bundled into the production image at build time from the sibling edp-api repo. For local dev, symlink:

ln -s ../edp-api/device_templates device_templates

Override the catalog root via env var if your layout differs:

TEMPLATE_CATALOG_ROOT=/path/to/device_templates npm run dev

API Endpoints

The API provides several endpoints to manage this structure:

  • POST /topology: Accepts the full DTM (devices + buses[]), persists it, and regenerates the AsyncAPI v3 spec. Called by platform-api at delivery time and on release rollout — not by the HMI.
  • GET /topology: Returns the full DTM — devices (slug-keyed dict, each with template, parent, connection, etc.) and buses[] (electrical bus topology). The HMI's primary source for the module browser and SLD rendering. Module kind is derived from the device's template ref via templates_used[slug].kind (leaf for terminal devices, module for aggregations).
  • GET /asyncapi: Returns the generated AsyncAPI v3 spec. Consumed by ems-industrial-gateway, ems-line-controller, and ems-hmi.
  • Device navigation: Clients fetch the full DTM via GET /topology and walk device.parent chains client-side. Per-device endpoints are deferred — current device counts (≤ 200) make full-load cheap (~15 KB gzipped) and avoid a second writer surface alongside POST /topology. ADR-002 §14 keeps per-device CRUD on the table for future when device counts cross ~1k or operator pain warrants.

Releases

No releases published

Packages

 
 
 

Contributors