hybridgroup/node-bebop

View on GitHub
lib/bebop.js

Summary

Maintainability
F
1 wk
Test Coverage
"use strict";

var EventEmitter = require("events").EventEmitter,
    dgram = require("dgram"),
    util = require("util"),
    net = require("net"),
    through = require("through"),
    ffmpeg = require("./ffmpeg-shim"),
    constants = require("./constants"),
    commands = require("./commands.json"),
    types = require("./types"),
    Piloting = require("./Piloting"),
    Animations = require("./Animations"),
    Camera = require("./Camera"),
    MediaRecord = require("./MediaRecord"),
    PilotingEvent = require("./PilotingEvent"),
    Network = require("./Network"),
    PilotingSettings = require("./PilotingSettings"),
    SpeedSettings = require("./SpeedSettings"),
    NetworkSettings = require("./NetworkSettings"),
    Settings = require("./Settings"),
    SettingsState = require("./SettingsState"),
    PictureSettings = require("./PictureSettings"),
    MediaStreaming = require("./MediaStreaming"),
    GPSSettings = require("./GPSSettings"),
    CameraState = require("./CameraState"),
    Antiflickering = require("./Antiflickering"),
    PROState = require("./PROState"),
    NetworkEvent = require("./NetworkEvent"),
    Common = require("./Common"),
    CommonState = require("./CommonState"),
    OverHeat = require("./OverHeat"),
    WifiSettings = require("./WifiSettings"),
    Mavlink = require("./Mavlink"),
    Calibration = require("./Calibration"),
    CalibrationState = require("./CalibrationState"),
    GPS = require("./GPS"),
    FlightPlanEvent = require("./FlightPlanEvent"),
    ARLibsVersionsState = require("./ARLibsVersionsState"),
    Audio = require("./Audio"),
    AudioState = require("./AudioState"),
    Headlights = require("./Headlights"),
    AnimationsState = require("./AnimationsState"),
    Accessory = require("./Accessory"),
    AccessoryState = require("./AccessoryState"),
    Charger = require("./Charger"),
    ChargerState = require("./ChargerState");

function networkFrameGenerator() {
  //
  // ARNETWORKAL_Frame_t
  //
  // uint8  type  - frame type ARNETWORK_FRAME_TYPE
  // uint8  id    - identifier of the buffer sending the frame
  // uint8  seq   - sequence number of the frame
  // uint32 size  - size of the frame
  //

  // each frame id has it"s own sequence number
  var seq = [];

  return function(cmd, type, id) {
    var hlen = 7, // size of ARNETWORKAL_Frame_t header
        buf = new Buffer(hlen);

    type = type || constants.ARNETWORKAL_FRAME_TYPE_DATA;
    id = id || constants.BD_NET_CD_NONACK_ID;

    if (!seq[id]) {
      seq[id] = 0;
    }

    seq[id]++;

    if (seq[id] > 255) {
      seq[id] = 0;
    }

    buf.writeUInt8(type, 0);
    buf.writeUInt8(id, 1);
    buf.writeUInt8(seq[id], 2);
    buf.writeUInt32LE(cmd.length + hlen, 3);

    return Buffer.concat([buf, cmd]);
  };
}

function networkFrameParser(buf) {
  var frame = {
    type: buf.readUInt8(0),
    id: buf.readUInt8(1),
    seq: buf.readUInt8(2),
    size: buf.readUInt32LE(3)
  };

  if (frame.size > 7) {
    frame.data = Buffer.concat([buf.slice(7, frame.size)]);
  }

  return frame;
}

function arstreamFrameParser(buf) {
  //
  // ARSTREAM_NetworkHeaders_DataHeader_t;
  //
  // uint16_t frameNumber;
  // uint8_t  frameFlags; // Infos on the current frame
  // uint8_t  fragmentNumber; // Index of the current fragment in current frame
  // uint8_t  fragmentsPerFrame; // Number of fragments in current frame
  //
  // * frameFlags structure :
  // *  x x x x x x x x
  // *  | | | | | | | \-> FLUSH FRAME
  // *  | | | | | | \-> UNUSED
  // *  | | | | | \-> UNUSED
  // *  | | | | \-> UNUSED
  // *  | | | \-> UNUSED
  // *  | | \-> UNUSED
  // *  | \-> UNUSED
  // *  \-> UNUSED
  // *
  //

  var frame = {
    frameNumber: buf.readUInt16LE(0),
    frameFlags: buf.readUInt8(2),
    fragmentNumber: buf.readUInt8(3),
    fragmentsPerFrame: buf.readUInt8(4),
  };

  frame.frame = Buffer.concat([buf.slice(5)]);

  return frame;
}


