# Description The Scales and Machinery node managers publish their facets and conformance units through `IConformanceContributor`. This aligns what they advertise with the published tables. **Machinery** (`fix(machinery): advertise the ProcessIrregularity job result unit`) - Adds *Machinery Job Management Result ProcessIrregularity* to the units reported for `WithPredefinedParameters()`. OPC 40001-3 Table 24 lists it, the Jobs NodeSet carries it as a `<Category>`, and `MachineryJobParameters` already recognises the parameter. - New test `PredefinedParametersAdvertiseEveryParameterUnitOfTheBaseFacetAsync` checks all Table 24 parameter units. **Scales** (`fix(scales): advertise only the facets the runtime satisfies`) - Base Scale (and every kind facet), Scale System, Feeder Module and Printer Module include the Machinery Machine Identification facet (OPC 40200 Tables 164 to 167). While every published scale and scale system is organized into the `Machines` folder, the server now also advertises that facet and its mandatory units *Machinery Find Machines* and *Machinery Machine Identification*. When a scale is not in the folder (`OrganizeIntoMachinesFolder = false`, or the folder is missing) those facets are no longer advertised. Production preset and SI unit facets do not include Machine Identification and are unaffected. - The Totalizing Hopper, Catchweigher and AutomaticWeightPriceLabeler kind facets and units are no longer claimed: no controller drives them yet (known limitations in `docs/Scales.md`). The scales still count towards Base Scale. - *PackML State Information* (Table 164, optional) is advertised when a scale or scale system runs a PackML state machine, qualified with the PackML namespace as the table writes it. Machinery units use namespace 0, as `MachineryNodeManager` reports them. - `docs/Scales.md` updated (Machines folder, options table, conformance matrix, known limitations). - New tests: `ScaleFacetsBringTheMachineIdentificationTheyIncludeAsync`, `ScaleFacetsAreNotClaimedOutsideTheMachinesFolderAsync`, `KindFacetsWithoutAControllerAreNotClaimedAsync`, `PackMLStateInformationIsClaimedWithAPackMLStateMachineAsync`. Each new test fails against master and passes with the change. Fork CI (full matrix incl. macOS) and CodeQL: green — [CI run 36779541067](https://github.com/biancode/UA-.NETStandard/actions/runs/36779541067), [CodeQL run 36779544537](https://github.com/biancode/UA-.NETStandard/actions/runs/36779544537). ## Related Issues - Fixes #4614 ## Checklist - [x] 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. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
40 KiB
OPC UA for Weighing Technology (OPC 40200)
The Opc.Ua.Scales* packages implement
OPC 40200
Weighing Technology 2.00, the companion specification for scales and scale
systems. It builds on OPC 40001-1 Machinery, OPC 10000-100 Device
Integration and OPC 30050 PackML.
- Server side: fluent builders publish scales with a working runtime. That runtime covers weighing arithmetic, zero and tare, product handling, statistics, the type-specific methods, recipes, PackML state machines, events and alarms.
- Client side: a client discovers the scales, reads them into typed snapshots, drives their methods and streams weights and notifications.
Contents
- Library layout
- The model
- Where a scale lives
- Quick start — server
- Quick start — client
- Server hosting
- Builder reference
- Runtime reference
- Client reference
- Contracts reference
- Conformance matrix
- Samples
- Testing
- Troubleshooting
- Known limitations
- Model provenance
- Where the specification and the NodeSet disagree
- See also
Library layout
| Package | Contents |
|---|---|
Opc.Ua.PackML |
The source-generated OPC 30050 PackML model. It also has PackMLStateMachineController, which drives the nested base / machine / execute state machines with their transition events |
Opc.Ua.Scales |
The source-generated OPC 40200 model. ScalesModel maps the 14 scale kinds, their product types and the Annex C notifications. ScalesProfiles holds the 21 facet URIs. The package also has the contracts both sides share |
Opc.Ua.Scales.Server |
ScalesNodeManager, the builders IScaleBuilder / IScaleSystemBuilder / IScaleModuleBuilder / IProductionPresetBuilder, the runtime handles and controllers, and the AddScales() / ConfigureScales() hosting pipeline |
Opc.Ua.Scales.Client |
ScalesClient for discovery, weighing, products, PackML, the type methods, recipes, snapshots and streaming. Also AddScalesClient() |
The contracts (ScaleIdentification, WeighingRangeDefinition,
ScaleReading, ScaleSnapshot, …) live in the model package. The server
fills them from its process and the client reads them back, and neither side
references the other.
All four packages target the stack's library frameworks. They are marked
NativeAOT-compatible on net10.0, and so are both samples.
The model
| Model URI | C# namespace | Model loader |
|---|---|---|
http://opcfoundation.org/UA/Scales/V2/ |
Opc.Ua.Scales |
AddOpcUaScales |
http://opcfoundation.org/UA/PackML/ |
Opc.Ua.PackML |
AddOpcUaPackML |
A Scales server must also expose the DI, IA, Machinery and PackML namespaces
(§13.1, Table 186). ScalesNodeManager loads all five into one node manager.
ScaleDeviceType is abstract. Every scale is one of 14 concrete subtypes,
and ScaleKind names them:
ScaleKind |
ObjectType | Product type | Facet | Runtime controller |
|---|---|---|---|---|
Simple |
SimpleScaleType |
SimpleProductType |
Simple Scale | — |
Laboratory |
LaboratoryScaleType |
SimpleProductType |
Laboratory Scale | LaboratoryController |
Hopper |
HopperScaleType |
SimpleProductType |
Hopper Scale | HopperController |
WeighingModule |
WeighingModuleType |
SimpleProductType |
Weighing Bridge | — |
AutomaticFilling |
AutomaticFillingScaleType |
AutomaticFillingProductType |
AutomaticFillingScale | AutomaticFillingController |
Catchweigher |
CatchweigherType |
CatchweigherProductType |
Catchweigher | zones in ScaleProductionPreset |
Checkweigher |
CheckweigherType |
CheckweigherProductType |
Checkweigher | checkweigher statistics |
AutomaticWeightPriceLabeler |
AutomaticWeightPriceLabelerType |
AutomaticWeightPriceLabelerProductType |
AutomaticWeightPriceLabeler | zones |
Continuous |
ContinuousScaleType |
ContinuousProductType |
Continuous Scale | ContinuousController |
LossInWeight |
LossInWeightScaleType |
ContinuousProductType |
LossInWeight Scale | LossInWeightController |
PieceCounting |
PieceCountingScaleType |
PieceCountingProductType |
PieceCountingScale | PieceCountingController |
Recipe |
RecipeScaleType |
RecipeProductType |
RecipeScale | RecipeController |
TotalizingHopper |
TotalizingHopperScaleType |
TotalizingHopperProductType |
Totalizing Hopper Scale | — |
Vehicle |
VehicleScaleType |
VehicleProductType |
Vehicle Scale | VehicleController |
ScalesModel.ObjectTypeOf,KindOf,ProductTypeOfandAllKindsmap between the kinds and the type NodeIds.ScalesProfiles.FacetOf(kind)returns the facet URI of a kind.ScaleSystemTypegroups several scales.FeederModuleTypeandPrinterModuleTypeare sub-devices of a scale.
The weight itself is a WeightItemType variable (§9.3) with a WeightType
value of Gross, Net and Tare. Properties such as Overload,
Underload, TareMode, WeightStable, InsideZero and CurrentRangeId
qualify it. Every scale has CurrentWeight; RegisteredWeight holds the last
value RegisterWeight recorded.
Where a scale lives
A scale is both a DI device and a Machinery machine, so a conforming server publishes the top-level node in two places:
- as a child of the DI
DeviceSet, and - through an
Organizesreference from the MachineryMachinesfolder. The Scales specification never mentions this folder. It is still required transitively, because the mandatory Machinery Machine Identification facet includes the CU Machinery Find Machines.
ScalesNodeManager does both by default
(ScalesServerOptions.OrganizeIntoMachinesFolder). With the option turned
off, the server no longer advertises the facets that include Machinery
Machine Identification: Base Scale and every kind facet, Scale System,
Feeder Module and Printer Module. The production preset and SI unit facets
do not include it and stay advertised.
Everything below a top-level node follows the DI modular-device pattern (§6.3):
DeviceSet
├── PackingLine1 ScaleSystemType (notifier)
│ └── SubDevices
│ └── Checkweigher1 CheckweigherType (notifier, HasNotifier from PackingLine1)
│ └── SubDevices
│ ├── Infeed FeederModuleType (notifier, HasNotifier from Checkweigher1)
│ └── Labeler PrinterModuleType (notifier, HasNotifier from Checkweigher1)
└── AnalyticalBalance LaboratoryScaleType (notifier)
- SupportedTypes. The NodeSet's
SupportedTypesfolders carry noOrganizesreferences. The builders add them per instance. - Event notifiers. Every event source is a notifier: scale systems,
scales, weighing modules, and feeder and printer modules. A nested source is
linked from its nearest notifier ancestor by
HasNotifier. A client can therefore subscribe to a nested scale directly, or to the system and receive the events of everything below it.
Quick start — server
builder.Services
.AddOpcUa()
.AddServer(options => { /* application name, endpoints, … */ })
.AddScales()
.ConfigureScales(async context =>
{
ScalesNodeManager manager = context.Manager;
ScaleHandle scale = await manager.CreateScaleAsync(
new QualifiedName("AnalyticalBalance", manager.InstanceNamespaceIndex),
ScaleKind.Laboratory,
b => b
.WithIdentification(new ScaleIdentification
{
Manufacturer = new LocalizedText("Contoso Lab"),
SerialNumber = "AB-220",
ProductInstanceUri = "urn:contoso:weighing:AB-220"
})
.WithWeighingRange(new WeighingRangeDefinition(0, 0.22, 0.0000001, 0.000001))
.WithUnit(ScaleUnits.Kilogram)
.WithAllowedEngineeringUnits(ScaleUnits.Kilogram, ScaleUnits.Gram)
.WithWeightDetails()
.WithZeroAndTare()
.WithRegisterWeight()
.WithTypeFeatures(),
context.CancellationToken);
// Keep the handle; the process publishes loads through it.
scale.PublishLoad(0.1);
});
WithIdentification and at least one WithWeighingRange are required,
because Identification and <ListOfWeighingRanges> are mandatory (§7.4.2).
Every other member is materialised only when a builder call asks for it, and
the runtime binds exactly what was materialised. For example, a scale built
without WithZeroAndTare has no SetZero method, not one that fails.
Quick start — client
ScalesClient scales = session.Scales(telemetry); // registers the OPC 40200 structures
if (!scales.IsSupported)
{
return;
}
await foreach (ScaleEntry entry in scales.EnumerateScalesAsync(ct))
{
ScaleSnapshot snapshot = await scales.ReadSnapshotAsync(entry, ct);
Console.WriteLine($"{entry.DisplayName.Text}: {snapshot.CurrentWeight.Gross}");
}
await scales.SetTareAsync(scaleId, ct);
ScaleReading? registered = await scales.RegisterWeightAsync(scaleId, ct);
await foreach (ScaleReading reading in scales.ObserveWeightAsync(scaleId, cancellationToken: ct))
{
Console.WriteLine(reading.Net);
}
Create the client through session.Scales(...). It registers the OPC 40200
encodeable types, so WeightType and the recipe structures decode into their
generated classes. Without it they arrive as ExtensionObject.
Server hosting
AddScales and ConfigureScales
AddScales(configure)registersScalesNodeManagerand validates the options. It also claims the Device Integration address space, because the node manager loads DI itself.ConfigureScales(...)registers a delegate that runs once the address space exists. It comes in sync and async overloads.- Delegates run in registration order.
- An exception aborts server startup.
- The delegate receives an
IScalesSetupContextwith theManager, theCancellationTokenandGetRequiredService<T>().
A second DI owner, such as AddOpcUaDi, is rejected with an
InvalidOperationException at registration time.
Options
ScalesServerOptions |
Default | Meaning |
|---|---|---|
OrganizeIntoMachinesFolder |
true |
Add top-level scales and systems to the Machinery Machines folder; required for every facet that includes Machinery Machine Identification |
RequireSiUnits |
false |
Accept only SI mass units as method inputs and advertise the International System of Units facet |
PackMLInitialState |
Idle |
The state a PackML state machine starts in (Aborted, Stopped or Idle) |
ZeroSettingRange |
0.04 |
The zero band as a fraction of the capacity, for SetZero, InsideZero and underload |
AdditionalNamespaceUris |
[] |
Extra namespaces for vendor extensions |
Creating scales
ScalesNodeManager member |
Purpose |
|---|---|
CreateScaleAsync(name, kind, configure[, parent]) |
A scale below the DeviceSet, or below an explicit parent |
CreateScaleSystemAsync(name, configure) |
A scale system with its scales |
Scales, ScaleSystems, FindScale(nodeId) |
The handles of everything created, weighing modules and system scales included |
LockService |
The DI ILockService behind product locking |
NamespaceIndices |
The runtime indices of the Scales, DI, Machinery, PackML and IA namespaces |
ServerProfiles, ConformanceUnits |
What the server advertises, derived from what was built |
Builder reference
IScaleBuilder
| Call | Materialises |
|---|---|
WithIdentification(identification) |
The mandatory Machinery Identification add-in, mirrored onto the DI nameplate. Manufacturer, SerialNumber and ProductInstanceUri are required |
WithWeighingRange(range) |
One WeighingRangeElementType. Call it once per range of a multi-range scale; ranges are ordered by capacity |
WithUnit(unit) |
The engineering unit of every weight (default kilogram) |
WithAllowedEngineeringUnits(units) |
AllowedEngineeringUnits, which SetPresetTare checks against (§7.4.3). Published automatically with the scale unit when a method takes a unit |
WithMinimalWeight(weight) |
MinimalWeight |
WithLegalForTrade() |
LegalForTrade. Weights round to the verification interval e instead of d |
WithWeightDetails() |
The optional WeightItemType properties: WeightStable, InsideZero, CenterOfZero, GrossNegative, CurrentRangeId, WeightId, HighResolutionValue, PrintableValue |
WithZeroAndTare(presetTare) |
SetZero, SetTare, ClearTare and, unless presetTare is false, SetPresetTare |
WithRegisterWeight() |
RegisterWeight and RegisteredWeight |
WithProductionPreset(configure) |
ProductionPreset with products; see below |
WithProductionOutput() |
ProductionOutput statistics (CheckweigherStatisticType counters on a checkweigher) |
WithPackMLState() |
The PackML State machine, driven by PackMLStateMachineController |
WithMachineryBuildingBlocks() |
MachineryItemState, MachineryOperationMode and the MachineryBuildingBlocks folder, wired as add-ins |
WithProcessState(id, message) |
ProcessStateId and ProcessStateMessage |
WithTypeFeatures() |
Every optional member of the kind, for example the laboratory draft shields, the vehicle weighing methods, recipe management, or the continuous-scale figures and master totalizer |
WithRecipeFiles() |
A RecipeFile on every recipe of a recipe scale (FileRecipeManagement) |
AddTotalizer(name) |
A <Totalizer> on a continuous or loss-in-weight scale |
AddFeederModule(name, configure) / AddPrinterModule(name, configure) |
Sub-devices below SubDevices |
AddWeighingModule(name, configure) |
A WeighingModuleType sub-device. It is a scale of its own with its own builder and handle |
With<TState>(configure) |
Direct access to the typed node for anything not covered above |
IProductionPresetBuilder
| Call | Effect |
|---|---|
AllowSelection() |
SelectProduct, DeselectProduct, SwitchProduct and CurrentProducts (SelectProduct) |
AllowManagement() |
AddProduct and RemoveProduct (ManageProduct, DynamicProductAddressSpace) |
WithLocking() |
A DI Lock on every product. A client changes a product only while holding the lock (§7.8.3) |
AddProduct(id, name, configure) |
A product of the kind's product type. configure gets the typed node, for example to set a checkweigher's NominalWeight |
Select(id) |
Puts the product into processing |
A preset without AllowManagement is a StaticProductAddressSpace. It needs
at least one product, because <Product> is a mandatory placeholder.
IScaleSystemBuilder and IScaleModuleBuilder
A system takes WithIdentification, WithProcessState,
WithProductionPreset, WithProductionOutput, WithPackMLState (its
SystemState) and WithMachineryBuildingBlocks. AddScale(name, kind, configure) adds each scale. §7.3.3 says the scales of one system must be of
different types. The builder does not enforce this.
A module takes WithIdentification, whose identification node is a
MachineryComponentIdentificationType because the declared
MachineryItemIdentificationType is abstract, and
WithMachineryBuildingBlocks. A feeder also takes WithFeederSpeed(min, max, unit, initial). A printer takes WithLabel(labelTypeId, length, width, unit).
Runtime reference
ScaleHandle
The application publishes the raw load. The handle does the rest.
| Member | Purpose |
|---|---|
PublishLoad(rawLoad, stable) |
Evaluates the load and publishes CurrentWeight with every property |
SetZero(), SetTare(), ClearTare(), SetPresetTare(value, unit), RegisterWeight() |
The same operations the methods run, for local operator panels |
CurrentReading, RegisteredReading |
The last published values as ScaleReading |
WeightRegistered |
Raised on every registration, whether by method or locally |
CommandInterceptor |
A Func<ScaleCommand, ServiceResult> that can veto any method before it runs, for example while the machine is interlocked |
Notifications |
Events and alarms, see below |
PackML, ProductionPreset, ProductionOutput |
The PackML controller, the product runtime and the statistics |
Continuous, LossInWeight, PieceCounting, Laboratory, Hopper, Vehicle, Recipes, AutomaticFilling |
The kind-specific controller, or null |
Modules, WeighingModules |
Feeder/printer handles and weighing-module scale handles |
SetItemState, SetOperationMode, SetProcessState |
The Machinery states and the process state |
Update(action) |
Runs application code that writes several values of the scale (for example typed nodes reached through With<TState>) as one step |
Threading. The runtime members of a scale, of its type controller and of
its feeder and printer modules take the scale's lock, so they are safe to call
from any thread. A client method call and an equipment-side report such as
SetActivity or CompleteCalibration therefore never interleave: a method
that checks a flag sees either the whole report or none of it. The lock is
re-entrant, so code inside Update may call these members.
The recipe controller, the production preset, the statistics, the
notifications, the PackML controller and a continuous scale's totalizers
synchronise on locks of their own. They are thread-safe as well, but Update
does not keep their calls out. The Update delegate runs under the scale's
lock and holds up every method call on the scale meanwhile, so keep it short
and synchronous: no I/O, no waiting and no calls into other locks.
- Operator commands (
SetZero,SetReferencePieceWeight,InboundWeighing, …) pass theCommandInterceptor, like the methods. - Equipment reports (
PublishLoad,PublishHopper,SetActivity,CompleteCalibration,PublishFeeder, …) take the lock but skip the interceptor: the equipment reports what happened, it does not ask. - Node writes need no node-manager lock. A
NodeStateguards its own attributes, and the scale's lock is what keeps related values consistent.
Weighing arithmetic (§9.3):
- Gross and net. Gross is the load minus the zero offset
SetZeroestablished. Net is gross minus the tare. - Weighing range. The range is the first one whose upper limit is not exceeded.
- Rounding. Values round to the range's
d, or toewhen the scale is legal for trade, and snap to that interval's decimal places. The high-resolution value keeps full precision. - Overload is gross above the largest capacity.
- Underload is gross below zero by more than the zero band. A scale resting slightly below zero needs a zero, not an underload alarm.
- Unstable weights.
SetZero,SetTareandRegisterWeightrefuse an unstable weight.SetZeroalso refuses a weight outside the zero band. - Registration refuses an overload or underload.
ScaleNotifications
RaiseEvent(id, message, severity, auxParameters)raises aScaleEventTypewith the Annex CNotificationIdand its category.RaiseVendorEventdoes the same for vendor ids, which must be above 5000.SetAlarm(id, active, message)drives oneScaleAlarmTypecondition per notification id. The condition is created on first use and carries aHasConditionreference from its source.- The runtime raises some notifications by itself:
OVERLOAD_FAULTandUNDERLOAD_FAULTalarms follow the weight.- A failed zero or tare raises
ZERO_SETTING_FAULTorTARE_SETTING_FAULT.
ScaleProductionPreset
The methods and the local API share the same rules.
- Products.
AddProduct,RemoveAsync,Select,DeselectandSwitch.Switchfails withBadInvalidStatewhen more than one product is in processing (§7.7.8). - Locking. On a preset built
WithLocking, a client changes a product only while holding its DI lock. Otherwise the change fails withBadUserAccessDenied. - Catchweigher zones. Zones come from
AddZone. - Vehicle information.
VehicleInformationProvideranswersGetVehicleInformationfrom the application's vehicle database.
ScaleStatistics
Record,RecordAcceptedandRecordRejected(weight, reason)maintain counts, sum, min, max, mean and standard deviation, andLastItem.Resetclears them.- On a checkweigher, each
CheckweigherRejectReasoncounts into its ownPackagesRejectedBy…counter.
Type controllers
| Controller | Behaviour |
|---|---|
ContinuousController |
PublishFlow, Totalize, ResetTotalizer (method and local) |
LossInWeightController |
DischargeStart/Stop, RefillStart/Stop reflected in Discharging/Refilling; PublishHopper |
PieceCountingController |
SetReferencePieceWeight, SetNumberOfReferencePieces, StartReference (method and local; the local SetReferencePieceWeight(weight, unit) takes the real weight, the method a UInt32); derives CurrentPieceCount from the net weight; the product's target counts |
LaboratoryController |
Draft shields, leveling, calibration and ionisator with their *Running flags. CompleteImmediately or CompleteLeveling / CompleteCalibration end the running operations |
HopperController |
PublishLevels for the limit switches and levels |
AutomaticFillingController |
EvaluateFilling sets Deviation and ToleranceState against the product target |
VehicleController |
InboundWeighing, OutboundWeighing, OnePassWeighing (method and local); computes DeltaWeight per Table 123 |
RecipeController |
Recipe management and processing, see below |
Recipes
A recipe is a directed acyclic graph of elements linked by
NextRecipeElement. The graph can fork and join (Annex B). The start
element's predecessor is the recipe node itself.
Building and running recipes.
AddRecipeandAddRecipeElementbuild the graph. An element can only be linked from existing elements, so the graph cannot become cyclic.StartRecipe,StopRecipe(pause),ContinueRecipe,SkipCurrentRecipeElementandAbortRecipewalk the graph.- The application executes the elements:
- It listens to
ElementStarted. - It reports each finished element with
CompleteElement.
- It listens to
- An element whose predecessors are all complete starts next.
- When the graph finishes,
RecipeCompletedis raised. The report goes to the product'sReportand, if present, itsReportFile. ElementStartedandRecipeCompletedare raised after the controller's lock is released, and for a method call after the call's processing. A handler may therefore call back into the controller, for example complete an element that takes no time straight from itsElementStartedhandler.
Recipe files. WithRecipeFiles() gives every recipe a RecipeFile. The
file format is vendor-specific; the specification does not define one.
- An upload is handed to
RecipeController.RecipeFileHandler, which parses the file and returns aServiceResult. A bad result rejects the upload. ReadRecipeFilereturns the last accepted content.- Once a recipe came from a file,
AddRecipeElementandRemoveRecipeElementon it returnBadInvalidState. This keeps the file and the address space from diverging (§7.31).
PackML
PackMLStateMachineController from Opc.Ua.PackML drives the three nested
machines: base (Aborted … Cleared), machine (Stopped … Running) and execute
(Idle … Complete).
- Commands. Methods on an inactive sub-machine return
BadStateNotActive. A command that is not valid in the current state returnsBadNotExecutable. - Events. Every change raises the standard transition event, as §5.3 requires.
- Acting states (Starting, Stopping, …) complete automatically unless
AutoCompleteActingStatesis false. The equipment then callsCompleteActingState(). - Guards.
CommandGuardcan veto a command.
Client reference
Creating the client
session.Scales(telemetry) or ScalesClientFactory. AddScalesClient()
registers the factory with a hosted client. IsSupported tells whether the
server publishes the Scales namespace.
Discovery
| Method | Returns |
|---|---|
DiscoverScalesAsync |
Every top-level scale and system from DeviceSet and Machines, de-duplicated |
EnumerateScalesAsync, EnumerateScalesUnderAsync(root) |
The same as a stream, or below any node |
EnumerateSystemScalesAsync(system) |
The scales of a scale system |
GetKindAsync(typeDefinition) |
The ScaleKind of a type, vendor subtypes included |
Each ScaleEntry carries the NodeId, names, type definition, Kind and
IsScaleSystem.
Weighing
ReadCurrentWeightAsync, ReadRegisteredWeightAsync,
ReadIdentificationAsync, ReadWeighingRangesAsync,
ReadAllowedEngineeringUnitsAsync, SetZeroAsync, SetTareAsync,
ClearTareAsync, SetPresetTareAsync, RegisterWeightAsync (returns the
registered weight) and ObserveWeightAsync.
A streamed reading carries gross, net and tare only. Read the units and
properties once with ReadCurrentWeightAsync.
ReadCurrentWeightAsync throws Bad_NotFound for a node without
CurrentWeight, such as a scale system or a module.
TryReadCurrentWeightAsync returns null instead, the contract
ReadRegisteredWeightAsync has for a node without RegisteredWeight. A
published but never-written value comes back as NaN gross, net and tare.
Products, PackML, type methods and recipes
| Area | Methods |
|---|---|
| Products | ReadProductsAsync, ReadCurrentProductsAsync, AddProductAsync, RemoveProductAsync, SelectProductAsync, DeselectProductAsync, SwitchProductAsync |
| PackML | ReadPackMLStateAsync (the innermost active state number), ExecutePackMLCommandAsync(owner, PackMLCommand) |
| Type methods | SetReferencePieceWeightAsync, SetNumberOfReferencePiecesAsync, StartReferenceAsync, SetDraftShieldsAsync, WeighVehicleAsync, SetFeederSpeedAsync, and InvokeAsync(target, method) for any method without arguments |
| Recipes | AddRecipeAsync, AddRecipeElementAsync, ProcessRecipeAsync(scale, method, recipe) |
Notifications and snapshots
- Notifications.
ObserveNotificationsAsync(source)streams the scale events and alarms of a scale, module or system asScaleNotificationInfo.CreateNotificationFilter()andDecode(fields)expose the same filter for your own subscriptions.DefinedIdmaps the numeric id back toScaleNotificationId.
- Snapshots.
ReadSnapshotAsync(entry)reads a whole scale in one pass: identification, both weights, ranges, units, products and PackML state.
Contracts reference
| Contract | Holds |
|---|---|
ScaleIdentification |
Manufacturer, SerialNumber, ProductInstanceUri and the optional Machinery identification fields |
WeighingRangeDefinition(Low, High, d, e) |
One weighing range; Validate() checks the limits and intervals |
ScaleReading |
Gross, net, tare, tare mode, overload, underload, stability, zero, range, weight id, unit, timestamp, status |
ScaleEntry, ScaleProductInfo, ScaleNotificationInfo, ScaleSnapshot |
The client's discovery, product, notification and snapshot results |
ScaleKind, ScaleNotificationId, ScaleNotificationCategory |
The kinds and the Annex C tables |
Conformance matrix
Status key: ✅ implemented and tested over a real session · 🧪 implemented, tested in-process only · 🔧 implemented, not covered by a test · ❌ not shipped.
OPC 40200 defines server facets only; there are no client facets or client
conformance units (§12). The server advertises the facets and CUs of what
it actually built through ServerProfiles and ConformanceUnits
(ServerAdvertisesTheFacetsOfWhatItBuiltAsync). A facet is advertised only
with the facets it includes: the Machinery Machine Identification facet
and its units come with Base Scale, and the kind facets without a controller
are left out (ScaleFacetsBringTheMachineIdentificationTheyIncludeAsync,
ScaleFacetsAreNotClaimedOutsideTheMachinesFolderAsync,
KindFacetsWithoutAControllerAreNotClaimedAsync).
Facets
| Facet (§12.2.2) | Status | Tests |
|---|---|---|
| Base Scale | ✅ | EveryKindIsPublishedWithItsMandatoryMembersAsync; ClientReadsIdentificationRangesAndUnitsAsync |
| Scale System | ✅ | ScaleSystemHostsScalesAndResetsGlobalStatisticsAsync; ClientDiscoversEveryScaleOnceWithItsKindAsync |
| Feeder Module | ✅ | FeederAndPrinterModulesAsync; ClientCallsTheTypeSpecificMethodsAsync |
| Printer Module | 🧪 | FeederAndPrinterModulesAsync |
| Minimal Production Preset | 🧪 | StaticPresetWithoutProductsIsRejectedAsync; ProductionPresetSelectsDeselectsAndSwitchesProductsAsync |
| Full Production Preset | ✅ | ProductionPresetAddsAndRemovesProductsAsync; LockedProductsRejectOtherClientsAsync; ClientManagesAndSelectsProductsAsync |
| International System of Units | 🧪 | PresetTareChecksAndConvertsTheUnitAsync; SiMassUnitsAreRecognised |
| Simple, Laboratory, Hopper, Weighing Bridge | ✅ / 🧪 | LaboratoryShieldsLevelingCalibrationAndIonisatorAsync; HopperPublishesLevelsAsync; WeighingModulesAreScalesOfTheirOwnAsync; E2E for Simple, Laboratory and Hopper |
| AutomaticFillingScale | 🧪 | AutomaticFillingEvaluatesAgainstTheTargetAsync |
| Catchweigher, AutomaticWeightPriceLabeler | 🧪 | CatchweigherProductZonesCanBeAddedAndRemovedAsync (not advertised, see limitations) |
| Checkweigher | ✅ | CheckweigherStatisticsCountAcceptedAndRejectedAsync; ClientSubscribesToANestedScaleThroughTheNotifierHierarchyAsync |
| Continuous Scale, LossInWeight Scale | ✅ | ContinuousScaleTotalizesAndResetsAsync; LossInWeightDischargesAndRefillsAsync; ClientStreamsWeightsEventsAndAlarmsAsync |
| PieceCountingScale | ✅ | PieceCountingReferencesAndCountsAsync; PieceCountingProductTargetsCanBeSetAsync; ClientCallsTheTypeSpecificMethodsAsync |
| RecipeScale | ✅ | RecipesAreManagedAndProcessedAlongTheirGraphAsync; RecipeProcessingGuardsAsync; RecipeFilesAreUploadedParsedAndReportedAsync; ClientBuildsAndProcessesARecipeAsync |
| Totalizing Hopper Scale | 🧪 | EveryKindIsPublishedWithItsMandatoryMembersAsync (structure only and not advertised, see limitations) |
| Vehicle Scale | ✅ | VehicleWeighingComputesTheDeltaAsync; ClientCallsTheTypeSpecificMethodsAsync |
Cross-cutting conformance units and behaviour
| Area | Status | Tests |
|---|---|---|
| Scales ScaleDeviceType: zero, tare, preset tare, register | ✅ | ZeroTareAndRegisterMethodsDriveTheWeightAsync; ClientZeroesTaresAndRegistersWeightsAsync |
WeightItemType semantics: rounding, ranges, overload, underload, zero band |
🧪 | WeightIsRoundedToTheIntervalOfItsRange; LegalForTradeRoundsToTheVerificationInterval; OverloadAndUnderloadFollowCapacityAndZeroBand; PublishedLoadReachesEveryWeightPropertyAsync |
| Scales DataChange | ✅ | ClientStreamsWeightsEventsAndAlarmsAsync |
ScaleEventType / ScaleAlarmType with Annex C ids, vendor ids > 5000 |
✅ | EventsAndVendorEventsCanBeRaisedAsync; FailedZeroOrTareRaisesAFaultAndOverloadAnAlarmAsync; ClientStreamsWeightsEventsAndAlarmsAsync |
Notifier hierarchy with HasNotifier, nested sources subscribable |
✅ | ClientSubscribesToANestedScaleThroughTheNotifierHierarchyAsync |
| PackML state information with transition events | ✅ | PackMLMethodsWalkTheStateMachineAsync; PackMLActingStatesCanBeCompletedByTheEquipmentAsync; ClientDrivesThePackMLStateMachineAsync; advertised as PackML State Information: PackMLStateInformationIsClaimedWithAPackMLStateMachineAsync |
| Machinery Machine Identification, Find Machines | ✅ | IdentificationWithoutMandatoryFieldsIsRejectedAsync; ClientDiscoversEveryScaleOnceWithItsKindAsync |
| Machinery building blocks, MachineryItemState, OperationMode | 🧪 | StateSettersPublishTheMachineryStatesAndProcessStateAsync |
| RecipeManagment, DynamicRecipeManagement, FileRecipeManagement | ✅ | as RecipeScale above |
Method veto through CommandInterceptor |
🧪 | CommandInterceptorCanVetoACommandAsync; PieceCountingLocalCallsMatchTheMethodsAsync |
| Runtime members thread-safe against method calls | 🧪 | EquipmentReportsAreAtomicAgainstTheScaleLockAsync; CalibrationReportsAreAtomicAgainstTheScaleLockAsync |
| Local operator calls for piece counting and vehicle weighing | 🧪 | PieceCountingLocalCallsMatchTheMethodsAsync; VehicleLocalCallsMatchTheMethodsAsync |
| Recipe events raised outside the lock | 🧪 | HandlersMayCompleteElementsSynchronouslyAsync |
| Rejecting a second DI owner | 🧪 | ServerHostingRefusesASecondDiOwner |
Client hosting AddScalesClient |
🧪 | ClientHostingRegistersTheFactory |
Samples
| Sample | Shows |
|---|---|
| ScalesServer | A packing line (scale system with a checkweigher, infeed feeder and labeler) plus a laboratory balance, a counting scale, a weighbridge and a recipe batching scale, all with a simulation that rejects packages and runs the labeler out of labels |
| ScalesClient | Discovery, snapshots, tare/register/switch product on the nested checkweigher, and streaming of its weights, events and alarms |
See the samples README for how to run them.
Testing
dotnet test tests/Opc.Ua.Scales.Tests
| Fixture | Covers |
|---|---|
ScalesUnitTests |
The weighing engine, units and the model tables, with no server |
ScalesNodeManagerTests |
Every kind, the methods, PackML, presets, zones, facets, in-process |
ScaleTypeControllerTests |
The type controllers, modules, recipes, recipe files and systems |
ScalesClientServerE2eTests |
A hosted server through AddScales / ConfigureScales, driven by ScalesClient over a real session |
The end-to-end fixture is the one that matters for anything a client sees. Two defects this work found passed every in-process assertion:
- Alarm conditions without an event type never reached a subscribed client.
- A nested scale was not a notifier, so a subscription on it failed.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
CS0104: 'BrowseNames' is ambiguous |
Opc.Ua and Opc.Ua.Scales both define BrowseNames |
Qualify as Opc.Ua.BrowseNames or alias it |
ArgumentException from CreateScaleAsync |
WithIdentification or WithWeighingRange missing, or an identification without Manufacturer / SerialNumber / ProductInstanceUri |
Add them; both are mandatory |
InvalidOperationException: DI address space already owned |
AddScales combined with AddOpcUaDi or another DI owner |
Use only AddScales; it loads DI |
SetZero returns BadOutOfRange |
The gross weight is outside the zero band | Unload the scale, or widen ZeroSettingRange |
SetZero/SetTare/RegisterWeight return BadInvalidState |
The weight is unstable, overloaded or underloaded | Publish a stable load inside the range |
SetPresetTare returns BadInvalidArgument |
The unit is not in AllowedEngineeringUnits (or not SI with RequireSiUnits) |
Pass an allowed unit |
A product change returns BadUserAccessDenied |
The preset is built WithLocking and the client holds no lock |
Call the product's Lock/InitLock first |
SwitchProduct returns BadInvalidState |
More than one product is in processing | Deselect down to one product |
PackML method returns BadNotExecutable / BadStateNotActive |
Command not valid in the current state / sub-machine not active | Read ReadPackMLStateAsync first |
Weights arrive as ExtensionObject |
Client not created through session.Scales(...) |
Use the extension |
| A streamed reading has no unit | Streams carry the value only | Read the unit once with ReadCurrentWeightAsync |
Known limitations
- Totalizing hopper. Published with its full structure, but no
controller drives
TipCounter. The application sets it throughWith<TotalizingHopperScaleState>or the product node. The Totalizing Hopper Scale facet and its CU are therefore not advertised; the scale still counts towards Base Scale. - Catchweigher and price labeler. These have no dedicated controller.
- Zones and
LastItemstatistics are implemented. - The application writes item prices and measured dimensions itself.
- Their kind facets and CUs are therefore not advertised; the scales still count towards Base Scale.
- Zones and
- Recipe elements. Elements are executed by the application through
ElementStarted/CompleteElement.ConditionSleepthresholds are not evaluated by the server. - Recipe file format. It is vendor-specific. Parsing is up to
RecipeFileHandler. - Machinery building block. The Machinery
Notificationsbuilding block is not used. Events are raised on the scale nodes directly. - Method status codes. OPC 40200 defines no method-specific status codes. The runtime uses the generic Part 4 codes listed in Troubleshooting.
Model provenance
- Scales NodeSet.
src/Opc.Ua.Scales/Model/Opc.Ua.Scales.NodeSet2.xmlis the unmodified OPC Foundation publication fromUA-Nodeset/latest/Scales:- model version 2.00, published 2025-03-01;
- requiring UA 1.05.03, DI 1.04.0, IA 1.01.2, Machinery 1.03.0 and PackML 1.01.
- PackML NodeSet.
src/Opc.Ua.PackML/Model/Opc.Ua.PackML.NodeSet2.xmlis version 1.01 fromUA-Nodeset/latest/PackML. - Identifier tables. The sibling
*.NodeIds.csvidentifier tables pin the symbol-to-NodeId mapping. The generator fails the build (MODELGEN025) when they disagree. RequiredModelversions are a lower bound. The repository's newer DI, IA and Machinery models satisfy them.- Generator fix. PackML exposed a source-generator bug:
- It declares seven method
InputArgumentsahead of their methods. - The generator derived symbolic ids in the same pass that normalised a
missing
ParentNodeId, so those children were named from the bare parent name. NodeSetToModelDesignnow normalises every parent first.- No existing generated symbol changed.
- It declares seven method
Where the specification and the NodeSet disagree
The implementation follows the NodeSet wherever the text and the NodeSet differ:
- CU spellings. The CU is spelled Scales RecipeManagment in Table 162
and on
RecipeManagementType, but RecipeManagement elsewhere. The server advertises the Table 162 spelling. - Facet URIs. Some facet URIs contain spaces (
…/Scales_LossInWeight Scale) or lack/Server/.ScalesProfilesreproduces them verbatim. - State machine browse names. §7.4.3 calls the state machine
"ScaleStateMachine", but its browse name is
State, andSystemStateon a scale system. - Method argument types.
SetReferencePieceWeighttakes aUInt32weight, andSetFeederSpeedtakes aFloat. - Browse names that differ from the text.
- The totalizing hopper target is
VolumeTargetValue. - The checkweigher percentage is
PercentageLowerToleranceLimit.
- The totalizing hopper target is
- Abstract identification type. Feeder and printer
Identificationare declared with the abstractMachineryItemIdentificationType. Instances useMachineryComponentIdentificationType. - No
GeneratesEventorHasNotifier. The NodeSet has neither, so the notifier wiring described in Where a scale lives is the server's own.
See also
- Machinery developer guide: the
Machinesfolder and the OPC 40001-1 building blocks - State machines: the finite-state-machine support PackML builds on
- OPC 40200 on the OPC Foundation reference site