Files
MySensors/NodeJsController/NodeJsController.js
Henrik Ekblad c562f0f173 fix #114
2015-07-31 20:47:22 +02:00

714 lines
18 KiB
JavaScript

//const gwType = 'Ethernet';
//const gwAddress = '10.0.1.99';
//const gwPort = 9999;
const gwType = 'Serial';
//const gwPort = 'COM4';
const gwPort = '/dev/ttyAMA0';
const gwBaud = 115200;
const dbAddress = '127.0.0.1';
const dbPort = 27017;
const dbName = 'MySensorsDb';
const fwSketches = [ ];
const fwDefaultType = 0xFFFF; // index of hex file from array above (0xFFFF
const FIRMWARE_BLOCK_SIZE = 16;
const BROADCAST_ADDRESS = 255;
const NODE_SENSOR_ID = 255;
const C_PRESENTATION = 0;
const C_SET = 1;
const C_REQ = 2;
const C_INTERNAL = 3;
const C_STREAM = 4;
const V_TEMP = 0;
const V_HUM = 1;
const V_LIGHT = 2;
const V_DIMMER = 3;
const V_PRESSURE = 4;
const V_FORECAST = 5;
const V_RAIN = 6;
const V_RAINRATE = 7;
const V_WIND = 8;
const V_GUST = 9;
const V_DIRECTION = 10;
const V_UV = 11;
const V_WEIGHT = 12;
const V_DISTANCE = 13;
const V_IMPEDANCE = 14;
const V_ARMED = 15;
const V_TRIPPED = 16;
const V_WATT = 17;
const V_KWH = 18;
const V_SCENE_ON = 19;
const V_SCENE_OFF = 20;
const V_HEATER = 21;
const V_HEATER_SW = 22;
const V_LIGHT_LEVEL = 23;
const V_VAR1 = 24;
const V_VAR2 = 25;
const V_VAR3 = 26;
const V_VAR4 = 27;
const V_VAR5 = 28;
const V_UP = 29;
const V_DOWN = 30;
const V_STOP = 31;
const V_IR_SEND = 32;
const V_IR_RECEIVE = 33;
const V_FLOW = 34;
const V_VOLUME = 35;
const V_LOCK_STATUS = 36;
const I_BATTERY_LEVEL = 0;
const I_TIME = 1;
const I_VERSION = 2;
const I_ID_REQUEST = 3;
const I_ID_RESPONSE = 4;
const I_INCLUSION_MODE = 5;
const I_CONFIG = 6;
const I_PING = 7;
const I_PING_ACK = 8;
const I_LOG_MESSAGE = 9;
const I_CHILDREN = 10;
const I_SKETCH_NAME = 11;
const I_SKETCH_VERSION = 12;
const I_REBOOT = 13;
const S_DOOR = 0;
const S_MOTION = 1;
const S_SMOKE = 2;
const S_LIGHT = 3;
const S_DIMMER = 4;
const S_COVER = 5;
const S_TEMP = 6;
const S_HUM = 7;
const S_BARO = 8;
const S_WIND = 9;
const S_RAIN = 10;
const S_UV = 11;
const S_WEIGHT = 12;
const S_POWER = 13;
const S_HEATER = 14;
const S_DISTANCE = 15;
const S_LIGHT_LEVEL = 16;
const S_ARDUINO_NODE = 17;
const S_ARDUINO_REPEATER_NODE = 18;
const S_LOCK = 19;
const S_IR = 20;
const S_WATER = 21;
const S_AIR_QUALITY = 22;
const ST_FIRMWARE_CONFIG_REQUEST = 0;
const ST_FIRMWARE_CONFIG_RESPONSE = 1;
const ST_FIRMWARE_REQUEST = 2;
const ST_FIRMWARE_RESPONSE = 3;
const ST_SOUND = 4;
const ST_IMAGE = 5;
const P_STRING = 0;
const P_BYTE = 1;
const P_INT16 = 2;
const P_UINT16 = 3;
const P_LONG32 = 4;
const P_ULONG32 = 5;
const P_CUSTOM = 6;
var fs = require('fs');
var path = require('path');
var requestify = require('requestify');
var appendedString="";
function crcUpdate(old, value) {
var c = old ^ value;
for (var i = 0; i < 8; ++i) {
if ((c & 1) > 0)
c = ((c >> 1) ^ 0xA001);
else
c = (c >> 1);
}
return c;
}
function pullWord(arr, pos) {
return arr[pos] + 256 * arr[pos + 1];
}
function pushWord(arr, val) {
arr.push(val & 0x00FF);
arr.push((val >> 8) & 0x00FF);
}
function pushDWord(arr, val) {
arr.push(val & 0x000000FF);
arr.push((val >> 8) & 0x000000FF);
arr.push((val >> 16) & 0x000000FF);
arr.push((val >> 24) & 0x000000FF);
}
function loadFirmware(fwtype, fwversion, sketch, db) {
var filename = path.basename(sketch);
console.log("compiling firmware: " + filename);
var req = {
files: [{
filename: filename,
content: fs.readFileSync(sketch).toString()
}],
format: "hex",
version: "105",
build: {
mcu: "atmega328p",
f_cpu: "16000000L",
core: "arduino",
variant: "standard"
}
};
requestify.post('https://codebender.cc/utilities/compile/', req).then(function(res) {
var body = JSON.parse(res.getBody());
if (body.success) {
console.log("loading firmware: " + filename);
fwdata = [];
var start = 0;
var end = 0;
var pos = 0;
var hex = body.output.split("\n");
for(l in hex) {
line = hex[l].trim();
if (line.length > 0) {
while (line.substring(0, 1) != ":")
line = line.substring(1);
var reclen = parseInt(line.substring(1, 3), 16);
var offset = parseInt(line.substring(3, 7), 16);
var rectype = parseInt(line.substring(7, 9), 16);
var data = line.substring(9, 9 + 2 * reclen);
var chksum = parseInt(line.substring(9 + (2 * reclen), 9 + (2 * reclen) + 2), 16);
if (rectype == 0) {
if ((start == 0) && (end == 0)) {
if (offset % 128 > 0)
throw new Error("error loading hex file - offset can't be devided by 128");
start = offset;
end = offset;
}
if (offset < end)
throw new Error("error loading hex file - offset lower than end");
while (offset > end) {
fwdata.push(255);
pos++;
end++;
}
for (var i = 0; i < reclen; i++) {
fwdata.push(parseInt(data.substring(i * 2, (i * 2) + 2), 16));
pos++;
}
end += reclen;
}
}
}
var pad = end % 128; // ATMega328 has 64 words per page / 128 bytes per page
for (var i = 0; i < 128 - pad; i++) {
fwdata.push(255);
pos++;
end++;
}
var blocks = (end - start) / FIRMWARE_BLOCK_SIZE;
var crc = 0xFFFF;
for (var i = 0; i < blocks * FIRMWARE_BLOCK_SIZE; ++i) {
var v = crc;
crc = crcUpdate(crc, fwdata[i]);
}
db.collection('firmware', function(err, c) {
c.update({
'type': fwtype,
'version': fwversion
}, {
$set: {
'filename': filename,
'blocks': blocks,
'crc': crc,
'data': fwdata
}
}, {
upsert: true
}, function(err, result) {
if (err)
console.log('Error writing firmware to database');
});
});
console.log("loading firmware done. blocks: " + blocks + " / crc: " + crc);
} else {
console.log("error: %j", res.body);
}
});
}
/*
function decode(msg) {
var msgs = msg.toString().split(";");
rsender = +msgs[0];
rsensor = +msgs[1];
rcommand = +msgs[2];
rtype = +msgs[3];
var pl = msgs[4].trim();
rpayload = [];
for (var i = 0; i < pl.length; i+=2) {
var b = parseInt(pl.substring(i, i + 2), 16);
rpayload.push(b);
}
}
*/
function encode(destination, sensor, command, acknowledge, type, payload) {
var msg = destination.toString(10) + ";" + sensor.toString(10) + ";" + command.toString(10) + ";" + acknowledge.toString(10) + ";" + type.toString(10) + ";";
if (command == 4) {
for (var i = 0; i < payload.length; i++) {
if (payload[i] < 16)
msg += "0";
msg += payload[i].toString(16);
}
} else {
msg += payload;
}
msg += '\n';
return msg.toString();
}
function saveProtocol(sender, payload, db) {
db.collection('node', function(err, c) {
c.update({
'id': sender
}, {
$set: {
'protocol': payload
}
}, {
upsert: true
}, function(err, result) {
if (err)
console.log("Error writing protocol to database");
});
});
}
function saveSensor(sender, sensor, type, db) {
db.collection('node', function(err, c) {
c.update({
'id': sender
}, {
$addToSet: {
sensor: type
}
}, function(err, result) {
if (err)
console.log("Error writing sensor to database");
});
});
}
function saveValue(sender, sensor, type, payload, db) {
var cn = "Value-" + sender.toString() + "-" + sensor.toString();
db.createCollection(cn, function(err, c) {
c.save({
'timestamp': new Date().getTime(),
'type': type,
'value': payload
}, function(err, result) {
if (err)
console.log("Error writing value to database");
});
});
}
function saveBatteryLevel(sender, payload, db) {
var cn = "BatteryLevel-" + sender.toString();
db.createCollection(cn, function(err, c) {
c.save({
'timestamp': new Date().getTime(),
'value': payload
}, function(err, result) {
if (err)
console.log("Error writing battery level to database");
});
});
}
function saveSketchName(sender, payload, db) {
db.collection('node', function(err, c) {
c.update({
'id': sender
}, {
$set: {
'sketchName': payload
}
}, function(err, result) {
if (err)
console.log("Error writing sketch name to database");
});
});
}
function saveSketchVersion(sender, payload, db) {
db.collection('node', function(err, c) {
c.update({
'id': sender
}, {
$set: {
'sketchVersion': payload
}
}, function(err, result) {
if (err)
console.log("Error writing sketch version to database");
});
});
}
function sendTime(destination, sensor, gw) {
var payload = new Date().getTime()/1000;
var command = C_INTERNAL;
var acknowledge = 0; // no ack
var type = I_TIME;
var td = encode(destination, sensor, command, acknowledge, type, payload);
console.log('-> ' + td.toString());
gw.write(td);
}
function sendNextAvailableSensorId(db, gw) {
db.collection('node', function(err, c) {
c.find({
$query: { },
$orderby: {
'id': 1
}
}).toArray(function(err, results) {
if (err)
console.log('Error finding nodes');
var id = 1;
for (var i = 0; i < results.length; i++)
if (results[i].id > i + 1) {
id = i + 1;
break;
}
if (id < 255) {
c.save({
'id': id
}, function(err, result) {
if (err)
console.log('Error writing node to database');
var destination = BROADCAST_ADDRESS;
var sensor = NODE_SENSOR_ID;
var command = C_INTERNAL;
var acknowledge = 0; // no ack
var type = I_ID_RESPONSE;
var payload = id;
var td = encode(destination, sensor, command, acknowledge, type, payload);
console.log('-> ' + td.toString());
gw.write(td);
});
}
});
});
}
function sendConfig(destination, gw) {
var payload = "M";
var sensor = NODE_SENSOR_ID;
var command = C_INTERNAL;
var acknowledge = 0; // no ack
var type = I_CONFIG;
var td = encode(destination, sensor, command, acknowledge, type, payload);
console.log('-> ' + td.toString());
gw.write(td);
}
function sendFirmwareConfigResponse(destination, fwtype, fwversion, db, gw) {
// keep track of type/versin info for each node
// at the same time update the last modified date
// could be used to remove nodes not seen for a long time etc.
db.collection('node', function(err, c) {
c.update({
'id': destination
}, {
$set: {
'type': fwtype,
'version': fwversion,
'reboot': 0
}
}, {
upsert: true
}, function(err, result) {
if (err)
console.log("Error writing node type and version to database");
});
});
if (fwtype == 0xFFFF) {
// sensor does not know which type / blank EEPROM
// take predefined type (ideally selected in UI prior to connection of new sensor)
if (fwDefaultType == 0xFFFF)
throw new Error('No default sensor type defined');
fwtype = fwDefaultType;
}
db.collection('firmware', function(err, c) {
c.findOne({
$query: {
'type': fwtype
},
$orderby: {
'version': -1
}
}, function(err, result) {
if (err)
console.log('Error finding firmware for type ' + fwtype);
else if (!result)
console.log('No firmware found for type ' + fwtype);
else {
var payload = [];
pushWord(payload, result.type);
pushWord(payload, result.version);
pushWord(payload, result.blocks);
pushWord(payload, result.crc);
var sensor = NODE_SENSOR_ID;
var command = C_STREAM;
var acknowledge = 0; // no ack
var type = ST_FIRMWARE_CONFIG_RESPONSE;
var td = encode(destination, sensor, command, acknowledge, type, payload);
console.log('-> ' + td.toString());
gw.write(td);
}
});
});
}
function sendFirmwareResponse(destination, fwtype, fwversion, fwblock, db, gw) {
db.collection('firmware', function(err, c) {
c.findOne({
'type': fwtype,
'version': fwversion
}, function(err, result) {
if (err)
console.log('Error finding firmware version ' + fwversion + ' for type ' + fwtype);
var payload = [];
pushWord(payload, result.type);
pushWord(payload, result.version);
pushWord(payload, fwblock);
for (var i = 0; i < FIRMWARE_BLOCK_SIZE; i++)
payload.push(result.data[fwblock * FIRMWARE_BLOCK_SIZE + i]);
var sensor = NODE_SENSOR_ID;
var command = C_STREAM;
var acknowledge = 0; // no ack
var type = ST_FIRMWARE_RESPONSE;
var td = encode(destination, sensor, command, acknowledge, type, payload);
console.log('-> ' + td.toString());
gw.write(td);
});
});
}
function saveRebootRequest(destination, db) {
db.collection('node', function(err, c) {
c.update({
'id': destination
}, {
$set: {
'reboot': 1
}
}, function(err, result) {
if (err)
console.log("Error writing reboot request to database");
});
});
}
function checkRebootRequest(destination, db, gw) {
db.collection('node', function(err, c) {
c.find({
'id': destination
}, function(err, item) {
if (err)
console.log('Error checking reboot request');
else if (item.reboot == 1)
sendRebootMessage(destination, gw);
});
});
}
function sendRebootMessage(destination, gw) {
var sensor = NODE_SENSOR_ID;
var command = C_INTERNAL;
var acknowledge = 0; // no ack
var type = I_REBOOT;
var payload = "";
var td = encode(destination, sensor, command, acknowledge, type, payload);
console.log('-> ' + td.toString());
gw.write(td);
}
function appendData(str, db, gw) {
pos=0;
while (str.charAt(pos) != '\n' && pos < str.length) {
appendedString=appendedString+str.charAt(pos);
pos++;
}
if (str.charAt(pos) == '\n') {
rfReceived(appendedString.trim(), db, gw);
appendedString="";
}
if (pos < str.length) {
appendData(str.substr(pos+1,str.length-pos-1), db, gw);
}
}
function rfReceived(data, db, gw) {
if ((data != null) && (data != "")) {
console.log('<- ' + data);
// decoding message
var datas = data.toString().split(";");
var sender = +datas[0];
var sensor = +datas[1];
var command = +datas[2];
var ack = +datas[3];
var type = +datas[4];
var rawpayload="";
if (datas[5]) {
rawpayload = datas[5].trim();
}
var payload;
if (command == C_STREAM) {
payload = [];
for (var i = 0; i < rawpayload.length; i+=2)
payload.push(parseInt(rawpayload.substring(i, i + 2), 16));
} else {
payload = rawpayload;
}
// decision on appropriate response
switch (command) {
case C_PRESENTATION:
if (sensor == NODE_SENSOR_ID)
saveProtocol(sender, payload, db);
saveSensor(sender, sensor, type, db);
break;
case C_SET:
saveValue(sender, sensor, type, payload, db);
break;
case C_REQ:
break;
case C_INTERNAL:
switch (type) {
case I_BATTERY_LEVEL:
saveBatteryLevel(sender, payload, db);
break;
case I_TIME:
sendTime(sender, sensor, gw);
break;
case I_VERSION:
break;
case I_ID_REQUEST:
sendNextAvailableSensorId(db, gw);
break;
case I_ID_RESPONSE:
break;
case I_INCLUSION_MODE:
break;
case I_CONFIG:
sendConfig(sender, gw);
break;
case I_PING:
break;
case I_PING_ACK:
break;
case I_LOG_MESSAGE:
break;
case I_CHILDREN:
break;
case I_SKETCH_NAME:
saveSketchName(sender, payload, db);
break;
case I_SKETCH_VERSION:
saveSketchVersion(sender, payload, db);
break;
case I_REBOOT:
break;
}
break;
case C_STREAM:
switch (type) {
case ST_FIRMWARE_CONFIG_REQUEST:
var fwtype = pullWord(payload, 0);
var fwversion = pullWord(payload, 2);
sendFirmwareConfigResponse(sender, fwtype, fwversion, db, gw);
break;
case ST_FIRMWARE_CONFIG_RESPONSE:
break;
case ST_FIRMWARE_REQUEST:
var fwtype = pullWord(payload, 0);
var fwversion = pullWord(payload, 2);
var fwblock = pullWord(payload, 4);
sendFirmwareResponse(sender, fwtype, fwversion, fwblock, db, gw);
break;
case ST_FIRMWARE_RESPONSE:
break;
case ST_SOUND:
break;
case ST_IMAGE:
break;
}
break;
}
checkRebootRequest(sender, db, gw);
}
}
var dbc = require('mongodb').MongoClient;
dbc.connect('mongodb://' + dbAddress + ':' + dbPort + '/' + dbName, function(err, db) {
if(err) {
console.log('Error connecting to database at mongodb://' + dbAddress + ':' + dbPort + '/' + dbName);
return;
}
console.log('Connected to database at mongodb://' + dbAddress + ':' + dbPort + '/' + dbName);
db.createCollection('node', function(err, collection) { });
db.createCollection('firmware', function(err, collection) { });
// ToDo : check for new hex files / only load if new / get type and version from filename
for (var i = 0; i < fwSketches.length; i++)
loadFirmware(i, 1, fwSketches[i], db);
var gw;
if (gwType == 'Ethernet') {
gw = require('net').Socket();
gw.connect(gwPort, gwAddress);
gw.setEncoding('ascii');
gw.on('connect', function() {
console.log('connected to ethernet gateway at ' + gwAddress + ":" + gwPort);
}).on('data', function(rd) {
appendData(rd.toString(), db, gw);
}).on('end', function() {
console.log('disconnected from gateway');
}).on('error', function() {
console.log('connection error - trying to reconnect');
gw.connect(gwPort, gwAddress);
gw.setEncoding('ascii');
});
} else if (gwType == 'Serial') {
var SerialPort = require('serialport').SerialPort;
gw = new SerialPort(gwPort, { baudrate: gwBaud });
gw.open();
gw.on('open', function() {
console.log('connected to serial gateway at ' + gwPort);
}).on('data', function(rd) {
appendData(rd.toString(), db, gw);
}).on('end', function() {
console.log('disconnected from gateway');
}).on('error', function() {
console.log('connection error - trying to reconnect');
gw.open();
});
} else {
throw new Error('unknown Gateway type');
}
setInterval(function() {
sendTime(BROADCAST_ADDRESS, NODE_SENSOR_ID, gw);
}, 5*60*1000);
});