From 7c32331cdda184e5ead5157164fed9bf1a983271 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 26 Aug 2026 09:10:11 -0400 Subject: [PATCH 1/4] docs: shared globals from Python, and function block instances in both languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things a user can now do in a native block that the docs did not cover, one of which they can get wrong silently. **Shared globals from Python.** A Python block can read and write a VAR_EXTERNAL, but because it runs in a separate process the read-modify-write is not atomic the way it is in ST or C++. The PLC hands the global's value in with the inputs and stores the block's copy back at the end of the cycle; each of those takes the global's lock, but the whole cycle sits between them, so anything another task writes in that window is overwritten. `g = g + 1` is therefore not a safe increment if anything else also writes g. That deserved a warning rather than a footnote, because nothing fails loudly — the value is never corrupt, just occasionally stale, and a counter simply runs slow. Measured on hardware and quoted: a Python block and a C++ block each adding 2 to one global left it holding consistently two thirds of their combined total. The guidance is one writer per global, read freely, and accumulate in ST. **Function block instances.** A Python block cannot declare one, and the page now says why: an instance only advances when something calls it, and a Python block has no way to call into the scan, so its pins would never update. Rather than hand over pins that silently stay put, the compiler refuses. The alternatives are documented — EN/ENO for controlling the Python block itself, and instantiating the FB in an ST block and passing its outputs in. A C++ block has no such limit, so the C++ page gains the worked example: declare the instance under VAR, set its pins, call it, read it back. Including the two details that are easy to get wrong — the instance keeps the name you declared while its pins are upper-cased, and a member named after its own type carries a trailing underscore. The C++ page also claimed inputs and outputs were the only classes available to a C++ block. All six are, so that is now a table, with a note that VAR_EXTERNAL in C++ *is* atomic within a scan — which is the contrast that makes the Python warning meaningful. Verified against hardware before writing: the C++ snippet is the code that ran, a standard-library TON reaching Q with ET at its preset and a user block's accumulator holding exactly twice the call count over 5116 calls. Depends on DOPE-584 (openplc-editor #1044 / openplc-web #701) — the variable classes and instance handling described here land with it. --- .../cpp-blocks/cpp-structure.md | 55 ++++++++++++- .../python-blocks/python-restrictions.md | 77 +++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md index 604449c..da89ed2 100644 --- a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md +++ b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md @@ -34,7 +34,22 @@ The signatures must match exactly. No parameters, no return value. If either fun ## Variables: Name in the Table = Name in the Code -Variables you declare in the Variables Table (Inputs and Outputs are the only two classes available for C++ blocks) are plain C++ variables inside `setup()` and `loop()`, using the exact names from the table. Internal state stays in your C++ source: declare ordinary variables at file scope above `setup()`/`loop()` to persist them between scan cycles, or inside the functions for scratch values. +Variables you declare in the Variables Table are plain C++ variables inside `setup()` and `loop()`, using the exact names from the table. + +Every variable class an IEC function block can declare is available to a C++ block: + +| Class | In a C++ block | +|---|---| +| `VAR_INPUT` | Read the value the caller passed in | +| `VAR_OUTPUT` | Write the value the caller reads back | +| `VAR_IN_OUT` | Read and write; the caller sees your changes | +| `VAR` | The block's own state, persisting across scan cycles | +| `VAR_TEMP` | Scratch storage | +| `VAR_EXTERNAL` | A global from the project's configuration, read and written under its lock | + +Because a C++ block runs inside the scan cycle, a `VAR_EXTERNAL` behaves exactly as it does in ST: `counter = counter + 1;` completes within one scan while holding that global's lock, so no update is lost. (A Python block cannot offer that guarantee — see [Python Restrictions](/docs/openplc-editor/custom-languages/python-blocks/python-restrictions).) + +You can also keep internal state in your C++ source rather than the table: declare ordinary variables at file scope above `setup()`/`loop()` to persist them between scan cycles, or inside the functions for scratch values. Use `VAR` when you want the value visible to the debugger, and a file-scope C++ variable when it's purely an implementation detail. ```cpp // With ENABLE (BOOL Input), SPEED (INT Input), MOTOR_ON (BOOL Output) declared in the table: @@ -218,6 +233,44 @@ buf += u" suffix"; W_STRING = buf; ``` +## Function Block Instances + +A C++ block runs inside the scan cycle, so it can declare a function block instance and call it, exactly as an ST block would. Declare it as a `VAR` in the Variables Table, then set its inputs, call it, and read its outputs. + +You refer to the instance by the name you declared in the table, but its **pins are upper-cased**, matching how the compiler emits them: + +```cpp +// With ton0 : TON and acc : Accum declared under VAR, +// and trigger : BOOL input, elapsed : TIME output, total : DINT output: + +void loop() { + // Standard library function block + ton0.IN = trigger; + ton0.PT = 1000000000LL; // T#1s, in nanoseconds + ton0(); // the call is what makes the timer advance + elapsed = ton0.ET; + + // One of your own function blocks + acc.STEP = 2; + acc(); + total = acc.TOTAL; +} +``` + +Each instance keeps its own state across scan cycles, just as it would in ST. Call it once per scan from `loop()`; an instance you never call never advances. + +> **Note:** `TIME`, `DATE`, `TOD` and `DT` are 64-bit integers in C++. `TIME`, `TOD` and `DT` are counts of nanoseconds, so `T#1s` is `1000000000LL`. `DATE` is a count of **days** since 1970-01-01. + +### Naming a member that matches its own type + +If a structure member is named the same as its type — `mode : Mode`, which CODESYS allows and real projects use — the compiler emits it with a trailing underscore, because C++ rejects a member that changes the meaning of its type name inside the class: + +```cpp +motor.MODE_ = MODE::RUNNING; // member `mode` of type `Mode` +``` + +Autocomplete offers the correct spelling, so you don't have to remember which members are affected. + ## Arduino Conditional Compilation When your target hardware is an Arduino-compatible board, the build adds the `ARDUINO` macro so you can use the Arduino API: diff --git a/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md b/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md index 355f671..a22953f 100644 --- a/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md +++ b/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md @@ -127,6 +127,83 @@ Network requests, file I/O, `time.sleep()`, large reads. All fine. They block yo - Strings are limited to 126 characters; longer values are truncated when written to a STRING output. - Array variables keep the length declared in the Variables Table. Don't `append`/`pop` on them. +## Shared Globals (VAR_EXTERNAL): One Writer Only + +A Python block can declare a `VAR_EXTERNAL` to reach a global from the project's configuration, and read and write it like any other variable. But because your block runs in a separate process, updating a global is **not** the atomic operation it is in ST, LD or C++. This is the one place where the process boundary changes the meaning of your code rather than just its timing, so it's worth understanding before you rely on it. + +### What actually happens + +Your block never touches the global directly. Each cycle: + +1. The PLC reads the global's current value and sends it in with your inputs. +2. Your `block_loop()` runs, in a different process, and may change its copy. +3. The PLC takes your copy back and stores it into the global. + +Steps 1 and 3 each take the global's lock, so you never see or write a half-updated value. What isn't protected is the gap between them — and your block's whole cycle sits in that gap. + +### The consequence: lost updates + +If anything else writes the same global while your block is mid-cycle, step 3 overwrites it. Your block sends back a value computed from what it read at step 1, which is now stale. + +So this line does **not** reliably increment a shared counter: + +```python +def block_loop(): + global shared_count + shared_count = shared_count + 1 # unsafe if anything else also writes shared_count +``` + +> **Warning:** In ST, LD or C++, `shared_count := shared_count + 1;` completes inside a single scan while holding the global's lock, so no update is lost. The same line in a Python block does not, because the read and the write happen a cycle apart in another process. + +Measured on real hardware: a Python block and a C++ block each adding 2 to the same global, every cycle. The global held consistently **two thirds** of the total the two blocks had added between them. The missing third is updates that were read, added to, and then overwritten. + +| Where the code runs | `g := g + 1` with another writer | +|---|---| +| ST / LD / FBD / IL | Safe. Read-modify-write completes in one scan under the global's lock | +| C++ function block | Safe. Same. The block runs inside the scan | +| **Python function block** | **Unsafe. Updates from other tasks in the same window are lost** | + +### How to use globals safely from Python + +- **Give each global a single writer.** If your Python block writes a global, nothing else should. That case is exact, with no lost updates at all. +- **Read freely.** Reading a global from Python is always safe. You may read a value that is a cycle old, but never a corrupt one. +- **Don't accumulate into a shared global.** If several tasks must contribute to one total, have each write its own contribution to its own variable and let an ST block add them up. The addition then happens inside the scan, where it is atomic. +- **Don't use a global as a lock or a semaphore** between a Python block and the rest of the program. Test-and-set cannot work across the boundary; two blocks can both see it free. + +This is a deliberate trade. Holding the global's lock from step 1 to step 3 would make Python's read-modify-write atomic, but it would also block every other task that touches that global for a whole Python cycle (~100 ms), which is far worse for the rest of your program. + +## Function Block Instances Are Not Available + +You cannot declare a function block instance in a Python block's Variables Table. A `TON`, a counter, or one of your own function blocks is rejected when you compile, with a message explaining why. + +The reason is not that the type is unsupported. It's that an instance only advances when something calls it. In ST you write `ton0(IN := start, PT := T#5s);`, and that call is what makes the timer tick. A Python block runs in its own process and has no way to call into the scan cycle, so the instance would sit there and never execute. Its `Q` would never go true and its `ET` would never move. Rather than give you pins that silently never update, the compiler refuses the declaration. + +### What to do instead + +**For execution control of the Python block itself**, use the `EN` / `ENO` pins that every function block instance has: + +``` +(* The Python block only runs while enabled *) +myPyBlock(EN := systemReady); +running := myPyBlock.ENO; +``` + +When `EN` is false the block does not execute at all, and `ENO` mirrors it. + +**When you need a function block's behaviour**, instantiate and call it in an ST, LD or FBD block, then wire its outputs into your Python block as inputs: + +``` +(* ST block: owns the timer and calls it *) +delayDone : TON; + +delayDone(IN := startSignal, PT := T#5s); +analyse(timerElapsed := delayDone.Q, sample := reading); +``` + +Your Python block receives `timerElapsed` as an ordinary BOOL input. The timer runs in the scan where it belongs, and Python sees its result. + +> **Tip:** C++ function blocks have no such restriction. They run inside the scan cycle, so they can declare a function block instance and call it directly. See [C++ Function Block Structure](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure). + ## Performance Considerations ### Keep block_loop() Fast From 1e187feb6caa41d2cafb9229b200f6348806242b Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 26 Aug 2026 11:26:54 -0400 Subject: [PATCH 2/4] docs: a Python block can hold a function block instance after all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page I wrote an hour ago said a Python block cannot declare one, and explained why the compiler refuses it. Accurate at the time and now wrong: the feature it described as impossible was in the plan, had been skipped, and has since been built. So the section says what actually happens. You declare the instance the way you always would; you cannot call it, because your block runs in another process and the instance lives in the PLC's; and you do not need to, because the PLC calls every instance your block declares once per scan and your code just uses the pins. What needed spelling out is the part that looks synchronous and is not. Setting an input and reading an output on adjacent lines reads like a call, but the sequence per scan is: apply what the block wrote, call the instance, publish what it produced — so the block sees that on its next cycle. It is the same one-cycle lag every Python input already has, and easier to miss here for exactly that reason. Also documented: pins are upper-cased (the compiler upper-cases members, and `ton0.in` would be a Python keyword anyway); outputs are read-only and internal state is not exposed at all; the instance runs on the PLC's scan rather than the block's ~100 ms loop, so a TON keeps accurate time however rarely Python looks at it; several instances are called in Variables Table order; and the two shapes still refused, an array of instances and a generic pin. The C++ page's cross-reference is corrected to match — it had said Python has no such capability, which is no longer the difference. The difference is the lag. --- .../cpp-blocks/cpp-structure.md | 2 + .../python-blocks/python-restrictions.md | 76 +++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md index da89ed2..3b61d70 100644 --- a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md +++ b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md @@ -259,6 +259,8 @@ void loop() { Each instance keeps its own state across scan cycles, just as it would in ST. Call it once per scan from `loop()`; an instance you never call never advances. +> **Note:** A Python block can hold an instance too, but cannot call it — the PLC calls it instead, once per scan, and Python only uses the pins. That difference costs a one-cycle lag on the outputs. See [Python Restrictions](/docs/openplc-editor/custom-languages/python-blocks/python-restrictions). + > **Note:** `TIME`, `DATE`, `TOD` and `DT` are 64-bit integers in C++. `TIME`, `TOD` and `DT` are counts of nanoseconds, so `T#1s` is `1000000000LL`. `DATE` is a count of **days** since 1970-01-01. ### Naming a member that matches its own type diff --git a/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md b/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md index a22953f..d36a6a9 100644 --- a/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md +++ b/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md @@ -172,37 +172,81 @@ Measured on real hardware: a Python block and a C++ block each adding 2 to the s This is a deliberate trade. Holding the global's lock from step 1 to step 3 would make Python's read-modify-write atomic, but it would also block every other task that touches that global for a whole Python cycle (~100 ms), which is far worse for the rest of your program. -## Function Block Instances Are Not Available +## Function Block Instances: The PLC Calls Them For You -You cannot declare a function block instance in a Python block's Variables Table. A `TON`, a counter, or one of your own function blocks is rejected when you compile, with a message explaining why. +You can declare a function block instance in a Python block's Variables Table — a `TON`, a counter, one of your own function blocks — and use its pins exactly as you would in ST. -The reason is not that the type is unsupported. It's that an instance only advances when something calls it. In ST you write `ton0(IN := start, PT := T#5s);`, and that call is what makes the timer tick. A Python block runs in its own process and has no way to call into the scan cycle, so the instance would sit there and never execute. Its `Q` would never go true and its `ET` would never move. Rather than give you pins that silently never update, the compiler refuses the declaration. +What you cannot do is *call* it. `ton0()` has no meaning in Python: your block runs in a separate process, and the instance lives in the PLC's. You don't need to. **The PLC calls every instance your block declares, once per scan cycle**, and your Python code just reads and writes the pins. -### What to do instead +```python +def block_loop(): + global elapsed, finished + ton0.IN = start_signal # drive the timer's inputs + ton0.PT = 5_000_000_000 # T#5s, in nanoseconds + + finished = ton0.Q # read what the instance produced + elapsed = ton0.ET +``` -**For execution control of the Python block itself**, use the `EN` / `ENO` pins that every function block instance has: +Declare it the way you always would: ``` -(* The Python block only runs while enabled *) -myPyBlock(EN := systemReady); -running := myPyBlock.ENO; +VAR + ton0 : TON; +END_VAR ``` -When `EN` is false the block does not execute at all, and `ENO` mirrors it. +### Pin names are upper-cased + +The compiler upper-cases members, so the pins are `ton0.IN`, `ton0.PT`, `ton0.Q`, `ton0.ET` — even if your own function block declares them in lower case. `ton0.in` would be a Python keyword anyway. + +### What you can write, and what you can only read + +| Pin kind | From Python | +|---|---| +| The block's inputs (`IN`, `PT`, …) | read and write | +| The block's in-outs | read and write | +| The block's outputs (`Q`, `ET`, …) | **read only** | +| The block's internal state | not available | + +Assigning to an output has no effect, for the same reason assigning to one of your own inputs has none: the next cycle overwrites it. Internal state is deliberately not exposed — it belongs to the instance, and writing it from outside would corrupt the block. + +### Outputs are one cycle behind + +> **Tip:** Setting an input and reading an output in the same `block_loop()` does **not** give you the result of this cycle's call. -**When you need a function block's behaviour**, instantiate and call it in an ST, LD or FBD block, then wire its outputs into your Python block as inputs: +The sequence per scan is: the PLC applies what your block wrote, calls the instance, then publishes what it produced. Your block sees that on its *next* cycle. So: +```python +def block_loop(): + global result + ton0.IN = True + result = ton0.Q # still the PREVIOUS cycle's value, not this one's ``` -(* ST block: owns the timer and calls it *) -delayDone : TON; -delayDone(IN := startSignal, PT := T#5s); -analyse(timerElapsed := delayDone.Q, sample := reading); +This is the same one-cycle lag every Python input already has (see [The ~100 ms Loop](#the-100-ms-loop) above) — it is just easier to overlook here, because setting a pin and reading a pin on adjacent lines *looks* synchronous. For logic where that matters, put the function block in an ST block instead. + +### The instance runs every scan, not every Python cycle + +The PLC calls the instance on its own scan cycle, which is typically much faster than your block's ~100 ms loop. A `TON` therefore keeps accurate time regardless of how often your Python code looks at it — the timer is not being driven by Python's cadence, only observed by it. + +If your block declares several instances, they are called in the order they appear in the Variables Table. + +### Controlling whether your block runs at all + +Use the `EN` / `ENO` pins that every function block instance has: + +``` +myPyBlock(EN := systemReady); +running := myPyBlock.ENO; ``` -Your Python block receives `timerElapsed` as an ordinary BOOL input. The timer runs in the scan where it belongs, and Python sees its result. +When `EN` is false the block does not execute — and neither do the instances it declares. + +### What is still not supported -> **Tip:** C++ function blocks have no such restriction. They run inside the scan cycle, so they can declare a function block instance and call it directly. See [C++ Function Block Structure](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure). +- **An array of function block instances** (`ARRAY [0..3] OF TON`) is refused. +- **A generic pin** (`ANY_NUM` and friends) has no concrete type until it is wired, so a block with one cannot cross. The compiler names the pin it could not describe. ## Performance Considerations From c6ecb44d4e5f74095eb4457fd39e07ddd76d5cae Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 27 Aug 2026 22:43:00 -0400 Subject: [PATCH 3/4] docs: structures, enumerations and arrays in Python and C++ blocks DOPE-584 gave native blocks full IEC type parity, and the docs still described the old world: base types and flat arrays only. Two new pages cover what a structure, an enumeration and an array actually look like on each side, because the two languages do not agree and the differences are silent when you get them wrong. Every example here was compiled and run on an SLM-RP4 (Runtime v4) through openplc-cli, with the values read back over the debugger. The findings that would not have survived guesswork: - Member spelling is opposite between the two languages. Python keeps the spelling from the Variables Table (`mot.speed`); C++ sees the compiler's uppercase (`mot.SPEED`). Getting this wrong in Python raises AttributeError at runtime, not at build time. - A Python list is indexed by the IEC index, so `ARRAY [1..2]` is a list of length 3 whose element 0 is None. Iterating it naively hands you a None. - A multi-dimensional array is `grid(i, j)` in C++, not `grid[i][j]`. - A scalar enumeration pin in C++ needs assigning to the raw enum first; `mode.get()` returns a wrapper that will not cast to int. An array element is already the raw enum and needs no such step. - A function block instance must be declared under VAR to be driven from Python. Declared as an Input its pins still arrive, but writes never reach the PLC, so a TON sits at zero with nothing to explain why. - DATE is a count of days while TIME, TOD and DT are nanoseconds. Also documents the missing-import failure mode, which reports itself as "PLC runtime has stopped." while the PLC is plainly still running: the wrapper's liveness check calls os.kill, `os` was never imported, and the NameError is reported as a stopped runtime. Corrects one claim on the existing C++ page: ordering comparisons work directly on STRING variables, without going through .get(). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ --- docs/_config.json | 12 +- .../cpp-blocks/cpp-data-types.md | 281 +++++++++++++++ .../cpp-blocks/cpp-structure.md | 6 +- .../python-blocks/python-data-types.md | 326 ++++++++++++++++++ .../python-blocks/python-restrictions.md | 9 + .../python-blocks/python-structure.md | 13 +- 6 files changed, 641 insertions(+), 6 deletions(-) create mode 100644 docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types.md create mode 100644 docs/openplc-editor/custom-languages/python-blocks/python-data-types.md diff --git a/docs/_config.json b/docs/_config.json index 8259abb..516e19c 100644 --- a/docs/_config.json +++ b/docs/_config.json @@ -398,6 +398,10 @@ "title": "Python Block Structure", "path": "python-structure" }, + { + "title": "Python Data Types", + "path": "python-data-types" + }, { "title": "Python Language Server", "path": "python-language-server" @@ -420,6 +424,10 @@ "title": "C++ Block Structure", "path": "cpp-structure" }, + { + "title": "C++ Data Types", + "path": "cpp-data-types" + }, { "title": "C++ Code Completion", "path": "cpp-completion" @@ -759,11 +767,11 @@ "path": "profile" }, { - "title": "Security → Email", + "title": "Security \u2192 Email", "path": "security-email" }, { - "title": "Security → Password", + "title": "Security \u2192 Password", "path": "security-password" }, { diff --git a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types.md b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types.md new file mode 100644 index 0000000..457c614 --- /dev/null +++ b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types.md @@ -0,0 +1,281 @@ +# C++ Data Types: Structures, Enumerations and Arrays + +A C++ function block reaches its variables directly — nothing is copied or serialised. A structure in the Variables Table is a real C++ `struct` in your code, an array is a real container, an enumeration is a real `enum class`. This page shows the spelling for each. + +For `STRING` and `WSTRING`, see [Working with STRING and WSTRING](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure#working-with-string-and-wstring) on the structure page. + +## The One Rule That Explains Everything + +> **Warning:** The IEC compiler **uppercases every identifier**. A structure member declared `speed` is `SPEED` in your C++ code. An enumeration value declared `Running` is `RUNNING`. + +The variable's own name is the exception: the block's pins are bound under the exact spelling from the Variables Table, so `motor` stays `motor` while its member becomes `motor.SPEED`. + +This is the opposite of a Python block, where members keep their declared spelling. If you are porting logic between the two, expect to change the case of every member access. + +## Structures + +Given a structure `Motor` with members `speed : INT`, `label : STRING` and `trims : ARRAY [0..2] OF INT`, and a Variables Table entry `mot : Motor`: + +```cpp +void loop() +{ + int16_t s = mot.SPEED; // member: UPPERCASE + out_label = mot.LABEL; // a STRING member behaves like a STRING pin + int16_t t = mot.TRIMS[1]; // nested array member +} +``` + +Writing a structure output is the same, member by member: + +```cpp +void loop() +{ + result.SPEED = 111; + result.LABEL = "written"; + result.TRIMS[2] = 77; +} +``` + +The type name itself is available if you need to declare your own local of that type, or write a helper: + +```cpp +static void copy_motor(MOTOR &dst, const MOTOR &src) +{ + dst.SPEED = src.SPEED; + dst.LABEL = src.LABEL; + for (int k = 0; k <= 2; k++) { + dst.TRIMS[k] = src.TRIMS[k]; + } +} + +void loop() +{ + copy_motor(result, mot); +} +``` + +> **Tip:** The struct type is spelled in uppercase too — `MOTOR`, not `Motor`. + +## Enumerations + +An enumeration becomes a C++ `enum class` whose values are uppercase. + +Given `Mode : (STOPPED, RUNNING, MANUAL)` and `mode : Mode`: + +```cpp +void loop() +{ + // A scalar enumeration pin converts implicitly to the raw enum + MODE current = mode; + + if (current == MODE::RUNNING) { + // ... + } + + // For the numeric value, cast the raw enum + out_number = static_cast(current); +} +``` + +Writing one: + +```cpp +void loop() +{ + state = MODE::MANUAL; // state : Mode (Output) +} +``` + +> **Warning:** `mode.get()` does **not** give you the raw enum — it returns an internal wrapper, and casting that to `int` fails to compile with *invalid cast from type `IEC_ENUM_Value`*. Assign to a `MODE` variable first, as above, and cast that. + +## Arrays + +How you index an array depends on its rank, because the compiler uses a different container for each. + +### One dimension — square brackets + +A 1-D array pin arrives as a pointer to its first element, with the IEC lower bound already folded in. Index it with the IEC index directly: + +```cpp +// names : ARRAY [0..1] OF STRING +// bank : ARRAY [0..1] OF Motor +// trims : ARRAY [1..3] OF INT +void loop() +{ + out_first = names[0]; + out_label = bank[1].LABEL; // array of structures + int16_t t = trims[1]; // declared [1..3] — index 1 is the first element +} +``` + +Because the lower bound is folded in, you always use the index you declared. A `[1..3]` array is read at `1`, `2`, `3` — never at `0`. + +> **Warning:** There is no bounds check and no length. `names[7]` on a two-element array compiles and reads whatever follows it in memory. Loop over the bounds you declared. + +### Two and three dimensions — parentheses + +Rank 2 and rank 3 arrays are container objects, and their element accessor is `operator()`, not `[]`: + +```cpp +// grid : ARRAY [1..2, 0..2] OF INT +void loop() +{ + int16_t sum = 0; + for (int i = 1; i <= 2; i++) { + for (int j = 0; j <= 2; j++) { + sum = sum + grid(i, j); // parentheses, one call, both indices + } + } + total = sum; +} +``` + +Writing is the same shape: + +```cpp +grid_out(2, 1) = 42; +``` + +> **Warning:** `grid[i][j]` does not compile for a multi-dimensional array. Use `grid(i, j)`. + +### Arrays of enumerations + +An element of an enumeration array is already the raw `enum class` — no conversion needed: + +```cpp +// modes : ARRAY [0..1] OF Mode +if (modes[1] == MODE::MANUAL) { + // ... +} +out_number = static_cast(modes[1]); + +modes_out[0] = MODE::MANUAL; // writing +``` + +This differs from a *scalar* enumeration pin, which needs the assignment-to-`MODE` step shown above. The array element skips it. + +### Arrays of strings + +Each element behaves like a STRING pin: + +```cpp +// names : ARRAY [0..1] OF STRING +out_len = (int32_t)names[0].length(); +names_out[0] = "first"; +names_out[1] = other_string; +``` + +## Quick Reference + +| Declaration | Read | Write | +|---|---|---| +| `mot : Motor` | `mot.SPEED` | `mot.SPEED = 1;` | +| `mot : Motor` (string member) | `mot.LABEL` | `mot.LABEL = "x";` | +| `mode : Mode` | `MODE m = mode;` | `mode = MODE::RUNNING;` | +| `a : ARRAY [0..3] OF INT` | `a[2]` | `a[2] = 5;` | +| `a : ARRAY [1..3] OF INT` | `a[1]` (first element) | `a[1] = 5;` | +| `g : ARRAY [1..2, 0..2] OF INT` | `g(i, j)` | `g(i, j) = 5;` | +| `b : ARRAY [0..1] OF Motor` | `b[1].LABEL` | `b[1].SPEED = 9;` | +| `m : ARRAY [0..1] OF Mode` | `m[1]` (raw enum) | `m[1] = MODE::MANUAL;` | +| `n : ARRAY [0..1] OF STRING` | `n[0].length()` | `n[0] = "x";` | + +## Temporal Types + +`TIME`, `TOD` and `DT` are 64-bit **nanosecond** counts; `DATE` is a count of **days**: + +```cpp +// pulse : TIME, today : DATE +int64_t ns = pulse; +int64_t days = today; + +if (pulse > 5000000000LL) { // longer than 5 seconds + // ... +} +``` + +## Function Block Instances + +An instance declared on a C++ block is a real object. Set its input pins, **call it**, then read its outputs: + +```cpp +// ton0 : TON (Local) +void loop() +{ + ton0.IN = go; + ton0.PT = 3000000000LL; // 3 s in nanoseconds + ton0(); // <-- evaluate it + done = ton0.Q; + elapsed = ton0.ET; +} +``` + +> **Warning:** The `ton0();` call is required, and forgetting it is silent. The block compiles, runs, and reports `Q = FALSE` with `ET = 0` forever, because nothing ever evaluates the timer. This is the opposite of a Python block, where the generated wrapper calls the instance for you once per scan and you must *not* call it yourself. + +Because a C++ block runs *inside* the scan cycle and you evaluate the instance yourself, there is no one-cycle lag: the values you read are the ones it just produced. A Python block sees the previous cycle's outputs. + +## A Note on `IEC_BOOL` + +`BOOL` is `uint8_t`, not the C++ `bool`. Assign `0` or `1`, and compare against `0` rather than against `1`: + +```cpp +flag = 1; +if (other_flag != 0) { + // ... +} +``` + +## Complete Example + +A block that scans a bank of motors, reports the fastest as a structure, and classifies the result as an enumeration: + +```cpp +static void copy_motor(MOTOR &dst, const MOTOR &src) +{ + dst.SPEED = src.SPEED; + dst.LABEL = src.LABEL; + for (int k = 0; k <= 2; k++) { + dst.TRIMS[k] = src.TRIMS[k]; + } +} + +void setup() +{ +} + +void loop() +{ + int best = 0; + for (int i = 0; i <= 3; i++) { + if (bank[i].SPEED > bank[best].SPEED) { + best = i; + } + } + + fastest_index = (int16_t)best; + copy_motor(fastest, bank[best]); + + if (bank[best].SPEED == 0) { + state = MODE::STOPPED; + } else if (manual_request != 0) { + state = MODE::MANUAL; + } else { + state = MODE::RUNNING; + } +} +``` + +Variables Table for the example: + +| Name | Class | Type | +|---|---|---| +| `bank` | Input | `ARRAY [0..3] OF Motor` | +| `manual_request` | Input | `BOOL` | +| `fastest` | Output | `Motor` | +| `state` | Output | `Mode` | +| `fastest_index` | Output | `INT` | + +## What's Next? + +- [C++ Block Structure](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure) — the two functions, the type mapping, and STRING handling +- [C++ Code Completion](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-completion) — member completion in the editor +- [Python Data Types](/docs/openplc-editor/custom-languages/python-blocks/python-data-types) — the same types, on the Python side diff --git a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md index 604449c..230501f 100644 --- a/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md +++ b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure.md @@ -197,10 +197,10 @@ if (COMMAND == "STOP") { /* ... */ } if (COMMAND != PREVIOUS) { /* ... */ } ``` -For ordering (`<`, `<=`, `>`, `>=`), compare the underlying buffers via `.get()`: +Ordering (`<`, `<=`, `>`, `>=`) works directly on the variables too: ```cpp -if (MY_STRING.get() < OTHER.get()) { /* lexicographic */ } +if (MY_STRING < OTHER) { /* lexicographic */ } ``` ### WSTRING — same shape, wide characters @@ -218,6 +218,8 @@ buf += u" suffix"; W_STRING = buf; ``` +> **Tip:** Structures, enumerations and arrays have their own spelling and indexing rules — including the fact that the compiler uppercases every member name, and that a multi-dimensional array is indexed with `grid(i, j)` rather than `grid[i][j]`. See [C++ Data Types](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types). + ## Arduino Conditional Compilation When your target hardware is an Arduino-compatible board, the build adds the `ARDUINO` macro so you can use the Arduino API: diff --git a/docs/openplc-editor/custom-languages/python-blocks/python-data-types.md b/docs/openplc-editor/custom-languages/python-blocks/python-data-types.md new file mode 100644 index 0000000..973bedc --- /dev/null +++ b/docs/openplc-editor/custom-languages/python-blocks/python-data-types.md @@ -0,0 +1,326 @@ +# Python Data Types: Structures, Enumerations and Arrays + +A Python function block is not limited to single numbers and strings. Any structure, enumeration or array you declare in the Variables Table crosses into your script as an ordinary Python object — a class instance, an `IntEnum` member, or a list. You never unpack bytes yourself. + +This page shows what each IEC type looks like on the Python side, and how to read and write it. + +## The One Rule That Explains Everything + +> **Tip:** Every variable in the Variables Table becomes a plain Python global with the **same name and the same spelling** you typed. Structure members keep their declared spelling too. + +If the Variables Table says `motor : Motor` and `Motor` has a member `speed`, then in Python you write `motor.speed` — lowercase, exactly as declared. This matters because C++ blocks are different: there, the compiler uppercases everything, so the same member is `motor.SPEED`. Two languages, two spellings, one Variables Table. + +## Base Types + +| IEC Type | Python Type | +|---|---| +| `BOOL` | `bool` | +| `SINT`, `INT`, `DINT`, `LINT` | `int` | +| `USINT`, `UINT`, `UDINT`, `ULINT` | `int` | +| `BYTE`, `WORD`, `DWORD`, `LWORD` | `int` | +| `REAL`, `LREAL` | `float` | +| `STRING` | `str` | +| `WSTRING` | `str` | +| `TIME`, `DATE`, `TOD`, `DT` | `int` (see [Temporal types](#temporal-types)) | + +## Structures + +A structure declared in the Data Types editor becomes a Python **class** with one attribute per member. + +Given a structure `Motor`: + +| Member | Type | +|---|---| +| `speed` | `INT` | +| `label` | `STRING` | +| `trims` | `ARRAY [0..2] OF INT` | + +and a Variables Table entry `mot : Motor` (Input), the class is generated for you and `mot` is already an instance when `block_loop()` runs: + +```python +def block_loop(): + global out_speed, out_label + + out_speed = mot.speed # 12 + out_label = mot.label # 'mot-lbl' + first_trim = mot.trims[1] # nested array member +``` + +### Writing a structure output + +Construct the class by name and assign it. The class is defined above your code, so you can use it freely: + +```python +def block_loop(): + global result # result : Motor (Output) + + result = Motor() + result.speed = 55 + result.label = 'py-written' + result.trims = [1, 2, 3] +``` + +> **Warning:** `Motor()` starts with its attributes unset. Assign every member you care about. A member you never assign keeps whatever the output held before. + +You can also modify an input structure and assign it onward — the input is a normal Python object, and changing it does not affect the PLC-side input: + +```python +def block_loop(): + global result + mot.speed = mot.speed + 1 # local change only + result = mot # ...but this reaches the PLC +``` + +## Enumerations + +An enumeration becomes a Python [`IntEnum`](https://docs.python.org/3/library/enum.html#enum.IntEnum) with the member names you declared. + +Given `Mode : (STOPPED, RUNNING, MANUAL)` and `mode : Mode` (Input): + +```python +def block_loop(): + global label, number, running + + label = mode.name # 'RUNNING' + number = int(mode) # 1 + running = (mode == Mode.RUNNING) + + if mode is Mode.MANUAL: + pass +``` + +Because it is an `IntEnum`, a member compares equal to its integer value, so `mode == 1` also works. Prefer the named form — it survives someone reordering the enumeration. + +### Writing an enumeration output + +Assign a member of the generated class: + +```python +def block_loop(): + global state # state : Mode (Output) + state = Mode.MANUAL +``` + +Assigning a bare integer works too, but nothing checks it is in range, so a typo becomes a silently invalid enumeration value on the PLC side. Use the named member. + +## Arrays + +An array becomes a Python **list**. + +```python +# names : ARRAY [0..1] OF STRING (Input) +def block_loop(): + global joined + joined = names[0] + '/' + names[1] + count = len(names) # 2 +``` + +### IEC indices are preserved — mind the lower bound + +This is the one place where Python's view differs from what you might expect. + +> **Warning:** The list is indexed by the **IEC index**, not from zero. An `ARRAY [1..2] OF INT` becomes a list of length **3**, where index `0` is `None` and the real elements are at `1` and `2`. + +```python +# grid : ARRAY [1..2, 0..2] OF INT (Input) +def block_loop(): + global first + first = grid[1][0] # the IEC element grid[1,0] + # grid[0] is None — there is no IEC element 0 + # len(grid) is 3, not 2 +``` + +So `for row in grid:` will hand you `None` first. Iterate over the declared range instead: + +```python +total = 0 +for i in range(1, 3): # IEC bounds 1..2 + for j in range(0, 3): # IEC bounds 0..2 + total += grid[i][j] +``` + +An array declared `[0..n]` has no hole, so it iterates naturally. Declaring your arrays from `0` is the simplest way to avoid the whole question. + +### Multi-dimensional arrays + +Rank 2 and rank 3 arrive as nested lists, indexed one bracket per dimension: + +```python +# grid : ARRAY [1..2, 0..2] OF INT +value = grid[2][1] # IEC grid[2,1] +``` + +Rank 4 and beyond cannot cross into a Python block; the build refuses them with a message naming the variable. + +### Arrays of structures + +A list of class instances, exactly as you would expect: + +```python +# bank : ARRAY [0..1] OF Motor (Input) +def block_loop(): + global total + total = bank[0].speed + bank[1].speed + name = bank[1].label +``` + +### Arrays of enumerations + +A list of `IntEnum` members: + +```python +# modes : ARRAY [0..1] OF Mode (Input) +if modes[1] is Mode.MANUAL: + pass +``` + +### Writing an array output + +Assign a whole list, or write elements in place: + +```python +def block_loop(): + global names_out, grid_out, bank_out + + names_out = ['first', 'second'] # whole list + + grid_out = [None, [10, 11, 12], [20, 21, 22]] # note the None at index 0 + # for a [1..2] lower bound + + m = Motor() + m.speed = 88 + m.label = 'bank-py' + m.trims = [0, 0, 7] + bank_out = [m, m] +``` + +> **Warning:** Keep the declared length. The PLC-side array is fixed size, so only the declared elements are copied back — a longer list is truncated and a shorter one leaves the remaining elements at their previous values. `append()` and `pop()` on an output list do not grow or shrink the PLC array. + +## Strings + +`STRING` and `WSTRING` are ordinary Python `str`. Nothing special is required: + +```python +def block_loop(): + global message + message = 'temp=' + str(reading) +``` + +> **Warning:** Strings crossing the boundary are capped at **126 characters**. A longer value is truncated at the boundary, not rejected — check `len()` yourself if the full value matters. + +## Temporal Types + +`TIME`, `DATE`, `TOD` and `DT` arrive as integers, and the unit depends on the type: + +| IEC Type | Python value | +|---|---| +| `TIME` | nanoseconds | +| `TOD` (`TIME_OF_DAY`) | nanoseconds since midnight | +| `DT` (`DATE_AND_TIME`) | nanoseconds since the epoch | +| `DATE` | **days** since the epoch | + +`DATE` being days rather than nanoseconds is the one that catches people: + +```python +import datetime + +def block_loop(): + global day_name + day_name = (datetime.date(1970, 1, 1) + datetime.timedelta(days=start_date)).isoformat() +``` + +## Function Block Instances + +A function block instance declared on a Python POU is called for you, once per PLC scan, by the generated wrapper. From Python you read and write its pins by name. + +> **Warning:** Declare the instance under **`VAR`** (Local), not Input. Only a Local or In/Out instance can be *driven* from Python. Declared as an Input, all four pins still arrive — you can read `Q` and `ET` — but your writes to `IN` and `PT` never reach the PLC, so the timer sits at zero and nothing tells you why. + +```python +# ton0 : TON (Local) +def block_loop(): + global elapsed, done + ton0.IN = True + ton0.PT = 5_000_000_000 # TIME is nanoseconds — 5 s + elapsed = ton0.ET # counts up: 3s100ms, 4s400ms, ... + done = ton0.Q # True once ET reaches PT +``` + +Only the pins that make sense in each direction are exchanged: `IN` and `PT` travel out to the PLC, `Q` and `ET` come back. You do not have to think about which — write the ones the block takes, read the ones it produces. + +> **Tip:** Pin names on a function block instance are **uppercase**, because that is how the standard library declares them (`IN`, `PT`, `Q`, `ET`). Structure members you declared yourself keep your spelling. When unsure, use the code completion — see [Python Language Server](/docs/openplc-editor/custom-languages/python-blocks/python-language-server). + +Because the instance is evaluated on the PLC scan and your block runs on its own ~100 ms loop, **outputs lag by one exchange**: `ton0.Q` inside `block_loop()` is the value from the previous cycle. That is correct for timers and counters, which are driven by the scan rather than by your loop. + +Gate an instance with `EN` and read `ENO` back: + +```python +ton0.EN = enable_flag # False: not evaluated this scan +was_run = ton0.ENO +``` + +## What Cannot Cross + +The build refuses these with a message naming the variable, rather than producing a block that misreads memory: + +| Declaration | Why | +|---|---| +| Rank 4 or higher array | The compiler declares array containers up to rank 3 | +| A named `ARRAY` type (an alias declared in Data Types) | Not supported yet — declare the array inline on the variable instead | +| A structure that nests too deeply, or refers to itself | The layout cannot be enumerated | +| `VAR_TEMP` | Has no meaning in a process that outlives the scan — use `VAR` | + +## Complete Example + +A block that takes a bank of motors, finds the fastest, and reports it as a structure and an enumeration: + +```python +import os +import time +import struct +from multiprocessing import shared_memory + + +def block_init(): + pass + + +def block_loop(): + global fastest, state, fastest_index + + best = 0 + for i in range(len(bank)): + if bank[i].speed > bank[best].speed: + best = i + + fastest_index = best + + fastest = Motor() + fastest.speed = bank[best].speed + fastest.label = bank[best].label + fastest.trims = list(bank[best].trims) + + if bank[best].speed == 0: + state = Mode.STOPPED + elif manual_request: + state = Mode.MANUAL + else: + state = Mode.RUNNING +``` + +Variables Table for the example: + +| Name | Class | Type | +|---|---|---| +| `bank` | Input | `ARRAY [0..3] OF Motor` | +| `manual_request` | Input | `BOOL` | +| `fastest` | Output | `Motor` | +| `state` | Output | `Mode` | +| `fastest_index` | Output | `INT` | + +Note `list(bank[best].trims)` rather than `bank[best].trims` — assigning the list directly would make the output share the input's list object. It still works, but copying keeps the two independent if you later modify one. + +## What's Next? + +- [Python Block Structure](/docs/openplc-editor/custom-languages/python-blocks/python-structure) — the two functions and how variables reach your script +- [Python Variable Restrictions](/docs/openplc-editor/custom-languages/python-blocks/python-restrictions) — the execution model and its limits +- [C++ Data Types](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types) — the same types, on the C++ side diff --git a/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md b/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md index 355f671..a6267c0 100644 --- a/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md +++ b/docs/openplc-editor/custom-languages/python-blocks/python-restrictions.md @@ -62,6 +62,15 @@ If an `import` fails because a package isn't installed, the Python process for t > **Keep the four imports at the top of the template** (`shared_memory`, `struct`, `time`, `os`). They're required even if you don't reference them in your own code. +The generated wrapper that surrounds your script uses all four and does not import them itself. Dropping one is easy to do — they look unused — and the symptom is misleading: the block starts, exits about a second later, and the log says + +``` +[Python] PLC runtime has stopped. +[Python] Stopping Python block: MyBlock +``` + +The runtime has not stopped. The wrapper's liveness check calls `os.kill(plc_pid, 0)`, that raises `NameError` because `os` was never imported, and the handler reports it as a stopped runtime. If you see that message while the PLC is plainly still running, check your imports first. + ### What You Cannot Do - **Install packages from inside a block**: `pip install` happens on the device, not from your block code. A block cannot install its own dependencies at runtime. diff --git a/docs/openplc-editor/custom-languages/python-blocks/python-structure.md b/docs/openplc-editor/custom-languages/python-blocks/python-structure.md index fbfa87d..5008daa 100644 --- a/docs/openplc-editor/custom-languages/python-blocks/python-structure.md +++ b/docs/openplc-editor/custom-languages/python-blocks/python-structure.md @@ -104,11 +104,18 @@ You can use any of the following IEC types as inputs, outputs, or locals: | USINT, UINT, UDINT, ULINT | `int` | | BYTE, WORD, DWORD, LWORD | `int` | | REAL, LREAL | `float` | -| STRING | `str` | -| ARRAY of any of the above | `list` | +| STRING, WSTRING | `str` | +| TIME, TOD, DT | `int` (nanoseconds) | +| DATE | `int` (days since the epoch) | +| ARRAY of any type | `list` | +| A structure from the Data Types editor | a generated class instance | +| An enumeration from the Data Types editor | a generated `IntEnum` member | +| A function block instance | an object with its pins as attributes | When you read a variable, you get a plain Python value of the corresponding type. When you assign to an output, the value is converted back to its IEC type and sent to the PLC. +Structures, enumerations, multi-dimensional arrays, arrays of structures and function block instances all have their own rules — indexing, member spelling and how to construct one. They are covered on their own page: [Python Data Types](/docs/openplc-editor/custom-languages/python-blocks/python-data-types). + ### Strings STRING variables behave as ordinary Python `str` values. The maximum length is **126 characters**: anything longer will be truncated when written to an output. @@ -135,6 +142,8 @@ def block_loop(): average = sum(samples) / len(samples) ``` +> **Warning:** The list is indexed by the **IEC index**. `ARRAY [0..9]` behaves exactly like a ten-element Python list, but `ARRAY [1..10]` becomes a list of length **11** whose index `0` is `None`. See [Python Data Types](/docs/openplc-editor/custom-languages/python-blocks/python-data-types). + ## Complete Example A function block that implements a temperature alarm with hysteresis and a counter. From 849ba17dada02071baca5c280e859624e98a415a Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 27 Aug 2026 22:49:16 -0400 Subject: [PATCH 4/4] docs: a located variable is Local, not Input or Output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class and the location answer different questions, and the examples conflated them. `Input` and `Output` define the pins of a Function Block — how a value moves in and out of that POU. A location says which physical address a variable is wired to. A Program's I/O is `Local` with a `%IX` or `%QX` location; the class never changes to match the direction of the signal. `local-variables.md` already stated this correctly ("To read or write a physical I/O pin from a Program | **Local** | ... The class stays Local"), so the examples were contradicting the reference page. Fixed: - `getting-started/quick-start.md` — `output_state` was `Class: Output` with `%QX0.0`. Now Local, with a note on why, since this is the first located variable most readers ever declare. - `examples/start-stop-seal-in.md` — told the reader that wiring the demo to real hardware meant switching the three variables to `Input` / `Output` class. It means adding locations and leaving them Local. Checked and left alone: Function Block variables tables legitimately use Input/Output (those are pins, and carry no location); the Resource's global variables use the `Global` class with locations; `modbus-slave-outputs.md` already had `Local` + `%QX0.0`; and "Digital Input" / "Analog Output" in the addressing tables name address kinds, not variable classes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ --- docs/getting-started/quick-start.md | 4 +++- docs/openplc-editor/examples/start-stop-seal-in.md | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 69b7e09..e749aee 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -167,7 +167,7 @@ In the IDE, you'll see the variables table at the top and the program editor bel 1. Click **+** again to add another variable 2. Set the following values: - **Name**: `output_state` - - **Class**: Output + - **Class**: Local - **Type**: BOOL (found under Base Type) - **Location**: `%QX0.0` @@ -175,6 +175,8 @@ The **Location** field is critical - it maps your program variable to the physic > **Understanding Located Variables**: In IEC 61131-3, the `%Q` prefix indicates an output, `X` indicates a bit (boolean), and `0.0` is the address (byte 0, bit 0). This creates a direct link between your program variable and the Modbus coil address. +> **Why the class is Local, not Output**: The class and the location answer two different questions. The **class** says who may pass the value in and out of *this POU* — `Input` and `Output` define the pins of a Function Block, which is why they are not what you want on a Program. The **location** says which physical address the variable is wired to. A variable driving a real output is `Local` with a `%QX` location; a variable reading a real input is `Local` with a `%IX` location. See [Local Variables](/docs/openplc-editor/working-with-variables/local-variables). + --- ## Step 5: Write the Program diff --git a/docs/openplc-editor/examples/start-stop-seal-in.md b/docs/openplc-editor/examples/start-stop-seal-in.md index dbba091..be2ba22 100644 --- a/docs/openplc-editor/examples/start-stop-seal-in.md +++ b/docs/openplc-editor/examples/start-stop-seal-in.md @@ -30,7 +30,7 @@ In a new LD program, add three variables: | `stop_btn` | Local | BOOL | Tick Debug | | `motor_on` | Local | BOOL | Tick Debug | -All three are local for this demo. In a real installation, `start_btn` and `stop_btn` would be `Input` class with `Location` mapped to physical discrete inputs (`%IX0.0`, `%IX0.1`); `motor_on` would be `Output` mapped to `%QX0.0`. See **[Variables editor](../working-with-variables/variables-editor)**. +All three stay `Local` for this demo. In a real installation they would still be `Local` — you would add a `Location`: `%IX0.0` and `%IX0.1` for the two buttons, `%QX0.0` for the motor. The class says how the value moves in and out of this POU; the location says which physical address it is wired to. A Program's I/O is `Local` plus a location, never `Input` or `Output`. See **[Variables editor](../working-with-variables/variables-editor)**. ## Step 2: Draw the rung @@ -67,6 +67,6 @@ The Simulator has no physical buttons, so you toggle the variables manually from ## Where to next -- Wire it to real hardware: change `start_btn` / `stop_btn` to `Input` class with `%IX` addresses, `motor_on` to `Output` with `%QX0.0`, deploy to a vPLC, and connect the pins. +- Wire it to real hardware: leave all three as `Local` and give them locations — `%IX0.0` / `%IX0.1` for the buttons, `%QX0.0` for the motor — then deploy to a vPLC and connect the pins. - Expose `motor_on` over Modbus so an HMI can see it. See **[Modbus slave: expose digital outputs](modbus-slave-outputs)**. - Try a reset-priority variant: if both buttons are held, which wins? Move `stop_btn` to before the parallel branch.