UNPKG

nodalis-compiler

Version:

Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.

294 lines (262 loc) 11 kB
/* eslint-disable curly */ /* eslint-disable eqeqeq */ // Copyright [2025] Nathan Skipper // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { execSync } from 'child_process'; import fs from 'fs'; import os from "os"; import path from "path"; import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js'; import * as iec from "./iec-parser/parser.js"; import { parseStructuredText } from './st-parser/parser.js'; import { transpile } from './st-parser/jstranspiler.js'; import which from "which"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class JSCompiler extends Compiler { constructor(options) { super(options); this.name = 'JSCompiler'; } get supportedLanguages() { return [IECLanguage.STRUCTURED_TEXT, IECLanguage.LADDER_DIAGRAM]; } get supportedOutputTypes() { return [OutputType.SOURCE_CODE, OutputType.EXECUTABLE]; } get supportedTargetDevices() { return ["jint", "nodejs"]; } get supportedProtocols() { return [CommunicationProtocol.MODBUS, CommunicationProtocol.OPC_UA, CommunicationProtocol.BACNET]; } get compilerVersion() { return '1.0.0'; } compile() { const { sourcePath, outputPath, target, outputType, resourceName } = this.options; var sourceCode = fs.readFileSync(sourcePath, 'utf-8'); const filename = path.basename(sourcePath, path.extname(sourcePath)); const jsFile = path.join(outputPath, `${filename}.js`); const stFile = path.join(outputPath, `${filename}.st`); if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){ if(typeof resourceName === "undefined" || resourceName === null || resourceName.length === 0){ throw new Error("You must provide the resourceName option for an IEC project file."); } var stcode = ""; const iecProj = iec.Project.fromXML(sourceCode); iecProj.Instances.Configurations.forEach( /** * @param {iec.Configuration} c */ (c) => { if(stcode.length > 0) return; /** * @type {iec.Resource} */ const res = c.Resources.find(r => r.Name === resourceName); if(res){ stcode = res.toST(); } } ); if(stcode.length > 0){ sourceCode = stcode; } else{ throw new Error("No resource was found by the name " + resourceName + " or the resource could not be parsed."); } } const parsed = parseStructuredText(sourceCode); const transpiledCode = transpile(parsed); let tasks = []; let programs = []; let globals = []; let taskCode = ""; let mapCode = ""; let plcname = "NodalisPLC"; if(typeof resourceName !== "undefined" && resourceName !== null){ plcname = resourceName; } const lines = sourceCode.split("\n"); lines.forEach((line) => { if(line.trim().startsWith("//Task=")){ var task = JSON.parse(line.substring(line.indexOf("=") + 1).trim()); task["Instances"] = []; tasks.push(task); } else if(line.trim().startsWith("//Instance=")){ var instance = JSON.parse(line.substring(line.indexOf("=") + 1).trim()); var task = tasks.find((t) => t.Name === instance.AssociatedTaskName); if(task){ task.Instances.push(instance); } } else if(line.trim().startsWith("//Map=")){ mapCode += `mapIO("${line.substring(line.indexOf("=") + 1).trim()}");\n`; } else if(line.indexOf("//Global=") > -1){ let global = JSON.parse(line.substring(line.indexOf("=") + 1).trim()); globals.push(`opcServer.mapVariable("${global.Name}", "${global.Address}");`) } else if(line.trim().startsWith("PROGRAM")){ var pname = line.trim().substring(line.trim().indexOf(" ") + 1).trim(); if(pname.includes(" ")){ pname = pname.substring(pname.indexOf(" ") + 1); } if(pname.includes("//")){ pname = pname.substring(pname.indexOf("//") + 1); } if(pname.includes("(*")){ pname = pname.substring(pname.indexOf("(*") + 1); } programs.push(pname); } }); if(tasks.length > 0){ tasks.forEach((t) => { var progCode = ""; t.Instances.forEach((i) => { progCode += i.TypeName + "();\n"; }); taskCode += ` ${target === "nodejs" ? `setInterval(() => {` : ""} ${progCode} ${target === "nodejs" ? `}, ${t.Interval});` : ""} `; }); } else{ if(target === "nodejs") taskCode = "setInterval(() => {\n"; programs.forEach((p) => { taskCode += p + "();\n"; }); if(target === "nodejs") taskCode += "}, 100);" } let includes = `import { readBit, writeBit, readByte, writeByte, readWord, writeWord, readDWord, writeDWord, readAddress, writeAddress, getBit, setBit, resolve, newStatic, RefVar, superviseIO, mapIO, createReference, TON, TOF, TP, R_TRIG, F_TRIG, CTU, CTD, CTUD, AND, OR, XOR, NOR, NAND, NOT, ASSIGNMENT, EQ, NE, LT, GT, GE, LE, MOVE, SEL, MUX, MIN, MAX, LIMIT } from "./nodalis.js"; import {OPCServer} from "./opcua.js";`; if(target === "jint"){ includes = ""; } let jsCode = `${includes} ${transpiledCode} ${target === "nodejs" ? `let opcServer = new OPCServer();`: ""} export async function setup(){ ${mapCode} ${target === "nodejs" ? "opcServer.setReadWriteHandlers(readAddress, writeAddress);\n" + `await opcServer.start();\n` + globals.join("\n") : ""} console.log("${plcname} is running!"); } export function run(){ ${target === "nodejs" ? "setInterval(superviseIO, 1);" : ""} ${taskCode} } `; if(target === "nodejs"){ jsCode += "\nsetup();\nrun();"; } if(target === "jint"){ jsCode = jsCode.replaceAll("export ", "").replaceAll("console.log", "log").replaceAll("console.error", "error"); } fs.mkdirSync(outputPath, { recursive: true }); fs.writeFileSync(jsFile, jsCode); if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){ fs.writeFileSync(stFile, sourceCode); } if(target === "nodejs"){ // Copy core headers and cpp support files const coreFiles = [ 'nodalis.js', 'modbus.js', "IOClient.js", "opcua.js" ]; let coreDir = path.resolve('./src/compilers/support/nodejs'); for (const file of coreFiles) { fs.copyFileSync(path.join(coreDir, file), path.join(outputPath, file)); } writePackageJson(outputPath, plcname); installDependencies(outputPath); } if (target === "jint" && outputType === "executable") { const supportDir = path.resolve(__dirname, "support/jint/Nodalis"); const buildScript = os.platform() === "win32" ? "build.bat" : "build.sh"; // 1. Copy all files from support/jint/nodalis to the output directory fs.cpSync(supportDir, outputPath, { recursive: true }); // 2. Run the build script inside the output directory const buildPath = path.resolve(path.join(outputPath, buildScript)); if(buildPath.endsWith(".sh")){ fs.chmodSync(buildPath, 0o755); // make executable } execSync(buildPath, { cwd: path.resolve(outputPath), stdio: "inherit", shell: true }); // 3. Copy the generated JS file to each publish folder const publishRoot = path.join(outputPath, "publish"); const platforms = fs.readdirSync(publishRoot, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => path.join(publishRoot, d.name)); const scriptName = filename + ".js" for (const platformDir of platforms) { const dest = path.join(platformDir, scriptName); fs.copyFileSync(jsFile, dest); // 2. Patch bootstrap.sh if present const shFile = path.join(platformDir, "bootstrap.sh"); if (fs.existsSync(shFile)) { let content = fs.readFileSync(shFile, "utf-8"); content = content.replace("{script}", scriptName); fs.writeFileSync(shFile, content, "utf-8"); fs.chmodSync(shFile, 0o755); // make executable } // 3. Patch bootstrap.bat if present const batFile = path.join(platformDir, "bootstrap.bat"); if (fs.existsSync(batFile)) { let content = fs.readFileSync(batFile, "utf-8"); content = content.replace("{script}", scriptName); fs.writeFileSync(batFile, content, "utf-8"); } } } } } function writePackageJson(outputDir,plcname) { const pkg = { name: "nodalis-" + plcname, version: "1.0.0", type: "module", main: plcname + ".js", dependencies: { "jsmodbus": "^4.0.6", "node-opcua": "^2.156.0" } }; fs.writeFileSync(path.join(outputDir, "package.json"), JSON.stringify(pkg, null, 2)); } function installDependencies(outputDir) { const npmPath = which.sync('npm'); // find actual npm binary console.log(`Running npm from: ${npmPath}`); execSync(`"${npmPath}" install`, { cwd: outputDir, stdio: 'inherit', shell: true }); }