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:
parent
2b49454237
commit
981a33a382
@ -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);
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user