Migrate device data source to DESC_IP_MERGED, fix PE/button bindings

- Rewrite generateDeviceManifest() to read DESC_IP sheet from
  PLC Data Generator/{PROJECT}/{MCM}_DESC_IP_MERGED.xlsx
- Rewrite generateProjectManifest() to discover projects from same files
- Key devices-manifest by {PROJECT}_{MCM} to avoid cross-project collisions
- Add fill.paint binding for standalone photoeye path elements
- Add fill.paint binding for button circle sub-elements
- Add s_str_ prefix to path/rect element names for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ilia.gurielidze 2026-04-02 15:47:46 +04:00
parent 2b49454237
commit 981a33a382
7 changed files with 17177 additions and 23073 deletions

View File

@ -23,9 +23,10 @@
} }
loadManifest(); loadManifest();
// Devices for current MCM, filtered out already-placed ones // Devices for current project+MCM, filtered out already-placed ones
let mcmDevices = $derived.by(() => { let mcmDevices = $derived.by(() => {
const list = allDevices[layout.currentMcm] || []; const key = `${layout.currentProject}_${layout.currentMcm}`;
const list = allDevices[key] || [];
const placedLabels = new Set(layout.symbols.map(s => s.label).filter(Boolean)); const placedLabels = new Set(layout.symbols.map(s => s.label).filter(Boolean));
return list.filter(d => !placedLabels.has(d.id)); return list.filter(d => !placedLabels.has(d.id));
}); });
@ -53,7 +54,7 @@
}); });
let totalCount = $derived(mcmDevices.length); let totalCount = $derived(mcmDevices.length);
let totalAll = $derived((allDevices[layout.currentMcm] || []).length); let totalAll = $derived((allDevices[`${layout.currentProject}_${layout.currentMcm}`] || []).length);
function toggleGroup(group: string) { function toggleGroup(group: string) {
const next = new Set(collapsedZones); const next = new Set(collapsedZones);

View File

@ -384,7 +384,8 @@ function svgElementToIgnition(el: Element): Record<string, any> | null {
if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')]; if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')];
} else if (tag === 'rect') { } else if (tag === 'rect') {
obj.type = 'rect'; obj.type = 'rect';
obj.name = el.getAttribute('id') || 'rect'; const rectId = el.getAttribute('id') || 'rect';
obj.name = rectId !== 'rect' ? `s_str_${rectId}` : 'rect';
if (el.getAttribute('id')) obj.id = el.getAttribute('id'); if (el.getAttribute('id')) obj.id = el.getAttribute('id');
for (const attr of ['x', 'y', 'width', 'height', 'rx', 'ry']) { for (const attr of ['x', 'y', 'width', 'height', 'rx', 'ry']) {
if (el.getAttribute(attr)) obj[attr] = el.getAttribute(attr); if (el.getAttribute(attr)) obj[attr] = el.getAttribute(attr);
@ -398,7 +399,8 @@ function svgElementToIgnition(el: Element): Record<string, any> | null {
if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')]; if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')];
} else if (tag === 'path') { } else if (tag === 'path') {
obj.type = 'path'; obj.type = 'path';
obj.name = el.getAttribute('id') || 'path'; const pathId = el.getAttribute('id') || 'path';
obj.name = pathId !== 'path' ? `s_str_${pathId}` : 'path';
if (el.getAttribute('id')) obj.id = el.getAttribute('id'); if (el.getAttribute('id')) obj.id = el.getAttribute('id');
obj.d = el.getAttribute('d') || ''; obj.d = el.getAttribute('d') || '';
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform'); if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');

View File

@ -317,6 +317,7 @@ function generateElementBindings(elements: SvgElement[]): Record<string, any> {
const isDpm = /_DPM\d*/i.test(elName); const isDpm = /_DPM\d*/i.test(elName);
const isMcm = /^MCM\d*$/i.test(elName); const isMcm = /^MCM\d*$/i.test(elName);
const isEpc = /_EPC\d*/i.test(elName); const isEpc = /_EPC\d*/i.test(elName);
const isPe = /_[TLJF]PE\d*$/i.test(elName) || /_BDS\d/i.test(elName) || /_TS\d/i.test(elName);
if (el.elements && el.elements.length > 0) { if (el.elements && el.elements.length > 0) {
if (isEpc) { if (isEpc) {
@ -335,12 +336,14 @@ function generateElementBindings(elements: SvgElement[]): Record<string, any> {
propConfig[`${prefix}.elements[1].fill.paint`] = fillPaintBinding(n); propConfig[`${prefix}.elements[1].fill.paint`] = fillPaintBinding(n);
} }
} else if (isButton) { } else if (isButton) {
// Buttons: NO fill binding on elements[0] (background rect keeps static color) // Buttons: elements[0] is background rect (static), elements[1] is circle (color binding),
// Only text elements get contrast color binding // text elements get contrast color binding
for (let m = 0; m < el.elements.length; m++) { for (let m = 0; m < el.elements.length; m++) {
const sub = el.elements[m]; const sub = el.elements[m];
if (sub.type === 'text') { if (sub.type === 'text') {
propConfig[`${prefix}.elements[${m}].fill.paint`] = textFillBinding(n); propConfig[`${prefix}.elements[${m}].fill.paint`] = textFillBinding(n);
} else if (sub.type === 'circle') {
propConfig[`${prefix}.elements[${m}].fill.paint`] = fillPaintBinding(n);
} }
} }
} else { } else {
@ -354,6 +357,9 @@ function generateElementBindings(elements: SvgElement[]): Record<string, any> {
} }
} }
} }
} else if (isPe) {
// Photoeyes are standalone path elements — bind fill.paint directly
propConfig[`${prefix}.fill.paint`] = fillPaintBinding(n);
} }
// Display filter // Display filter

