ModelingEvolution.RocketWelder.StationManagement 0.1.0-preview.7fcc18f4
ModelingEvolution.RocketWelder.StationManagement
rw2 as one piece of a station environment (epic-107, docs/epics/epic-107-station-twin/). Built on
ModelingEvolution.StationManagement — the foundation that knows containers, networks and waits, and no domain nouns.
This package is a client of rw2: of its container image and of its published REST language. It references no rw2 application project, and no vendor or simulator package.
await using var host = await StationHost.Start("pamet-01"); // one station = one network, fixed IPs
var store = await host.EventStore("rw2-store", reachableFromHost: true);
var rw = await host.RocketWelder("2.21.34", store, simulatorTrack: true, sinkPorts: [17002]);
// ↑ always a container; add build: "../rocket-welder2/bin/Debug" to mount the branch under test over /app
var fr20 = await rw.Devices.Ensure(real.Robot("FR20").SimulatedBy(Simulator.Robot, sim));
await rw.Devices.Ensure(real.Track("7th Axis").SimulatedBy(Simulator.Track, sim)
.Keeping(x => x.Travel, x => x.Speed, x => x.Tolerance));
await rw.Cameras.Ensure(real.Camera("41943012").SimulatedBy(Mjpeg.Camera, sim.Preview, sinkPort: 17002, boundTo: fr20));
await rw.Programs.Ensure("Pamet", "AxisThenArm", p => p.AxisMove(500).CapturePoint("A").MoveLinear("A"));
var run = await rw.Programs.Run("AxisThenArm");
await run.ShouldFinishWithin(2.Minutes());
Console.WriteLine(host.Differences.Render());
| Rule | Where |
|---|---|
rw2 is ALWAYS a container; a branch under test is a bin/Debug mounted over /app |
RocketWelderHosting |
A mounted build's machine state never leaks in: plugin path, recordings, FsRoot, repositories are always set |
RocketWelderHosting.Spec |
| A sink a human is meant to see is published on the SAME number — rw2 tells the browser to open it directly | RocketWelderHosting.Spec, RocketWelder.Sink |
Commands are not queries: every Ensure posts, then waits for the READ MODEL |
RwDevices, RwPipelines |
The proof of a camera is a JPEG off its tcpserversink, never rw2's own status |
RwPipelines.WaitServing |
| A value rw2 refuses becomes a stated difference, never a silent default | Substitutions.Keeping, RwDevices.Ensure |
A run is asserted on ProgramExecution-<programId> in the station's own store |
RunHandle |
| The MJPEG pipeline graph travels with the PACKAGE; a station says where the stream is | CameraPipeline |
| A station writes its program, not rw2's wire shape — no etag, no JSON, no anchors | RwPrograms.Ensure, ProgramBuilder |
| A station can state a difference only it knows | Substitutions.NotCovering |
| Anything this package does not wrap is reached through rw2's own typed client | RocketWelder.Api |
rw2 runs with ASPNETCORE_ENVIRONMENT=Development — the customer never does |
RocketWelderHosting.Spec |
| A value rw2 cannot be asked about is never reported as kept | RwDevices.Unverifiable |
Why Development, when the point is to run what the customer runs
seed-handeye — rw2's only endpoint that binds a camera's intrinsics and mount — is served only when rw2 runs in
the Development environment (CamerasEndpoints.cs:98 returns 404 otherwise, RW-3). A twin that could not carry the
station's calibration would not be a twin, so the host always sets it, and this is the one place a twin knowingly
differs from a customer's cell. It is also the switch that changed the linear-track default on 2026-09-17, which is why
simulatorTrack: is an explicit parameter rather than an inherited default.
How it matches the station
Real() · EmulatedBy(endpoint) · SimulatedBy(deviceType, endpoint) · .Keeping(x => …) produce both the rw2
registration and a Match, so a twin always says how alike it is:
FR20 simulated Simulator not: the Fairino driver
7th Axis simulated SimulatorTrack not: the FairinoTrack driver, travel, speed, tolerance (rw2 refused: 400 …)
Megmeet simulated SimulatorWeldingMachine not: the MegmeetWeldingMachine_ArtsenPlusPro driver
Plasma simulated SimulatorWeldingMachine not: the FairinoDoWeldingMachine driver
41943012 simulated MjpegCamera calibration kept · not: the Camera driver
.Keeping(x => x.Travel, …) is not decoration. rw2's SimulatorTrack driver clamps every commanded position
client-side into its own TravelMinMm/TravelMaxMm (SimulatorTrackConfig.cs:31-34 and Clamp at :83), whose
defaults are ±1000 mm. A Pamet script that sets the rail's travel on the simulator but not on the rw2 device cannot
command a 5000 mm move at all — it is silently clamped. Set the same travel on both.
Two values are demoted rather than claimed:
- The
travel, speed, tolerance (rw2 refused: …)line is RW-1:POST /api/peripheral-devicesvalidates against the registered property types rather than the device type's own schema, soSimulatorTrack's declaredTravelMinMm,MaxSpeedMmPerSecondandToleranceMmare answered with400. The device is registered without them and the twin says so. When RW-1 lands, those three move to kept with no change to a station script.
Only rw2's OWN device types live here (Simulator, SimulatorTrack, SimulatorWeldingMachine,
SimulatorDistanceSensor, MjpegCamera). A vendor's device type is a string in the station's description — this
package never references a vendor package.
Writing a station script
A station script is usually a file-based C# app (dotnet run up.cs). Those disable reflection-based JSON, and
every call in this package would fail with "Reflection-based serialization has been disabled for this
application" — so a script needs one line above its #:package directives:
#:property JsonSerializerIsReflectionEnabledByDefault=true
Test
dotnet build src/StationManagement.sln
dotnet test src/StationManagement.sln --filter "Category!=Docker" # pure logic
dotnet test src/StationManagement.sln --filter "Category=Docker" # a real station
This package has its own solution, src/StationManagement.sln, and is deliberately NOT in src/RocketWelder.sln:
rw2's CI and its docker build must never restore from the internal feed over a package rw2 does not ship.
The Docker suite brings a whole twin up on a developer machine — it needs the rw2 image (20 GB) and
modelingevolution/simulator:local, so it never runs in CI.
Authoring a program headlessly
Our own test programs, written as the program reads. Etags, JsonObjects and block anchors stay inside the package:
await rw.Programs.Ensure("Pamet", "AxisThenArm", p => p
.AxisMove(500) // the external axis, mm — omit the axis name where the cell leaves no choice
.CapturePoint("A") // teaches the pose the cell is STANDING IN, then declares it
.MoveLinear("A")); // a straight-line arm move; travel defaults to "20%"
var run = await rw.Programs.Run("AxisThenArm");
await run.ShouldFinishWithin(2.Minutes());
Ensure creates the repository, the program and its blocks and compiles it, and is idempotent — a station script
run twice converges instead of authoring the program twice. Anything beyond these three steps is rw2's own
rw.Api.Programs (block editing, arcs, corners, splines); this package does not re-wrap it.
Worth knowing before writing station tests:
CapturePointreads the live cell, and refuses if anything moved, is notStandstill, is faulted, unhomed or slow to answer (400; a 422 means there is no repository directory to teach into). It also records every registered axis in the point, and at run time a move to it is refused unless each axis is still at its taught value — so park the axis where the run will have it, then capture. That ordering is what makesAxisMove(500) … CapturePoint("A") … MoveLinear("A")work: the point is taught at 500 mm, and the run drives the axis there before the arm moves.- Blocks bind devices by INTERFACE, never by name — the generated program resolves
IRobot/ILinearTrack, so a device registered only under a name is unreachable. - A non-dry run of a program with an enabled
Pauseis refused (400): nobody is at a run screen to resume it. AxisMoveneeds rw2 2.21.35 (RW-10 — an older rw2 refuses that one step with 400 naming the block type), and so does.Keeping(x => x.Travel, …)carrying the station's axis values (RW-1).- An out-of-travel target behaves differently in a twin than on the cell. rw2's
SimulatedTrackdriver CLAMPS a commanded position into its own travel (SimulatorTrackConfig.Clamp, ±1000 mm by default) where a real Fairino REJECTS it withOutOfRange— so never write a twin test that expects a refusal there; it will pass on the cell and fail in the twin, or worse, the reverse.
Which rw2 it works against
Minimum: rw2 2.21.18, and 2.21.35 for two things: authoring an AxisMove over REST (RW-10) and an axis that
carries the station's travel, speed and tolerance (RW-1). Everything else works from 2.21.18. That is the first release carrying the headless POST /api/repositories/{repoId}/programs
this package creates its own programs with (git tag --contains 3a2d6395). It also needs
PATCH /api/pipeline/{id}/autostart, POST /api/pipeline/import, and — for Cameras.Ensure to bind a calibration —
POST /api/cameras/calibration/seed-handeye, which rw2 serves only in the Development environment (the host sets
ASPNETCORE_ENVIRONMENT=Development for exactly that reason).
Proven against 2.21.34, the version the integration suite runs. Anything between 2.21.18 and that is untested.
Published
Internal feed only (nuget.modelingevolution.com) — never nuget.org.
| A release | tag station-management-v1.2.3 → .github/workflows/publish-station-management-nuget.yml |
| A feature branch | X.Y.Z-preview.<sha> → .github/workflows/preview-station-management-nuget.yml |
The package carries its own version, not rw2's image version: its surface moves when rw2's REST language moves, which is much rarer than an image build. What ties it to rw2 is the minimum version stated above.
No packages depend on ModelingEvolution.RocketWelder.StationManagement.
.NET 10.0
- ModelingEvolution.StationManagement (>= 0.1.1)
- ModelingEvolution.StationManagement.KurrentDb (>= 0.1.1)
- RocketWelder.SDK.Http (>= 2.26.1)
| Version | Downloads | Last updated |
|---|---|---|
| 0.3.1-preview.f72598b12 | 4 | 10/11/2026 |
| 0.3.1-preview.becb30244 | 43 | 10/07/2026 |
| 0.3.1-preview.ab52494fc | 0 | 10/07/2026 |
| 0.3.1-preview.aa0b72dc8 | 0 | 10/11/2026 |
| 0.3.1-preview.7933f6e4b | 0 | 10/07/2026 |
| 0.3.1-preview.52c3c4d2a | 22 | 10/07/2026 |
| 0.3.1-preview.388e4a9ab | 46 | 10/11/2026 |
| 0.3.1-preview.332633ffd | 0 | 10/07/2026 |
| 0.3.0 | 6 | 09/28/2026 |
| 0.2.2-preview.f8d5e697f | 0 | 09/28/2026 |
| 0.2.2-preview.f13421609 | 0 | 09/28/2026 |
| 0.2.2-preview.c7d3e39d3 | 0 | 09/28/2026 |
| 0.2.1 | 2 | 09/25/2026 |
| 0.2.1-preview.8952a5eb | 0 | 09/25/2026 |
| 0.2.1-preview.7da5ea94 | 0 | 09/25/2026 |
| 0.2.1-preview.4ada13b8 | 0 | 09/25/2026 |
| 0.2.1-preview.66486488 | 0 | 09/25/2026 |
| 0.2.0 | 1 | 09/25/2026 |
| 0.1.0-preview.e705254e | 0 | 09/25/2026 |
| 0.1.0-preview.e32fdfa2 | 2 | 09/18/2026 |
| 0.1.0-preview.bcbf4393 | 0 | 09/25/2026 |
| 0.1.0-preview.ae609c75 | 1 | 09/18/2026 |
| 0.1.0-preview.887555e | 0 | 09/18/2026 |
| 0.1.0-preview.81feef9 | 0 | 09/18/2026 |
| 0.1.0-preview.7fcc18f4 | 0 | 09/18/2026 |
| 0.1.0-preview.5f287869 | 1 | 09/18/2026 |
| 0.1.0-preview.2c875b27 | 1 | 09/18/2026 |
| 0.1.0-preview.7093278 | 0 | 09/18/2026 |