Self-contained models (pack, unpack, reverse --self-contained)
A model normally lives as a pair of files: <name>.model.yaml, which you write, and
<name>.model.symbols.csv, which generate maintains so that every node keeps its
NodeId across regenerations. The self-contained profile puts the pair, plus what the
model needs from its dependencies, into one file: <name>.opcua.yaml.
# pair → one file
opcua-modeler pack -i pump.model.yaml
# writes pump.opcua.yaml
# one file → pair
opcua-modeler unpack -i pump.opcua.yaml
# writes pump.model.yaml and pump.model.symbols.csv
# NodeSet2.xml → one file, in a single step
opcua-modeler reverse -i Opc.Ua.Pumps.NodeSet2.xml --self-contained
# writes Opc.Ua.Pumps.opcua.yaml
The design is specified in the Self-Contained Profile working draft; this page is the practical summary.
What is inside an .opcua.yaml
The file is a YAML stream: several documents separated by ---.
| Document | Written by | Content |
|---|---|---|
| first document | you | your model, exactly as in .model.yaml, plus profile: self-contained |
kind: symbols | the tool | the NodeId table as a tree that follows your model's sections |
kind: declarations | the tool | for a dependency that is not a well-known companion spec: the nodes your model references, with their NodeIds |
Everything below the first --- is generated. Editors fold it; you never edit it by hand.
namespaceUri: http://acme.com/UA/Pump/
version: 1.0.0
profile: self-contained
namespaces: [di]
objectTypes:
- browseName: CentrifugalPumpType
subtypeOf: di:DeviceType
components:
- { browseName: Speed, typeDefinition: ua:AnalogUnitType, dataType: ua:Double }
---
kind: symbols
namespace: http://acme.com/UA/Pump/
format: 1
sealed: sha256:9a07c2…
nextId: 1004
assigned:
objectTypes:
CentrifugalPumpType:
$id: 1001
Speed:
$id: 1002
ua:EngineeringUnits: 1003
The symbol tree reads like the model: a leaf is Name: id, a node with children is a
mapping whose own id is $id. Browse names from another namespace keep their prefix
(ua:EngineeringUnits, di:SerialNumber), so a name with an underscore can never be
misread as two levels.
retired, reserved, sealed, nextId
retiredkeeps the ids of nodes that are no longer in the model (or that a symbol CSV listed without the model emitting them). A node that comes back at the same path regains its id. Retired ids are never reallocated.reservedblocks ids or ranges that must never be allocated here, with a reason.sealedis a digest of the model document. When it no longer matches, the table was written for an older model; the tool says how many nodes have no id yet.nextIdis the next free id; allocation never goes below it.
pack
opcua-modeler pack -i <model.yaml> [-o <name>.opcua.yaml] [--symbols <file.csv>] [--declarations private|all] [--force]
pack compiles the model with its CSV, then writes the stream. The compile is what
makes the tree exact: a CSV alone cannot tell A_B/C from A/B_C. If the model does not
compile, nothing is written.
| Option | Meaning |
|---|---|
-i, --input | <name>.model.yaml with its <name>.model.symbols.csv beside it, or an existing .opcua.yaml to re-pack |
-o, --output | default: <name>.opcua.yaml next to the input |
-s, --symbols | another CSV to seed from |
--declarations | private (default): declaration documents only for dependencies that are not well-known companion specs, since every processor bundles those. all: one for every dependency, for a file readable without any tooling |
-f, --force | overwrite an existing output |
unpack
opcua-modeler unpack -i <name>.opcua.yaml [-o <name>.model.yaml] [--force]
Writes the model document back to <name>.model.yaml and the table to
<name>.model.symbols.csv, retired ids included as rows. unpack compiles the model in
frozen mode against the embedded table: every node must have an id and every id must
name a node, otherwise it refuses. That is also the check to run in CI on a file someone
sent you.
reverse --self-contained
opcua-modeler reverse -i <NodeSet2.xml> --self-contained [--declarations private|all] [--keep-pair]
Reverses the NodeSet as usual, then packs the result with the NodeIds of the XML as the
table. The dependencies named in <RequiredModel> are resolved from a pinned copy next
to the input, from dependencies/ or model/dependencies/, or from the bundled
companion specifications. --keep-pair also leaves the intermediate .model.yaml and
.model.symbols.csv on disk.
The hosted API offers the same through POST /api/v1/reverse?profile=self-contained
(&declarations=all optional); the response carries profile: "self-contained". The MCP
tool opcua_model_reverse takes self_contained: true.
generate on an .opcua.yaml
opcua-modeler generate -i plant.opcua.yaml [--frozen]
generate reads the file directly. The NodeIds come from the file's symbol
document (a sidecar CSV is not read), the NodeSet2 is written as usual, and on
success the symbol document is rewritten in the file: new nodes get ids,
nodes that disappeared move to retired, the digest is resealed. Other tail
documents are left untouched.
- If the model was edited after the table was sealed,
generatesays so and how many nodes have no id yet, then allocates them. --frozenrefuses instead: any node without an id, or any id without a node, is an error. That is the check to run in CI on a file you received.
How a self-contained file is read back
For every dependency named in the model, the tool tries, in order, and uses the first hit:
- the
filenamehint of the matchingimportsentry; - a NodeSet2 file in the same folder,
dependencies/ormodel/dependencies/whose<Model ModelUri>matches; - the bundled copy, for a well-known companion specification;
- the
kind: declarationsdocument in the file, materialised as a minimal NodeSet2 holding exactly the declared nodes.
When step 4 is what compiled a dependency, generate says so in a warning. The
winning source is compared with the pins (version, publicationDate, sha256);
a mismatch is a warning. The model's own namespace is never resolved: its ids come from
the symbols document.
A declared DataType carries its definition (enumeration values, structure fields and their types, unions) and its encoding objects, so a model that uses a foreign enumeration or structure compiles from the shim as it would from the real file.
Embedding a private dependency as a full model (kind: model), and the rename
and move operations that keep an id across a rename, are the next steps of this
profile.