Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/DeviceIntegration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ companion-spec server that exposes a configurable number of fully
simulated `PumpType` instances. The sample uses the hand-written fluent
node-manager style for the common pump model and simulation wiring, then
uses `ConfigureDevicesFor` with the topology-element builder to add an
ad-hoc Diagnostics functional group to each pump.
ad-hoc Diagnostics functional group to each pump. The simulated asset is
specified by its own product datasheet,
[`samples/PumpDeviceIntegrationServer/DATASHEET.md`](../samples/PumpDeviceIntegrationServer/DATASHEET.md),
which maps every nameplate field, engineering range and alarm trip point
to its OPC UA browse path.

## Quick start

Expand Down
19 changes: 12 additions & 7 deletions docs/NodeManagers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1172,17 +1172,17 @@ child on demand (matching the runtime's `AddEngineeringUnits` /
`AddEURange` helpers) and then set the Value attribute.

```csharp
builder.Variable<double>("Pumps/Pump #1/Operational/Measurements/FluidTemperature")
builder.Variable<double>("Pumps/Pump_1/Operational/Measurements/FluidTemperature")
.OnRead(SimulateTemperature)
.WithEngineeringUnits(
new EUInformation("K", "Kelvin",
"http://www.opcfoundation.org/UA/units/un/cefact"))
.WithEURange(min: 233.15, max: 473.15);
.WithEURange(min: 263.15, max: 393.15);

// Convenience: set both at once.
builder.Variable<double>("Pumps/Pump #1/Operational/Measurements/Pressure")
builder.Variable<double>("Pumps/Pump_1/Operational/Measurements/DifferentialPressure")
.OnRead(SimulatePressure)
.WithUnits(EUInformations.Pascal, min: 0, max: 1_000_000);
.WithUnits(EUInformations.Pascal, min: 0, max: 400_000);
```

