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/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/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..e48244f --- /dev/null +++ b/docs/openplc-editor/custom-languages/cpp-blocks/cpp-data-types.md @@ -0,0 +1,267 @@ +# 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 is a real object: set its input pins, **call it**, then read its outputs — and its pins are uppercase like every other member. The call is what makes it advance, and forgetting it is silent. + +See [Function Block Instances](/docs/openplc-editor/custom-languages/cpp-blocks/cpp-structure#function-block-instances) on the structure page for the full treatment, including how this differs from a Python block. + +## 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..64b5e5b 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: @@ -197,10 +212,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 +233,48 @@ 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:** 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 + +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. + +> **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..6d6ce82 --- /dev/null +++ b/docs/openplc-editor/custom-languages/python-blocks/python-data-types.md @@ -0,0 +1,303 @@ +# 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 + +An instance declared on a Python POU is called by the PLC once per scan — you never call it yourself — and you drive it through its pins, which are uppercase (`ton0.IN`, `ton0.PT`, `ton0.Q`, `ton0.ET`). Declare it under `VAR`. + +See [Function Block Instances](/docs/openplc-editor/custom-languages/python-blocks/python-restrictions#function-block-instances-the-plc-calls-them-for-you) in Python Variable Restrictions for the full treatment, including `EN` / `ENO` and the one-cycle lag on outputs. + +## 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..dacd9c6 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. @@ -127,6 +136,127 @@ 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: The PLC Calls Them For You + +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. + +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. + +```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 +``` + +Declare it the way you always would: + +``` +VAR + ton0 : TON; +END_VAR +``` + +### 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. + +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 +``` + +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; +``` + +When `EN` is false the block does not execute — and neither do the instances it declares. + +### What is still not supported + +- **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 ### Keep block_loop() Fast 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. 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.