View File

@ -1,22 +1,13 @@
import type { ProjectInfo, McmInfo } from './types.js'; import type { ProjectInfo, McmInfo } from './types.js';
/** /**
* Scans the static/projectes/ directory structure to discover available projects and MCMs. * Discovers available projects and MCMs from a build-time manifest.
* *
* Expected structure: * Data source: DESC_IP_MERGED.xlsx files from PLC Data Generator/{PROJECT}/
* projectes/{PROJECT}/excel/{PROJECT}_SYSDL_{MCM}*.xlsx * The Vite plugin scans these at build time and writes manifest.json.
* projectes/{PROJECT}/pdf/{PROJECT}_SYSDL_{MCM}*-SYSDL.pdf
*
* Since we're in a static SPA, we use a manifest approach:
* At build time or at runtime, we fetch a directory listing.
* For simplicity, we'll use a manifest file that can be auto-generated.
*/ */
// We'll scan using a known project list fetched from a manifest const MCM_REGEX = /(MCM\d+)/i;
// For the static SPA, we generate a manifest at build time via a vite plugin,
// or we hardcode discovery by trying known paths.
const MCM_REGEX = /SYSDL[_ ]+(MCM\d+)/i;
export async function discoverProjects(): Promise<ProjectInfo[]> { export async function discoverProjects(): Promise<ProjectInfo[]> {
try { try {

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +1,145 @@
[ [
{
"name": "BNA8",
"mcms": [
{
"name": "MCM01",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/BNA8/BNA8_MCM01_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM02",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/BNA8/BNA8_MCM02_DESC_IP_MERGED.xlsx",
"pdfPath": null
}
]
},
{ {
"name": "CDW5", "name": "CDW5",
"mcms": [ "mcms": [
{ {
"name": "MCM01", "name": "MCM01",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM01.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CDW5/CDW5_MCM01_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM02",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM02.xlsx",
"pdfPath": null
},
{
"name": "MCM03",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM03.xlsx",
"pdfPath": null
},
{
"name": "MCM04",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM04.xlsx",
"pdfPath": null
},
{
"name": "MCM05",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM05.xlsx",
"pdfPath": null
},
{
"name": "MCM06",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM06.xlsx",
"pdfPath": null
},
{
"name": "MCM07",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM07.xlsx",
"pdfPath": null
},
{
"name": "MCM08",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM08.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM09", "name": "MCM09",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM09 Non Con PH1.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CDW5/CDW5_MCM09_DESC_IP_MERGED.xlsx",
"pdfPath": "/projectes/CDW5/pdf/CDW5_SYSDL_MCM09 Non Con PH1-SYSDL.pdf" "pdfPath": "/projectes/CDW5/pdf/CDW5_SYSDL_MCM09 Non Con PH1-SYSDL.pdf"
}, },
{
"name": "MCM10",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM10.xlsx",
"pdfPath": null
},
{ {
"name": "MCM11", "name": "MCM11",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM11.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CDW5/CDW5_MCM11_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM12",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM12.xlsx",
"pdfPath": null
},
{
"name": "MCM13",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM13.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM14", "name": "MCM14",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM14.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CDW5/CDW5_MCM14_DESC_IP_MERGED.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM15", "name": "MCM15",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM15.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CDW5/CDW5_MCM15_DESC_IP_MERGED.xlsx",
"pdfPath": null
}
]
},
{
"name": "CNO8",
"mcms": [
{
"name": "MCM01",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CNO8/CNO8_MCM01_DESC_IP_MERGED.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM16", "name": "MCM02",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM16.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CNO8/CNO8_MCM02_DESC_IP_MERGED.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM17", "name": "MCM03",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM17.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CNO8/CNO8_MCM03_DESC_IP_MERGED.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM18", "name": "MCM04",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM18.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CNO8/CNO8_MCM04_DESC_IP_MERGED.xlsx",
"pdfPath": null "pdfPath": null
}, },
{ {
"name": "MCM19", "name": "MCM05",
"excelPath": "/projectes/CDW5/excel/CDW5_SYSDL_MCM19.xlsx", "excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/CNO8/CNO8_MCM05_DESC_IP_MERGED.xlsx",
"pdfPath": null
}
]
},
{
"name": "MTN6",
"mcms": [
{
"name": "MCM01",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM01_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM02",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM02_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM03",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM03_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM04",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM04_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM05",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM05_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM06",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM06_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM07",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/MTN6/MTN6_MCM07_DESC_IP_MERGED.xlsx",
"pdfPath": null
}
]
},
{
"name": "SAT9",
"mcms": [
{
"name": "MCM01",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/SAT9/SAT9_MCM01_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM02",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/SAT9/SAT9_MCM02_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM03",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/SAT9/SAT9_MCM03_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM04",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/SAT9/SAT9_MCM04_DESC_IP_MERGED.xlsx",
"pdfPath": null
},
{
"name": "MCM05",
"excelPath": "/home/iliagurielidze/projects/plc_generation/PLC Data Generator/SAT9/SAT9_MCM05_DESC_IP_MERGED.xlsx",
"pdfPath": null "pdfPath": null
} }
] ]

View File

@ -5,49 +5,73 @@ import path from 'path';
// @ts-ignore — xlsx has no type declarations // @ts-ignore — xlsx has no type declarations
import XLSX from 'xlsx'; import XLSX from 'xlsx';
/** Detect WSL environment and return the correct Ignition base path. */
function getIgnitionBasePath(): string {
const winPath = 'C:/Program Files/Inductive Automation/Ignition/data/projects';
try {
const procVersion = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
if (procVersion.includes('microsoft')) {
return '/mnt/c/Program Files/Inductive Automation/Ignition/data/projects';
}
} catch { /* not Linux / no /proc/version */ }
return winPath;
}
/** Return the PowerShell executable name (WSL needs .exe suffix). */
function getPowerShellCmd(): string {
try {
const procVersion = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
if (procVersion.includes('microsoft')) return 'powershell.exe';
} catch { /* not Linux */ }
return 'powershell';
}
/** Resolve the PLC Data Generator directory (two levels up from svelte-app/) */
function getPlcDataGenDir(): string {
return path.resolve('..', '..', 'PLC Data Generator');
}
/** Regex to extract project and MCM from DESC_IP_MERGED filenames */
const DESC_IP_REGEX = /^([A-Z0-9]+)_(MCM\d+)_DESC_IP_MERGED\.xlsx$/;
function generateProjectManifest() { function generateProjectManifest() {
const staticDir = path.resolve('static'); const staticDir = path.resolve('static');
const projectesDir = path.join(staticDir, 'projectes'); const projectesDir = path.join(staticDir, 'projectes');
if (!fs.existsSync(projectesDir)) fs.mkdirSync(projectesDir, { recursive: true });
if (!fs.existsSync(projectesDir)) return; const plcDataGenDir = getPlcDataGenDir();
if (!fs.existsSync(plcDataGenDir)) return;
const MCM_REGEX = /SYSDL[_ ]+(MCM\d+)/i;
const projects: { name: string; mcms: { name: string; excelPath: string; pdfPath: string | null }[] }[] = []; const projects: { name: string; mcms: { name: string; excelPath: string; pdfPath: string | null }[] }[] = [];
for (const projectName of fs.readdirSync(projectesDir)) { for (const projectName of fs.readdirSync(plcDataGenDir)) {
const projectDir = path.join(projectesDir, projectName); const projectDir = path.join(plcDataGenDir, projectName);
if (!fs.statSync(projectDir).isDirectory()) continue; if (!fs.statSync(projectDir).isDirectory()) continue;
const excelDir = path.join(projectDir, 'excel'); // Find DESC_IP_MERGED files in this project directory
const pdfDir = path.join(projectDir, 'pdf'); const mergedFiles = fs.readdirSync(projectDir).filter((f: string) => DESC_IP_REGEX.test(f));
if (mergedFiles.length === 0) continue;
const mcmMap = new Map<string, { excelPath: string; pdfPath: string | null }>(); const mcmMap = new Map<string, { excelPath: string; pdfPath: string | null }>();
if (fs.existsSync(excelDir)) { for (const f of mergedFiles) {
for (const f of fs.readdirSync(excelDir)) { const match = f.match(DESC_IP_REGEX);
if (!f.endsWith('.xlsx')) continue; if (!match) continue;
const match = f.match(MCM_REGEX); const mcm = match[2];
if (!match) continue; mcmMap.set(mcm, { excelPath: path.join(projectDir, f), pdfPath: null });
const mcm = match[1];
mcmMap.set(mcm, {
excelPath: `/projectes/${projectName}/excel/${f}`,
pdfPath: null,
});
}
} }
// Check for PDFs in static/projectes/{PROJECT}/pdf/ (if they exist)
const pdfDir = path.join(projectesDir, projectName, 'pdf');
if (fs.existsSync(pdfDir)) { if (fs.existsSync(pdfDir)) {
const PDF_MCM_REGEX = /SYSDL[_ ]+(MCM\d+)/i;
for (const f of fs.readdirSync(pdfDir)) { for (const f of fs.readdirSync(pdfDir)) {
if (!f.endsWith('.pdf')) continue; if (!f.endsWith('.pdf')) continue;
const match = f.match(MCM_REGEX); const match = f.match(PDF_MCM_REGEX);
if (!match) continue; if (!match) continue;
const mcm = match[1]; const mcm = match[1];
if (mcmMap.has(mcm)) { if (mcmMap.has(mcm)) {
mcmMap.get(mcm)!.pdfPath = `/projectes/${projectName}/pdf/${f}`; mcmMap.get(mcm)!.pdfPath = `/projectes/${projectName}/pdf/${f}`;
} else {
mcmMap.set(mcm, {
excelPath: '',
pdfPath: `/projectes/${projectName}/pdf/${f}`,
});
} }
} }
} }
@ -114,64 +138,74 @@ function extractZone(dev: string): string {
} }
function generateDeviceManifest() { function generateDeviceManifest() {
const projectesDir = path.resolve('..', 'projectes'); const plcDataGenDir = getPlcDataGenDir();
const staticDir = path.resolve('static', 'projectes'); const staticDir = path.resolve('static', 'projectes');
if (!fs.existsSync(projectesDir)) return; if (!fs.existsSync(plcDataGenDir)) return;
if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
const manifest: Record<string, { id: string; svg: string; zone: string }[]> = {}; const manifest: Record<string, { id: string; svg: string; zone: string }[]> = {};
for (const projectName of fs.readdirSync(projectesDir)) { for (const projectName of fs.readdirSync(plcDataGenDir)) {
const projectDir = path.join(projectesDir, projectName); const projectDir = path.join(plcDataGenDir, projectName);
if (!fs.statSync(projectDir).isDirectory()) continue; if (!fs.statSync(projectDir).isDirectory()) continue;
const excelDir = path.join(projectDir, 'excel');
if (!fs.existsSync(excelDir)) continue;
// Find device IO file and IP addresses file const mergedFiles = fs.readdirSync(projectDir).filter((f: string) => DESC_IP_REGEX.test(f));
const files = fs.readdirSync(excelDir).filter((f: string) => f.endsWith('.xlsx'));
const ioFile = files.find((f: string) => /Devices?\s*IO/i.test(f));
const ipFile = files.find((f: string) => /IP\s*Address/i.test(f));
if (!ioFile) continue;
const ioWb = XLSX.readFile(path.join(excelDir, ioFile)); for (const fileName of mergedFiles) {
const fileMatch = fileName.match(DESC_IP_REGEX);
if (!fileMatch) continue;
const mcm = fileMatch[2]; // e.g. MCM09
// Also load IP addresses for VFDs, FIOMs, DPMs let wb: XLSX.WorkBook;
let ipWb: XLSX.WorkBook | null = null; try { wb = XLSX.readFile(path.join(projectDir, fileName)); } catch { continue; }
if (ipFile) {
try { ipWb = XLSX.readFile(path.join(excelDir, ipFile)); } catch {}
}
for (const sheetName of ioWb.SheetNames) { const ws = wb.Sheets['DESC_IP'];
const mcmMatch = sheetName.match(/^(MCM\d+)$/); if (!ws) continue;
if (!mcmMatch) continue;
const mcm = mcmMatch[1]; const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' }) as string[][];
if (rows.length < 2) continue;
// Column indices: TAGNAME=0, DESCA=2, DPM=6, DEVICE_TYPE=9
const COL_TAGNAME = 0;
const COL_DESCA = 2;
const COL_DPM = 6;
const COL_DEVICE_TYPE = 9;
const seen = new Set<string>(); const seen = new Set<string>();
const devices: { id: string; svg: string; zone: string }[] = []; const devices: { id: string; svg: string; zone: string }[] = [];
// Parse IO sheet
const ws = ioWb.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' }) as string[][];
// Extract FIO/SIO controllers from column 0 (controller name)
for (let i = 1; i < rows.length; i++) { for (let i = 1; i < rows.length; i++) {
const ctrl = String(rows[i][0] || '').trim(); const tagname = String(rows[i][COL_TAGNAME] || '').trim();
if (!ctrl || seen.has(ctrl)) continue; const desca = String(rows[i][COL_DESCA] || '').trim();
if (/FIO|SIO/i.test(ctrl)) { const dpm = String(rows[i][COL_DPM] || '').trim();
seen.add(ctrl); const deviceType = String(rows[i][COL_DEVICE_TYPE] || '').trim();
devices.push({ id: ctrl, svg: 'fio_sio_fioh', zone: extractZone(ctrl) });
// Extract VFDs from TAGNAME where DEVICE_TYPE=APF
if (deviceType === 'APF' && tagname && !seen.has(tagname)) {
seen.add(tagname);
devices.push({ id: tagname, svg: 'conveyor', zone: extractZone(tagname) });
} }
}
// Extract assigned devices from column 3 // Extract FIOMs from TAGNAME where DEVICE_TYPE=FIOM
for (let i = 1; i < rows.length; i++) { if (deviceType === 'FIOM' && tagname && !seen.has(tagname)) {
const dev = String(rows[i][3] || '').trim(); seen.add(tagname);
if (!dev || seen.has(dev)) continue; devices.push({ id: tagname, svg: 'fio_sio_fioh', zone: extractZone(tagname) });
seen.add(dev); }
const svg = classifyDevice(dev); // Extract DPMs from DPM column (only actual DPM names, not part numbers)
if (dpm && /DPM\d*$/i.test(dpm) && !seen.has(dpm)) {
seen.add(dpm);
devices.push({ id: dpm, svg: 'dpm', zone: extractZone(dpm) });
}
// Extract individual devices from DESCA column
if (!desca || desca === 'SPARE' || seen.has(desca)) continue;
seen.add(desca);
const svg = classifyDevice(desca);
if (!svg) continue; if (!svg) continue;
const zone = extractZone(dev); const zone = extractZone(desca);
// Consolidate PDP: only add once per PDP number // Consolidate PDP: only add once per PDP number
if (svg === 'pdp') { if (svg === 'pdp') {
@ -183,38 +217,13 @@ function generateDeviceManifest() {
// Consolidate beacon: BCN1_A + BCN1_H -> one BCN1 per zone // Consolidate beacon: BCN1_A + BCN1_H -> one BCN1 per zone
if (svg === 'beacon') { if (svg === 'beacon') {
const bcnBase = dev.replace(/_[ARHBWG]$/, ''); const bcnBase = desca.replace(/_[ARHBWG]$/, '');
if (devices.some(d => d.id === bcnBase)) continue; if (devices.some(d => d.id === bcnBase)) continue;
devices.push({ id: bcnBase, svg, zone }); devices.push({ id: bcnBase, svg, zone });
continue; continue;
} }
devices.push({ id: dev, svg, zone }); devices.push({ id: desca, svg, zone });
}
// Parse IP/network sheet for VFDs, FIOMs, DPMs
if (ipWb) {
const ipSheets = ipWb.SheetNames.filter((s: string) => s.toUpperCase().includes(mcm));
for (const ipSheet of ipSheets) {
const ipWs = ipWb.Sheets[ipSheet];
const ipRows = XLSX.utils.sheet_to_json(ipWs, { header: 1, defval: '' }) as string[][];
for (const row of ipRows) {
for (const cell of row) {
const val = String(cell || '').trim();
if (!val || val.startsWith('11.') || seen.has(val)) continue;
if (/^[A-Z]+\d*_\d+.*VFD$/.test(val)) {
seen.add(val);
devices.push({ id: val, svg: 'conveyor', zone: extractZone(val) });
} else if (/^[A-Z]+\d*_\d+.*FIOM\d*$/.test(val)) {
seen.add(val);
devices.push({ id: val, svg: 'fio_sio_fioh', zone: extractZone(val) });
} else if (/^[A-Z]+\d*_\d+.*DPM\d*$/.test(val) && !/_P\d+$/.test(val)) {
seen.add(val);
devices.push({ id: val, svg: 'dpm', zone: extractZone(val) });
}
}
}
}
} }
// Always include the MCM symbol itself // Always include the MCM symbol itself
@ -224,7 +233,7 @@ function generateDeviceManifest() {
// Sort by zone then id // Sort by zone then id
devices.sort((a, b) => a.zone.localeCompare(b.zone) || a.id.localeCompare(b.id)); devices.sort((a, b) => a.zone.localeCompare(b.zone) || a.id.localeCompare(b.id));
manifest[mcm] = devices; manifest[`${projectName}_${mcm}`] = devices;
} }
} }
@ -264,7 +273,7 @@ export default defineConfig({
req.on('end', () => { req.on('end', () => {
try { try {
const { projectName, viewName, viewPath, viewJson, resourceJson } = JSON.parse(body); const { projectName, viewName, viewPath, viewJson, resourceJson } = JSON.parse(body);
const ignitionBase = 'C:/Program Files/Inductive Automation/Ignition/data/projects'; const ignitionBase = getIgnitionBasePath();
const viewSubPath = viewPath ? `${viewPath}/${viewName}` : viewName; const viewSubPath = viewPath ? `${viewPath}/${viewName}` : viewName;
const viewDir = path.join(ignitionBase, projectName, 'com.inductiveautomation.perspective/views', viewSubPath); const viewDir = path.join(ignitionBase, projectName, 'com.inductiveautomation.perspective/views', viewSubPath);
fs.mkdirSync(viewDir, { recursive: true }); fs.mkdirSync(viewDir, { recursive: true });
@ -299,7 +308,7 @@ try {
Invoke-WebRequest -Uri 'http://localhost:8088/system/gateway-scripts' -Method POST -Body $body -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 Invoke-WebRequest -Uri 'http://localhost:8088/system/gateway-scripts' -Method POST -Body $body -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5
} catch {} } catch {}
`; `;
try { execSync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { timeout: 10000 }); } catch {} try { execSync(`${getPowerShellCmd()} -Command "${script.replace(/\n/g, ' ')}"`, { timeout: 10000 }); } catch {}
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));
} catch (err: any) { } catch (err: any) {