Source: tinkatop.js

// TODO - Add Print Mode
/**
 * Class that represents a generic TinkaTop.
 * This sets up the shared methods used by all tops.
 * Typically used as an 'unsupported' top.
 */
class TinkaTop {
    /**
     * Creates an instance of the TinkaTop class
     */
    constructor() {
        this.id = 0;
        this.name = 'unsupported';
        this.return_types = ['i'];
    }
    /**
     * Typically what the sensor detects.
     * The default TinkaTop is not an actual sensor and only returns 0.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {0} Always returns 0
     *
     * @private
     */
    sense(command_id, command) {
        return 0;
    }

    /**
     * Returns the sensor values in the Open Sound Control (OSC) format.
     * Can range from a list of one values or multiple values depending
     * on the type of sensor.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {boolean | Object[]} args - a list of formatted OSC arguments
     * @returns {string} arg.type - the data type
     * @returns {*} arg.value - the sensor value
     *
     * @private
     */
    get_osc_args(command_id, command) {
        let reading = this.sense(command_id, command);
        let args = [];

        if (!reading && !(reading === 0)) { // If false or undefined
            return false;
        }

        for (let i=0; i<this.return_types.length; i++) {
            args.push(
                {
                    type: this.return_types[i],
                    value: reading[i] || reading
                }
            )
        }
        return args;
    }
    /** Get the value of id.
     *  ID's are determined by the Tinkamo hardware protocol.
     *  @returns {number}
     */
    get_id() {
        return this.id;
    }

    /** Get the value of name
     * @returns {string}
     */
    get_name() {
        return this.name;
    }
}

/**
 * Class representing a Button TinkaTop
 * @extends TinkaTop
 */
class Button extends TinkaTop {
    /**
     * Creates an instance of the Button class.
     */
    constructor() {
        super();
        this.id = 1;
        this.name = 'button';
        this.return_types = ['i'];
    }

    /**
     * Returns the state of the button, pressed (1) or depressed (0).
     * @param {number} command_id
     * @param {number[]} command
     * @returns {number}
     */
    sense(command_id, command) {
        let buttonState = command[0];
        return buttonState;
    }
}

/**
 * Class representing a Knob TinkaTop
 * @extends TinkaTop
 */
class Knob extends TinkaTop {
    /**
     * Creates an instance of the Knob class.
     */
    constructor() {
        super();

        this.id = 2;
        this.name = 'knob';
        this.return_types = ['f'];
    }

    /**
     * Returns the state of the potentiometer as a float in range -10 to 10.
     * The mapping from voltage to value was estimated from the original run
     * of Tinkamo and may need revision.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {number}
     */
    sense(command_id, command) {
        let fullNum = create_float(command);

        // Not quite a perfect mapping, but pretty close
        let knobNum = mapToRange(fullNum, 0, 3.45, -10, 10);

        return knobNum;
    }
}

/**
 * Class representing a Slider TinkaTop
 * @extends TinkaTop
 */
class Slider extends TinkaTop {
    /**
     * Creates an instance of the Slider class
     */
    constructor() {
        super();

        this.id = 3;
        this.name = 'knob';
        this.return_types = ['f'];
    }

    /**
     * Returns the state of the potentiometer as a float in range 0 to 10.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {number}
     */
    sense(command_id, command) {
        let sliderReading = 255 - command[0];
        let sliderNum = mapToRange(sliderReading, 0, 255, 0, 10);

        return sliderNum;
    }
}

/**
 * Class representing a Joystick TinkaTop
 * @extends TinkaTop
 */
class Joystick extends TinkaTop {
    /**
     * Creates an instance of the Joystick class
     */
    constructor() {
        super();

        this.id = 4;
        this.name = 'joystick';
        this.return_types = ['f', 'f'];
    }

    /**
     * Returns the X and Y positions as distance values from the center
     * ranging from -10 to 10.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {number[]} an array of size 2 containing x and y
     */
    sense(command_id, command) {
        let horizontalInt = command[0];
        let horizontalDec = command[1];
        let verticalInt = command[2];
        let verticalDec = command[3];

        let horizontalIn = horizontalInt + (horizontalDec/255);
        let verticalIn = verticalInt + (verticalDec/255);

        let horizontalNum = mapToRange(horizontalIn, 0, 4, -10, 10);
        let verticalNum = -1 * mapToRange(verticalIn, 0, 4, -10, 10);

        return [horizontalNum, verticalNum];
    }
}