function validatePitch(val) {
  if (val > 100) {
    return 100;
  } else if (val < 0) {
    return 0;
  }

  return val | 0;
}

var Bebop = module.exports = function(opts) {
  opts = opts || {};
  this.navData = {};
  this.ip = opts.ip || "192.168.42.1";
  this.c2dPort = opts.c2dPort || 54321;
  this.d2cPort = opts.d2cPort || 43210;
  this.discoveryPort = opts.discoveryPort || 44444;
  this._c2dClient = dgram.createSocket("udp4");
  this._d2cServer = dgram.createSocket("udp4");
  this._discoveryClient = new net.Socket();
  this._networkFrameGenerator = networkFrameGenerator();
  this._arstreamFrame = {
    frameNumber: 0,
    frame: new Buffer([]),
    fragments: [],
  };
  this._pcmd = {};
  this.Piloting = new Piloting(this);
  this.Animations = new Animations(this);
  this.Camera = new Camera(this);
  this.MediaRecord = new MediaRecord(this);
  this.PilotingEvent = new PilotingEvent(this);
  this.Network = new Network(this);
  this.PilotingSettings = new PilotingSettings(this);
  this.SpeedSettings = new SpeedSettings(this);
  this.NetworkSettings = new NetworkSettings(this);
  this.Settings = new Settings(this);
  this.SettingsState = new SettingsState(this);
  this.PictureSettings = new PictureSettings(this);
  this.MediaStreaming = new MediaStreaming(this);
  this.GPSSettings = new GPSSettings(this);
  this.CameraState = new CameraState(this);
  this.Antiflickering = new Antiflickering(this);
  this.PROState = new PROState(this);
  this.Network = new Network(this);
  this.NetworkEvent = new NetworkEvent(this);
  this.Settings = new Settings(this);
  this.SettingsState = new SettingsState(this);
  this.Common = new Common(this);
  this.CommonState = new CommonState(this);
  this.OverHeat = new OverHeat(this);
  this.WifiSettings = new WifiSettings(this);
  this.Mavlink = new Mavlink(this);
  this.Calibration = new Calibration(this);
  this.CalibrationState = new CalibrationState(this);
  this.GPS = new GPS(this);
  this.FlightPlanEvent = new FlightPlanEvent(this);
  this.ARLibsVersionsState = new ARLibsVersionsState(this);
  this.Audio = new Audio(this);
  this.AudioState = new AudioState(this);
  this.Headlights = new Headlights(this);
  this.Animations = new Animations(this);
  this.AnimationsState = new AnimationsState(this);
  this.Accessory = new Accessory(this);
  this.AccessoryState = new AccessoryState(this);
  this.Charger = new Charger(this);
  this.ChargerState = new ChargerState(this);
};

util.inherits(Bebop, EventEmitter);

Bebop.prototype.getVideoStream = function() {
  var stream = through(function write(data) {
    this.emit("data", data);
  });

  this.on("video", function(data) {
    stream.write(data);
  });

  return stream;
};

Bebop.prototype.getMjpegStream = function(opts) {
  opts = opts || {};
  opts.size = opts.size || "640x368";
  opts.fps = opts.fps || 30;
  opts.quality = opts.quality || "1";

  var stream = through(function write(data) {
    this.emit("data", data);
  });

  ffmpeg(this.getVideoStream())
    .toFormat("mjpeg")
    .size(opts.size)
    .inputFPS(opts.fps)
    .outputOptions(["-qscale:v " + opts.quality])
    .writeToStream(stream);

  return stream;
};

Bebop.prototype.connect = function(callback) {
  this.discover(function() {

    // nav and video
    this._d2cServer.bind(this.d2cPort);
    this._d2cServer.on("message", this._packetReceiver.bind(this));

    // send pcmd values at 40hz
    setInterval(function() {
      this._writePacket(this._generatePCMD(this._pcmd));
    }.bind(this), 25);

    this.generateAllStates();
    this.flatTrim();

    if (typeof callback === "function") {
      callback();
    }

    this.emit("ready");
  }.bind(this));
};

