On this page · 8 sections

A project is one JSON document, schema: "smart-industrial-project/1". The designer writes it with Project › Export the project; the agent stores it as project.json next to a published app; the runtime renders it.

{
  "schema": "smart-industrial-project/1",
  "name": "Plant overview", "version": 1, "theme": "industrial-dark",
  "grid": { "columns": 12, "rowHeight": 40, "gap": 12 },
  "sources": [
    { "id": "sim", "type": "simulator", "interval": 500,
      "tags": { "PT-2101.PV": { "min": 5.2, "max": 6.8, "period": 30000, "unit": "bar" } } },
    { "id": "agent", "type": "agent", "url": "", "token": "", "tags": [] }
  ],
  "pages": [
    { "id": "overview", "name": "Overview", "components": [
      { "id": "pt2101", "tag": "smart-faceplate", "at": { "col": 1, "row": 6, "w": 3, "h": 7 },
        "props": { "tag": "PT-2101", "unit": "bar", "min": 0, "max": 10, "interactive": true },
        "bind": { "processValue": "sim.PT-2101.PV", "setpoint": "sim.PT-2101.SP" },
        "on": { "setpointChange": { "write": "setpoint" } } },
      { "id": "trend", "tag": "smart-trend", "at": { "col": 1, "row": 14, "w": 8, "h": 8 },
        "props": { "pens": [ { "field": "pressure", "label": "PT-2101", "unit": "bar", "min": 0, "max": 10 } ] },
        "feed": { "kind": "trend", "interval": 1000, "fields": { "pressure": "sim.PT-2101.PV" } } },
      { "id": "title", "tag": "studio-heading", "at": { "col": 1, "row": 1, "w": 8, "h": 1 }, "props": { "text": "NORTHFIELD PLANT" } }
    ] }
  ],
  "flows": [
    { "id": "flow1", "name": "Reactor temperature alarm", "runOn": "browser", "enabled": true,
      "nodes": [
        { "id": "n1", "type": "tag-in",    "x": 40,  "y": 80, "config": { "tag": "sim.TT-2108.PV" } },
        { "id": "n2", "type": "threshold", "x": 300, "y": 80, "config": { "op": ">", "value": 92, "clear": 90 } },
        { "id": "n3", "type": "alarm",     "x": 560, "y": 60, "config": { "id": "TT-2108-high", "message": "Reactor outlet high: {{payload}} {{unit}}", "priority": "2", "component": "alarms" } }
      ],
      "wires": [ { "from": "n1", "port": 0, "to": "n2", "toPort": 0 }, { "from": "n2", "port": 0, "to": "n3", "toPort": 0 }, { "from": "n2", "port": 1, "to": "n3", "toPort": 1 } ] }
  ]
}

sources

One per data session. type is a Smart.Industrial Connect adapter (simulator, websocket, sse, poll, manual) or agent; every other field is the adapter's configuration. A simulator's tags double as the tag list the designer shows; an agent's are fetched from it (url empty means the agent the app is served from; token when the agent has auth). Tags are written source.tag.

pages

Components on a grid. at is { col, row, w, h } in grid cells, 1-based. props are the element's properties as written; what is not listed keeps the element's default. A page may carry its own grid.

tag is a Smart.Industrial element (smart-…) or one of the blocks the designer itself draws:

BlockWhat it isprops
studio-heading, studio-text, studio-labelText on the screentext, align
studio-boxA box or frame behind a group, as a front panel frames onetext, style (raised, recessed, flat)
studio-pageAnother page of this project, rendered here with its bindings live. A page cannot hold itself or one that already holds itpage, scale
studio-imageA picture: a plant photograph, a logo, a drawing the screen is built oversrc, fit, alt
studio-cameraA camera: an MJPEG stream, or a still re-fetched every refresh ms. RTSP cannot be opened by a browser and needs a gateway that re-serves itsrc, refresh, fit, alt
studio-frameA page from somewhere else, sandboxed unless allowScriptssrc, allowScripts, title
studio-cardsThis project's pages as cards to press: a landing screenpages, columns, label
studio-shapeA geometric figure whose colour can follow a tagshape, fill, stroke, strokeWidth, rotate, text, rules

A studio-shape is a figure - rectangle, rounded, circle, ellipse, diamond, triangle, triangleDown, pentagon, hexagon, octagon, star, arrowRight, arrowLeft, arrowUp, arrowDown, chevron, trapezoid, parallelogram, cylinder, cross, line, roundedTop - drawn as SVG so it stays sharp at any size and can be stroked and rotated without the three fighting each other. On a line the colour is the line, because a line has no inside.

It is in a process tool rather than a drawing program because of rules: colours by value, tried in order against the component's bound value.

{ "tag": "studio-shape",
  "props": { "shape": "circle", "fill": "#2a7a3d",
             "rules": [ { "over": 80, "colour": "#a51f1f" },
                        { "under": 20, "colour": "#1f5c94" },
                        { "equals": "fault", "colour": "#a51f1f" } ] },
  "bind": { "value": "plc.TT-101.PV" } }

The first rule that matches wins; none matching leaves fill. over and under compare as numbers, equals as text, so one shape can follow a measurement or a state word. A rectangle that turns red over 80 is the cheapest indicator there is, and it needs no diagram behind it.

Process symbols. The thirty-three equipment symbols the mimic draws - pump, valve, motor, vessel, exchanger, six kinds of valve, column, reactor, drum, silo, cyclone, turbine, generator, agitator, conveyor, transformer, disconnect, orifice, heater, cooler, breaker, fan, compressor, filter, instrument - are also placeable on a page on their own, as smart-mimic-symbol with a symbol property. They are equipment, and equipment belongs on a panel as much as on a mimic.

