Source: L8.js

var util = require("util");
var EventEmitter = require("events").EventEmitter;

var SerialPort = require("serialport").SerialPort;
var CRC = require("crc");
var SLCP = require("./SLCP");

var AccelerationStream = require("./AccelerationStream").AccelerationStream;

/**
 * Main API entry point providing all the public API in order to Control
 * an L8 Smartlight
 *
 * @fires L8#frameSent
 * @fires L8#frameReceived
 *
 * @constructor
 *
 * @example ```
 *  var L8 = require("l8smartlight").L8;
 *
 *  var l8 = new L8();
 *  l8.open("/dev/YOUR_L8_SERIALPORT_DEVICE", null, function(error) {
 *      if (error) {
 *          throw new Error("Error occurred: " + error);
 *      }
 *      l8.setScrollingText("Hello World!", {r: 15, g: 0, b: 0}, "fast", false, function() {
 *          console.log("Now scrolling text ;)");
 *      });
 *  });
 * ```
 *
 * Alternatively you may use the automatically generated Promise API. All methods,
 * which require a callback allow you to omit the callback and retrieve a Promise
 * instead.
 *
 * @example ```
 *  var L8 = require("l8smartlight").L8;
 *
 *  var l8 = new L8();
 *  l8.open("/dev/YOUR_L8_SERIALPORT_DEVICE", null).then(function() {
 *      return l8.clearMatrix();
 *  }).then(function(response) {
 *      return l8.setScrollingText("Hello World!", {r: 15, g: 0, b: 0}, "fast", false);
 *  }).then(function() {
 *      console.log("Now scrolling text ;)");
 *  }).catch(function(error) {
 *      console.error(error.stack);
 *  });
 * ```
 */
var L8 = function() {
    EventEmitter.call(this);

    /**
     * Indicator whether a connection is established or not.
     * @type {boolean}
     */
    this.isConnected = false;

    /**
     * SerialPort connection used for communication
     * @type {SerialPort}
     * @private
     */
    this.serialport_ = null;

    /**
     * Buffer for storing all received data, before it is processed.
     *
     *  @type {Object}
     * @private
     */
    this.receiveBuffer_ = {
        buffer: new Buffer(4096),
        length: 0
    };
};
util.inherits(L8, EventEmitter);

/**
 * Magic byte sequence used to identify any SLCP command or response
 *
 * @type {Buffer}
 * @const
 */
L8.MAGIC_BYTES = new Buffer("AA55", "hex");

/**
 * Open a connection to the given port using an optionally provided baudrate
 *
 * If no baudrate is specified a default speed of `115200` will be used.
 *
 * After the port has been successfully opened the given callback will be fired
 *
 * The operation is asynchronous. Further communication with the L8 is only feasible
 * after the `open` callback has been fired.
 *
 * @param {String} port
 * @param {Number?} baudrate
 * @param fn
 */
L8.prototype.open = function(port, baudrate, fn) {
    // Default value for speed argument
    if (baudrate === null || baudrate === undefined) {
        baudrate = 115200;
    }

    this.receiveBuffer_.length = 0;

    this.serialport_ = new SerialPort(port, {
        baudrate: baudrate,
        databits: 8,
        stopbits: 1,
        parity: "none"
    }, false);

    this.serialport_.on("data", this.onResponse_.bind(this));

    this.serialport_.open(function(error){
        this.isConnected = true;
        fn(error, !error);
    }.bind(this));
};

/**
 * Close an established connection to the L8
 *
 * @param {Function} fn
 */
L8.prototype.close = function(fn) {
   if (!this.isConnected) {
       return;
   }

    this.serialport_.close(function(error) {
        this.isConnected = false;
        this.serialport_ = null;
        fn(error, !error);
    }.bind(this));
};

/**
 * Try to parse as many frames out of the given receiveBuffer, as possible.
 *
 * The response is either an array containing a frame definition or `false` if the frame wasn't complete yet.
 *
 * @param {{buffer: Buffer, length: Number}} receiveBuffer
 * @returns {{command: Number, parameters: Buffer, checksum: String, payloadLength: Number, raw: Buffer}}
 * @private
 */
