Skip to main content
Version: 1.0.8

User-Defined Types (UDTs)

A UDT is a template for a piece of equipment. Define a motor once — its members, data types, units, scaling, alarms and address patterns — then stamp out an instance per physical motor with a parameter or two.

Change the definition later and QUBIQ reconciles every instance.

Why​

Twenty hand-built motor folders is twenty places to make a mistake and twenty places to fix one. The 21st motor is another hour of work. With a UDT it is thirty seconds, and a definition change propagates everywhere at once.

Definitions​

A definition declares parameters and members.

UDT: Motor
parameters:
Device (string) e.g. "M101"
MaxSpeed (number) e.g. 3000

members:
Speed float RPM address: ns=2;s={Device}.ActSpeed eng 0–{MaxSpeed}
Current float A address: ns=2;s={Device}.Current
Running bool address: ns=2;s={Device}.Run
Fault bool address: ns=2;s={Device}.Fault
alarm: on_boolean = true, priority high, "Motor {Device} faulted"
Diagnostics (folder)
RunHours float h address: ns=2;s={Device}.RunHours
ElementNotes
ParametersNamed placeholders written {Param}. Substituted into member names, addresses, descriptions, messages and ranges. Two are predefined: {InstanceName} and {InstancePath}.
MembersTags with their full property set, and folders for structure. A tag member is a device, internal or computed tag.
Nested UDTsA member may itself be a UDT instance, so a Line can contain three Motors.
Alarm definitionsCarried on members, so every instance is alarmed identically without extra work. See alarms in a type.
Binding templatesThe address pattern; the connection is chosen per instance.

A member's source Changed in 1.0.7​

A member takes the same Tag Source as any tag — device, internal or expression — and each has one rule the type editor enforces when you save:

SourceRule
DeviceMay be saved without a binding while you are still building the type. An instance of it gets an internal tag until the binding is filled in; the next sync after that turns it into a device tag.
InternalCarries no binding. A value QUBIQ owns cannot also come from a device.
ExpressionNeeds an expression and carries no binding. See computed members.

The source is kept in step on every sync. Before 1.0.7 it was set once, at creation: a member that gained its binding later kept writing to QUBIQ's own store and never reached the device, which is a write that "works" everywhere except the plant.

A member also carries its own publish mode for MQTT egress — follow the broker, only when it changes or every reading — set on the member's Publishing tab. 1.0.8 It belongs on the type because it is a property of the signal: a counter is a counter in every instance. One instance can still override it. Before 1.0.8 the member editor had no control for it, and a choice made on an instance's tag was undone by the next sync. → Broker publishing

An internal member's Initial Value is checked against its data type as you type, exactly as on a plain tag, and may be a {Param} resolved per instance — a parameter the type does not declare is reported as you type it, not on save. Unlike a plain tag, the box is not filled in for you: here it is part of the type definition, and an automatic 0 would record a value you never set in every internal member of every type.

What a member carries to its instances Changed in 1.0.8​

A member reuses the tag's own tabs, and in 1.0.8 everything those tabs show reaches the instances: it is written onto every instance tag, kept in step on every sync, and overridable per instance. Until 1.0.8 several settings were shown on the member and silently dropped when it was saved.

TabWhat now reaches the instances
ScalingThe engineering range, whether or not the member is scaled. It used to be kept only with scaling on, and the form filled in an invented 0 – 100.
HistoryEvery storage rule: deadband and its unit (units or % of range), minimum time between samples, store interval, interpolation. The type is checked as a tag is: a percent deadband needs the engineering range.
BindingStale after; for MQTT, Retain and the device timestamp, quality and status-topic settings. A sync that changes a member's QoS or JSON path keeps the rest of the binding's settings — it used to rebuild the binding from those two and erase the others, retain included.
AlarmsEvery alarm setting — see alarms in a type.
PublishingThe publish mode.

A member with history gets the system historian. 1.0.8 The History tab shows the system historian read-only and so saved no storage connection, and an instance of any member with history on was refused — historian connection is required when history is enabled. The designated historian is now filled in when an instance is created or synced. A member that names a connection of its own, or a {Param}, keeps it.

A bulk edit from the tree is an override. 1.0.8 Enable History…, Enable Write… and the other bulk edits on the tree's context menu — including on a folder, reaching an instance's tags below it — are now recorded as overrides. The next sync of the type used to put the type's values back.

Computed members New in 1.0.7​

