mbed-simulator
Version:
Mbed OS 5 simulator
434 lines (349 loc) • 13.7 kB
JavaScript
const express = require('express');
const ips = require('./get_ips')();
const bodyParser = require('body-parser');
const net = require('net');
const dgram = require('dgram');
const hbs = require('hbs');
const Path = require('path');
const fs = require('fs');
const compile = require('./compile');
const { exists } = require('../build-tools/helpers');
const promisify = require('es6-promisify').promisify;
const udp = require('dgram');
const ttnGwClient = udp.createSocket('udp4');
const mac = require('getmac');
const timesyncServer = require('timesync/server');
let startupTs = Date.now();
module.exports = function(outFolder, port, callback) {
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
app.set('view engine', 'html');
app.set('views', Path.join(__dirname, '..', 'viewer'));
app.engine('html', hbs.__express);
app.use('/out', express.static(outFolder));
app.use('/demos', express.static(Path.join(__dirname, '..', 'demos')));
app.use('/peripherals', express.static(Path.join(__dirname, '..', 'mbed-simulator-hal', 'peripherals')));
app.use('/timesync', timesyncServer.requestHandler);
app.use(express.static(Path.join(__dirname, '..', 'viewer')));
app.use(bodyParser.json());
app.get('/api/network/ip', (req, res, next) => {
if (!ips.length) return res.send('');
res.send(ips[0].iface.address);
});
app.get('/api/network/mac', (req, res, next) => {
if (!ips.length) return res.send('');
res.send(ips[0].iface.mac);
});
app.get('/api/network/netmask', (req, res, next) => {
if (!ips.length) return res.send('');
res.send(ips[0].iface.netmask);
});
let sockets = {};
let socketIx = 0;
app.post('/api/network/socket_open', (req, res, next) => {
if (req.body.protocol === 0) { // TCP
console.log('Opening new TCP socket');
let s = sockets[++socketIx] = new net.Socket();
s.packets = Buffer.from([]);
res.send(socketIx + '');
s.on('data', data => {
console.log('Received TCP packet on socket', socketIx, data);
s.packets = Buffer.concat([s.packets, data]);
});
}
else if (req.body.protocol === 1) { // UDP
console.log('Opening new UDP socket');
let s = sockets[++socketIx] = dgram.createSocket('udp4');
s.packets = Buffer.from([]);
res.send(socketIx + '');
s.on('message', (msg, rinfo) => {
console.log('Received UDP packet on socket', socketIx, msg, rinfo);
s.packets = Buffer.concat([s.packets, msg]);
// s.packets.push({ msg: msg, rinfo: rinfo });
});
}
else {
res.send('' + -1);
}
});
app.post('/api/network/socket_close', (req, res, next) => {
console.log('Closing socket', req.body.id);
if (!sockets[req.body.id]) {
return res.send('' + -3001);
}
let s = sockets[req.body.id];
if (s instanceof net.Socket) {
s.destroy();
}
else {
s.close();
}
delete sockets[req.body.id];
res.send('0');
});
app.post('/api/network/socket_send', (req, res, next) => {
console.log('Sending socket', req.body.id, req.body.data.length, 'bytes');
if (!sockets[req.body.id]) {
return res.send('' + -3001);
}
let s = sockets[req.body.id];
if (s instanceof net.Socket) {
s.write(Buffer.from(req.body.data));
}
else {
s.send(Buffer.from(req.body.data), s.port, s.hostname);
}
res.send(req.body.data.length + '');
});
app.post('/api/network/socket_connect', (req, res, next) => {
console.log('Connecting socket', req.body.id, req.body.hostname, req.body.port);
if (!sockets[req.body.id]) {
return res.send('' + -3001);
}
let s = sockets[req.body.id];
if (s instanceof net.Socket) {
s.connect(req.body.port, req.body.hostname);
}
else {
s.port = req.body.port;
s.hostname = req.body.hostname;
}
res.send('0');
});
app.post('/api/network/socket_recv', (req, res, next) => {
console.log('Receiving from socket', req.body.id, 'max size', req.body.size);
if (!sockets[req.body.id]) {
return res.send('' + -3001);
}
let s = sockets[req.body.id];
function send() {
let buff = [].slice.call(s.packets.slice(0, req.body.size));
s.packets = s.packets.slice(req.body.size);
res.send(JSON.stringify(buff));
}
if (s.packets.length > 0) {
return send();
}
// if no data... need to block until there is
let iv = setInterval(() => {
if (s.packets.length > 0) {
clearInterval(iv);
send();
}
}, 33);
});
app.get('/view/:script', (req, res, next) => {
if (/\.js\.mem$/.test(req.params.script)) {
return res.sendFile(Path.join(outFolder, req.params.script));
}
if (/\.js\.map$/.test(req.params.script)) {
return res.sendFile(Path.join(outFolder, req.params.script));
}
if (/\.data$/.test(req.params.script)) {
return res.sendFile(Path.join(outFolder, req.params.script));
}
(async function() {
let jshal = [];
let jsui = [];
let peripherals = [];
let componentsPath = Path.join(outFolder, req.params.script + '.js.components');
if (await exists(componentsPath)) {
let components = JSON.parse(await promisify(fs.readFile)(componentsPath, 'utf-8'));
jshal = jshal.concat(components.jshal);
jsui = jsui.concat(components.jsui);
peripherals = peripherals.concat(components.peripherals);
}
function normalize(a) {
return a.map(f => {
if (f.indexOf('mbed-simulator-hal/peripherals/') === 0) {
return f.replace(/^mbed-simulator-hal/, '');
}
return '/out/' + f;
}).map(f => { return { script: f } });
}
// map to proper route
jshal = normalize(jshal);
jsui = normalize(jsui);
res.render('viewer.html', {
script: req.params.script,
jshal: jshal,
jsui: jsui,
peripherals: JSON.stringify(peripherals)
});
})().catch(err => {
return next(err);
});
});
let tokenVal = 0;
function getNextToken() {
tokenVal++;
if (tokenVal > 255*255) tokenVal = 0;
return [ tokenVal >> 8 & 0xff, tokenVal & 0xff ];
}
let gwId = [0, 0, 0, 0, 0, 0, 0, 0];
app.post('/api/lora/send', (req, res, next) => {
if (!req.body.payload) return next('Missing body.payload');
if (!req.body.host) return next('Missing body.host');
if (!req.body.port) return next('Missing body.port');
if (!req.body.freq) return next('Missing body.freq');
if (!req.body.bandwidth) return next('Missing body.bandwidth');
if (!req.body.datarate) return next('Missing body.datarate');
let [ t1, t2 ] = getNextToken();
let buff = Buffer.from([
0x02, // protocol version
t1, t2, // random token
0x0, // PUSH_DATA
gwId[0], gwId[1], gwId[2], gwId[3], gwId[4], gwId[5], gwId[6], gwId[7], // gw mac address
]);
let payload = Buffer.from(req.body.payload);
let bw = 'BW125';
switch (req.body.bandwidth) {
case 7: bw = 'BW125'; break;
case 8: bw = 'BW250'; break;
case 9: bw = 'BW500'; break;
// todo
}
let sf = 'SF7';
switch (req.body.datarate) {
case 7: sf = 'SF7'; break;
case 8: sf = 'SF8'; break;
case 9: sf = 'SF9'; break;
case 10: sf = 'SF10'; break;
case 11: sf = 'SF11'; break;
case 12: sf = 'SF12'; break;
}
// this happens quite fast... every 72 minutes.
if ((Date.now() - startupTs) * 1000 > 0xffffffff) {
console.log('startupTs overflown');
startupTs = Date.now();
}
// @todo: fix this
let msg = {
"rxpk": [{
"time": new Date().toISOString(),
// this needs to be uint32_t in us... it's not a lot
"tmst": (Date.now() - startupTs) * 1000,
"chan": 2,
"rfch": 0,
"freq": req.body.freq / 1000 / 1000,
"stat": 1,
"modu": "LORA",
"datr": sf + bw,
"codr": "4/6",
"rssi": -35,
"lsnr": 5,
"size": payload.length,
"data": payload.toString('base64')
}]
};
console.log('sending', msg);
buff = Buffer.concat([ buff, Buffer.from(JSON.stringify(msg))]);
console.log('[TTNGW] Sending', buff);
ttnGwClient.send(buff, req.body.port, req.body.host, function(err) {
if (err) return next(err);
res.send('OK');
});
});
app.get('/', (req, res, next) => {
res.render('simulator.html');
});
let compilationId = 0;
app.post('/compile', (req, res, next) => {
let id = compilationId++;
console.time('compile' + id);
compile(req.body.code, outFolder).then(name => {
console.timeEnd('compile' + id);
console.log('Compilation succeeded', id);
res.send(name);
}).catch(err => {
console.timeEnd('compile' + id);
console.log('Compilation failed', id, err);
res.status(500).send(err);
});
});
ttnGwClient.on('message',function(msg, info) {
if (msg[0] != 0x2) return; // not right protocol
let id1 = msg[1];
let id2 = msg[2];
let action = msg[3];
if (action === 0x03) { // PULL_RESP
let tx_ack = Buffer.from([ 0x02, id1, id2, 0x05 /*TX_ACK*/, gwId[0], gwId[1], gwId[2], gwId[3], gwId[4], gwId[5], gwId[6], gwId[7] ]);
ttnGwClient.send(tx_ack, 1700, 'router.eu.thethings.network', function(err) {
console.log('[TTNGW] TX_ACK OK');
});
var data = JSON.parse(msg.slice(4).toString('utf-8'));
var buff = new Buffer(data.txpk.data, 'base64');
let sf = data.txpk.datr.match(/SF(\d+)/)[1];
let bw1 = data.txpk.datr.match(/BW(\d+)/)[1];
let bw = 7;
switch (bw1) {
case '125': bw = 7; break;
case '250': bw = 8; break;
case '500': bw = 9; break;
}
console.log('[TTNGW] got downlink msg', msg, data);
let delay = 0;
// immediate?
if (data.txpk.imme) {
delay = 0;
}
else if (!data.txpk.tmst) {
console.warn('tmst missing from txpk');
delay = 0;
}
else {
let now = Date.now() - startupTs;
let tts = (data.txpk.tmst / 1000) - now;
console.log('time to send is', tts);
if (tts < 0 || tts > 5000) {
console.log('tts invalid');
delay = 0;
}
else {
// time to start...
delay = tts;
}
}
io.sockets.emit('lora-downlink', {
data: Array.from(buff),
freq: data.txpk.freq * 1000 * 1000,
modulation: data.txpk.modu,
datarate: Number(sf),
bandwidth: bw,
sendTs: Date.now() + delay
});
}
else if (action === 0x04) { //PULL_DATA_OK
// ignore...
}
else {
console.log('[TTNGW] Received %d bytes from %s:%d',msg.length, info.address, info.port);
console.log('[TTNGW] Message:', msg);
}
});
setInterval(() => {
let [ t1, t2 ] = getNextToken();
let pull_data = Buffer.from([ 0x02, t1, t2, 0x02 /*PULL_DATA*/, gwId[0], gwId[1], gwId[2], gwId[3], gwId[4], gwId[5], gwId[6], gwId[7] ]);
ttnGwClient.send(pull_data, 1700, 'router.eu.thethings.network', function(err) {
// console.log('PULL_DATA OK');
});
}, 5000);
mac.getMac(function(err, m) {
if (err) {
return console.error('Could not find MAC address... Disabling LoRa simulation');
}
gwId = m.split(':').map(d => parseInt(d, 16));
gwId.splice(3, 0, 0x0);
gwId.splice(3, 0, 0x0);
console.log('LoRaWAN Gateway ID is', gwId.map(d => {
let v = d.toString(16);
if (v.length === 1) return '0' + v;
return v;
}).join(':'), '-', `Make sure the gateway registered in The Things Network running the *legacy packet forwarder*`);
});
server.listen(port, process.env.HOST || '0.0.0.0', function () {
console.log('Web server listening on port %s!', port);
callback();
});
};