{
  "openapi": "3.1.1",
  "info": {
    "title": "Twinworks PLC Scan Simulator API",
    "version": "1.0.0",
    "summary": "Structured Text, ladder or state machine + input timeline → scan-by-scan outputs, timers and trace.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://twinworksplcsimulator.usefulweb.app"
    }
  ],
  "paths": {
    "/api/v1/calculate": {
      "post": {
        "operationId": "calculate",
        "summary": "Structured Text, ladder or state machine + input timeline → scan-by-scan outputs, timers and trace.",
        "description": "Run a small PLC program in a simulated scan cycle: an IEC 61131-3 style Structured Text subset, a ladder diagram in text form or a state machine. Inputs change on a timeline; every scan reads inputs, executes the program and writes outputs. You get the output and state timeline, TON/TOF/TP timers and CTU/CTD counters per standard behaviour, the generated Structured Text and a timing diagram. Simulation only, no real PLC I/O.\n\nMethodology:\n- Own parser for a Structured Text subset (no eval): declarations, expressions with IEC precedence, IF/ELSIF/ELSE, CASE with ranges, FOR/WHILE/REPEAT with EXIT, function block calls with := inputs and => outputs. Types are checked before running; only widening conversions are implicit (INT → DINT → REAL), INT/DINT wrap at 16/32 bits like a PLC.\n- Ladder rungs and state machines compile to the same program representation and run in the same interpreter; the generated Structured Text is shown so you can see exactly what runs. Each rung is evaluated once per scan, top to bottom, so a coil written in a later rung is seen by earlier rungs only in the next scan.\n- Scan cycle: at k × cycle time the input image is taken from the timeline, the program executes once, and the output image is recorded. Timers use the scan start time: TON switches on at the first scan where IN has been TRUE for at least PT; TOF holds Q for PT after IN falls; TP gives a non-retriggerable pulse of length PT. CTU/CTD count rising edges of CU/CD between scans (pulses shorter than a scan are missed, as on a real PLC).\n- State machines become a CASE over State: at most one transition per scan, checked in the listed order; exit action, transition action, then entry action of the new state. StateTime is the time since the state was entered.\n- Sandbox: a watchdog stops a scan after 200,000 instructions, loops after 100,000 iterations, a run after 500,000 scans.\n\nTeaching and design aid only: a subset of IEC 61131-3 implemented from public descriptions, simulated in virtual time. It is not a certified runtime and never connects to real inputs or outputs; never use it to validate safety functions.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "twinworks_plc_simulator",
        "x-owner-app": "twinworks",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TwinworksPlcSimulatorInput"
              },
              "examples": {
                "example1": {
                  "summary": "TON, TOF, TP and CTU on one push button",
                  "value": {
                    "language": "st",
                    "program": "PROGRAM Timers\nVAR_INPUT\n  Button : BOOL;\nEND_VAR\nVAR_OUTPUT\n  OnDelay, OffDelay, Pulse : BOOL;\n  Presses : INT;\nEND_VAR\nVAR\n  T1 : TON;  T2 : TOF;  T3 : TP;  C1 : CTU;\nEND_VAR\nT1(IN := Button, PT := T#300ms);\nT2(IN := Button, PT := T#300ms);\nT3(IN := Button, PT := T#300ms);\nC1(CU := Button, R := FALSE, PV := 3);\nOnDelay := T1.Q;\nOffDelay := T2.Q;\nPulse := T3.Q;\nPresses := C1.CV;\nEND_PROGRAM",
                    "timeline": "0 Button=0\n100ms Button=1\n200ms Button=0\n400ms Button=1\n1000ms Button=0\n1100ms Button=1\n1150ms Button=0",
                    "cycleMs": 10,
                    "durationMs": 1500
                  }
                },
                "example2": {
                  "summary": "Start/stop with interlock (seal-in rung)",
                  "value": {
                    "language": "ladder",
                    "program": "// M2 runs until buffer B2 is full or M3 is down, and restarts only when B2 is empty again.\nrung Seal-in: (Start | Run) & !Stop & !BufferFull -> Run",
                    "timeline": "0 Start=1 Stop=0 BufferFull=0\n2s Start=0\n20s BufferFull=1\n35s BufferFull=0\n50s Start=1\n60s Stop=1\n70s Stop=0",
                    "cycleMs": 500,
                    "durationMs": 80000
                  }
                },
                "example3": {
                  "summary": "Buffer-level control with hysteresis",
                  "value": {
                    "language": "st",
                    "program": "PROGRAM BufferLevel\nVAR_INPUT\n  Level : INT;  (* parts in buffer B1 *)\nEND_VAR\nVAR_OUTPUT\n  Feed : BOOL;  (* release parts at the source *)\nEND_VAR\nVAR CONSTANT\n  High : INT := 4;\n  Low : INT := 1;\nEND_VAR\n(* two-point control: stop feeding at High, resume at Low *)\nIF Level >= High THEN\n  Feed := FALSE;\nELSIF Level <= Low THEN\n  Feed := TRUE;\nEND_IF;\nEND_PROGRAM",
                    "timeline": "0 Level=0\n10s Level=2\n20s Level=4\n30s Level=3\n40s Level=1\n50s Level=0",
                    "cycleMs": 1000,
                    "durationMs": 60000
                  }
                },
                "example4": {
                  "summary": "Two-station handshake (state machine)",
                  "value": {
                    "language": "statemachine",
                    "program": "name Handshake\ninput PartReady, M2Busy\noutput Load, M1Run\n// M1 may only start a part while the transfer buffer B1 is empty; M2 loads one part at a time.\nalways M1Run := NOT PartReady;\nstate Wait initial\n  entry Load := FALSE;\nstate Transfer\n  entry Load := TRUE;\nstate Process\n  entry Load := FALSE;\ntransition Wait -> Transfer when PartReady AND NOT M2Busy\ntransition Transfer -> Process when M2Busy\ntransition Process -> Wait when NOT M2Busy",
                    "timeline": "0 PartReady=0 M2Busy=0\n5s PartReady=1\n6s M2Busy=1\n7s PartReady=0\n30s M2Busy=0",
                    "cycleMs": 500,
                    "durationMs": 40000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TwinworksPlcSimulatorResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tool": {
      "get": {
        "operationId": "getToolDefinition",
        "summary": "Inputs, output schema and examples for this tool",
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "TwinworksPlcSimulatorInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "language": {
            "title": "Language",
            "type": "string",
            "enum": [
              "st",
              "ladder",
              "statemachine"
            ],
            "default": "st"
          },
          "program": {
            "title": "Program",
            "description": "Structured Text: VAR_INPUT / VAR_OUTPUT / VAR blocks (BOOL, INT, DINT, REAL, TIME, TON, TOF, TP, CTU, CTD, CTUD, R_TRIG, F_TRIG, SR, RS), IF/ELSIF/ELSE, CASE, FOR, WHILE, REPEAT, := assignments, PLC_TIME. Ladder: one rung per line, \"(Start | Motor) & !Stop -> Motor\", outputs Name, !Name, S Name, R Name, TON T1 PT=T#5s, CTU C1 PV=3 R=Reset; POS(x)/NEG(x) edge contacts. State machine: input/output/var lines, \"state Name [initial]\", entry/do/exit statements, \"transition A -> B when GUARD\", StateTime.",
            "type": "string",
            "maxLength": 100000,
            "default": "PROGRAM Timers\nVAR_INPUT\n  Button : BOOL;\nEND_VAR\nVAR_OUTPUT\n  OnDelay, OffDelay, Pulse : BOOL;\n  Presses : INT;\nEND_VAR\nVAR\n  T1 : TON;  T2 : TOF;  T3 : TP;  C1 : CTU;\nEND_VAR\nT1(IN := Button, PT := T#300ms);\nT2(IN := Button, PT := T#300ms);\nT3(IN := Button, PT := T#300ms);\nC1(CU := Button, R := FALSE, PV := 3);\nOnDelay := T1.Q;\nOffDelay := T2.Q;\nPulse := T3.Q;\nPresses := C1.CV;\nEND_PROGRAM"
          },
          "timeline": {
            "title": "Input timeline",
            "description": "One line per time: time then Name=value pairs, e.g. \"0 Start=0\", \"250ms Start=1\", \"2s Stop=TRUE\". Times in ms unless s/m/h or T#… is given. Inputs keep their value until changed.",
            "type": "string",
            "maxLength": 50000,
            "default": "0 Button=0\n100ms Button=1\n200ms Button=0\n400ms Button=1\n1000ms Button=0\n1100ms Button=1\n1150ms Button=0"
          },
          "cycleMs": {
            "title": "Scan cycle time",
            "type": "integer",
            "minimum": 1,
            "maximum": 60000,
            "default": 10,
            "x-unit": "ms"
          },
          "durationMs": {
            "title": "Duration",
            "type": "integer",
            "minimum": 1,
            "maximum": 86400000,
            "default": 1500,
            "x-unit": "ms"
          },
          "watch": {
            "title": "Extra variables to trace (optional)",
            "description": "Comma-separated, e.g. T1.ET, Count. Inputs, outputs, timer Q and counter CV are always traced.",
            "type": "string",
            "maxLength": 1000,
            "default": ""
          }
        },
        "required": []
      },
      "TwinworksPlcSimulatorResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "program",
              "language",
              "cycleMs",
              "durationMs",
              "scans",
              "signals",
              "final",
              "structuredText"
            ],
            "properties": {
              "program": {
                "type": "string"
              },
              "language": {
                "type": "string"
              },
              "cycleMs": {
                "type": "integer"
              },
              "durationMs": {
                "type": "number"
              },
              "scans": {
                "type": "integer"
              },
              "signals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "type",
                    "changes"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string"
                    },
                    "changes": {
                      "type": "array",
                      "description": "[time ms, value] pairs (BOOL as 0/1, TIME in ms).",
                      "items": {
                        "type": "array",
                        "items": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              },
              "final": {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                }
              },
              "states": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string"
                }
              },
              "structuredText": {
                "type": "string",
                "description": "The Structured Text the program runs as (generated for ladder and state machines)."
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}