nodalis-compiler 1.0.16 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/compilers/ArduinoCompiler.js +446 -0
- package/src/compilers/CPPCompiler.js +178 -55
- package/src/compilers/CodeSysCompiler.js +65 -0
- package/src/compilers/Compiler.js +2 -4
- package/src/compilers/JSCompiler.js +104 -15
- package/src/compilers/iec-parser/parser.js +146 -40
- package/src/compilers/st-parser/expressionConverter.js +218 -2
- package/src/compilers/st-parser/gcctranspiler.js +149 -15
- package/src/compilers/st-parser/jstranspiler.js +145 -10
- package/src/compilers/st-parser/parser.js +64 -47
- package/src/compilers/st-parser/tokenizer.js +38 -6
- package/src/compilers/support/arduino/gpio.cpp +230 -0
- package/src/compilers/support/arduino/gpio.h +42 -0
- package/src/compilers/support/arduino/json.hpp +25526 -0
- package/src/compilers/support/arduino/modbus.cpp +599 -0
- package/src/compilers/support/arduino/modbus.h +155 -0
- package/src/compilers/support/arduino/nodalis.cpp +355 -0
- package/src/compilers/support/arduino/nodalis.h +1242 -0
- package/src/compilers/support/generic/gpio.cpp +429 -0
- package/src/compilers/support/generic/gpio.h +51 -0
- package/src/compilers/support/generic/nodalis.cpp +10 -0
- package/src/compilers/support/generic/nodalis.h +616 -81
- package/src/compilers/support/jint/nodalis/NodalisEngine/NodalisEngine.cs +516 -87
- package/src/compilers/support/jint/nodalis/NodalisEngine/bin/Debug/net8.0/NodalisEngine.deps.json +1595 -27
- package/src/compilers/support/jint/nodalis/NodalisEngine/bin/Debug/net8.0/NodalisEngine.dll +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/bin/Debug/net8.0/NodalisEngine.pdb +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/bin/Debug/net8.0/NodalisEngine.xml +3 -42
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.AssemblyInfo.cs +3 -3
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.AssemblyInfoInputs.cache +1 -1
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.assets.cache +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.csproj.AssemblyReference.cache +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.csproj.CoreCompileInputs.cache +1 -1
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.dll +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.pdb +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.sourcelink.json +1 -1
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/NodalisEngine.xml +3 -42
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/ref/NodalisEngine.dll +0 -0
- package/src/compilers/support/jint/nodalis/NodalisEngine/obj/Debug/net8.0/refint/NodalisEngine.dll +0 -0
- package/src/compilers/support/jint/nodalis/NodalisPLC/Program.cs +5 -4
- package/src/compilers/support/nodejs/IOClient.js +3 -1
- package/src/compilers/support/nodejs/bacnet.js +374 -0
- package/src/compilers/support/nodejs/gpio.js +241 -0
- package/src/compilers/support/nodejs/nodalis.js +219 -133
- package/src/compilers/templates/codesys-default.export +5571 -0
- package/src/nodalis.d.ts +39 -4
- package/src/nodalis.js +75 -11
- package/src/programmers/ArduinoProgrammer.js +71 -0
- package/src/programmers/FileProgrammer.js +205 -0
- package/src/programmers/Programmer.js +5 -6
- package/src/programmers/SSHProgrammer.js +207 -0
- package/src/programmers/utils.js +260 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.17] 2026-02-25
|
|
4
|
+
- Added support for compilation of multiple ST files as a single project.
|
|
5
|
+
- Added support for formal parameters.
|
|
6
|
+
- Added all standard math, logic, comparison, and select functions.
|
|
7
|
+
- Added support for type casting.
|
|
8
|
+
- Tested to PLCOpen reliability standard.
|
|
9
|
+
- Added support for Arduino targets.
|
|
10
|
+
- Added File and SSH programmers.
|
|
11
|
+
|
|
3
12
|
## [1.0.16] - 2026-02-13
|
|
4
13
|
|
|
5
14
|
- Fixed issues with compiling function blocks in jint.
|
package/README.md
CHANGED
|
@@ -170,6 +170,7 @@ Choose the desired format via the CLI `--target`/`--outputType` flags.
|
|
|
170
170
|
| `src/nodalis.js` | CLI entry point and core controller |
|
|
171
171
|
| `src/compilers/CPPCompiler.js` | C++ backend implementation |
|
|
172
172
|
| `src/compilers/JSCompiler.js` | Node.js backend implementation |
|
|
173
|
+
| `src/compilers/ArduinoCompiler.js` | Arduino backend implementation |
|
|
173
174
|
| `test/st/*.js` | Unit tests for compilers |
|
|
174
175
|
| `examples/*.iec` | Example IEC programs |
|
|
175
176
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/* eslint-disable curly */
|
|
2
|
+
/* eslint-disable eqeqeq */
|
|
3
|
+
// Copyright [2025] Nathan Skipper
|
|
4
|
+
//
|
|
5
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
// you may not use this file except in compliance with the License.
|
|
7
|
+
// You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
// See the License for the specific language governing permissions and
|
|
15
|
+
// limitations under the License.
|
|
16
|
+
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { dirname } from 'node:path';
|
|
22
|
+
import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
|
|
23
|
+
import * as iec from './iec-parser/parser.js';
|
|
24
|
+
import { parseStructuredText } from './st-parser/parser.js';
|
|
25
|
+
import { transpile } from './st-parser/gcctranspiler.js';
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ARDUINO_FQBN = {
|
|
31
|
+
'arduino-opta': 'arduino:mbed_opta:opta'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const MODBUS_ARDUINO_CORE_IDS = new Set([
|
|
35
|
+
'arduino:megaavr',
|
|
36
|
+
'arduino:samd',
|
|
37
|
+
'arduino:mbed_nano',
|
|
38
|
+
'arduino:mbed_portenta',
|
|
39
|
+
'arduino:mbed_opta'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const UNSUPPORTED_ARDUINO_MODBUS_FQBNS = new Set([
|
|
43
|
+
'arduino:mbed_portenta:portenta_x8'
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const UNSUPPORTED_ARDUINO_TARGET_ALIASES = {
|
|
47
|
+
'arduino-portenta-x8': 'arduino:mbed_portenta:portenta_x8',
|
|
48
|
+
'arduino-portenta_x8': 'arduino:mbed_portenta:portenta_x8'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let CachedArduinoTargetTable = null;
|
|
52
|
+
|
|
53
|
+
const getExecOutputText = (bufferOrString) => {
|
|
54
|
+
if (typeof bufferOrString === 'string') return bufferOrString.trim();
|
|
55
|
+
if (Buffer.isBuffer(bufferOrString)) return bufferOrString.toString('utf8').trim();
|
|
56
|
+
return '';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export class ArduinoCompiler extends Compiler {
|
|
60
|
+
constructor(options) {
|
|
61
|
+
super(options);
|
|
62
|
+
this.name = 'ArduinoCompiler';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get supportedLanguages() {
|
|
66
|
+
return [IECLanguage.STRUCTURED_TEXT, IECLanguage.LADDER_DIAGRAM];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get supportedOutputTypes() {
|
|
70
|
+
return [OutputType.EXECUTABLE, OutputType.SOURCE_CODE];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get supportedTargetDevices() {
|
|
74
|
+
return this.getArduinoTargetTable().targets;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
canHandleTarget(target) {
|
|
78
|
+
if (typeof target !== 'string') return false;
|
|
79
|
+
if (target.startsWith('arduino:')) return true;
|
|
80
|
+
return this.supportedTargetDevices.includes(target);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get supportedProtocols() {
|
|
84
|
+
return [CommunicationProtocol.MODBUS, CommunicationProtocol.GPIO];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get compilerVersion() {
|
|
88
|
+
return '1.0.0';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async compile() {
|
|
92
|
+
const { sourcePath, outputPath, target, outputType, resourceName, language } = this.options;
|
|
93
|
+
const sourcePathStat = fs.lstatSync(sourcePath);
|
|
94
|
+
const sourceIsDirectory = sourcePathStat.isDirectory();
|
|
95
|
+
const isStructuredTextLanguage = String(language || '').toUpperCase() === IECLanguage.STRUCTURED_TEXT;
|
|
96
|
+
const directoryBundleMode = sourceIsDirectory && isStructuredTextLanguage && typeof resourceName === 'string' && resourceName.trim().length > 0;
|
|
97
|
+
let compilerConfig = {};
|
|
98
|
+
|
|
99
|
+
const sourceDir = sourceIsDirectory ? sourcePath : path.dirname(sourcePath);
|
|
100
|
+
const toolchainConfigPath = path.join(sourceDir, 'toolchain.json');
|
|
101
|
+
if (fs.existsSync(toolchainConfigPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const customToolchain = JSON.parse(fs.readFileSync(toolchainConfigPath, 'utf-8'));
|
|
104
|
+
if (typeof customToolchain !== 'object' || customToolchain === null) {
|
|
105
|
+
throw new Error('The toolchain configuration must be a JSON object.');
|
|
106
|
+
}
|
|
107
|
+
compilerConfig = customToolchain;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw new Error(`Failed to load toolchain configuration from ${toolchainConfigPath}: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sourceName = directoryBundleMode ? resourceName : sourcePath;
|
|
114
|
+
let filename = path.basename(sourceName, path.extname(sourceName));
|
|
115
|
+
let sourceCode = '';
|
|
116
|
+
let bundleEntryProgram = '';
|
|
117
|
+
if (directoryBundleMode) {
|
|
118
|
+
const { combinedSource, entryProgramName } = this.loadStructuredTextBundle(sourcePath, resourceName);
|
|
119
|
+
sourceCode = combinedSource;
|
|
120
|
+
bundleEntryProgram = entryProgramName;
|
|
121
|
+
} else {
|
|
122
|
+
sourceCode = fs.readFileSync(sourcePath, 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
const sketchName = path.basename(path.resolve(outputPath));
|
|
125
|
+
const inoFile = path.join(outputPath, `${sketchName}.ino`);
|
|
126
|
+
const stFile = path.join(outputPath, directoryBundleMode ? 'nodalisplc.st' : `${filename}.st`);
|
|
127
|
+
if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml')) {
|
|
128
|
+
if (typeof resourceName === 'undefined' || resourceName === null || resourceName.length === 0) {
|
|
129
|
+
throw new Error('You must provide the resourceName option for an IEC project file.');
|
|
130
|
+
}
|
|
131
|
+
let stcode = '';
|
|
132
|
+
const iecProj = iec.Project.fromXML(sourceCode);
|
|
133
|
+
iecProj.Instances.Configurations.forEach(
|
|
134
|
+
/**
|
|
135
|
+
* @param {iec.Configuration} c
|
|
136
|
+
*/
|
|
137
|
+
(c) => {
|
|
138
|
+
if (stcode.length > 0) return;
|
|
139
|
+
/**
|
|
140
|
+
* @type {iec.Resource}
|
|
141
|
+
*/
|
|
142
|
+
const res = c.Resources.find(r => r.Name === resourceName);
|
|
143
|
+
if (res) {
|
|
144
|
+
stcode = res.toST();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
if (stcode.length > 0) {
|
|
149
|
+
sourceCode = stcode;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
throw new Error('No resource was found by the name ' + resourceName + ' or the resource could not be parsed.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const parsed = parseStructuredText(sourceCode);
|
|
157
|
+
const transpiledCode = transpile(parsed);
|
|
158
|
+
|
|
159
|
+
let tasks = [];
|
|
160
|
+
let programs = [];
|
|
161
|
+
let globals = [];
|
|
162
|
+
let taskCode = '';
|
|
163
|
+
let mapCode = '';
|
|
164
|
+
|
|
165
|
+
const lines = sourceCode.split('\n');
|
|
166
|
+
lines.forEach((line) => {
|
|
167
|
+
if (line.trim().startsWith('//Task=')) {
|
|
168
|
+
const task = JSON.parse(line.substring(line.indexOf('=') + 1).trim());
|
|
169
|
+
task.Instances = [];
|
|
170
|
+
tasks.push(task);
|
|
171
|
+
}
|
|
172
|
+
else if (line.trim().startsWith('//Instance=')) {
|
|
173
|
+
const instance = JSON.parse(line.substring(line.indexOf('=') + 1).trim());
|
|
174
|
+
const task = tasks.find((t) => t.Name === instance.AssociatedTaskName);
|
|
175
|
+
if (task) {
|
|
176
|
+
task.Instances.push(instance);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (line.trim().startsWith('//Map=')) {
|
|
180
|
+
mapCode += `mapIO("${line.substring(line.indexOf('=') + 1).trim()}");\n`;
|
|
181
|
+
}
|
|
182
|
+
else if (line.indexOf('//Global=') > -1) {
|
|
183
|
+
const global = JSON.parse(line.substring(line.indexOf('=') + 1).trim());
|
|
184
|
+
globals.push(`modbusServer.mapVariable("${global.Name}", "${global.Address}");`);
|
|
185
|
+
}
|
|
186
|
+
else if (line.trim().startsWith('PROGRAM')) {
|
|
187
|
+
let pname = line.trim().substring(line.trim().indexOf(' ') + 1).trim();
|
|
188
|
+
if (pname.includes(' ')) {
|
|
189
|
+
pname = pname.substring(pname.indexOf(' ') + 1);
|
|
190
|
+
}
|
|
191
|
+
if (pname.includes('//')) {
|
|
192
|
+
pname = pname.substring(pname.indexOf('//') + 1);
|
|
193
|
+
}
|
|
194
|
+
if (pname.includes('(*')) {
|
|
195
|
+
pname = pname.substring(pname.indexOf('(*') + 1);
|
|
196
|
+
}
|
|
197
|
+
programs.push(pname);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (tasks.length > 0) {
|
|
202
|
+
tasks.forEach((t) => {
|
|
203
|
+
let progCode = '';
|
|
204
|
+
t.Instances.forEach((i) => {
|
|
205
|
+
progCode += i.TypeName + '();\n';
|
|
206
|
+
});
|
|
207
|
+
taskCode += `\n if(PROGRAM_COUNT % ${t.Interval} == 0){\n ${progCode}\n }\n`;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (directoryBundleMode) {
|
|
211
|
+
taskCode += `\n if(PROGRAM_COUNT % 100 == 0){\n ${bundleEntryProgram}();\n }\n`;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
programs.forEach((p) => {
|
|
215
|
+
taskCode += p + '();\n';
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const inoCode = `#include "nodalis.h"
|
|
220
|
+
#include <stdint.h>
|
|
221
|
+
#include "modbus.h"
|
|
222
|
+
|
|
223
|
+
NodalisModbusTcpServer modbusServer;
|
|
224
|
+
${transpiledCode}
|
|
225
|
+
|
|
226
|
+
void setup() {
|
|
227
|
+
${globals.join('\n')}
|
|
228
|
+
modbusServer.start();
|
|
229
|
+
${mapCode}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
void loop() {
|
|
233
|
+
modbusServer.poll();
|
|
234
|
+
superviseIO();
|
|
235
|
+
${taskCode}
|
|
236
|
+
delay(1);
|
|
237
|
+
PROGRAM_COUNT++;
|
|
238
|
+
if(PROGRAM_COUNT == UINT64_MAX){
|
|
239
|
+
PROGRAM_COUNT = 0;
|
|
240
|
+
}
|
|
241
|
+
}`;
|
|
242
|
+
|
|
243
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
244
|
+
fs.writeFileSync(inoFile, inoCode);
|
|
245
|
+
if (sourcePath.toLowerCase().endsWith('.iec') || sourcePath.toLowerCase().endsWith('.xml') || directoryBundleMode) {
|
|
246
|
+
fs.writeFileSync(stFile, sourceCode);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const supportDir = path.resolve(__dirname + '/support/arduino');
|
|
250
|
+
fs.cpSync(path.join(supportDir, 'nodalis.h'), path.join(outputPath, 'nodalis.h'), { force: true });
|
|
251
|
+
fs.cpSync(path.join(supportDir, 'nodalis.cpp'), path.join(outputPath, 'nodalis.cpp'), { force: true });
|
|
252
|
+
fs.cpSync(path.join(supportDir, 'modbus.h'), path.join(outputPath, 'modbus.h'), { force: true });
|
|
253
|
+
fs.cpSync(path.join(supportDir, 'modbus.cpp'), path.join(outputPath, 'modbus.cpp'), { force: true });
|
|
254
|
+
fs.cpSync(path.join(supportDir, 'gpio.h'), path.join(outputPath, 'gpio.h'), { force: true });
|
|
255
|
+
fs.cpSync(path.join(supportDir, 'gpio.cpp'), path.join(outputPath, 'gpio.cpp'), { force: true });
|
|
256
|
+
fs.cpSync(path.join(supportDir, 'json.hpp'), path.join(outputPath, 'json.hpp'), { force: true });
|
|
257
|
+
|
|
258
|
+
if (outputType === 'executable') {
|
|
259
|
+
const arduinoCli = compilerConfig.arduino_cli || compilerConfig.arduinoCli || 'arduino-cli';
|
|
260
|
+
const arduinoFqbn = this.resolveArduinoFqbn(target, compilerConfig);
|
|
261
|
+
if (!arduinoFqbn) {
|
|
262
|
+
throw new Error(`No Arduino FQBN configured for target "${target}". Use an explicit FQBN target (e.g. arduino:mbed_opta:opta) or add "arduino_fqbn" to toolchain.json.`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.assertArduinoModbusTargetSupported(arduinoFqbn);
|
|
266
|
+
this.ensureArduinoCoreInstalled(arduinoCli, arduinoFqbn);
|
|
267
|
+
|
|
268
|
+
const buildDir = path.join(outputPath, 'build');
|
|
269
|
+
const binDir = path.join(outputPath, 'bin');
|
|
270
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
271
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
272
|
+
const arduinoCompileCmd = `${arduinoCli} compile --fqbn "${arduinoFqbn}" "${outputPath}" --build-path "${buildDir}" --output-dir "${binDir}" --export-binaries`;
|
|
273
|
+
try {
|
|
274
|
+
execSync(arduinoCompileCmd, { stdio: 'pipe' });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
const stderrText = getExecOutputText(err?.stderr);
|
|
277
|
+
const stdoutText = getExecOutputText(err?.stdout);
|
|
278
|
+
const compilerOutput = [stderrText, stdoutText].filter(Boolean).join('\n');
|
|
279
|
+
const details = compilerOutput || err.message;
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Arduino CLI build failed for "${arduinoFqbn}". Verify arduino-cli and board core availability.\n${details}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
loadStructuredTextBundle(sourcePath, resourceName) {
|
|
288
|
+
const stFiles = fs.readdirSync(sourcePath, { withFileTypes: true })
|
|
289
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.st'))
|
|
290
|
+
.map((entry) => entry.name);
|
|
291
|
+
|
|
292
|
+
if (stFiles.length === 0) {
|
|
293
|
+
throw new Error(`No .st files found in source directory "${sourcePath}".`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const normalizedResource = String(resourceName || '').trim();
|
|
297
|
+
const candidateNames = new Set([
|
|
298
|
+
normalizedResource,
|
|
299
|
+
normalizedResource.toLowerCase(),
|
|
300
|
+
normalizedResource.toLowerCase().endsWith('.st') ? normalizedResource.toLowerCase() : `${normalizedResource.toLowerCase()}.st`
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
const entryFile = stFiles.find((file) => {
|
|
304
|
+
const lower = file.toLowerCase();
|
|
305
|
+
return candidateNames.has(file) || candidateNames.has(lower);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!entryFile) {
|
|
309
|
+
throw new Error(`resourceName "${resourceName}" is not an .st file in "${sourcePath}".`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const orderedFiles = [
|
|
313
|
+
...stFiles.filter((file) => file !== entryFile).sort((a, b) => a.localeCompare(b)),
|
|
314
|
+
entryFile
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const combinedSource = orderedFiles
|
|
318
|
+
.map((file) => fs.readFileSync(path.join(sourcePath, file), 'utf-8').trim())
|
|
319
|
+
.filter((text) => text.length > 0)
|
|
320
|
+
.join('\n\n');
|
|
321
|
+
|
|
322
|
+
const entrySource = fs.readFileSync(path.join(sourcePath, entryFile), 'utf-8');
|
|
323
|
+
const entryProgramName = this.extractFirstProgramName(entrySource) || path.basename(entryFile, path.extname(entryFile));
|
|
324
|
+
|
|
325
|
+
return { combinedSource, entryProgramName };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
extractFirstProgramName(sourceCode) {
|
|
329
|
+
const match = String(sourceCode || '').match(/^\s*PROGRAM\s+([A-Za-z_]\w*)/im);
|
|
330
|
+
return match ? match[1] : null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
resolveArduinoFqbn(target, compilerConfig = {}) {
|
|
334
|
+
const explicit = compilerConfig.arduino_fqbn || compilerConfig.arduinoFqbn;
|
|
335
|
+
if (explicit && typeof explicit === 'string' && explicit.includes(':')) {
|
|
336
|
+
return explicit;
|
|
337
|
+
}
|
|
338
|
+
if (typeof target === 'string' && target.startsWith('arduino:')) {
|
|
339
|
+
return target;
|
|
340
|
+
}
|
|
341
|
+
if (typeof target === 'string' && UNSUPPORTED_ARDUINO_TARGET_ALIASES[target]) {
|
|
342
|
+
return UNSUPPORTED_ARDUINO_TARGET_ALIASES[target];
|
|
343
|
+
}
|
|
344
|
+
const table = this.getArduinoTargetTable();
|
|
345
|
+
return table.targetToFqbn[target] || DEFAULT_ARDUINO_FQBN[target] || null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
ensureArduinoCoreInstalled(arduinoCli, arduinoFqbn) {
|
|
349
|
+
const fqbnParts = arduinoFqbn.split(':');
|
|
350
|
+
if (fqbnParts.length < 3) {
|
|
351
|
+
throw new Error(`Invalid Arduino FQBN "${arduinoFqbn}".`);
|
|
352
|
+
}
|
|
353
|
+
const coreId = `${fqbnParts[0]}:${fqbnParts[1]}`;
|
|
354
|
+
|
|
355
|
+
let coreList = '';
|
|
356
|
+
try {
|
|
357
|
+
coreList = execSync(`${arduinoCli} core list`, { encoding: 'utf8' });
|
|
358
|
+
} catch (err) {
|
|
359
|
+
throw new Error(`Failed to query installed Arduino cores using "${arduinoCli}". ${err.message}`);
|
|
360
|
+
}
|
|
361
|
+
if (coreList.includes(coreId)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
execSync(`${arduinoCli} core update-index`, { stdio: 'inherit' });
|
|
367
|
+
execSync(`${arduinoCli} core install ${coreId}`, { stdio: 'inherit' });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
throw new Error(`Failed to install Arduino core "${coreId}" required for ${arduinoFqbn}. ${err.message}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
assertArduinoModbusTargetSupported(arduinoFqbn) {
|
|
374
|
+
if (UNSUPPORTED_ARDUINO_MODBUS_FQBNS.has(arduinoFqbn)) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Arduino target "${arduinoFqbn}" is currently unsupported for Modbus-TCP builds. ` +
|
|
377
|
+
'Use a supported target like "arduino:mbed_portenta:envie_m7" or "arduino:mbed_opta:opta".'
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
getArduinoTargetTable(refresh = false) {
|
|
383
|
+
if (!refresh && CachedArduinoTargetTable) {
|
|
384
|
+
return CachedArduinoTargetTable;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const targetToFqbn = { ...DEFAULT_ARDUINO_FQBN };
|
|
388
|
+
const targets = Object.keys(DEFAULT_ARDUINO_FQBN);
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const boardListRaw = execSync('arduino-cli board listall --format json', { encoding: 'utf8' });
|
|
392
|
+
const boardList = JSON.parse(boardListRaw);
|
|
393
|
+
const boards = Array.isArray(boardList.boards) ? boardList.boards : [];
|
|
394
|
+
|
|
395
|
+
for (const board of boards) {
|
|
396
|
+
const fqbn = board?.fqbn;
|
|
397
|
+
if (!fqbn || typeof fqbn !== 'string') {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const parts = fqbn.split(':');
|
|
401
|
+
if (parts.length < 3) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const coreId = `${parts[0]}:${parts[1]}`;
|
|
405
|
+
if (!MODBUS_ARDUINO_CORE_IDS.has(coreId)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (UNSUPPORTED_ARDUINO_MODBUS_FQBNS.has(fqbn)) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!targetToFqbn[fqbn]) {
|
|
413
|
+
targetToFqbn[fqbn] = fqbn;
|
|
414
|
+
targets.push(fqbn);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const boardId = parts[2];
|
|
418
|
+
const alias = `arduino-${boardId.toLowerCase()}`;
|
|
419
|
+
if (!targetToFqbn[alias]) {
|
|
420
|
+
targetToFqbn[alias] = fqbn;
|
|
421
|
+
targets.push(alias);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const boardName = typeof board?.name === 'string' ? board.name : '';
|
|
425
|
+
if (boardName.length > 0) {
|
|
426
|
+
let nameSlug = boardName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
427
|
+
nameSlug = nameSlug.replace(/^arduino-/, '');
|
|
428
|
+
if (nameSlug.length > 0) {
|
|
429
|
+
const nameAlias = `arduino-${nameSlug}`;
|
|
430
|
+
if (!targetToFqbn[nameAlias]) {
|
|
431
|
+
targetToFqbn[nameAlias] = fqbn;
|
|
432
|
+
targets.push(nameAlias);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
// Keep defaults when arduino-cli is unavailable.
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
CachedArduinoTargetTable = { targetToFqbn, targets };
|
|
442
|
+
return CachedArduinoTargetTable;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export default ArduinoCompiler;
|