nodejs-poolcontroller 8.1.0 → 8.1.1
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/README.md +5 -16
- package/anslq25/MessagesMock.ts +5 -1
- package/anslq25/boards/MockSystemBoard.ts +1 -1
- package/anslq25/chemistry/MockChlorinator.ts +32 -8
- package/app.ts +10 -2
- package/controller/State.ts +2 -2
- package/controller/boards/SystemBoard.ts +5 -1
- package/controller/comms/Comms.ts +9 -8
- package/controller/comms/messages/Messages.ts +1 -1
- package/controller/nixie/circuits/Circuit.ts +1 -1
- package/logger/Logger.ts +61 -18
- package/package.json +1 -1
- package/web/Server.ts +167 -18
- package/web/services/config/Config.ts +2 -2
package/README.md
CHANGED
|
@@ -66,8 +66,6 @@ If you don't know anything about NodeJS, these directions might be helpful.
|
|
|
66
66
|
1. Run the app with `npm start`.
|
|
67
67
|
* `npm start` will compile the Typescript code. You should use this every time you download/clone/pull the latest code.
|
|
68
68
|
* `npm run start:cached` will run the app without compiling the code which can be much faster.
|
|
69
|
-
1. Running `npm start` will also create a `config.json` file for your installation. If you need to modify any properties (e.g. the path to your serialport adapter, enabling socat, etc) then stop the app, edit the `config.json` per the [instructions](module_nodejs-poolController--config.json) below, and start the app again.
|
|
70
|
-
1. Verify your pool equipment is correctly identified by inspecting the `/data/*.json` files.
|
|
71
69
|
1. Install a [webclient](module_nodejs-poolController--clients) for a browser experience and/or a [binding](module_nodejs-poolController--bindings) to have two way control with Home Automation systems.
|
|
72
70
|
|
|
73
71
|
For a very thorough walk-through, see [this](https://www.troublefreepool.com/threads/pentair-intellicenter-pool-control-dashboard-instructional-guide.218514/) great thread on Trouble Free Pool. Thanks @MyAZPool.
|
|
@@ -101,16 +99,13 @@ To do anything with this app, you need a client to connect to it. A client can
|
|
|
101
99
|
|
|
102
100
|
## Web Clients
|
|
103
101
|
1. [nodejs-poolController-dashPanel](https://github.com/rstrouse/nodejs-poolController-dashPanel). Full compatibility with IntelliCenter, *Touch, REM (RelayEquipmentManager).
|
|
104
|
-
1. Deprecated - ~~[nodejs-poolController-webClient](http://github.com/tagyoureit/nodejs-poolController-webClient). Built primarily around EasyTouch/IntelliTouch but will work with other systems.~~
|
|
105
102
|
|
|
106
|
-
* This app has the default to only listen to clients from localhost (127.0.0.1). If you need to have clients connect from other machines you will need to change the [ip](#module_nodejs-poolController--config.json) in `config.json`.
|
|
107
103
|
|
|
108
104
|
<a name="module_nodejs-poolController--bindings"></a>
|
|
109
105
|
|
|
110
106
|
## Home Automation Bindings (previously Integrations)
|
|
111
|
-
**NOTE: Existing integrations built of 5.3 or earlier WILL NOT WORK. They need to be upgraded to leverage 6.0. **
|
|
112
107
|
|
|
113
|
-
Available
|
|
108
|
+
Available automations:
|
|
114
109
|
* [Vera Home Automation Hub](https://github.com/rstrouse/nodejs-poolController-veraPlugin) - A plugin that integrates with nodejs-poolController. [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#vera)
|
|
115
110
|
* [Hubitat](https://github.com/bsileo/hubitat_poolcontroller) by @bsileo (prev help from @johnny2678, @donkarnag, @arrmo). [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#smartthingshubitat)
|
|
116
111
|
* [Homebridge/Siri/EVE](https://github.com/gadget-monk/homebridge-poolcontroller) by @gadget-monk, adopted from @leftyflip
|
|
@@ -118,31 +113,25 @@ Available for 6.x:
|
|
|
118
113
|
* [MQTT](https://github.com/crsherman/nodejs-poolController-mqtt) original release by @crsherman, re-write by @kkzonie, testing by @baudfather and others. [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#mqtt)
|
|
119
114
|
* [Homeseer](https://github.com/tagyoureit/nodejs-poolController/wiki/Homeseer-Setup-Instructions) - Integration directions by @miamijerry to integrate Homeseer through MQTT
|
|
120
115
|
|
|
121
|
-
|
|
116
|
+
Outdated:
|
|
122
117
|
* [Another SmartThings Controller](https://github.com/dhop90/pentair-pool-controller/blob/master/README.md) by @dhop90
|
|
123
118
|
* [ISY](src/integrations/socketISY.js). Original credit to @blueman2, enhancements by @mayermd
|
|
124
119
|
* [ISY Polyglot NodeServer](https://github.com/brianmtreese/nodejs-pool-controller-polyglotv2) created by @brianmtreese
|
|
125
120
|
|
|
126
121
|
# Support
|
|
127
|
-
1. For discussions, recommendations, designs, and clarifications, we recommend you join the [Github discussions](https://github.com/tagyoureit/nodejs-poolController/discussions
|
|
122
|
+
1. For discussions, recommendations, designs, and clarifications, we recommend you join the [Github discussions](https://github.com/tagyoureit/nodejs-poolController/discussions.
|
|
128
123
|
1. Check the [wiki](https://github.com/tagyoureit/nodejs-poolController/wiki) for tips, tricks and additional documentation.
|
|
129
124
|
1. For bug reports you can open a [github issue](https://github.com/tagyoureit/nodejs-poolController/issues/new),
|
|
130
125
|
|
|
131
|
-
### Virtual Controller
|
|
132
|
-
v6 adds all new configuration and support for virtual pumps, chlorinators (and soon, Intellichem)
|
|
133
|
-
|
|
134
|
-
* [Virtual Pump Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Virtual-Pump-Controller---v6)
|
|
135
|
-
* [Virtual Chlorinator Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Virtual-Chlorinator-Controller-v6)
|
|
136
|
-
* Virtual Chem Controller
|
|
137
126
|
|
|
138
127
|
# Changes
|
|
139
128
|
See [Changelog](https://github.com/tagyoureit/nodejs-poolController/blob/master/Changelog)
|
|
140
129
|
|
|
141
|
-
|
|
142
130
|
<a name="module_nodejs-poolController--config.json"></a>
|
|
143
131
|
# Config.json changes
|
|
144
132
|
|
|
145
133
|
## Controller section - changes to the communications for the app
|
|
134
|
+
Most of these can be configured directly from the UI in dashPanel.
|
|
146
135
|
* `rs485Port` - set to the name of you rs485 controller. See [wiki](https://github.com/tagyoureit/nodejs-poolController/wiki/RS-485-Adapter-Details) for details and testing.
|
|
147
136
|
* `portSettings` - should not need to be changed for RS485
|
|
148
137
|
* `mockPort` - opens a "fake" port for this app to communicate on. Can be used with [packet captures/replays](https://github.com/tagyoureit/nodejs-poolController/wiki/How-to-capture-all-packets-for-issue-resolution).
|
|
@@ -186,7 +175,7 @@ See [Changelog](https://github.com/tagyoureit/nodejs-poolController/blob/master/
|
|
|
186
175
|
# License
|
|
187
176
|
|
|
188
177
|
nodejs-poolController. An application to control pool equipment.
|
|
189
|
-
Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com
|
|
178
|
+
Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025. Russell Goldin, tagyoureit. russ.goldin@gmail.com
|
|
190
179
|
|
|
191
180
|
This program is free software: you can redistribute it and/or modify
|
|
192
181
|
it under the terms of the GNU Affero General Public License as
|
package/anslq25/MessagesMock.ts
CHANGED
|
@@ -196,7 +196,11 @@ export class MessagesMock {
|
|
|
196
196
|
HeaterStateMessage.process(outboundMsg);
|
|
197
197
|
break;*/
|
|
198
198
|
case Protocol.Chlorinator:
|
|
199
|
-
|
|
199
|
+
// Only process outbound messages (commands from OCP to chlorinator)
|
|
200
|
+
// Inbound messages (responses from chlorinator to OCP) should not be processed by mock
|
|
201
|
+
if (msg.dest >= 80) {
|
|
202
|
+
mockChlor.process(msg);
|
|
203
|
+
}
|
|
200
204
|
/*
|
|
201
205
|
case Protocol.Hayward:
|
|
202
206
|
PumpStateMessage.processHayward(msg);
|
|
@@ -50,7 +50,7 @@ export class MockSystemBoard {
|
|
|
50
50
|
return await msg.sendAsync();
|
|
51
51
|
// is the controller on a real/physical port or a mock port?
|
|
52
52
|
/* let port = conn.findPortById(sys.anslq25.portId);
|
|
53
|
-
if (port.
|
|
53
|
+
if (port.mock) {
|
|
54
54
|
let inbound = new Inbound();
|
|
55
55
|
inbound.protocol = msg.protocol;
|
|
56
56
|
inbound.header = msg.header;
|
|
@@ -15,13 +15,16 @@ export class MockChlorinator {
|
|
|
15
15
|
switch (inbound.action){
|
|
16
16
|
case 0: // Set control OCP->Chlorinator: [16,2,80,0][0][98,16,3]
|
|
17
17
|
this.chlorSetControl(inbound, response);
|
|
18
|
+
break;
|
|
18
19
|
case 17: // OCP->Chlorinator set output. [16,2,80,17][15][130,16,3]
|
|
19
20
|
this.chlorSetOutput(inbound, response);
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
break;
|
|
22
|
+
case 19: // iChlor keep alive(?) [16, 2, 80, 19][117, 16, 3]
|
|
23
|
+
this.chlorKeepAlive(inbound, response);
|
|
22
24
|
break;
|
|
23
25
|
case 20: // OCP->Chlorinator Get model [16,2,80,20][0][118,16,3]
|
|
24
26
|
this.chlorGetModel(inbound, response);
|
|
27
|
+
break;
|
|
25
28
|
default:
|
|
26
29
|
logger.info(`No mock chlorinator response for ${inbound.toShortPacket()} `);
|
|
27
30
|
}
|
|
@@ -45,13 +48,34 @@ export class MockChlorinator {
|
|
|
45
48
|
/*
|
|
46
49
|
{"port":0,"id":42639,"valid":true,"dir":"out","proto":"chlorinator","pkt":[[],[], [16,2,80,17], [100],[215,16,3]],"ts":"2022-07-19T21:46:00.302-0700"}
|
|
47
50
|
{"port":0,"id":42640,"valid":true,"dir":"in","proto":"chlorinator","for":[42639],"pkt":[[],[],[16,2,0,18],[78,128],[242,16,3]],"ts": "2022-07-19T21:46:00.341-0700"} */
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Simulate a response from the chlorinator (inbound message)
|
|
52
|
+
const payload = [Math.floor(this.random(90-56, true))+56, 128];
|
|
53
|
+
const header = [16, 2, 0, 18];
|
|
54
|
+
const term = [242, 16, 3];
|
|
55
|
+
let responseMsg = new Inbound();
|
|
56
|
+
responseMsg.protocol = inbound.protocol;
|
|
57
|
+
responseMsg.portId = inbound.portId;
|
|
58
|
+
responseMsg.header = header;
|
|
59
|
+
responseMsg.payload = payload;
|
|
60
|
+
responseMsg.term = term;
|
|
61
|
+
// The Inbound class will parse header/payload/action/source/dest automatically
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
let port = conn.findPortById(inbound.portId);
|
|
64
|
+
if (port) {
|
|
65
|
+
port.pushIn(Buffer.from(responseMsg.toPacket()));
|
|
66
|
+
}
|
|
67
|
+
}, 50);
|
|
54
68
|
}
|
|
69
|
+
public chlorKeepAlive(inbound: Inbound, response: Outbound){
|
|
70
|
+
/*
|
|
71
|
+
{"port":0,"id":42647,"valid":true,"dir":"out","proto":"chlorinator","pkt":[[],[], [16,2,80,19],[117,16,3]],"ts":"2022-07-19T21:46:00.645-0700"}
|
|
72
|
+
{"port":0,"id":42648,"valid":true,"dir":"in","proto":"chlorinator","for":[42647],"pkt":[[],[],[16,2,0,20],[0],[118,16,3]],"ts":"2022-07-19T21:46:00.700-0700"} */
|
|
73
|
+
response.action = 20;
|
|
74
|
+
response.appendPayloadBytes(0, 1);
|
|
75
|
+
response.setPayloadByte(0, 0);
|
|
76
|
+
conn.queueSendMessage(response);
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
public chlorGetModel(inbound: Inbound, response: Outbound){
|
|
56
80
|
/*
|
|
57
81
|
{"port":0,"id":42645,"valid":true,"dir":"out","proto":"chlorinator","pkt":[[],[], [16,2,80,20], [0],[118,16,3]],"ts":"2022-07-19T21:46:00.645-0700"}
|
package/app.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { conn } from "./controller/comms/Comms";
|
|
|
25
25
|
import { sys } from "./controller/Equipment";
|
|
26
26
|
|
|
27
27
|
import { state } from "./controller/State";
|
|
28
|
-
import { webApp } from "./web/Server";
|
|
28
|
+
import { webApp, REMInterfaceServer } from "./web/Server";
|
|
29
29
|
import * as readline from 'readline';
|
|
30
30
|
import { sl } from './controller/comms/ScreenLogic'
|
|
31
31
|
|
|
@@ -52,6 +52,9 @@ export async function startPacketCapture(bResetLogs: boolean) {
|
|
|
52
52
|
if (bResetLogs){
|
|
53
53
|
sys.resetSystem();
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
// Start packet capture on the REM server
|
|
57
|
+
await REMInterfaceServer.startPacketCaptureOnRemServer();
|
|
55
58
|
}
|
|
56
59
|
catch (err) {
|
|
57
60
|
console.error(`Error starting replay: ${ err.message }`);
|
|
@@ -61,7 +64,12 @@ export async function stopPacketCaptureAsync() {
|
|
|
61
64
|
let log = config.getSection('log');
|
|
62
65
|
log.app.captureForReplay = false;
|
|
63
66
|
config.setSection('log', log);
|
|
64
|
-
|
|
67
|
+
|
|
68
|
+
// Stop packet capture on the REM server and collect its logs
|
|
69
|
+
let remLogs = await REMInterfaceServer.stopPacketCaptureOnRemServer();
|
|
70
|
+
|
|
71
|
+
// Pass REM logs to the logger for inclusion in the backup
|
|
72
|
+
return logger.stopCaptureForReplayAsync(remLogs);
|
|
65
73
|
}
|
|
66
74
|
export async function stopAsync(): Promise<void> {
|
|
67
75
|
try {
|
package/controller/State.ts
CHANGED
|
@@ -1096,7 +1096,7 @@ export class ScheduleStateCollection extends EqStateCollection<ScheduleState> {
|
|
|
1096
1096
|
}
|
|
1097
1097
|
st.calcSchedule(state.time, sys.schedules.getItemById(ssched.id));
|
|
1098
1098
|
if (typeof st.startTime === 'undefined') continue;
|
|
1099
|
-
if (ssched.isOn || st.shouldBeOn || st.startTime.getTime() > new Date().getTime()) activeScheds.push(ssched);
|
|
1099
|
+
if (ssched.isOn || st.shouldBeOn || (st.startTime && st.startTime.getTime() > new Date().getTime())) activeScheds.push(ssched);
|
|
1100
1100
|
}
|
|
1101
1101
|
return activeScheds;
|
|
1102
1102
|
}
|
|
@@ -1256,7 +1256,7 @@ export class ScheduleTime extends ChildEqState {
|
|
|
1256
1256
|
let dtCalc = typeof this.calculatedDate !== 'undefined' && typeof this.calculatedDate.getTime === 'function' ? new Date(this.calculatedDate.getTime()).setHours(0, 0, 0, 0) : new Date(1970, 0, 1, 0, 0).getTime();
|
|
1257
1257
|
let recalc = !this.calculated;
|
|
1258
1258
|
if (!recalc && sod.getTime() !== dtCalc) recalc = true;
|
|
1259
|
-
if (!recalc && (this.endTime.getTime() < new Date().getTime() && this.startTime.getTime() < dtCalc)) {
|
|
1259
|
+
if (!recalc && (this.endTime && this.endTime.getTime() < new Date().getTime() && this.startTime && this.startTime.getTime() < dtCalc)) {
|
|
1260
1260
|
recalc = true;
|
|
1261
1261
|
logger.info(`Recalculating expired schedule ${sched.id}`);
|
|
1262
1262
|
}
|
|
@@ -3656,8 +3656,12 @@ export class ScheduleCommands extends BoardCommands {
|
|
|
3656
3656
|
ssched.display = sched.display = display;
|
|
3657
3657
|
ssched.startTimeOffset = sched.startTimeOffset = startTimeOffset;
|
|
3658
3658
|
ssched.endTimeOffset = sched.endTimeOffset = endTimeOffset;
|
|
3659
|
-
|
|
3659
|
+
// Nixie controller managing schedules (master = 1), physical OCP (master = 0)
|
|
3660
|
+
if (sys.controllerType === ControllerType.Nixie) {
|
|
3660
3661
|
sched.master = 1;
|
|
3662
|
+
} else {
|
|
3663
|
+
sched.master = 0;
|
|
3664
|
+
}
|
|
3661
3665
|
await ncp.schedules.setScheduleAsync(sched, data);
|
|
3662
3666
|
// update end time in case sched is changed while circuit is on
|
|
3663
3667
|
let cstate = state.circuits.getInterfaceById(sched.circuit);
|
|
@@ -198,7 +198,7 @@ export class Connection {
|
|
|
198
198
|
let c = cfg[section];
|
|
199
199
|
if (typeof c.type === 'undefined') {
|
|
200
200
|
let type = 'local';
|
|
201
|
-
if (c.
|
|
201
|
+
if (c.mock) type = 'mock';
|
|
202
202
|
else if (c.netConnect) type = 'network';
|
|
203
203
|
config.setSection(`controller.${section}`, c);
|
|
204
204
|
console.log(section);
|
|
@@ -260,7 +260,7 @@ export class Connection {
|
|
|
260
260
|
if (anslq25port >= 0) {
|
|
261
261
|
let ports = this.rs485Ports;
|
|
262
262
|
for (let i = 0; i < ports.length; i++) {
|
|
263
|
-
// if (ports[i].
|
|
263
|
+
// if (ports[i].mock) continue;
|
|
264
264
|
if (ports[i].portId === currPort.portId) continue;
|
|
265
265
|
if (ports[i].portId === anslq25port) continue; // don't resend
|
|
266
266
|
if (!ports[i].isOpen) continue;
|
|
@@ -380,7 +380,8 @@ export class Connection {
|
|
|
380
380
|
let msgs = [];
|
|
381
381
|
// conn.queueInboundToBroadcast(msg);
|
|
382
382
|
conn.queueOutboundToBroadcast(msg);
|
|
383
|
-
/* if (msgs.
|
|
383
|
+
/* if (msgs.le
|
|
384
|
+
ngth > 0) {
|
|
384
385
|
msgs.push(msg);
|
|
385
386
|
let promises: Promise<boolean>[] = [];
|
|
386
387
|
for (let i = 0; i < msgs.length; i++) {
|
|
@@ -511,7 +512,7 @@ export class RS485Port {
|
|
|
511
512
|
public reconnects: number = 0;
|
|
512
513
|
public emitter: EventEmitter;
|
|
513
514
|
public get portId() { return typeof this._cfg !== 'undefined' && typeof this._cfg.portId !== 'undefined' ? this._cfg.portId : 0; }
|
|
514
|
-
public get type() { return typeof this._cfg.type !== 'undefined' ? this._cfg.type : this._cfg.netConnect ? 'netConnect' : this._cfg.
|
|
515
|
+
public get type() { return typeof this._cfg.type !== 'undefined' ? this._cfg.type : this._cfg.netConnect ? 'netConnect' : this._cfg.mock ? 'mock' : 'local' };
|
|
515
516
|
public isOpen: boolean = false;
|
|
516
517
|
public closing: boolean = false;
|
|
517
518
|
private _cfg: any;
|
|
@@ -682,7 +683,7 @@ export class RS485Port {
|
|
|
682
683
|
// be open if a hardware interface is used and this method returns.
|
|
683
684
|
sp.open((err) => {
|
|
684
685
|
if (err) {
|
|
685
|
-
this.resetConnTimer();
|
|
686
|
+
if (!this.mock) this.resetConnTimer();
|
|
686
687
|
this.isOpen = false;
|
|
687
688
|
logger.error(`Error opening port ${this.portId}: ${err.message}. ${this._cfg.inactivityRetry > 0 && !this.mock ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; (fwiw, inactivityRetry set to ${this._cfg.inactivityRetry})`}`);
|
|
688
689
|
resolve(false);
|
|
@@ -712,7 +713,7 @@ export class RS485Port {
|
|
|
712
713
|
if (!this.mock && !this.isPaused) this.resetConnTimer();
|
|
713
714
|
this.pushIn(data);
|
|
714
715
|
});
|
|
715
|
-
this.resetConnTimer();
|
|
716
|
+
if (!this.mock) this.resetConnTimer();
|
|
716
717
|
this.emitPortStats();
|
|
717
718
|
});
|
|
718
719
|
sp.on('close', (err) => {
|
|
@@ -730,7 +731,7 @@ export class RS485Port {
|
|
|
730
731
|
if (typeof this.writeTimer !== 'undefined') { clearTimeout(this.writeTimer); this.writeTimer = null; }
|
|
731
732
|
this.isOpen = false;
|
|
732
733
|
if (sp.isOpen) sp.close((err) => { }); // call this with the error callback so that it doesn't emit to the error again.
|
|
733
|
-
this.resetConnTimer();
|
|
734
|
+
if (!this.mock) this.resetConnTimer();
|
|
734
735
|
logger.error(`Serial Port ${this.portId}: An error occurred : ${this._cfg.rs485Port}: ${JSON.stringify(err)}`);
|
|
735
736
|
this.emitPortStats();
|
|
736
737
|
|
|
@@ -823,7 +824,7 @@ export class RS485Port {
|
|
|
823
824
|
protected resetConnTimer(...args) {
|
|
824
825
|
//console.log(`resetting connection timer`);
|
|
825
826
|
if (this.connTimer !== null) clearTimeout(this.connTimer);
|
|
826
|
-
if (!this._cfg.
|
|
827
|
+
if (!this._cfg.mock && this._cfg.inactivityRetry > 0 && !this.closing) this.connTimer = setTimeout(async () => {
|
|
827
828
|
try {
|
|
828
829
|
if (this._cfg.netConnect)
|
|
829
830
|
logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
|
|
@@ -873,7 +873,7 @@ class OutboundCommon extends Message {
|
|
|
873
873
|
case Protocol.Heater:
|
|
874
874
|
case Protocol.Hayward:
|
|
875
875
|
this.chkHi = Math.floor(sum / 256);
|
|
876
|
-
this.chkLo = (sum - (
|
|
876
|
+
this.chkLo = (sum - (this.chkHi * 256));
|
|
877
877
|
break;
|
|
878
878
|
case Protocol.AquaLink:
|
|
879
879
|
case Protocol.Chlorinator:
|
|
@@ -385,7 +385,7 @@ export class NixieCircuit extends NixieEquipment {
|
|
|
385
385
|
return new InterfaceServerResponse(200, 'Success');
|
|
386
386
|
}
|
|
387
387
|
if (this._sequencing) return new InterfaceServerResponse(200, 'Success');
|
|
388
|
-
|
|
388
|
+
let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, { isOn: val, latch: val ? 10000 : undefined });
|
|
389
389
|
if (res.status.code === 200) {
|
|
390
390
|
// Set this up so we can process our egg timer.
|
|
391
391
|
if (val && val !== cstate.isOn) {
|
package/logger/Logger.ts
CHANGED
|
@@ -270,6 +270,10 @@ class Logger {
|
|
|
270
270
|
// start new replay directory
|
|
271
271
|
|
|
272
272
|
if (!fs.existsSync(this.captureForReplayPath)) fs.mkdirSync(this.captureForReplayBaseDir, { recursive: true });
|
|
273
|
+
|
|
274
|
+
// Create logs subdirectory for additional log files
|
|
275
|
+
let logsSubDir = path.join(this.captureForReplayBaseDir, 'logs');
|
|
276
|
+
if (!fs.existsSync(logsSubDir)) fs.mkdirSync(logsSubDir, { recursive: true });
|
|
273
277
|
if (bResetLogs){
|
|
274
278
|
if (fs.existsSync(path.join(process.cwd(), 'data/poolConfig.json'))) {
|
|
275
279
|
fs.copyFileSync(path.join(process.cwd(), 'data/poolConfig.json'), path.join(process.cwd(),'data/', `poolConfig-${this.getLogTimestamp()}.json`));
|
|
@@ -360,29 +364,68 @@ class Logger {
|
|
|
360
364
|
logger._logger.add(this.transports.file);
|
|
361
365
|
this.transports.console.level = 'silly';
|
|
362
366
|
}
|
|
363
|
-
public async stopCaptureForReplayAsync():Promise<string> {
|
|
367
|
+
public async stopCaptureForReplayAsync(remLogs?: any[]):Promise<string> {
|
|
364
368
|
return new Promise<string>(async (resolve, reject) => {
|
|
365
369
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
+
// Get REM server configurations from config
|
|
371
|
+
let configData = config.getSection();
|
|
372
|
+
let remServers = [];
|
|
373
|
+
if (configData.web && configData.web.interfaces) {
|
|
374
|
+
for (let interfaceName in configData.web.interfaces) {
|
|
375
|
+
let interfaceConfig = configData.web.interfaces[interfaceName];
|
|
376
|
+
if (interfaceConfig.type === 'rem' && interfaceConfig.enabled) {
|
|
377
|
+
remServers.push({
|
|
378
|
+
name: interfaceConfig.name || interfaceName,
|
|
379
|
+
uuid: interfaceConfig.uuid,
|
|
380
|
+
host: interfaceConfig.options?.host || '',
|
|
381
|
+
backup: true
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Use the existing backup logic to create the base backup
|
|
388
|
+
let backupOptions = {
|
|
389
|
+
njsPC: true,
|
|
390
|
+
servers: remServers,
|
|
391
|
+
name: `Packet Capture ${this.currentTimestamp}`,
|
|
392
|
+
automatic: false
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
let backupFile = await webApp.backupServer(backupOptions);
|
|
396
|
+
|
|
397
|
+
// Add packet capture logs to the existing backup zip
|
|
398
|
+
let jszip = require("jszip");
|
|
399
|
+
let zip = await jszip.loadAsync(fs.readFileSync(backupFile.filePath));
|
|
400
|
+
|
|
401
|
+
// Add packet capture logs to the njsPC/logs directory
|
|
402
|
+
zip.file(`njsPC/logs/${this.getPacketPath()}`, fs.readFileSync(logger.pktPath));
|
|
403
|
+
zip.file(`njsPC/logs/${this.getConsoleToFilePath()}`, fs.readFileSync(this.consoleToFilePath));
|
|
404
|
+
|
|
405
|
+
// Add REM server logs if provided
|
|
406
|
+
if (remLogs && remLogs.length > 0) {
|
|
407
|
+
logger.info(`Adding ${remLogs.length} REM logs to backup`);
|
|
408
|
+
for (let remLog of remLogs) {
|
|
409
|
+
// Create logs directory for the REM server using the hardcoded name
|
|
410
|
+
let logPath = `Relay Equipment Manager/logs/${remLog.logFileName}`;
|
|
411
|
+
logger.info(`Adding REM log to backup: ${logPath} (size: ${remLog.logData.length} characters)`);
|
|
412
|
+
zip.file(logPath, remLog.logData);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
logger.info(`No REM logs provided to add to backup`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Generate the updated zip
|
|
419
|
+
await zip.generateAsync({type:'nodebuffer'}).then(content => {
|
|
420
|
+
fs.writeFileSync(backupFile.filePath, content);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Restore original logging configuration
|
|
370
424
|
this.cfg = config.getSection('log');
|
|
371
425
|
logger._logger.remove(this.transports.file);
|
|
372
426
|
this.transports.console.level = this.cfg.app.level;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
let zip = new jszip();
|
|
376
|
-
zip.file('config.json', fs.readFileSync(path.join(this.captureForReplayBaseDir, 'config.json')));
|
|
377
|
-
zip.file('poolConfig.json', fs.readFileSync(path.join(this.captureForReplayBaseDir, 'poolConfig.json')));
|
|
378
|
-
zip.file('poolState.json', fs.readFileSync(path.join(this.captureForReplayBaseDir, 'poolState.json')));
|
|
379
|
-
zip.file(this.getPacketPath(), fs.readFileSync(path.join(this.captureForReplayBaseDir, `packetLog${this.getLogTimestamp()}`)));
|
|
380
|
-
zip.file(this.getConsoleToFilePath(), fs.readFileSync(this.consoleToFilePath));
|
|
381
|
-
await zip.generateAsync({type:'nodebuffer'}).then(content=>
|
|
382
|
-
{
|
|
383
|
-
fs.writeFileSync(zipPath, content);
|
|
384
|
-
});
|
|
385
|
-
resolve(zipPath);
|
|
427
|
+
|
|
428
|
+
resolve(backupFile.filePath);
|
|
386
429
|
}
|
|
387
430
|
catch (err) {
|
|
388
431
|
reject(err.message);
|
package/package.json
CHANGED
package/web/Server.ts
CHANGED
|
@@ -1423,18 +1423,151 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1423
1423
|
try {
|
|
1424
1424
|
let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
|
|
1425
1425
|
return response;
|
|
1426
|
-
} catch (err) {
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
logger.error(`Error requesting GET /config/backup/controller: ${err.message}`);
|
|
1428
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1429
|
+
errorResponse.error = new Error(`Error requesting GET /config/backup/controller: ${err.message}`);
|
|
1430
|
+
return errorResponse;
|
|
1431
|
+
}
|
|
1427
1432
|
}
|
|
1428
1433
|
public async validateRestore(cfg): Promise<InterfaceServerResponse> {
|
|
1429
1434
|
try {
|
|
1430
1435
|
let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
|
|
1431
1436
|
return response;
|
|
1432
|
-
} catch (err) {
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
logger.error(`Error requesting PUT /config/restore/validate ${err.message}`);
|
|
1439
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1440
|
+
errorResponse.error = new Error(`Error requesting PUT /config/restore/validate: ${err.message}`);
|
|
1441
|
+
return errorResponse;
|
|
1442
|
+
}
|
|
1433
1443
|
}
|
|
1434
1444
|
public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
|
|
1435
1445
|
try {
|
|
1436
1446
|
return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
|
|
1437
|
-
} catch (err) {
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
logger.error(`Error requesting PUT /config/restore/file ${err.message}`);
|
|
1449
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1450
|
+
errorResponse.error = new Error(`Error requesting PUT /config/restore/file: ${err.message}`);
|
|
1451
|
+
return errorResponse;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
public async startPacketCapture(): Promise<InterfaceServerResponse> {
|
|
1455
|
+
try {
|
|
1456
|
+
let response = await this.sendClientRequest('PUT', '/config/packetCapture/start', undefined, 10000);
|
|
1457
|
+
return response;
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
logger.error(`Error requesting PUT /config/packetCapture/start: ${err.message}`);
|
|
1460
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1461
|
+
errorResponse.error = new Error(`Error requesting PUT /config/packetCapture/start: ${err.message}`);
|
|
1462
|
+
return errorResponse;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
public async stopPacketCapture(): Promise<InterfaceServerResponse> {
|
|
1466
|
+
try {
|
|
1467
|
+
let response = await this.sendClientRequest('PUT', '/config/packetCapture/stop', undefined, 10000);
|
|
1468
|
+
return response;
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
logger.error(`Error requesting PUT /config/packetCapture/stop: ${err.message}`);
|
|
1471
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1472
|
+
errorResponse.error = new Error(`Error requesting PUT /config/packetCapture/stop: ${err.message}`);
|
|
1473
|
+
return errorResponse;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
public async getPacketCaptureLog(): Promise<InterfaceServerResponse> {
|
|
1477
|
+
try {
|
|
1478
|
+
let response = await this.sendClientRequest('GET', '/config/packetCapture/log', undefined, 15000);
|
|
1479
|
+
return response;
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
logger.error(`Error requesting GET /config/packetCapture/log: ${err.message}`);
|
|
1482
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1483
|
+
errorResponse.error = new Error(`Error requesting GET /config/packetCapture/log: ${err.message}`);
|
|
1484
|
+
return errorResponse;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
// Static methods to handle the REM server
|
|
1490
|
+
public static async startPacketCaptureOnRemServer(): Promise<void> {
|
|
1491
|
+
let remServers = webApp.findServersByType('rem') as REMInterfaceServer[];
|
|
1492
|
+
logger.info(`Found ${remServers ? remServers.length : 0} REM servers`);
|
|
1493
|
+
|
|
1494
|
+
if (remServers && remServers.length > 0) {
|
|
1495
|
+
let server = remServers[0]; // Get the single REM server
|
|
1496
|
+
logger.info(`Attempting to start packet capture on REM server: ${server.name} (connected: ${server.isConnected})`);
|
|
1497
|
+
|
|
1498
|
+
if (server.isConnected) {
|
|
1499
|
+
try {
|
|
1500
|
+
let response = await server.startPacketCapture();
|
|
1501
|
+
logger.info(`Start packet capture response: ${JSON.stringify(response)}`);
|
|
1502
|
+
|
|
1503
|
+
if (response && response.status.code === 200) {
|
|
1504
|
+
logger.info(`Started packet capture on REM server: ${server.name}`);
|
|
1505
|
+
} else {
|
|
1506
|
+
logger.warn(`Failed to start packet capture on REM server: ${server.name}. Status: ${response?.status?.code}, Error: ${response?.error?.message}`);
|
|
1507
|
+
}
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
logger.error(`Error starting packet capture on REM server ${server.name}: ${err.message}`);
|
|
1510
|
+
}
|
|
1511
|
+
} else {
|
|
1512
|
+
logger.warn(`REM server ${server.name} is not connected, cannot start packet capture`);
|
|
1513
|
+
}
|
|
1514
|
+
} else {
|
|
1515
|
+
logger.warn(`No REM servers found or configured`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
public static async stopPacketCaptureOnRemServer(): Promise<any[]> {
|
|
1520
|
+
let remServers = webApp.findServersByType('rem') as REMInterfaceServer[];
|
|
1521
|
+
let remLogs = [];
|
|
1522
|
+
|
|
1523
|
+
logger.info(`Found ${remServers ? remServers.length : 0} REM servers for stop packet capture`);
|
|
1524
|
+
|
|
1525
|
+
if (remServers && remServers.length > 0) {
|
|
1526
|
+
let server = remServers[0]; // Get the single REM server
|
|
1527
|
+
logger.info(`Attempting to stop packet capture on REM server: ${server.name} (connected: ${server.isConnected})`);
|
|
1528
|
+
|
|
1529
|
+
if (server.isConnected) {
|
|
1530
|
+
try {
|
|
1531
|
+
// Stop packet capture
|
|
1532
|
+
let stopResponse = await server.stopPacketCapture();
|
|
1533
|
+
logger.info(`Stop packet capture response: ${JSON.stringify(stopResponse)}`);
|
|
1534
|
+
|
|
1535
|
+
if (stopResponse && stopResponse.status.code === 200) {
|
|
1536
|
+
logger.info(`Stopped packet capture on REM server: ${server.name}`);
|
|
1537
|
+
|
|
1538
|
+
// Get the log file
|
|
1539
|
+
let logResponse = await server.getPacketCaptureLog();
|
|
1540
|
+
logger.info(`Get log response: ${JSON.stringify(logResponse)}`);
|
|
1541
|
+
|
|
1542
|
+
if (logResponse && logResponse.status.code === 200 && logResponse.data) {
|
|
1543
|
+
// Use the actual log file name from the REM response
|
|
1544
|
+
logger.info(`Log response obj: ${JSON.stringify(logResponse.obj)}`);
|
|
1545
|
+
let logFileName = logResponse.obj && logResponse.obj.logFile ? logResponse.obj.logFile : `rem_${server.name}_packetCapture.log`;
|
|
1546
|
+
logger.info(`Using log filename: ${logFileName}`);
|
|
1547
|
+
remLogs.push({
|
|
1548
|
+
serverName: server.name,
|
|
1549
|
+
logData: logResponse.data,
|
|
1550
|
+
logFileName: logFileName
|
|
1551
|
+
});
|
|
1552
|
+
logger.info(`Retrieved packet capture log from REM server: ${server.name}, log size: ${logResponse.data.length} characters, filename: ${logFileName}`);
|
|
1553
|
+
} else {
|
|
1554
|
+
logger.warn(`Failed to retrieve packet capture log from REM server: ${server.name}. Status: ${logResponse?.status?.code}, Error: ${logResponse?.error?.message}`);
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
logger.warn(`Failed to stop packet capture on REM server: ${server.name}. Status: ${stopResponse?.status?.code}, Error: ${stopResponse?.error?.message}`);
|
|
1558
|
+
}
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
logger.error(`Error stopping packet capture on REM server ${server.name}: ${err.message}`);
|
|
1561
|
+
}
|
|
1562
|
+
} else {
|
|
1563
|
+
logger.warn(`REM server ${server.name} is not connected, cannot stop packet capture`);
|
|
1564
|
+
}
|
|
1565
|
+
} else {
|
|
1566
|
+
logger.warn(`No REM servers found or configured for stop packet capture`);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
logger.info(`Returning ${remLogs.length} REM logs`);
|
|
1570
|
+
return remLogs;
|
|
1438
1571
|
}
|
|
1439
1572
|
private async initConnection() {
|
|
1440
1573
|
try {
|
|
@@ -1483,12 +1616,15 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1483
1616
|
resolve();
|
|
1484
1617
|
}
|
|
1485
1618
|
}
|
|
1486
|
-
catch (err) {
|
|
1619
|
+
catch (err) {
|
|
1620
|
+
logger.error(`initConnection setTimeout error: ${err.message}`);
|
|
1621
|
+
reject(new Error(`initConnection setTimeout: ${err.message}`));
|
|
1622
|
+
}
|
|
1487
1623
|
}, 3000);
|
|
1488
1624
|
});
|
|
1489
1625
|
}
|
|
1490
1626
|
catch (err) {
|
|
1491
|
-
logger.error(`Error with REM Interface Server initConnection: ${err}`)
|
|
1627
|
+
logger.error(`Error with REM Interface Server initConnection: ${err.message}`);
|
|
1492
1628
|
}
|
|
1493
1629
|
}
|
|
1494
1630
|
public async stopAsync() {
|
|
@@ -1556,7 +1692,10 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1556
1692
|
});
|
|
1557
1693
|
req.on('abort', () => { logger.warn('Request Aborted'); reject(new Error('Request Aborted.')); });
|
|
1558
1694
|
req.end(sbody);
|
|
1559
|
-
}).catch((err) => {
|
|
1695
|
+
}).catch((err) => {
|
|
1696
|
+
logger.error(`Error Sending REM Request: ${opts.method} ${url} ${err.message}`);
|
|
1697
|
+
ret.error = err;
|
|
1698
|
+
});
|
|
1560
1699
|
logger.verbose(`REM server request returned. ${opts.method} ${opts.path} ${sbody}`);
|
|
1561
1700
|
if (ret.status.code > 200) {
|
|
1562
1701
|
// We have an http error so let's parse it up.
|
|
@@ -1574,7 +1713,9 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1574
1713
|
}
|
|
1575
1714
|
catch (err) {
|
|
1576
1715
|
logger.error(`Error sending HTTP ${method} command to ${url}: ${err.message}`);
|
|
1577
|
-
|
|
1716
|
+
let errorResponse = new InterfaceServerResponse();
|
|
1717
|
+
errorResponse.error = new Error(`Http ${method} Error ${url}:${err.message}`);
|
|
1718
|
+
return errorResponse;
|
|
1578
1719
|
}
|
|
1579
1720
|
}
|
|
1580
1721
|
private initSockets() {
|
|
@@ -1585,7 +1726,10 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1585
1726
|
//console.log(this.cfg);
|
|
1586
1727
|
this.sockClient = sockClient(url, extend(true,
|
|
1587
1728
|
{ reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000, transports: ['websocket'], upgrade: true, }, this.cfg.socket));
|
|
1588
|
-
if (typeof this.sockClient === 'undefined')
|
|
1729
|
+
if (typeof this.sockClient === 'undefined') {
|
|
1730
|
+
logger.error('Could not Initialize REM Server. Invalid configuration.');
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1589
1733
|
//this.sockClient = io.connect(url, { reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000 });
|
|
1590
1734
|
//console.log(this.sockClient);
|
|
1591
1735
|
//console.log(typeof this.sockClient.on);
|
|
@@ -1604,7 +1748,9 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1604
1748
|
});
|
|
1605
1749
|
this.isRunning = true;
|
|
1606
1750
|
}
|
|
1607
|
-
catch (err) {
|
|
1751
|
+
catch (err) {
|
|
1752
|
+
logger.error(`Error Initializing Sockets: ${err.message}`);
|
|
1753
|
+
}
|
|
1608
1754
|
}
|
|
1609
1755
|
private isJSONString(s: string): boolean {
|
|
1610
1756
|
if (typeof s !== 'string') return false;
|
|
@@ -1613,23 +1759,23 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1613
1759
|
}
|
|
1614
1760
|
public async getApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
|
|
1615
1761
|
// Calls a rest service on the REM to set the state of a connected device.
|
|
1616
|
-
|
|
1617
|
-
|
|
1762
|
+
let ret = await this.sendClientRequest('GET', url, data, timeout);
|
|
1763
|
+
return ret;
|
|
1618
1764
|
}
|
|
1619
1765
|
public async putApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
|
|
1620
1766
|
// Calls a rest service on the REM to set the state of a connected device.
|
|
1621
|
-
|
|
1622
|
-
|
|
1767
|
+
let ret = await this.sendClientRequest('PUT', url, data, timeout);
|
|
1768
|
+
return ret;
|
|
1623
1769
|
}
|
|
1624
1770
|
public async searchApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
|
|
1625
1771
|
// Calls a rest service on the REM to set the state of a connected device.
|
|
1626
|
-
|
|
1627
|
-
|
|
1772
|
+
let ret = await this.sendClientRequest('SEARCH', url, data, timeout);
|
|
1773
|
+
return ret;
|
|
1628
1774
|
}
|
|
1629
1775
|
public async deleteApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
|
|
1630
1776
|
// Calls a rest service on the REM to set the state of a connected device.
|
|
1631
|
-
|
|
1632
|
-
|
|
1777
|
+
let ret = await this.sendClientRequest('DELETE', url, data, timeout);
|
|
1778
|
+
return ret;
|
|
1633
1779
|
}
|
|
1634
1780
|
public async getDevices() {
|
|
1635
1781
|
try {
|
|
@@ -1640,7 +1786,10 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1640
1786
|
}
|
|
1641
1787
|
return (response.status.code === 200) ? JSON.parse(response.data) : [];
|
|
1642
1788
|
}
|
|
1643
|
-
catch (err) {
|
|
1789
|
+
catch (err) {
|
|
1790
|
+
logger.error(`getDevices: ${err.message}`);
|
|
1791
|
+
return [];
|
|
1792
|
+
}
|
|
1644
1793
|
}
|
|
1645
1794
|
}
|
|
1646
1795
|
export class BackupFile {
|
|
@@ -74,7 +74,7 @@ export class ConfigRoute {
|
|
|
74
74
|
let cport = extend(true, { enabled: false, netConnect: false, mock: false }, cfg[section]);
|
|
75
75
|
let port = conn.findPortById(cport.portId || 0);
|
|
76
76
|
if (typeof cport.type === 'undefined'){
|
|
77
|
-
cport.type = cport.netConnect ? 'netConnect' : cport.
|
|
77
|
+
cport.type = cport.netConnect ? 'netConnect' : cport.mock ? 'mock' : 'local'
|
|
78
78
|
}
|
|
79
79
|
if (typeof port !== 'undefined') cport.stats = port.stats;
|
|
80
80
|
if (port.portId === 0 && port.type === 'screenlogic') {
|
|
@@ -1009,7 +1009,7 @@ export class ConfigRoute {
|
|
|
1009
1009
|
return next(new ServiceProcessError(`File already exists ${req.file.originalname}`, 'POST: app/backup/file', 'writeFile'));
|
|
1010
1010
|
else {
|
|
1011
1011
|
try {
|
|
1012
|
-
fs.writeFileSync(bf.filePath, req.file.buffer);
|
|
1012
|
+
fs.writeFileSync(bf.filePath, new Uint8Array(req.file.buffer));
|
|
1013
1013
|
} catch (e) { logger.error(`Error writing backup file ${e.message}`); }
|
|
1014
1014
|
}
|
|
1015
1015
|
return res.status(200).send(bf);
|