06 — ENCAPSULATION
The sealed panel — a block you can trust because nobody can reach inside it.
Estimated 24 minutes.
Operators get the buttons. Nobody gets the terminals.
Every panel on the floor is built to this rule. On the front of the door: a start button, a lamp, a selector, maybe a small HMI. Behind the door: the terminals, the fuses, the relay that actually latches. Nobody argues about it, because the consequence of getting it wrong is measured in volts. A function block is the same object. What is on the pins is the contract; what is inside is the block's own business, and it stays that way only if the rest of the program cannot reach it.
Why this is the lesson that makes the others worth learning
Lessons 03 to 05 gave you a block with memory, a set of declaration sections and a shelf of standard blocks. None of that buys you anything if any rung in the project can reach into an instance and move its state. Encapsulation is what turns "code I reuse" into "a component I trust" — and trust is the only reason to build components at all.
Open the door of LP-301 and reach inside
V-301 is a 3-inch globe valve on a caustic transfer line, run by a working FB_Valve. Every line in this panel is one somebody typed for a real reason on a real commissioning day. Touch each internal and watch two things: what the skid does on the next scan, and what the line costs six months later. Nothing resets; the log at the bottom just gets longer.
A narrow block, in both languages
The ladder box and the declaration are the same statement. If a member has no pin and no specifier that lets you reach it, it does not exist as far as the rest of the project is concerned.
The force that snaps back
Somebody writes V_301.Position := 100.0; from a rung outside the block, once per scan. FB_Valve computes Position from the 4–20 mA feedback card at the top of every call. What does an online watch window show?
- 100.0 — the outside write is the last one, so it wins
- The block's value, because the outside write is silently discarded
- A value that depends entirely on which of the two ran last in the scan
- A compile error on every controller
Three ways in, and what each one costs
Writing an output the block owns gives you two writers on one value, and a coil that does what the scan order says rather than what either author intended. Reading an internal turns an implementation detail into an API nobody agreed to, so the block can never be refactored again. And a global used as a back channel makes behaviour that is invisible at the call site — the block does something the pins cannot explain, and it can never be reused without dragging the global list along with it.
The same block after three years of small favours
Compare this box to the five-pin one four screens back. Same behaviour, same body. The difference is entirely in how much of the block the rest of the plant is now allowed to depend on.
Seal the panel
Here is that block with all nine members reachable from outside. Put each one where it belongs: on the front of the door, behind the door on the service rail, or in the sealed back chamber. Watch the pin fan on the right — that is the contract surface, and every pin on it is a promise you have to keep for as long as the block is in service.
PUBLIC, PRIVATE, PROTECTED, INTERNAL
The 3rd edition of IEC 61131-3 added four access specifiers, and they go on methods, on properties and on variable sections. PUBLIC is reachable from anywhere. PRIVATE is this block only. PROTECTED is this block and anything that EXTENDS it — the service chamber for the family, not for the plant. INTERNAL is everything inside the same library or application, which is how a vendor ships a block whose parts cooperate without exposing them to the machine builder.
What they look like on a real controller
A PROPERTY is the sharpest tool here: its GET and its SET can carry different specifiers, so the plant may read the stem position while only a derived block may write it.
Who can see a PROTECTED member?
FB_Valve declares Cal_Offset under VAR PROTECTED. FB_ValveWithPositioner EXTENDS FB_Valve. A PROGRAM declares V_301 : FB_Valve and V_302 : FB_ValveWithPositioner. Which code can read Cal_Offset?
- Only FB_Valve itself
- FB_Valve and FB_ValveWithPositioner, but not the PROGRAM
- Everything in the same library, including the PROGRAM
- Everything — PROTECTED only affects methods, not variables
Where they are enforced, and where they are only a note
Be honest about your toolchain. CODESYS and TwinCAT implement all four specifiers and will refuse the access at compile time — but an unmarked VAR in a function block defaults to public there, so an instance member is reachable until you say otherwise. Rockwell goes further in its own way: an Add-On Instruction's Local Tags genuinely cannot be referenced outside the instruction, and every parameter carries an External Access setting of Read/Write, Read Only or None. Siemens SCL has no PUBLIC or PRIVATE keyword at all — an FB's static variables live in its instance DB and anything that knows the DB can reach them; the closest equivalents are the per-variable "Accessible from HMI/OPC UA" and "Visible in HMI engineering" attributes.
When your compiler has none of this
Most of the code running in most plants was written on a toolchain with no access specifiers, and it can still be encapsulated — just by agreement rather than by the compiler. Two habits carry almost all of the value: mark internals in the name, usually with a leading underscore, so a reviewer can see the violation on the line rather than by opening the block; and put a short PUBLIC INTERFACE comment at the top of the declaration listing exactly what is safe to depend on. Add CODESYS's {attribute 'hide_all_locals'} pragma if you have it, and the online watch list stops advertising the internals at all.
Two owners, one coil
This is what a broken boundary actually looks like at runtime. TryPLC's browser engine executes the 2nd-edition core, and it refuses V_302.Cmd_Open := TRUE outright — its diagnostic reads "Cmd_Open is an output of function block type FB_VALVE and is read-only from outside". Real controllers are usually more permissive. So the stage below stages the same fight the way it most often reaches a plant anyway: the block owns a coil through a VAR_IN_OUT pin, and an override rung one line below the call writes that same coil.
Watch the coil flap, scan by scan
Two instances of one FB_Valve. V-301 is called and left alone; V-302 has the override rung. Press Run, then scrub back through the scans where the guard is open — the sealed coil never moves, and the overridden one changes state on every single scan.
The invariant, and the three lines that undo it
The block held its rule perfectly the whole time. Poked.Cmd_Open was FALSE on every scan the guard was open. The failure is not in the logic — it is in who else was allowed to write the output.
An invariant is a promise a block can actually keep
Every block worth writing has one sentence it guarantees: this valve is never commanded open while the interlock chain is broken; this counter never goes backwards; this axis never accepts a move while the drive is disabled. Say it out loud, enforce it in exactly one place in the body, and the block is a component you can put on a P&ID. Encapsulation is what makes the promise holdable — if anything outside can write the output, the guarantee is not a guarantee, it is a hope.
The 3 a.m. question
A valve opened with the guard chain broken. You open the project and FB_Valve's body plainly cannot do that — every path through it drops Cmd_Open when Interlock_OK is FALSE. What is the first thing to search for?
- A bug in the interlock wiring — the block cannot be at fault
- Every other write to the solenoid tag in the whole project
- A timing problem between the guard input and the scan
- Whether the block was called twice in the same scan
What you learned
- A block's pins are its contract; everything else is its own business, and it stays that way only if nothing outside can reach it.
- Writing a member the block owns gives you two writers on one value, and the scan order decides which one you get.
- Reading an internal is just as damaging: it turns an implementation detail into an API you can never change.
- A global read inside a block is behaviour you cannot see at the call site — and a block you cannot reuse elsewhere.
- PUBLIC, PRIVATE, PROTECTED and INTERNAL are 3rd-edition specifiers on methods, properties and variables; CODESYS and TwinCAT enforce them, Rockwell hides AOI Local Tags outright, Siemens SCL has no keyword at all.
- With no specifiers, convention still works: an underscore on every internal and a PUBLIC INTERFACE comment at the top of the declaration.
- Every good block guarantees one sentence. Encapsulation is what makes that guarantee something you can rely on rather than something you hope for.
ENCAPSULATION — done
- You can name what each way of reaching into a block costs, and when the bill arrives.
- You can size a block's contract surface and argue for making it smaller with a specific failure, not a principle.
- You watched two instances of one block share an invariant, and watched an override rung take one of them apart on the real engine.