Adds FocasAlarmProjection with two modes (ActiveOnly default, ActivePlusHistory)
that polls cnc_rdalmhistry on connect + on a configurable cadence (5 min default,
HistoryDepth=100 capped at 250). Emits historic events via IAlarmSource with
SourceTimestampUtc set from the CNC's reported timestamp; dedup keyed on
(OccurrenceTime, AlarmNumber, AlarmType). Ships the ODBALMHIS packed-buffer
decoder + encoder in Wire/FocasAlarmHistoryDecoder.cs and threads
ReadAlarmHistoryAsync through IFocasClient (default no-op so existing transport
variants stay back-compat). FocasDriver now implements IAlarmSource.
13 new unit tests cover: mode switch, dedup, distinct-timestamp emission,
type-as-key behaviour, OccurrenceTime passthrough (not Now), HistoryDepth
clamp/fallback, and decoder round-trip. All 341 FOCAS unit tests still pass.
Docs: docs/drivers/FOCAS.md (new), docs/v2/focas-deployment.md (new),
docs/v2/implementation/focas-wire-protocol.md (new),
docs/v2/implementation/focas-simulator-plan.md (new),
docs/drivers/FOCAS-Test-Fixture.md (alarm-history bullet appended).
Closes#267
Replaces the per-tag Plc.ReadAsync loop in S7Driver.ReadAsync with a
batched ReadMultipleVarsAsync path. Scalar fixed-width tags (Bool, Byte,
Int16/UInt16, Int32/UInt32, Float32, Float64) are bin-packed into ≤18-item
batches at the default 240-byte PDU using S7.Net.Types.DataItem; arrays,
strings, dates, 64-bit ints, and UDT-shaped types stay on the legacy
ReadOneAsync path. On batch-level failure each tag in the batch falls
back to ReadOneAsync so good tags still produce values and the offender
gets its per-item StatusCode (BadDeviceFailure / BadCommunicationError).
100 scalar reads now coalesce into ≤6 PDU round-trips instead of 100.
Closes#292
Adds the 9th capability interface (IMethodInvoker) so the OPC UA Client
driver can mirror upstream OPC UA Method nodes into the local address
space and forward Call invocations as Session.CallAsync. Method-bearing
servers (e.g. ProgramStateMachine, Acknowledge / Confirm methods, custom
control surfaces) now show up downstream instead of being silently
filtered out.
- Core.Abstractions: IMethodInvoker + MethodCallResult; default no-op
IAddressSpaceBuilder.RegisterMethodNode + MirroredMethodNodeInfo +
MethodArgumentInfo. Default impls keep tag-based drivers and existing
builders compiling without forced overrides.
- OpcUaClientDriver: BrowseRecursiveAsync now lifts the Method node-class
filter; for each method it walks HasProperty to pick up InputArguments
+ OutputArguments and decodes the Argument extension objects into
MethodArgumentInfo. Status-codes pass through verbatim (cascading
quality), local NodeId-parse + lost-session faults surface as
BadNodeIdInvalid / BadCommunicationError.
- 7 new unit tests cover the capability surface, the DTO shapes, and the
back-compat default no-op for RegisterMethodNode. Suite green at
160/160.
Server-side OnCallMethod dispatch (wiring local MethodNode handlers to
IMethodInvoker.CallMethodAsync inside DriverNodeManager) is deferred to
a follow-up — the driver-side surface + browse mirror ship cleanly here.
Closes#281
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in pass-3 walk of the upstream TypesFolder (i=86) so the OPC UA
Client driver can mirror upstream type definitions into the local address
space. Honours the curation rules from PR-7 (#359). Structural mirror only —
binary-encoding priming via LoadDataTypeSystem is tracked as a follow-up
because that helper was removed from the public ISession surface in
OPCFoundation.NetStandard 1.5.378+.
- IAddressSpaceBuilder.RegisterTypeNode (default no-op for back-compat)
- MirroredTypeNodeInfo + MirroredTypeKind in Core.Abstractions
- OpcUaClientDriverOptions.MirrorTypeDefinitions (default false)
- DiscoverAsync pass-3: FetchTypeTreeAsync + recursive HasSubtype walk per
branch (ObjectTypes/VariableTypes/DataTypes/ReferenceTypes), best-effort
IsAbstract read, IncludePaths/ExcludePaths still applied
- 6 new unit tests; all 153 OpcUaClient unit tests pass
Closes#280
Adds OpcUaClientCurationOptions on OpcUaClientDriverOptions.Curation with:
- IncludePaths/ExcludePaths globs (* and ? only) matched against the
slash-joined BrowsePath segments collected during BrowseRecursiveAsync.
Empty Include = include all so existing deployments are unaffected;
Exclude wins over Include. Pruned folders are skipped wholesale, so
descendants don't reach the wire.
- NamespaceRemap (URI -> URI) applied to DriverAttributeInfo.FullName when
registering variables. Index-0 / standard nodes round-trip unchanged;
remapped nodes serialise via ExpandedNodeId so downstream clients see
the local-side URI.
- RootAlias replaces the hard-coded "Remote" folder name at the top of
the mirrored tree.
Closes#279
Adds optional `DiscoveryUrl` knob to OpcUaClientDriverOptions. When set,
the driver runs `DiscoveryClient.CreateAsync` + `FindServersAsync` +
`GetEndpointsAsync` against that URL during InitializeAsync and prepends
the discovered endpoint URLs (filtered to matching SecurityPolicy +
SecurityMode) to the failover candidate list. De-duplicates URLs that
appear in both discovered and static lists (case-insensitive). Discovery
failures are non-fatal — falls back to statically configured candidates.
The doc comment notes that FindServers requires SecurityMode=None on the
discovery channel per OPC UA spec, even when the data channel uses Sign
or SignAndEncrypt.
Closes#278
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capability-matrix correctness fix: real 16i ladders use F (CNC->PMC) and
G (PMC->CNC) signal groups for handshakes, but PmcLetters(Sixteen_i) was
returning {X,Y,R,D} only. Widen to {X,Y,F,G,R,D}; M/C/E/A/K/T remain
0i-F / 30i-only. Updated the matching test row.
Closes#265
Adds optional `@N` path suffix to FocasAddress (PARAM:1815@2, R100@3.0,
MACRO:500@2, DIAG:280@2/1) with PathId defaulting to 1 for back-compat.
Per-device PathCount is discovered via cnc_rdpathnum at first connect and
cached on DeviceState; reads with PathId>PathCount return BadOutOfRange.
The driver issues cnc_setpath before each non-default-path read and
tracks LastSetPath so repeat reads on the same path skip the wire call.
Closes#264