L8.prototype.parseFrames_ = function(receiveBuffer) {
    var frames = [];
    while(receiveBuffer.length > 0) {
        if (receiveBuffer.length < 4) {
            // The minimum message is 4 byte. Therefore we need to wait for more data
            break;
        }

        var data = receiveBuffer.buffer;

        // Validate for magic bytes present
        if (data[0] !== L8.MAGIC_BYTES[0] || data[1] !== L8.MAGIC_BYTES[1]) {
            throw new EvalError("Invalid L8 response. Magic bytes not found: " + data.toString("hex"));
        }

        var payloadLength = data[2];

        if (receiveBuffer.length < 2 /*MAGIC*/ + 1 /*LENGTH*/ + payloadLength + 1 /*CHECKSUM*/) {
            // Not yet complete
            break;
        }

        // We need this buffer later on, after the receive buffer has already changed its state
        // Therefore it is copied
        var payloadBuffer = new Buffer(payloadLength);
        data.copy(payloadBuffer, 0, 2 /*MAGIC*/ + 1 /*LENGTH*/, 3 + payloadLength);

        var checksumPosition = 2 /*MAGIC*/ + 1 /*LENGTH*/ + payloadLength;
        var receivedChecksum = data.slice(checksumPosition, checksumPosition + 1).toString("hex");

        var calculatedChecksum = CRC.crc8(payloadBuffer);

        if (calculatedChecksum !== receivedChecksum) {
            throw new EvalError("Response checksum did not match. Expected " + receivedChecksum + " got " + calculatedChecksum);
        }

        // Yeah! We got a valid response let's decode it ;)
        var command = payloadBuffer[0];
        var parametersSlice = payloadBuffer.slice(1, payloadBuffer.length);

        frames.push({
            command: command,
            parameters: parametersSlice,
            checksum: receivedChecksum,
            payloadLength: payloadLength,
            payload: payloadBuffer
        });

        // Remove the decoded frame from the receiveBuffer
        data.copy(data, 0, 2 + /*MAGIC*/ + 1 /*LENGTH*/ + payloadLength + 1 /*CHECKSUM*/);
        receiveBuffer.length -= 2 + /*MAGIC*/ + 1 /*LENGTH*/ + payloadLength + 1 /*CHECKSUM*/;
    }

    if (frames.length === 0) {
        return false;
    } else {
        return frames;
    }
};

/**
 * Callback executed each time data has been received from the L8.
 *
 * @param {Buffer} data
 * @private
 */
L8.prototype.onResponse_ = function(data) {
    var responses;

    data.copy(this.receiveBuffer_.buffer, this.receiveBuffer_.length);
    this.receiveBuffer_.length += data.length;

    if ((responses = this.parseFrames_(this.receiveBuffer_)) === false) {
        // The response has been incomplete. Therefore we wait until it is complete
        return;
    }

    // Redirect all incoming data to all methods, which wanted to be informed about it
    responses.forEach(function(response) {
        /**
         * Event fired every time a frame is received
         *
         * @event L8#frameReceived
         *
         * @type {{command: Number, parameters: Buffer, checksum: String, payloadLength: Number, raw: Buffer}}
         */
        this.emit("frameReceived", response);
    }.bind(this));
};

/**
 * Send a raw buffer bytestream to the connected L8
 *
 * If the expectedResponse property is set to true the callback will be hold back
 * until a corresponding CMD_OK response from the L8 has been received. This response
 * will be given to the callback then.
 *
 * If the command does not answer with a callback `false` may be given
 *
 * If the command does answer with another then the usual CMD_OK response it's
 * command code and optional parameters need to be specified as object. The object
 * has the following structure:
 *
 * `{command: Number, parameters: Buffer?}`
 *
 * The parameters are optional. If no `parameters` are specified any paramter will be
 * accepted as soon as the command is matched.
 *
 * Most commands will want to wait for the response.
 *
 * @param {Buffer} buffer
 * @param {Boolean|{command: Number, parameters: Buffer?}} expectedResponse
 * @param fn
 */
