Job models and state machine
MicroDCS.JobControl holds the ISA-95 Job Control models. Application logic works on version-neutral records; the generated OPC UA types of V1 and V2 stay behind the conversions.
| Part | Type | Use |
|---|---|---|
| Records | JobOrder, JobResponse, JobWorkMaster, JobParameter, JobProperty, JobMaterial, JobResource |
The neutral model |
| Conversions | JobControlConversions, JobStateArray |
To and from the generated V1 and V2 types. Same-version round trips are lossless |
| Codec | OpcUaJsonCodec, OpcUaJsonPayloadDescriptor<T> |
OPC UA JSON, verbose |
| Validation | JobControlValidation |
Required ids; the decoder accepts missing fields |
| Schema | JobControlSchemas.GenerateJson<T>() |
JSON Schema for dataschema, generated at runtime |
| State machine | JobStateMachine.Apply(version, state, operation) |
Pure function, no I/O |
The codec sets no size limits of its own; the transport bounds a message. It keeps the OPC UA nesting limit, and an over-deep payload is a PayloadException.
State machine
Apply returns a JobTransition: the ReturnStatus bitmap, the new state and the effect. A rejected operation is a result, not an exception, and leaves the state unchanged.
| Status bit | Meaning |
|---|---|
0 Success |
Accepted |
1 UnknownJobOrderId |
The order does not exist |
2 InvalidCommand |
V1 only: the command does not apply |
3 InvalidStatus |
V2: the state does not permit the operation |
4 UnableToAccept |
Store of an order that exists |
32 InvalidRequest |
A required field is missing |
The V2 rules come from a table generated from the vendored NodeSet2, with a test that regenerates it. Store, StoreAndStart, Cancel and Clear are a hand-written supplement. Update keeps the sub-state; any other entry into a state takes its initial sub-state.
Effects: a transition that the NodeSet marks with an effect emits a status event. Cancel, Clear and V1 Stop remove the order and emit a catalog change. Sub-state transitions, including Close, emit nothing.
V1 and V2
| V1 state | Neutral state | V2 view |
|---|---|---|
| Waiting | NotAllowedToStartWaiting |
NotAllowedToStart |
| Ready | AllowedToStartReady |
AllowedToStart |
| Loaded | V1Loaded |
Running |
| Running, Held, Suspended, Completed, Closed, Aborted | same | same |
| Error | V1Error |
Aborted |
The Prepare sub-states exist in V2 only. A V1 view shows all three as Waiting or Ready. V1 has no Pause, Resume, Abort or RevokeStart. V1 Stop removes the order.
Work Master extension
ISA95WorkMasterExtDataType is a subtype of the V2 Work Master with DataSchema and Data. Its model is NodeSets/MicroDcs.JobControl.NodeSet2.xml; the OPC UA source generator creates the type at build time. It has its own CloudEvent type (JobControlTypes.WorkMasterExt), because a subtype inside a job order decodes as the base type. Job orders reference Work Masters by id.
Machinery Jobs processor
MicroDCS.MachineryJobs serves Job Control V2 on the binding machinery_jobs. It needs Valkey. microdcs serve runs it when that binding is configured.
| CloudEvent type | Handling |
|---|---|
org.opcfoundation.ISA95-JOBCONTROL_V2.{Store, StoreAndStart, Update}.v2 |
Carry JobOrder and Comment; acceptance check, then the state machine |
org.opcfoundation.ISA95-JOBCONTROL_V2.{Start, Stop, Abort, Pause, Resume, RevokeStart, Cancel, Clear}.v2 |
Carry JobOrderID and Comment |
org.opcfoundation.Machinery.Jobs.RequestJobResponseByJobOrder{ID, State}.v1 |
Queries; a state query with only a top-level state matches its sub-states |
com.github.aschamberger.ISA95-JOBCONTROL_V2.config.{equipment, materialclass, materialdefinition, personnel, physicalasset, workmaster, workmaster.ext, jobacceptance}.v1 |
Station configuration. CloudEvent method DELETE removes, anything else stores. No reply |
A response uses the call's type id with .response before the version. ReturnStatus is a JSON string, as OPC UA JSON writes every UInt64.
A status event (ISA95JobOrderStatusEventType.v2, intent Event, subject is the scope) follows each transition the NodeSet marks with an effect. Every entry into AllowedToStart also queues the job's recipe, atomically with the order. Cancel and Clear delete the order and leave a change entry; the job response stays. An order id belongs to one scope: a command from another scope gets UnknownJobOrderId.
Once at start one replica publishes a retained metadata event (org.opcfoundation.Machinery.Jobs.Meta.v1, intent Meta) with the supported commands.
| Configuration | Type | Default | Description |
|---|---|---|---|
MicroDcs:JobAcceptance:CheckEquipmentId |
bool | true |
Equipment requirements must be in the scope's list. |
MicroDcs:JobAcceptance:CheckMaterialClassId |
bool | true |
Material classes must be in the scope's list. |
MicroDcs:JobAcceptance:CheckMaterialDefinitionId |
bool | true |
Material definitions must be in the scope's list. |
MicroDcs:JobAcceptance:CheckPersonnelId |
bool | true |
Personnel requirements must be in the scope's list. |
MicroDcs:JobAcceptance:CheckPhysicalAssetId |
bool | true |
Physical asset requirements must be in the scope's list. |
MicroDcs:JobAcceptance:CheckWorkMaster |
bool | true |
Work Masters must be known in the scope. |
MicroDcs:JobAcceptance:CheckMaxDownloadableJobOrders |
bool | true |
Limit the number of job orders of a scope. |
MicroDcs:JobAcceptance:MaxDownloadableJobOrders |
int | 0 |
Limit for a scope without its own; 0 means no limit. A limit set through config.jobacceptance always applies, also 0, which accepts nothing. |
The inspector jobs backs the MCP tools job_orders and job_order. The maintenance tool maint_job_delete_plan removes one order; it bypasses the state machine.
Job order publisher
The publisher keeps last-value state topics for the MES, on the transport the binding machinery_jobs names. It reads the change stream of each scope, so a command never waits for it. MQTT uses retained messages, NATS a key-value bucket. A transport without state topics ends the start.
Topic below {prefix}/{scope levels}/{discriminator}/data/ |
Payload | Written |
|---|---|---|
state-index |
JobOrderStateIndexDataType |
After every batch of changes |
order/{id} |
ISA95JobOrderDataType, no state |
On Store, StoreAndStart and Update; removed on Cancel and Clear |
result/{id} |
ISA95JobResponseDataType |
When a job in Completed, Closed or Aborted has a response; removed on Cancel and Clear |
The state index holds Sequence, Scope, PublishedAt and per job JobOrderID, State and HasResult, which is true when the result/{id} topic exists. Sequence grows with every index of the scope and survives restarts. An MES that sees the value it last read has missed nothing. A higher value means it should read the index again. A rebuild raises it too. All payloads are OPC UA JSON, and Sequence is a JSON string as every Int64 is.
A job order id becomes a topic level, so it may only use letters, digits, _ and -. A Store, StoreAndStart or Update with any other id gets InvalidRequest.
Delivery is at least once. A scope is rebuilt from the stored documents on its first visit under a lease, when its cursor is missing, when the change stream was trimmed past the cursor, and at half the time to live so that idle orders keep their topics.
Deployment
MicroDcs:Roles decides what an instance runs. In production run the processors with Processor and one publisher with Publisher: a stalled MES broker or a costly index then stays out of the control path, and the publisher can use another broker through its own environment. All, the default, runs both in one process for development and small sites. A publisher-only instance connects but does not subscribe. One lease in Valkey lets only one instance publish, also with several replicas and during a rolling update.
| Configuration | Type | Default | Description |
|---|---|---|---|
MicroDcs:JobPublisher:RetainedTtl |
TimeSpan | 48:00:00 |
How long a topic stays when nothing refreshes it. Topics are refreshed at half of this. At least 2 s. |
MicroDcs:JobPublisher:ReadCount |
int | 50 |
Most change entries per scope in one round. |
MicroDcs:JobPublisher:LeaseTtl |
TimeSpan | 00:00:15 |
Lifetime of the publisher lease, renewed at a third of this. At least 3 s. |
The inspector publisher backs the MCP tool publisher_status. maint_publisher_reset_plan makes the publisher write a scope again, and maint_retained_clear_plan removes state topics from the broker.