mavlink
Version:
Encode and decode MAVLink messages
630 lines (531 loc) • 18.8 kB
JavaScript
var fs = require('fs'),
xml2js = require('xml2js');
var EventEmitter = require('events').EventEmitter;
var parser = new xml2js.Parser();
var DEBUG = 0;
//Container for message information
var mavlinkMessage = function(buffer) {
//Reported length
this.length = buffer[1];
//Sequence number
this.sequence = buffer[2];
//System ID
this.system = buffer[3];
//Component ID
this.component = buffer[4];
//Message ID
this.id = buffer[5];
//Message payload buffer
this.payload = new Buffer(this.length);
buffer.copy(this.payload,0,6,6+this.length);
//Checksum
this.checksum = buffer.readUInt16LE(this.length+6);
//Whole message as a buffer
this.buffer = new Buffer(this.length + 8);
buffer.copy(this.buffer,0,0,8+this.length);
}
var mavlink = function(sysid, compid, version, definitions) {
EventEmitter.call(this);
//MAVLink Version, default to v1.0
this.version = version || "v1.0";
//Definitions to load, default to common and APM
var defs = definitions || ["common", "ardupilotmega"];
//ID's, default to zeros which mean return all messages (but cannot transmit)
this.sysid = sysid || 0;
this.compid = compid || 0;
//Create receive message buffer
this.buffer = new Buffer(512);
this.bufferIndex = 0;
this.messageLength = 0;
//Send message sequence
this.sequence = 0;
//Message definitions
this.definitions = new Array();
this.messagesByID = new Array(255);
this.messagesByName = new Object();
this.enums = new Array();
//Add definitions to be loaded
for (var i = 0; i<defs.length; i++) {
this.addDefinition(defs[i]);
}
//Initialise message checksums
this.messageChecksums = new Array();
if (DEBUG) {
console.log("MAVLink: Loading definitions");
}
//Load definitions
this.loadDefinitions();
//Initialise counter for outgoing messages
this.lastCounter = 0;
};
mavlink.super_ = EventEmitter;
mavlink.prototype = Object.create(EventEmitter.prototype, {
constructor: {
value: mavlink,
enumerable: false
}
});
//Add new definitions to the array
mavlink.prototype.addDefinition = function(definition) {
this.definitions[this.definitions.length] = definition;
if (DEBUG) {
console.log("MAVLink: Added " + definition + " definition");
}
}
//Add new message to messages array, create duplicate arrays to allow lookup by ID or name
mavlink.prototype.addMessage = function(message) {
this.messagesByID[message.$.id] = message;
this.messagesByName[message.$.name] = message;
if (DEBUG) {
console.log("MAVLink: Added " + message.$.name + " message");
}
}
//Add new enum to enums array
mavlink.prototype.addEnum = function(en) {
this.enums[this.enums.length] = en;
if (DEBUG) {
console.log("MAVLink: Added " + en.$.name + " enum");
}
}
//Add all messages and calculate checksums for v1.0
mavlink.prototype.addMessages = function(messages) {
for (var j = 0; j < messages.length; j++) {
//If we're v1.0 then calculate message checksums
if (this.version == "v1.0") {
this.messageChecksums[messages[j].$.id] = this.calculateMessageChecksum(messages[j]);
}
this.addMessage(messages[j]);
}
}
//Add all enums
mavlink.prototype.addEnums = function(enums) {
for (var j = 0; j < enums.length; j++) {
this.addEnum(enums[j]);
}
}
//Allow look-up of message IDs by name
mavlink.prototype.getMessageID = function(name) {
if (this.messagesByName[name] !== undefined) {
return this.messagesByName[name].$.id;
}
return -1;
}
//Allow look-up of message name from ID
mavlink.prototype.getMessageName = function(id) {
if (this.messagesByID[id] !== undefined) {
return this.messagesByID[id].$.name;
}
return "";
}
//Load definitions from the XML files
mavlink.prototype.loadDefinitions = function() {
//Track how many files we've parsed
var parsed = 0;
//Loop over all definitions present, load them in turn
for (var i = 0; i<this.definitions.length; i++) {
//self invoking function to preserve loop values
(function(self, i){
if (DEBUG) {
console.log("MAVLink: Reading " + self.definitions[i] + ".xml");
}
//Read the XML file and parse it when loaded
fs.readFile(__dirname + '/mavlink/message_definitions/' + self.version + '/' + self.definitions[i] + '.xml', function(err, data) {
//Pass XML data to the parser
parser.parseString(data, function (err, result) {
if (DEBUG) {
console.log("MAVLink: Parsing " + self.definitions[i] + ".xml");
}
//Extract the arrays of enums and messages
if (DEBUG) {
console.log("MAVLink: " + self.definitions[i] + " enums");
}
self.addEnums(result['mavlink']['enums'][0]['enum']);
if (DEBUG) {
console.log("MAVLink: " + self.definitions[i] + " messages");
}
self.addMessages(result['mavlink']['messages'][0].message);
//When all files has been parsed, emit event
if (parsed++ == self.definitions.length-1) {
self.emit("ready");
}
});
});
})(this,i); //Call of self invoking function
}
};
//Function to determine the size of the fields data type
mavlink.prototype.fieldTypeLength = function(field) {
//Define all the lengths
var typeLengths = {
'float' : 4,
'double' : 8,
'char' : 1,
'int8_t' : 1,
'uint8_t' : 1,
'uint8_t_mavlink_version' : 1,
'int16_t' : 2,
'uint16_t' : 2,
'int32_t' : 4,
'uint32_t' : 4,
'int64_t' : 8,
'uint64_t' : 8,
}
return typeLengths[field.$.type.replace("[", " ").replace("]", " ").split(" ")[0]];
}
//Function to determine the total size of a field ([type size] x [array size])
mavlink.prototype.fieldLength = function(field) {
//Get the types size
var typeLength = this.fieldTypeLength(field);
//Split up the field name to find array size
var fieldSplit = field.$.type.replace("[", " ").replace("]", " ").split(" ");
//For each element after the type name (>1), multiply up
for (var i = 1; i<fieldSplit.length; i++) {
if (fieldSplit[i] != "") {
typeLength *= fieldSplit[i];
}
}
return typeLength;
}
//Order fields by type size
mavlink.prototype.orderFields = function(message) {
message.payloadLength = 0;
//First make a few corrections
for (var i=0; i<message.field.length; i++) {
//add initial position in XML to preserve this if sizes equal (see sort function below)
message.field[i].initialPos = i;
//change a few types
if (message.field[i].$.type == 'uint8_t_mavlink_version') {
message.field[i].$.type = 'uint8_t';
}
if (message.field[i].$.type == 'array') {
message.field[i].$.type = 'int8_t';
}
//Calculate some useful lengths
message.field[i].length = this.fieldLength(message.field[i]);
message.field[i].typeLength = this.fieldTypeLength(message.field[i]);
message.field[i].arrayLength = message.field[i].length/message.field[i].typeLength;
message.payloadLength += message.field[i].length;
}
//Sort fields by type length
message.field.sort(function(a, b){
//Determine lengths of a and b
var lenA = a.typeLength;
var lenB = b.typeLength;
//if lengths are equal, preserve initial ordering
if (lenA == lenB) {
return a.initialPos - b.initialPos;
} else {
//otherwise reverse sort on size
return lenB-lenA;
}
})
}
//Implementation of X25 checksum from mavutil.py
mavlink.prototype.calculateChecksum = function(buffer) {
checksum = 0xffff;
for (var i = 0; i < buffer.length; i++) {
var tmp = buffer[i] ^ (checksum & 0xff);
tmp = (tmp ^ (tmp<<4)) & 0xFF;
checksum = (checksum>>8) ^ (tmp<<8) ^ (tmp<<3) ^ (tmp>>4);
checksum = checksum & 0xFFFF;
}
return checksum;
}
//Determine message checksums, based on message name, field names, types and sizes
mavlink.prototype.calculateMessageChecksum = function(message) {
//First order fields
this.orderFields(message);
var checksumString = message.$.name + " ";
for (var i = 0; i < message.field.length; i++) {
var type = message.field[i].$.type.replace("[", " ").replace("]", " ").split(" ");
checksumString += type[0] + " ";
checksumString += message.field[i].$.name + " ";
if (type[1] !== undefined) {
checksumString += String.fromCharCode(type[1]);
}
}
var checksum = this.calculateChecksum(new Buffer(checksumString));
return (checksum&0xFF) ^ (checksum>>8);
}
//Function to return start charater depending on version
mavlink.prototype.startCharacter = function() {
if (this.version == "v1.0") {
return 0xFE;
} else if (this.version == "v0.9") {
return 0x55;
}
}
mavlink.prototype.parseChar = function(ch) {
//If we have no data yet, look for start character
if (this.bufferIndex == 0 && ch == this.startCharacter()) {
this.buffer[this.bufferIndex] = ch;
this.bufferIndex++;
return;
}
//Determine packet length
if (this.bufferIndex == 1) {
this.buffer[this.bufferIndex] = ch;
this.messageLength = ch;
this.bufferIndex++;
return;
}
//Receive everything else
if (this.bufferIndex > 1 && this.bufferIndex < this.messageLength + 8) {
this.buffer[this.bufferIndex] = ch;
this.bufferIndex++;
}
//If we're at the end of the packet, see if it's valid
if (this.bufferIndex == this.messageLength + 8) {
if (this.version == "v1.0") {
//Buffer for checksummable data
var crc_buf = new Buffer(this.messageLength+6);
this.buffer.copy(crc_buf,0,1,this.messageLength+6);
//Add the message checksum on the end
crc_buf[crc_buf.length-1] = this.messageChecksums[this.buffer[5]];
} else {
//Buffer for checksummable data
var crc_buf = new Buffer(this.messageLength+5);
this.buffer.copy(crc_buf,0,1,this.messageLength+6);
}
//Test the checksum
if (this.calculateChecksum(crc_buf) == this.buffer.readUInt16LE(this.messageLength+6)) {
//If checksum is good but sequence is screwed, fire off an event
if (this.buffer[2] > 0 && this.buffer[2] - this.lastCounter != 1) {
this.emit("sequenceError", this.buffer[2] - this.lastCounter - 1);
}
//update counter
this.lastCounter = this.buffer[2];
//use message object to parse headers
var message = new mavlinkMessage(this.buffer);
//if system and component ID's dont match, ignore message. Alternatively if zeros were specified we return everything.
if ((this.sysid == 0 && this.compid == 0) || (message.system == this.sysid && message.component == this.compid)) {
//fire an event with the message data
this.emit("message", message);
//fire additional event for specific message type
this.emit(this.getMessageName(this.buffer[5]), message, this.decodeMessage(message));
}
} else {
//If checksum fails, fire an event with some debugging information. Message ID, Message Checksum (XML), Calculated Checksum, Received Checksum
this.emit("checksumFail", this.buffer[5], this.messageChecksums[this.buffer[5]], this.calculateChecksum(crc_buf), this.buffer.readUInt16LE(this.messageLength+6));
}
//We got a message, so reset things
this.bufferIndex = 0;
this.messageLength = 0;
}
};
//Function to call parseChar on all characters in a buffer
mavlink.prototype.parse = function(buffer) {
for (var i=0; i<buffer.length; i++) {
this.parseChar(buffer[i]);
}
}
//Function to place a fields value in to a message buffer
mavlink.prototype.bufferField = function(buf, offset, field, value) {
//Split up the field name to see if it's an array
//TODO: Add some functions to do this as it's used in a few places
var fieldSplit = field.$.type.replace("[", " ").replace("]", " ").split(" ");
//If field is not an array, make a temporary array (size 1) and assign the value to its only element
if (fieldSplit.length == 1) {
var valueArr = Array(1);
valueArr[0] = value;
} else { //otherwise copy the array
var valueArr = value;
}
//For all the elements in the array, place the values in the buffer.
//TODO: check sizes here, if input data is less than field size that's fine (with a warning?)
// but if input is bigger than field size this will probably corrupt the buffer
for (var i = 0; i<valueArr.length; i++) {
//Figure out the data and write as appropriate
switch (fieldSplit[0]){
case 'float':
buf.writeFloatLE(Number(valueArr[i]),offset);
break;
case'double':
buf.writeDoubleLE(Number(valueArr[i]),offset);
break;
case 'char':
buf.writeUInt8(valueArr[i].charCodeAt(0),offset);
break;
case 'int8_t':
buf.writeInt8(Number(valueArr[i]),offset);
break;
case 'uint8_t':
buf.writeUInt8(Number(valueArr[i]),offset);
break;
case 'uint8_t_mavlink_version':
buf.writeUInt8(Number(valueArr[i]),offset);
break;
case 'int16_t':
buf.writeInt16LE(Number(valueArr[i]),offset);
break;
case 'uint16_t':
buf.writeUInt16LE(Number(valueArr[i]),offset);
break;
case 'int32_t':
buf.writeInt32LE(Number(valueArr[i]),offset);
break;
case 'uint32_t':
buf.writeUInt32LE(Number(valueArr[i]),offset);
break;
//TODO: Add support for the 64bit types
case 'int64_t':
console.warn("No 64-bit Integer support yet!");
//buf.writeFloatLE(value[i],offset);
break;
case 'uint64_t':
console.warn("No 64-bit Integer support yet!");
//buf.writeFloatLE(value[i],offset);
break;
}
//Keep track of how far we've come
offset += field.typeLength;
}
}
//Decode an incomming message in to its individual fields
mavlink.prototype.decodeMessage = function(message) {
//determine the fields
var fields = this.messagesByID[message.id].field;
//initialise the output object and buffer offset
var values = new Object();
var offset = 0;
//loop over fields
for (var i = 0; i<fields.length; i++) {
//determine if field is an array
var fieldSplit = fields[i].$.type.replace("[", " ").replace("]", " ").split(" ");
//determine field name
var fieldTypeName = fieldSplit[0];
//if field is an array, initialise output array
if (fieldSplit.length > 1) {
values[fields[i].$.name] = new Array(fields[i].arrayLength);
}
//loop over all elements in field and read from buffer
for (var j = 0; j<fields[i].arrayLength; j++) {
var val = 0;
switch (fieldTypeName){
case 'float':
val = message.payload.readFloatLE(offset);
break;
case'double':
val = message.payload.readDoubleLE(offset);
break;
case 'char':
val = message.payload.readUInt8(offset);
break;
case 'int8_t':
val = message.payload.readInt8(offset);
break;
case 'uint8_t':
val = message.payload.readUInt8(offset);
break;
case 'uint8_t_mavlink_version':
val = message.payload.readUInt8(offset);
break;
case 'int16_t':
val = message.payload.readInt16LE(offset);
break;
case 'uint16_t':
val = message.payload.readUInt16LE(offset);
break;
case 'int32_t':
val = message.payload.readInt32LE(offset);
break;
case 'uint32_t':
val = message.payload.readUInt32LE(offset);
break;
//TODO: Add support for the 64bit types
case 'int64_t':
console.warn("No 64-bit Integer support yet!");
//buf.writeFloatLE(value[i],offset);
break;
case 'uint64_t':
//console.warn("No 64-bit Unsigned Integer support yet!");
var val1 = message.payload.readUInt32LE(offset);
var val2 = message.payload.readUInt32LE(offset+4);
val = (val1<<32) + (val2);
break;
}
//increment offset by field type size
offset += fields[i].typeLength;
//if field is an array, output in to array
if (fieldSplit.length > 1) {
values[fields[i].$.name][j] = val;
} else {
values[fields[i].$.name] = val;
}
}
//reconstruct char arrays in to strings
if (fieldSplit.length > 1 && fieldTypeName == 'char') {
values[fields[i].$.name] = (new Buffer(values[fields[i].$.name])).toString();
}
}
return values;
}
//Function to creae a MAVLink message to send out
//Input either the numeric message id or name, and the data as a structure of field-name : value
//For example:
// myMAV.createMessage("ATTITUDE", {
// 'time_boot_ms':30,
// 'roll':0.1,
// 'pitch':0.2,
// 'yaw':0.3,
// 'rollspeed':0.4,
// 'pitchspeed':0.5,
// 'yawspeed':0.6
// }, callback);
mavlink.prototype.createMessage = function(msgid, data, cb) {
//if ID's are zero we can't send data
if (this.sysid == 0 && this.compid == 0) {
console.log("System and component ID's are zero, cannot create message!");
}
var id = msgid;
//Is message id numerical? If not then look it up
if (isNaN(Number(msgid))) {
id = this.getMessageID(msgid);
}
//Get the details of the message
var message = this.messagesByID[id];
if (message === undefined) {
console.log("Message '" + msgid + "' does not exist!");
return;
}
//Create a buffer for the payload and null fill it
var payloadBuf = new Buffer(message.payloadLength);
payloadBuf.fill('\0');
//Loop over the fields in the message
var offset = 0;
for (var i = 0; i < message.field.length; i++) {
//If we don't have data for a field quit out with an error
if (data[message.field[i].$.name] === undefined) {
console.log("MAVLink: No data supplied for '" + message.field[i].$.name + "'");
return;
}
//If we have data, add it to the buffer
this.bufferField(payloadBuf, offset, message.field[i], data[message.field[i].$.name]);
//Increment the buffer offset with the total field size
offset += message.field[i].length;
}
//Create a buffer for the entire message and null fill
var msgBuf = new Buffer(message.payloadLength + 8);
msgBuf.fill('\0');
//Determine sequence number
if (this.sequence++ == 255) {
this.sequence = 0;
}
//Construct the header information
msgBuf[0] = this.startCharacter();
msgBuf[1] = message.payloadLength;
msgBuf[2] = this.sequence;
msgBuf[3] = this.sysid;
msgBuf[4] = this.compid;
msgBuf[5] = id;
//Copy in the payload buffer
payloadBuf.copy(msgBuf,6,0);
//Calculate the CRC
var crc_buf = new Buffer(message.payloadLength+6);
msgBuf.copy(crc_buf,0,1,message.payloadLength+6);
crc_buf[crc_buf.length-1] = this.messageChecksums[id];
msgBuf.writeUInt16LE(this.calculateChecksum(crc_buf), message.payloadLength+6);
var msgObj = new mavlinkMessage(msgBuf);
cb(msgObj);
}
module.exports = mavlink;