Skip to content

System Architecture

One X-Edge AI box runs a self-contained three-service stack. Everything — configuration, model versions, history, logs — lives on the box; no cloud dependency exists at runtime.

flowchart LR
  subgraph Box["X-Edge AI Box (Docker)"]
    FE["Frontend\nnginx + React"] -->|REST + SignalR| BE["Backend\nASP.NET orchestration"]
    BE -->|gRPC :50051| INF["Inference runtime\nPython + ONNX Runtime"]
    BE --- DB[("SQLite\nconfig · history · logs")]
    PLUG["Datasource plugins\nOPC UA · MQTT · CSV"] --- BE
  end
  SENSORS["PLCs · brokers · devices"] --> PLUG
  PLUG -->|predictions written back| SENSORS
  USER["Operator browser"] --> FE
ServiceRolePort
FrontendDashboard UI, served by nginx80 (443 with TLS)
BackendOrchestration: datasources, models, inference lifecycle, auth, persistence5000
InferenceONNX model execution (CUDA on the Jetson TX2, CPU fallback)50051 (gRPC)

Industrial protocols are implemented as plugins loaded by the backend at startup. Each plugin describes its connection settings as a typed schema, which the UI renders as a dynamic form — adding a protocol never requires a new UI build.

  • Inputs (read path): OPC UA (polled every tick), MQTT (push with staleness guard), and CSV replay for testing.
  • Outputs (write path): OPC UA, MQTT, and rotating CSV files. An output tag’s address is used verbatim (an MQTT topic, an OPC UA NodeId, a CSV column), and each value is scaled as value × scale + offset.

The two paths fail differently on purpose: a dead input tag halts inference visibly (feeding a model stale data is the worst failure mode for anomaly detection), while a failing output sink never stalls inference — it raises a coalesced Output write failed notification instead.

A flow binds one input datasource, one model version and zero or more output datasources. The flow is what you enable, and at most one flow is enabled at a time. Several flows can share an input, so you can keep variants ready and switch between them. See Flows.

flowchart LR
  IN["Input datasource\nOPC UA · MQTT · CSV"] --> M["Model version\n(enabled flow)"]
  M --> OUT["Output datasources\n0..N"]
  M --> DASH["Dashboard · history"]

The enabled flow’s input feeds its model:

  1. The backend samples mapped tags at the datasource’s sampling rate and streams them to the inference runtime over gRPC.
  2. The runtime maintains a sliding window of window size × tag count samples; once full, every new sample produces a prediction (health_score, failure_probability, rul_normalized).
  3. Predictions flow back and fan out to the dashboard (SignalR), the history store, the flow’s output sinks, and the on-box SQLite audit trail.

Throughput therefore scales with the input’s sampling rate, not with the publisher’s rate — a 100 Hz sensor sampled at 10 Hz yields 10 predictions/s.

Backpressure is per-consumer: slow dashboards are never allowed to throttle inference (their events drop first), output sinks get a bounded lossless queue that slows the pipeline end-to-end when a sink can’t keep up, and non-finite predictions (NaN/Inf) are filtered and counted. All of these are visible as counters — see Monitoring.

Models are authored off-box with modelctl (convert → validate → quantize → package), then uploaded through the UI. Upload validates the ONNX contract via the inference runtime (shapes + SHA-256) without loading it; the model only enters the engine when a flow that uses it is enabled. Loading is a zero-downtime hot swap, and versions are immutable — re-uploading a changed file under the same version is rejected. Rollback re-activates the previous valid version.

Live dashboard updates (sensor batches, predictions, health, state changes, notifications) travel over a SignalR WebSocket hub. This stream is fire-and-forget and in-memory — it is never the source of truth. If a browser disconnects, missed events are not replayed; authoritative state always comes from the REST API and the on-box database.

LayerPurposeBound
Container stdout (docker logs)Live debugging10 MB × 3 files per service
Rolling file on a persistent volume (Warning+)Post-mortem forensics — survives container recreation and database corruption10 MB × 7 (backend) / × 6 (inference)
SQLite log storeThe LogViewer page and notification alerts10,000 entries, swept every 60 s

Per-tick pipeline lines are logged at Debug level; the default Info stream carries lifecycle events, heartbeats, and warnings only.

Admins can export the full inference configuration — input and output datasources with their tag mappings, and the flows binding them to models — as one versioned JSON envelope, and re-import it on another box. Flows reference their datasources and model by name, so a file carries no database Ids.

Secrets are redacted on export and must be re-entered on the target. Imports run in a single transaction: any per-item failure rolls the whole file back and returns the complete error list, while an item skipped (most often a flow whose model is not on this box) is reported and the rest commits.

An import that would rewrite a datasource the enabled flow is using is refused before anything is written, naming the flow.