FOCAS: browse-committed tags are unreadable (BadNodeIdUnknown) — device-host prefix is written into TagConfig.Address #540
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Authoring FOCAS tags through
/raw→ device → Browse device… produces tags the driver can never read. Every committed tag reads0x80340000 BadNodeIdUnknownforever. The failure is silent end to end: the AdminUI accepts it, the deploy gate seals green, and the driver reports Healthy with 0 errors while ignoring the tags entirely.Net effect: the fixed-tree surface (
Identity/Axes/Spindle/Program/OperationMode/Timers) cannot be published through any v3 authoring path. v2 auto-injected it; #507 removed that injection as superseded, and the browse-commit route that was supposed to replace it does not work.Found while cutting
wonder-app-vd03over to v3 (4e461951) against a real Makino Pro 5 / Fanuc 30i at10.201.31.5:8193.Reproduce
/raw(device endpoint = a real CNC, seriesThirty_i).⋯→ Browse device… → Connect & browse → toggle Create matching tag-groups on.FOCAS→<device>and tick every leaf underIdentity,Spindle,Program,OperationMode,Timers.Deployment seals. The tree materializes over OPC UA. Then every new node fails:
0 of 20 browse-committed tags read. The two hand-authored
MACRO:tags on the same device read Good, which isolates it to the address, not the connection.Root cause
The browse
FullNamecarries the device host, and it is written into the tag address unmodified:src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs—FixedTreeReference(deviceHost, path) => $"{deviceHost}/{path}", used byEmitIdentityVariable/EmitAxisVariable/ the generic emitter (~lines 628, 649, 684). So a leaf'sFullNameis literally10.201.31.5:8193/Identity/SeriesNumber.src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/RawBrowseCommitMapper.cs:248—Address = addresswhereaddressis thatFullName. No prefix stripping, no translation.src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasAddress.cs—TryParseaccepts onlyPARAM:<n>[/bit],MACRO:<n>, or PMCletter + digits [.bit]. A string beginning1fails atchar.IsLetter(src[0])and returnsnull.FocasDriver.InitializeAsyncthen logs "FOCAS tag has invalid Address …; skipping" and the tag never enters the poll table, so every read returnsBadNodeIdUnknown.Confirmed by opening a committed tag in the AdminUI — its
Addressfield reads10.201.31.5:8193/Identity/SeriesNumberverbatim.Why nothing catches it
Three gates that look like they'd catch this, don't:
FocasTagConfigModel.Validate()only checks the address is non-empty. Authoring accepts it.225e145ereachedSealed/In syncwith 20 dead tags in it. A sealed deployment is not evidence the tags work.focas-z-34184shows Healthy, errors/5 min = 0,Last error —, with a live-advancingLast read— while silently ignoring 20 of its 22 tags. Unparseable addresses are skipped at init rather than counted as errors, so per-tag authoring mistakes are invisible in driver health.(3) is arguably the more dangerous half: an operator has no signal anywhere that their tags aren't live.
Second defect, same area
The manual FOCAS picker is broken the same way:
src/Server/…/Pickers/FocasAddressBuilder.cs:14—Build(group, id) => $"{group}:{id}"src/Server/…/Pickers/FOCASAddressPickerBody.razor:82— defaults_group = "axis", so the picker's initial value isaxis:5axis:5is not a formFocasAddress.TryParseaccepts either. So only hand-typedPARAM:/MACRO:/ PMC addresses currently work at all — both UI-assisted authoring paths emit strings the runtime rejects.Suggested direction
This is a design call, not a one-liner. Roughly:
{deviceHost}/at commit — necessary but not sufficient:Identity/SeriesNumberis a logical fixed-tree path, not a PMC/PARAM/MACRO address, so the parser must also learn a fourth address form (e.g.FIXED:<path>) that resolves againstFixedTreeCacherather than a FOCAS area read. Same treatment for the picker'saxis:/spindle:groups.Whichever way it goes, two guardrails are worth adding independently:
FocasTagConfigModel.Validate()actually runFocasAddress.TryParseso a bad address is rejected at authoring time and at the deploy gate, instead of at driver init where nothing surfaces it./hosts, or a degraded driver state — so "Healthy, 0 errors" cannot coexist with "ignoring most of my tags".Note for anyone reproducing
On a real CNC the browse may report duplicate sibling names — this Makino returns two axis folders both called
AA, which would collide as RawPath segments.Axeswas excluded from this test for that reason, so the 20 tags above are the non-axis folders only. Worth deciding how commit should disambiguate duplicates before the axis path is fixed.Fixed on
fix/gitea-540-focas-fixed-tree-addressing@b61fe388. Both defects, plus the first of the two suggested guardrails. The second guardrail is fleet-wide and is now #541.What was done
Taking the first of the two suggested directions. Re-materializing the fixed tree through discovery/injection would re-open what #507 closed, and it fights v3's model, where raw tags are authored explicitly and the served tree changes only on a config redeploy.
Within that direction the issue suggested stripping
{deviceHost}/at commit. It was fixed one step earlier instead — at what the driver emits — because a captured leaf's id IS itsDriverAttributeInfo.FullName, and that one string feeds both the/rawbrowse-commit mapper and the manual picker's tree-select. Fixing the emitter repairs both paths at once,RawBrowseCommitMapperneeds no FOCAS special-case, and there is exactly one canonical address form rather than one emitted and a different one stored.The address now carries no device at all. A tag's device already comes from its raw-tag Device row, so a host in the address was a second, conflicting routing channel — which is the actual root cause, not just the parse failure.
Changes
FocasAddressgainsFIXED:<path>and moves toDriver.FOCAS.Contracts, so the AdminUI and ControlPlane can parse an address without dragging the runtime driver into their build graphs (#526). Pure file move, same namespace.FIXED:Identity/SeriesNumberetc. as its leaf reference.FixedTreeReferencesurvives as the internal per-device cache key, now documented as such.FIXED:read is served from the poll-loop snapshot — no area read, no connection forced. A path the machine does not expose is a genuineBadNodeIdUnknown; a device that has not answered yet reports a comms fault instead, because the address may be fine. Writes refuse withBadNotWritablebefore opening a connection: nothing in that tree is a writable location on the CNC.FocasCapabilityMatrixhandles the new kind explicitly (no numeric range applies).Two things the issue did not mention, found while fixing it
Both would have made the fix look complete and still not work in the field:
FixedTree.Enableddefaults tofalse, and the universal browser force-enables it only for the duration of a browse. So the corrected flow — browse, commit, deploy — lands on a driver whose fixed-tree loop never starts: every tag Bad, with a perfectly correct address. Authoring anyFIXED:tag now arms the loop by itself. The config flag remains the way to publish the tree without authoring anything.A dead CNC would have served its last fixed-tree values as
Goodforever. The poll loop swallows a failed tick by design (LogDebug+ retry), and unlike an area read a fixed-tree read issues no traffic that could fail — so nothing marked the caches stale. That was harmless while nothing could bind to the tree. It is not harmless now. Values are dated from the last answered poll and goBadCommunicationErroronce it is older than 8 poll cadences (floored at 30 s).Second defect — the manual picker
FocasAddressBuildercomposed{group}:{id}overaxis/spindle/program/status, none of which is a FOCAS addressing space. It now composes PMC /PARAM:/MACRO:throughFocasAddress, so its output is parseable by construction and cannot drift from the parser. The picker UI follows: area-letter dropdown for PMC, optional bit with the right range per space, and an explicit "not a valid address" state instead of rendering an unusable string as if it were fine.FIXED:is deliberately absent from the manual composer — fixed-tree paths are discovered from the machine, not composed from parts.Guardrail 1 (suggested, done)
FocasTagConfigModel.Validate()runs the real parser instead of a non-empty check, with a message naming the four legal forms and calling out the "pasted a browse id" case by name.FocasTagDefinitionFactory.Inspect()— already wired into the deploy gate viaEquipmentTagConfigInspector— warns on an unparseable address, which covers CSV import and the API, i.e. the paths that never touch the modal.Guardrail 2 (suggested) → #541
Not done here, on purpose. Eight drivers (FOCAS, Modbus, S7, AbCip, AbLegacy, TwinCAT, Sql, Mqtt) skip-and-log unmappable tags identically, so a FOCAS-only counter would be the wrong shape. It also cannot simply be
Degraded: that would interact badly with #527's comms-loss conjunction and reconnect-loop a healthy device whose tags were all skipped. Filed as #541 with aDriverHealthChanged+/hostsdirection modelled on #521.Tests
Written to assert addresses round-trip through the parser, not to match an expected string. The old
FocasAddressBuilderTestspinned"axis:5"exactly — which is how a fully green suite coexisted with a picker whose default value nothing could read. The regression checks now sweep the whole emitted tree and the whole offered PMC surface rather than sampling.Verified red-first: restoring the host-prefixed emitter fails 2 of the new tests.
FOCAS 308·AdminUI 1095·ControlPlane 169·Core.Abstractions 265·Commons 316— all pass, 0 warnings underTreatWarningsAsErrors.Still open, recorded rather than fixed
Duplicate sibling names. Your note stands: the Makino reports two axis folders both called
AA. Commit-time collision detection already refuses to author both (same RawPath), so nothing silently binds wrong — but the runtime cache is keyed by the same display name too, so the second axis would overwrite the first. TheAxes/subtree is therefore not trustworthy on such a machine. Documented indocs/drivers/FOCAS.mdas a caveat; disambiguation is a separate decision (suffix by index? by the CNC's axis number?) and did not belong in this fix.Not yet on the box
wonder-app-vd03still carries the 20 dead tags. They now serve as the live reproduction. Redeploying with this build should turn them Good without re-authoring — the tags' RawPaths do not change, only their addresses, so the browse-commit needs re-running to pick up theFIXED:form.Closing — fixed, merged, and now proven on the box.
Merged to
master2026-08-05, merge4e22f024(branchfix/gitea-540-focas-fixed-tree-addressing, fix commitb61fe388— see the comment above for the full record).Deployed to
wonder-app-vd03the same day (box4e461951→4e22f024; no EF migration in the delta, plain binary swap). The 20 dead tags had their addresses rewritten in place to theFIXED:form rather than being re-browsed — an exact 1:1 transform preserving every RawPath / Name / DataType / TagGroup / UNS reference, so nothing downstream rebound — and they now read 20/20 Good over OPC UA with real machine data (SeriesG431, 7 axes, modeAUTO, program//CNC_MEM/USER/LIBRARY/O1234), where they had been 0/20 since the v3 cutover. The "Not yet on the box" note above is superseded.Both split-outs are resolved:
355e59aa, 2026-08-06): every driver now reports its skipped tags onDriverHealthChanged, surfaced as the Tags column on/hosts— "Healthy, 0 errors" can no longer coexist silently with "ignoring most of my tags".Axes/remains deliberately unauthored on the wonder Makino until that lands.