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
| Service | Role | Port |
|---|---|---|
| Frontend | Dashboard UI, served by nginx | 80 (443 with TLS) |
| Backend | Orchestration: datasources, models, inference lifecycle, auth, persistence | 5000 |
| Inference | ONNX model execution on CPU/CUDA/TensorRT | 50051 (gRPC) |
Datasource plugins
Section titled “Datasource plugins”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.
Inference pipeline
Section titled “Inference pipeline”One enabled input datasource feeds one paired model:
- The backend samples mapped tags at the datasource’s sampling rate and streams them to the inference runtime over gRPC.
- The runtime maintains a sliding window of
window size × tag countsamples; once full, every new sample produces a prediction (health_score,failure_probability,rul_normalized). - 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.
Model lifecycle
Section titled “Model lifecycle”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.
Real-time eventing
Section titled “Real-time eventing”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.
Logging — three bounded layers
Section titled “Logging — three bounded layers”| Layer | Purpose | Bound |
|---|---|---|
Container stdout (docker logs) | Live debugging | 10 MB × 3 files per service |
| Rolling file on a persistent volume (Warning+) | Post-mortem forensics — survives container recreation and database corruption | 10 MB × 7 (backend) / × 6 (inference) |
| SQLite log store | The LogViewer page and notification alerts | 10,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.
Configuration import / export
Section titled “Configuration import / export”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.