Flux Architecture
Flux is the application boundary around Ignition-facing tooling, runtime data capture, and development environment simulation.
System Shape
Ignition Production -> WebDev -> fluxy -> Flux.serve -> Flux.opt -> Flux.spot / Flux.chart / Flux.web
Ignition Development -> WebDev -> fluxy -> Flux.sim -> Flux.base -> Flux.serve -> FieldAgent processes
Flux.web -> Configurator -> Flux.sim / Flux.base / Flux.serve
Flux.Deep -> OpenPLC runtime artifacts
Boundaries
- Flux is Linux-exclusive runtime software. Supported operation assumes Linux,
bash, systemd/user-systemd service ownership, Gunicorn for Django serving, and
Linux-compatible OpenPLC/FieldAgent/QuestDB process control. Windows, PowerShell,
.cmdlaunchers, Windows Services, and Waitress are intentionally unsupported. fluxy/: Python-for-Ignition client, WebDev deployment tooling, and Ignition expression/tag export utilities.field/: FieldAgent OPC UA adapter executable. It is not a Flux domain owner;Flux.servestarts one FieldAgent adapter process for each materialized runtime device.sim/: standaloneFlux.simcore for importing Ignition tag exports, reconstructing provider models, flattening OPC requests, and generating simulation/FieldAgent configuration.deep/: isolatedFlux.Deepcore for OpenPLC-backed PLC emulation experiments. It starts with Logix L5X source intent and OpenPLC IEC 61131-3 runtime targets.web/Flux/: Django/HTMX UX, admin, status, configuration, and worker-control surface. It should adapt core packages, not own their core logic.scripts/flux: operator CLI for local service control, health checks, and managed Ignition dev-cell commands.
Django Role
Django is the UX and configurator layer:
flux.spot: lightweight DB-backed current-state visualization.flux.chart: power-user charting, historical exploration, and live trace trials over recorded runtime samples.flux.trace: chart/history persistence models backing Flux.chart until a planned schema migration.flux.opt: tag browse optimizer and runtime read planning.flux.serve: worker/service orchestration UX and adapters.flux.base: persistent datastore, including simulation catalog rows, materialized runtime device/tag config, and FieldAgent endpoint config.flux.sim: simulation UX and adapters over rootsim/.
Core import, flattening, expression, server generation, and OPC UA simulation logic should stay outside Django.
Flux.Deep Path
Flux.Deep is intentionally isolated from the Ignition-facing simulation path:
- Keep Logix L5X or related PLC source artifacts as source intent.
- Translate or target OpenPLC-compatible IEC 61131-3 artifacts for execution.
- Use OpenPLC as the backend runtime for PLC emulation trials.
- Add bridges back into Flux.web or Flux.serve only after the runtime adapter contract is proven.
The seed workspace is deep/examples/hello_world/. It includes a Logix ladder L5X
file and an OpenPLC Structured Text target that alternates DisplayText between
hello and world on one second cycles.
Simulation Path
The development replication path is:
- Import an Ignition tag provider export into a standalone
simdatabase. - Reconstruct the provider tree and UDT structures.
- Use
fluxy.ignition_expressionto resolve UDT parameter bindings and flatten OPC requests. - Store the desired simulated devices and tags as
Flux.simcatalog configuration. - Materialize enabled simulated devices and tags into
Flux.baseruntime FieldAgent endpoint/device/tag rows. - Use
Flux.serveto supervise one FieldAgent OPC UA adapter process per enabled materialized runtime device. - Use
fluxythrough WebDev to configure the Ignition development gateway with matching OPC UA connections and tags.
Production Path
The production path is:
Flux.serveruns worker servicing against production Ignition throughfluxyand WebDev.Flux.optcontrols browse/read planning and reduces inefficient tag IO.- Runtime values are persisted into Flux runtime storage.
Flux.spot,Flux.chart, andFlux.webrender from Flux storage instead of causing browser-driven Ignition tag IO.
Flux.chart reads runtime.TagSample and renders uPlot charts from stored samples. The historical chart supports pinned chart markers, a copied Markdown marker-value table, and prompt-based annotations. The streaming chart polls new samples and only follows the newest right edge when the user is already viewing that edge; panning back preserves the inspected viewport while new samples continue to merge. Chart JavaScript architecture is documented in docs/charts-architecture.md.
Live-to-sim extraction is documented in docs/live-extraction.md. The current trial stays at the Fluxy public API boundary for tag/config/history reads and writes. Raw historian deletion is intentionally deferred to database-specific cleanup adapters because Ignition does not expose a public delete-data-points API.
Runtime Health Contract
Flux runtime health is a cached-state contract, not a browser read loop.
Flux.serve worker -> Flux.opt block read -> LatestTagValue / TagSample -> Flux.spot / Dashboard
Ownership:
Flux.servesupervises long-running samplers and writes heartbeats/snapshots.Flux.optchooses due tags, honors active demand, performs block reads, and writes runtime samples.Flux.spotdefines freshness and current-state presentation.Flux.webrenders cached state and may poll small cached fragments.
Current gap: the dashboard can refresh selected stale tags from a request path, and the Fluxolot sampler is explicitly started by flux start, but the general interface-health sampler should be promoted to a required service when interface runtime tags exist.
Target contract: page reloads or HTMX polls may update what the user sees, but they must not be the mechanism that makes runtime health fresh.
OPC Runtime Truth Contract
FieldAgent endpoint rows need composed evidence before the UI claims they are running.
Minimum evidence:
- desired endpoint state from
FieldEndpoint - fresh
FieldAgentHeartbeat process_id- endpoint URL and derived/listening port
- fresh
ServeServiceSnapshot - optional OS/TCP probe from the service layer
FieldEndpoint.status == running alone means “last persisted endpoint state,” not proven current process truth. If heartbeat or snapshot evidence is stale, UI surfaces should say stale or last reported running and show the reason.
Local Service Boundary
Local development runtime is owned by a Linux user systemd service:
flux-stack.service -> scripts/flux-start.sh -> Django + FieldAgent + Fluxolot sampler
The CLI and desktop launchers start/stop the service. Django should not directly own long-lived OS processes from request handlers.
See docs/operator-guide.md.
Health Utility Boundary
Health checks are split by responsibility:
scripts/flux: operator-facing CLI, shell/service/process checks, output formatting, and fix suggestions.dashboard.management.commands.flux_doctor_state: JSON app-health bridge for the CLI.dashboard.services: runtime readiness, Live Ignition Bridge config, stale-tag classification, and block-read recovery.fluxy: Ignition Gateway, WebDev, and datasource probing.
This keeps flux doctor useful without turning the CLI into an untestable copy of Django business logic.
FieldAgent Runtime Adapter Contract
FieldAgent is the concrete OPC UA runtime adapter below the Flux architecture, not a separate Flux module with domain ownership.
Flux.simowns device and tag domain configuration: provider import, reconstructed trees, UDT binding flattening, simulated device identity, and simulated tag behavior intent.Flux.basepersists both the desired catalog state and the materialized runtime endpoint/device/tag state that can be supervised.Flux.serveowns process supervision and starts one FieldAgent process for each enabled materialized runtime device.- FieldAgent loads the generated per-device runtime config and serves OPC UA for that one device process.
Flux.webconfigures and displays the catalog/materialization/supervision state; it should not directly own long-lived FieldAgent processes from request handlers.fluxyconfigures Ignition through WebDev by creating OPC UA connections and matching Ignition tags that point at the supervised FieldAgent endpoints.
This resolves the earlier implicit Flux.field boundary: the thing running under Flux.serve is FieldAgent as an adapter process servicing Flux.sim.device, while Flux.sim remains the device/tag domain owner.
Current limitations:
FLUX_FIELD_AGENT_MODE=legacystill exists and runs one FieldAgent fromweb/Flux/field/field-config.json; it is a compatibility/operator path, not the target architecture.- Supervised mode writes runtime configs under
.runtime/field-agent, but process lifecycle is still local-development oriented underflux-stack.service. - Device-level delay metadata is materialized, but the current C# FieldAgent runtime does not yet apply request-level delay behavior.
- Full-provider simulations can generate large FieldAgent/Ignition configurations; use small closed-loop trials before scaling to the full ACM02 provider.
- Ignition cleanup is generated-folder and generated-connection oriented; broad historical or raw gateway cleanup remains adapter-specific and intentionally outside the simulation boundary.
Next tests:
- Closed-loop device lifecycle: create a test-specific
Flux.sim.device, materialize it, start the supervised FieldAgent process, configure Ignition throughfluxy, read a test tag, confirm changing values, delete the Ignition tag/folder, verify gone, then remove the runtime device. - Multi-device supervision: materialize two enabled devices and verify
Flux.servestarts two distinct FieldAgent processes with distinct endpoint configs. - Restart recovery: restart
flux-stack.servicein supervised mode and verify materialized enabled devices are restored without manual config regeneration. - Device behavior: prove
slow_networkonce FieldAgent consumes device delay metadata. - Scale smoke: run a limited preserved-tree ACM02 trial before attempting the full
tags02.jsonconfiguration.
Configurator Contract
The configurator coordinates three execution domains:
Flux.sim: owns desired simulated environment, device/tag catalog configuration, and generated tag artifacts.Flux.base: owns persistence for desired catalog state and materialized runtime endpoint/device/tag configuration.Flux.serve: owns worker process plans, FieldAgent adapter supervision, and health/status.
The configurator should persist intent and show status. It should call package APIs or CLIs, not duplicate their internals.