UNPKG

mavlink

Version:

Encode and decode MAVLink messages

630 lines (531 loc) 18.8 kB
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;