/**
* L5X RLL Exporter — Allen Bradley Studio 5000 / Logix5000 format.
*
* Converts a Lego Ladder Program back into L5X XML with RLL content.
* Produces a valid L5X file that can be imported into Studio 5000.
*/
import type {
Program, Rung, RungElement, Contact, Coil, Timer, Counter,
LogicGate, CompareElement, MoveElement, MathElement,
CoilType
} from './types';
// ─── Type mapping ─────────────────────────────────────────────────
function coilTypeToL5X(coilType: CoilType): string {
switch (coilType) {
case 'OTE':
case 'OUTPUT':
return 'OTE';
case 'SET':
case 'LATCH':
return 'OTL';
case 'RESET':
case 'UNLATCH':
return 'OTU';
case 'TOGGLE':
// No direct L5X equivalent — approximate as OTE
return 'OTE';
default:
return 'OTE';
}
}
// ─── Element → L5X mnemonic ──────────────────────────────────────
function elementToMnemonic(el: RungElement): string {
switch (el.type) {
case 'contact': {
const c = el as Contact;
return c.contactType === 'NO' ? `XIC(${c.address})` : `XIO(${c.address})`;
}
case 'coil': {
const c = el as Coil;
const op = coilTypeToL5X(c.coilType);
return `${op}(${c.address})`;
}
case 'timer': {
const t = el as Timer;
const presetSec = (t.preset / 1000).toFixed(1);
return `${t.timerType}(${t.instanceId},.EN,.TT,${presetSec})`;
}
case 'counter': {
const ct = el as Counter;
return `${ct.counterType}(${ct.instanceId},.CD,.DN,${ct.preset})`;
}
case 'gate': {
const g = el as LogicGate;
const inputs = g.inputs.length >= 2 ? g.inputs.join(',') : g.inputs[0];
return `${g.gateType}(${inputs},${g.outputAddress})`;
}
case 'compare': {
const cmp = el as CompareElement;
const opMap: Record<string, string> = {
'==': 'EQU', '!=': 'NEQ', '>': 'GRT',
'<': 'LES', '>=': 'GEQ', '<=': 'LEQ',
};
const l5xOp = opMap[cmp.op] || 'EQU';
return `${l5xOp}(${cmp.inputA},${cmp.inputB})`;
}
case 'math': {
const m = el as MathElement;
return `${m.operator}(${m.inputA},${m.inputB},${m.outputAddress})`;
}
case 'move': {
const mv = el as MoveElement;
return `MOV(${mv.source},${mv.destination})`;
}
case 'oneshot': {
return `OSR(${(el as any).address})`;
}
case 'scale': {
const s = el as any;
return `SLC(${s.inputAddress},${s.inMin},${s.inMax},${s.outMin},${s.outMax},${s.destination})`;
}
case 'noop':
case 'branch':
default:
return '';
}
}
// ─── Rung → L5X text ─────────────────────────────────────────────
function rungToText(rung: Rung): string {
const parts: string[] = [];
// All elements are in series[] — coil is typically the last element
for (const el of rung.series) {
const mnemonic = elementToMnemonic(el);
if (mnemonic) parts.push(mnemonic);
}
// Semicolon terminates the rung
return parts.join(' ') + ';';
}
// ─── Program → L5X XML ───────────────────────────────────────────
function escapeXml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function programToL5X(program: Program): string {
const lines: string[] = [];
lines.push(`<Program Name="${escapeXml(program.name)}" Reentrant="false" ExecutionPeriod="1" ExecutionPeriodTimeBasis="MS" DisableUpdateOutputs="false">`);
lines.push(` <Routine Name="${escapeXml(program.name)}" Type="RLL">`);
lines.push(` <RLLComment><Text><![CDATA[Exported from Lego Ladder]]></Text></RLLComment>`);
lines.push(` <RLLContent>`);
for (let i = 0; i < program.rungs.length; i++) {
const rung = program.rungs[i];
const text = rungToText(rung);
const rungNum = rung.id.replace('rung-', '') || String(i);
lines.push(` <Rung Number="${rungNum}" Type="N">`);
lines.push(` <Text><![CDATA[${escapeXml(text)}]]></Text>`);
lines.push(` </Rung>`);
}
lines.push(` </RLLContent>`);
lines.push(` </Routine>`);
lines.push(`</Program>`);
return lines.join('\n');
}
// ─── Full L5X document ───────────────────────────────────────────
/**
* Generate a complete L5X XML document from one or more Programs.
* The output can be imported into Studio 5000.
*/
export function exportToL5X(programs: Program[]): string {
const now = new Date();
const timestamp = now.toISOString().replace('T', ' ').substring(0, 19);
const lines: string[] = [];
lines.push(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`);
lines.push(`<RSLogix5000Content SchemaRevision="1.0" SoftwareRevision="32.01" TargetName="LegoLadder_Export" TargetType="Controller" ContainsContext="false" Owner="Lego Ladder" ExportDate="${timestamp}" ExportOptions="NoRawData L5KData DecoratedData">`);
lines.push(` <Controller Use="Target" Name="LegoLadder_Export" ProcessorType="5570-L830E" MajorRev="32" MinorRev="1" ProjectCreationDate="${timestamp}">`);
lines.push(` <RedundancyInfo Enabled="false"/>`);
lines.push(` <Security/>`);
lines.push(` <DataTypes/>`);
lines.push(` <Programs>`);
for (const program of programs) {
lines.push(programToL5X(program));
}
lines.push(` </Programs>`);
lines.push(` <Tasks>`);
lines.push(` <Task Name="MainTask" Type="CONTINUOUS" Priority="1" Watchdog="500">`);
lines.push(` <ScheduledPrograms>`);
for (const program of programs) {
lines.push(` <ScheduledProgram Name="${escapeXml(program.name)}"/>`);
}
lines.push(` </ScheduledPrograms>`);
lines.push(` </Task>`);
lines.push(` </Tasks>`);
lines.push(` </Controller>`);
lines.push(`</RSLogix5000Content>`);
return lines.join('\n');
}
/**
* Export a single program to L5X format.
*/
export function exportProgramToL5X(program: Program): string {
return exportToL5X([program]);
}
/**
* Generate a downloadable .l5x file blob from a program.
*/
export function downloadL5X(program: Program, filename: string = 'program.l5x'): void {
const content = exportProgramToL5X(program);
const blob = new Blob([content], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}