Skip to content

Supply the motion verbs OPC 40010 leaves undefined - #47

Open
marcschier wants to merge 9 commits into
mainfrom
robot-intent-spec
Open

Supply the motion verbs OPC 40010 leaves undefined#47
marcschier wants to merge 9 commits into
mainfrom
robot-intent-spec

Conversation

@marcschier

@marcschier marcschier commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What this PR changes or proposes

Adds OPC UA — Robot Intent (metaverse-specs/robot-intent/, Release 0.1.0 draft): an information model for commanding a robot at the level of task intent — move there, follow this path, weld this seam, pick from here, place at that — with a lifecycle that survives the minutes such work actually takes.

  • OPC-UA-Robot-Intent.md — the specification (13 clauses, 4 annexes)
  • OPC-UA-Robot-Intent-Research.md — the prior art, the gaps, and the decisions those gaps forced
  • Opc.Ua.RobotIntent.NodeSet2.xml / .NodeIds.csv — generated (256 nodes, 69 types)
  • extras/robot-intent/tools/ — generator, validator, generated Annex A

It also fixes a defect in both generators: a DataTypeEncoding browses as Default Binary in namespace 0, and Vision and Robot Intent were both emitting 1:Default Binary. Vision is regenerated — 9 lines, no NodeId churn.

Why

OPC 40010-1 Robotics describes robot topology in detail and defines no motion verbs at all. Its entire actuation surface is Start, Stop and loading a named program. A conformant client can discover everything about a robot's construction and cannot ask it to move anywhere. Every integration above "run program 7" is therefore bespoke, even though every vendor already has joint, linear and circular moves, a speed, an acceleration, a blend, a tool frame and a work frame.

This supplies the verbs, and nothing else, so the two compose rather than compete.

The lifecycle is the harder half. A motion takes seconds and a pick takes a minute, while OPC 10000-4 §5.12.2 discards a method result when the Session ends "independent of the task actually performed at the Server" — so a synchronous motion method loses the outcome of work that has already physically happened. Submission therefore returns a Part 10 program instance the client watches, which is the resolution OPC 10000-10 §4.1 already reaches for exactly this case.

Design decisions worth review

  • Verbs are a DataType hierarchy, not one Method each. A submission and a mission step are then the same shape, extension is a subtyping act, and discovery is a read of SupportedIntents rather than a probe for BrowseNames.
  • Orientation is a unit quaternion (x, y, z, w). OPC UA defines no quaternion type, and the A/B/C fields of ThreeDOrientation carry no convention of their own. Annex C carries the normative bidirectional ThreeDFrame conversion, including the clamp that stops a pole orientation becoming a domain error.
  • ExecutionState refines the Part 10 state rather than restating it. Queued, Cancelling and the three terminal outcomes cannot be told apart from CurrentState alone; §6.3 tabulates every legal pairing, so the refinement is checkable rather than asserted.
  • Pick/Place reference Location nodes, not station strings, and Fasten references a joint in OPC 40450/40451 rather than restating tightening. A free-text identifier would be a second definition of a fact the address space already holds.
  • Queueing is PLCopen MC_BufferMode and concurrency is VDA 5050 blockingType, both adopted unchanged. No OPC UA specification defines either, and both are already implemented across the industry.
  • Mission branching follows IEC 61131-3, not a behaviour tree. Steps and transitions with alternative and parallel divergence are the notation the controller audience knows and have an IEC serialization; a behaviour tree needs a tick runtime controller vendors do not provide. Transition conditions reuse the base UA ContentFilter so nobody has to write a parser for an invented expression language, and an empty transition array leaves a mission the flat sequence it was.
  • The NodeSet is standalone on base OPC UA, following the Vision precedent; OPC 40010-1 interop is an optional profile carried by HasIntentController (Annex B).

Real-time: a division of labour, not an exclusion

