/**
* L5X RLL Importer — Allen Bradley Studio 5000 / Logix5000 format.
*
* Parses <RLLContent> rungs from an L5X XML file and converts them
* into Lego Ladder Program objects.
*
* Supported mnemonics:
* XIC, XIO → Contact (NO/NC)
* OTE, OTL, OTU → Coil
* TON, TOF, TP, RTO → Timer
* CTU, CTD, CTUD → Counter
* EQU, NEQ, GRT, LES, GEQ, LEQ → Compare
* AND, OR, XOR, NOT, NAND, NOR → LogicGate
* MOV → MoveElement
* ADD, SUB, MUL, DIV → MathElement
*
* Rung text grammar:
* - Sequential ops: space-separated
* - Parallel branches: enclosed in [...] separated by commas
* - Semicolon terminates the rung
*/
import type {
Program, Rung, RungElement, Contact, Coil, Timer, Counter,
LogicGate, CompareElement, MoveElement, MathElement, Branch,
CoilType, TimerType, CounterType, GateType, MathOperator
} from './types';
// ─── L5X instruction mapping ──────────────────────────────────────
const CONTACT_OPS = new Set(['XIC', 'XIO']);
const COIL_OPS = new Set(['OTE', 'OTL', 'OTU', 'SET', 'CLR']);
const TIMER_OPS = new Set(['TON', 'TOF', 'TP', 'RTO']);
const COUNTER_OPS = new Set(['CTU', 'CTD', 'CTUD']);
const COMPARE_OPS = new Set(['EQU', 'NEQ', 'GRT', 'LES', 'GEQ', 'LEQ']);
const GATE_OPS = new Set(['AND', 'OR', 'XOR', 'NOT', 'NAND', 'NOR']);
const MATH_OPS = new Set(['ADD', 'SUB', 'MUL', 'DIV']);
// L5X compare op → Lego Ladder ComparisonOp
const L5X_COMPARE_MAP: Record<string, string> = {
EQU: '==',
NEQ: '!=',
GRT: '>',
LES: '<',
GEQ: '>=',
LEQ: '<=',
};
// ─── Tokenizer ────────────────────────────────────────────────────
interface Token {
op: string;
args: string[];
}
function parseTokens(text: string): Token[] {
text = text.replace(/^\s*<!\[CDATA\[/, '').replace(/\]\]>\s*$/, '').trim();
const tokens: Token[] = [];
let pos = 0;
while (pos < text.length) {
while (pos < text.length && /[,;\s]/.test(text[pos])) pos++;
if (pos >= text.length) break;
if (text[pos] === '[') {
// Parallel branch group
const branches: Token[][] = [];
let depth = 0;
let branchStart = pos + 1;
for (let i = pos; i < text.length; i++) {
if (text[i] === '[') depth++;
else if (text[i] === ']') {
depth--;
if (depth === 0) {
const branchText = text.substring(branchStart, i).trim();
if (branchText) {
const bTokens = parseBranchText(branchText);
if (bTokens.length) branches.push(bTokens);
}
branchStart = i + 1;
}
} else if (text[i] === ',' && depth === 1) {
const branchText = text.substring(branchStart, i).trim();
if (branchText) {
const bTokens = parseBranchText(branchText);
if (bTokens.length) branches.push(bTokens);
}
branchStart = i + 1;
}
}
const lastBranch = text.substring(branchStart, pos).trim();
if (lastBranch) {
const bTokens = parseBranchText(lastBranch);
if (bTokens.length) branches.push(bTokens);
}
pos++;
if (branches.length === 1) {
tokens.push(...branches[0]);
} else if (branches.length > 1) {
// Flatten parallel branches — import all instructions
for (const b of branches) tokens.push(...b);
}
continue;
}
const opMatch = /^([A-Z]+)\s*\(([^)]*)\)/.exec(text.substring(pos));
if (opMatch) {
const op = opMatch[1];
const args = opMatch[2].split(',').map(a => a.trim()).filter(a => a !== '');
tokens.push({ op, args });
pos += opMatch[0].length;
} else {
pos++;
}
}
return tokens;
}
function parseBranchText(text: string): Token[] {
const parts: string[] = [];
let depth = 0;
let current = '';
for (const ch of text) {
if (ch === '(') depth++;
else if (ch === ')') depth--;
if (ch === ' ' && depth === 0) {
if (current.trim()) parts.push(current.trim());
current = '';
} else {
current += ch;
}
}
if (current.trim()) parts.push(current.trim());
const tokens: Token[] = [];
for (const part of parts) {
const m = /^([A-Z]+)\s*\(([^)]*)\)/.exec(part);
if (m) {
const args = m[2].split(',').map(a => a.trim()).filter(a => a !== '');
tokens.push({ op: m[1], args });
}
}
return tokens;
}
// ─── Token → RungElement ─────────────────────────────────────────
function tokenToElement(token: Token): RungElement | null {
const { op, args } = token;
if (CONTACT_OPS.has(op)) {
return {
type: 'contact',
contactType: op === 'XIC' ? 'NO' : 'NC',
address: args[0] || 'I:0/0',
} as Contact;
}
if (COIL_OPS.has(op)) {
let coilType: CoilType = 'OTE';
if (op === 'OTL' || op === 'SET') coilType = 'SET';
else if (op === 'OTU' || op === 'CLR') coilType = 'RESET';
return {
type: 'coil',
coilType,
address: args[0] || 'O:0/0',
} as Coil;
}
if (TIMER_OPS.has(op)) {
return {
type: 'timer',
timerType: op as TimerType,
instanceId: args[0] || 'T4:0',
preset: parseFloat(args[args.length - 1] || '0') * 1000, // L5X seconds → ms
accumulated: 0,
} as Timer;
}
if (COUNTER_OPS.has(op)) {
return {
type: 'counter',
counterType: op as CounterType,
instanceId: args[0] || 'C5:0',
preset: parseInt(args[args.length - 1] || '0', 10),
current: 0,
} as Counter;
}
if (COMPARE_OPS.has(op)) {
return {
type: 'compare',
op: L5X_COMPARE_MAP[op] || '==',
inputA: args[0] || '0',
inputB: args[1] || '0',
outputAddress: args[2] || `CMP_${Date.now()}`,
} as CompareElement;
}
if (GATE_OPS.has(op)) {
return {
type: 'gate',
gateType: op as GateType,
inputs: args.length >= 2 ? args.slice(0, 2) : [args[0] || 'I:0/0'],
outputAddress: args[args.length - 1] || `GATE_${Date.now()}`,
} as LogicGate;
}
if (MATH_OPS.has(op)) {
return {
type: 'math',
operator: op as MathOperator,
inputA: args[0] || '0',
inputB: args[1] || '0',
outputAddress: args[2] || 'N7:0',
} as MathElement;
}
if (op === 'MOV') {
return {
type: 'move',
source: args[0] || '0',
destination: args[1] || 'N7:0',
} as MoveElement;
}
// COP, BTD, and other instructions — skip for now
return null;
}
// ─── Tokens → Rung ───────────────────────────────────────────────
function tokensToRung(tokens: Token[], rungNumber: number): Rung | null {
if (tokens.length === 0) return null;
const elements: RungElement[] = [];
// Find last coil (output element)
let coilIndex = -1;
for (let i = tokens.length - 1; i >= 0; i--) {
if (COIL_OPS.has(tokens[i].op)) {
coilIndex = i;
break;
}
}
// Build series: conditions first, then coil at the end
for (let i = 0; i < tokens.length; i++) {
if (i === coilIndex) continue;
const element = tokenToElement(tokens[i]);
if (element) elements.push(element);
}
// Append coil last
if (coilIndex >= 0) {
const coil = tokenToElement(tokens[coilIndex]);
if (coil) elements.push(coil);
}
if (elements.length === 0) return null;
return {
id: `rung-${rungNumber}`,
label: `Rung ${rungNumber}`,
series: elements,
enabled: true,
};
}
// ─── L5X XML Parsing ─────────────────────────────────────────────
export function parseL5X(l5xContent: string): Program[] {
const programs: Program[] = [];
// Extract RLL routines from <Program> blocks
const programRegex = /<Program[^>]*>([\s\S]*?)<\/Program>/g;
let programMatch;
while ((programMatch = programRegex.exec(l5xContent)) !== null) {
const fullMatch = programMatch[0];
const programBlock = programMatch[1];
const nameMatch = /<Program[^>]*Name="([^"]*)"/.exec(fullMatch);
const programName = nameMatch?.[1] || 'Unknown';
const rungs = parseRLLContent(programBlock);
if (rungs.length > 0) {
programs.push({
name: programName,
rungs,
cycleTime: 100,
});
}
}
// Fallback: parse standalone <Routine> blocks
if (programs.length === 0) {
const routineRegex = /<Routine[^>]*Type="RLL"[^>]*>([\s\S]*?)<\/Routine>/g;
let routineMatch;
while ((routineMatch = routineRegex.exec(l5xContent)) !== null) {
const routineBlock = routineMatch[1];
const fullMatch = routineMatch[0];
const nameMatch = /<Routine[^>]*Name="([^"]*)"/.exec(fullMatch || '');
const routineName = nameMatch?.[1] || 'Imported_Routine';
const rungs = parseRLLContent(routineBlock);
if (rungs.length > 0) {
programs.push({
name: routineName,
rungs,
cycleTime: 100,
});
}
}
}
return programs;
}
function parseRLLContent(block: string): Rung[] {
const rungs: Rung[] = [];
const rllMatch = /<RLLContent>([\s\S]*?)<\/RLLContent>/.exec(block);
if (!rllMatch) return rungs;
// Use a more robust regex that handles the 's' flag (dotall)
const rungRegex = /<Rung\s+Number="(\d+)"[^>]*>([\s\S]*?)<\/Rung>/g;
let rungMatch;
while ((rungMatch = rungRegex.exec(rllMatch[1])) !== null) {
const rungNumber = parseInt(rungMatch[1], 10);
const rungBlock = rungMatch[2];
const textMatch = /<!\[CDATA\[([\s\S]*)\]\]>/.exec(rungBlock);
if (!textMatch) continue;
const text = textMatch[1].trim();
if (!text) continue;
const tokens = parseTokens(text);
if (tokens.length === 0) continue;
const rung = tokensToRung(tokens, rungNumber);
if (rung) rungs.push(rung);
}
return rungs;
}
// ─── Public API ───────────────────────────────────────────────────
export function importL5X(l5xContent: string): Program | null {
const programs = parseL5X(l5xContent);
return programs[0] || null;
}
export function importL5XAll(l5xContent: string): Program[] {
return parseL5X(l5xContent);
}