Bebop.prototype.discover = function(callback) {
  this._discoveryClient.connect(this.discoveryPort, this.ip, function() {
    this._discoveryClient.write(JSON.stringify({
      "controller_type": "computer",
      "controller_name": "node-bebop",
      "d2c_port": this.d2cPort.toString()
    }));
  }.bind(this));

  this._discoveryClient.on("data", function(data) {
    this._discoveryClient.destroy();
    callback(data);
  }.bind(this));
};

Bebop.prototype._packetReceiver = function(message) {
  var networkFrame = networkFrameParser(message);

  //
  // libARNetwork/Sources/ARNETWORK_Receiver.c#ARNETWORK_Receiver_ThreadRun
  //
  if (networkFrame.type === constants.ARNETWORKAL_FRAME_TYPE_DATA_WITH_ACK) {
    this._writePacket(this._createAck(networkFrame));
  }

  if (networkFrame.type === constants.ARNETWORKAL_FRAME_TYPE_DATA_LOW_LATENCY &&
      networkFrame.id === constants.BD_NET_DC_VIDEO_DATA_ID)
  {
    var arstreamFrame = arstreamFrameParser(networkFrame.data);
    this._writePacket(this._createARStreamACK(arstreamFrame));
  }

  //
  // libARCommands/Sources/ARCOMMANDS_Decoder.c#ARCOMMANDS_Decoder_DecodeBuffer
  //

  if (networkFrame.id === constants.BD_NET_DC_EVENT_ID ||
      networkFrame.id === constants.BD_NET_DC_NAVDATA_ID) {

    var commandProject = networkFrame.data.readUInt8(0),
        commandClass = networkFrame.data.readUInt8(1),
        commandId = networkFrame.data.readUInt16LE(2);

    var offset = 4;
    var args = {};
    var event = null;


    try {
      event = commands
        .first({id: commandProject}).class
        .first({id: commandClass}).cmd;
    }
    catch (err) {
      this.emit("unknown", networkFrame.data);
    }

    if (event) {

      if (event instanceof Array) {
        event = event[commandId];
        if (!event) {
          this.emit("unknown", networkFrame.data);
          return;
        }
      }

      if (typeof event.arg !== "undefined") {
        if (event.arg instanceof Array) {
          event.arg.forEach(function(arg) {
            if (types.hasOwnProperty(arg.type)) {
              args[arg.name] = types[arg.type]
                .read(networkFrame.data, offset, arg);

              offset += types[arg.type].length;
            }
          });
        }
        else if (event.arg instanceof Object) {
          if (types.hasOwnProperty(event.arg.type)) {
            args[event.arg.name] = types[event.arg.type]
              .read(networkFrame.data, offset, event.arg);
          }
        }
      }

      this.emit(event.name, args);
    }

    switch (commandProject) {
      case constants.ARCOMMANDS_ID_PROJECT_COMMON:
        switch (commandClass) {
          case constants.ARCOMMANDS_ID_COMMON_CLASS_COMMONSTATE:
            switch (commandId) {
              case constants.ARCOMMANDS_ID_COMMON_COMMONSTATE_CMD_BATTERYSTATECHANGED:
                this.navData.battery = networkFrame.data.readUInt8(4);
                this.emit("battery", this.navData.battery);
                break;
            }
            break;
        }
        break;
      case constants.ARCOMMANDS_ID_PROJECT_ARDRONE3:
        switch (commandClass) {
          case constants.ARCOMMANDS_ID_ARDRONE3_CLASS_PILOTINGSTATE:
            switch (commandId) {
              case constants.ARCOMMANDS_ID_ARDRONE3_PILOTINGSTATE_CMD_FLATTRIMCHANGED:
                break;
              case constants.ARCOMMANDS_ID_ARDRONE3_PILOTINGSTATE_CMD_FLYINGSTATECHANGED:
                switch (networkFrame.data.readInt32LE(4)) {
                  case constants.ARCOMMANDS_ARDRONE3_PILOTINGSTATE_FLYINGSTATECHANGED_STATE_LANDED:
                    this.navData.flyingState = { landed: true };
                    this.emit("landed");
                    break;
                  case constants.ARCOMMANDS_ARDRONE3_PILOTINGSTATE_FLYINGSTATECHANGED_STATE_TAKINGOFF:
                    this.navData.flyingState = { takingOff: true };
                    this.emit("takingOff");
                    break;
                  case constants.ARCOMMANDS_ARDRONE3_PILOTINGSTATE_FLYINGSTATECHANGED_STATE_HOVERING:
                    this.navData.flyingState = { hovering: true };
                    this.emit("hovering");
                    break;
                  case constants.ARCOMMANDS_ARDRONE3_PILOTINGSTATE_FLYINGSTATECHANGED_STATE_FLYING:
                    this.navData.flyingState = { flying: true };
                    this.emit("flying");
                    break;
                  case constants.ARCOMMANDS_ARDRONE3_PILOTINGSTATE_FLYINGSTATECHANGED_STATE_LANDING:
                    this.navData.flyingState = { landing: true };
                    this.emit("landing");
                    break;
                  case constants.ARCOMMANDS_ARDRONE3_PILOTINGSTATE_FLYINGSTATECHANGED_STATE_EMERGENCY:
                    this.navData.flyingState = { emergency: true };
                    this.emit("emergency");
                    break;
                }
                break;
            }
            break;
        }
        break;
    }
  }

  //
  // libARNetwork/Sources/ARNETWORK_Receiver.c#ARNETWORK_Receiver_ThreadRun
  //
  if (networkFrame.id === constants.ARNETWORK_MANAGER_INTERNAL_BUFFER_ID_PING) {
    this.navData.flyingTime = networkFrame.data.readUInt32LE(0) + (networkFrame.data.readUInt32LE(4) / 1000000000.0);
    this._writePacket(this._createPong(networkFrame));
  }
};