Fail-fast behaviour: calling these on a non-`BaseAnalogState` variable
Expand All @@ -1200,15 +1200,20 @@ Typed overloads exist for every built-in OPC UA scalar (`string`,
hatch.

```csharp
builder.Node("Pumps/Pump #1/Identification")
.WithProperty("Manufacturer", "SimPump Corp")
.WithProperty("Model", "PumpX-2000")
builder.Node("Pumps/Pump_1/Identification")
.WithProperty("Manufacturer", new LocalizedText("SimPump Corp"))
.WithProperty("Model", new LocalizedText("PumpX-2000"))
.WithProperty("SerialNumber", "SN-001")
.WithProperty("DeviceClass", "Pump")
.WithProperty("ProductInstanceUri",
"urn:simdevice:SimPump:PumpX-2000:SN-001");
```

Pass the CLR type the model declares for the property — `LocalizedText`
for `Manufacturer` / `Model` / `ComponentName`, `ushort` for
`YearOfConstruction`, `byte` for `MonthOfConstruction`, and so on. The
typed overloads make the choice explicit at the call site.

Reference resolution is by browse-name only (case-sensitive,
namespace-agnostic), matching the AOT-safe constraint of the rest of
the fluent surface. When the child exists it is updated; when it exists
Expand Down
2 changes: 1 addition & 1 deletion docs/OpenUsd.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ agree:

```csharp
NodeId target = result.ResolveBindingTargetNodeId(
"/Plant/Pumps/P101/Pump/Impeller", "xformOp:rotateZ");
"/Plant/Pumps/Pump_1/Pump/Impeller", "xformOp:rotateZ");
```

### Conformance units
Expand Down
422 changes: 298 additions & 124 deletions samples/PumpDeviceIntegrationServer/Assets/Plant.usda

Large diffs are not rendered by default.

84 changes: 39 additions & 45 deletions samples/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Regenerates the referenced pump assets from the P101 master in Plant.usda.
"""Regenerates the referenced pump asset from the P101 master in Plant.usda.

pump.usda and remote-pump.usda are the component assets a ProductionLine
references once per aggregated pump. They must carry the same geometry as
/Plant/Pumps/P101 so every pump in the line renders at full fidelity, but they
have to be self-contained: material paths are rewritten to a local Looks scope.
pump.usda is the component asset the server references once per configured
pump. It must carry the same geometry as /Plant/Pumps/P101 so every pump in the
hall renders at full fidelity, but it has to be self-contained: material paths
are rewritten to a local Looks scope.

Run from the repository root:
python samples/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
Expand All @@ -15,30 +15,26 @@
HERE = pathlib.Path(__file__).parent
PLANT = HERE / "Plant.usda"

# The P101 children that make up the machine itself. Beacon and StatusLight are
# plant-side signalling equipment, not part of the pump component asset.
# The P101 children that make up the machine itself, including the indications.
# Every configured pump is composed from this asset and driven by its own
# bindings, so each machine carries its own gauges, suction vessel and alarm
# ring rather than sharing one plant-side set.
WANTED = [
"Baseplate",
"Body",
"Casing",
"Suction",
"SuctionVessel",
"Discharge",
"PowerEnd",
"Bearing",
"OverheatHalo",
"Impeller",
"CouplingGuard",
"Motor",
"AlarmRing",
]

# KSB signal blue (RAL 5005) -> OEM moss green (RAL 6011), so the pump federated
# from the remote server is obvious at a glance.
GREEN = {
"(0.02, 0.24, 0.60)": "(0.24, 0.36, 0.20)",
"(0.02, 0.22, 0.55)": "(0.22, 0.33, 0.18)",
"(0.02, 0.20, 0.50)": "(0.20, 0.30, 0.16)",
}


def block(lines, start):
"""Returns the line range of a brace-delimited prim block starting at `start`."""
depth = 0
Expand Down Expand Up @@ -80,13 +76,10 @@ def rehome(text):
return text


def build(plant, doc, recolour):
def build(plant, doc):
parts = [looks(plant)]
parts += [extract(plant, name, 12) for name in WANTED]
body = rehome("\n\n".join(parts))
if recolour:
for blue, green in GREEN.items():
body = body.replace(blue, green)
return f'''#usda 1.0
(
doc = """{doc}"""
Expand All @@ -99,44 +92,45 @@ def Xform "Pump" (
kind = "component"
)
{{
matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
uniform token[] xformOpOrder = ["xformOp:transform"]

{body}
}}
'''


PUMP_DOC = """Reusable single-pump USD asset for the OPC UA - OpenUSD Bindings
composition demo. A ProductionLine aggregates 1..n pumps by *referencing*
(and instancing) this asset's default prim </Pump> once per aggregated pump
(spec 5.12, CompositionArc = Instance/Reference). The cross-server component
references an equivalent OEM asset (remote-pump.usda) on another server (5.14).

Generated by generate_pump_assets.py from the /Plant/Pumps/P101 master in
Plant.usda, so every aggregated pump renders at the same fidelity as the hero
pump: an EN 733 size 65-200 end-suction centrifugal pump (Grundfos NK /
KSB Etanorm proportions) driven by an IEC 160M motor on a 1.80 x 0.46 m
baseplate, shaft centreline 0.160 m above the baseplate. Do not edit by hand -
edit P101 and re-run the generator.

The plant-side signalling equipment (Beacon, StatusLight) is deliberately not
part of the component asset."""

REMOTE_DOC = """OEM pump asset served by a *different* OPC UA server, referenced
across the federation boundary by the cross-server <Component> binding
(spec 5.14). Geometrically identical to pump.usda so the composed line reads
consistently, but wearing the OEM's own livery - moss green (RAL 6011)
castings instead of KSB signal blue.
pump twin. The server composes one referenced </Pump> prim per configured
pump (--pumps N), so the rendered hall holds exactly the pumps the connected
server simulates - nothing else (spec 5.12, CompositionArc = Reference).

Generated by generate_pump_assets.py from the /Plant/Pumps/P101 master in
Plant.usda. Do not edit by hand - edit P101 and re-run the generator."""
Plant.usda, so every composed pump renders at the same fidelity: an EN 733
size 65-200 end-suction centrifugal pump (Grundfos NK / KSB Etanorm
proportions) driven by an IEC 160M motor on a 1.80 x 0.46 m baseplate, shaft
centreline 0.160 m above the baseplate. Do not edit by hand - edit P101 and
re-run the generator.

The asset carries its own instrumentation and indications - pressure and
bearing-temperature gauges, suction vessel, cooling fan, the alarm ring and
the two fault halos - because each composed pump is driven by its own
bindings and has to show its own state.

The root prim declares an identity xformOp:transform and lists it in
xformOpOrder because that is what makes the bay position binding work. A
connector resolves a Translation render target into a single matrix op, and
USD only evaluates ops named in xformOpOrder - a list that is uniform, so the
connector cannot add itself to it from a stronger layer. If the asset omits
the op, the bay position is silently discarded, every composed pump renders
on the origin, and the hall looks like it holds a single machine."""


def main():
plant = PLANT.read_text(encoding="utf-8")
(HERE / "pump.usda").write_text(
build(plant, PUMP_DOC, recolour=False), encoding="utf-8", newline="\r\n")
(HERE / "remote-pump.usda").write_text(
build(plant, REMOTE_DOC, recolour=True), encoding="utf-8", newline="\r\n")
print("wrote pump.usda and remote-pump.usda")
build(plant, PUMP_DOC), encoding="utf-8", newline="\r\n")
print("wrote pump.usda")


if __name__ == "__main__":
Expand Down
Loading
Loading