Documentation

The full docs ship with the public release.
This page covers the basics.

Quickstart, concepts, and a worked example. Reference docs are being written alongside the parsers; design partners get them ahead of GA.

For technical questions in the meantime, send a note to hello@koyl.ai.

Step 1

Install.

The framework, IDE, and vendor support ship to design partners ahead of public release.

# coming with public release · today via design partner program
pip install plx

Step 2

Write your first FB.

@fb turns a class into a function block. Descriptors declare its interface; logic() is the body the compiler reads.

from plx.framework import fb, input_var, output_var

@fb
class StartStopLatch:
    start: bool = input_var()
    stop: bool = input_var()
    running: bool = output_var()

    def logic(self):
        if self.stop:
            self.running = False
        elif self.start:
            self.running = True

Step 3

Emit native vendor files.

One Python source. Three vendor outputs. The same FB lands as an AOI in ControlLogix, an FB in TwinCAT 3, and an FB in TIA Portal.

from plx.framework import project
from plx.ab import target as ab
from plx.beckhoff import target as beckhoff
from plx.siemens import target as siemens

from controllers import StartStopLatch

proj = project(
    name="line_a",
    pous=[StartStopLatch],
)

# emit native vendor files
proj.compile().emit(ab,       out="exports/line_a.L5X")
proj.compile().emit(beckhoff, out="exports/line_a.TcPOU")
proj.compile().emit(siemens,  out="exports/line_a.SimaticML")

Step 4

Test it before it touches hardware.

The simulator walks the IR scan by scan. Drive inputs, advance cycles, assert on outputs. pytest discovers the tests automatically.

from plx.simulate import scan
from controllers import StartStopLatch

fb = StartStopLatch()
fb.start = True
scan(fb, cycles=1)
assert fb.running is True

fb.start = False
fb.stop = True
scan(fb, cycles=1)
assert fb.running is False

Concepts

Four layers, four jobs.

Framework

What you write.

Decorators turn ordinary Python classes into POUs (program organization units). @fb, @program, @function, @method. Descriptors (input_var, output_var, static_var, inout_var, temp_var) declare interfaces. Type constructors (BOOL, INT, REAL, ARRAY, STRING) build typed references.

src/plx/framework/

Universal IR

What the compiler produces.

A vendor-agnostic Pydantic model. Every IF, FOR, FB invocation, tag reference compiles to a typed IR node. The simulator walks it. AI agents read and write it. Static analyzers query it.

src/plx/model/

Vendor IRs

Where round-trip happens.

Per-vendor Pydantic models that mirror native vendor schemas (L5X, SimaticML, TcPOU). Lossless parsing into and emission from the vendor IR. The Universal IR is a view onto the vendor IR, not a re-encoding.

src/plx/{ab,siemens,beckhoff}/

Simulator

Deterministic scan-cycle execution.

A tree-walking interpreter that executes the IR cycle by cycle. Same inputs produce same outputs. pytest, coverage, and CI work because the source is Python.

src/plx/simulate/

A deeper architectural walkthrough lives at how it works. The capability matrix per vendor lives at multi-vendor round-trip.

Reference (in progress)

What the framework gives you today.

Decorators

  • @fb
  • @program
  • @function
  • @method
  • @struct
  • @enumeration
  • @global_vars

Descriptors

  • input_var()
  • output_var()
  • static_var()
  • inout_var()
  • temp_var()
  • global_var()

Types

  • BOOL · INT · DINT · REAL · LREAL
  • STRING · WSTRING · CHAR · WCHAR
  • TIME · DATE · LTIME · LDATE
  • ARRAY(T, n) · POINTER_TO(T)
  • REFERENCE_TO(T)

Hero examples on the homepage are generated by the live compiler. CI fails if they drift from the framework's actual surface.

Get the rest of the docs

Reference, guides, and worked examples ship to design partners ahead of GA.

Apply for the design partner program for hands-on access to the full toolchain (chat agent, simulator, vendor parsers) and a direct line to the engineers building it.