Object model
The object model is the single structured representation of the machine's state - temperatures, axis positions, the current job, network configuration, loaded plugins, and so on. Clients read it, subscribe to changes to it, and reference it inside expressions. It is defined once in DuetAPI and maintained at runtime by DCS.
- Definition:
src/DuetAPI/ObjectModel/ObjectModel.csand theObjectModel/subtree - DCS provider and locking:
src/DuetControlServer/Model/ObjectModel.cs,Model/LockWrapper.cs
Top-level keys
ObjectModel exposes these top-level keys, each a subtree of the model:
| Key | Contents |
|---|---|
Boards |
Connected mainboard and expansion boards |
Directories |
Virtual directory paths (see File management) |
Fans |
Fan configurations |
Global |
User-defined global variables (arbitrary JSON values) |
Heat |
Heaters, sensors, and temperature control |
Inputs |
The state of each code channel |
Job |
The active print job (file, progress, layers, timing) |
LedStrips |
Addressable LED strip configurations |
Limits |
Machine configuration limits |
Messages |
Generic message log (status, errors, M118 output) - SBC-maintained |
Move |
Axes, extruders, kinematics, compensation |
Network |
Interfaces and protocols - partly SBC-maintained |
Plugins |
Installed plugins - SBC-maintained |
Sensors |
Endstops, probes, and other sensors |
Spindles |
Spindle configurations |
State |
Machine status, message box, log level |
Tools |
Tool definitions |
Volumes |
Mass-storage volumes - SBC-maintained |
Base types
Every node in the model derives from one of a small set of base types in
src/DuetAPI/ObjectModel/Base/. They all raise change notifications so the model can be observed:
ModelObject- base class for a structured node. ImplementsINotifyPropertyChanging/INotifyPropertyChangedand updates properties throughSetPropertyValue. Two flavours exist: static model objects (properties are updated in place) and dynamic ones (a property may be replaced with a new instance).StaticModelCollection<T>- an observable, typed list for fixed-schema arrays (Tools,Boards,Fans, ...). Raises granular add/remove/replace notifications.StaticModelDictionary<T>- a typed key/value store (Plugins). Keys keep their original case (they are not camel-cased). Supports a "null removes the item" mode.JsonModelDictionary- an untyped key/value store of raw JSON values, used forGlobal.
Two attributes annotate properties and drive query/merge behaviour:
[SbcProperty]marks a property maintained by DSF rather than by the firmware. A constructor argument records whether it is also available in standalone mode. When firmware updates are merged these properties are skipped, and expressions that reference them are evaluated on the SBC instead of being forwarded to RRF.[Live]marks frequently-changing properties (temperatures, positions) that are only included when the live query flag is set.
JSON serialization
The model is serialized to JSON with a camelCase naming policy
(src/DuetAPI/ObjectModel/ObjectModelContext.cs): State.Status becomes state.status. Dictionary
keys (Plugins, Global) keep their original case. The companion source generator
(src/DuetAPI.SourceGenerators/) emits, for every model type, fast UpdateFromJson /
UpdateFromJsonReader and Assign methods used when merging firmware updates and cloning - this
avoids reflection on the hot path. The generated update code honours [SbcProperty] so a firmware
merge never clobbers SBC-owned state.
Maintaining the model in DCS
Locking
DCS holds one global ObjectModel instance, guarded by an async reader/writer lock
(Model/LockWrapper.cs, Model/ObjectModel.cs):
AccessReadOnly()/AccessReadOnlyAsync()- many concurrent readers.AccessReadWrite()/AccessReadWriteAsync()- one exclusive writer.
The lock wrapper is disposable; releasing a write lock signals observers that the model changed.
Condition variables let callers wait for the next update (WaitForUpdate) or for a full firmware
update. A watchdog (MaxMachineModelLockTime) logs and shuts the app down if a lock is held too long,
to surface deadlocks rather than hang silently.
Lock contention on this single model is a real failure mode: any long-running work must gather data outside the lock and apply it inside a short write-lock window. The periodic update service below is written this way.
Updates from the firmware
Model/UpdateService.cs requests object-model JSON from RRF over the Firmware link and
merges each section with the generated UpdateFromFirmwareJson methods, skipping [SbcProperty]
fields. Per-section sequence numbers (Seqs) tell DCS which sections actually changed since the last
poll, so it only re-requests what moved.
Model/PeriodicUpdateService.cs fills in the host-side facts the firmware cannot know - network
interfaces, storage volumes, SBC CPU/memory/distribution info. It gathers these asynchronously
outside the lock, then applies them under a brief write lock.
Observing changes and patches
flowchart LR
FW["Firmware update<br/>(SPI)"] --> MODEL
PERIODIC["PeriodicUpdateService"] --> MODEL
CODE["Code execution"] --> MODEL
MODEL["Global ObjectModel<br/>(reader/writer lock)"] -->|"property/collection<br/>change events"| OBS["Model/Observer"]
OBS -->|"(path, changeType, value)"| SUB["ModelSubscription processor"]
SUB -->|"JSON patch or full model"| CLIENTS["Subscribers:<br/>DWC, ModelObserver, ..."]
OBS --> TRIG["SbcTriggerService<br/>(M581.1)"]
Model/Observer/Observer.cs recursively subscribes to the change events of every node. When
something changes it raises OnPropertyPathChanged with the dotted path (e.g. ["state","status"]),
a PropertyChangeType (Property, Collection, or the special MessageCollection), and the new
value. The ModelSubscription IPC processor turns these into JSON patches
(or sends the whole model, depending on the subscriber's mode) and pushes them to clients - this is
what drives the live DWC interface through the DuetWebServer WebSocket.
Model/SbcTriggerService.cs is a second observer: it re-evaluates M581.1 external-trigger
expressions that reference SBC fields (which RRF cannot evaluate) whenever a relevant path changes,
and queues codes when a trigger fires.
Querying by path
Model/Filter.cs resolves dotted paths such as state.status or heat.heaters[0].current into a
node traversal, and parses the query flags used by M409 and the API: live-only, verbose, include
obsolete, include nulls, maximum depth, and an array start index for pagination. The same path
resolution backs SBC-side expression evaluation.
See also
- IPC - the
Subscribeconnection mode and the object-model commands - G-code flow - how expressions read the model
- Firmware link - where firmware updates come from