L8.prototype.sendFrame = function(buffer, expectedResponse, fn) {
    if (!this.isConnected) {
        throw new Error("L8 is not connected. Can't send data to it.");
    }

    this.serialport_.write(buffer, function(error, writeCount) {
        if (error) {
            fn(error, writeCount);
            return;
        }

        this.serialport_.drain(function(error, drainCount) {
            if (error) {
                fn(error, writeCount + drainCount);
                return;
            }

            /**
             * Event fired every time a frame is sent
             *
             * The provied value is the raw buffer, which has been sent.
             *
             * @event L8#frameSent
             * @type {Buffer}
             */
            this.emit("frameSent", buffer);

            // If a response is expected we need to introduce another indirection.
            // Otherwise we might return immediately
            if (expectedResponse === false) {
                // No response expected. We are ready to return
                fn(error, writeCount + drainCount);
                return;
            }

            var onReceive = function(frame) {
                /* Skip processing if it is not the awaited response */
                if (expectedResponse === true) {
                    if (frame.command !== SLCP.CMD.OK || frame.parameters[0] !== buffer[3]) {
                        // It is not the OK response for the issued command. Skip it.
                        return;
                    }
                } else {
                    // We are expecting another return command
                    if (frame.command !== expectedResponse.command) {
                        // Not the command/response we waited for
                        // Skip it.
                        return;
                    }

                    if (expectedResponse.parameters !== undefined && frame.parameters.toString("hex") !== expectedResponse.parameters.toString("hex")) {
                        // Not the parameters we expected.
                        // Skip it
                        return;
                    }
                }
                this.removeListener("frameReceived", onReceive);
                fn(error, frame);
            }.bind(this); /* onReceive */
            this.on("frameReceived", onReceive);
        }.bind(this)); /* drain */
    }.bind(this)); /* write */
};

/**
 * Build a frame in order to be sent to the L8
 *
 * An L8 frame has the following structure:
 * ```
 * MAGIC_BYTES|PayloadLength|Payload|PayloadCRC8
 * ```
 *
 * The Payload consists of a command as well as optional arguments
 *
 * The command is an integer, as documented here:
 * http://www.l8smartlight.com/dev/slcp/1.0/
 *
 * The {@link module:SLCP} does encode all the specified commands into
 * a human readable object structure. You may want to take a look at it, if you
 * intend to use this method for sending raw commands.
 *
 * The parameters are documented there as well. If a command does not have a
 * parameter (eg. `CMD_PING`). The `parametersBuffer` argument to this function may
 * be omitted.
 *
 * The parameters may be provided as a hex string or a Buffer object for convenience.
 *
 * The return value of this method is a ready to be sent Buffer, which can be
 * given to {@link L8#sendFrame} for transmission.
 *
 * @param {Number} command
 * @param {Buffer|String} [parametersBuffer]
 * @returns {Buffer}
 */
L8.prototype.buildFrame = function(command, parametersBuffer) {
    // Allow parameters to be buffer or hex string for convenience
    if (typeof parametersBuffer === "string") {
        parametersBuffer = new Buffer(parametersBuffer, "hex");
    }

    var commandBuffer = new Buffer(1);
    commandBuffer[0] = command;

    var fullPayloadBuffer;
    // There are commands without parameters
    if (parametersBuffer === null || parametersBuffer === undefined) {
        fullPayloadBuffer = commandBuffer;
    } else {
        fullPayloadBuffer = Buffer.concat([
            commandBuffer,
            parametersBuffer
        ], 1 + parametersBuffer.length);
    }

    var contentLengthBuffer = new Buffer(1);
    contentLengthBuffer[0] = fullPayloadBuffer.length;

    var frameLength = 2 /*MAGIC_BYTES*/ + 1 /*LENGTH*/ + fullPayloadBuffer.length + 1 /*CHECKSUM*/;

    var checksumBuffer = new Buffer(
        CRC.crc8(fullPayloadBuffer),
        "hex"
    );

    return Buffer.concat([
        L8.MAGIC_BYTES,
        contentLengthBuffer,
        fullPayloadBuffer,
        checksumBuffer
    ], frameLength);
};

/**
 * Create an object stream of acceleration data.
 *
 * The optional `samplingRate` time which is waited between asking the L8 for
 * data in milliseconds.
 *
 * See {@link AccelerationStream} for details about the created stream.
 *
 * @param {Number} [samplingRate]
 * @returns {AccelerationStream}
 */
L8.prototype.createAccelerationStream = function(samplingRate) {
    return new AccelerationStream(this, samplingRate);
};

/**
 * Query the L8s accelerometer for data.
 *
 * The data given to the callback is an object of the following form:
 *
 * ```
 *  {
 *      x: Number,
 *      y: Number,
 *      z: Number,
 *      lying: String,
 *      orientation: String,
 *      tap: Boolean,
 *      shake: Boolean
 *  }
 * ```
 *
 * The data structure corresponds to the response specified inside the
 * [L8 SLCP documentation about the CMD_L8_ACC_RESPONSE](http://www.l8smartlight.com/dev/slcp/1.0/#_Toc380755790).
 *
 * @param {Function} fn
 */