Bebop.prototype.up = function(val) {
  this._pcmd.gaz = validatePitch(val);
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.down = function(val) {
  this._pcmd.gaz = validatePitch(val) * -1;
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.front = function(val) {
  return this.forward(val);
};

Bebop.prototype.forward = function(val) {
  this._pcmd.pitch = validatePitch(val);
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.back = function(val) {
  return this.backward(val);
};

Bebop.prototype.backward = function(val) {
  this._pcmd.pitch = validatePitch(val) * -1;
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.right = function(val) {
  this._pcmd.roll = validatePitch(val);
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.left = function(val) {
  this._pcmd.roll = validatePitch(val) * -1;
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.clockwise = function(val) {
  this._pcmd.yaw = validatePitch(val);
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.counterClockwise = function(val) {
  this._pcmd.yaw = validatePitch(val) * -1;
  this._pcmd.flag = 1;
  return this;
};

Bebop.prototype.stop = Bebop.prototype.level = function() {
  this._pcmd = {
    flag: 0,
    roll: 0,
    pitch: 0,
    yaw: 0,
    gaz: 0,
  };

  return this;
};

Bebop.prototype.frontFlip = function() {
  return this._animationsFlip(constants.ARCOMMANDS_ARDRONE3_ANIMATIONS_FLIP_DIRECTION_FRONT);
};

Bebop.prototype.backFlip = function() {
  return this._animationsFlip(constants.ARCOMMANDS_ARDRONE3_ANIMATIONS_FLIP_DIRECTION_BACK);
};

Bebop.prototype.rightFlip = function() {
  return this._animationsFlip(constants.ARCOMMANDS_ARDRONE3_ANIMATIONS_FLIP_DIRECTION_RIGHT);
};

Bebop.prototype.leftFlip = function() {
  return this._animationsFlip(constants.ARCOMMANDS_ARDRONE3_ANIMATIONS_FLIP_DIRECTION_LEFT);
};

Bebop.prototype._animationsFlip = function(direction) {
  //
  //  ARCOMMANDS_Generator_GenerateARDrone3AnimationsFlip
  //

  var buf = new Buffer(8);

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_ANIMATIONS, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_ANIMATIONS_CMD_FLIP, 2);
  buf.writeUInt32LE(direction, 4);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype.takeoff = function(callback) {
  return this.takeOff(callback);
};

Bebop.prototype.takeOff = function(callback) {
  //
  //  ARCOMMANDS_Generator_GenerateARDrone3PilotingTakeOff
  //

  var buf = new Buffer(4);

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_PILOTING, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_PILOTING_CMD_TAKEOFF, 2);

  this._writePacket(this._networkFrameGenerator(buf));
  this.once("flying", callback || function() {});
  return this;
};

Bebop.prototype.land = function(callback) {
  //
  // ARCOMMANDS_Generator_GenerateARDrone3PilotingLanding
  //

  var buf = new Buffer(4);

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_PILOTING, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_PILOTING_CMD_LANDING, 2);

  this._writePacket(this._networkFrameGenerator(buf));
  this.once("landed", callback || function() {});
  return this;
};

Bebop.prototype.emergency = function() {
  //
  // ARCOMMANDS_Generator_GenerateARDrone3PilotingEmergency
  //

  var buf = new Buffer(4);

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_PILOTING, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_PILOTING_CMD_EMERGENCY, 2);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype.generateAllStates = function() {
  //
  // ARCOMMANDS_Generator_GenerateCommonCommonAllStates
  //

  var buf = new Buffer(4);

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_COMMON, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_COMMON_CLASS_COMMON, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_COMMON_COMMON_CMD_ALLSTATES, 2);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype.calibrate = function() {
  return this.flatTrim();
};

Bebop.prototype.flatTrim = function() {
  //
  // ARCOMMANDS_Generator_GenerateARDrone3PilotingFlatTrim
  //

  var buf = new Buffer(4);

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_PILOTING, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_PILOTING_CMD_FLATTRIM, 2);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype.takePicture = function(opts) {
  //
  // ARCOMMANDS_Generator_GenerateARDrone3MediaRecordPicture
  //

  var buf = new Buffer(5);

  opts = opts || {};
  opts.storageId = opts.storageId || 0;

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_MEDIARECORD, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_MEDIARECORD_CMD_PICTURE, 2);
  buf.writeUInt8(opts.storageId, 4);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype.startRecording = function(opts) {
  opts = opts || {};
  opts.recordState = constants.ARCOMMANDS_ARDRONE3_MEDIARECORD_VIDEO_RECORD_START;

  var buf = this._videoRecord(opts);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype.stopRecording = function(opts) {
  opts = opts || {};
  opts.recordState = constants.ARCOMMANDS_ARDRONE3_MEDIARECORD_VIDEO_RECORD_STOP;

  var buf = this._videoRecord(opts);

  this._writePacket(this._networkFrameGenerator(buf));
  return this;
};

Bebop.prototype._videoRecord = function(opts) {
  //
  // ARCOMMANDS_Generator_GenerateARDrone3MediaRecordVideo
  //

  var buf = new Buffer(9);

  opts = opts || {};
  opts.storageId = opts.storageId || 0;
  opts.recordState = opts.recordState || 0;

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_MEDIARECORD, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_MEDIARECORD_CMD_VIDEO, 2);
  buf.writeUInt32LE(opts.recordState, 4);
  buf.writeUInt8(opts.storageId, 8);

  return buf;
};

Bebop.prototype._createAck = function(networkFrame) {
  var buf = new Buffer(1);

  //
  // ARNETWORK_Receiver_ThreadRun
  //
  buf.writeUInt8(networkFrame.seq, 0);

  //
  //
  // libARNetwork/Sources/ARNETWORK_Manager.h#ARNETWORK_Manager_IDOutputToIDAck
  //
  var id = networkFrame.id + (constants.ARNETWORKAL_MANAGER_DEFAULT_ID_MAX / 2);

  return this._networkFrameGenerator(buf, constants.ARNETWORKAL_FRAME_TYPE_ACK, id);
};

Bebop.prototype._createPong = function(networkFrame) {
  return this._networkFrameGenerator(networkFrame.data, constants.ARNETWORKAL_FRAME_TYPE_DATA, constants.ARNETWORK_MANAGER_INTERNAL_BUFFER_ID_PONG);
};

Bebop.prototype._createARStreamACK = function(arstreamFrame) {
  //
  // ARSTREAM_NetworkHeaders_AckPacket_t;
  //
  // uint16_t frameNumber;    // id of the current frame
  // uint64_t highPacketsAck; // Upper 64 packets bitfield
  // uint64_t lowPacketsAck;  // Lower 64 packets bitfield
  //
  // libARStream/Sources/ARSTREAM_NetworkHeaders.c#ARSTREAM_NetworkHeaders_AckPacketSetFlag
  //

  if (arstreamFrame.frameNumber !== this._arstreamFrame.frameNumber) {
    if (this._arstreamFrame.fragments.length > 0) {
      var emit = false;

      // if we missed some frames, wait for the next iframe
      if (arstreamFrame.frameNumber !== this._arstreamFrame.frameNumber + 1) {
        this._arstreamFrame.waitForIframe = true;
      }

      // if it's an iframe
      if (this._arstreamFrame.frameFlags === 1) {
        this._arstreamFrame.waitForIframe = false;
        emit = true;
      } else if (!this._arstreamFrame.waitForIframe) {
        emit = true;
      }

      if (emit) {
        var skip = false;
        for (var i = 0; i < this._arstreamFrame.fragments.length; i++) {
          // check if any fragments are missing
          if (!Buffer.isBuffer(this._arstreamFrame.fragments[i])) {
            skip = true;
            break;
          }
          this._arstreamFrame.frame = Buffer.concat([this._arstreamFrame.frame, this._arstreamFrame.fragments[i]]);
        }

        if (!skip) {
          this.emit("video", this._arstreamFrame.frame);
        }
      }
    }

    this._arstreamFrame.fragments = [];
    this._arstreamFrame.frame = new Buffer(0);
    this._arstreamFrame.frameACK = new Buffer(16);
    this._arstreamFrame.frameACK.fill(0);
    this._arstreamFrame.frameNumber = arstreamFrame.frameNumber;
    this._arstreamFrame.frameFlags = arstreamFrame.frameFlags;
  }

  this._arstreamFrame.fragments[arstreamFrame.fragmentNumber] = Buffer.concat([arstreamFrame.frame]);

  //
  // each bit in the highPacketsAck and lowPacketsAck correspond to the
  // fragmentsPerFrame which have been received per frameNumber, so time to
  // flip some bits!
  //

  var bufferPosition = arstreamFrame.fragmentNumber / 8 | 0;
  var tmp = this._arstreamFrame.frameACK.readUInt8(bufferPosition);

  tmp |= 1 << (arstreamFrame.fragmentNumber % 8);

  this._arstreamFrame.frameACK.writeUInt8(tmp, bufferPosition);

  // lowPacketsAck and highPacketsAck are stored contiguously
  // in a 16 byte buffer and then reordered accordingly for transport
  var ackPacket = {
    frameNumber: this._arstreamFrame.frameNumber,
    packetsACK: Buffer.concat([this._arstreamFrame.frameACK.slice(8), this._arstreamFrame.frameACK.slice(0, 8)]),
  };

  var ret = new Buffer(18);
  ret.fill(0);
  ret.writeUInt16LE(ackPacket.frameNumber, 0);
  ackPacket.packetsACK.copy(ret, 2);

  return this._networkFrameGenerator(ret, constants.ARNETWORKAL_FRAME_TYPE_DATA, constants.BD_NET_CD_VIDEO_ACK_ID);
};

Bebop.prototype._generatePCMD = function(opts) {
  //
  // ARCOMMANDS_Generator_GenerateARDrone3PilotingPCMD
  //
  // uint8 - flag Boolean flag to activate roll/pitch movement
  // int8  - roll Roll consign for the drone [-100;100]
  // int8  - pitch Pitch consign for the drone [-100;100]
  // int8  - yaw Yaw consign for the drone [-100;100]
  // int8  - gaz Gaz consign for the drone [-100;100]
  // float - psi [NOT USED] - Magnetic north heading of the
  //         controlling device (deg) [-180;180]
  //

  var buf = new Buffer(13);

  this._pcmd = opts || {};

  buf.writeUInt8(constants.ARCOMMANDS_ID_PROJECT_ARDRONE3, 0);
  buf.writeUInt8(constants.ARCOMMANDS_ID_ARDRONE3_CLASS_PILOTING, 1);
  buf.writeUInt16LE(constants.ARCOMMANDS_ID_ARDRONE3_PILOTING_CMD_PCMD, 2);
  buf.writeUInt8(this._pcmd.flag || 0, 4);
  buf.writeInt8(this._pcmd.roll || 0, 5);
  buf.writeInt8(this._pcmd.pitch || 0, 6);
  buf.writeInt8(this._pcmd.yaw || 0, 7);
  buf.writeInt8(this._pcmd.gaz || 0, 8);
  buf.writeFloatLE(this._pcmd.psi || 0, 9);

  return this._networkFrameGenerator(buf);
};

Bebop.prototype._writePacket = function(packet) {
  this._c2dClient.send(packet, 0, packet.length, this.c2dPort, this.ip,
    function(err) {
      if (err) {
        throw err;
      }
    }
  );
};