nodalis-compiler
Version:
Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.
273 lines (244 loc) • 9.59 kB
JavaScript
/* 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 os from 'os';
import fs from 'fs';
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/gcctranspiler.js';
export class CPPCompiler extends Compiler {
constructor(options) {
super(options);
this.name = 'CPPCompiler';
}
get supportedLanguages() {
return [IECLanguage.STRUCTURED_TEXT, IECLanguage.LADDER_DIAGRAM];
}
get supportedOutputTypes() {
return [OutputType.EXECUTABLE, OutputType.SOURCE_CODE];
}
get supportedTargetDevices() {
return ['linux', 'macos', 'windows'];
}
get supportedProtocols() {
return [CommunicationProtocol.MODBUS];
}
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 cppFile = path.join(outputPath, `${filename}.cpp`);
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 +=
`
if(PROGRAM_COUNT % ${t.Interval} == 0){
${progCode}
}
`;
});
}
else{
programs.forEach((p) => {
taskCode += p + "();\n";
});
}
const cppCode =
`#include "nodalis.h"
#include <chrono>
#include <thread>
#include <cstdint>
#include <limits>
#include "opcua.h"
OPCUAServer opcServer;
${transpiledCode}
int main() {
${globals.join("\n")}
opcServer.start();
${mapCode}
std::cout << "${plcname} is running!\\n";
while (true) {
try{
superviseIO();
${taskCode}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
PROGRAM_COUNT++;
if(PROGRAM_COUNT >= std::numeric_limits<uint64_t>::max()){
PROGRAM_COUNT = 0;
}
}
catch(const std::exception& e){
std::cout << "Caught exception: " << e.what() << "\\n";
}
}
return 0;
}`;
fs.mkdirSync(outputPath, { recursive: true });
fs.writeFileSync(cppFile, cppCode);
if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml")){
fs.writeFileSync(stFile, sourceCode);
}
// Copy core headers and cpp support files
const coreFiles = [
'nodalis.h',
'nodalis.cpp',
'modbus.h',
'modbus.cpp',
"json.hpp",
"opcua.h",
"opcua.cpp",
"open62541.h",
"open62541.c"
];
const coreDir = path.resolve('./src/compilers/support/generic');
for (const file of coreFiles) {
fs.copyFileSync(path.join(coreDir, file), path.join(outputPath, file));
}
const pathTo = name => path.join(outputPath, name);
if (outputType === 'executable') {
let compiler = null;
const isWindows = os.platform() === 'win32';
// Step 1: Detect compilers
try {
execSync('clang++ --version', { stdio: 'ignore' });
compiler = 'clang++';
} catch {
try {
execSync('g++ --version', { stdio: 'ignore' });
compiler = 'g++';
} catch {
try {
execSync('cl.exe /?', { stdio: 'ignore' });
compiler = 'cl.exe';
} catch {
throw new Error('No C++ compiler found (clang++, g++, or cl.exe)');
}
}
}
// Step 2: Compile open62541.c with C compiler
const cCompiler = compiler === 'cl.exe' ? 'cl.exe' : compiler.replace('++', '');
const open62541c = pathTo('open62541.c');
const open62541o = pathTo('open62541.o');
let cCompileCmd;
if (compiler === 'cl.exe') {
// Compile C file with cl
cCompileCmd = `cl.exe /c /TC "${open62541c}" /Fo"${pathTo('open62541.obj')}"`;
} else {
cCompileCmd = `${cCompiler} -std=c11 -D_DEFAULT_SOURCE -D_BSD_SOURCE -c "${open62541c}" -o "${open62541o}"`;
}
execSync(cCompileCmd, { stdio: 'inherit' });
// Step 3: Compile C++ files with C++ compiler and link object
let exeFile = path.join(outputPath, filename);
if (isWindows && !exeFile.endsWith('.exe')) {
exeFile += '.exe';
}
let cppCompileCmd;
if (compiler === 'cl.exe') {
cppCompileCmd = `cl.exe /EHsc /std:c++17 /Fe:"${exeFile}" ` +
`"${cppFile}" "${pathTo('nodalis.cpp')}" "${pathTo('modbus.cpp')}" "${pathTo('opcua.cpp')}" "${pathTo('open62541.obj')}"`;
} else {
cppCompileCmd = `${compiler} -std=c++17 -o "${exeFile}" ` +
`"${cppFile}" "${pathTo('nodalis.cpp')}" "${pathTo('modbus.cpp')}" "${pathTo('opcua.cpp')}" "${open62541o}"`;
}
execSync(cppCompileCmd, { stdio: 'inherit' });
}
}
}
export default CPPCompiler;