# ACC PLC Simulator — How to Generate Programs with AI

**A complete guide for creating ladder logic (.json) and Structured Text (.st) files that load directly into the ACC PLC Simulator**

Simulator: https://accautomation.ca/simulator/ · Guide URL: https://accautomation.ca/simulator/acc-ai-guide.md · ACC Automation

Verified against `acc-plc-simulator.html` v1.122 and `acc-st.js` v1.015 by running both engines: 66 ladder behaviour tests and 66 Structured Text tests, plus both complete examples end to end. The v1.015 engine adds the nested function-block fix described in Section 8.4.

---

## Instructions for AI assistants

If a user has asked you to convert, write or fix a PLC program for the ACC PLC Simulator, follow this guide exactly.

1. **Ask which language** the user wants if they have not said: ladder logic (delivered as a `.json` file) or Structured Text (delivered as a `.st` plain-text file). Never wrap ST in JSON.
2. **Map the I/O** to the simulator's addresses (Section 3.1). If the user names a scene, use its I/O table (Section 3.4). Remember that Stop and E-stop inputs are wired normally closed (Section 3.3). If the user has more than one scene connected, add each scene's address offset (Section 3.5).
3. **Convert the logic** using Section 5. The user's original program may be written for any PLC brand; convert its instructions to the equivalents listed in this guide, and tell the user about anything that has no equivalent (Section 5.1).
4. **Write the file** in the exact format of Section 4 (ladder) or Section 8 (ST). Copy the structure of the complete examples in Sections 6 to 8.
5. **Check it** against Section 10 before giving it to the user, then explain how to import it (Section 2) and how to test it (Section 10.3).

Reference files on this site:

| File | URL |
|---|---|
| This guide | https://accautomation.ca/simulator/acc-ai-guide.md |
| AI index (llms.txt) | https://accautomation.ca/simulator/llms.txt |
| Ladder example — conveyor | https://accautomation.ca/simulator/acc-example-conveyor-shuttle.json |
| ST example — conveyor | https://accautomation.ca/simulator/acc-example-conveyor-shuttle.st |
| Ladder example — analog | https://accautomation.ca/simulator/acc-example-analog-tank-level.json |
| ST example — analog | https://accautomation.ca/simulator/acc-example-analog-tank-level.st |
| Ladder file validator (Python) | https://accautomation.ca/simulator/acc-validate-program.py |
| Simulator help | https://accautomation.ca/simulator/acc-plc-simulator-help.html |
| Scene guide | https://accautomation.ca/simulator/acc-scene-help.html |

---

## 1. What this guide is for

The ACC PLC Simulator is a free, browser-based PLC that runs ladder logic and IEC 61131-3 Structured Text against interactive 3D industrial scenes. Programs can be typed in by hand, but they can also be written by an AI assistant (Claude, ChatGPT, Gemini, Copilot and others) and imported as a file.

An AI can only produce a file that loads correctly if it knows the exact file format, the addresses the simulator accepts, and how the simulator scans a program. This guide supplies all three. Every ladder format rule in it was checked against the simulator's own import, export and scan code. It is written so that a person with no programming background can follow how it works, and so that the whole document can be pasted into an AI chat as its instructions.

**How to use it:**

The quickest way is to give the AI the link and your program:

> Read https://accautomation.ca/simulator/acc-ai-guide.md and convert my program below to ladder logic for the ACC PLC Simulator.

AI assistants that can browse the web will read the guide and follow it. If the AI cannot open links:

1. Open a new AI chat.
2. Paste this entire guide, or attach it as a file.
3. Describe the machine or process you want to control (see the prompt template in Section 11).
4. Ask for either a **ladder logic JSON file** or a **Structured Text .st file**.
5. Check the result with Section 10, then import it into the simulator.

---

## 2. The two file types — an important distinction

The simulator stores the two programming languages in **different file formats**.

| Language | File type | What it looks like | How to load it |
|---|---|---|---|
| Ladder logic | `.json` | Structured data describing rungs, contacts, coils and blocks | **📂 LOAD → ⬆ Import .json** (in the Save/Load dialog) |
| Structured Text | `.st` | Plain text source code, exactly as typed in the editor | Switch to **ST** mode, then use **Import .st** |

Structured Text is **not** stored as JSON. An ST program is simply the text you would type into the ST editor, saved in a plain text file ending in `.st`. When someone asks for "an ST file using JSON", the correct deliverable is a `.st` text file. Only ladder logic uses JSON.

A program is written in ladder **or** in ST. Both languages use the same I/O addresses, the same 100 ms scan, the same 3D scenes and the same Modbus bridge, so the same machine can be controlled either way.

---

## 3. Simulator facts every program must respect

### 3.1 Memory map (addresses)

| Prefix | Range | Type | Read by | Written by |
|---|---|---|---|---|
| `X` | X1–X16 | Digital input (pushbuttons, sensors) | Contacts, ST | The scene or the user |
| `Y` | Y1–Y16 | Digital output (motors, valves, lights) | Contacts, ST | OTE / OTL / OTU coils, ST |
| `C` | C1–C256 | Internal relay (program memory bit) | Contacts, ST | OTE / OTL / OTU coils, ST |
| `T` | T1–T256 | Ladder timer | Contacts (`T1`, `T1/DN`, `T1/EN`, `T1/TT`); ST reads the done bit only | TON / TOF blocks, RES |
| `CT` | CT1–CT256 | Ladder counter | Contacts (`CT1`, `CT1/DN`); ST reads the done bit only | CTU / CTD blocks, RES |
| `AX` | AX1–AX8 | Analog input, integer 0–4095 | **Compare** elements, MOV source, ST | The scene or the AN-tab slider |
| `AY` | AY1–AY4 | Analog output, integer 0–4095 | Compare elements, MOV source, ST | **MOV** block, ST |

Notes:

- A bare `T1` means `T1/DN` (the done bit). A bare `CT1` means `CT1/DN`.
- **The `/DN` form is ladder-only.** In ladder, `T1`, `T1/DN`, `T1/EN` and `T1/TT` are all valid contact addresses. In Structured Text only the bare `T1` and `CT1` work — `T1/DN` there is a syntax error, because `/` is read as division.
- **Contacts cannot read analog registers.** `XIC AX1` is always false and `XIO AX1` is always true. Use a compare element (Section 4.6) instead.
- `C100` and up is the recommended convention for internal relays, so they are not confused with timer and counter numbers. Programs using C1 and up also work.
- Addresses must be inside these ranges. An out-of-range timer or counter number (such as `T0` or `T300`) causes a script error on every scan, so the program stops updating. An out-of-range `C` address (`C0`, `C300`) does not error — it reads and writes silently but never appears on the I/O panel, so it looks like the program is doing nothing. Keep to C1–C256.
- Analog values 0–4095 represent 0–100 % (0–10 V or 4–20 mA on real hardware). A percentage converts as `value = percent × 4095 ÷ 100`, rounded. For example 20 % = 819, 85 % = 3481, 95 % = 3890.

### 3.2 How the scan works

Every 100 ms the simulator:

1. Reads inputs (X and AX) from the connected scene.
2. Executes the program from **Rung 0000 down to the last rung**, left to right within each rung.
3. Writes outputs (Y and AY) to the scene.
4. Redraws the display.

These consequences matter when writing a program:

- **Memory updates immediately.** When a rung turns on `C1`, every rung *below* it sees `C1` on during the same scan. Rungs *above* it see the new value on the next scan. Rung order therefore changes behaviour.
- **OTE follows its rung every scan.** OTE writes ON when the rung is true and OFF when it is false. OTL and OTU only act when their rung is true, so the bit keeps its value otherwise.
- **MOV only writes when its rung is true.** The AY register keeps its last value when the rung goes false. If an output must return to a value, write a second MOV rung for that case.
- **STOP clears Y, C and AY, but not timers or counters.** Timer accumulators and counter counts keep their values through STOP. Loading or importing a program clears all memory, including X inputs, timers and counters. Programs should not rely on Y or C bits surviving a STOP, and should reset counters deliberately with RES.

### 3.3 Input wiring conventions used by the scenes

Stop pushbuttons and E-stops in the scenes are wired **normally closed (N.C.)** for fail-safe operation. The input is **ON when the button is not pressed** and turns OFF when pressed or if the wire breaks. So a Stop button is programmed with an **XIC** contact in series with the run logic. This surprises many beginners.

Always check the scene's I/O list (shown in the Connect dialog and the scene's About panel) before writing a program. For example, the Conveyor scene uses X1 Start (N.O.), X2 Stop (N.C.), X3 left sensor, X4 right sensor, Y1 motor run and Y2 direction.

### 3.4 Scene I/O reference

Current live scenes, from the simulator's scene registry (`acc-scenes.js`). The Connect dialog in the simulator always shows the current list.

**Control Panel** (`acc-panel-scene.html`)

| Address | Signal | Notes |
|---|---|---|
| X1 | Start PB | N.O. |
| X2 | Stop PB | **N.C.** — ON when not pressed |
| X3 | Jog PB | N.O. |
| Y1 | Motor run light | |