A member can be an expression tag, computed from other tags. Pick Expression as its source and write the formula in the member's Expression tab. Before 1.0.7 the choice existed but made a tag with no expression, which computed nothing, and the tab the form told you to use was not there.

An expression names its inputs by absolute path, but a member is stamped out once per instance. A path typed literally would make every instance read the same tag — computing perfectly and wrong everywhere. {InstancePath} is the instance's own location, filled in per instance:

Member SpeedKnots expression tag["{InstancePath}/Sensors/Speed"] * 1.94384

Instance Ship1 → tag["Fleet/Ship1/Sensors/Speed"] * 1.94384
Instance Ship2 → tag["Fleet/Ship2/Sensors/Speed"] * 1.94384

You do not have to spell those paths. The Expression tab has a This instance picker beside Tag and Function: it lists every tag one instance will hold, nested types expanded under their folder, each with its data type, and picking one writes the {InstancePath} reference at the cursor. Some entries are listed but greyed, with the reason, so a missing entry never looks like a bug:

  • another computed member of the same type — a computed member cannot read one (below);
  • a tag inside a nested type whose name comes from that type's own parameters — its final name is not known until an instance exists.

The rules:

  • Declaration order does not matter. A computed member can read a member declared below it.
  • A computed member cannot read another computed member of the same type. It is refused by name, not reported as an unknown tag.
  • The data type is derived from what the expression returns, and the tag is read-only — the member's Data Type and Writable settings are ignored.
  • The expression is checked when an instance is created, not while you edit the type, because the paths it reads only exist then. A mistake is reported naming the member.
  • In a nested type, {InstancePath} means the nested folder, so inside Rack1/ChA it is …/Rack1/ChA and a nested type stays self-contained. To reach the outer instance, declare a parameter on the nested type and pass it down.
  • A plain path is allowed. tag["Plant/Ambient"], picked from Tag, means that one shared tag in every instance, which is sometimes the intent. The sync plan warns about it rather than refusing it.

An instance's computed tags start evaluating as soon as the instance is created or synced; editing the expression in the type and syncing recompiles it in every instance.

Alarms in a type Changed in 1.0.8​

Alarms defined on a member reach every instance as the type says:

  • Switched off stays off. A type alarm with Enabled or Requires acknowledgement off produced an instance alarm with it on, until a later sync corrected it. Both are kept on create, on sync and for a member that a sync adds.
  • Every setting syncs. A sync compared and wrote only some of an alarm's settings: a change to the alarm's deadband, deadband mode, span or Evaluate on bad quality never reached an existing instance. One list of settings now drives both the comparison and the write.
  • An instance alarm that is disabled exactly as its type says is no longer reported as an override.

A type saved before 1.0.8 may have lost an alarm's Requires acknowledgement off when it was stored. Such an alarm now reaches its instances with acknowledgement required, which is what its editor already showed. Check alarms you meant to need no acknowledgement.

→ Alarms in UDTs

Instances​

An instance is a folder in the namespace, materialised from a definition with concrete parameters.

Instance Line1/Filler/Motor1 Motor {Device: "M101", MaxSpeed: 3000} connection: PLC1
Instance Line1/Filler/Motor2 Motor {Device: "M102", MaxSpeed: 3000} connection: PLC1
Instance Line1/Capper/Motor1 Motor {Device: "M201", MaxSpeed: 1500} connection: PLC2

Materialising creates ordinary namespace nodes, tags and bindings. Everything downstream sees normal tags — screens, alarms, history and scripts need no UDT awareness.

The New instance wizard Changed in 1.0.7​

The wizard lays the type's parameters out as a table — Parameter (with its description), Type, Value — and the preview as another — Tag, Data type, Address. Each table scrolls inside itself with its header pinned, so a type with many parameters cannot push the dialog off the screen.

A parameter of type connection gets a connection picker, and it offers only connections that fit how the type uses that parameter: one substituted into a Modbus binding is offered Modbus connections, one used by a Sparkplug binding MQTT connections, one used as a history storage connection historians only. A parameter no member uses yet is offered every connection, grouped by protocol; when none of the right kind exists, the picker names the kind that is missing.

Before 1.0.7 this picker said No connections available whatever existed, so a connection could not be chosen from it at all.

Reconciling changes​

Add a member to the definition, fix a unit, adjust an alarm setpoint — then sync.

ActionScope
Preview the change for one instanceShows what would be added, changed or removed
Sync one instanceThat instance
Sync every instanceAll instances of the definition
Sync all 1.0.7Every out-of-date instance, of every type
List instances that have driftedEverything needing a sync

