KloradDocs

Act on a twin

Define an Action that sends a command to a device, check who may take it, and watch the twin follow the device.

This guide makes an entrance barrier operable: operators may open it, visitors may not, and the twin shows the barrier open only once the device says it is.

interaction.ts
import { allowEveryone, createAccessPolicy } from "@klorad/access";
import { connectSource, fixtureDevice } from "@klorad/api/integration";
import { createInteraction, createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "lamp", name: "Street lamp", position: { east: -8, north: 4 }, properties: { level: 100 } });
scene.add({
  id: "barrier",
  name: "Entrance barrier",
  position: { east: 6, north: -2 },
  correspondence: "twin",
  binding: { source: "plc", entity: "barrier-1" },
});

// The physical side: a simulated PLC that reports the barrier's state and accepts commands.
const plc = fixtureDevice({
  id: "plc",
  entities: { "barrier-1": { state: "closed" } },
  respond: (command, now) => (now.state === command.name ? `already ${now.state}` : { state: command.name }),
});
connectSource(scene, plc);

const policy = createAccessPolicy({
  scene,
  roles: { operator: "*", visitor: [] },
  worlds: [{ id: "site", name: "Site", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "operator" }] }],
});

const interaction = createInteraction(scene, {
  entitlement: policy,
  actuators: [plc],
  behaviours: [
    { id: "dim", run: ({ update, animate }) => (update({ properties: { level: 30 } }), animate({ kind: "pulse" })) },
    { id: "requested", run: ({ update, now }) => update({ properties: { requestedAt: now } }) },
  ],
  actions: [
    { id: "dim", label: "Dim", appliesTo: (o) => o.id === "lamp", behaviour: "dim" },
    { id: "open", label: "Open", appliesTo: (o) => o.correspondence === "twin", behaviour: "requested", command: () => ({ name: "open" }) },
  ],
});

const operator = { id: "eleni", roles: ["operator"] };
const visitor = { id: "guest", roles: ["visitor"] };

async function demo() {
  await interaction.perform({ action: "dim", objectId: "lamp", actor: visitor, world: "site" }); // { outcome: "denied", reason: ... }
  await interaction.perform({ action: "dim", objectId: "lamp", actor: operator, world: "site" }); // { outcome: "done" }
  await interaction.perform({ action: "open", objectId: "barrier", actor: operator, world: "site" }); // { outcome: "sent", command }
  // About 600 ms later the PLC reports state "open": scene.latest("barrier", "state")?.value === "open"
  return interaction.available("lamp", operator, "site").map((a) => a.id); // ["dim"]
}

// For a demo or a single-user tool, allowEveryone says so in its name.
const playground = createInteraction(createScene({ coordinateSystem: { origin: { lat: 0, lon: 0 } } }), { entitlement: allowEveryone });

export { demo, playground };

Step by step

  1. The barrier is a twin. correspondence: "twin" with a binding to the PLC that controls it. A shadow could not be commanded.
  2. The device is an actuator and a source. fixtureDevice simulates the PLC: it reports state and accepts commands. In production this is your own Actuator, sending the command to the real controller, plus the source that reports its state.
  3. The policy decides. Operators have every Action in the site World; visitors have none. Swap in your own roles and Worlds; see Give a team its own view.
  4. The Action carries a command. open applies to twins and sends { name: "open" }. Its Behaviour only records that the opening was requested.
  5. The reading closes the loop. About 600 ms later the device reports state: "open", and scene.latest("barrier", "state") follows.

Handling outcomes

Show the outcome to the person who acted. denied and failed carry a reason worth displaying; sent means "the device accepted it", not "it happened". If the device never reports back, the twin keeps showing the old state, which is the truth as far as anyone knows.

Run it liveSandbox: ActionsAction, Entitlement, Behaviour, Animation: an operator may act and a visitor may not; a twin's command goes out and its state follows the device.

Something wrong or unclear? Tell your contact at Prieston Technologies.