A complete DevOps toolchain for PLC projects supporting CODESYS, Rockwell/Allen-Bradley, and Siemens TIA Portal / AX platforms. Export, diff, merge, and import with full support for additions, modifications, and deletions.
Problem: CODESYS V3.x GUI PLCOpenXML export/import is broken - exported XML files cannot be re-imported reliably.
Solution: Use the CODESYS Scripting API which:
- ✅ Direct project manipulation (no broken export/import cycle)
- ✅ Full text-based workflow for version control
- ✅ Supports additions, modifications, AND deletions
- ✅ Can run headless for CI/CD
- ✅ Authoritative imports (import directory is source of truth)
# From PLCOpenXML file (no CODESYS needed)
.\codesys-export.ps1 -FromXml "MyProject.xml" -OutputDir ".\export"# Export single L5X file
.\l5x-export.ps1 -Input "Motor_Control.L5X" -OutputDir ".\export"
# Export all L5X files in directory
.\l5x-export.ps1 -Input ".\PLC" -OutputDir ".\export"# Parse a single Siemens ST file
.\siemens-export.ps1 -Input "conveyorControl.st"
# Parse all ST files in a directory
.\siemens-export.ps1 -Input ".\siemens_project"
# Parse and run ontology analysis
.\siemens-export.ps1 -Input ".\siemens_project" -Analyze.\codesys-diff.ps1 -BaseDir ".\export_v1" -TargetDir ".\export_v2" -OutputDir ".\diffs".\codesys-apply.ps1 -DiffDir ".\diffs" -TargetDir ".\export" -OutputDir ".\merged"# Preview changes first
.\codesys-import.ps1 -ProjectPath "MyProject.project" -ImportDir ".\merged" -DryRun
# Apply changes
.\codesys-import.ps1 -ProjectPath "MyProject.project" -ImportDir ".\merged"# One command to apply diffs and import
.\codesys-merge.ps1 -DiffDir ".\diffs" -TargetDir ".\export" -ProjectPath "MyProject.project" -DryRun.
├── codesys-export.ps1 # Driver: Export CODESYS project to text files
├── codesys-diff.ps1 # Driver: Generate diffs between exports
├── codesys-apply.ps1 # Driver: Apply diffs to exports
├── codesys-import.ps1 # Driver: Import text files to CODESYS project
├── codesys-merge.ps1 # Driver: Complete merge workflow
├── l5x-export.ps1 # Driver: Export Rockwell L5X files to text
├── siemens-export.ps1 # Driver: Parse Siemens ST files
├── scripts/ # Python implementation
│ ├── codesys_export.py # Export (runs in CODESYS)
│ ├── codesys_export_from_xml.py # Export from PLCOpenXML
│ ├── codesys_import.py # Import (runs in CODESYS)
│ ├── codesys_import_external.py # Import wrapper (headless)
│ ├── codesys_diff.py # Generate unified diffs
│ ├── codesys_apply.py # Apply diffs to text files
│ ├── l5x_export.py # Export Rockwell L5X files
│ └── siemens_parser.py # Parse Siemens TIA/AX ST files
├── PLC/ # Sample Rockwell L5X files
├── legacy/ # Deprecated XML-based tools
├── tests/ # Test files and outputs
└── docs/ # Documentation
| Extension | Type |
|---|---|
NAME.prg.st |
Program |
NAME.fb.st |
Function Block |
NAME.fun.st |
Function |
NAME.gvl.st |
Global Variable List |
POU_METHOD.meth.st |
Method (in POU) |
| Extension | Type |
|---|---|
NAME.aoi.sc |
Add-On Instruction |
NAME.udt.sc |
User Defined Type |
| Construct | Maps To |
|---|---|
CLASS ... END_CLASS |
Function Block (FB) |
TYPE ... STRUCT ... END_TYPE |
User Defined Type (UDT) |
PROGRAM ... END_PROGRAM |
Program |
CONFIGURATION ... END_CONFIGURATION |
Configuration (tasks, I/O mapping) |
METHOD ... END_METHOD |
Routine (within a CLASS) |
NAMESPACE ... END_NAMESPACE |
Grouping / package |
| Extension | Meaning |
|---|---|
*.diff |
Unified diff (modifications) |
*.added |
New file content |
*.removed |
Marker for deletion |
diff_summary.txt |
Summary statistics |
# Export current state
.\codesys-export.ps1 -FromXml "Project.xml" -OutputDir ".\v1"
# Make changes in CODESYS, export again
.\codesys-export.ps1 -FromXml "Project_modified.xml" -OutputDir ".\v2"
# Generate diff for review/commit
.\codesys-diff.ps1 -BaseDir ".\v1" -TargetDir ".\v2" -OutputDir ".\changes"# You have: diffs from feature branch, current project export
.\codesys-merge.ps1 `
-DiffDir ".\feature_diffs" `
-TargetDir ".\current_export" `
-ProjectPath "MyProject.project" `
-DryRun # Preview first!# Edit GVL.gvl.st in your text editor, then import
.\codesys-import.ps1 -ProjectPath "MyProject.project" -ImportDir ".\modified"# Export all L5X files from PLC directory
.\l5x-export.ps1 -Input ".\PLC" -OutputDir ".\export"
# Commit the .sc files to version control
git add export/
git commit -m "Add PLC logic from Rockwell project"The L5X export tool converts Rockwell Logix 5000 L5X files to structured code (.sc) format for version control and code review.
- Add-On Instructions (AOIs): Complete instruction definitions including parameters, local tags, and ladder logic
- User Defined Types (UDTs): Custom data structures with member definitions
- Ladder Logic: RLL rungs with comments preserved
- Structured Text: ST code blocks (when present)
Each L5X file is exported to a subdirectory containing:
export/
├── Motor_Reversing/
│ ├── Motor_Reversing.aoi.sc # Main AOI
│ ├── HMI_MotorControl.udt.sc # Custom data type
│ ├── Error_Motor.udt.sc # Error structure
│ └── HMIBitEnable.aoi.sc # Dependency AOI
└── IO_DigitalInput/
├── IO_DigitalInput.aoi.sc
└── HMI_DigitalInput.udt.sc
(* AOI: Motor_Reversing *)
(* Type: AddOnInstruction *)
(* Revision: 1.0 *)
(* Vendor: De Clerck Arnaud *)
(* Description: Controls a reversing motor contactor *)
(* PARAMETERS *)
VAR_INPUT
tInTimeout: INT; // Timeout time
bInEstop: BOOL; // Estop
END_VAR
VAR_OUTPUT
bOutCommandForward: BOOL; // Output for motor relay forward
bOutError: BOOL; // Motor Error Exists
END_VAR
(* LOCAL TAGS *)
VAR
bTemp: BOOL;
TON_TimeOut: TIMER;
END_VAR
(* IMPLEMENTATION *)
(* ROUTINE: Logic [RLL] *)
// Rung 0: Inputs
OTU(bTemp);
// Rung 1: Read HMI input buttons
[EQU(mode,2) OTE(bTemp) ,HMIBitEnable(...) ];
- Version Control: Track changes to PLC logic over time
- Code Review: Review ladder logic in pull requests
- Documentation: Self-documenting with comments preserved
- Diffing: Use standard diff tools to compare versions
- Search: Grep through PLC logic to find specific tags/instructions
The Siemens parser reads .st source files (Structured Text exported from TIA Portal or Siemens AX) and extracts them into the same internal data model used by the Rockwell pipeline, so the ontology analyzer works identically for both platforms.
- Classes (Function Blocks):
CLASS ... END_CLASSwith public/private variables and methods - Types (UDTs):
TYPE ... STRUCT ... END_STRUCT END_TYPEwith member definitions - Programs:
PROGRAM ... END_PROGRAMwith external references and inline logic - Configurations:
CONFIGURATION ... END_CONFIGURATIONwith task assignments andVAR_GLOBALI/O mappings (AT %IX / %QX addresses) - Methods: Extracted as routines with local variables and ST logic bodies
- Namespaces: Used as grouping metadata
| Siemens Construct | Internal Direction |
|---|---|
VAR_INPUT |
Input |
VAR_OUTPUT |
Output |
VAR_IN_OUT, VAR_EXTERNAL, VAR_GLOBAL |
InOut |
VAR PUBLIC (in CLASS) |
Output (externally visible) |
VAR PRIVATE (in CLASS) |
Local |
VAR (in PROGRAM/METHOD) |
Local |
# Parse and display all blocks
.\siemens-export.ps1 -Input ".\scripts\siemensprocessing\examplesiemens"
# Run ontology analysis (requires ANTHROPIC_API_KEY)
python scripts/ontology_analyzer.py ".\scripts\siemensprocessing\examplesiemens" --siemens -v
# Parse a single file directly
python scripts/siemens_parser.py "conveyorControl.st"A single .st file can contain multiple blocks (e.g., a TYPE and a CLASS). The parser extracts each as a separate entity for individual analysis, which mirrors how Rockwell exports each AOI/UDT as a separate .sc file.
The import is authoritative - the import directory is the source of truth:
- POUs/GVLs not in import directory → DELETED from project
- Methods not in import directory → DELETED from their POU
- GVL content is fully replaced (variables not in file are removed)
Use --dry-run to preview deletions before applying!
- CODESYS V3.5 SP21 or later
- Python 3.x
- PowerShell 5.1+ (for driver scripts)
- Windows (CODESYS is Windows-only)
# Export from XML
python scripts/codesys_export_from_xml.py "Project.xml" "output_dir"
# Generate diff
python scripts/codesys_diff.py "dir1" "dir2" "diff_output"
# Apply diff
python scripts/codesys_apply.py "diff_dir" "target_dir" "project.project" "output_dir"
# Import (runs CODESYS headless)
python scripts/codesys_import_external.py "Project.project" "import_dir" --dry-run# Export L5X file
python scripts/l5x_export.py "Motor_Control.L5X" "output_dir"
# Export all L5X files in directory
python scripts/l5x_export.py "PLC" "output_dir"# Parse a single ST file
python scripts/siemens_parser.py "conveyorControl.st"
# Parse all ST files in a directory
python scripts/siemens_parser.py "siemens_project/"
# Run ontology analysis on Siemens ST files
python scripts/ontology_analyzer.py "siemens_project/" --siemens -v
# Analyze a single Siemens file (auto-detected by .st extension)
python scripts/ontology_analyzer.py "conveyorControl.st" -v- docs/import_instructions.md - Detailed import instructions