-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrfjs_server.js
More file actions
429 lines (384 loc) · 15.4 KB
/
rfjs_server.js
File metadata and controls
429 lines (384 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
#!/usr/bin/env node
const DO_DEBUG = false;
const dbg_data = '{\n' +
' "10.0.0.1":{"name":"DEMO 001","comment":"This is demo receiver #1","freq":"684.000","squelch":"15","afOut":"18","lastUpdate":1635089614363,"rf1":{"min":0,"max":0,"active":true},"rf2":{"min":0,"max":0,"active":false},"flags":{"lastCycleMute":1,"lastCycleTxMute":1,"lastCycleRfMute":1,"lastCycleRxMute":1},"lastCyclePilot":1,"rf":{"current":0,"antenna":"1","pilot":false},"af":{"currentPeak":3,"currentHold":3,"mute":1,"txMute":1,"rfMute":1,"rxMute":1},"battery":{"known":true,"percentage":0},"warningString":"Low Batt"},\n' +
' "10.0.0.2":{"name":"DEMO 002","comment":"And this is his companion, demo #2","freq":"670.150","squelch":"15","afOut":"18","lastUpdate":1635089614378,"rf1":{"min":0,"max":0,"active":true},"rf2":{"min":0,"max":0,"active":false},"flags":{"lastCycleMute":1,"lastCycleTxMute":1,"lastCycleRfMute":1,"lastCycleRxMute":1},"lastCyclePilot":0,"rf":{"current":0,"antenna":"1","pilot":false},"af":{"currentPeak":4,"currentHold":4,"mute":1,"txMute":1,"rfMute":1,"rxMute":1},"battery":{"known":false,"percentage":0},"warningString":"RF Mute"}\n' +
'}';
let dbgRcv = JSON.parse(dbg_data);
let dbgMap = new Map();
Object.entries(dbgRcv).forEach(entry => {
const [key,val] = entry;
dbgMap.set(key.replace('"',''), val);
});
dbgMap = new Map([...dbgMap.entries()].sort(compareIPv4mapKeys));
let dbg_str = JSON.stringify([...dbgMap]);
let commentsMap = new Map();
const dgram = require('dgram');
const http = require('http');
const fs = require('fs');
const os = require('os');
const url = require("url");
let httpServerPort = 80;
let removeReceiversAfterNoUpdateInSeconds = 60;
const udpSock = dgram.createSocket('udp4');
const udpBindAddress = findSuitableNetworkAddressForUDP(os);
const udpCommentSock = dgram.createSocket('udp4');
udpCommentSock.on("listening", () => {
const address = udpCommentSock.address();
console.log('Comment annotation interface listening on '+address.address+':'+address.port);
});
udpCommentSock.on("error", (err) => {
console.log("Comment annotation interface server error: \n" + err.stack);
udpCommentSock.close();
});
udpCommentSock.on('message', (msg, senderInfo) => {
console.log("new comment message received");
let receivedCommentsMap;
try {
receivedCommentsMap = new Map(JSON.parse(msg.toString()));
commentsMap = new Map([...commentsMap, ...receivedCommentsMap]);
} catch(e) {
console.log("udp comment socket: comments JSON parse error: "+e);
}
});
const httpServer = http.createServer();
httpServer.listen(httpServerPort);
let pushIntervalHandle;
let removeIntervalHandle;
let knownReceiversFull = new Map();
let knownReceiversShort = new Map();
httpServer.on("listening", () => {
let httpAddrInfo = httpServer.address();
if(typeof httpAddrInfo === "object")
console.log("Webapp available at "+httpAddrInfo.address+":"+httpAddrInfo.port);
});
udpSock.on("listening", () => {
const address = udpSock.address();
console.log('MCP server listening on '+address.address+':'+address.port);
});
udpSock.on("error", (err) => {
console.log("udp socket server error: \n" + err.stack);
udpSock.close();
});
httpServer.on("error", (err) => {
console.log("http server error: \n" + err.stack);
httpServer.close();
});
httpServer.on("request", httpServerRequestResponder);
udpSock.on('message', (msg, senderInfo) => {
// let msgDebug = "["+senderInfo.address+"] "+msg.toString().replace(/[\n\r]/g, ' | ')+"";
// console.log(msgDebug);
if(!knownReceiversFull.has(senderInfo.address))
addNewReceiver(udpSock, senderInfo.address);
else
updateReceiver(senderInfo.address, msg);
});
if(udpBindAddress === null) {
udpSock.bind({port: 53212}, () => {
udpSock.setBroadcast(true);
});
udpCommentSock.bind({port: 53210});
}
else {
udpSock.bind({port: 53212, address: udpBindAddress}, () => {
udpSock.setBroadcast(true);
});
udpCommentSock.bind({port: 53210, address: udpBindAddress});
}
if(!DO_DEBUG) {
sendCyclicRequest(udpSock);
pushIntervalHandle = setInterval(sendCyclicRequest, 80000, udpSock);
}
function sendCyclicRequest(conn, addr=null) {
let pushMsg = "Push 100 100 7\r";
if(addr === null)
addr = "255.255.255.255";
conn.send(pushMsg, 0, (pushMsg.length), 53212, addr, sendCallback);
}
function sendConfigRequest(conn, addr=null) {
let initMsg = "Push 0 0 1\r";
if(addr === null)
addr = "255.255.255.255";
conn.send(initMsg, 0, (initMsg.length), 53212, addr, sendCallback);
}
function sendCallback(err) {
if(err !== null)
console.log("Msg send error: "+err);
}
/**
* Lookup the comment string for a given receiver address
* @param addr IP address string of the receiver
*/
function lookupReceiverComment(addr) {
let commentStr = "["+addr+"]"; //default comment string without lookup
if(commentsMap.has(addr)) {
try {
commentStr = commentsMap.get(addr);
} catch(e) {
console.log("comment lookup error.");
}
}
else {
commentsMap.set(addr, commentStr);
}
return commentStr;
}
/**
* Check the Map of receivers and remove every entry that was last seen more than
* removeReceiversAfterNoUpdateInSeconds seconds ago
*/
function removeUnconnectedReceivers() {
let now = Date.now();
let deletedAny = false;
knownReceiversFull.forEach(function(rx, key) {
if(now > (rx.lastUpdate + removeReceiversAfterNoUpdateInSeconds * 1000)) {
deletedAny = true;
knownReceiversFull.delete(key);
knownReceiversShort.delete(key);
}
});
if(deletedAny) {
knownReceiversFull = new Map([...knownReceiversFull.entries()].sort(compareIPv4mapKeys));
knownReceiversShort = new Map([...knownReceiversShort.entries()].sort(compareIPv4mapKeys));
}
}
removeIntervalHandle = setInterval(removeUnconnectedReceivers, Math.ceil(removeReceiversAfterNoUpdateInSeconds * 1000 / 4));
/**
* Add a newly discovered receiver to the Map
* @param conn
* @param address
*/
function addNewReceiver(conn, address) {
knownReceiversFull.set(address, {"name": "unknown"});
knownReceiversShort.set(address, {"name": "unknown"});
knownReceiversFull = new Map([...knownReceiversFull.entries()].sort(compareIPv4mapKeys));
knownReceiversShort = new Map([...knownReceiversShort.entries()].sort(compareIPv4mapKeys));
sendConfigRequest(conn, address);
sendCyclicRequest(conn, address);
}
/**
*
* @param address
* @param msg
*/
function updateReceiver(address, msg) {
let receivedItemsFull = {};
let receivedItemsShort = {};
receivedItemsFull.comment = lookupReceiverComment(address);
let blocks = msg.toString().split(/[\n\r]/g);
blocks.forEach((val ) => {
let item = val.toString().split(" ");
switch(item[0].toLowerCase()) {
case "name":
receivedItemsFull.name = val.toString().replace(item[0]+" ",'').trim();
break;
case "frequency":
receivedItemsFull.freq = (parseFloat(item[1]) / 1000).toFixed(3);
break;
case "squelch":
receivedItemsFull.squelch = item[1];
break;
case "afout":
receivedItemsFull.afOut = item[1];
break;
case "rf1":
receivedItemsFull.rf1 = {min: parseInt(item[1],10), max: parseInt(item[2],10), active: (item[3] === "1")};
receivedItemsShort.rf1 = {min: receivedItemsFull.rf1.min, max: receivedItemsFull.rf1.max};
break;
case "rf2":
receivedItemsFull.rf2 = {min: parseInt(item[1],10), max: parseInt(item[2],10), active: (item[3] === "1")};
receivedItemsShort.rf2 = {min: receivedItemsFull.rf2.min, max: receivedItemsFull.rf2.max};
break;
case "states":
let muteFlag = parseInt(item[1],10);
let pilotFlag = parseInt(item[2],10);
receivedItemsFull.flags = {
lastCycleMute: !!(muteFlag & 1 > 0),
lastCycleTxMute: !!(muteFlag & 2 > 0),
lastCycleRfMute: !!(muteFlag & 4 > 0),
lastCycleRxMute: !!(muteFlag & 8 > 0)
};
receivedItemsShort.flags = {lastCycleMute: !!(muteFlag & 1 > 0)};
receivedItemsFull.lastCyclePilot = pilotFlag;
receivedItemsShort.lastCyclePilot = pilotFlag;
break;
case "rf":
receivedItemsFull.rf = {
current: parseInt(item[1],10),
antenna: (item[2] === "1") ? "1" : "2",
pilot: (item[3] === "1")
}
receivedItemsShort.rf = {
current: parseInt(item[1],10),
antenna: (item[2] === "1") ? "1" : "2"
}
break;
case "af":
let muteState = parseInt(item[3],10);
receivedItemsFull.af = {
currentPeak: parseInt(item[1],10),
currentHold: parseInt(item[2],10),
mute: !!(muteState & 1 > 0),
txMute: !!(muteState & 2 > 0),
rfMute: !!(muteState & 4 > 0),
rxMute: !!(muteState & 8 > 0)
}
receivedItemsShort.af = {currentPeak: receivedItemsFull.af.currentPeak, currentHold: receivedItemsFull.af.currentHold};
break;
case "bat":
receivedItemsFull.battery = {
known: (item[1] !== "?"),
percentage: (item[1] === "?") ? 0 : parseInt(item[1], 10)
};
receivedItemsShort.battery = receivedItemsFull.battery;
break;
case "msg":
// console.log(item[1].toString());
receivedItemsFull.warningString = item[1].toString().replaceAll("_", " ");
receivedItemsShort.warningString = receivedItemsFull.warningString;
break;
}
});
receivedItemsFull.lastUpdate = Date.now();
receivedItemsShort.lastUpdate = Date.now();
let newObj = Object.assign({}, knownReceiversFull.get(address), receivedItemsFull);
knownReceiversFull.set(address, newObj);
knownReceiversShort.set(address, Object.assign({}, knownReceiversShort.get(address), receivedItemsShort));
/* This section allows storing the results to a local file for offline debugging without receivers present */
// fs.writeFile('RxFull.json', JSON.stringify([...knownReceiversFull]), (err) => {
// if(err !== null)
// console.log("file write error: "+err);
// });
// fs.writeFile('RxShort.json', JSON.stringify([...knownReceiversShort]), (err) => {
// if(err !== null)
// console.log("file write error: "+err);
// });
}
/**
* Crawl IP addresses of all interfaces and return a suitable address to bind the MCP listener to
* @param os OS handle
* @returns {null|*} IP address or null
*/
function findSuitableNetworkAddressForUDP(os) {
const networkInterfaces = os.networkInterfaces();
for (const netIdx in networkInterfaces) {
for(const addrIdx in networkInterfaces[netIdx]) {
if(networkInterfaces[netIdx][addrIdx].family === "IPv6") // MCP is v4 only
continue;
if(networkInterfaces[netIdx][addrIdx].address.includes("127.0.0.1")) // skip localhost
continue;
/* THE FOLLOWING RULES ARE QUITE SPECIFIC FOR MY USE CASE, set your own! */
if(networkInterfaces[netIdx][addrIdx].netmask !== "255.255.255.0") // only look for /24 networks
continue;
if(networkInterfaces[netIdx][addrIdx].address.includes("192.168.0")) //skip over default update interface
continue;
return networkInterfaces[netIdx][addrIdx].address;
}
}
return null;
}
/**
* IPv4 address comparison, from https://stackoverflow.com/a/65950890
* @param addrStrA IP address A as string
* @param addrStrB IP address B as string
* @returns {number}
*/
function compareIPv4mapKeys(addrStrA, addrStrB) {
// noinspection CommaExpressionJS
const numA = Number(
addrStrA[0].split('.')
.map((num, idx) => num * Math.pow(2, (3 - idx) * 8))
.reduce((a, v) => ((a += v), a), 0)
);
// noinspection CommaExpressionJS
const numB = Number(
addrStrB[0].split('.')
.map((num, idx) => num * Math.pow(2, (3 - idx) * 8))
.reduce((a, v) => ((a += v), a), 0)
);
return numA - numB;
}
/**
* HTTP request handler, delivers webapp files from file system and JSON files by parsing the status map
* @param req http request
* @param res http response
* @returns {*}
*/
function httpServerRequestResponder(req,res) {
if(req.url.includes("rxShort.json")) {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
if(DO_DEBUG)
return res.end(dbg_str);
else
return res.end(JSON.stringify([...knownReceiversShort]));
}
else if(req.url.includes("rxFull.json")) {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
if(DO_DEBUG)
return res.end(dbg_str);
else
return res.end(JSON.stringify([...knownReceiversFull]));
}
else if(req.url.includes("rf.js")) {
fs.readFile("www/rf.js", function(err, data) {
if (err) {
res.statusCode = 500;
return res.end("File not readable.");
}
res.statusCode = 200;
res.setHeader("Content-Type", "application/javascript");
return res.end(data);
});
}
else if(req.url.includes("rf.css")) {
fs.readFile("www/rf.css", function(err, data) {
if (err) {
res.statusCode = 500;
return res.end("File not readable.");
}
res.setHeader("Content-Type", "text/css");
res.statusCode = 200;
return res.end(data);
});
}
else if(req.url.includes("rfColors.css")) {
fs.readFile("www/rfColors.css", function(err, data) {
if (err) {
res.statusCode = 500;
return res.end("File not readable.");
}
res.setHeader("Content-Type", "text/css");
res.statusCode = 200;
return res.end(data);
});
}
else if(req.url.includes("icon.png")) {
let stream = fs.createReadStream("www/icon.png");
stream.on("open", function() {
res.setHeader("Content-Type", "image/png");
res.statusCode = 200;
stream.pipe(res);
});
stream.on("error", function() {
res.statusCode = 500;
res.end("File not readable.");
});
}
else if(req.url === "/" || req.url.includes("index.html")) {
fs.readFile("www/index.html", function(err, data) {
if (err) {
res.statusCode = 500;
return res.end("File not readable.");
}
res.statusCode = 200;
return res.end(data);
});
}
else {
res.statusCode = 404;
// console.log("Request to >"+req.url+"<");
return res.end("I have no idea what you are looking for. This file does not exist.");
}
}