Always read the plan before syncing a definition with many instances. It is a dry run, and it is the difference between a confident change and a surprise.

Sync all New in 1.0.7​

An instance whose type has changed since it was last synced carries an out of date badge in Materialized instances. Sync all (N) above that list brings every one of them back in line — across types — in one action; N is always the number of badges. Before 1.0.7 that meant opening each instance and syncing it on its own, so a type edit that touched a dozen instances was a dozen dialogs.

The confirmation lists the out-of-date instances grouped by type. Once started, the sync runs in the background on the server: the dialog shows how far it has got and which instance it is on, Run in background closes it without stopping anything, and while it runs the button shows Syncing 12/40… — also to anyone who opens the UDT manager meanwhile — and reopens the progress on click. Only one Sync all runs at a time, and a single-instance sync never works on the same instance at the same moment.

The result names every instance that could not be synced and why. Those instances are left unchanged and stay flagged out of date, so Try again picks up exactly what is left: nothing is synced twice and nothing is half-done. Starting Sync all needs Manage Namespace. It is recorded in the audit journal twice — when it starts (who, and from where) and when it finishes, with how many instances were synced, how many failed and the names of the first twenty failures.

What makes an instance out of date Changed in 1.0.7​

A change to what a type produces — a member added, removed, renamed or edited, a binding template, an alarm — marks its instances, and the instances of every type nesting it, out of date. A change that produces nothing does not:

  • Renaming a type, or editing its description or intended protocol, changes no tag. Before 1.0.7 it flagged every instance anyway, for a sync that had nothing to do.
  • Saving a type without changing anything no longer marks anything.

The instance lists refresh themselves every 20 seconds while the UDT manager is open, and when you return to the window, so a type another engineer just edited shows its badges without a reload.

When a sync fails, the dialog now says why in words you can act on — the database was busy (try again), or a tag already exists at one of the instance's paths and is not part of it (rename or remove it) — and a problem in the type itself names the member. It used to say only Failed to sync instance.

Overrides​

Real plants have exceptions: one motor with a different high-speed setpoint, one instrument with a non-standard range.

An override pins a member's property on a single instance. Reconciliation respects it — the member is not reset to the definition's value — and the instance is reported as overridden rather than silently diverging.

Reading overrides needs View Namespace; setting or clearing them needs Manage Namespace.

Overrides are a signal, not a habit

One or two per definition is normal. Ten means the definition is wrong — add a parameter instead.

Member context​

For alarm tuning on a specific instance, QUBIQ records the member context: which definition member a node came from, and what the definition says versus what this instance has.

ActionWhat it does
Member contextShows which definition member a node came from
CaptureAdopts the instance's current state as its own
Revert alarmPuts an alarm back to the definition's values

This is what lets an engineer tune one noisy alarm in the field and later ask "what did the standard say?" — and put it back.

Moving types between systems New in 1.0.8​

A namespace export now carries the types its instances need. An instance is exported with its type by name, its parameters, its overrides and its source path, and the file's type bundle holds every type required, nested types first. Before 1.0.8 an instance was flattened into plain folders and tags, and imported elsewhere as loose tags with no type, no sync and no overrides.

On import, a type that is already there with the same content is reused — importing a second instance of a type never duplicates it. A type that differs follows the import's conflict policy, and Restore tags from file previews what will happen before anything is written. → Importing UDT instances

Designing a good UDT​

  1. Parameterise what varies, hard-code what does not. A device identifier and an engineering range vary. A unit does not.
  2. Include alarms in the definition. The main payoff is consistent alarming across identical equipment.
  3. Keep members shallow. One folder of structure is fine; four levels makes every path long.
  4. Name members for the quantity, not the instance — Speed, never Motor1Speed. The instance name is already in the path.
  5. Nest deliberately. A Line containing three Motors is good modelling. A five-level nest is hard to reconcile and hard to read.

Pairing UDTs with embedded views​

The visualization counterpart is an embedded view with parameters: author the motor faceplate once, pass the instance path, and reuse it for every motor.

UDT instance Line1/Filler/Motor1
Embedded view MotorFaceplate { tagPath: "Line1/Filler/Motor1" }

Twenty motors then cost one UDT and one view — not twenty folders and twenty screens.

→ Views and layout

Permissions​

Reading definitions and instances needs View Namespace. Creating, editing and deleting definitions — and syncing, overriding or reverting members — needs Manage Namespace. Creating an instance needs Create Namespace Node; deleting one needs Delete Namespace Node.

Next​

→ Bulk import