nodalis-compiler 1.0.27 → 1.0.28
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 +4 -0
- package/package.json +1 -1
- package/src/compilers/ArduinoCompiler.js +45 -1
- package/src/compilers/support/arduino/gpio.cpp +1 -0
- package/src/compilers/support/arduino/modbus.cpp +96 -19
- package/src/compilers/support/arduino/modbus.h +1 -1
- package/src/compilers/support/arduino/network_config.cpp +502 -0
- package/src/compilers/support/arduino/network_config.h +20 -0
- package/src/compilers/support/arduino/nodalis.cpp +119 -15
- package/src/compilers/support/arduino/nodalis.h +10 -0
- package/src/compilers/support/generic/gpio.cpp +9 -0
- package/src/compilers/support/generic/gpio.h +1 -0
- package/src/compilers/support/nodejs/gpio.js +3 -0
- package/src/programmers/ArduinoProgrammer.js +37 -1
- package/src/programmers/SSHProgrammer.js +50 -2
- package/src/programmers/utils.js +27 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.28] 2026-03-23
|
|
4
|
+
- Improved support for Arduino, fully tested Arduino compile -> program stack.
|
|
5
|
+
- Added a command line interface for nodalis on Arduino to use in setting IP, read/write of bits, and map info.
|
|
6
|
+
|
|
3
7
|
## [1.0.27] 2026-03-20
|
|
4
8
|
- Corrected issues with SSH Programmer.
|
|
5
9
|
|
package/package.json
CHANGED
|
@@ -219,20 +219,62 @@ export class ArduinoCompiler extends Compiler {
|
|
|
219
219
|
|
|
220
220
|
const inoCode = `#include "nodalis.h"
|
|
221
221
|
#include <stdint.h>
|
|
222
|
+
#include <Ethernet.h>
|
|
222
223
|
#include "modbus.h"
|
|
224
|
+
#include "network_config.h"
|
|
223
225
|
|
|
224
226
|
NodalisModbusTcpServer modbusServer;
|
|
227
|
+
IPAddress localIp;
|
|
228
|
+
|
|
229
|
+
#if defined(LEDR)
|
|
230
|
+
const int NODALIS_HEARTBEAT_LED_PIN = LEDR;
|
|
231
|
+
#elif defined(LED_BUILTIN)
|
|
232
|
+
const int NODALIS_HEARTBEAT_LED_PIN = LED_BUILTIN;
|
|
233
|
+
#else
|
|
234
|
+
const int NODALIS_HEARTBEAT_LED_PIN = -1;
|
|
235
|
+
#endif
|
|
236
|
+
|
|
237
|
+
void nodalisHeartbeatTask() {
|
|
238
|
+
if (NODALIS_HEARTBEAT_LED_PIN < 0) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static uint64_t lastToggle = 0;
|
|
243
|
+
static bool ledState = false;
|
|
244
|
+
const uint64_t now = elapsed();
|
|
245
|
+
if (now - lastToggle < 500) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
lastToggle = now;
|
|
250
|
+
ledState = !ledState;
|
|
251
|
+
digitalWrite(NODALIS_HEARTBEAT_LED_PIN, ledState ? HIGH : LOW);
|
|
252
|
+
}
|
|
225
253
|
${transpiledCode}
|
|
226
254
|
|
|
227
255
|
void setup() {
|
|
256
|
+
Serial.begin(115200);
|
|
257
|
+
nodalisLogInfo("Setup starting");
|
|
258
|
+
if (NODALIS_HEARTBEAT_LED_PIN >= 0) {
|
|
259
|
+
pinMode(NODALIS_HEARTBEAT_LED_PIN, OUTPUT);
|
|
260
|
+
digitalWrite(NODALIS_HEARTBEAT_LED_PIN, LOW);
|
|
261
|
+
}
|
|
262
|
+
localIp = nodalisLoadIpAddress();
|
|
263
|
+
nodalisBeginEthernet(localIp);
|
|
264
|
+
nodalisLogInfo("Ethernet initialized");
|
|
228
265
|
${globals.join('\n')}
|
|
229
|
-
modbusServer.start()
|
|
266
|
+
if (!modbusServer.start()) {
|
|
267
|
+
nodalisLogError("Modbus server start failed");
|
|
268
|
+
}
|
|
230
269
|
${mapCode}
|
|
270
|
+
nodalisLogInfo("Setup complete");
|
|
231
271
|
}
|
|
232
272
|
|
|
233
273
|
void loop() {
|
|
274
|
+
nodalisPollSerialIpConfig(localIp);
|
|
234
275
|
modbusServer.poll();
|
|
235
276
|
superviseIO();
|
|
277
|
+
nodalisHeartbeatTask();
|
|
236
278
|
${taskCode}
|
|
237
279
|
delay(1);
|
|
238
280
|
PROGRAM_COUNT++;
|
|
@@ -254,6 +296,8 @@ void loop() {
|
|
|
254
296
|
fs.cpSync(path.join(supportDir, 'modbus.cpp'), path.join(outputPath, 'modbus.cpp'), { force: true });
|
|
255
297
|
fs.cpSync(path.join(supportDir, 'gpio.h'), path.join(outputPath, 'gpio.h'), { force: true });
|
|
256
298
|
fs.cpSync(path.join(supportDir, 'gpio.cpp'), path.join(outputPath, 'gpio.cpp'), { force: true });
|
|
299
|
+
fs.cpSync(path.join(supportDir, 'network_config.h'), path.join(outputPath, 'network_config.h'), { force: true });
|
|
300
|
+
fs.cpSync(path.join(supportDir, 'network_config.cpp'), path.join(outputPath, 'network_config.cpp'), { force: true });
|
|
257
301
|
fs.cpSync(path.join(supportDir, 'json.hpp'), path.join(outputPath, 'json.hpp'), { force: true });
|
|
258
302
|
|
|
259
303
|
if (this.isExecutableOutput()) {
|
|
@@ -55,7 +55,7 @@ ModbusResponse NodalisModbusServer::handleRequest(const ModbusRequest &request)
|
|
|
55
55
|
NodalisModbusClient::NodalisModbusClient(const std::string &ip, uint16_t port, uint8_t unitId)
|
|
56
56
|
: IOClient("MODBUS-TCP"),
|
|
57
57
|
ip(ip),
|
|
58
|
-
port(port
|
|
58
|
+
port(port),
|
|
59
59
|
modbusTcpClient(tcpClient),
|
|
60
60
|
deviceAddress(unitId == 0 ? 1 : unitId),
|
|
61
61
|
serverResolved(false)
|
|
@@ -63,6 +63,10 @@ NodalisModbusClient::NodalisModbusClient(const std::string &ip, uint16_t port, u
|
|
|
63
63
|
if (!ip.empty())
|
|
64
64
|
{
|
|
65
65
|
serverResolved = parseIp(ip, serverIp);
|
|
66
|
+
if (!serverResolved)
|
|
67
|
+
{
|
|
68
|
+
logErrorThrottled("Invalid remote IP " + ip);
|
|
69
|
+
}
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -109,7 +113,7 @@ void NodalisModbusClient::onMappingAdded(const IOMap &map)
|
|
|
109
113
|
{
|
|
110
114
|
ip = map.moduleID;
|
|
111
115
|
}
|
|
112
|
-
if (
|
|
116
|
+
if (!map.modulePort.empty())
|
|
113
117
|
{
|
|
114
118
|
port = static_cast<uint16_t>(std::atoi(map.modulePort.c_str()));
|
|
115
119
|
}
|
|
@@ -126,6 +130,10 @@ void NodalisModbusClient::connect()
|
|
|
126
130
|
if (!ip.empty() && !serverResolved)
|
|
127
131
|
{
|
|
128
132
|
serverResolved = parseIp(ip, serverIp);
|
|
133
|
+
if (!serverResolved)
|
|
134
|
+
{
|
|
135
|
+
logErrorThrottled("Could not parse target IP " + ip);
|
|
136
|
+
}
|
|
129
137
|
}
|
|
130
138
|
connected = ensureConnected();
|
|
131
139
|
}
|
|
@@ -133,7 +141,7 @@ void NodalisModbusClient::connect()
|
|
|
133
141
|
bool NodalisModbusClient::connectTCP(const std::string &newIp, uint16_t newPort)
|
|
134
142
|
{
|
|
135
143
|
ip = newIp;
|
|
136
|
-
port =
|
|
144
|
+
port = newPort;
|
|
137
145
|
serverResolved = parseIp(ip, serverIp);
|
|
138
146
|
connected = ensureConnected();
|
|
139
147
|
return connected;
|
|
@@ -143,13 +151,20 @@ bool NodalisModbusClient::ensureConnected()
|
|
|
143
151
|
{
|
|
144
152
|
if (!serverResolved)
|
|
145
153
|
{
|
|
154
|
+
logErrorThrottled("Server IP is unresolved");
|
|
146
155
|
return false;
|
|
147
156
|
}
|
|
148
157
|
if (modbusTcpClient.connected())
|
|
149
158
|
{
|
|
150
159
|
return true;
|
|
151
160
|
}
|
|
152
|
-
|
|
161
|
+
const uint16_t effectivePort = (port == 0) ? 502 : port;
|
|
162
|
+
const bool started = modbusTcpClient.begin(serverIp, effectivePort) == 1;
|
|
163
|
+
if (!started)
|
|
164
|
+
{
|
|
165
|
+
logErrorThrottled("TCP connect failed to " + ip + ":" + std::to_string(effectivePort));
|
|
166
|
+
}
|
|
167
|
+
return started;
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
void NodalisModbusClient::disconnect()
|
|
@@ -183,15 +198,16 @@ bool NodalisModbusClient::sendRequest(const ModbusRequest &request, ModbusRespon
|
|
|
183
198
|
return false;
|
|
184
199
|
}
|
|
185
200
|
|
|
186
|
-
|
|
201
|
+
bool NodalisModbusClient::parseRemoteAddress(const std::string &remote, uint16_t &address) const
|
|
187
202
|
{
|
|
188
203
|
char *endPtr = nullptr;
|
|
189
204
|
const long parsed = std::strtol(remote.c_str(), &endPtr, 10);
|
|
190
|
-
if (endPtr == remote.c_str() || parsed < 0)
|
|
205
|
+
if (endPtr == remote.c_str() || *endPtr != '\0' || parsed < 0 || parsed > 65535)
|
|
191
206
|
{
|
|
192
|
-
return
|
|
207
|
+
return false;
|
|
193
208
|
}
|
|
194
|
-
|
|
209
|
+
address = static_cast<uint16_t>(parsed & 0xFFFF);
|
|
210
|
+
return true;
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
bool NodalisModbusClient::readBit(const std::string &remote, int &result)
|
|
@@ -200,10 +216,17 @@ bool NodalisModbusClient::readBit(const std::string &remote, int &result)
|
|
|
200
216
|
{
|
|
201
217
|
return false;
|
|
202
218
|
}
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
uint16_t addr = 0;
|
|
220
|
+
if (!parseRemoteAddress(remote, addr))
|
|
221
|
+
{
|
|
222
|
+
logErrorThrottled("Invalid remote bit address \"" + remote + "\"");
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const long val = modbusTcpClient.discreteInputRead(deviceAddress, addr);
|
|
205
226
|
if (val < 0)
|
|
206
227
|
{
|
|
228
|
+
logErrorThrottled("Discrete input read failed remote=\"" + remote + "\" parsed=" + std::to_string(addr) +
|
|
229
|
+
" unit=" + std::to_string(deviceAddress) + ": " + modbusTcpClient.lastError());
|
|
207
230
|
return false;
|
|
208
231
|
}
|
|
209
232
|
result = (val == 0) ? 0 : 1;
|
|
@@ -216,8 +239,20 @@ bool NodalisModbusClient::writeBit(const std::string &remote, int value)
|
|
|
216
239
|
{
|
|
217
240
|
return false;
|
|
218
241
|
}
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
uint16_t addr = 0;
|
|
243
|
+
if (!parseRemoteAddress(remote, addr))
|
|
244
|
+
{
|
|
245
|
+
logErrorThrottled("Invalid remote bit address \"" + remote + "\"");
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const int rc = modbusTcpClient.coilWrite(deviceAddress, addr, value != 0);
|
|
249
|
+
if (rc != 1)
|
|
250
|
+
{
|
|
251
|
+
logErrorThrottled("Coil write failed remote=\"" + remote + "\" parsed=" + std::to_string(addr) +
|
|
252
|
+
" unit=" + std::to_string(deviceAddress) + ": " + modbusTcpClient.lastError());
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
221
256
|
}
|
|
222
257
|
|
|
223
258
|
bool NodalisModbusClient::readByte(const std::string &remote, uint8_t &result)
|
|
@@ -242,10 +277,17 @@ bool NodalisModbusClient::readWord(const std::string &remote, uint16_t &result)
|
|
|
242
277
|
{
|
|
243
278
|
return false;
|
|
244
279
|
}
|
|
245
|
-
|
|
280
|
+
uint16_t addr = 0;
|
|
281
|
+
if (!parseRemoteAddress(remote, addr))
|
|
282
|
+
{
|
|
283
|
+
logErrorThrottled("Invalid remote register address \"" + remote + "\"");
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
246
286
|
const long val = modbusTcpClient.holdingRegisterRead(deviceAddress, addr);
|
|
247
287
|
if (val < 0)
|
|
248
288
|
{
|
|
289
|
+
logErrorThrottled("Holding register read failed remote=\"" + remote + "\" parsed=" + std::to_string(addr) +
|
|
290
|
+
" unit=" + std::to_string(deviceAddress) + ": " + modbusTcpClient.lastError());
|
|
249
291
|
return false;
|
|
250
292
|
}
|
|
251
293
|
result = static_cast<uint16_t>(val & 0xFFFF);
|
|
@@ -258,15 +300,32 @@ bool NodalisModbusClient::writeWord(const std::string &remote, uint16_t value)
|
|
|
258
300
|
{
|
|
259
301
|
return false;
|
|
260
302
|
}
|
|
261
|
-
|
|
262
|
-
|
|
303
|
+
uint16_t addr = 0;
|
|
304
|
+
if (!parseRemoteAddress(remote, addr))
|
|
305
|
+
{
|
|
306
|
+
logErrorThrottled("Invalid remote register address \"" + remote + "\"");
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
const int rc = modbusTcpClient.holdingRegisterWrite(deviceAddress, addr, value);
|
|
310
|
+
if (rc != 1)
|
|
311
|
+
{
|
|
312
|
+
logErrorThrottled("Holding register write failed remote=\"" + remote + "\" parsed=" + std::to_string(addr) +
|
|
313
|
+
" unit=" + std::to_string(deviceAddress) + ": " + modbusTcpClient.lastError());
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
263
317
|
}
|
|
264
318
|
|
|
265
319
|
bool NodalisModbusClient::readDWord(const std::string &remote, uint32_t &result)
|
|
266
320
|
{
|
|
267
321
|
uint16_t hi = 0;
|
|
268
322
|
uint16_t lo = 0;
|
|
269
|
-
|
|
323
|
+
uint16_t addr = 0;
|
|
324
|
+
if (!parseRemoteAddress(remote, addr))
|
|
325
|
+
{
|
|
326
|
+
logErrorThrottled("Invalid remote register address \"" + remote + "\"");
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
270
329
|
|
|
271
330
|
if (!readWord(std::to_string(addr), hi))
|
|
272
331
|
{
|
|
@@ -283,7 +342,12 @@ bool NodalisModbusClient::readDWord(const std::string &remote, uint32_t &result)
|
|
|
283
342
|
|
|
284
343
|
bool NodalisModbusClient::writeDWord(const std::string &remote, uint32_t value)
|
|
285
344
|
{
|
|
286
|
-
|
|
345
|
+
uint16_t addr = 0;
|
|
346
|
+
if (!parseRemoteAddress(remote, addr))
|
|
347
|
+
{
|
|
348
|
+
logErrorThrottled("Invalid remote register address \"" + remote + "\"");
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
287
351
|
const uint16_t hi = static_cast<uint16_t>((value >> 16) & 0xFFFF);
|
|
288
352
|
const uint16_t lo = static_cast<uint16_t>(value & 0xFFFF);
|
|
289
353
|
|
|
@@ -296,7 +360,12 @@ bool NodalisModbusClient::writeDWord(const std::string &remote, uint32_t value)
|
|
|
296
360
|
|
|
297
361
|
bool NodalisModbusClient::readLWord(const std::string &remote, uint64_t &result)
|
|
298
362
|
{
|
|
299
|
-
|
|
363
|
+
uint16_t addr = 0;
|
|
364
|
+
if (!parseRemoteAddress(remote, addr))
|
|
365
|
+
{
|
|
366
|
+
logErrorThrottled("Invalid remote register address \"" + remote + "\"");
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
300
369
|
uint16_t regs[4] = {0, 0, 0, 0};
|
|
301
370
|
|
|
302
371
|
for (uint16_t i = 0; i < 4; ++i)
|
|
@@ -316,7 +385,12 @@ bool NodalisModbusClient::readLWord(const std::string &remote, uint64_t &result)
|
|
|
316
385
|
|
|
317
386
|
bool NodalisModbusClient::writeLWord(const std::string &remote, uint64_t value)
|
|
318
387
|
{
|
|
319
|
-
|
|
388
|
+
uint16_t addr = 0;
|
|
389
|
+
if (!parseRemoteAddress(remote, addr))
|
|
390
|
+
{
|
|
391
|
+
logErrorThrottled("Invalid remote register address \"" + remote + "\"");
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
320
394
|
const uint16_t regs[4] = {
|
|
321
395
|
static_cast<uint16_t>((value >> 48) & 0xFFFF),
|
|
322
396
|
static_cast<uint16_t>((value >> 32) & 0xFFFF),
|
|
@@ -404,6 +478,7 @@ bool NodalisModbusTcpServer::start(uint8_t serverId)
|
|
|
404
478
|
|
|
405
479
|
if (modbusServer.begin(serverId) != 1)
|
|
406
480
|
{
|
|
481
|
+
nodalisLogError("Failed to start Modbus TCP server");
|
|
407
482
|
return false;
|
|
408
483
|
}
|
|
409
484
|
|
|
@@ -418,6 +493,7 @@ bool NodalisModbusTcpServer::start(uint8_t serverId)
|
|
|
418
493
|
|
|
419
494
|
tcpServer.begin();
|
|
420
495
|
started = true;
|
|
496
|
+
nodalisLogInfo("Modbus TCP server started");
|
|
421
497
|
|
|
422
498
|
for (size_t i = 0; i < globals.size(); ++i)
|
|
423
499
|
{
|
|
@@ -441,6 +517,7 @@ void NodalisModbusTcpServer::poll()
|
|
|
441
517
|
EthernetClient client = tcpServer.available();
|
|
442
518
|
if (client)
|
|
443
519
|
{
|
|
520
|
+
nodalisLogInfo("Accepted Modbus TCP client");
|
|
444
521
|
modbusServer.accept(client);
|
|
445
522
|
}
|
|
446
523
|
modbusServer.poll();
|
|
@@ -108,7 +108,7 @@ private:
|
|
|
108
108
|
uint8_t deviceAddress;
|
|
109
109
|
bool serverResolved;
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
bool parseRemoteAddress(const std::string &remote, uint16_t &address) const;
|
|
112
112
|
uint8_t parseUnitId(const IOMap &map) const;
|
|
113
113
|
bool ensureConnected();
|
|
114
114
|
bool parseIp(const std::string &value, IPAddress &out) const;
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#include "network_config.h"
|
|
2
|
+
#include "nodalis.h"
|
|
3
|
+
|
|
4
|
+
#include <ctype.h>
|
|
5
|
+
#include <stdio.h>
|
|
6
|
+
#include <string.h>
|
|
7
|
+
|
|
8
|
+
#if defined(ARDUINO_ARCH_MBED) && __has_include(<BlockDevice.h>) && __has_include(<MBRBlockDevice.h>) && __has_include(<LittleFileSystem.h>) && __has_include(<FATFileSystem.h>)
|
|
9
|
+
#include <BlockDevice.h>
|
|
10
|
+
#include <MBRBlockDevice.h>
|
|
11
|
+
#include <LittleFileSystem.h>
|
|
12
|
+
#include <FATFileSystem.h>
|
|
13
|
+
#define NODALIS_HAS_OPTA_FS 1
|
|
14
|
+
#else
|
|
15
|
+
#define NODALIS_HAS_OPTA_FS 0
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
#if defined(ARDUINO_ARCH_MBED) && __has_include(<kvstore_global_api.h>)
|
|
19
|
+
#include <kvstore_global_api.h>
|
|
20
|
+
#define NODALIS_HAS_KVSTORE 1
|
|
21
|
+
#else
|
|
22
|
+
#define NODALIS_HAS_KVSTORE 0
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
#if !NODALIS_HAS_KVSTORE && __has_include(<EEPROM.h>)
|
|
26
|
+
#include <EEPROM.h>
|
|
27
|
+
#define NODALIS_HAS_EEPROM 1
|
|
28
|
+
#else
|
|
29
|
+
#define NODALIS_HAS_EEPROM 0
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
namespace
|
|
33
|
+
{
|
|
34
|
+
const uint8_t DEFAULT_MAC[6] = {0x02, 0x4E, 0x4F, 0x44, 0x41, 0x4C};
|
|
35
|
+
const int EEPROM_BASE = 0;
|
|
36
|
+
const uint8_t EEPROM_MAGIC_0 = 0x4E;
|
|
37
|
+
const uint8_t EEPROM_MAGIC_1 = 0x49;
|
|
38
|
+
const uint8_t EEPROM_VERSION = 0x01;
|
|
39
|
+
const size_t SERIAL_BUFFER_LIMIT = 48;
|
|
40
|
+
const char *KVSTORE_IP_KEY = "/kv/nodalis_ip";
|
|
41
|
+
const char *USERDATA_IP_PATH = "/user/nodalis_ip.bin";
|
|
42
|
+
|
|
43
|
+
String serialBuffer;
|
|
44
|
+
|
|
45
|
+
bool nodalisIsBitAddress(const String &address)
|
|
46
|
+
{
|
|
47
|
+
const std::vector<int> parts = parseAddress(std::string(address.c_str()));
|
|
48
|
+
return parts.size() == 4 && parts[0] >= 0 && parts[2] >= 0 && parts[3] >= 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void nodalisPrintBitValue(const String &address)
|
|
52
|
+
{
|
|
53
|
+
Serial.print(address);
|
|
54
|
+
Serial.print(" = ");
|
|
55
|
+
Serial.println(readBit(std::string(address.c_str())) ? 1 : 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
bool nodalisHandleBitCommand(const String &command, const String &upper)
|
|
59
|
+
{
|
|
60
|
+
if (upper == "MAPS")
|
|
61
|
+
{
|
|
62
|
+
nodalisDumpMappings();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (upper.startsWith("READBIT "))
|
|
67
|
+
{
|
|
68
|
+
String address = command.substring(8);
|
|
69
|
+
address.trim();
|
|
70
|
+
if (!nodalisIsBitAddress(address))
|
|
71
|
+
{
|
|
72
|
+
Serial.println("Invalid bit address.");
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
nodalisPrintBitValue(address);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (upper.startsWith("WRITEBIT "))
|
|
80
|
+
{
|
|
81
|
+
const int splitIndex = command.lastIndexOf(' ');
|
|
82
|
+
if (splitIndex <= 8)
|
|
83
|
+
{
|
|
84
|
+
Serial.println("Usage: WRITEBIT %IX0.0 1");
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
String address = command.substring(9, splitIndex);
|
|
89
|
+
String valueText = command.substring(splitIndex + 1);
|
|
90
|
+
address.trim();
|
|
91
|
+
valueText.trim();
|
|
92
|
+
|
|
93
|
+
if (!nodalisIsBitAddress(address) || (valueText != "0" && valueText != "1"))
|
|
94
|
+
{
|
|
95
|
+
Serial.println("Usage: WRITEBIT %IX0.0 1");
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
writeBit(std::string(address.c_str()), valueText == "1");
|
|
100
|
+
nodalisPrintBitValue(address);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
bool nodalisParseIpString(const String &input, IPAddress &ip)
|
|
108
|
+
{
|
|
109
|
+
unsigned int octets[4] = {0, 0, 0, 0};
|
|
110
|
+
int octetIndex = 0;
|
|
111
|
+
unsigned int current = 0;
|
|
112
|
+
bool hasDigit = false;
|
|
113
|
+
|
|
114
|
+
for (size_t i = 0; i < input.length(); ++i)
|
|
115
|
+
{
|
|
116
|
+
const char ch = input.charAt(i);
|
|
117
|
+
if (ch >= '0' && ch <= '9')
|
|
118
|
+
{
|
|
119
|
+
hasDigit = true;
|
|
120
|
+
current = (current * 10U) + static_cast<unsigned int>(ch - '0');
|
|
121
|
+
if (current > 255U)
|
|
122
|
+
{
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (ch != '.' || !hasDigit || octetIndex >= 3)
|
|
129
|
+
{
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
octets[octetIndex++] = current;
|
|
134
|
+
current = 0;
|
|
135
|
+
hasDigit = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!hasDigit || octetIndex != 3)
|
|
139
|
+
{
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
octets[octetIndex] = current;
|
|
144
|
+
ip = IPAddress(octets[0], octets[1], octets[2], octets[3]);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
String nodalisNormalizeCommand(String value)
|
|
149
|
+
{
|
|
150
|
+
value.trim();
|
|
151
|
+
while (value.startsWith(" "))
|
|
152
|
+
{
|
|
153
|
+
value.remove(0, 1);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
uint8_t nodalisChecksum(const IPAddress &ip)
|
|
159
|
+
{
|
|
160
|
+
return static_cast<uint8_t>(ip[0] ^ ip[1] ^ ip[2] ^ ip[3] ^ EEPROM_VERSION);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#if NODALIS_HAS_OPTA_FS || NODALIS_HAS_KVSTORE
|
|
164
|
+
struct PersistentIpRecord
|
|
165
|
+
{
|
|
166
|
+
uint8_t magic0;
|
|
167
|
+
uint8_t magic1;
|
|
168
|
+
uint8_t version;
|
|
169
|
+
uint8_t ip[4];
|
|
170
|
+
uint8_t checksum;
|
|
171
|
+
};
|
|
172
|
+
#endif
|
|
173
|
+
|
|
174
|
+
#if NODALIS_HAS_OPTA_FS
|
|
175
|
+
template <typename FileSystemType>
|
|
176
|
+
bool nodalisReadRecordFromFileSystem(mbed::MBRBlockDevice &userData, FileSystemType &fs, PersistentIpRecord &record)
|
|
177
|
+
{
|
|
178
|
+
if (fs.mount(&userData) != 0)
|
|
179
|
+
{
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
bool success = false;
|
|
184
|
+
FILE *fp = fopen(USERDATA_IP_PATH, "rb");
|
|
185
|
+
if (fp)
|
|
186
|
+
{
|
|
187
|
+
const size_t readCount = fread(&record, sizeof(record), 1, fp);
|
|
188
|
+
fclose(fp);
|
|
189
|
+
success = (readCount == 1);
|
|
190
|
+
}
|
|
191
|
+
fs.unmount();
|
|
192
|
+
return success;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
template <typename FileSystemType>
|
|
196
|
+
bool nodalisWriteRecordToFileSystem(mbed::MBRBlockDevice &userData, FileSystemType &fs, const PersistentIpRecord &record)
|
|
197
|
+
{
|
|
198
|
+
if (fs.mount(&userData) != 0)
|
|
199
|
+
{
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
bool success = false;
|
|
204
|
+
FILE *fp = fopen(USERDATA_IP_PATH, "wb");
|
|
205
|
+
if (fp)
|
|
206
|
+
{
|
|
207
|
+
const size_t writeCount = fwrite(&record, sizeof(record), 1, fp);
|
|
208
|
+
fflush(fp);
|
|
209
|
+
fclose(fp);
|
|
210
|
+
success = (writeCount == 1);
|
|
211
|
+
}
|
|
212
|
+
fs.unmount();
|
|
213
|
+
return success;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
bool nodalisReadStoredIpFromUserData(IPAddress &ip)
|
|
217
|
+
{
|
|
218
|
+
mbed::BlockDevice *root = mbed::BlockDevice::get_default_instance();
|
|
219
|
+
if (!root || root->init() != 0)
|
|
220
|
+
{
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
mbed::MBRBlockDevice userData(root, 4);
|
|
225
|
+
mbed::LittleFileSystem littleFs("user");
|
|
226
|
+
mbed::FATFileSystem fatFs("user");
|
|
227
|
+
PersistentIpRecord record = {};
|
|
228
|
+
|
|
229
|
+
const bool readOk = nodalisReadRecordFromFileSystem(userData, littleFs, record) ||
|
|
230
|
+
nodalisReadRecordFromFileSystem(userData, fatFs, record);
|
|
231
|
+
root->deinit();
|
|
232
|
+
|
|
233
|
+
if (!readOk)
|
|
234
|
+
{
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (record.magic0 != EEPROM_MAGIC_0 || record.magic1 != EEPROM_MAGIC_1 || record.version != EEPROM_VERSION)
|
|
239
|
+
{
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
IPAddress stored(record.ip[0], record.ip[1], record.ip[2], record.ip[3]);
|
|
244
|
+
if (record.checksum != nodalisChecksum(stored))
|
|
245
|
+
{
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
ip = stored;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
bool nodalisWriteStoredIpToUserData(const IPAddress &ip)
|
|
254
|
+
{
|
|
255
|
+
mbed::BlockDevice *root = mbed::BlockDevice::get_default_instance();
|
|
256
|
+
if (!root || root->init() != 0)
|
|
257
|
+
{
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
mbed::MBRBlockDevice userData(root, 4);
|
|
262
|
+
mbed::LittleFileSystem littleFs("user");
|
|
263
|
+
mbed::FATFileSystem fatFs("user");
|
|
264
|
+
const PersistentIpRecord record = {
|
|
265
|
+
EEPROM_MAGIC_0,
|
|
266
|
+
EEPROM_MAGIC_1,
|
|
267
|
+
EEPROM_VERSION,
|
|
268
|
+
{ip[0], ip[1], ip[2], ip[3]},
|
|
269
|
+
nodalisChecksum(ip)
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const bool writeOk = nodalisWriteRecordToFileSystem(userData, littleFs, record) ||
|
|
273
|
+
nodalisWriteRecordToFileSystem(userData, fatFs, record);
|
|
274
|
+
root->deinit();
|
|
275
|
+
return writeOk;
|
|
276
|
+
}
|
|
277
|
+
#endif
|
|
278
|
+
|
|
279
|
+
#if NODALIS_HAS_KVSTORE
|
|
280
|
+
|
|
281
|
+
bool nodalisReadStoredIp(IPAddress &ip)
|
|
282
|
+
{
|
|
283
|
+
#if NODALIS_HAS_OPTA_FS
|
|
284
|
+
if (nodalisReadStoredIpFromUserData(ip))
|
|
285
|
+
{
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
#endif
|
|
289
|
+
PersistentIpRecord record = {};
|
|
290
|
+
size_t actualSize = 0;
|
|
291
|
+
const int readResult = kv_get(KVSTORE_IP_KEY, &record, sizeof(record), &actualSize);
|
|
292
|
+
if (readResult != 0 || actualSize != sizeof(record))
|
|
293
|
+
{
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (record.magic0 != EEPROM_MAGIC_0 || record.magic1 != EEPROM_MAGIC_1 || record.version != EEPROM_VERSION)
|
|
298
|
+
{
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
IPAddress stored(record.ip[0], record.ip[1], record.ip[2], record.ip[3]);
|
|
303
|
+
if (record.checksum != nodalisChecksum(stored))
|
|
304
|
+
{
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
ip = stored;
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
bool nodalisWriteStoredIp(const IPAddress &ip)
|
|
313
|
+
{
|
|
314
|
+
#if NODALIS_HAS_OPTA_FS
|
|
315
|
+
if (nodalisWriteStoredIpToUserData(ip))
|
|
316
|
+
{
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
#endif
|
|
320
|
+
PersistentIpRecord record = {
|
|
321
|
+
EEPROM_MAGIC_0,
|
|
322
|
+
EEPROM_MAGIC_1,
|
|
323
|
+
EEPROM_VERSION,
|
|
324
|
+
{ip[0], ip[1], ip[2], ip[3]},
|
|
325
|
+
nodalisChecksum(ip)
|
|
326
|
+
};
|
|
327
|
+
return kv_set(KVSTORE_IP_KEY, &record, sizeof(record), 0) == 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#elif NODALIS_HAS_EEPROM
|
|
331
|
+
bool nodalisReadStoredIp(IPAddress &ip)
|
|
332
|
+
{
|
|
333
|
+
if (EEPROM.read(EEPROM_BASE) != EEPROM_MAGIC_0 || EEPROM.read(EEPROM_BASE + 1) != EEPROM_MAGIC_1 || EEPROM.read(EEPROM_BASE + 2) != EEPROM_VERSION)
|
|
334
|
+
{
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
IPAddress stored(
|
|
339
|
+
EEPROM.read(EEPROM_BASE + 3),
|
|
340
|
+
EEPROM.read(EEPROM_BASE + 4),
|
|
341
|
+
EEPROM.read(EEPROM_BASE + 5),
|
|
342
|
+
EEPROM.read(EEPROM_BASE + 6));
|
|
343
|
+
|
|
344
|
+
if (EEPROM.read(EEPROM_BASE + 7) != nodalisChecksum(stored))
|
|
345
|
+
{
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
ip = stored;
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
bool nodalisWriteStoredIp(const IPAddress &ip)
|
|
354
|
+
{
|
|
355
|
+
EEPROM.update(EEPROM_BASE, EEPROM_MAGIC_0);
|
|
356
|
+
EEPROM.update(EEPROM_BASE + 1, EEPROM_MAGIC_1);
|
|
357
|
+
EEPROM.update(EEPROM_BASE + 2, EEPROM_VERSION);
|
|
358
|
+
EEPROM.update(EEPROM_BASE + 3, ip[0]);
|
|
359
|
+
EEPROM.update(EEPROM_BASE + 4, ip[1]);
|
|
360
|
+
EEPROM.update(EEPROM_BASE + 5, ip[2]);
|
|
361
|
+
EEPROM.update(EEPROM_BASE + 6, ip[3]);
|
|
362
|
+
EEPROM.update(EEPROM_BASE + 7, nodalisChecksum(ip));
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
#endif
|
|
366
|
+
|
|
367
|
+
void nodalisProcessSerialCommand(String command, IPAddress ¤tIp)
|
|
368
|
+
{
|
|
369
|
+
command = nodalisNormalizeCommand(command);
|
|
370
|
+
if (command.length() == 0)
|
|
371
|
+
{
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
String upper = command;
|
|
376
|
+
upper.toUpperCase();
|
|
377
|
+
|
|
378
|
+
if (nodalisHandleBitCommand(command, upper))
|
|
379
|
+
{
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (upper == "HELP" || upper == "IP HELP")
|
|
384
|
+
{
|
|
385
|
+
nodalisPrintNetworkConfigHelp();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (upper == "IP?" || upper == "SHOW IP")
|
|
390
|
+
{
|
|
391
|
+
nodalisPrintCurrentIp(currentIp);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
String ipText = command;
|
|
396
|
+
if (upper.startsWith("SETIP "))
|
|
397
|
+
{
|
|
398
|
+
ipText = command.substring(6);
|
|
399
|
+
ipText.trim();
|
|
400
|
+
}
|
|
401
|
+
else if (upper.startsWith("IP "))
|
|
402
|
+
{
|
|
403
|
+
ipText = command.substring(3);
|
|
404
|
+
ipText.trim();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
IPAddress newIp;
|
|
408
|
+
if (!nodalisParseIpString(ipText, newIp))
|
|
409
|
+
{
|
|
410
|
+
Serial.println("Invalid IP. Use SETIP a.b.c.d or IP?");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const bool ipChanged = !(newIp == currentIp);
|
|
415
|
+
currentIp = newIp;
|
|
416
|
+
#if NODALIS_HAS_KVSTORE || NODALIS_HAS_EEPROM
|
|
417
|
+
if (nodalisWriteStoredIp(currentIp))
|
|
418
|
+
{
|
|
419
|
+
Serial.println(ipChanged ? "Stored new IP address." : "Stored current IP address.");
|
|
420
|
+
}
|
|
421
|
+
else
|
|
422
|
+
{
|
|
423
|
+
Serial.println("Failed to store IP address.");
|
|
424
|
+
}
|
|
425
|
+
#else
|
|
426
|
+
Serial.println(ipChanged ? "Persistent storage not available. IP will reset on reboot." : "IP address unchanged and persistent storage not available.");
|
|
427
|
+
#endif
|
|
428
|
+
Serial.println(ipChanged ? "Applying IP address..." : "IP address unchanged.");
|
|
429
|
+
nodalisBeginEthernet(currentIp);
|
|
430
|
+
nodalisPrintCurrentIp(currentIp);
|
|
431
|
+
}
|
|
432
|
+
} // namespace
|
|
433
|
+
|
|
434
|
+
IPAddress nodalisDefaultIpAddress()
|
|
435
|
+
{
|
|
436
|
+
return IPAddress(192, 168, 1, 15);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
IPAddress nodalisLoadIpAddress()
|
|
440
|
+
{
|
|
441
|
+
IPAddress ip = nodalisDefaultIpAddress();
|
|
442
|
+
#if NODALIS_HAS_KVSTORE || NODALIS_HAS_EEPROM
|
|
443
|
+
IPAddress stored;
|
|
444
|
+
if (nodalisReadStoredIp(stored))
|
|
445
|
+
{
|
|
446
|
+
ip = stored;
|
|
447
|
+
}
|
|
448
|
+
#endif
|
|
449
|
+
return ip;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
void nodalisBeginEthernet(const IPAddress &ip)
|
|
453
|
+
{
|
|
454
|
+
#if defined(ARDUINO_ARCH_MBED)
|
|
455
|
+
Ethernet.begin(ip);
|
|
456
|
+
#else
|
|
457
|
+
Ethernet.begin(const_cast<uint8_t *>(DEFAULT_MAC), ip);
|
|
458
|
+
#endif
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
void nodalisPrintCurrentIp(const IPAddress &ip)
|
|
462
|
+
{
|
|
463
|
+
Serial.print("Ethernet IP: ");
|
|
464
|
+
Serial.println(ip);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
void nodalisPrintNetworkConfigHelp()
|
|
468
|
+
{
|
|
469
|
+
Serial.println("Serial commands: HELP, IP?, SETIP a.b.c.d, MAPS, READBIT %IX0.0, WRITEBIT %IX0.0 1");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
void nodalisPollSerialIpConfig(IPAddress ¤tIp)
|
|
473
|
+
{
|
|
474
|
+
while (Serial.available() > 0)
|
|
475
|
+
{
|
|
476
|
+
const char ch = static_cast<char>(Serial.read());
|
|
477
|
+
if (ch == '\r')
|
|
478
|
+
{
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (ch == '\n')
|
|
483
|
+
{
|
|
484
|
+
nodalisProcessSerialCommand(serialBuffer, currentIp);
|
|
485
|
+
serialBuffer = "";
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (isPrintable(static_cast<unsigned char>(ch)))
|
|
490
|
+
{
|
|
491
|
+
if (serialBuffer.length() < SERIAL_BUFFER_LIMIT)
|
|
492
|
+
{
|
|
493
|
+
serialBuffer += ch;
|
|
494
|
+
}
|
|
495
|
+
else
|
|
496
|
+
{
|
|
497
|
+
serialBuffer = "";
|
|
498
|
+
Serial.println("Command too long.");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright [2025] Nathan Skipper
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
|
|
6
|
+
#pragma once
|
|
7
|
+
#ifndef NODALIS_NETWORK_CONFIG_H
|
|
8
|
+
#define NODALIS_NETWORK_CONFIG_H
|
|
9
|
+
|
|
10
|
+
#include <Arduino.h>
|
|
11
|
+
#include <Ethernet.h>
|
|
12
|
+
|
|
13
|
+
IPAddress nodalisDefaultIpAddress();
|
|
14
|
+
IPAddress nodalisLoadIpAddress();
|
|
15
|
+
void nodalisBeginEthernet(const IPAddress &ip);
|
|
16
|
+
void nodalisPrintCurrentIp(const IPAddress &ip);
|
|
17
|
+
void nodalisPrintNetworkConfigHelp();
|
|
18
|
+
void nodalisPollSerialIpConfig(IPAddress ¤tIp);
|
|
19
|
+
|
|
20
|
+
#endif
|
|
@@ -11,6 +11,31 @@ uint64_t elapsed()
|
|
|
11
11
|
return static_cast<uint64_t>(millis());
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
bool nodalisSerialReady()
|
|
15
|
+
{
|
|
16
|
+
return static_cast<bool>(Serial);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static void nodalisLog(const char *level, const String &message)
|
|
20
|
+
{
|
|
21
|
+
if (!nodalisSerialReady())
|
|
22
|
+
return;
|
|
23
|
+
Serial.print("[Nodalis][");
|
|
24
|
+
Serial.print(level);
|
|
25
|
+
Serial.print("] ");
|
|
26
|
+
Serial.println(message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void nodalisLogInfo(const String &message)
|
|
30
|
+
{
|
|
31
|
+
nodalisLog("INFO", message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void nodalisLogError(const String &message)
|
|
35
|
+
{
|
|
36
|
+
nodalisLog("ERROR", message);
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
static bool validAddressParts(const std::vector<int> &parts, int expectedWidth, bool allowBit)
|
|
15
40
|
{
|
|
16
41
|
if (parts.size() != 4)
|
|
@@ -196,6 +221,11 @@ void IOClient::addMapping(const IOMap &map)
|
|
|
196
221
|
moduleID = map.moduleID;
|
|
197
222
|
mappings.push_back(map);
|
|
198
223
|
onMappingAdded(mappings.back());
|
|
224
|
+
logInfo("Added map " + describeMapping(mappings.back()));
|
|
225
|
+
}
|
|
226
|
+
else
|
|
227
|
+
{
|
|
228
|
+
logInfo("Skipping duplicate map " + describeMapping(map));
|
|
199
229
|
}
|
|
200
230
|
}
|
|
201
231
|
|
|
@@ -209,9 +239,43 @@ bool IOClient::hasMapping(std::string localAddress)
|
|
|
209
239
|
return false;
|
|
210
240
|
}
|
|
211
241
|
|
|
242
|
+
void IOClient::dumpMappings() const
|
|
243
|
+
{
|
|
244
|
+
nodalisLogInfo(String(protocol.c_str()) + ": mapping dump begin");
|
|
245
|
+
for (size_t i = 0; i < mappings.size(); ++i)
|
|
246
|
+
{
|
|
247
|
+
nodalisLogInfo(String(protocol.c_str()) + ": " + describeMapping(mappings[i]).c_str());
|
|
248
|
+
}
|
|
249
|
+
nodalisLogInfo(String(protocol.c_str()) + ": mapping dump end");
|
|
250
|
+
}
|
|
251
|
+
|
|
212
252
|
const std::string &IOClient::getProtocol() const { return protocol; }
|
|
213
253
|
const std::string &IOClient::getModuleID() const { return moduleID; }
|
|
214
254
|
|
|
255
|
+
void IOClient::logInfo(const std::string &message) const
|
|
256
|
+
{
|
|
257
|
+
nodalisLogInfo(String(protocol.c_str()) + ": " + message.c_str());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
void IOClient::logErrorThrottled(const std::string &message)
|
|
261
|
+
{
|
|
262
|
+
const uint64_t now = elapsed();
|
|
263
|
+
if (now - lastErrorReport < 2000)
|
|
264
|
+
return;
|
|
265
|
+
lastErrorReport = now;
|
|
266
|
+
nodalisLogError(String(protocol.c_str()) + ": " + message.c_str());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
std::string IOClient::describeMapping(const IOMap &map) const
|
|
270
|
+
{
|
|
271
|
+
std::string text = map.localAddress + " <= " + map.remoteAddress + " [" + map.protocol + "]";
|
|
272
|
+
if (!map.moduleID.empty())
|
|
273
|
+
text += " module=" + map.moduleID;
|
|
274
|
+
if (!map.modulePort.empty())
|
|
275
|
+
text += ":" + map.modulePort;
|
|
276
|
+
return text;
|
|
277
|
+
}
|
|
278
|
+
|
|
215
279
|
void IOClient::poll()
|
|
216
280
|
{
|
|
217
281
|
if (!connected)
|
|
@@ -219,7 +283,12 @@ void IOClient::poll()
|
|
|
219
283
|
if (elapsed() - lastAttempt >= 15000)
|
|
220
284
|
{
|
|
221
285
|
lastAttempt = elapsed();
|
|
286
|
+
logInfo("Attempting connection");
|
|
222
287
|
connect();
|
|
288
|
+
if (connected)
|
|
289
|
+
logInfo("Connection ready");
|
|
290
|
+
else
|
|
291
|
+
logErrorThrottled("Connection attempt failed");
|
|
223
292
|
}
|
|
224
293
|
return;
|
|
225
294
|
}
|
|
@@ -232,71 +301,84 @@ void IOClient::poll()
|
|
|
232
301
|
|
|
233
302
|
if (map.direction == IOType::Output)
|
|
234
303
|
{
|
|
304
|
+
bool success = true;
|
|
235
305
|
switch (map.width)
|
|
236
306
|
{
|
|
237
307
|
case 1:
|
|
238
|
-
writeBit(map.remoteAddress, ::readBit(map.localAddress) ? 1 : 0);
|
|
308
|
+
success = writeBit(map.remoteAddress, ::readBit(map.localAddress) ? 1 : 0);
|
|
239
309
|
break;
|
|
240
310
|
case 8:
|
|
241
|
-
writeByte(map.remoteAddress, ::readByte(map.localAddress));
|
|
311
|
+
success = writeByte(map.remoteAddress, ::readByte(map.localAddress));
|
|
242
312
|
break;
|
|
243
313
|
case 16:
|
|
244
|
-
writeWord(map.remoteAddress, ::readWord(map.localAddress));
|
|
314
|
+
success = writeWord(map.remoteAddress, ::readWord(map.localAddress));
|
|
245
315
|
break;
|
|
246
316
|
case 32:
|
|
247
|
-
writeDWord(map.remoteAddress, ::readDWord(map.localAddress));
|
|
317
|
+
success = writeDWord(map.remoteAddress, ::readDWord(map.localAddress));
|
|
248
318
|
break;
|
|
249
319
|
case 64:
|
|
250
|
-
writeLWord(map.remoteAddress, ::readLWord(map.localAddress));
|
|
320
|
+
success = writeLWord(map.remoteAddress, ::readLWord(map.localAddress));
|
|
251
321
|
break;
|
|
252
322
|
default:
|
|
323
|
+
success = false;
|
|
253
324
|
break;
|
|
254
325
|
}
|
|
326
|
+
if (!success)
|
|
327
|
+
logErrorThrottled("Write failed for " + describeMapping(map));
|
|
255
328
|
continue;
|
|
256
329
|
}
|
|
257
330
|
|
|
258
331
|
if (map.direction == IOType::Input)
|
|
259
332
|
{
|
|
333
|
+
bool success = true;
|
|
260
334
|
switch (map.width)
|
|
261
335
|
{
|
|
262
336
|
case 1:
|
|
263
337
|
{
|
|
264
338
|
int val = 0;
|
|
265
|
-
|
|
266
|
-
|
|
339
|
+
success = readBit(map.remoteAddress, val);
|
|
340
|
+
if (success)
|
|
341
|
+
::writeBit(map.localAddress, val != 0);
|
|
267
342
|
break;
|
|
268
343
|
}
|
|
269
344
|
case 8:
|
|
270
345
|
{
|
|
271
346
|
uint8_t val = 0;
|
|
272
|
-
|
|
273
|
-
|
|
347
|
+
success = readByte(map.remoteAddress, val);
|
|
348
|
+
if (success)
|
|
349
|
+
::writeByte(map.localAddress, val);
|
|
274
350
|
break;
|
|
275
351
|
}
|
|
276
352
|
case 16:
|
|
277
353
|
{
|
|
278
354
|
uint16_t val = 0;
|
|
279
|
-
|
|
280
|
-
|
|
355
|
+
success = readWord(map.remoteAddress, val);
|
|
356
|
+
if (success)
|
|
357
|
+
::writeWord(map.localAddress, val);
|
|
281
358
|
break;
|
|
282
359
|
}
|
|
283
360
|
case 32:
|
|
284
361
|
{
|
|
285
362
|
uint32_t val = 0;
|
|
286
|
-
|
|
287
|
-
|
|
363
|
+
success = readDWord(map.remoteAddress, val);
|
|
364
|
+
if (success)
|
|
365
|
+
::writeDWord(map.localAddress, val);
|
|
288
366
|
break;
|
|
289
367
|
}
|
|
290
368
|
case 64:
|
|
291
369
|
{
|
|
292
370
|
uint64_t val = 0;
|
|
293
|
-
|
|
294
|
-
|
|
371
|
+
success = readLWord(map.remoteAddress, val);
|
|
372
|
+
if (success)
|
|
373
|
+
::writeLWord(map.localAddress, val);
|
|
295
374
|
break;
|
|
296
375
|
}
|
|
297
376
|
default:
|
|
377
|
+
success = false;
|
|
298
378
|
break;
|
|
299
379
|
}
|
|
380
|
+
if (!success)
|
|
381
|
+
logErrorThrottled("Read failed for " + describeMapping(map));
|
|
300
382
|
}
|
|
301
383
|
}
|
|
302
384
|
}
|
|
@@ -337,12 +419,20 @@ std::unique_ptr<IOClient> createClient(IOMap &map)
|
|
|
337
419
|
void mapIO(std::string map)
|
|
338
420
|
{
|
|
339
421
|
IOMap newMap(map);
|
|
422
|
+
nodalisLogInfo(String("Loading map: ") + newMap.localAddress.c_str() + " <= " + newMap.remoteAddress.c_str() + " [" + newMap.protocol.c_str() + "]");
|
|
340
423
|
IOClient *existing = findClient(newMap);
|
|
341
424
|
if (!existing)
|
|
342
425
|
{
|
|
343
426
|
std::unique_ptr<IOClient> client = createClient(newMap);
|
|
344
427
|
if (client)
|
|
428
|
+
{
|
|
429
|
+
nodalisLogInfo(String("Created IO client for protocol ") + newMap.protocol.c_str());
|
|
345
430
|
Clients.push_back(std::move(client));
|
|
431
|
+
}
|
|
432
|
+
else
|
|
433
|
+
{
|
|
434
|
+
nodalisLogError(String("Unsupported IO protocol: ") + newMap.protocol.c_str());
|
|
435
|
+
}
|
|
346
436
|
}
|
|
347
437
|
}
|
|
348
438
|
|
|
@@ -353,3 +443,17 @@ void superviseIO()
|
|
|
353
443
|
Clients[i]->poll();
|
|
354
444
|
}
|
|
355
445
|
}
|
|
446
|
+
|
|
447
|
+
void nodalisDumpMappings()
|
|
448
|
+
{
|
|
449
|
+
if (Clients.empty())
|
|
450
|
+
{
|
|
451
|
+
nodalisLogInfo("No IO clients loaded");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (size_t i = 0; i < Clients.size(); ++i)
|
|
456
|
+
{
|
|
457
|
+
Clients[i]->dumpMappings();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
@@ -46,6 +46,9 @@ extern uint64_t PROGRAM_COUNT;
|
|
|
46
46
|
extern uint64_t MEMORY[64][16];
|
|
47
47
|
|
|
48
48
|
uint64_t elapsed();
|
|
49
|
+
bool nodalisSerialReady();
|
|
50
|
+
void nodalisLogInfo(const String &message);
|
|
51
|
+
void nodalisLogError(const String &message);
|
|
49
52
|
|
|
50
53
|
inline std::string toLowerCase(const std::string &input)
|
|
51
54
|
{
|
|
@@ -264,6 +267,7 @@ void setBit(RefVar<T> &var, int bit, bool value)
|
|
|
264
267
|
}
|
|
265
268
|
|
|
266
269
|
void superviseIO();
|
|
270
|
+
void nodalisDumpMappings();
|
|
267
271
|
|
|
268
272
|
enum class IOType
|
|
269
273
|
{
|
|
@@ -300,6 +304,7 @@ public:
|
|
|
300
304
|
void addMapping(const IOMap &map);
|
|
301
305
|
bool hasMapping(std::string localAddress);
|
|
302
306
|
void poll();
|
|
307
|
+
void dumpMappings() const;
|
|
303
308
|
|
|
304
309
|
const std::string &getProtocol() const;
|
|
305
310
|
const std::string &getModuleID() const;
|
|
@@ -309,6 +314,7 @@ protected:
|
|
|
309
314
|
std::string moduleID;
|
|
310
315
|
std::vector<IOMap> mappings;
|
|
311
316
|
uint64_t lastAttempt = 0;
|
|
317
|
+
uint64_t lastErrorReport = 0;
|
|
312
318
|
|
|
313
319
|
virtual bool readBit(const std::string &remote, int &result) = 0;
|
|
314
320
|
virtual bool writeBit(const std::string &remote, int value) = 0;
|
|
@@ -325,6 +331,10 @@ protected:
|
|
|
325
331
|
{
|
|
326
332
|
(void)map;
|
|
327
333
|
}
|
|
334
|
+
|
|
335
|
+
void logInfo(const std::string &message) const;
|
|
336
|
+
void logErrorThrottled(const std::string &message);
|
|
337
|
+
std::string describeMapping(const IOMap &map) const;
|
|
328
338
|
};
|
|
329
339
|
|
|
330
340
|
extern std::vector<std::unique_ptr<IOClient>> Clients;
|
|
@@ -40,6 +40,10 @@ void GPIOClient::onMappingAdded(const IOMap &map)
|
|
|
40
40
|
{
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
+
if (!ensureActiveLow(pin))
|
|
44
|
+
{
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
43
47
|
if (!ensureDirection(pin, direction))
|
|
44
48
|
{
|
|
45
49
|
return;
|
|
@@ -364,6 +368,11 @@ bool GPIOClient::ensureDirection(int globalPin, const std::string &direction) co
|
|
|
364
368
|
return writeTextFile("/sys/class/gpio/gpio" + std::to_string(globalPin) + "/direction", direction);
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
bool GPIOClient::ensureActiveLow(int globalPin) const
|
|
372
|
+
{
|
|
373
|
+
return writeTextFile("/sys/class/gpio/gpio" + std::to_string(globalPin) + "/active_low", "1");
|
|
374
|
+
}
|
|
375
|
+
|
|
367
376
|
bool GPIOClient::resolveGlobalPin(const IOMap &map, int &globalPin) const
|
|
368
377
|
{
|
|
369
378
|
const std::string chipName = normalizeChipName(map.moduleID);
|
|
@@ -40,6 +40,7 @@ private:
|
|
|
40
40
|
bool readIntFile(const std::string &path, int &value) const;
|
|
41
41
|
bool writeTextFile(const std::string &path, const std::string &value) const;
|
|
42
42
|
bool ensureExported(int globalPin) const;
|
|
43
|
+
bool ensureActiveLow(int globalPin) const;
|
|
43
44
|
bool ensureDirection(int globalPin, const std::string &direction) const;
|
|
44
45
|
bool resolveGlobalPin(const IOMap &map, int &globalPin) const;
|
|
45
46
|
bool resolveRemotePin(const std::string &remote, int &globalPin) const;
|
|
@@ -74,6 +74,9 @@ export class GPIOClient extends IOClient {
|
|
|
74
74
|
if (!this.ensureExported(pin)) {
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
+
if (!writeTextFile(`/sys/class/gpio/gpio${pin}/active_low`, "1")) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
77
80
|
if (!writeTextFile(`/sys/class/gpio/gpio${pin}/direction`, direction)) {
|
|
78
81
|
return;
|
|
79
82
|
}
|
|
@@ -18,6 +18,36 @@ import { Programmer } from './Programmer.js';
|
|
|
18
18
|
import { runCommand } from './utils.js';
|
|
19
19
|
import { getManagedArduinoCliExecOptions, getManagedArduinoCliPath } from '../toolchains.js';
|
|
20
20
|
|
|
21
|
+
const ARDUINO_UPLOAD_NON_FATAL_STDERR_PATTERNS = [
|
|
22
|
+
/dfu-util:\s*Warning:\s*Invalid DFU suffix signature/i,
|
|
23
|
+
/dfu-util:\s*A valid DFU suffix will be required in a future dfu-util release/i
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function classifyUploadStderr(stderrText) {
|
|
27
|
+
const lines = String(stderrText || '')
|
|
28
|
+
.split(/\r?\n/)
|
|
29
|
+
.map(line => line.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
|
|
32
|
+
if (lines.length === 0) {
|
|
33
|
+
return { fatal: [], nonFatal: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fatal = [];
|
|
37
|
+
const nonFatal = [];
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const isKnownNonFatal = ARDUINO_UPLOAD_NON_FATAL_STDERR_PATTERNS.some(pattern => pattern.test(line));
|
|
41
|
+
if (isKnownNonFatal) {
|
|
42
|
+
nonFatal.push(line);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
fatal.push(line);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { fatal, nonFatal };
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
export class ArduinoProgrammer extends Programmer {
|
|
22
52
|
constructor(options) {
|
|
23
53
|
super(options);
|
|
@@ -69,7 +99,13 @@ export class ArduinoProgrammer extends Programmer {
|
|
|
69
99
|
console.log(stdout.trim());
|
|
70
100
|
}
|
|
71
101
|
if (stderr) {
|
|
72
|
-
|
|
102
|
+
const { fatal, nonFatal } = classifyUploadStderr(stderr);
|
|
103
|
+
if (nonFatal.length > 0) {
|
|
104
|
+
console.log(nonFatal.join('\n'));
|
|
105
|
+
}
|
|
106
|
+
if (fatal.length > 0) {
|
|
107
|
+
console.error(fatal.join('\n'));
|
|
108
|
+
}
|
|
73
109
|
}
|
|
74
110
|
|
|
75
111
|
return true;
|
|
@@ -130,6 +130,13 @@ export class SSHProgrammer extends Programmer {
|
|
|
130
130
|
? `cp -a ${quoteForPosixSingle(`${remoteStagingPath}/.`)} ${quoteForPosixSingle(`${remotePath}/`)}`
|
|
131
131
|
: `cp -a ${quoteForPosixSingle(path.posix.join(remoteStagingPath, path.basename(sourcePath)))} ${quoteForPosixSingle(`${remotePath}/`)}`;
|
|
132
132
|
|
|
133
|
+
await this.#runSsh(credentials.password, [
|
|
134
|
+
'-p',
|
|
135
|
+
sshPort,
|
|
136
|
+
`${credentials.username}@${host}`,
|
|
137
|
+
this.#buildSudoCommand(credentials.password, this.#buildStopProgramCommand(remoteProgramPath, runtime))
|
|
138
|
+
]);
|
|
139
|
+
|
|
133
140
|
await this.#runSsh(credentials.password, [
|
|
134
141
|
'-p',
|
|
135
142
|
sshPort,
|
|
@@ -256,6 +263,48 @@ export class SSHProgrammer extends Programmer {
|
|
|
256
263
|
return `sudo sh -lc ${quoteForPosixSingle(command)}`;
|
|
257
264
|
}
|
|
258
265
|
|
|
266
|
+
#buildStopProgramCommand(remoteProgramPath, runtime) {
|
|
267
|
+
const quotedPath = quoteForPosixSingle(remoteProgramPath);
|
|
268
|
+
const quotedProgramName = quoteForPosixSingle(path.posix.basename(remoteProgramPath));
|
|
269
|
+
return `
|
|
270
|
+
if command -v pgrep >/dev/null 2>&1; then
|
|
271
|
+
if [ ${quoteForPosixSingle(runtime)} = 'node' ]; then
|
|
272
|
+
pids=$(ps -eo pid=,args= | awk '
|
|
273
|
+
index($0, ${quotedPath}) > 0 && $1 != "'$$'" && $1 != "'"$PPID"'" { print $1 }
|
|
274
|
+
' 2>/dev/null || true)
|
|
275
|
+
else
|
|
276
|
+
pids=$(pgrep -x -- ${quotedProgramName} 2>/dev/null || true)
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
target_pids=""
|
|
280
|
+
for pid in $pids; do
|
|
281
|
+
if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then
|
|
282
|
+
continue
|
|
283
|
+
fi
|
|
284
|
+
target_pids="$target_pids $pid"
|
|
285
|
+
done
|
|
286
|
+
|
|
287
|
+
if [ -n "$target_pids" ]; then
|
|
288
|
+
kill $target_pids >/dev/null 2>&1 || true
|
|
289
|
+
for _ in 1 2 3 4 5; do
|
|
290
|
+
sleep 1
|
|
291
|
+
remaining=""
|
|
292
|
+
for pid in $target_pids; do
|
|
293
|
+
if kill -0 "$pid" >/dev/null 2>&1; then
|
|
294
|
+
remaining="$remaining $pid"
|
|
295
|
+
fi
|
|
296
|
+
done
|
|
297
|
+
if [ -z "$remaining" ]; then
|
|
298
|
+
exit 0
|
|
299
|
+
fi
|
|
300
|
+
target_pids="$remaining"
|
|
301
|
+
done
|
|
302
|
+
kill -9 $target_pids >/dev/null 2>&1 || true
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
`.trim();
|
|
306
|
+
}
|
|
307
|
+
|
|
259
308
|
async #cleanupRemoteStaging(credentials, host, sshPort, remoteStagingPath) {
|
|
260
309
|
if (!remoteStagingPath) {
|
|
261
310
|
return;
|
|
@@ -286,8 +335,7 @@ export class SSHProgrammer extends Programmer {
|
|
|
286
335
|
SSH_ASKPASS: askPassPath,
|
|
287
336
|
SSH_ASKPASS_REQUIRE: 'force',
|
|
288
337
|
DISPLAY: process.env.DISPLAY || 'nodalis:0'
|
|
289
|
-
}
|
|
290
|
-
stdio: ['ignore', 'inherit', 'inherit']
|
|
338
|
+
}
|
|
291
339
|
});
|
|
292
340
|
}
|
|
293
341
|
finally {
|
package/src/programmers/utils.js
CHANGED
|
@@ -65,18 +65,43 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
65
65
|
|
|
66
66
|
export async function runCommandInteractive(command, args = [], options = {}) {
|
|
67
67
|
return new Promise((resolve, reject) => {
|
|
68
|
+
const childStdio = options.stdio ?? ['inherit', 'pipe', 'pipe'];
|
|
68
69
|
const child = spawn(command, args, {
|
|
69
|
-
stdio:
|
|
70
|
+
stdio: childStdio,
|
|
70
71
|
...options
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
let stdout = '';
|
|
75
|
+
let stderr = '';
|
|
76
|
+
|
|
77
|
+
if (child.stdout) {
|
|
78
|
+
child.stdout.on('data', data => {
|
|
79
|
+
const text = data.toString();
|
|
80
|
+
stdout += text;
|
|
81
|
+
process.stdout.write(text);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (child.stderr) {
|
|
86
|
+
child.stderr.on('data', data => {
|
|
87
|
+
const text = data.toString();
|
|
88
|
+
stderr += text;
|
|
89
|
+
process.stderr.write(text);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
child.on('error', reject);
|
|
74
94
|
child.on('close', code => {
|
|
75
95
|
if (code === 0) {
|
|
76
96
|
resolve({ code });
|
|
77
97
|
return;
|
|
78
98
|
}
|
|
79
|
-
|
|
99
|
+
const detail = (stderr || stdout).trim();
|
|
100
|
+
reject(new Error(
|
|
101
|
+
detail
|
|
102
|
+
? `Command failed (${command} ${args.join(' ')}) with exit code ${code}: ${detail}`
|
|
103
|
+
: `Command failed (${command} ${args.join(' ')}) with exit code ${code}`
|
|
104
|
+
));
|
|
80
105
|
});
|
|
81
106
|
});
|
|
82
107
|
}
|