Trajectory execution is in scope — the whole time-parameterised path is handed over once and the robot's own motion kernel runs it, which is what ROS FollowJointTrajectory and the PLCopen buffered path blocks already do. Streaming control is not expressible over OPC UA client/server, so the model brokers a channel instead: it describes and leases RTDE, EGM, FRI, RSI, MotoROS2 or OPC UA FX (OPC 10000-80…84), and the samples never traverse OPC UA. Same shape as the Vision model brokering a media endpoint rather than carrying pixels.

Safety: awareness and a refusal duty, explicitly not a rating

This one cannot be brought into scope as asked, and the specification says so rather than working around it.

OPC 10000-15 carries cyclic safety data from a SafetyProvider to a SafetyConsumer; the consumer's request holds an identifier, a monitoring number and one octet of explicitly non-safety flags, so a caller has no channel through which to supply safety-rated arguments. Every safety fieldbus — PROFIsafe, CIP Safety, FSoE, openSAFETY — expresses a safety command as a continuously asserted cyclic signal, because the integrity argument rests on the fail-safe state that follows when assertion stops. A Method call has no defined behaviour when it stops being called, and therefore cannot be a safety function however it is labelled.

So the model reports what the safety system is enforcing (SafetyStateType, with the IEC 61800-5-2 function vocabulary) and refuses work that would exceed it (§10.4), and states plainly that neither of those makes anything safe. A Server may observe the safety system and refuse on what it sees; it may never instruct it.

Standards check

The widely repeated claim that OPC 40010 Parts 2 (Skills) and 3 (Motion Program) are in development was tested against primary sources and is not supported — the OPC Foundation reference index lists only Part 1. What exists is opcua-skills/skill-nodeset, a VDMA SOArc prototype stalled at v0.0.5 since 2020 under http://opcfoundation.org/UA/Skills/. That namespace is avoided and the prior art is cited (Annex D). Whether a non-public VDMA working draft exists is the one question public research cannot close.

Type

  • Feedback / annotation only (no spec authored — maintainers will draft the change)
  • Concrete spec / prose change
  • Model change (NodeSet / CSV / generated artifacts, via the tooling)
  • Tooling / examples
  • Docs

Checklist

  • I edited the source (spec document, descriptor, or generator) — not generated NodeSets / CSV / Annex tables by hand
  • For a generated-spec change, I regenerated and ran validation (metaverse-specs/validate_all.py --self-contained and extras/robot-intent/tools/validate_local.py)
  • I opened this from a topic branch and am ready to discuss

Validation

  • metaverse-specs/validate_all.py --self-contained — all extensions OK
  • Determinism — regenerating both models reproduces the committed bytes exactly
  • NodeIds CSV against main83 added, 0 removed, 0 renumbered for Robot Intent; unchanged for Vision

The validator re-derives everything from the committed artifacts rather than asking the generator what it emitted, and cross-checks the specification against the model in both directions: every type and enumeration literal the model declares must be named in the prose, and every ns=1;i=<n> the prose cites must exist in the model. That check earned its keep twice — 48 unnamed members the first time, and another 48 when the scope was enlarged.

It is mutation-tested against 17 defects and catches all of them, restoring the tree byte-exactly afterwards. That includes the case that is easy to miss — a defect introduced in the generator and then fully regenerated, so the NodeSet, the CSV and Annex A all agree with each other and only the specification invariants can catch it — plus, since the scope grew, a process intent reparented off its abstract base, a transition condition swapped off ContentFilter, and a mandatory safety member demoted to optional.

Note for the reviewer

