8fc147d8d4
Adds the driver-expansion program design (umbrella: universal Discover-backed browser + MTConnect, MQTT/Sparkplug B, BACnet/IP, SQL poll, Omron, Modbus RTU; MELSEC deferred) plus the per-driver research reports. All docs went through a 7-agent parallel review against the codebase before this commit. Highlights fixed in review: - universal browser: FOCAS FixedTree fills post-connect -> UntilStable settle + FixedTree.Enabled patch; MQTT reconciled to bespoke (was contradicting the program doc's SupportsOnlineDiscovery=false verdict) - modbus-rtu: SerialPort.ReadTimeout doesn't bound async BaseStream reads -> linked-CTS per-op deadline (R2-01 class); BCL enum reuse would leak System.IO.Ports into Contracts - bacnet: DiscoveryRediscoverPolicy enum name; UDP 47808 contention; live suite rewritten around unicast Who-Is + BBMD (broadcast doesn't cross VMs) - sql-poll: real tier registration via DriverFactoryRegistry.Register; blackhole gate must not docker-pause the shared central SQL Server - mqtt: Sparkplug v3.0 STATE topic form; first-in-repo proto codegen noted - omron: host hardcodes isIdempotent:false today (retry seam unshipped); v1 scopes UDTs to dotted-leaf access - mtconnect: SecurityClassification.ViewOnly; factory ParseEnum<T> pattern - program doc: both valid enum-serialization patterns; IRediscoverable is change-signal-gated; RTU P2 adds System.IO.Ports; label is host-side
295 lines
20 KiB
Markdown
295 lines
20 KiB
Markdown
# Existing-driver browse audit — which of the 6 no-browser drivers should get an `IDriverBrowser`
|
||
|
||
> **Status:** research complete, 2026-07-15. Author: browse-audit sweep.
|
||
> **Scope:** the 6 shipped drivers with **no** `IDriverBrowser` today — AbCip, TwinCAT,
|
||
> AbLegacy, FOCAS, S7, Modbus. (OpcUaClient + Galaxy already have browsers — see
|
||
> `docs/plans/2026-05-28-driver-browsers-design.md`.)
|
||
> **Question answered:** which protocols support live online discovery / symbol enumeration,
|
||
> what the browse would return, and how a picked node maps to that driver's `TagConfig`.
|
||
|
||
## TL;DR
|
||
|
||
The single most important finding: **AbCip and TwinCAT already contain a complete, working
|
||
symbol-enumeration stack** — but it is buried in the *runtime* driver's `DiscoverAsync`
|
||
(gated by an `EnableControllerBrowse` option) and is **not** exposed as the interactive,
|
||
per-click `IDriverBrowser` the AdminUI address picker consumes. Building their browsers is
|
||
therefore mostly a *wrapping* job over code that already ships and is already unit-tested,
|
||
not a from-scratch protocol implementation. That makes them high-value / low-effort and the
|
||
clear top priority.
|
||
|
||
---
|
||
|
||
## 1. Verdict table
|
||
|
||
| Driver | Browseable? | Mechanism (protocol-native discovery) | Existing code to reuse | Recommended action |
|
||
|---|---|---|---|---|
|
||
| **AbCip** (ControlLogix EtherNet/IP CIP) | **Yes** | CIP **Symbol Object class 0x6B** enumerated via libplctag's `@tags` pseudo-tag; UDT members via **Template Object class 0x6C** | `IAbCipTagEnumerator` / `LibplctagTagEnumerator` + `CipSymbolObjectDecoder` + `CipTemplateObjectDecoder` + `AbCipTemplateCache` + `AbCipUdtMemberLayout` **already exist** | **Add browser — priority 1** |
|
||
| **TwinCAT** (Beckhoff ADS) | **Yes** | ADS **symbol upload** (`ADSIGRP_SYM_UPLOAD`) via `SymbolLoaderFactory` (flat mode) | `AdsTwinCATClient.BrowseSymbolsAsync` + `TwinCATSymbolExpander` **already exist** | **Add browser — priority 2** |
|
||
| **FOCAS** (Fanuc CNC) | **Borderline** (curated, not free-form) | Fixed FOCAS API surface: live-enumerated axes/spindles/identity + per-series valid Macro/Parameter/PMC ranges | `FocasDriver.DiscoverAsync` FixedTree + `FixedTreeCache` + `FocasCapabilityMatrix` **already exist** | **Add curated browser — priority 3 (defer)** |
|
||
| **AbLegacy** (Rockwell PCCC / PLC-5 / SLC) | **No** | PCCC data-table is numeric (`N7:0`), no symbolic catalog; libplctag exposes no file-directory list | none (discovery = static re-advertisement of configured tags) | **Skip** |
|
||
| **S7** (Siemens S7comm) | **No** | Symbols live in the TIA/STEP7 project, not the CPU; S7netplus exposes no symbol/block-list API | none | **Skip** |
|
||
| **Modbus** | **No** | Flat register space; no discovery in the Modbus protocol | none | **Skip** |
|
||
|
||
---
|
||
|
||
## 2. Protocol-browseability evidence (sources)
|
||
|
||
- **CIP Symbol Object 0x6B** — the CIP object used to *browse* controller symbol addresses;
|
||
Rockwell's `Get Instance Attribute List` (service 0x55) enumerates the tag table, and the
|
||
ControlLogix tag database is accessed via ANSI-symbolic Data-Table-Read against the returned
|
||
tag names. Some firmware/devices don't implement 0x6B, so a browse must degrade gracefully.
|
||
(AdvancedHMI *Generic EtherNet/IP and CIP Technical Information*; scadaprotocols.com *CIP
|
||
Object Model*; PLCS.net tag-read threads.)
|
||
- **ADS symbol upload** — Beckhoff's documented flow reads `ADSIGRP_SYM_UPLOADINFO2` then
|
||
`ADSIGRP_SYM_UPLOAD` (+ `ADSIGRP_SYM_DT_UPLOAD` for datatypes); the .NET `SymbolLoaderFactory`
|
||
wraps this and yields a `DynamicSymbolsCollection` you enumerate by `InstancePath`, descending
|
||
`SubSymbols` for struct/UDT/FB members. (Beckhoff InfoSys *Access Data via Symbol Loader* /
|
||
*SymbolLoaderFactory.Create*; fisothemes *TwinCAT ADS in .NET* guide.)
|
||
- **S7comm has no on-wire symbol table** — S7-300/400/1200/1500 CPUs store only compiled code;
|
||
symbol tables + comments live in the offline TIA/STEP7 project. S7-1200/1500 *optimized* blocks
|
||
are addressable only symbolically and only via the PLC's **OPC UA server** — classic S7comm
|
||
GET/PUT (what S7netplus speaks) cannot browse or even read them by name. (FlowFuse *Read S7
|
||
optimized datablocks*; Industrial Monitor Direct *S7-1500 optimized block access* + *Siemens S7
|
||
upload missing symbols*.)
|
||
- **PCCC is numeric** — PLC-5/SLC access is `Protected Typed Logical Read/Write` against numeric
|
||
data-table addresses (`N7:0`, length N). There are no symbolic names on the wire and no
|
||
standard "list the files" catalog service. (Rockwell 1785-6.8.5 *Connecting PLC-5/SLC*; Ipesoft
|
||
*EtherNet/IP and encapsulated PCCC*.)
|
||
|
||
---
|
||
|
||
## 3. Browse designs for the **Yes / Borderline** drivers
|
||
|
||
The two existing browsers are the template: a sibling `*.Browser` project holding an
|
||
`IDriverBrowser` factory + an `IBrowseSession`, registered one line in
|
||
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs` (`AddAdminUI`). The
|
||
picker razor bodies already exist per-driver under
|
||
`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/` — today
|
||
`AbCipAddressPickerBody.razor`, `TwinCATAddressPickerBody.razor`, and `FOCASAddressPickerBody.razor`
|
||
are **static manual-entry stubs**; only `OpcUaClientAddressPickerBody` and
|
||
`GalaxyAddressPickerBody` embed the shared `DriverBrowseTree`. So "wiring the picker" is the
|
||
same delta that was applied to those two.
|
||
|
||
> **Architectural note that differs from OpcUaClient/Galaxy:** for those two, the enumeration
|
||
> logic lives *inside* the `.Browser` project. For AbCip and TwinCAT the enumeration already
|
||
> lives in the *runtime* `Driver.*` project. The design doc's decision #4 ("keep AdminUI from
|
||
> pulling runtime `Driver.*` SDK chains") was about not dragging heavy SDKs transitively — but
|
||
> the AbCip/TwinCAT **browsers must connect with those exact SDKs anyway** (`libplctag`,
|
||
> `Beckhoff.TwinCAT.Ads`), so that concern is moot here. Two clean options:
|
||
> **(a)** the `.Browser` project references the runtime `Driver.AbCip` / `Driver.TwinCAT`
|
||
> project directly and calls the existing enumerator (least work); or
|
||
> **(b)** extract the enumerator + decoders into a small shared core lib both the runtime driver
|
||
> and the browser consume (cleaner separation, more churn). Recommendation: **(a)** for the
|
||
> first cut — the enumerator interfaces (`IAbCipTagEnumerator`, `ITwinCATClient.BrowseSymbolsAsync`)
|
||
> are already the seam, so a later extraction to (b) is mechanical.
|
||
|
||
### 3.1 AbCip browser (`DriverType = "AbCip"`)
|
||
|
||
**Mechanism.** Open a libplctag connection from the form's `AbCipDriverOptions` JSON (the same
|
||
shape the runtime factory + `AbCipDriverProbe` consume). For each configured device
|
||
(`AbCipDeviceOptions.HostAddress`), read the `@tags` pseudo-tag and decode with the existing
|
||
`CipSymbolObjectDecoder` → a stream of `AbCipDiscoveredTag { Name, ProgramScope, DataType,
|
||
ReadOnly, IsSystemTag, ElementCount, IsArray, TemplateInstanceId }`. For a `Structure`-typed
|
||
tag, resolve UDT members through the existing `AbCipTemplateCache` /
|
||
`CipTemplateObjectDecoder` (class 0x6C) → `AbCipUdtShape` member list.
|
||
|
||
**Session semantics:**
|
||
- `RootAsync` → one `Folder` node per device, plus a two-way split of the symbol table into
|
||
`Controller/` (controller-scope tags, `ProgramScope == null`) and `Programs/<prog>/`
|
||
(program-scope tags), driven off the already-decoded `ProgramScope`. Apply
|
||
`AbCipSystemTagFilter` so `@`-prefixed infrastructure tags don't clutter the tree (parity
|
||
with the runtime discovery filter).
|
||
- `ExpandAsync(nodeId)` →
|
||
- device / scope folder → its atomic tags as `Leaf` and its UDT tags as `Folder`;
|
||
- a UDT `Folder` → its members (via the Template Object) as `Leaf`, recursing for nested
|
||
structs (member `nodeId = "{tagPath}.{member}"`).
|
||
- `AttributesAsync` → **empty** (AbCip has no Galaxy-style attribute side-panel; the leaf *is*
|
||
the address). Kind is the terminating signal, same as OpcUaClient.
|
||
|
||
**Node → `TagConfig` mapping.** The picker commits the leaf's `NodeId` (the Logix symbolic
|
||
path) into the AbCip TagConfig JSON authored by `AbCipTagConfigModel` / parsed by
|
||
`AbCipEquipmentTagParser.TryParse`:
|
||
|
||
```json
|
||
{ "tagPath": "Program:Main.MyTag", "dataType": "DInt",
|
||
"deviceHostAddress": "ab://10.0.0.5/1,0",
|
||
"isArray": false, "arrayLength": 1, "writable": true }
|
||
```
|
||
|
||
The browser already knows `dataType` (from `AbCipDiscoveredTag.DataType`), `isArray` +
|
||
`arrayLength` (from `IsArray` + `ElementCount`), and can pre-fill `writable = !ReadOnly` — so
|
||
the pick can populate the whole config, not just the path (a nicer UX than OpcUaClient's
|
||
path-only commit). `deviceHostAddress` is the device the tree node came from.
|
||
|
||
**Project layout** (mirrors `Driver.OpcUaClient.Browser`):
|
||
```
|
||
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Browser/
|
||
AbCipDriverBrowser.cs (IDriverBrowser; DriverType="AbCip"; opens libplctag conn)
|
||
AbCipBrowseSession.cs (IBrowseSession over IAbCipTagEnumerator + template cache)
|
||
ZB.MOM.WW.OtOpcUa.Driver.AbCip.Browser.csproj
|
||
→ refs Commons + Driver.AbCip (option a) [+ libplctag transitively]
|
||
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Browser.Tests/ (fake enumerator, decoder golden)
|
||
```
|
||
|
||
**DI line** to add in `EndpointRouteBuilderExtensions.AddAdminUI`:
|
||
```csharp
|
||
services.AddSingleton<IDriverBrowser, AbCipDriverBrowser>();
|
||
```
|
||
|
||
**Effort: ~2–3 days.** The protocol-hard part (0x6B decode, 0x6C template decode, system-tag
|
||
filter, UDT layout) is done and tested. Work is: JSON-open path (copy the probe's connect),
|
||
the two-level `RootAsync`/`ExpandAsync` projection, and the picker razor (copy the OpcUaClient
|
||
tree wiring). Live-gate against the `abcip`/`controllogix` docker fixture on `10.100.0.35:44818`.
|
||
|
||
### 3.2 TwinCAT browser (`DriverType = "TwinCAT"`)
|
||
|
||
**Mechanism.** Open an `AdsClient` from the form's `TwinCATDriverOptions` JSON (same shape the
|
||
runtime consumes), then call the **already-implemented**
|
||
`AdsTwinCATClient.BrowseSymbolsAsync` → `IAsyncEnumerable<TwinCATDiscoveredSymbol>`. That method
|
||
builds a `SymbolLoaderFactory.Create(_client, SymbolsLoadMode.Flat)` loader and runs the pure
|
||
`TwinCATSymbolExpander.ExpandLeaves` to descend struct/UDT/FB instances to atomic members
|
||
(deduped by `InstancePath`). Each yielded symbol already carries the mapped `TwinCATDataType`,
|
||
optional array length, and read-only flag.
|
||
|
||
**Session semantics:**
|
||
- `RootAsync` → one `Folder` per device; under it the top-level PLC scopes as folders
|
||
(`Constants`, `MAIN`, `GVL`, `Global_Version`, `TwinCAT_SystemInfoVarList`, …), synthesised
|
||
by splitting each symbol's dotted `InstancePath` on the first segment. Apply
|
||
`TwinCATSystemSymbolFilter` to hide the infrastructure symbol lists (parity with runtime
|
||
discovery).
|
||
- `ExpandAsync(nodeId)` → children at the next `InstancePath` depth: atomic symbols as `Leaf`,
|
||
structs/FB instances as `Folder`. Because `BrowseSymbolsAsync` yields the fully-expanded leaf
|
||
set, the session can build the whole path→children index once on open and serve expands from
|
||
memory (no per-click wire call — the symbol blob is downloaded once by the loader).
|
||
- `AttributesAsync` → **empty** (leaf is the address).
|
||
|
||
**Node → `TagConfig` mapping.** Commit the leaf's `InstancePath` as `symbolPath` in the TwinCAT
|
||
TagConfig JSON (`TwinCATTagDefinition`: `symbolPath`, `dataType`, `deviceHostAddress`,
|
||
`arrayLength`, `writable`). The browser knows `dataType` + `arrayLength` + `writable`
|
||
(`!ReadOnly`) from the discovered symbol, so it pre-fills the full config.
|
||
|
||
**Two live caveats (already flagged in `AdsTwinCATClient`):**
|
||
1. **AMS router.** The AdminUI host process needs a reachable AMS router to browse — on Windows
|
||
from TwinCAT XAR, elsewhere from the `Beckhoff.TwinCAT.Ads.TcpRouter` package hosted in-proc.
|
||
`Beckhoff.TwinCAT.Ads` **7.0.172** is cross-platform-capable (netstandard), so this runs on
|
||
the Linux/macOS AdminUI host *if* a router is wired — but that is a deployment prerequisite to
|
||
document, not a code change.
|
||
2. **Flat-mode `SubSymbols`.** The code comment warns that on some TC3 firmware, `Flat` mode may
|
||
not populate `SubSymbols` for struct/UDT/FB symbols, silently dropping members; the documented
|
||
fallback is `SymbolsLoadMode.VirtualTree`. This must be **live-gated on a real TC3 target**
|
||
before trusting struct expansion.
|
||
|
||
**Project layout:** `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Browser/`
|
||
(`TwinCATDriverBrowser` + `TwinCATBrowseSession`, refs Commons + Driver.TwinCAT).
|
||
**DI line:** `services.AddSingleton<IDriverBrowser, TwinCATDriverBrowser>();`
|
||
|
||
**Effort: ~3–4 days** + a live-gate session on a real TC3 PLC for the `SubSymbols`/router risk.
|
||
The enumeration + expansion + type-mapping are done and unit-tested; work is the JSON-open
|
||
path, the path→tree projection, the picker razor, and the live validation.
|
||
|
||
### 3.3 FOCAS browser (`DriverType = "FOCAS"`) — curated, borderline
|
||
|
||
FOCAS has **no free-form symbol tree** — it's a fixed API surface. But the driver already does a
|
||
**live protocol-native discovery**: `FocasDriver.DiscoverAsync` (FixedTree) live-reads
|
||
`cnc_sysinfo` / `cnc_rdaxisname` / `cnc_rdspdlname` at session init (cached in `FixedTreeCache`)
|
||
to enumerate the machine's actual axes, spindles, identity, timers, program info, and status.
|
||
Separately, `FocasCapabilityMatrix` encodes the *valid ranges* of Macro variables, CNC
|
||
Parameters, and PMC letter-areas per `FocasCncSeries` (static tables, not live-queried).
|
||
|
||
**A curated browse would return:**
|
||
- `RootAsync` → one `Folder` per device, then category folders: `Identity`, `Axes`, `Spindles`,
|
||
`Program`, `Timers`, `Status` (populated live from `FixedTreeCache`), plus `Macros`,
|
||
`Parameters`, `PMC` (populated from `FocasCapabilityMatrix` for the device's declared series).
|
||
- `ExpandAsync` → e.g. `Axes/` → the live axis names as folders → per-axis `Leaf`s
|
||
(`AbsolutePosition`, `MachinePosition`, `ServoLoad`, …); `Macros/` → a `Leaf` per valid macro
|
||
number in the series range (or a paged range picker for the large ranges); `PMC/R` → valid R
|
||
addresses.
|
||
- `AttributesAsync` → **empty** (leaf is the address).
|
||
|
||
**Node → `TagConfig` mapping.** Commit the canonical FOCAS address string (`X0.0`, `R100`,
|
||
`PARAM:1815/0`, `MACRO:500`) as `address` in the FOCAS TagConfig, with `dataType` pre-filled
|
||
from the category (positions → Float64, macros → Float64, PMC bit → Bit, etc.).
|
||
|
||
**Effort: ~3–5 days**, medium value (curated list, not the "discover unknown tags" win that
|
||
AbCip/TwinCAT give). Reuses `FixedTreeCache` + `FocasCapabilityMatrix`. **Defer** behind AbCip
|
||
and TwinCAT.
|
||
|
||
---
|
||
|
||
## 4. Justification for the **No** drivers
|
||
|
||
### AbLegacy (PCCC / PLC-5 / SLC) — No
|
||
PCCC addresses are opaque numeric data-table references (`N7:0`, `F8:0`, `B3:0/5`) with **no
|
||
symbolic names on the wire** and **no standard catalog service**. libplctag (the driver's client,
|
||
`libplctag` 1.5.2) exposes no PCCC data-table-file directory listing. The driver's `DiscoverAsync`
|
||
merely re-advertises the statically configured `_options.Tags` — zero live reads. The only
|
||
"browse" one could build is blind range-probing (try reading `N7:0`, `N9:0`, …, infer existence
|
||
from status codes) which is slow, unreliable, and enumerates addresses not meaningful names — no
|
||
better than the existing manual `N7:0`-style entry. **Skip.**
|
||
|
||
### S7 (S7comm) — No
|
||
The Siemens CPU holds only compiled code; the symbol table lives exclusively in the offline
|
||
TIA/STEP7 project. Classic S7comm (GET/PUT — what `S7netplus` 0.20.0 speaks) has no symbol-browse
|
||
service, and `S7netplus` exposes no block-list/SZL API. S7-1200/1500 *optimized* blocks are
|
||
reachable **only** symbolically via the CPU's own OPC UA server — which is precisely the
|
||
`OpcUaClient` driver's job, not S7comm's. (An operator who wants to browse an S7-1500 optimized
|
||
DB should point the **OpcUaClient** driver at the PLC's OPC UA server, where the existing
|
||
OpcUaClient browser already works.) The driver's own `S7DriverOptions.Tags` doc-comment states it
|
||
plainly: "S7 has a symbol-table protocol but S7.Net does not expose it, so the driver operates off
|
||
a static tag list." **Skip.**
|
||
|
||
### Modbus — No
|
||
Modbus is a flat, typeless register/coil space (`Coils`, `DiscreteInputs`, `InputRegisters`,
|
||
`HoldingRegisters`) with **no discovery mechanism in the protocol at all** — a device cannot tell
|
||
you which registers exist or what they mean. The transport is hand-rolled (`ModbusTcpTransport`,
|
||
no NuGet). The driver's own option doc-comment says it: "Modbus has no discovery protocol — the
|
||
driver returns exactly these." **Skip.** (The existing `ModbusAddressPickerBody` region+address
|
||
builder is the right and only tool here.)
|
||
|
||
---
|
||
|
||
## 5. Priority ordering, shareable code, effort
|
||
|
||
**Build order (value × effort):**
|
||
|
||
| Priority | Browser | Value | Effort | Why this order |
|
||
|---|---|---|---|---|
|
||
| **1** | **AbCip** | High (ControlLogix ubiquity; symbolic tag browse is the flagship UX) | **~2–3 days** | Full 0x6B + 0x6C decode stack already ships and is tested; lowest effort, highest payoff |
|
||
| **2** | **TwinCAT** | High (ADS symbol upload is complete + rich) | **~3–4 days** (+ TC3 live-gate) | `BrowseSymbolsAsync` already ships; small residual risk (Flat `SubSymbols`, AMS router) needs live validation |
|
||
| **3** | **FOCAS** | Medium (curated, not free-form) | **~3–5 days** | Reuses FixedTree + capability matrix, but lower discovery value; defer |
|
||
| — | AbLegacy / S7 / Modbus | — | — | No browser (documented above) |
|
||
|
||
**Shareable-code opportunities:**
|
||
- **AbCip → future Omron / generic-EtherNet/IP browser.** The CIP browse core
|
||
(`CipSymbolObjectDecoder` 0x6B + `CipTemplateObjectDecoder` 0x6C + `AbCipSystemTagFilter` +
|
||
`AbCipUdtMemberLayout`) is protocol-CIP, not Rockwell-specific. If a second CIP-family driver
|
||
ever lands (Omron NX/NJ also speak CIP tag access), extracting that core into a shared
|
||
`ZB.MOM.WW.OtOpcUa.Cip.Core` lib (design-doc option **b**) would let both browsers share the
|
||
decode path. Not worth doing pre-emptively for one consumer — flag it as the natural refactor
|
||
point when/if the second CIP driver appears.
|
||
- **AbCip + TwinCAT both already put their enumerator behind an interface**
|
||
(`IAbCipTagEnumerator`, `ITwinCATClient.BrowseSymbolsAsync`) — that interface *is* the reuse
|
||
seam between runtime `DiscoverAsync` and the new browser session. No new abstraction needed.
|
||
- Nothing shareable across the No drivers (they have no discovery to share).
|
||
|
||
**Net recommendation:** build **AbCip** then **TwinCAT** browsers now (both are wrap-existing-code
|
||
jobs, ~1 week combined); **defer FOCAS** (curated, medium value); **do not build** AbLegacy, S7,
|
||
or Modbus browsers — their protocols have no online discovery and the existing manual-entry
|
||
pickers are the correct surface.
|
||
|
||
---
|
||
|
||
## Appendix — key source paths
|
||
|
||
- Contracts: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/{IDriverBrowser,IBrowseSession,BrowseNode}.cs`
|
||
- Templates: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/`, `…Driver.Galaxy.Browser/`
|
||
- DI: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs` (`AddAdminUI`, lines 44–50)
|
||
- Picker bodies: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/*AddressPickerBody.razor`
|
||
- **AbCip** enumeration: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/{IAbCipTagEnumerator,LibplctagTagEnumerator,CipSymbolObjectDecoder,CipTemplateObjectDecoder,AbCipTemplateCache,AbCipUdtMemberLayout,AbCipSystemTagFilter}.cs`; TagConfig: `…Driver.AbCip.Contracts/{AbCipDriverOptions,AbCipEquipmentTagParser}.cs`; browse gate `AbCipDriverOptions.EnableControllerBrowse`
|
||
- **TwinCAT** enumeration: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/{AdsTwinCATClient (BrowseSymbolsAsync),TwinCATSymbolExpander,TwinCATSystemSymbolFilter}.cs`; TagConfig: `…Driver.TwinCAT.Contracts/{TwinCATDriverOptions,TwinCATEquipmentTagParser}.cs`; browse gate `TwinCATDriverOptions.EnableControllerBrowse`
|
||
- **FOCAS**: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/{FocasDriver (DiscoverAsync/FixedTree),FocasCapabilityMatrix,IFocasClient}.cs`; TagConfig: `…Driver.FOCAS.Contracts/FocasDriverOptions.cs`
|
||
- **AbLegacy**: `…Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs`; `…Driver.AbLegacy/AbLegacyDriver.cs` (`DiscoverAsync` = static re-advertise)
|
||
- **S7**: `…Driver.S7.Contracts/S7DriverOptions.cs` (see `Tags` doc-comment)
|
||
- **Modbus**: `…Driver.Modbus.Contracts/ModbusDriverOptions.cs` (see `Tags` doc-comment); `…Driver.Modbus/ModbusTcpTransport.cs`
|
||
- Package versions (`Directory.Packages.props`): `libplctag` 1.5.2, `Beckhoff.TwinCAT.Ads` 7.0.172, `S7netplus` 0.20.0
|