L8.prototype.getAcceleration = function(fn) {
    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_ACC_QUERY),
        {command: SLCP.CMD.L8_ACC_RESPONSE},
        function(error, data) {
            if (error) {
                fn(error, false);
                return;
            }

            var orientation;
            var parameters;
            var response;

            /*
             * Handle proper orientation mapping
             */
            parameters = data.parameters;
            switch (parameters[4]) {
                case 1:
                    orientation = 'up';
                    break;
                case 2:
                    orientation = 'down';
                    break;
                case 5:
                    orientation = 'left';
                    break;
                case 6:
                    orientation = 'right';
                    break;
                default:
                    //@TODO: Passing the parameter through in case it is not recognized
                    //       doesn't seem to be the right way. Analyse what happens here
                    //       and provide proper output in the future.
                    orientation = parameters[4];
            }

            response = {
                'x': parameters[0],
                'y': parameters[1],
                'z': parameters[2],
                'lying': (parameters[3] === 2) ? 'up' : 'upside_down',
                'orientation': orientation,
                'tap': (parameters[5] === 1), /* currently this always seems to be 1. Bug in the L8 firmware? */
                'shake': (parameters[6] !== 0)
            };

            fn(error, response);
        }
    );
};

/**
 * Ping the L8 in order to check if it is there.
 *
 * The response is currently not handled automatically. If you want to check if
 * a pong reply has been sent back using the {@link L8#event:frameReceived} event.
 *
 * @param {Function} fn
 */
L8.prototype.ping = function(fn) {
    this.sendFrame(
        this.buildFrame(SLCP.CMD.PING, null),
        true, fn
    );
};

/**
 * Encode a color object to the BGR bytesequence accepted by the L8 as a single color (3-byte)
 *
 * The color needs to be represented as object with `r`, `g` and `b` properties.
 *
 * The color values need to be within the limits [0,15]. Where 0 represents the lightest
 * and 15 the highest value.
 *
 * @param {Object} color
 * @returns {Buffer}
 */
L8.prototype.encodeBGRSingleColor = function(color) {
    if (color.r === undefined || color.r < 0 || color.r > 15
     || color.g === undefined || color.g < 0 || color.g > 15
     || color.b === undefined || color.b < 0 || color.b > 15) {
        throw new RangeError("Invalid color definition provided: " + JSON.stringify(color));
    }

    var colorBuffer = new Buffer(3);
    colorBuffer[0] = color.b;
    colorBuffer[1] = color.g;
    colorBuffer[2] = color.r;

    return colorBuffer;
};

/**
 * Encode a color object to the RGB bytesequence accepted by the L8 as a single color (3-byte)
 *
 * This color is for example used by the text scroller application
 *
 * The color needs to be represented as object with `r`, `g` and `b` properties.
 *
 * The color values need to be within the limits [0,15]. Where 0 represents the lightest
 * and 15 the highest value.
 *
 * @param {Object} color
 * @returns {Buffer}
 */
L8.prototype.encodeRGBSingleColor = function(color) {
    if (color.r === undefined || color.r < 0 || color.r > 15
        || color.g === undefined || color.g < 0 || color.g > 15
        || color.b === undefined || color.b < 0 || color.b > 15) {
        throw new RangeError("Invalid color definition provided: " + JSON.stringify(color));
    }

    var colorBuffer = new Buffer(3);
    colorBuffer[0] = color.r;
    colorBuffer[1] = color.g;
    colorBuffer[2] = color.b;

    return colorBuffer;
};

/**
 * Encode a color object to the BGR bytesequence accepted by the L8 as a matrix color (2-byte)
 *
 * The color needs to be represented as object with `r`, `g` and `b` properties.
 *
 * The color values need to be within the limits [0,15]. Where 0 represents the lightest
 * and 15 the highest value.
 *
 * @param {Object} color
 * @returns {Buffer}
 */
L8.prototype.encodeBGRMatrixColor = function(color) {
    if (color.r === undefined || color.r < 0 || color.r > 15
        || color.g === undefined || color.g < 0 || color.g > 15
        || color.b === undefined || color.b < 0 || color.b > 15) {
        throw new RangeError("Invalid color definiiton provided: " + JSON.stringify(color));
    }

    var colorBuffer = new Buffer(2);
    colorBuffer[0] = color.b;
    colorBuffer[1] = color.g << 4 | color.r;

    return colorBuffer;
};

