From c6ecb44d4e5f74095eb4457fd39e07ddd76d5cae Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Thu, 27 Aug 2026 22:43:00 -0400 Subject: [PATCH] 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.