Browse-commit discards the discovered data type, so a browsed REAL commits as a DINT (AbCip, TwinCAT) #536
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?
Found while running the #468 controller-browse live gate on docker-dev, now that the AbCip fixture serves
@tags.What happens
Browsing an AbCip device and committing two leaves produced:
Pump_1_Speedis a REAL. The rowDataTypeis right (Float) — the discovered type does reach the commit — but theTagConfigsaysDInt, becauseRawBrowseCommitMapper.BuildTagConfigsets only the address field and leaves everything else at the model default, andAbCipTagConfigModel.DataTypedefaults toAbCipDataType.DInt.Why it matters
The driver does not read the row's
DataType.AbCipTagDefinitionFactory.FromTagConfigreadsdataTypeout of theTagConfigJSON and builds the libplctag tag definition from it, so a browse-committed REAL is read as a DINT — four bytes of IEEE-754 reinterpreted as a signed integer. The OPC UA node is typedFloatwhile the driver decodesInt32, and nothing reconciles the two.This is silent: the deploy gate's only
dataTypecheck warns on a present-but-invalid enum (05/CONV-2), andDIntis perfectly valid. It is also easy to miss on a sim, where every tag reads 0 and a misdecode still yields 0.Scope
Two drivers are affected — the ones that are both browsable and decode off
TagConfig.dataType:AbCipTagConfigModel.DataType = AbCipDataType.DInt, consumed byAbCipTagDefinitionFactory. Reproduced above.TwinCATTagConfigModel.DataType = TwinCATDataType.DInt, consumed byTwinCATTagDefinitionFactory. Same shape by inspection; not reproduced, because the ADS symbol-upload backend is still fixture-blocked (the sibling half of #468).The other typed models also carry a defaulted
DataType(AbLegacy,Focas,Modbus,Mqtt,MTConnect), but they are either not browsable or do not decode off that field — worth re-checking rather than assuming when this is picked up.Suggested fix
BuildTagConfigalready receives everything it needs: the browse side-channel supplies theDriverDataTypethatRawBrowseCommitMapper.MapDataTypeturns into the row's built-in type name. Thread the same value into the driver-typed model — mappingDriverDataTypetoAbCipDataType/TwinCATDataType— instead of leaving the model default.Note this widens
BuildTagConfig's stated contract ("setting ONLY the driver's address field ... leaving every other field at its model default"), so the doc-comment should change with it. Committing no type is not an option — the model has no unset state, so the default lands either way; the choice is between a known-correct type and a guess.Test note
A fixture where every tag reads 0 cannot tell a correct decode from a wrong one. Any test for this wants a non-zero REAL whose DINT reinterpretation is obviously different —
1.0freads back as1065353216.Found by: #468 live gate, 2026-08-01.
Fixed in
6b3da931.The scope was wider than filed: FOCAS is affected too. All three drivers whose
TagConfigcarries their own data-type enum — AbCip, TwinCAT, FOCAS — decode the wire bytes by that field rather than by the raw row's OPC UADataType, and all three are universal-browser drivers.The obvious fix would not have worked. Reverse-mapping the leaf's driver-agnostic
DriverDataTypefixes the REAL and silently keeps getting the integers wrong:SInt/Int/DInt/USInt/UIntall collapse intoInt32on the way out of the driver, so a 16-bit INT would still bind as a 32-bit DINT. The driver now states its own type name (DriverAttributeInfo.NativeDataType→AttributeInfo.AddressFields["dataType"]→RawBrowseCommitMapper); a leaf that states nothing keeps the model default, which is exactly the old behaviour rather than a guess.A numeric string is refused before parsing —
Enum.TryParseaccepts"9"for any enum andEnum.IsDefinedthen confirms it, so an unguarded parse binds a tag to a type nobody named. My first version of the guard had that hole; a test caught it.Live on docker-dev, browse-committing against the patched
ab_serverfixture:Pump_1_Speed{"dataType":"DInt"}← committed before the fixTestString{"dataType":"String"}Motor_Run{"dataType":"Bool"}TestSINT{"dataType":"SInt"}TestSINTis the falsifier: its rowDataTypeisInt32, identical toTestDINT's, so no reverse map from the agnostic type could have producedSInt.⚠️ Pre-existing browse-committed tags keep their wrong
dataType— the fix applies to new commits only. Re-commit or edit them in the typed editor.A second defect found on the way, fixed in the same commit
While verifying I re-tested the Test Connect limitation this repo had documented as a fixture gap ("
ab_serverdoesn't implement@raw_cpu_type"). Every part of that was wrong, and it was hiding a production defect.@raw_cpu_typeis not a libplctag special tag — the CIP dispatch knows only@raw,@tagsand@udt/. It is sent as an ordinary symbolic name that no controller defines, and the controller's "tag does not exist" is the successful answer.ab_serveranswered an unresolvable tag path with CIP 0x03 where a Logix controller answers 0x04 —ErrorBadParamvsErrorNotFound, which the probe classifies oppositely. Fixed in vendored patch0002.AbCipDriverProbe.IsReachableExceptionmatched the native C spellings (STATUS_NOT_FOUND), but libplctag.NET throws with the managed enum name (ErrorNotFound). Those substrings can never match, so the reachable classification could not fire on the exception path — the path every no-authored-tags probe takes. Test Connect was failing on real hardware too, not only against the sim, and it fails at exactly the moment an operator uses it: on a device with no tags authored yet.Rejected along the way: switching the fallback to
@tags. libplctag builds a tag-listing tag with no network I/O, so it reports OK against any TCP listener — measured green against a Mosquitto broker.Full solution suite green. Falsification runs confirm exactly the intended tests go red: 9 for the type carry, 3 (the reachable statuses only) for the probe classification.