/**
 * Set a specific LED inside the 8x8 L8 grid to a given color
 *
 * The color needs to be represented as object with `r`, `g` and `b` properties.
 *
 * The color values need to be within the limits [0,15]. Where 0 represents the lightest
 * and 15 the highest value.
 *
 * @param {Number} x
 * @param {Number} y
 * @param {Object} color
 * @param {Function} fn
 */
L8.prototype.setLED = function(x, y, color, fn) {
    if (x < 0 || x > 7 || y < 0 || y > 7) {
        throw new RangeError("x and y led coordinates are out of bounds: (" + x + " x " + y + ")");
    }

    var coordinateBuffer = new Buffer(2);
    coordinateBuffer[0] = x;
    coordinateBuffer[1] = y;

    var parametersBuffer = Buffer.concat([
        coordinateBuffer,
        this.encodeBGRSingleColor(color)
    ], 2 + 4);

    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_LED_SET, parametersBuffer),
        true, fn
    );
};

/**
 * Convenience function to switch of a single LED in the matrix.
 *
 * The same behaviour is archivable by setting all color components of the LED
 * to 0.
 *
 * @param {Number} x
 * @param {Number} y
 * @param {Function} fn
 */
L8.prototype.clearLED = function(x, y, fn) {
    this.setLED(x, y, {r: 0, g: 0, b: 0}, fn);
};

/**
 * Set the complete matrix of LEDs with one command
 *
 * This method should be used if you want display an "image" of sorts
 *
 * The provided matrix needs to be a 64 elements long array of color objects.
 *
 * Each of the color objects needs to provide an `r`, `g` and `b` property.
 *
 * The color values need to be within the limits [0,15]. Where 0 represents the
 * lightest and 15 the highest value.
 *
 * The matrix is provided line by line. Starting in the upper left corner, while
 * ending in the lower right.
 *
 * @param {Array} matrix
 * @param fn
 */
L8.prototype.setMatrix = function(matrix, fn) {
    if (matrix.length != 64) {
        throw new RangeError("Given matrix has the wrong length. Expected 64, got " + matrix.length);
    }

    var parametersBuffer = Buffer.concat(
        matrix.map(this.encodeBGRMatrixColor.bind(this)),
        8 /*LINES*/ * 8 /*COLUMNS*/ * 2 /*COLOR_LENGTH*/
    );

    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_MATRIX_SET, parametersBuffer),
        true, fn
    );
};

/**
 * Clear the matrix by switching off all its LEDs
 *
 * It is not necessary to execute this command between different `setMatrix` calls
 * Only use it if you explicitly want to turn the matrix off.
 *
 * This command does not affect the Super LED, which needs to be controlled
 * separately.
 *
 * @param fn
 */
L8.prototype.clearMatrix = function(fn) {
    /* According to the documentation of the protocol the CMD_L8_MATRIX_OFF has no
       parameters. Unfortunately this doesn't seem to be true. A zero byte is needed
       as parameter in order for the command to be accepted */
    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_MATRIX_OFF, "00"),
        true, fn
    );
};

/**
 * Set the SuperLED on the back of the L8 to the given color.
 *
 * The color needs to be represented as object with `r`, `g` and `b` properties.
 *
 * The color values need to be within the limits [0,15]. Where 0 represents the lightest
 * and 15 the highest value.
 *
 * @param {Object} color
 * @param {Function} fn
 */
L8.prototype.setSuperLED = function(color, fn) {
    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_SUPERLED_SET, this.encodeBGRSingleColor(color)),
        true, fn
    );
};

/**
 * Convenience function to switch off the SuperLED.
 *
 * The same effect may be created by setting all color components of the LED to 0
 *
 * @param {Function} fn
 */
L8.prototype.clearSuperLED = function(fn) {
    this.setSuperLED({r:0, g: 0, b: 0}, fn);
};

/**
 * Stop the currently running L8 application on the device.
 *
 * The L8 has certain predefined "Apps", which can be executed. This method
 * stops the currently running application.
 *
 * @param {Function} fn
 */
L8.prototype.stopApplication = function(fn) {
    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_APP_STOP, null),
        true, fn
    );
};

