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 on CPU/CUDA/TensorRT50051 (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.

One enabled input datasource feeds one paired 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, configured output sinks, and the on-box SQLite audit trail.

Throughput therefore scales with sampling rate × datasources, 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 its datasource 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.