studio-cards lists the pages named in pages (ids, comma-separated, in that order), or every page but the one it stands on when pages is empty - so a page added later needs no editing. A card shows the page's name, and its description under it when the page has one.

Pressing a card does not switch pages by itself. It raises studio-open-page from the cell, bubbling and composed, with detail: { page, name }; whatever is hosting the project decides what to do with it. The published screen renders that page and moves the page strip. A host of your own - the embed SDK, a page of your own - listens for the same event, which is why a card works the same in all of them.

The designer's canvas deliberately does not listen: it renders live, so a card there is a real button, and one that navigated could not be clicked to select it and edit which pages it lists. Press Preview to use the cards as an operator will.

  • bind - property → "source.tag", or { "tag": "source.tag", "unit": "bar", "displayUnit": "psi" } with the options Connect takes. A bound property follows its tag with quality and staleness as Connect handles them.
  • feed - for a chart that takes records: kind is stream (one record per interval) or trend (live and history); fields maps a record field to a tag.
  • on - event → action. { "write": "setpoint" } writes the event's detail.value to the tag bound to that property, so a request goes out and the confirmed value comes back through the binding.
  • time - "$now", "$now-3600000", "$now+60000" anywhere in props is resolved when the property is applied.

flows

runOn is browser or agent; enabled false keeps a flow without running it. A node is { id, type, x, y, name?, config } with config holding the fields of its type (what is not set keeps the default); a wire is { from, port, to, toPort } with the ports 0 unless the node has several. The node reference lists the types and their fields.

A flow with "kind": "dataflow" is a diagram: its node types come from the diagram node reference, its wires join typed terminals (one wire per input), and its frames carry w and h - a node whose centre lies inside the rectangle is in the frame. A member of a Case frame carries case, the index of the case it belongs to (0 when absent), and the frame carries shown, the case the designer displays. A diagram with sub-input or sub-output nodes is a subdiagram, called by call nodes whose config.diagram is its id. Without kind a flow is a message flow.

alarms

The alarm table: a list of { id, tag, condition, limit, deadband, delay, priority, message, area, unit, enabled }, one condition on one tag each. condition is above, below, equals, not-equals, on, off or bad (above when absent); limit is needed by the first four; deadband is in the tag's unit, delay in seconds; priority is 1 (critical) to 4 (low), 2 when absent; enabled: false keeps an alarm without evaluating it. The ids are unique in the project, and a source may not be called alarms: that name is the project's alarm list, whose tags alarms.summary, alarms.active, alarms.unacknowledged, alarms.total and alarms.highest a component binds like any other.

"alarms": [
    { "id": "TT-2108-high", "tag": "sim.TT-2108.PV", "condition": "above", "limit": 92, "deadband": 2, "priority": 2, "message": "Reactor outlet high: {value} {unit}", "area": "Reactor" },
    { "id": "FT-2104-low", "tag": "sim.FT-2104.PV", "condition": "below", "limit": 256, "delay": 5, "priority": 3 }
]

Versions and migration

version is yours to bump; the agent lists it. The schema changes only when a document written today would no longer render; Studio.migrate(project) brings an older file to the current schema (a file from the first releases without a schema line, the front-panel theme's old name, a chart's autoScale: false, missing ids), the designer runs it on import and the runtime on open, and Studio.validateProject(project) lists what is wrong with a file, field by field. The format is written down as a JSON Schema too, schema/project.schema.json, for any validator.

Types

types is a list of records the project knows by name:

"types": [ { "id": "loop-reading", "name": "Loop reading", "description": "What a control loop reports",
             "fields": [ { "name": "pv", "type": "number", "unit": "bar", "min": 0, "max": 10 },
                         { "name": "sp", "type": "number", "unit": "bar" },
                         { "name": "auto", "type": "boolean" } ] } ]

A diagram node that follows a type carries config.typeId beside the names it took from it, so a project file is still complete on its own: a reader that knows nothing of types sees the field names. Studio.typeOf(project, id) and Studio.typeNames(type) read them.

Modules

A module is one file (.studio-module.json, schema smart-industrial-module/1) holding a part of a project to be used in another one:

{ "schema": "smart-industrial-module/1", "name": "Agitator watch", "kind": "diagram",
  "from": "Plant overview", "madeWith": "1.18.0",
  "parts": { "pages": [], "flows": [ … ] },
  "needs": { "tags": ["sim.ST-2112"], "components": ["agitator"] } }

kind is page, diagram or controls. parts holds the pages and flows exactly as a project holds them; needs is what the module expects to find where it lands. Studio.packModule(project, { kind, pageId, flowId, componentIds, name }) makes one and Studio.applyModule(project, module, { sourceMap, pageId }) puts one into a project, returning what it added and what it could not connect.

In a repository

Studio.serialize(project) writes a project with its keys in a fixed order and two-space indentation, so two saves of the same project are the same bytes and a diff shows only what changed; the designer's export and the agent's versions use it. For a repository, one file per page and per flow reads better than one document:

node scripts/studio-project.js split plant.json plant/         project.json, sources.json, pages/<id>.json, flows/<id>.json
node scripts/studio-project.js join plant/ plant.json          back into one file, migrated and checked
node scripts/studio-project.js check plant.json                the problems, if any (exit 1)
node scripts/studio-project.js format plant.json               the canonical order
node scripts/studio-project.js publish plant/ http://station-7:8800 line-3 --token …     a CI step

The designer does the same in the browser: Project › Export as a folder… gives the folder as a zip, and Import a project… takes that zip back (or a single JSON file). Ids are stable - a component keeps its id for as long as it exists, a node its id, a page its id - so a diff of pages/overview.json is a diff of that page.