/**
 * Start the internal L8 application to scroll text across the device
 *
 * Once the scrolling has been started it needs to be stopped explicitly using
 * {@link L8#stopApplication} or the convenience method {@link L8#clearScrollingText}
 * Simply setting other colors to the LED matrix is not enough to cancel the
 * scrolling text.
 *
 * The `text` is supposed to be an ascii encoded string
 *
 * Color is specified as object with the usual `r`, `g`, `b` properties ranging
 * from 0-15.
 *
 * `speed` defines the scrolling speed. It is supposed to be one of "slow", "medium"
 * or "fast".
 *
 * `loop` is a boolean value specifying if the animation is supposed to be looped,
 * after it completed once. If it is set to `false` the scrolling application is
 * exited after the first run through the text.
 *
 * This command is not acquitted by an L8 response. Instead the sent bytes are given to
 * the callback
 *
 * @param {String} text
 * @param {Object} color
 * @param {String} speed
 * @param {Boolean} loop
 * @param {Function} fn
 */
L8.prototype.setScrollingText = function(text, color, speed, loop, fn) {
    var parametersBuffer = new Buffer(1 /*LOOP*/ + 1 /*SPEED*/ + 3 /*COLOR*/ + text.length);

    parametersBuffer[0] = (loop === true) ? 1 : 0;

    switch(speed) {
        case "slow":
            parametersBuffer[1] = 3;
        break;
        case "medium":
            parametersBuffer[1] = 2;
        break;
        case "fast":
            parametersBuffer[1] = 0;
        break;
        default:
            throw new RangeError("Invalid speed for scrolling text provided. Expected one of slow, medium or fast, got " + speed);
    }

    var colorBuffer = this.encodeRGBSingleColor(color);
    colorBuffer.copy(parametersBuffer, 2);

    var textBuffer = new Buffer(text, "ascii");
    textBuffer.copy(parametersBuffer, 5);

    this.sendFrame(
        this.buildFrame(SLCP.CMD.L8_SET_TEXT, parametersBuffer),
        false, fn
    );
};

/**
 * Convenience method to stop scrolling text from being displayed.
 *
 * The same behaviour is achievable by calling {@link L8#stopApplication} as the text
 * scroller is an internal L8 application.
 *
 * @param {Function} fn
 */
L8.prototype.clearScrollingText = function(fn) {
    this.stopApplication(fn);
};

/**
 * Manually set the orientation of the L8
 *
 * The orientation setting determines in which way the pixel matrix is drawn.
 *
 * Valid orientations are:
 *
 * - up
 * - down
 * - left
 * - right
 * - auto
 *
 * The orientation denotes, which side of the L8 is facing upwards. While the side
 * with the power on switch is considered the Top side, while the one with the usb
 * port is the Bottom side.
 *
 * This command is not acquitted by an L8 response all cases. Instead the sent
 * bytes are given to the callback in this situations.
 *
 * @param {String} orientation
 * @param {Function} fn
 */
L8.prototype.setOrientation = function(orientation, fn) {
    var parametersBuffer = new Buffer(1);

    if (orientation === "auto") {
        // Enable autoration an exit.
        parametersBuffer[0] = 1;
        this.sendFrame(
            this.buildFrame(SLCP.CMD.L8_SET_AUTOROTATE, parametersBuffer),
            {command: SLCP.CMD.OK, parameters: new Buffer("6a", "hex")}, fn
        );
    } else {
        // Disable autoration and set orientation manually.
        parametersBuffer[0] = 0;
        this.sendFrame(
            this.buildFrame(SLCP.CMD.L8_SET_AUTOROTATE, parametersBuffer),
            {command: SLCP.CMD.OK, parameters: new Buffer("6a", "hex")},
            function(error, response) {
                switch(orientation) {
                    case "up":
                        parametersBuffer[0] = 1;
                        break;
                    case "down":
                        parametersBuffer[0] = 2;
                        break;
                    case "left":
                        parametersBuffer[0] = 5;
                        break;
                    case "right":
                        parametersBuffer[0] = 6;
                        break;
                    default:
                        throw new RangeError("Unexpected orientation: " + orientation);
                }

                this.sendFrame(
                    this.buildFrame(SLCP.CMD.L8_SET_ORIENTATION, parametersBuffer),
                    false, fn
                );
            }.bind(this)
        );
    }
};

exports.L8 = L8;