Skip to content

feat(opc): read the live edge from the SCADA's OPC UA - #145

Merged
mrcsin merged 1 commit into
masterfrom
opc-live-source
Oct 21, 2026
Merged

mrcsin merged 1 commit into
masterfrom
opc-live-source

Conversation

@mrcsin

@mrcsin mrcsin commented Oct 21, 2026

Copy link
Copy Markdown
Member

Closes #116

Problem

The chart's live edge polled the archive. Simple-Scada flushes the archive 1 to 3 minutes late for a fast pen,
about 8 minutes for a slow one and up to a day for a constant one, so a change on the plant could not reach the
chart within 1 s.

What changed

The live edge reads the SCADA's OPC UA server; history stays on the archive.

  • SemiPlot.DataSource.OpcUa, a new project:
    • OpcUaLiveSource is the second implementation of ILiveSource, registered by AddOpcUaLiveSource.
    • One 2.0.1 ManagedSession per process. It dials the configured opc.tcp://<host>:<port> and keeps it
      (UpdateBeforeConnect = false).
    • It takes the anonymous token of the None endpoint, and finds the project ID by browsing Objects for
      root, browsing again on every new session.
    • It subscribes each pen set at 100 ms, monitored-item queue 1. SourceTimestamp, which is true UTC, becomes
      Sample.TimestampUtc.
  • Breaks:
    • A lost session writes one break per pen, 1 ms past its newest stamp.
    • A Bad notification writes a break. Uncertain draws as a value.
    • The chart (TrendPenState) writes a break ahead of a pen's first live sample past its last column, so a
      pen-set change draws no gap.
  • Faults:
    • The pens the source cannot serve (unknown or non-numeric) are one Warning entry per subscription.
    • OPC UA has a second status-bar indicator, which is neutral while connecting. A Warning and a recovery
      Info go through ArchiveFailureMapper with the new LiveSourceError in SemiPlot.Core/Data/Errors.
    • A 30 s grace at start holds the first Unreachable back.
  • Configuration: a new opc/ section (ConfigFiles/opc/opc.yaml, host: 127.0.0.1, port: 49875). A file
    that does not parse or carries a bad value is a startup failure; an unreachable server is not.
    DeliveredConfigurationTests covers the loader.
  • Deleted: PostgresLiveSource, RealtimePoll, the RealtimePoll and RealtimeBaseline statements, and
    poll_interval_ms with its settings field, resx keys and tests. A leftover key in an installed file is
    ignored.
  • Stand: converge gains a required --opc-port and writes opc/opc.yaml. The AppHost passes 55875 and runs
    the demo writer with --archive-lag 60.
  • Docs:
    • [DEC:opc-live-edge] in sources.md, and the README locked decision.
    • data-integration.md#realtime, and its time boundary.
    • RT-1 in trend-feature-spec.md.
    • charting.md, overview.md, ui-text.md, testing-strategy.md, bench.md, AGENTS.md.
    • The OPC measurements in scada-archive-observed.md.

How it was verified

  • dotnet build SemiPlot.slnx: 0 errors, only the AppHost's two NU1902.
  • dotnet format --verify-no-changes exits 0. dotnet terse over every changed .cs file exits 0.
  • SemiPlot.Tests.Unit: 2498/2498. SemiPlot.Tests.Integration: 205/205.
  • The OpcUa integration filter passes 13/13 with no container, against the in-process demo server on free
    ports. Its coverage:
    • values under Asia/Tokyo;
    • a restart in place and a transport cut;
    • the grace and a late server;
    • unknown pens;
    • the bounded Dispose() under a dropping SynchronizationContext;
    • a pen-set change;
    • the journey to the chart.
  • git grep proves the deletions and that no OPC test names 49875.
  • The real-SCADA acceptance is recorded in a comment on this PR.
  • Not run:
    • the manual steps that need a gesture or a stopped project (plan Post-Completion steps 4-7);
    • the stand checks;
    • the README screenshots, which still show the removed poll field and one indicator.

Known gaps left open

  • StartupFailureViewModel still offers the settings dialog on an opc/ failure. The dialog cannot edit opc/
    until opc-settings lands.
  • Four narrow races from the last review pass:
    • Close() can land between OnLost's check and Emit (PenSubscriptionSet.cs:93-95).
    • ApplyProjectAsync ignores a changed namespace index.
    • A standing setup fault can show while ConnectAsync retries after a successful discovery.
    • A drop in the few milliseconds before the state handlers attach gives no loss breaks.
  • A brief browse failure on a live server turns a standing ProjectMissing into Unreachable and back, which
    writes two extra panel entries.

The archive poll could show a change only once the SCADA flushed it,
1 to 3 minutes late for a fast pen and up to a day for a constant one,
so a change could not reach the chart within 1 s.

The live edge now reads the SCADA's OPC UA server through
OpcUaLiveSource in a new SemiPlot.DataSource.OpcUa project, the second
implementation of ILiveSource. One ManagedSession per process dials
the configured opc.tcp://<host>:<port> from the new opc/ section
(tracked as 127.0.0.1 and 49875), keeps that address, finds the
project ID by browsing for the root folder and subscribes each pen set
at 100 ms with a monitored-item queue of 1. SourceTimestamp, true UTC,
becomes the sample time.

A lost session writes one break per pen 1 ms past its newest stamp, a
Bad notification writes a break, and the chart writes one ahead of a
pen's first live sample past its last column, so a pen-set change draws
no gap. Pens the source cannot serve are one Warning entry per
subscription. OPC UA has its own status-bar indicator and a Warning
through ArchiveFailureMapper, after a 30 s grace at start. A bad opc/
file is a startup failure; an unreachable server is not.

Deleted: PostgresLiveSource, RealtimePoll, its two statements and
poll_interval_ms with its settings field and resx keys. converge gains
a required --opc-port and writes the stand's opc/opc.yaml; the AppHost
passes 55875 and runs the demo writer with --archive-lag 60.
@mrcsin

mrcsin commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

Acceptance on the real SCADA, 2026-10-21

Simple-Scada 2.7.6.0 on this machine, project running, OPC UA on 127.0.0.1:49875. The viewer ran from
c9201ef with --logging-level debug, the tracked opc/opc.yaml, for 5.5 minutes, window sticky, no
gesture.

The archive side was a throwaway Postgres container. It held a pg_dump of the SCADA's capture database,
taken at start, with the catalogue filled for the 21 SCADA variables (ids 2-22). Its semiplot_tags was
brought to the v0.5.0 columns. The capture database itself was not written.

Check Result
Session Connected 1.3 s after start, no OPC entry in the log, no Warning or Error from the live source
Apply moment minus the newest SourceTimestamp (debug line Live batch applied) 337 batches: median 94 ms, p99 203 ms, max 703 ms (the first batch, the initial values); 0 at or over 1000 ms
History reads while sticky none after the opening reads; only the minimap's own band reads
Bool1 (21), Int64Array1 (22) served as numbers (the server sends Byte, as measured in the roadmap); no PensUnserved entry

Not checked here:

  • Post-Completion step 3, nothing changes when the archive catches up. The archive copy was static, so no
    rows arrived.
  • Steps 4-7: a gesture, a stopped project, a killed server, a start while the project is stopped.
  • The stand checks.

@mrcsin
mrcsin marked this pull request as ready for review October 21, 2026 00:47
@mrcsin
mrcsin merged commit 5f8ea2c into master Oct 21, 2026
2 checks passed
@mrcsin
mrcsin deleted the opc-live-source branch October 21, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Live data: show a value within 1 s of its change

1 participant