metaverse-specs/README.md was rebased over the OPC Foundation review move (#44/#46). The release-spec-link markers and the review notice are preserved verbatim; the section heading is now "The OpenUSD pair: two parts, two directions" because the tree holds four specifications rather than two, and vision/ and robot-intent/ are listed alongside them.

Related

The .NET stack implementation is now open as a draft at OPCFoundation/UA-.NETStandard#4165. It generates the model from this NodeSet, so the two move together — 149 tests pass there on net48 and net10.0.

The encoding BrowseName fix in this PR is what unblocked it: the .NET model source generator fails on 1:Default Binary with a bare NullReferenceException that names no node.

marcschier and others added 2 commits August 2, 2026 15:25
OPC 40010-1 describes robot topology in detail and defines no motion verbs
at all: its whole actuation surface is Start, Stop and loading a named
program. A client can discover everything about a robot's construction and
cannot ask it to move anywhere.

This adds OPC UA - Robot Intent, which supplies the verbs and nothing else,
so the two compose rather than compete.

The lifecycle is the harder half. A motion takes seconds and a pick takes a
minute, while OPC 10000-4 clause 5.12.2 discards a method result when the
Session ends "independent of the task actually performed at the Server" - so
a synchronous motion method loses the outcome of work that has already
physically happened. Submission therefore returns a Part 10 program instance
and the client watches it, which is the resolution OPC 10000-10 clause 4.1
already reaches for exactly this case.

Verbs are a DataType hierarchy rather than one Method each, so a single
submission and a mission step are the same shape and extension is subtyping.
Queueing is PLCopen MC_BufferMode and concurrency is VDA 5050 blockingType,
both adopted unchanged because both are already implemented everywhere and
no OPC UA specification defines either. Missions carry an immutable
committed base and a revisable horizon.

Orientation is a unit quaternion: OPC UA defines no quaternion type, and the
A/B/C fields of ThreeDOrientation carry no convention of their own. Annex C
carries the bidirectional conversion, including the clamp that stops a pole
orientation becoming a domain error.

Pick and Place reference Location nodes rather than naming a station in a
string, which would be a second definition of a fact the address space
already holds.

Clause 10 declares the interface non-safety-rated, restricts submission to
Automatic and Automatic External, and records that a stop request selects no
IEC 60204-1 stop category. Clause 8 says plainly that command authority
arbitrates between clients and is not the single point of control ISO
10218-2 requires.

The NodeSet is standalone on base OPC UA, following the Vision precedent;
OPC 40010-1 interop is an optional profile carried by a ReferenceType.

The validator re-derives from the committed artifacts rather than asking the
generator what it emitted, and cross-checks the specification against the
model in both directions. It was mutation-tested against thirteen defects,
including one introduced in the generator and fully regenerated so that the
NodeSet, the CSV and Annex A all agree with each other - which only the
specification invariants can catch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
A DataTypeEncoding browses as "Default Binary" in namespace 0. The BrowseName
is standard, not model-defined, and every published companion NodeSet writes
it that way. Both generators were emitting 1:Default Binary.

This is not cosmetic. Tooling that resolves encodings by BrowseName cannot
find them: the UA-.NETStandard model source generator fails outright on such
a NodeSet, and the failure surfaces as a NullReferenceException with no
indication of which node caused it - it took a binary search over the node
list to find.

Both models are regenerated. No NodeId moves, because only the BrowseName and
the added SymbolicName change: Vision is nine lines, Robot Intent eighteen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
Five things the specification did not do. Four are now defined; the fifth
turns out not to be a scoping choice at all.

Real-time was an exclusion and is now a division of labour. Trajectory
execution is expressible over request/response - the whole path is handed
over once and the robot's own motion kernel runs it, which is what
FollowJointTrajectory and the PLCopen buffered path blocks already do - so
TrajectoryIntentDataType, CartesianPathIntentDataType and ForceIntentDataType
are defined here. Streaming control is not expressible, so the model brokers
a channel instead: RealTimeChannelType describes and leases RTDE, EGM, FRI,
RSI, MotoROS2 or OPC UA FX and the samples never traverse OPC UA. That is the
same shape as the Vision model brokering a media endpoint rather than
carrying pixels.

Safety cannot be brought into scope as a rating, and the reason is
structural rather than editorial. OPC 10000-15 carries cyclic safety data
from a provider to a consumer; the consumer's request holds an identifier, a
monitoring number and one octet of explicitly non-safety flags, so a caller
has no channel through which to supply safety-rated arguments. Every safety
fieldbus expresses a safety command as a continuously asserted cyclic signal,
because the integrity argument rests on the fail-safe state that follows when
assertion stops - and a Method call has no defined behaviour when it stops
being called. So safety is in scope as awareness plus a duty: SafetyStateType
reports what the safety system enforces, clause 10.4 requires a Server to
refuse work that would exceed it, and the same clause says plainly that
neither of those makes anything safe. A Server may observe the safety system
and refuse on what it sees; it may never instruct it.

Robot construction was excluded on the grounds that OPC 40010 covers it. It
does not cover this part: OPC 40010-1 describes topology and axes and defines
no kinematic chain an IK solver could use, and no tool centre point at all.
RobotDescriptionType is therefore additive, and Annex B fixes which side
decides where both are present.

Process models are defined for all six applications, with fastening left
deliberately thin: OPC 40450 and OPC 40451 already own joining and
tightening, so FastenIntentDataType references a joint there rather than
restating torque strategies - the rule that already made Pick take a Location
node instead of a station string.

Mission branching follows IEC 61131-3 rather than a behaviour tree. Steps and
transitions with alternative and parallel divergence are the notation the
controller audience knows and have an IEC serialization; a behaviour tree
needs a tick runtime controller vendors do not provide. Transition conditions
reuse the base UA ContentFilter, so nobody has to write a parser for an
invented expression language, and an empty transition array leaves a mission
the flat sequence it was.

83 nodes added, 0 removed, 0 renumbered. The validator gained invariants for
the process hierarchy, the ContentFilter reuse and the mandatory safety
members, and the mutation suite grew from 13 defects to 17 - all caught, with
the tree restored byte-exactly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
@marcschier

Copy link
Copy Markdown
Owner Author

Stacked below #49. That PR branches from this one and restructures what this specification interoperates with, so this one should land first.

Relevant to reviewing this PR: #49 adds Annex E and the RI-Interop-Vision facet here, and renumbers Vision's frame-role enumeration to match this model's — the two both cite ISO 9787 while disagreeing on whether Tool is 2 or 3, and Vision is the one that moves.

The prose said TrajectoryIntentDataType carries PathTolerance,
GoalTolerance and GoalTimeTolerance "all MotionToleranceDataType".
GoalTimeTolerance is a Duration. An implementer decoding the structure
from the prose gets the wire format wrong at that field, and the
structure is not self-describing there. Lateness is one number, not a
pose deviation.

The 6.3 state diagram had Retriable --> Queued and gave Retriable no exit
to terminal, so execution resumed on the same instance. Clause 3 defines
Retriable as a state "from which execution does not resume", 6.6 says
Retry creates a NEW operation with the original left terminal, and 6.7
says a terminal Result shall not change thereafter. The model cannot
distinguish the two readings - Retriable to Queued is Halted to Ready,
which Part 10 permits - so a conformance test built from the model alone
rejects neither, and two implementations get incompatible operation
lifetimes.

FinalResultData and ProgramDiagnostic are Optional in
ProgramStateMachineType and were not promoted. So 6.7's "shall also be
reachable under FinalResultData" rested on a member a conformant Server
may omit, and 1.2's headline "auditable commanding" claim - which IS
ProgramDiagnostic and nothing else - was false against a legal
implementation. Both are now Mandatory on IntentOperationType, which is
the repository's stated remedy: a conformance unit that depends on an
inherited member promotes that member.

Written beside their type the promotions renumbered 39 members; they are
allocated at the end of the id space instead. 0 moved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
@marcschier

Copy link
Copy Markdown
Owner Author

Review response — 3 High findings fixed

GoalTimeTolerance had the wrong DataType in the prose. §5.4.1 said PathTolerance, GoalTolerance and GoalTimeTolerance were "all MotionToleranceDataType". It is a Duration. An implementer decoding TrajectoryIntentDataType from the prose gets the wire format wrong at that field, and the structure is not self-describing there. Lateness is one number, not a pose deviation.

The state diagram let execution resume from a state the document defines as terminal. §6.3 had Retriable --> Queued and gave Retriable no exit to terminal, while §3 defines it as a state "from which execution does not resume", §6.6 says Retry creates a new operation, and §6.7 says a terminal Result shall not change. The model cannot arbitrate — RetriableQueued maps to Part 10 HaltedReady, which is legal — so a conformance test built from the model alone rejects neither reading, and two implementations end up with incompatible operation lifetimes.

Two normative claims rested on Optional Part 10 members that were never promoted. §6.7's "shall also be reachable under FinalResultData" and §1.2's headline auditable commanding — which is ProgramDiagnostic and nothing else — were both false against a fully conformant Server, because Part 10 leaves both Optional. Both are now Mandatory on IntentOperationType, which is this repository's stated remedy: a conformance unit that depends on an inherited member promotes that member.

Writing the promotions beside their type renumbered 39 members; they are allocated at the end of the id space instead. 0 moved. 9/9 checks green.

Not fixed

Seven Medium findings are left for a follow-up rather than folded in here, since none is a correctness bug in the model: three §10.3 references that should read §10.4; §11.3's NodeId validation rule reading oddly against two members' purpose; Released/ReleasedStepCount having no rule binding them; BlockingModeEnum not matching the VDA 5050 attribution; GoalTolerance.Time overlapping GoalTimeTolerance with two failure codes; blending requestable two ways with no precedence rule; and the Vision PublicationDate not bumped for the change this PR makes to it.

marcschier and others added 5 commits August 5, 2026 09:10
Implementing this specification in the OPC UA .NET Standard stack surfaced six
defects. Each is a claim the document already made that the model did not let a
conformant Server keep.

Clause 6.2 orders six refusals and clause 5.8 calls the failure set small and
diagnosable on purpose, but SubmitIntent returned only IntentId and Operation.
A client could see that something had been refused and never which of the six it
was, so the ordering was unobservable and the failure taxonomy unreachable.
SubmitIntent, SubmitMission and Retry now return Accepted, Failure and Message,
clause 6.2 states that a Server returns Good and reports the refusal there
rather than substituting a Bad StatusCode, and OpenRealTimeChannel gains a
Message because clause 6.9 hid four grounds for refusal behind one boolean.

IntentOperationType declared ProgramDiagnostic as a Property of PropertyType
reached by HasProperty. OPC 10000-10 declares it as a Variable of
ProgramDiagnostic2Type reached by HasComponent, so the declaration added a
second member beside the inherited one instead of promoting it - which is why a
Part 10 client would not have found it where Part 10 says it is, and why the
first stack to generate the model deleted it. A promotion changes the
ModellingRule and nothing else; clause 6.1 now says so.

WaitIntentDataType.Signal read "an OutputSignal or other node", which clause
11.3 cannot check, and an unvalidated NodeId is the surface clause 11.3 exists
to close. It is now bounded to an OutputSignal under the controller or a
Boolean Variable under it, and clause 11.3 tabulates the expected type of every
NodeId-valued member rather than leaving "the expected type" to the reader.

Ready, ActiveIntent, ActiveMission and ControlOwner were in the model and in no
clause; clause 5.7.0 gives them normative meaning. Clause 6.9 now bounds
RequestedLease, which the lease rules never limited.

The NodeSet, the CSV and Annex A are regenerated; no previously assigned NodeId
moves.
Implementing the specification a second time surfaced three more places where
a conformant Server could tell a client something untrue.

Clause 9 already had three rules keeping the capability declaration honest, but
none of them said that a declared capability must come with the Methods that
make it usable. SubmitMission, UpdateMission, CancelMission and both real-time
channel Methods are Optional on IntentControllerType, so a Server could report
MissionsSupported true and omit SubmitMission entirely - which is exactly what
the first implementation did, and the contradiction only surfaced when a client
called something that was not there. A fourth rule and a table now fix which
Methods each declaration implies, and like the other three it is checkable by
browsing a running Server.

Clause 6.5 gave StopMode the PossibleStopModes vocabulary of OPC 40010-1 and
then said nothing about a Server that treats every value alike, so accepting the
argument and discarding it looked conformant. A Server must now either honour it
or treat every value as its single stop behaviour, and should say which - a
client that asks for OnPath and silently receives a QuickStop has been told
something untrue about how the cell came to rest.

Clause 6.4 left the stop mode of a superseded intent undefined, because an
Aborting submission names none. The Server chooses, should choose the most
urgent stop the cell tolerates since the successor is about to command motion of
its own, and should document which.

Prose only; the model is unchanged and regenerates identically.
Clause 12 defined conformance in terms of facets and then gave a Server
nowhere to say which ones it had. A client that wanted to know whether a
controller supported blending had to re-derive the whole of Table 12.2
from the address space itself, which is what the reference implementation
ended up doing.

That is worse than verbose. Several rows of the table are behavioural --
"the four blending buffer modes accepted and honoured", "the refusal
rules of 6.2", "the base immutability rules of 7.2" -- and no amount of
browsing settles them. Two clients deriving conformance independently
could therefore reach opposite conclusions about the same Server and both
be reading the specification correctly.

OPC 10000-5 already answers this with ServerCapabilitiesType.
ServerProfileArray: the Server states its profiles rather than leaving
them to be inferred. IntentCapabilitiesType now carries SupportedFacets
on the same principle, Mandatory, naming the facets of Table 12.2
verbatim.

Clause 12.2 now separates the two kinds of requirement it had been
mixing. Structural requirements are settled by reading -- the Outputs
folder, more than one ToolType, axes covering 0 to AxisCount - 1 -- and a
Server shall not list a facet whose structural requirements are unmet.
Behavioural requirements are the Server's attestation, and clause 9 says
what that means: listing RI-Blending while treating the buffer modes as
Buffered is the same false statement rule 3 already forbids, whatever
BlendingSupported says.

RI-Base requires SupportedFacets, since a conformance claim that cannot
be read is not a claim.

The member appends at i=6139; no existing NodeId moves.
Two defects, both found by implementing the draft and reviewing the
result.

The promotions of FinalResultData and ProgramDiagnostic were emitted with
namespace-1 BrowseNames. OPC 10000-3 says a subtype overrides an
inherited InstanceDeclaration by re-declaring a child with the same
QualifiedName, and a QualifiedName includes the namespace index -- so
1:ProgramDiagnostic never overrode 0:ProgramDiagnostic, it declared a
second member beside it and left the inherited Optional one in place.
That is the same failure the TypeDefinition and reference type were
already careful to avoid, reached by a different route, and it defeated
the entire point of promoting: clause 6.7's SHALL and the clause 1.2
auditable-commanding claim still rested on members a conformant Server
could omit. The generated client made it visible -- its accessor used
`new` to hide the base Part 10 one. Both now carry namespace-0
BrowseNames, using the same mechanism already applied to DataTypeEncoding
browse names for the same reason, and every other NodeSet in the
reference stack promotes inherited state-machine members unprefixed.

Clause 6.6 required Pause to suspend execution retaining position, but
the model defines no channel through which a Server can tell an actuator
to pause mid-motion. A Server whose controller offers no pause could
therefore only publish Suspended and hope -- and Part 10 defines that
state as position retained, so an operator reading the HMI sees the arm
at rest while it finishes the move. The reference implementation did
exactly that until a review caught it.

The requirement stays, because stopping the robot is what Pause should
mean. What is added is the honesty rule the rest of clause 9 already
applies elsewhere: a Server that cannot suspend a running intent declares
PauseSupported false, may still stop its queue, and leaves the executing
operation Executing with Ready reflecting that nothing new is admitted.
Suspended is never reported while the robot is still moving. That makes
clause 9 four rules rather than three, alongside the BlendingSupported
rule it is modelled on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b
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.

1 participant