docs: native function blocks — globals, instances, and the full IEC type surface - #14
Open
thiagoralves wants to merge 5 commits into
Open
docs: native function blocks — globals, instances, and the full IEC type surface#14thiagoralves wants to merge 5 commits into
thiagoralves wants to merge 5 commits into
Conversation
…h languages 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.
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.
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) <[email protected]> Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
Brings the structures/enumerations/arrays pages onto this branch so the native-block documentation lands as one change. Resolved the overlap rather than keeping both copies: - `cpp-structure.md` keeps this branch's Function Block Instances section and gains a pointer to the new C++ Data Types page. - The new pages' own Function Block Instances sections are trimmed to a link, since this branch already covers the topic in full — including that the C++ `ton0()` call is what makes a timer advance, which a hardware run independently confirmed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
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) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Documents what DOPE-584 actually delivered for native function blocks: shared globals, function block instances, and the full IEC type surface — structures, enumerations, multi-dimensional arrays and arrays of structures.
Pages
New
Updated
cpp-structure.md— function block instances,VAR_EXTERNAL, the member-underscore mangling, temporal units, plus a correction (see below)python-restrictions.md— shared globals, function block instances,EN/ENO, and the missing-import failure modepython-structure.md— the supported-types table now reflects realityEverything was run on hardware
Not written from reading the source. Each example was compiled and uploaded to an SLM-RP4 (Runtime v4) with
openplc-cli, and the outputs read back through the debugger — 35 C++ patterns and 26 Python checks. Several findings contradict what the source alone would suggest:mot.speed); C++ sees the compiler's uppercase (mot.SPEED).AttributeErrorat runtime, not a build error. I hit this writing the examples.ARRAY [1..2]is a list of length 3 whose index0isNone.for row in grid:hands you aNonefirst.grid(i, j), notgrid[i][j]for multi-dimensional arrays in C++.[i][j]does not compile.MODE m = mode;first.mode.get()returns a wrapper that will not cast toint.VAR.ton0()call is what makes a timer advance.Q = FALSEandET = 0forever. Confirmed by running both versions.DATEis days;TIME/TOD/DTare nanoseconds.D#1970-01-11→10,T#2s→2000000000.Also documents a failure mode that lies about itself: drop one of the four required imports and the block exits reporting "PLC runtime has stopped." while the PLC is plainly still running. The wrapper's liveness check calls
os.kill,oswas never imported, and theNameErrorsurfaces as a stopped runtime. Easy to hit — the imports look unused.Correction to an existing claim
cpp-structure.mdsaid ordering comparisons on STRING need.get()on both sides. They work directly on the variables; verified by compiling both forms.Not covered
STRING[n](a declared length) is still the transport's 126-character budget, so the docs state the cap rather than the declared length. Deferred to strucpp by DOPE-584.ARRAY [-10..10]) are legal IEC 61131-3 but rejected project-wide by both the parser and the variables table, so they are not documented as usable.🤖 Generated with Claude Code
https://claude.ai/code/session_01UaSZK4LqFWtZpERcqnZ8uQ