**Motor Starter** (`acc-motor-starter-scene.html`)

| Address | Signal | Notes |
|---|---|---|
| X1 | Start PB | N.O. |
| X2 | Stop PB | **N.C.** |
| X3 | Motor overload auxiliary contact | **N.C., maintained** — ON while healthy |
| Y1 | Motor contactor | |
| Y2 | Run light | |
| Y3 | Warning horn | Sounds 2 s before the contactor pulls in |

**Conveyor System** (`acc-conveyor-scene.html`)

| Address | Signal | Notes |
|---|---|---|
| X1 | Start PB | N.O. |
| X2 | Stop PB | **N.C.** |
| X3 | Left sensor | |
| X4 | Right sensor | |
| Y1 | Motor run | |
| Y2 | Direction | ON = reverse |

**Pick & Place Palletizer** (`acc-palletizer-scene.html`)

| Address | Signal | Address | Signal |
|---|---|---|---|
| X1 | Box present | Y1 | Arm move X |
| X2 | Arm home X | Y2 | Arm move Y (down) |
| X3 | Arm home Y (up) | Y3 | Grip close (vacuum) |
| X4 | Grip confirm (vacuum) | Y4 | Feed conveyor enable |
| X5 | At pick X | Y5 | Pallet eject |
| X6 | At place X1 (column 1) | | |
| X7 | At place X2 (column 2) | | |
| X8 | Arm down | | |
| X9 | Contact pressure | | |
| X10 | Skid present | | |

**Tank Fill Station** (`acc-tank-scene.html`)

| Address | Signal | Address | Signal |
|---|---|---|---|
| X1 | Low float (20 %) | Y1 | Inlet valve |
| X2 | High float (85 %) | Y2 | Drain pump |
| X3 | Start PB (N.O.) | Y3 | Overflow alarm (strobe) |
| X4 | Stop PB (**N.C.**) | Y4 | Mixer motor |
| X5 | E-stop (**N.C.**, latching) | | |
| X6 | Auto/Manual selector (maintained) | | |
| X7 | Inlet valve PB (manual) | | |
| X8 | Drain pump PB (manual) | | |
| X9 | Mixer PB (manual) | | |
| X10 | Overflow float (95 %) | | |

The tank level is published only as Modbus Input Register 30001 for external PLCs. It is **not** an AX address inside the simulator, so tank programs written for the simulator use the float switches.

**Traffic Light Intersection** (`acc-traffic-scene.html`)

| Address | Signal | Address | Signal |
|---|---|---|---|
| X1 | Ped PB N/S (cross E/W road) | Y1 | NS green |
| X2 | Ped PB E/W (cross N/S road) | Y2 | NS yellow |
| | | Y3 | NS red |
| | | Y4 | EW green |
| | | Y5 | EW yellow |
| | | Y6 | EW red |

**Analog inputs:** no live scene currently drives AX1–AX8. Programs that use analog values are tested with the AN-tab sliders, or with a gamepad (triggers and sticks drive AX1–AX5).

### 3.5 Running more than one scene at once (address offsets)

The addresses in Section 3.4 are what a scene uses **when it is the only scene connected**. That is the normal case, and unless the user tells you otherwise, write the program against those addresses exactly as listed.

From simulator v1.121 several scenes can be connected at the same time. Two scenes both using X1–X3 would fight over the same inputs, so the simulator gives each one its own block of PLC memory and translates. The scene still knows itself as X1–X3 internally; the **program** must use the shifted addresses.

| | Scene's own address | PLC address |
|---|---|---|
| Motor Starter, first connected (offset 0) | X1–X3 / Y1–Y3 | X1–X3 / Y1–Y3 |
| Control Panel, connected second (offset X+3 / Y+3) | X1–X3 / Y1 | **X4–X6 / Y4** |

The rule is one addition: **PLC address = scene address + that scene's offset**, applied separately to X and to Y. The offset never exceeds 15, and X and Y are offset independently.

**How to find the offsets.** They are shown in three places, and the user can read any of them to you:

- The CONNECT dialog lists each connected scene and its block, e.g. `Motor Starter X1–X3 · Y1–Y3`.
- The PLC memory strip at the top of that dialog colours the addresses each scene holds.
- The scene window itself relabels its I/O rows to the PLC address once it is offset, and its panel header reads `INPUTS X (scene X1–X3 → PLC X4–X6)`.

**What to do when writing a program:**

- If the user says nothing about offsets, assume every scene is at offset 0 and use the Section 3.4 addresses. This is correct for the single-scene case, which is most of the time.
- If the user names offsets, or pastes a block like `X4–X6 · Y4`, add the offset to every **discrete** address for that scene — X contacts and Y coils. Address comments should name the device, not the offset, since the offset can change.
- **Do not offset analog.** `AX` and `AY` addresses in a compare or a MOV stay exactly as Section 3.4 lists them. A scene that uses analog has to run at offset 0 anyway (see below), so its analog addresses are already correct.
- Never mix: a program written for one scene at offset 0 will drive the wrong machine if that scene is later given an offset. Say which offsets the program assumes.
- **Modbus addresses never shift.** Offsets exist only on the simulator-to-scene link. A scene in Modbus mode is never offset, so X5 is always Discrete Input 10005 regardless.

**Analog scenes run at offset 0.** The offset mechanism applies the digital offsets to the much smaller analog space (8 AX, 4 AY), so analog is dropped silently on an offset scene. Any scene using AX or AY must therefore be the only connected scene, or the first one launched. In practice this means: if a program uses analog, write it against the Section 3.4 addresses unchanged, and say in your notes that the analog scene must be connected first.

---

## 4. Ladder logic JSON format

### 4.1 Overall file structure

This is the **version 2** format that the simulator currently exports.

```json
{
  "version": 2,
  "name": "Program Name",
  "exported": "2026-09-16T00:00:00.000Z",
  "notes": "Optional documentation for people reading the file.",
  "program": [
    { "id": 1, "rn": 0, "comment": "…", "elements": [ … ] },
    { "id": 2, "rn": 1, "comment": "…", "elements": [ … ] }
  ],
  "comments": {
    "X1": "Start PB",
    "Y1": "Motor Run"
  }
}
```

| Field | Required | What the simulator does with it |
|---|---|---|
| `version` | Recommended | Use `2`. Written on export; not checked on import. |
| `name` | Recommended | Written on export (always as "ACC PLC Program"); ignored on import. Useful for people. |
| `exported` | Recommended | ISO-8601 date/time string. Ignored on import. |
| `notes` | Optional | **Ignored** by the simulator and not kept on re-export. Use it only as documentation inside the file. |
| `program` | **Yes** | Array of rung objects, in execution order. |
| `comments` | Recommended | Address comments (Section 4.8). |

The importer also accepts a bare array of rungs with no wrapper, but the wrapped form above is preferred.

### 4.2 Rung object

| Field | Meaning |
|---|---|
| `id` | Integer, unique among rungs. Use 1, 2, 3 … |
| `rn` | Rung number starting at 0. The simulator renumbers rungs 0, 1, 2 … in array order on import, so the array order is what matters, but matching numbers keep the file readable. |
| `comment` | Rung comment shown in the rung header. Describe what the rung does. |
| `elements` | Array of elements, read **left to right**. |

**Rules for `elements`:**

- At least one **input** element (contact, compare, or parallel group), and at least one **output** element (coil or block). The **last** element must be an output or a block — a contact in last position drives nothing at all.
- **More than one output on a rung is allowed.** Coils and blocks in series all act, and power passes straight through them, so `XIC X1 → OTE Y1 → OTE Y2 → OTL C5` energises all three from the one condition. This is how the shipped Palletizer program sets and clears several step bits on a single rung. A contact placed *after* an output gates only the outputs that follow it, which is a compact way to write "do A always, and B only if…". One output per rung is still the clearer default; reach for several only when they genuinely share a condition.
- Input elements listed one after another are in **series** (Boolean AND).
- An OR is made with a `parallel` element (Section 4.5).
- A rung with only an output and no inputs is treated as always true, but the editor never creates one; use an always-true contact instead (Section 4.7, MOV).
- **Do not create empty rungs.** A rung with `"elements": []` scans harmlessly, but the simulator does not draw its number or comment, so a "documentation rung" is invisible. Put documentation in rung comments, the `comments` map, or `notes`.

### 4.3 Element IDs

Every element has an integer `id`. IDs must be **unique across the whole file**, including elements inside parallel branches, because the editor finds elements by id. A simple method is to number elements 101, 102, 103 … in the order they are written. After import, the simulator gives new elements ids above the highest one in the file.

### 4.4 Complete element reference

