Allen Bradley
ControlLogix · CompactLogix
IDE
Studio 5000
Files
.L5X · .ACD (via L5X export)
AOIs and routines round-trip through the Universal IR. FB inheritance from the Python framework flattens cleanly into Studio 5000's AOI model.
Product · Multi-vendor round-trip
Open a working L5X, SimaticML, or TcPOU. Edit it in Python. Export it back. The diff is clean and the structure your team depends on stays intact.
Round-trip is not an export feature. It is a constraint on the entire architecture. Vendor IR mirrors the vendor's schema exactly so nothing rounds off. The Universal IR is a view, not a re-encoding.
One source · three outputs
The output on the right was produced by the current compiler from the file on the left, with no edits. Each tab opens directly in its vendor's IDE.
"""ValveCtrl — the Python the Koyl landing hero shows.
This file is the canonical source for the hero side-by-side. The Koyl website
reads it at build time and renders it as the left-hand panel. The right-hand
panel renders ``valve_ctrl.L5X``, which is regenerated from this file by
``generate.py`` and pinned by CI.
If this file changes, the L5X must be regenerated. CI will fail otherwise.
"""
from datetime import timedelta
from plx.framework import TON, Input, Output, fb, project
@fb
class ValveCtrl:
cmd_open: Input[bool]
feedback: Input[bool]
valve_out: Output[bool]
is_open: Output[bool]
fault: Output[bool]
fault_timer: TON
def logic(self):
self.valve_out = self.cmd_open
if self.cmd_open:
self.is_open = self.feedback
self.fault_timer(IN=not self.feedback, PT=timedelta(seconds=3))
self.fault = self.fault_timer.Q
else:
self.is_open = False
self.fault = False
hero_project = project("HeroDemo", pous=[ValveCtrl])
<?xml version="1.0" ?>
<RSLogix5000Content SchemaRevision="1.0" SoftwareRevision="34.00" TargetName="HeroDemo" TargetType="Controller" ContainsContext="true">
<Controller Use="Target" Name="HeroDemo" MajorRev="1" MinorRev="0" SFCExecutionControl="CurrentActive" SFCRestartPosition="MostRecent" SFCLastScan="DontScan" MatchProjectToController="false" CanUseRPIFromProducer="false" InhibitAutomaticFirmwareUpdate="0" DownloadProjectDocumentationAndExtendedProperties="true" DownloadProjectCustomProperties="true" ReportMinorOverflow="false">
<SafetyInfo/>
<DataTypes/>
<Modules/>
<AddOnInstructionDefinitions>
<AddOnInstructionDefinition Name="ValveCtrl" ExecutePrescan="false" ExecutePostscan="false" ExecuteEnableInFalse="false" SoftwareRevision="34.00" SignatureRunModeProtect="false" IsEncrypted="false">
<Parameters>
<Parameter Name="cmd_open" TagType="Base" DataType="BOOL" Usage="Input" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
<Parameter Name="feedback" TagType="Base" DataType="BOOL" Usage="Input" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
<Parameter Name="valve_out" TagType="Base" DataType="BOOL" Usage="Output" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
<Parameter Name="is_open" TagType="Base" DataType="BOOL" Usage="Output" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
<Parameter Name="fault" TagType="Base" DataType="BOOL" Usage="Output" Required="false" Visible="true" ExternalAccess="Read/Write" Constant="false"/>
</Parameters>
<LocalTags>
<LocalTag Name="fault_timer" DataType="FBD_TIMER" ExternalAccess="Read/Write"/>
</LocalTags>
<Routines>
<Routine Name="Logic" Type="ST">
<STContent>
<Line Number="0"><![CDATA[valve_out := cmd_open;]]></Line>
<Line Number="1"><![CDATA[IF cmd_open THEN]]></Line>
<Line Number="2"><![CDATA[ is_open := feedback;]]></Line>
<Line Number="3"><![CDATA[ fault_timer.PRE := 3000;]]></Line>
<Line Number="4"><![CDATA[ fault_timer.TimerEnable := NOT feedback;]]></Line>
<Line Number="5"><![CDATA[ TONR(fault_timer);]]></Line>
<Line Number="6"><![CDATA[ fault := fault_timer.DN;]]></Line>
<Line Number="7"><![CDATA[ELSE]]></Line>
<Line Number="8"><![CDATA[ is_open := FALSE;]]></Line>
<Line Number="9"><![CDATA[ fault := FALSE;]]></Line>
<Line Number="10"><![CDATA[END_IF;]]></Line>
</STContent>
</Routine>
</Routines>
</AddOnInstructionDefinition>
</AddOnInstructionDefinitions>
<Tags/>
<Programs/>
<Tasks/>
<Trends/>
<DataLogs/>
</Controller>
</RSLogix5000Content> <?xml version="1.0" ?>
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.12">
<POU Name="ValveCtrl" Id="{07bec35d-1c17-41fb-a35b-2840245ff68e}" SpecialFunc="None">
<Declaration><![CDATA[FUNCTION_BLOCK ValveCtrl
VAR_INPUT
cmd_open : BOOL;
feedback : BOOL;
END_VAR
VAR_OUTPUT
valve_out : BOOL;
is_open : BOOL;
fault : BOOL;
END_VAR
VAR
fault_timer : TON;
END_VAR]]></Declaration>
<Implementation>
<ST><![CDATA[valve_out := cmd_open;
IF cmd_open THEN
is_open := feedback;
fault_timer(IN := NOT feedback, PT := T#3s);
fault := fault_timer.DN;
ELSE
is_open := FALSE;
fault := FALSE;
END_IF]]></ST>
</Implementation>
</POU>
</TcPlcObject> <?xml version="1.0" ?>
<Document xmlns:ns0="http://www.siemens.com/automation/Openness/SW/Interface/v5" xmlns:ns1="http://www.siemens.com/automation/Openness/SW/NetworkSource/StructuredText/v3">
<Engineering version="V17"/>
<SW.Blocks.FB ID="0">
<AttributeList>
<Name>ValveCtrl</Name>
<Number>0</Number>
<AutoNumber>true</AutoNumber>
<ProgrammingLanguage>SCL</ProgrammingLanguage>
<MemoryLayout>Optimized</MemoryLayout>
<HeaderVersion>0.1</HeaderVersion>
<Interface>
<ns0:Sections>
<ns0:Section Name="Input">
<ns0:Member Name="cmd_open" Datatype="Bool"/>
<ns0:Member Name="feedback" Datatype="Bool"/>
</ns0:Section>
<ns0:Section Name="Output">
<ns0:Member Name="valve_out" Datatype="Bool"/>
<ns0:Member Name="is_open" Datatype="Bool"/>
<ns0:Member Name="fault" Datatype="Bool"/>
</ns0:Section>
<ns0:Section Name="Static">
<ns0:Member Name="fault_timer" Datatype="TON_TIME"/>
</ns0:Section>
</ns0:Sections>
</Interface>
</AttributeList>
<ObjectList>
<SW.Blocks.CompileUnit ID="1" CompositionName="CompileUnits">
<AttributeList>
<ProgrammingLanguage>SCL</ProgrammingLanguage>
</AttributeList>
<ObjectList>
<ns1:StructuredText ID="2">
<ns1:Access UId="1" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#valve_out"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Access UId="2" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#cmd_open"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Token Text=";"/>
<ns1:NewLine/>
<ns1:Token Text="IF"/>
<ns1:Blank Num="1"/>
<ns1:Access UId="3" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#cmd_open"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text="THEN"/>
<ns1:NewLine/>
<ns1:Blank Num="2"/>
<ns1:Access UId="4" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#is_open"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Access UId="5" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#feedback"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Token Text=";"/>
<ns1:NewLine/>
<ns1:Blank Num="2"/>
<ns1:Access UId="6" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#fault_timer"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Token Text="("/>
<ns1:Access UId="7" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#IN"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Token Text="NOT"/>
<ns1:Blank Num="1"/>
<ns1:Access UId="8" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#feedback"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Token Text=","/>
<ns1:Blank Num="1"/>
<ns1:Access UId="9" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#PT"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Access UId="10" Scope="TypedConstant">
<ns1:Constant>
<ns1:ConstantType>Time</ns1:ConstantType>
<ns1:ConstantValue>T#3s</ns1:ConstantValue>
</ns1:Constant>
</ns1:Access>
<ns1:Token Text=")"/>
<ns1:Token Text=";"/>
<ns1:NewLine/>
<ns1:Blank Num="2"/>
<ns1:Access UId="11" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#fault"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Access UId="12" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#fault_timer"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Token Text="."/>
<ns1:Token Text="DN"/>
<ns1:Token Text=";"/>
<ns1:NewLine/>
<ns1:Token Text="ELSE"/>
<ns1:NewLine/>
<ns1:Blank Num="2"/>
<ns1:Access UId="13" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#is_open"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Access UId="14" Scope="TypedConstant">
<ns1:Constant>
<ns1:ConstantType>Bool</ns1:ConstantType>
<ns1:ConstantValue>FALSE</ns1:ConstantValue>
</ns1:Constant>
</ns1:Access>
<ns1:Token Text=";"/>
<ns1:NewLine/>
<ns1:Blank Num="2"/>
<ns1:Access UId="15" Scope="LocalVariable">
<ns1:Symbol>
<ns1:Component Name="#fault"/>
</ns1:Symbol>
</ns1:Access>
<ns1:Blank Num="1"/>
<ns1:Token Text=":="/>
<ns1:Blank Num="1"/>
<ns1:Access UId="16" Scope="TypedConstant">
<ns1:Constant>
<ns1:ConstantType>Bool</ns1:ConstantType>
<ns1:ConstantValue>FALSE</ns1:ConstantValue>
</ns1:Constant>
</ns1:Access>
<ns1:Token Text=";"/>
<ns1:NewLine/>
<ns1:Token Text="END_IF"/>
<ns1:Token Text=";"/>
<ns1:NewLine/>
</ns1:StructuredText>
</ObjectList>
</SW.Blocks.CompileUnit>
</ObjectList>
</SW.Blocks.FB>
</Document> ● Examples are generated by the live compiler. CI fails if they drift.
Vendor coverage
Allen Bradley
IDE
Studio 5000
Files
.L5X · .ACD (via L5X export)
AOIs and routines round-trip through the Universal IR. FB inheritance from the Python framework flattens cleanly into Studio 5000's AOI model.
Siemens
IDE
TIA Portal
Files
.SimaticML
FCs, FBs, and DBs round-trip through SimaticML. Inheritance from the framework flattens into a single FB on raise; technicians open the result in TIA Portal as native Siemens code.
Beckhoff
IDE
TwinCAT 3 / Visual Studio
Files
.TcPOU · .tsproj
Native EXTENDS / SUPER^() support means FB inheritance from the framework maps directly. Closest one-to-one of the three vendor mappings.
Capability matrix
| Capability | Allen Bradley | Siemens | Beckhoff |
|---|---|---|---|
| Parser (vendor file → vendor IR) | Ships | Ships | Ships |
| Exporter (vendor IR → vendor file) | Ships | Ships | Ships |
| Lower (vendor IR → Universal IR) | Ships | Ships | Ships |
| Raise (Universal IR → vendor IR) | Ships | Ships | Ships |
| Round-trip lossless | Ships | Ships | Ships |
What "lossless" actually means
The vendor IR is a typed Pydantic mirror of the vendor's native schema. Every attribute, every comment, every order-sensitive element survives the parse. When we re-emit, the file lines up byte-for-byte against the parts you didn't change.
The Universal IR is a view on top of the vendor IR: a vendor-agnostic projection used by the framework, the simulator, and the AI agent. The vendor IR remains the source of truth for vendor-specific detail. Round-tripping does not flow through the universal layer; it stays in the vendor IR.
That's what makes Koyl safe to use on the project your business runs on. You can edit one routine and check in the diff. Nothing else changes.
Pilot it on your codebase
Design partners run Koyl against their existing L5X, SimaticML, or TcPOU files and tell us what surprises us both.