/**
 * Class representing a Distance TinkaTop
 * @extends TinkaTop
 */
class Distance extends TinkaTop {
    /**
     * Creates an instance of the Distance class
     */
    constructor() {
        super();

        this.id = 23;
        this.name = 'distance';
        this.return_types = ['f'];
    }

    /**
     * When distance of objects are in range, return a number, typically
     * ranging from about 2 to 60.
     * When the sensor becomes inaccurate due to objects lying at too great a
     * distance, it sends a particular string of numbers that we interpret
     * and return as false.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {number | boolean}
     */
    sense(command_id, command) {
        let distNum = create_float(command);

        // distance sensor sends three messages when it gets out of range
        // Right now I am returning false to ignore - could return 120
        if (distNum > 60) {
            return false;
        }

        return distNum;
    }
}

/**
 * Class representing a Color TinkaTop
 * @extends TinkaTop
 */
class Color extends TinkaTop {
    /**
     * Creates an instance of the Color class
     */
    constructor() {
        super();

        this.id = 27;
        this.name = 'color';
        this.return_types = ['i', 'i', 'i'];
    }

    /**
     * Returns an array containing three integers refering to the red, green,
     * and blue content of a surface captured by the sensor.
     * The sensor may also send a value indicating that it stopped receiving
     * color (e.g. when held too far from from a surface) which we interpret
     * and return as false.
     * @param {number} command_id
     * @param {number[]} command
     * @returns {boolean | number[]} array with three values [red, green, blue]
     */
    sense(command_id, command) {
        let red = command[0];
        let green = command[1];
        let blue = command[2];
        let brightness = command[3]; // Not used - unsure what it is

        // The color sensor seems like it is bugging out and will periodically
        // flash all black... An extra if-statement prevents this problem...
        // Hardware bug (feature?) - If all zeros, ignore
        if (!(red + green + blue)) {
            return false;
        }

        return [red, green, blue]
    }
}

/**
 * Class representing a Motor TinkaTop.
 * STILL IN PROGRESS AND NOT TOTALLY SUPPORTED YET.
 * @extends TinkaTop
 *
 * @private
 */
class Motor extends TinkaTop {
    /**
     * Creates an instance of the Motor class
     */
    constructor(){
        super();

        this.id = 5;
        this.name = 'motor';
        this.return_type = ['i', 'f'];

        console.log("THIS WORKS");
    }

    /**
     * STILL IN PROGRESS - NOT FULLY IMPLEMENTED OR SUPPORTED
     * @param {number} command_id
     * @param {number[]} command
     * @returns {number[]}
     */
    sense(command_id, command){
        let direction = command[0];
        let intensityInt = command[1];
        let intensityDecimal = command[2];
        let intensityFloat = create_float([intensityInt, intensityDecimal]);
        if(intensityFloat != false)
            return [direction, intensityFloat];
    }

    /**
     * Creates a message for the motor to return
     * @param {number} direction Represents the direction of the motor, out of 255
     * @param {*} intensityInt The floored integer value of the speed
     * @param {*} intensityDecimal The decimal value of the speed
     * @returns {number[]} The motor message
     *
     *
     */
    createSpeedMotorMessage(direction, intensityInt, intensityDecimal){
        var motorMessage = new Uint8Array([90,171, 10,0,0,2,5,0,0,direction, intensityInt, intensityDecimal]);
        return motorMessage;
    }
}

// ----------------------Helper Functions----------------------

/**
 * The byte strings sent by the sensors represent decimal numbers in two
 * positions in their command message.
 * @param {number[]} command
 * @returns {number} The complete floating point number
 *
 * @private
 */
function create_float(command) {
    if (command.length != 2) {
        throw 'Create Float function called on incorrect sensor.';
        return false;
    }

    let intNum = command[0];
    let decNum = command[1];
    let fullNum = intNum + (decNum/255);

    return fullNum;
}

//
/**
 * Simple mapping function. Output number is guaranteed to be within output
 * range.
 * @param {number} num
 * @param {number} in_min
 * @param {number} in_max
 * @param {number} out_min
 * @param {number} out_max
 * @returns {number}
 *
 * @private
 */
function mapToRange(num, in_min, in_max, out_min, out_max) {
    let mappedVal = (num - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;

    if (mappedVal > out_max) return out_max;
    if (mappedVal < out_min) return out_min;
    return mappedVal;
}

export { TinkaTop, Button, Knob, Slider, Joystick, Distance, Color, Motor };