| `kind` | `type` values | `addr` | Extra fields | Position |
|---|---|---|---|---|
| `contact` | `XIC`, `XIO` | X, Y, C, T (/DN /EN /TT), CT (/DN) | — | Input area or inside a branch |
| `compare` | `GRT`, `LES`, `GEQ`, `LEQ`, `EQU`, `NEQ` | AX1–AX8 or AY1–AY4 | `cmp` (number) | Input area or inside a branch |
| `parallel` | `"XIC"` (placeholder) | `""` | `branches` | Input area |
| `output` | `OTE`, `OTL`, `OTU` | Y1–Y16, C1–C256 | — | Last element |
| `block` | `TON`, `TOF` | T1–T256 | `pre` (milliseconds) | Last element |
| `block` | `CTU`, `CTD` | CT1–CT256 | `pre` (count) | Last element |
| `block` | `RES` | T1–T256 or CT1–CT256 | `pre`: `0` | Last element |
| `block` | `MOV` | AY1–AY4 (destination) | `src` (string), `pre`: `0` | Last element |

The importer only reads `id`, `kind`, `type`, `addr`, plus `pre` and `src` for blocks, `cmp` for compares and `branches` for parallel groups. Anything else on an element is ignored, except a legacy `comment` (Section 4.8).

### 4.5 Contacts and parallel branches

**Contacts**

```json
{"id":101,"kind":"contact","type":"XIC","addr":"X1"}
```

| `type` | Name | Passes power when… |
|---|---|---|
| `XIC` | Examine If Closed (normally open, `-| |-`) | the addressed bit is ON (1) |
| `XIO` | Examine If Open (normally closed, `-|/|-`) | the addressed bit is OFF (0) |

**Parallel branches (OR logic)**

A parallel group is one element holding two or more **branches**. Each branch is an array of contacts and/or compares in series. The group passes power if **any** branch passes power.

```json
{"id":121,"kind":"parallel","type":"XIC","addr":"","branches":[
  [ {"id":122,"kind":"contact","type":"XIC","addr":"X1"} ],
  [ {"id":123,"kind":"contact","type":"XIC","addr":"C1"},
    {"id":124,"kind":"contact","type":"XIO","addr":"C2"} ]
]}
```

This reads as: `X1 OR (C1 AND NOT C2)`.

Rules:

- `"type": "XIC"` and `"addr": ""` on the parallel element are placeholders. The simulator's own export leaves them out and the importer fills them in, so either form loads.
- A group needs **at least two** branches, and no branch may be empty.
- Branches contain **contacts and compares only**. Outputs and blocks never go inside a branch.
- Keep branches **flat** — never put a parallel group inside a branch. A nested group *evaluates* correctly, but the simulator sizes a rung from the branch count of its top-level groups only, so the nested rows are drawn over the rows below them and the rung becomes unreadable. If the logic is `(A OR B) AND C OR D`, expand it into branches such as `[A, C]`, `[B, C]`, `[D]`.
- A single-branch group loads and behaves as that one branch, but the editor's Parallel Branch Editor expects at least two. Use a plain series contact instead of a one-branch group.

### 4.6 Compare elements (analog thresholds)

```json
{"id":101,"kind":"compare","type":"LES","addr":"AX1","cmp":819}
```

A compare sits in the input area, like a contact, and passes power when the register value meets the condition against the fixed number in `cmp`.

| `type` | Passes power when… |
|---|---|
| `GRT` | register > `cmp` |
| `LES` | register < `cmp` |
| `GEQ` | register ≥ `cmp` |
| `LEQ` | register ≤ `cmp` |
| `EQU` | register = `cmp` (exact match) |
| `NEQ` | register ≠ `cmp` |

- `addr` is normally an analog input, `AX1`–`AX8`. `AY1`–`AY4` also works, for example to check a value the program has already moved, but only AX addresses are listed on the I/O tab.
- `cmp` is a **number**, not a string, normally 0–4095. It is a fixed value; a compare cannot compare two registers.
- Analog signals rarely hit an exact value, so prefer `GRT`/`LES`/`GEQ`/`LEQ` over `EQU`.
- For on/off control with a dead band (hysteresis), use two rungs: one compare with `OTL` and a second compare with `OTU`, as in Section 7.
- Compares can be placed in series with contacts and inside parallel branches.

### 4.7 Output coils and blocks

**Coils**

```json
{"id":110,"kind":"output","type":"OTE","addr":"Y1"}
```

| `type` | Name | Behaviour |
|---|---|---|
| `OTE` | Output Energize `-( )-` | Bit ON while rung is true, OFF when false |
| `OTL` | Output Latch `-(L)-` | Sets the bit ON when rung is true; stays ON |
| `OTU` | Output Unlatch `-(U)-` | Sets the bit OFF when rung is true |

Coils write `Y1`–`Y16` or `C1`–`C256`. A coil addressed to an `X` input is not rejected — it does write the input bit — but a connected scene rewrites every X at the start of each scan, so the value is overwritten and the rung appears to do nothing. Never target X with a coil. A coil addressed to `AY` does nothing at all; use MOV.

**Timers**

```json
{"id":128,"kind":"block","type":"TON","addr":"T1","pre":15000}
```

| `type` | Behaviour |
|---|---|
| `TON` | On-delay. While the rung is true, the accumulator counts up (to a maximum of `pre`) and DN turns on when it reaches `pre`. When the rung goes false, the timer resets to 0 and DN turns off. |
| `TOF` | Off-delay. While the rung is true, DN is on and the accumulator is 0. When the rung goes false, the accumulator counts; DN turns off once it reaches `pre`. |

`pre` is in **milliseconds** (1000 = 1 second). A missing or zero `pre` makes the timer done immediately. The accumulator advances by the real elapsed time each scan (about 100 ms). Read the result on another rung with a contact such as `XIC T1`, `XIC T1/TT` (timing) or `XIC T1/EN` (enabled).

**Counters**

```json
{"id":140,"kind":"block","type":"CTU","addr":"CT1","pre":5}
```

| `type` | Behaviour |
|---|---|
| `CTU` | Adds 1 on each OFF→ON transition of its rung. DN turns on when the count reaches `pre`. The count keeps rising past `pre`. |
| `CTD` | Subtracts 1 on each OFF→ON transition, but never below 0. DN turns on when the count reaches 0. |

- Holding the rung true does not keep counting; the rung must go false and true again.
- Counts start at 0, so a `CTD` on its own never changes. Use `CTD` together with a `CTU` on the **same address** (an up/down counter), and give both the same `pre`, because each block writes its preset every scan.
- Counts are cleared only by a `RES` rung or by loading a program. **STOP does not clear them.**

**Reset**

```json
{"id":150,"kind":"block","type":"RES","addr":"CT1","pre":0}
```

