Files
UA-.NETStandard/samples/DI/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
d525bc3f28 Bound reconnect cleanup and organize samples by domain (#4276)
# Description

This PR fixes bounded ManagedSession reconnect cleanup and reorganizes
the sample applications into domain-focused folders.

## Reconnect cleanup

- Bounds reconnect integration cleanup with independent 30-second close
and dispose limits.
- Sends `CloseSession` through the awaited transport request path during
`Session.DisposeAsync`.
- Preserves explicit `Session.CloseAsync` behavior that waits for or
cancels outstanding publish requests.
- Covers the disposal request path with a regression test.

## Sample organization

- `samples/OpenUsd`: `GeneratorServer`, `SiteCompositionServer`
- `samples/Redundancy`: `RedundantClient`, `RedundantServer`,
`RedundantPubSub`
- `samples/Isa95`: `MinimalIsa95Server`
- `samples/MinimalApi`: `MinimalBoilerServer`, `MinimalCalcServer`,
`MinimalClient`
- `samples/DI`: `PumpDeviceIntegrationServer`
- `samples/Lds`: `ConsoleLdsServer`
- `samples/Reference`: `ConsoleReferenceClient`,
`ConsoleReferenceServer`
- `samples/PubSub`: `ConsoleReferencePubSubClient`

`UA.slnx` now exposes the same requested domain groups. Project
references, tests, runtime sample probes, CI workflows, Dockerfiles,
Compose files, helper scripts, and documentation were updated to the new
paths. `samples/Robotics/MinimalRobotServer` remains under `Robotics`.

## Validation

- `dotnet build UA.slnx`
- 3 Redundancy short-haul sample tests
- 70 DI sample tests
- 914 OpenUSD tests
- 1 WotCon model-materialization test
- 137 AOT tests
- `docker compose config` for all 6 moved Redundancy Compose files

## Related Issues

- Fixes #4273

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce new build or
analyzer warnings.
- [x] I built the complete `UA.slnx` solution locally.
- [ ] I ran all tests locally against at least .NET Framework and .NET
10.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <mschier@microsoft.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
2026-08-21 04:35:47 +00:00

138 lines
4.6 KiB
Python

"""Regenerates the referenced pump asset from the P101 master in Plant.usda.
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/DI/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
"""
import pathlib
import re
HERE = pathlib.Path(__file__).parent
PLANT = HERE / "Plant.usda"
# 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",
]
def block(lines, start):
"""Returns the line range of a brace-delimited prim block starting at `start`."""
depth = 0
index = start
seen = False
while index < len(lines):
depth += lines[index].count("{") - lines[index].count("}")
if "{" in lines[index]:
seen = True
if seen and depth == 0:
return start, index + 1
index += 1
raise ValueError(f"unterminated block at line {start}")
def extract(text, name, indent):
"""Extracts the named prim block at the given indent, dedented to column 4."""
lines = text.splitlines()
pattern = re.compile(rf'^{" " * indent}def \w+ "{re.escape(name)}"')
for index, line in enumerate(lines):
if pattern.match(line):
first, last = block(lines, index)
body = lines[first:last]
trim = indent - 4
return "\n".join(line[trim:] if line.startswith(" " * trim) else line
for line in body)
raise ValueError(f"prim {name!r} not found at indent {indent}")
def looks(text):
"""Extracts the shared Looks scope and rehomes it under /Pump."""
scope = extract(text, "Looks", 4)
return scope.replace("</Plant/Looks/", "</Pump/Looks/")
def rehome(text):
text = text.replace("</Plant/Looks/", "</Pump/Looks/")
text = text.replace("</Plant/Pumps/P101/", "</Pump/")
return text
def build(plant, doc):
parts = [looks(plant)]
parts += [extract(plant, name, 12) for name in WANTED]
body = rehome("\n\n".join(parts))
return f'''#usda 1.0
(
doc = """{doc}"""
defaultPrim = "Pump"
metersPerUnit = 1
upAxis = "Z"
)
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
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, 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), encoding="utf-8", newline="\r\n")
print("wrote pump.usda")
if __name__ == "__main__":
main()