While its rung is true, `RES` clears the named timer or counter: the count or accumulated time goes to 0 and DN turns off (a timer's EN also turns off). `addr` is `T1`–`T256` or `CT1`–`CT256`, without `/DN`. Include `"pre": 0`.

A TON whose rung is still true starts timing again on the next scan after a reset.

**Move to analog output**

```json
{"id":160,"kind":"block","type":"MOV","addr":"AY1","src":"4095","pre":0}
```

While its rung is true, `MOV` writes the source value to the destination.

- `addr` is the destination, `AY1`–`AY4`.
- `src` is a **string**: either a constant such as `"2048"`, or a register such as `"AX1"` or `"AY2"`. A source that is neither is treated as 0.
- The written value is rounded and limited to 0–4095.
- Include `"pre": 0`.
- The destination **keeps its value** when the rung goes false, so a two-state output needs two MOV rungs (one for each state).
- To copy an input straight through (for example a speed pot to a drive reference), use a rung that is always true in RUN, such as `XIC X2` for a Stop that is normally on, with `MOV AY1` and `"src": "AX1"`.

### 4.8 Address comments

The simulator keeps **one comment per address**. Every contact, compare, coil or block that uses `X1` shows the same "Start PB" label.

In version 2 files, address comments live in a top-level `comments` object:

```json
"comments": {
  "X1": "Start PB",
  "X2": "Stop PB N.C.",
  "AX1": "Tank Level 0-4095",
  "C101": "Fill Request",
  "T1": "Mix Timer"
}
```

- Keys are addresses in **upper case**, exactly as written in the program (`X1`, `C101`, `T1`, `CT1`, `AX1`, `AY1`). A contact written as `T1/DN` or `T1/TT` looks up that exact key, so add a key for every form the program uses (for example both `T1` and `T1/TT`).
- Values are plain text.
- **Legacy format:** older files put a `"comment"` on each element. The importer still reads these, keeping the *first* comment it finds for each address and ignoring later different ones. If a `comments` map is present, it overrides them. New files should use the map only.

### 4.9 Reading and writing a rung — a worked translation

Boolean expression: `C1 := (X1 OR C1) AND X2`

Ladder:

```
    X1        X2
|---+----| |---+----| |------------------------(C1)---|
    |    C1    |
    +----| |---+
```

JSON:

```json
{"id": 1, "rn": 0,
 "comment": "RUN SEAL-IN — (Start X1 OR seal-in C1) AND Stop X2 (N.C.)",
 "elements": [
   {"id":101,"kind":"parallel","type":"XIC","addr":"","branches":[
     [ {"id":102,"kind":"contact","type":"XIC","addr":"X1"} ],
     [ {"id":103,"kind":"contact","type":"XIC","addr":"C1"} ]
   ]},
   {"id":104,"kind":"contact","type":"XIC","addr":"X2"},
   {"id":105,"kind":"output","type":"OTE","addr":"C1"}
 ]}
```

---

## 5. How to design ladder logic from a description or from ST

Follow these steps in order. They work whether you start from a written sequence of operation or from an existing Structured Text program.

**Step 1 — List the I/O.** Write down every input and output, its address, whether it is N.O. or N.C., and the scaling of any analog signal.

**Step 2 — Decide the states.** Break the sequence into steps (Idle, Forward, Reverse; or Fill, Mix, Drain). Give each active step its own internal relay (C bit). "Idle" is usually *all state bits off* and does not need its own bit.

**Step 3 — Replace integer state variables with bits.** Ladder has no `CASE` statement. An ST variable such as `State : INT` with values 0, 1, 2 becomes C bits, one per non-idle state. This keeps the ladder to plain contacts and coils.

**Step 4 — Make one-shots for pushbuttons.** A start command that should act once per press needs a one-shot. In ladder, use two rungs, in this order:

```
Rung A:  XIC X1  XIO C4  → OTE C3     (C3 = pulse, true for one scan)
Rung B:  XIC X1          → OTE C4     (C4 = X1 from last scan)
```

Rung A must come **before** Rung B, or the pulse never fires.

**Step 5 — Convert analog conditions to compares.** Each threshold becomes a compare element with its value converted to 0–4095. On/off control with a dead band becomes an OTL rung at one threshold and an OTU rung at the other.

**Step 6 — Write each state rung.** For a state held with seal-in (OTE style):

```
[Stop OK] AND ([enter condition] OR [this state bit AND NOT exit condition]) → OTE state bit
```

Or use an OTL rung to enter and a separate OTU rung to leave. Both styles are correct; seal-in is easier to read, and latch/unlatch is easier when a state has many entry or exit paths. With latches, remember to unlatch every state bit when Stop is pressed.

**Step 7 — Order the rungs for same-scan transitions.** Because memory updates immediately, place the rung that sets the *next* state before the rung that clears the *current* state. The next-state rung still sees the current bit on, and the current-state rung can then drop out using the next-state bit as an interlock.

**Step 8 — Add interlocks.** Two states that must never be on together should each include an XIO contact of the other. This replaces ST's `ELSE State := 0` safety net.

**Step 9 — Drive the outputs last.** Write one rung per physical output, driven from the state bits (for example `C1 OR C2 → Y1`). For analog outputs, write one MOV rung per value the output can take. Keeping outputs separate from state logic makes troubleshooting much easier. Where a single transition really does set and clear several bits at once, putting those coils on one rung (section 4.2) keeps the step logic in one place instead of repeating the same condition on three rungs.

**Step 10 — Document.** Give every rung a comment, add every used address to the `comments` map, and describe the program in `notes`.

### 5.1 ST-to-ladder translation table

| Structured Text | Ladder equivalent |
|---|---|
| `Y1 := X1;` | `XIC X1 → OTE Y1` |
| `Y1 := NOT X1;` | `XIO X1 → OTE Y1` |
| `Y1 := X1 AND X2;` | `XIC X1, XIC X2 → OTE Y1` (series) |
| `Y1 := X1 OR X2;` | parallel `[X1]`,`[X2]` → `OTE Y1` |
| `IF X1 THEN C1 := TRUE; END_IF;` | `XIC X1 → OTL C1` |
| `IF X2 THEN C1 := FALSE; END_IF;` | `XIC X2 → OTU C1` |
| `Y3 := AX1 > 3890;` | `GRT AX1 cmp 3890 → OTE Y3` |
| `IF AX1 < 819 THEN C101 := TRUE; END_IF;` | `LES AX1 cmp 819 → OTL C101` |
| `IF C101 THEN AY1 := 4095; ELSE AY1 := 0; END_IF;` | `XIC C101 → MOV AY1 src "4095"` and `XIO C101 → MOV AY1 src "0"` |
| `AY1 := AX1;` | always-true rung → `MOV AY1 src "AX1"` |
| `State : INT` with `CASE` | one C bit per non-idle state (Steps 2–3) |
| `R_TRIG` / `NOT Last` pattern | two-rung one-shot (Step 4) |
| `tmr(IN := X1, PT := T#5s); Y1 := tmr.Q;` | `XIC X1 → TON T1 pre 5000`, then `XIC T1 → OTE Y1` |
| `cnt(CU := X1, R := X3, PV := 5); Y4 := cnt.Q;` | `XIC X1 → CTU CT1 pre 5`, `XIC CT1 → OTE Y4`, `XIC X3 → RES CT1` |

Two ST features have no direct ladder form in this simulator: arithmetic (scaling, adding) and comparing two registers against each other. Keep those programs in ST, or restate the logic using fixed thresholds.

---

## 6. Complete ladder example — Conveyor Shuttle Control

**Sequence:** Press Start (X1) and the conveyor runs forward until the right sensor (X4), then reverses until the left sensor (X3), then stops. Stop (X2, N.C.) stops everything at any time.

**Bit map:** C1 = Forward, C2 = Reverse, C3 = Start pulse, C4 = Start last scan, C5 = Start request from Idle.

**Ladder:**

```
Rung 0 – Start one-shot
    X1        C4
|----| |-------|/|------------------------------------------(C3)---|

Rung 1 – Start memory
    X1
|----| |----------------------------------------------------(C4)---|

Rung 2 – Start request from Idle
    X2        C3        C1        C2
|----| |-------| |-------|/|-------|/|-----------------------(C5)---|

Rung 3 – Reverse state
    X2        C1        X4
|----| |---+----| |-------| |---+---------------------------(C2)---|
           |    C5        X4    |
           +----| |-------| |---+
           |    C2        X3    |
           +----| |-------|/|---+

Rung 4 – Forward state
    X2        C1        X4        C2
|----| |---+----| |---+----|/|-------|/|--------------------(C1)---|
           |    C5    |
           +----| |---+

Rung 5 – Conveyor run
    C1
|---+----| |---+--------------------------------------------(Y1)---|
    |    C2    |
    +----| |---+

Rung 6 – Reverse direction
    C2
|----| |----------------------------------------------------(Y2)---|
```

**JSON file** (https://accautomation.ca/simulator/acc-example-conveyor-shuttle.json):

```json
{
  "version": 2,
  "name": "Conveyor Shuttle Control",
  "exported": "2026-09-16T00:00:00.000Z",
  "notes": "Conveyor scene. Press Start to travel right to X4, reverse back to X3, then stop. Stop (N.C.) drops everything. Idle = C1 and C2 both OFF.",
  "program": [
    {"id": 1, "rn": 0,
     "comment": "START ONE-SHOT — C3 ON for one scan when X1 goes true (X1 AND NOT C4)",
     "elements": [
       {"id":101,"kind":"contact","type":"XIC","addr":"X1"},
       {"id":102,"kind":"contact","type":"XIO","addr":"C4"},
       {"id":103,"kind":"output","type":"OTE","addr":"C3"}
     ]},
    {"id": 2, "rn": 1,
     "comment": "START MEMORY — C4 follows X1; must be after Rung 0 so the one-shot works",
     "elements": [
       {"id":104,"kind":"contact","type":"XIC","addr":"X1"},
       {"id":105,"kind":"output","type":"OTE","addr":"C4"}
     ]},
    {"id": 3, "rn": 2,
     "comment": "START REQUEST — Stop OK, Start pulse, and Idle (not Forward, not Reverse)",
     "elements": [
       {"id":106,"kind":"contact","type":"XIC","addr":"X2"},
       {"id":107,"kind":"contact","type":"XIC","addr":"C3"},
       {"id":108,"kind":"contact","type":"XIO","addr":"C1"},
       {"id":109,"kind":"contact","type":"XIO","addr":"C2"},
       {"id":110,"kind":"output","type":"OTE","addr":"C5"}
     ]},
    {"id": 4, "rn": 3,
     "comment": "REVERSE STATE — enter at right limit X4 from Forward or Start request; hold until left limit X3; Stop drops it",
     "elements": [
       {"id":111,"kind":"contact","type":"XIC","addr":"X2"},
       {"id":118,"kind":"parallel","type":"XIC","addr":"","branches":[
         [
           {"id":112,"kind":"contact","type":"XIC","addr":"C1"},
           {"id":113,"kind":"contact","type":"XIC","addr":"X4"}
         ],
         [
           {"id":114,"kind":"contact","type":"XIC","addr":"C5"},
           {"id":115,"kind":"contact","type":"XIC","addr":"X4"}
         ],
         [
           {"id":116,"kind":"contact","type":"XIC","addr":"C2"},
           {"id":117,"kind":"contact","type":"XIO","addr":"X3"}
         ]
       ]},
       {"id":119,"kind":"output","type":"OTE","addr":"C2"}
     ]},
    {"id": 5, "rn": 4,
     "comment": "FORWARD STATE — Start request or seal-in; drops at right limit X4, when Reverse is on, or on Stop",
     "elements": [
       {"id":120,"kind":"contact","type":"XIC","addr":"X2"},
       {"id":123,"kind":"parallel","type":"XIC","addr":"","branches":[
         [
           {"id":121,"kind":"contact","type":"XIC","addr":"C1"}
         ],
         [
           {"id":122,"kind":"contact","type":"XIC","addr":"C5"}
         ]
       ]},
       {"id":124,"kind":"contact","type":"XIO","addr":"X4"},
       {"id":125,"kind":"contact","type":"XIO","addr":"C2"},
       {"id":126,"kind":"output","type":"OTE","addr":"C1"}
     ]},
    {"id": 6, "rn": 5,
     "comment": "Y1 CONVEYOR RUN — Forward OR Reverse",
     "elements": [
       {"id":129,"kind":"parallel","type":"XIC","addr":"","branches":[
         [
           {"id":127,"kind":"contact","type":"XIC","addr":"C1"}
         ],
         [
           {"id":128,"kind":"contact","type":"XIC","addr":"C2"}
         ]
       ]},
       {"id":130,"kind":"output","type":"OTE","addr":"Y1"}
     ]},
    {"id": 7, "rn": 6,
     "comment": "Y2 REVERSE — ON only in Reverse state",
     "elements": [
       {"id":131,"kind":"contact","type":"XIC","addr":"C2"},
       {"id":132,"kind":"output","type":"OTE","addr":"Y2"}
     ]}
  ],
  "comments": {
    "X1": "Start PB",
    "X2": "Stop PB N.C.",
    "X3": "Left Sensor",
    "X4": "Right Sensor",
    "Y1": "Conveyor Run",
    "Y2": "Reverse",
    "C1": "Forward State",
    "C2": "Reverse State",
    "C3": "Start Pulse",
    "C4": "Start Last Scan",
    "C5": "Start Request"
  }
}
```

---

## 7. Complete ladder example — Analog Tank Level Control

This example uses every instruction in Sections 4.6 and 4.7. It is not tied to a scene: test it by dragging the AX1 slider on the AN tab and toggling X2 and X3.

**Sequence:** AX1 is the tank level (0–4095 = 0–100 %). When the level drops below 20 %, the inlet valve opens fully (Y1 on, AY1 = 4095). It closes when the level rises above 85 % or when Stop (X2, N.C.) is pressed. A high-level alarm (Y3) turns on at 95 %. Counter CT1 counts fill cycles; after 5 fills, Service Due (Y4) turns on until the Service Reset button (X3) is pressed.

**Ladder:**

```
Rung 0 – Fill request on
   [LES AX1 < 819]     X2
|-----[ LES ]---------| |---------------------------------(L C101)---|

Rung 1 – Fill request off
   [GRT AX1 > 3481]
|---+---[ GRT ]---+---------------------------------------(U C101)---|
    |      X2     |
    +-----|/|-----+

Rung 2 – Inlet valve
    C101
|----| |----------------------------------------------------(Y1)-----|

Rung 3 – Valve fully open
    C101
|----| |-----------------------------------------[MOV 4095 → AY1]----|

Rung 4 – Valve closed
    C101
|----|/|-----------------------------------------[MOV 0 → AY1]-------|

Rung 5 – High level alarm
   [GEQ AX1 ≥ 3890]
|-----[ GEQ ]-----------------------------------------------(Y3)-----|

Rung 6 – Fill cycle counter
    C101
|----| |-----------------------------------------[CTU CT1 pre 5]-----|

Rung 7 – Service due
    CT1
|----| |----------------------------------------------------(Y4)-----|

Rung 8 – Counter reset
    X3
|----| |-----------------------------------------[RES CT1]-----------|
```

Points to notice: Rung 1 shows a compare inside a parallel branch. Rungs 3 and 4 are both needed because MOV keeps its last value. The counter only counts when C101 turns on, not while it stays on. After a reset, Y4 turns off one scan later, because Rung 7 runs before Rung 8.

**JSON file** (https://accautomation.ca/simulator/acc-example-analog-tank-level.json):

```json
{
  "version": 2,
  "name": "Analog Tank Level Control",
  "exported": "2026-09-16T00:00:00.000Z",
  "notes": "Generic example (not tied to one scene). AX1 = tank level 0-4095 (0-100%). Fill starts below 20% (819) and stops above 85% (3481). AY1 = inlet valve position. Y3 = high-level alarm above 95% (3890). CT1 counts fill cycles; Y4 = service due after 5 fills; X3 resets the count.",
  "program": [
    {"id": 1, "rn": 0,
     "comment": "FILL REQUEST ON — level below 20% (819) and Stop OK",
     "elements": [
       {"id":101,"kind":"compare","type":"LES","addr":"AX1","cmp":819},
       {"id":102,"kind":"contact","type":"XIC","addr":"X2"},
       {"id":103,"kind":"output","type":"OTL","addr":"C101"}
     ]},
    {"id": 2, "rn": 1,
     "comment": "FILL REQUEST OFF — level above 85% (3481) OR Stop pressed (compare inside a branch)",
     "elements": [
       {"id":106,"kind":"parallel","type":"XIC","addr":"","branches":[
         [
           {"id":104,"kind":"compare","type":"GRT","addr":"AX1","cmp":3481}
         ],
         [
           {"id":105,"kind":"contact","type":"XIO","addr":"X2"}
         ]
       ]},
       {"id":107,"kind":"output","type":"OTU","addr":"C101"}
     ]},
    {"id": 3, "rn": 2,
     "comment": "Y1 INLET VALVE — open while filling",
     "elements": [
       {"id":108,"kind":"contact","type":"XIC","addr":"C101"},
       {"id":109,"kind":"output","type":"OTE","addr":"Y1"}
     ]},
    {"id": 4, "rn": 3,
     "comment": "AY1 VALVE FULL OPEN — MOV constant 4095 while filling",
     "elements": [
       {"id":110,"kind":"contact","type":"XIC","addr":"C101"},
       {"id":111,"kind":"block","type":"MOV","addr":"AY1","src":"4095","pre":0}
     ]},
    {"id": 5, "rn": 4,
     "comment": "AY1 VALVE CLOSED — MOV constant 0 when not filling (MOV holds its last value, so both states need a rung)",
     "elements": [
       {"id":112,"kind":"contact","type":"XIO","addr":"C101"},
       {"id":113,"kind":"block","type":"MOV","addr":"AY1","src":"0","pre":0}
     ]},
    {"id": 6, "rn": 5,
     "comment": "Y3 HIGH LEVEL ALARM — level at or above 95% (3890)",
     "elements": [
       {"id":114,"kind":"compare","type":"GEQ","addr":"AX1","cmp":3890},
       {"id":115,"kind":"output","type":"OTE","addr":"Y3"}
     ]},
    {"id": 7, "rn": 6,
     "comment": "FILL CYCLE COUNTER — counts each OFF-to-ON of the fill request",
     "elements": [
       {"id":116,"kind":"contact","type":"XIC","addr":"C101"},
       {"id":117,"kind":"block","type":"CTU","addr":"CT1","pre":5}
     ]},
    {"id": 8, "rn": 7,
     "comment": "Y4 SERVICE DUE — counter done (5 fills)",
     "elements": [
       {"id":118,"kind":"contact","type":"XIC","addr":"CT1"},
       {"id":119,"kind":"output","type":"OTE","addr":"Y4"}
     ]},
    {"id": 9, "rn": 8,
     "comment": "COUNTER RESET — Service Reset PB clears CT1",
     "elements": [
       {"id":120,"kind":"contact","type":"XIC","addr":"X3"},
       {"id":121,"kind":"block","type":"RES","addr":"CT1","pre":0}
     ]}
  ],
  "comments": {
    "X2": "Stop PB N.C.",
    "X3": "Service Reset PB",
    "AX1": "Tank Level 0-4095",
    "AY1": "Inlet Valve Position",
    "Y1": "Inlet Valve Open",
    "Y3": "High Level Alarm",
    "Y4": "Service Due",
    "C101": "Fill Request",
    "CT1": "Fill Cycle Count"
  }
}
```

Both example files were run through the simulator's own import and scan code: the conveyor completes forward → reverse → stop and refuses to start while Stop is pressed, and the tank example fills, closes, alarms, counts to 5 and resets as described. The Structured Text versions in Section 8 were compiled and run on the real `acc-st.js` engine and produce the same output sequences.

---

## 8. Structured Text (.st) files

### 8.1 Format

A `.st` file is plain UTF-8 text containing the program exactly as it would appear in the simulator's ST editor. There is no JSON wrapper, no header, and no special encoding.

### 8.2 Rules

- **Do not declare I/O addresses.** X1–X16, Y1–Y16, C1–C256, AX1–AX8, AY1–AY4, T1–T256 and CT1–CT256 are built in. Use them directly. Declaring one anyway (`VAR X1 : BOOL; END_VAR`) raises no error: the declaration is silently ignored, the real input still wins, and a phantom entry appears in the variables watch — which makes the program hard to debug.
- **Declare your own variables** in one or more `VAR … END_VAR` blocks at the top.
- **Assignment is `:=`.** A single `=` is a comparison. The editor flags `=` used as assignment.
- **Every statement ends with `;`**, including `END_IF;`, `END_CASE;`, `END_FOR;` and `END_WHILE;`.
- **The whole program runs every scan.** Never use a `WHILE` loop to wait for something. Use a timer function block. All loops share a per-scan iteration budget; exceeding it stops the program with "Execution budget exceeded — possible infinite loop (check WHILE/REPEAT/FOR)".
- **STOP resets ST state.** Unlike ladder timers and counters, ST variables and function-block instances are reset when the PLC goes to STOP, and variables declared with `:= value` start from that value again. One consequence to watch: if an input is still on when RUN resumes, an edge block (`R_TRIG`, or a `CTU` counting an input) sees a fresh rising edge on the first scan and fires once.
- Comments use `(* … *)`, `/* … */` or `//`.
- Names are not case-sensitive.
- `T1` and `CT1` are read-only done bits owned by the ladder timer table. Assigning to one is refused at compile time: "Cannot assign to timer/counter bit T1 directly — use a TON/CTU function block". Write the bare name only — `T1/DN` is a syntax error in ST. ST timing and counting use IEC function blocks instead.
- ST can write to `X` addresses, exactly like a ladder coil, and with the same caveat: a connected scene overwrites every X each scan.
- Operator precedence, from loosest to tightest: `OR`, `XOR`, `AND`, `=` `<>`, `<` `>` `<=` `>=`, `+` `-`, `*` `/` `MOD`, `**`. Comparisons bind tighter than `AND`/`OR`, so `AX1 < 819 AND X2` means `(AX1 < 819) AND X2`. Use brackets whenever in doubt.
- Function blocks accept named parameters (recommended) or positional ones.

### 8.3 Supported language

| Area | Supported |
|---|---|
| Declarations | `name : TYPE;` or `name : TYPE := initial;`, several names per line allowed (`a, b : INT;`). Optional `PROGRAM … END_PROGRAM` wrapper. |
| Data types | `BOOL`; `INT`, `DINT`, `SINT`, `UINT`, `LINT`, `WORD`, `DWORD`, `BYTE` (all integers; decimals are dropped on assignment); `REAL`, `LREAL`; `TIME` (`T#500ms`, `T#2s`, `T#1m30s`); `ARRAY[lo..hi] OF type` |
| Boolean | `AND` (or `&`), `OR`, `XOR`, `NOT` |
| Compare | `=`, `<>`, `<`, `>`, `<=`, `>=` |
| Arithmetic | `+`, `-`, `*`, `/`, `MOD`, `**` (divide by zero returns 0) |
| Control | `IF / ELSIF / ELSE`, `CASE … OF` (single values, lists `1, 2:`, ranges `3..5:`, `ELSE`), `FOR … TO … BY`, `WHILE`, `REPEAT … UNTIL … END_REPEAT`, `EXIT`, `CONTINUE`, `RETURN` |
| Function blocks | `TON`, `TOF`, `TP` (IN, PT → Q, ET); `CTU` (CU, R, PV → Q, CV); `CTD` (CD, LD, PV → Q, CV); `R_TRIG`, `F_TRIG` (CLK → Q) |
| Functions | `ABS`, `SQRT`, `MIN`, `MAX`, `LIMIT(min, in, max)`, `TRUNC`, `ROUND`, `EXPT(base, exp)`, `SEL(g, in0, in1)`, `SHL`, `SHR`, `REAL_TO_INT`, `INT_TO_REAL`, `BOOL_TO_INT`, `INT_TO_BOOL`, plus user `FUNCTION` and `FUNCTION_BLOCK` (see the limitation in 8.4) |
| Time literals | `T#500ms`, `T#2s`, `T#1m30s`, `T#1h`, `T#1d`, and combinations |

### 8.4 Function block usage

Declare the instance in `VAR`, call it **once every scan** with named parameters, then read its outputs:

```
VAR
    onDelay : TON;
END_VAR

onDelay(IN := X1, PT := T#2s);
Y1  := onDelay.Q;      (* TRUE 2 s after X1 turns on *)
AY1 := onDelay.ET;     (* elapsed time in ms *)
```

Calling a timer inside an `IF` that is sometimes skipped is a common mistake. The timer only updates on scans where it is called.

**Function blocks may be declared inside your own `FUNCTION_BLOCK` (fixed in `acc-st.js` v1.015).** A `TON`, `CTU`, `R_TRIG` or another user block declared in a user-defined `FUNCTION_BLOCK` now works, and so does nesting user blocks several levels deep. Each instance owns its own state, so two instances of the same block type keep separate timers and counts.

```
(* Works on v1.015 and later *)
FUNCTION_BLOCK Deb
  VAR_INPUT inp : BOOL; END_VAR
  VAR_OUTPUT outp : BOOL; END_VAR
  VAR t : TON; END_VAR
  t(IN := inp, PT := T#50ms);
  outp := t.Q;
END_FUNCTION_BLOCK

VAR
    dStart : Deb;   (* each instance keeps its own timer *)
    dStop  : Deb;
END_VAR

dStart(inp := X1);
dStop(inp := X2);
Y1 := dStart.outp AND NOT dStop.outp;
```

Declaring timers at the top level of the program also works and is often clearer for a short program — use whichever suits.

> **On `acc-st.js` v1.014 and earlier this failed.** A block instance declared inside a user `FUNCTION_BLOCK` compiled without complaint and then threw `"<name>" is not a function-block instance` on **every** scan, which stops the PLC rather than just the rung. If a program has to run on an older copy of the engine, keep every timer and counter at the top level. Check the version in the `acc-st.js` header or via `AccST.version`.

A user `FUNCTION_BLOCK` holding only plain variables and logic works normally, and so does a user `FUNCTION`.

### 8.5 Template

```
(* PROGRAM TITLE - Structured Text *)
(* ACC PLC Simulator - <Scene name> Scene *)
(* I/O: X1 = ..., X2 = ... (N.C.), Y1 = ... *)

VAR
    State     : INT := 0;   (* describe each value *)
    StartEdge : R_TRIG;     (* one-shot on Start *)
END_VAR

(* 1. Edge detection and inputs *)
StartEdge(CLK := X1);

(* 2. Stops and faults first - they have priority *)
IF NOT X2 THEN
    State := 0;
END_IF;

(* 3. State transitions *)
CASE State OF
    0: IF StartEdge.Q AND X2 THEN State := 1; END_IF;
    1: (* ... *)
ELSE
    State := 0;
END_CASE;

(* 4. Outputs driven from state - always assign every output *)
Y1 := (State = 1);
```

### 8.6 Complete ST example — Conveyor Shuttle Control

Download: https://accautomation.ca/simulator/acc-example-conveyor-shuttle.st

```
(* CONVEYOR SHUTTLE CONTROL - Structured Text *)
(* ACC PLC Simulator - Conveyor Scene *)
(* X1 Start PB (N.O.)  X2 Stop PB (N.C.)  X3 Left sensor  X4 Right sensor *)
(* Y1 Conveyor run     Y2 Reverse                                         *)

VAR
    State      : INT  := 0;      (* 0 = Idle, 1 = Forward, 2 = Reverse *)
    StartLast  : BOOL := FALSE;  (* Start button state, previous scan  *)
    StartPulse : BOOL := FALSE;  (* One-shot on the Start press        *)
END_VAR

(* One-shot the Start button *)
StartPulse := X1 AND NOT StartLast;
StartLast  := X1;

(* Stop has priority over everything *)
IF NOT X2 THEN
    State := 0;
ELSIF StartPulse AND State = 0 THEN
    State := 1;
END_IF;

(* State transitions *)
CASE State OF
    1:  (* Forward - travelling right *)
        IF X4 THEN
            State := 2;
        END_IF;
    2:  (* Reverse - travelling left *)
        IF X3 THEN
            State := 0;
        END_IF;
    ELSE
        State := 0;
END_CASE;

(* Outputs driven from state *)
Y1 := (State = 1) OR (State = 2);
Y2 := (State = 2);
```

### 8.7 Complete ST example — Analog Tank Level Control

The same program as Section 7. Download: https://accautomation.ca/simulator/acc-example-analog-tank-level.st

```
(* ANALOG TANK LEVEL CONTROL - Structured Text *)
(* AX1 Tank level 0-4095  X2 Stop PB (N.C.)  X3 Service reset *)
(* Y1 Inlet valve  Y3 High alarm  Y4 Service due  AY1 Valve position *)

VAR
    FillCount : CTU;
END_VAR

(* Fill request with 20 % / 85 % dead band; Stop closes the valve *)
IF AX1 < 819 AND X2 THEN
    C101 := TRUE;
END_IF;
IF AX1 > 3481 OR NOT X2 THEN
    C101 := FALSE;
END_IF;

(* Outputs *)
Y1 := C101;
IF C101 THEN
    AY1 := 4095;
ELSE
    AY1 := 0;
END_IF;
Y3 := AX1 >= 3890;

(* Count fill cycles; service due after 5 *)
FillCount(CU := C101, R := X3, PV := 5);
Y4 := FillCount.Q;
```

---

## 9. What the simulator does when a JSON file is imported

This is useful when a file loads but does not behave as expected.

1. The file is parsed. The importer uses the `program` array, or the whole file if it is a bare array. Anything else fails with "Invalid format".
2. The user confirms the import; the current program is replaced.
3. Address comments are cleared, then rebuilt: legacy element `comment` values first (first one per address wins), then the `comments` map on top.
4. Each element is rebuilt from `id`, `kind`, `type` (default `XIC`), `addr` (default empty), plus `pre`/`src` for blocks, `cmp` for compares and `branches` for parallel groups.
5. Rungs are renumbered 0, 1, 2 … in array order.
6. All memory (X, Y, C, T, CT, AX, AY) is cleared, and the undo history is reset.

If a rung has no `elements` array, the import fails without a message on screen (the error appears only in the browser's developer console), and the existing address comments are lost.

Because missing fields get quiet defaults (an element with no `type` becomes `XIC`; a compare with no `cmp` compares against 0; a timer with no `pre` is done immediately; a MOV with no `src` moves 0), a file can import without an error message and still be wrong. The validator in Section 10.2 catches these cases.

---

## 10. Checking the result before importing

### 10.1 Checklist

**Ladder JSON**

- [ ] The file is valid JSON (no trailing commas, no comments inside the JSON).
- [ ] Top level has `"version": 2`, `program`, and a `comments` map.
- [ ] Every rung has at least one input and at least one output, and its last element is an output or block.
- [ ] No empty rungs.
- [ ] Every rung and element has an integer `id`; rung ids are unique, and element ids are unique across the file.
- [ ] Coils only write Y or C; timers use T, counters use CT; MOV writes AY.
- [ ] Timer `pre` values are in milliseconds.
- [ ] Compares use AX or AY with a numeric `cmp` between 0 and 4095.
- [ ] MOV has a string `src` (constant or AX/AY) and `"pre": 0`; RES has `"pre": 0`.
- [ ] No contacts on AX or AY addresses.
- [ ] Every MOV output that must change back has a second MOV rung.
- [ ] Every CTD has a matching CTU on the same address.
- [ ] Every parallel group has at least two non-empty branches of contacts and compares only.
- [ ] N.C. field devices (Stop, E-stop) use XIC contacts.
- [ ] Rungs that set a bit are above the rungs that must see it in the same scan.
- [ ] Every address matches the scene's I/O list and is in the `comments` map.

**Structured Text**

- [ ] Saved as plain text with a `.st` extension.
- [ ] No I/O addresses declared in `VAR`.
- [ ] Every assignment uses `:=`, every statement ends with `;`.
- [ ] No waiting loops; timing uses TON/TOF/TP.
- [ ] Every function block instance is called unconditionally each scan.
- [ ] Every output is assigned on every scan (avoid outputs that are only set inside an `IF`, unless latching is intended).

### 10.2 Automatic validator (ladder JSON)

Download https://accautomation.ca/simulator/acc-validate-program.py (or copy the code below) and run `python3 acc-validate-program.py my-program.json` (Python 3, no extra packages). It prints `OK` or a list of problems, with the rung and element where each was found. Warnings point out things that load but are probably not what was intended.

```python
#!/usr/bin/env python3
"""Validate an ACC PLC Simulator ladder program (.json) before importing it.
Checks the rules used by the simulator's own import and scan code
(acc-plc-simulator.html v1.122), verified by running that engine.
Usage:  python3 acc-validate-program.py my-program.json
"""
import json, re, sys

N256 = r'([1-9]\d?|1\d\d|2[0-4]\d|25[0-6])'
N16  = r'([1-9]|1[0-6])'
BIT_ADDR = re.compile(rf'^(X{N16}|Y{N16}|C{N256}|T{N256}(/(DN|EN|TT))?|CT{N256}(/DN)?)$')
OUT_ADDR = re.compile(rf'^(Y{N16}|C{N256})$')
TIMER    = re.compile(rf'^T{N256}$')
COUNTER  = re.compile(rf'^CT{N256}$')
INT_REG  = re.compile(r'^(AX[1-8]|AY[1-4])$')
AY_REG   = re.compile(r'^AY[1-4]$')
ANY_ADDR = re.compile(rf'^(X{N16}|Y{N16}|C{N256}|T{N256}(/(DN|EN|TT))?|CT{N256}(/DN)?|AX[1-8]|AY[1-4])$')
_C_NUM = re.compile(r'^C(\d+)$')


def out_of_range_c(a):
    """C0 and C257+ read and write, but never show on the I/O panel."""
    m = _C_NUM.match(a.upper())
    return bool(m) and not (1 <= int(m.group(1)) <= 256)

errors, warnings, ids, rung_ids = [], [], [], []
ctu, ctd = set(), set()

def err(w, m):  errors.append(f"{w}: {m}")
def warn(w, m): warnings.append(f"{w}: {m}")
def is_num(v):  return isinstance(v, (int, float)) and not isinstance(v, bool)

def common(e, w):
    if not isinstance(e.get("id"), int):
        err(w, "every element needs an integer 'id'")
    else:
        ids.append(e["id"])
    if e.get("comment"):
        warn(w, "element 'comment' is the legacy format; put address comments in the top-level 'comments' map")

def check_input(e, w):
    """Contacts and compares may sit before the output or inside branches."""
    common(e, w)
    k, t, a = e.get("kind"), e.get("type"), str(e.get("addr", ""))
    if k == "contact":
        if t not in ("XIC", "XIO"):
            err(w, f"contact type must be XIC or XIO, not {t!r}")
        if a.upper().startswith(("AX", "AY")):
            err(w, f"contacts cannot read analog registers ({a}); use a compare element")
        elif out_of_range_c(a):
            warn(w, f"{a} is outside the documented C1-C256 range: it reads and writes, but never "
                    f"appears on the I/O panel, so the bit is invisible while debugging")
        elif not BIT_ADDR.match(a):
            err(w, f"invalid contact address {a!r}")
    elif k == "compare":
        if t not in ("GRT", "LES", "GEQ", "LEQ", "EQU", "NEQ"):
            err(w, f"compare type must be GRT/LES/GEQ/LEQ/EQU/NEQ, not {t!r}")
        if not INT_REG.match(a):
            err(w, f"compare address must be AX1-AX8 or AY1-AY4 (got {a!r})")
        if isinstance(e.get("cmp"), str):
            err(w, "'cmp' must be a number, not a string (EQU/NEQ never match a string comparand)")
        elif not is_num(e.get("cmp")):
            err(w, "compare needs a numeric 'cmp' (the comparand)")
        elif not 0 <= e["cmp"] <= 4095:
            warn(w, f"comparand {e['cmp']} is outside 0-4095, so the compare is always true or always false")
    elif k == "parallel":
        err(w, "parallel group nested inside a branch: it evaluates correctly but the rung is sized from "
               "top-level groups only, so the nested rows draw over the rungs below. Flatten it into more branches")
    else:
        err(w, f"only contacts and compares may be used as inputs (found kind={k!r})")

def check_output(e, w):
    common(e, w)
    k, t, a = e.get("kind"), e.get("type"), str(e.get("addr", ""))
    if k == "output":
        if t not in ("OTE", "OTL", "OTU"):
            err(w, f"output type must be OTE/OTL/OTU, not {t!r}")
        if a.upper().startswith("X"):
            err(w, f"coil writes input {a} — a connected scene overwrites every X each scan, so the rung does nothing; write a Y or C instead")
        elif a.upper().startswith(("AX", "AY")):
            err(w, f"coils cannot write analog register {a}; use a MOV block")
        elif out_of_range_c(a):
            warn(w, f"{a} is outside the documented C1-C256 range: it reads and writes, but never "
                    f"appears on the I/O panel, so the bit is invisible while debugging")
        elif not OUT_ADDR.match(a):
            err(w, f"coils may only write Y1-Y16 or C1-C256 (got {a!r})")
    elif k == "block":
        if t in ("TON", "TOF"):
            if not TIMER.match(a): err(w, f"timer address must be T1-T256 (got {a!r})")
            if not isinstance(e.get("pre"), int) or e["pre"] <= 0:
                err(w, "timer needs a positive integer 'pre' in milliseconds")
        elif t in ("CTU", "CTD"):
            if not COUNTER.match(a): err(w, f"counter address must be CT1-CT256 (got {a!r})")
            if not isinstance(e.get("pre"), int) or e["pre"] < 0:
                err(w, "counter needs a non-negative integer 'pre' (the target count)")
            (ctu if t == "CTU" else ctd).add(a)
        elif t == "RES":
            if not (TIMER.match(a) or COUNTER.match(a)):
                err(w, f"RES address must be a timer (T1) or counter (CT1), got {a!r}")
        elif t == "MOV":
            if not AY_REG.match(a):
                err(w, f"MOV destination must be AY1-AY4 (got {a!r})")
            src = str(e.get("src", ""))
            if "src" not in e:
                err(w, "MOV needs a 'src' string: a constant such as \"2048\" or a register such as \"AX1\"")
            elif not INT_REG.match(src.upper()):
                try:
                    v = float(src)
                    if not 0 <= v <= 4095:
                        warn(w, f"MOV source {src} is clamped to 0-4095")
                except ValueError:
                    err(w, f"MOV src {src!r} is not a number, AX1-AX8 or AY1-AY4 (it would move 0)")
        else:
            err(w, f"block type must be TON/TOF/CTU/CTD/RES/MOV, not {t!r}")
    else:
        err(w, f"the last element must be an output coil or a block (found kind={k!r})")

def check_rung(r, n, w):
    if not isinstance(r.get("id"), int):
        err(w, "rung needs an integer 'id'")
    else:
        rung_ids.append(r["id"])
    if r.get("rn") != n:
        warn(w, f"'rn' is {r.get('rn')!r}; the simulator renumbers on import, but {n} keeps the file readable")
    els = r.get("elements")
    if not isinstance(els, list):
        err(w, "missing 'elements' array (the import would fail silently)"); return
    if not els:
        warn(w, "empty rung: it scans harmlessly, but its comment is NOT displayed; remove it or add logic")
        return
    kinds = [e.get("kind") for e in els]
    if not any(k in ("output", "block") for k in kinds):
        err(w, "rung has no output: it needs at least one coil or block")
    if kinds[-1] not in ("output", "block"):
        err(w, "rung ends with a contact or compare, which drives nothing; "
               "the last element must be a coil or block")
    for j, e in enumerate(els[:-1]):
        ew = f"{w} element[{j}]"
        if e.get("kind") in ("output", "block"):
            # Legal and used by the shipped example programs: coils in series all fire,
            # and any contact after one gates only the outputs that follow it.
            check_output(e, ew)
            continue
        if e.get("kind") == "parallel":
            common(e, ew)
            br = e.get("branches", [])
            if len(br) == 1:
                warn(ew, "single-branch parallel group: it loads and behaves as that branch, but the editor "
                         "expects 2+. Use a plain series contact instead")
            elif not br:
                err(ew, "parallel group has no branches")
            for b, branch in enumerate(br):
                if not branch: err(ew, f"branch {b} is empty")
                for c, be in enumerate(branch):
                    check_input(be, f"{ew} branch{b}[{c}]")
        else:
            check_input(e, ew)
    check_output(els[-1], f"{w} element[{len(els)-1}]")

def main(path):
    with open(path, encoding="utf-8") as f:
        d = json.load(f)
    if isinstance(d, list):
        warn("file", "bare rung array accepted by the importer; the wrapped v2 object is preferred")
        prog, d = d, {"program": d}
    else:
        prog = d.get("program")
        if not isinstance(prog, list):
            err("file", "missing top-level 'program' array"); prog = []
        if d.get("version") != 2:
            warn("file", "use \"version\": 2 (current export format)")
    for n, r in enumerate(prog):
        check_rung(r, n, f"rung {n:04d}")
    cm = d.get("comments")
    if cm is None:
        warn("file", "no top-level 'comments' map; addresses will show no comments")
    elif not isinstance(cm, dict):
        err("file", "'comments' must be an object of address -> text")
    else:
        for k, v in cm.items():
            if k != k.upper():
                err("comments", f"key {k!r} must be upper case")
            elif not ANY_ADDR.match(k) and not out_of_range_c(k):
                err("comments", f"key {k!r} must be an address such as X1, C101, T1, T1/DN, AX1")
            if not isinstance(v, str):
                err("comments", f"comment for {k} must be a string")
    for a in sorted(ctd - ctu):
        warn("file", f"{a} has CTD but no CTU; CTD cannot count below 0, so it will never change")
    rdup = sorted({i for i in rung_ids if rung_ids.count(i) > 1})
    if rdup:
        err("file", f"duplicate rung ids: {rdup}")
    dup = sorted({i for i in ids if ids.count(i) > 1})
    if dup:
        err("file", f"duplicate element ids: {dup}")
    for m in warnings: print("  warning -", m)
    if errors:
        print(f"FAILED - {len(errors)} problem(s):")
        for m in errors: print("  -", m)
        sys.exit(1)
    print(f"OK - {len(prog)} rungs, {len(ids)} elements, ids unique"
          + (f", {len(warnings)} warning(s)" if warnings else ""))

if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("Usage: python3 acc-validate-program.py <program.json>")
    main(sys.argv[1])
```

### 10.3 Testing in the simulator

1. Import the file (ladder: **📂 LOAD → ⬆ Import .json**; ST: **ST** mode → **Import .st**).
2. For ST, fix any red-highlighted lines. RUN is refused until the program compiles.
3. In STOP mode with no scene connected, toggle inputs on the **X** tab, drag analog inputs on the **AN** tab, and press **⏭ STEP** to execute one scan at a time.
4. Press **F5** to run. Watch power flow (ladder) or the **Live Values** column (ST). Check timers and counters on the **T** and **CT** tabs.
5. Click **CONNECT**, launch the matching scene, and run the full sequence. Test Stop and E-stop at every step.

---

## 11. Prompt template to give an AI

Copy this, fill in the brackets, and send it together with this guide:

```
Read https://accautomation.ca/simulator/acc-ai-guide.md (or the attached copy) and follow it exactly.
Using that guide,
create a [ladder logic JSON file / Structured Text .st file] for the ACC PLC Simulator.

Scene: [Conveyor / Tank Fill Station / Traffic Light / other]
Scene address offset: [none / X+n Y+n — only if several scenes are connected at once]

I/O:
- X1 = [description] (N.O. or N.C.)
- X2 = [description] (N.O. or N.C.)
- Y1 = [description]
- AX1 = [description, what 0 and 4095 mean] (if analog)
- AY1 = [description, what 0 and 4095 mean] (if analog)

Sequence of operation:
1. [what happens first]
2. [what happens next]
3. [what stops the machine, and what happens on Stop / E-stop]

Requirements:
- Follow the file format, address ranges and scan rules in the guide exactly.
- Use the version 2 JSON format with a top-level "comments" map.
- Comment every rung and every address used.
- Explain the rung order and any interlocks after the file.
- Check the result against the Section 10 checklist before giving it to me.
```

---

## 12. Common mistakes and how to avoid them

| Mistake | Result | Fix |
|---|---|---|
| Stop button programmed with XIO | Machine never starts (the N.C. input is normally ON) | Use XIC for N.C. field devices |
| One-shot rungs in the wrong order | Start press does nothing | Pulse rung above the "last state" rung |
| Rung ends with a contact | The trailing contact drives nothing | End every rung with a coil or block |
| Rung has no coil or block at all | Nothing is written | Add an output |
| Duplicate element or rung `id` values | Editing one element or rung changes another | Keep ids unique |
| Output or block inside a parallel branch | Invalid structure | Branches hold contacts and compares only |
| Parallel group nested inside a branch | Evaluates right but draws over the rungs below it | Flatten into more branches (4.5) |
| Coil addressed to an X input | Silently overwritten if a connected scene owns that address; holds its value if none does, which is worse — it works until a scene is connected | Coils write Y or C only |
| `C0` or `C300` used as an internal relay | Works invisibly — never shows on the I/O panel | Keep to C1–C256 |
| Timer preset entered in seconds | Timer finishes 1000× too fast | `pre` is milliseconds |
| `XIC AX1` used as an analog check | Always false | Use a compare element |
| Section 3.4 addresses used while the scene is offset | Program drives the wrong block, or nothing | Add the scene's offset (3.5) |
| `cmp` missing | Compares against 0 | Always include `cmp` |
| `cmp` written as a string (`"819"`) | GRT/LES/GEQ/LEQ still work, but EQU and NEQ never match | Write `cmp` as a number |
| Timer `pre` missing or 0 | Timer is done immediately | Always include `pre` in milliseconds |
| `src` written as a number | Works, but differs from the export format | Write `src` as a string, e.g. `"2048"` |
| Only one MOV rung for an on/off analog output | Output stays at the last value | One MOV rung per value |
| CTD used without CTU | Count never changes | Pair with CTU on the same address |
| Empty "documentation" rung | Its comment is invisible | Use rung comments, `comments` or `notes` |
| Address comments only on elements | Only the first comment per address is kept | Use the top-level `comments` map |
| Relying on `notes` or `name` inside the simulator | Not shown, lost on re-export | Keep them as file documentation only |
| ST program declares `X1 : BOOL` | Conflicts with built-in I/O | Never declare addresses |
| ST uses `=` for assignment | Compile error | Use `:=` |
| ST uses `T1/DN` | Syntax error — `/` is read as division | Use bare `T1` in ST |
| ST declares a timer inside a user `FUNCTION_BLOCK` | Runtime error every scan; program does nothing | Declare timers at the top level (8.4) |
| ST waits with `WHILE NOT X3 DO` | Loop budget error | Use state logic and timers |

---

*ACC Automation · accautomation.ca · Free PLC training in your browser*
