//
// EKS Otus HID controller script v1.0
// Copyright (C) 2012, Sean M. Pappalardo, Ilkka Tuohela
// but feel free to tweak this to your heart's content!
// For Mixxx version 1.11.x
//

// EKS Otus HID interface specification
function EKSOtusController() {
    this.controller = new HIDController();

    // Initialized to firmware version by version response packet
    this.version_major = undefined;
    this.version_minor = undefined;
    this.controller.activeDeck = 1;

    this.controller.LEDColors = { off: 0x0, red: 0x0f, green: 0xf0, amber: 0xff };
    this.controller.deckOutputColors = { 1: "red", 2: "green", 3: "red", 4: "green"};

    // Static variables for HID specs
    this.wheelLEDCount = 60;
    this.buttonLEDCount = 22;
    this.sliderLEDCount = 20;

    this.registerInputPackets = function() {
        var packet = undefined;
        var name = undefined;
        var offset = 0;

        packet = new HIDPacket("control", 0, undefined, [0x35]);
        packet.addControl("hid","wheel_position",2,"H");
        packet.addControl("hid","wheel_speed",4,"h");
        packet.addControl("hid","timestamp",6,"I");
        packet.addControl("hid","slider_value",10,"H");
        packet.addControl("hid","slider_position",12,"H");
        packet.addControl("hid","rate_encoder",14,"B",undefined,true);
        packet.addControl("hid","jog_se",15,"B",undefined,true);
        packet.addControl("hid","jog_sw",16,"B",undefined,true);
        packet.addControl("hid","rate_encoder",17,"B",undefined,true);
        packet.addControl("hid","gain_1",18,"H");
        packet.addControl("hid","gain_2",20,"H");
        packet.addControl("hid","eq_high_1",22,"H");
        packet.addControl("hid","eq_high_2",24,"H");
        packet.addControl("hid","eq_mid_1",26,"H");
        packet.addControl("hid","eq_mid_2",28,"H");
        packet.addControl("hid","eq_low_1",30,"H");
        packet.addControl("hid","eq_low_2",32,"H");
        packet.addControl("hid","crossfader",34,"H");
        packet.addControl("hid","headphones",36,"H");
        packet.addControl("hid","trackpad_x",38,"H");
        packet.addControl("hid","trackpad_y",40,"H");
        packet.addControl("hid","slider_pos_2",42,"H");
        packet.addControl("hid","slider_pos_1",44,"H");
        packet.addControl("hid","keylock",46,"I",0x1);
        packet.addControl("hid","beatloop_8",46,"I",0x2);
        packet.addControl("hid","beatloop_4",46,"I",0x4);
        packet.addControl("hid","beatloop_2",46,"I",0x8);
        packet.addControl("hid","beatloop_1",46,"I",0x10);
        packet.addControl("hid","loop_in",46,"I",0x20);
        packet.addControl("hid","loop_out",46,"I",0x40);
        packet.addControl("hid","reloop_exit",46,"I",0x80);
        packet.addControl("hid","slider_scale",46,"I",0x100);
        packet.addControl("hid","jog_se_button",46,"I",0x200);
        packet.addControl("hid","eject_right",46,"I",0x400);
        packet.addControl("hid","deck_switch",46,"I",0x800);
        packet.addControl("hid","eject_left",46,"I",0x1000);
        packet.addControl("hid","jog_sw_button",46,"I",0x2000);
        packet.addControl("hid","stop",46,"I",0x4000);
        packet.addControl("hid","play",46,"I",0x8000);
        packet.addControl("hid","cue",46,"I",0x10000);
        packet.addControl("hid","reverse",46,"I",0x20000);
        packet.addControl("hid","brake",46,"I",0x40000);
        packet.addControl("hid","fastforward",46,"I",0x80000);
        packet.addControl("hid","jog_nw_button",46,"I",0x100000);
        packet.addControl("hid","jog_touch",46,"I",0x200000);
        packet.addControl("hid","trackpad_left",46,"I",0x400000);
        packet.addControl("hid","trackpad_right",46,"I",0x800000);
        packet.addControl("hid","hotcue_1",46,"I",0x1000000);
        packet.addControl("hid","hotcue_2",46,"I",0x2000000);
        packet.addControl("hid","hotcue_3",46,"I",0x4000000);
        packet.addControl("hid","hotcue_4",46,"I",0x8000000);
        packet.addControl("hid","hotcue_5",46,"I",0x10000000);
        packet.addControl("hid","hotcue_6",46,"I",0x20000000);
        packet.addControl("hid","touch_slider",46,"I",0x40000000)
        packet.addControl("hid","touch_trackpad",46,"I",0x80000000);
        packet.addControl("hid","packet_number",51,"B");
        packet.addControl("hid","deck_status",52,"B");
        this.controller.registerInputPacket(packet);

        packet = new HIDPacket("firmware_version", 0xa, undefined, [0x4]);
        packet.addControl("hid","major",2,"B");
        packet.addControl("hid","minor",3,"B");
        this.controller.registerInputPacket(packet);

        packet = new HIDPacket("trackpad_mode", 0x5, undefined, [0x3]);
        packet.addControl("hid","status",2,"B");
        this.controller.registerInputPacket(packet);

    }

    this.registerOutputPackets = function() {
        var packet = undefined;
        var name = undefined;
        var offset = 0;

        packet = new HIDPacket("button_leds", 0x16, undefined, [0x18]);
        offset = 1;
        packet.addOutput("hid","jog_nw",offset++,"B");
        packet.addOutput("hid","jog_ne",offset++,"B");
        packet.addOutput("hid","jog_se",offset++,"B");
        packet.addOutput("hid","jog_sw",offset++,"B");
        packet.addOutput("hid","beatloop_8",offset++,"B");
        packet.addOutput("hid","beatloop_4",offset++,"B");
        packet.addOutput("hid","beatloop_2",offset++,"B");
        packet.addOutput("hid","beatloop_1",offset++,"B");
        packet.addOutput("hid","loop_in",offset++,"B");
        packet.addOutput("hid","loop_out",offset++,"B");
        packet.addOutput("hid","reloop_exit",offset++,"B");
        packet.addOutput("hid","eject_right",offset++,"B");
        packet.addOutput("hid","deck_switch",offset++,"B");
        packet.addOutput("hid","trackpad_right",offset++,"B");
        packet.addOutput("hid","trackpad_left",offset++,"B");
        packet.addOutput("hid","eject_left",offset++,"B");
        packet.addOutput("hid","stop",offset++,"B");
        packet.addOutput("hid","play",offset++,"B");
        packet.addOutput("hid","reverse",offset++,"B");
        packet.addOutput("hid","cue",offset++,"B");
        packet.addOutput("hid","brake",offset++,"B");
        packet.addOutput("hid","fastforward",offset++,"B");
        this.controller.registerOutputPacket(packet);

        packet = new HIDPacket("slider_leds", 0x17, undefined, [0x16]);
        offset = 1;
        packet.addOutput("pitch","slider_1",offset++,"B");
        packet.addOutput("pitch","slider_2",offset++,"B");
        packet.addOutput("pitch","slider_3",offset++,"B");
        packet.addOutput("pitch","slider_4",offset++,"B");
        packet.addOutput("pitch","slider_5",offset++,"B");
        packet.addOutput("pitch","slider_6",offset++,"B");
        packet.addOutput("pitch","slider_7",offset++,"B");
        packet.addOutput("pitch","slider_8",offset++,"B");
        packet.addOutput("pitch","slider_9",offset++,"B");
        packet.addOutput("pitch","slider_10",offset++,"B");
        packet.addOutput("pitch","slider_11",offset++,"B");
        packet.addOutput("pitch","slider_12",offset++,"B");
        packet.addOutput("pitch","slider_13",offset++,"B");
        packet.addOutput("pitch","slider_14",offset++,"B");
        packet.addOutput("pitch","slider_15",offset++,"B");
        packet.addOutput("pitch","slider_16",offset++,"B");
        packet.addOutput("pitch","slider_17",offset++,"B");
        packet.addOutput("pitch","slider_scale_1",offset++,"B");
        packet.addOutput("pitch","slider_scale_2",offset++,"B");
        packet.addOutput("pitch","slider_scale_3",offset++,"B");
        this.controller.registerOutputPacket(packet);

        packet = new HIDPacket("led_wheel_left", 0x14, undefined, [0x20]);
        offset = 1;
        for (var led_index=1;led_index<=this.wheelLEDCount/2;led_index++)
            packet.addOutput("hid","wheel_" + led_index,offset++,"B");
        this.controller.registerOutputPacket(packet);

        packet = new HIDPacket("led_wheel_right", 0x15, undefined, [0x20]);
        offset = 1;
        for (var led_index=this.wheelLEDCount/2+1;led_index<=this.wheelLEDCount;led_index++)
            packet.addOutput("hid","wheel_" + led_index,offset++,"B");
        this.controller.registerOutputPacket(packet);

        packet = new HIDPacket("request_firmware_version", 0xa, undefined, [0x2]);
        this.controller.registerOutputPacket(packet);

        packet = new HIDPacket("set_trackpad_mode", 0x5, undefined, [0x3]);
        packet.addControl("hid","mode",2,"B");
        this.controller.registerOutputPacket(packet);

        packet = new HIDPacket("set_ledcontrol_mode", 0x1d, undefined, [0x3]);
        packet.addControl("hid","mode",2,"B");
        this.controller.registerOutputPacket(packet);
    }

    // Otus specific output packet to request device firmware version
    this.requestFirmwareVersion = function() {
        var packet = this.controller.getOutputPacket("request_firmware_version");
        if (packet==undefined)
            return;
        HIDDebug("Requesting firmware version " + packet.name);
        packet.send();
    }

    // Set LED Control Mode on Otus firmware versions > 1.6. Major and minor must
    // contain the version numbers for firmware as received from response.
    // Valid modes are:
    //      0   disable all LEDs
    //      1   Re-enable LEDs
    //      2   Revert to built-in light functionality
    this.setLEDControlMode = function(mode) {
        var controller = this.controller;
        if (this.version_major<=1 && this.version_minor<6) {
            // Firmware version does not support LED Control Mode Setting
            return;
        }
        if (mode!=0 && mode!=1 && mode!=2) {
            HIDDebug("Unknown value for LED Control Mode Setting: " + mode);
            return;
        }
        var packet = controller.getOutputPacket("set_ledcontrol_mode");
        var field = packet.getField("hid","mode");
        if (field==undefined) {
            HIDDebug("EksOtus.setLEDControlMode error fetching field mode");
            return;
        }
        field.value = mode;
        packet.send();
    }

    // Firmware version response. Required to finish device INIT
    this.FirmwareVersionResponse = function(packet,delta) {
        var controller = this.controller;
        var field_major = packet.getField("hid","major");
        var field_minor = packet.getField("hid","minor");
        if (field_major==undefined || field_minor==undefined) {
            HIDDebug("Error parsing response version packet");
            return;
        }
        this.version_major = field_major.value;
        this.version_minor = field_minor.value;
        controller.initialized = true;

        this.setLEDControlMode(1);
        if (controller.activeDeck!=undefined) {
            controller.setOutput("hid","deck_switch", controller.LEDColors[controller.deckOutputColors[controller.activeDeck]]);
            controller.switchDeck(controller.activeDeck);
        } else {
            var value = controller.LEDColors["amber"];
            this.controller.setOutputToggle("hid","deck_switch",value);
        }
        this.updateLEDs();
        HIDDebug("EKS " + EksOtus.id +
            " v"+EksOtus.version_major+"."+EksOtus.version_minor+
            " initialized"
        );
    }

    // Otus specific output packet to set the trackpad control mode
    this.setTrackpadMode = function(mode) {
        if (mode!=0 && mode!=1) {
            HIDDebug("Unsupported trackpad mode value: " + mode);
            return;
        }
        var packet = this.controller.getOutputPacket("set_trackpad_mode");
        if (packet==undefined) {
            HIDDebug("Output not registered: set_trackpad_mode");
            return;
        }
        var field = packet.getField("hid","mode");
        if (field==undefined) {
            HIDDebug("EksOtus.setTrackpadMode error fetching field mode");
            return;
        }
        field.value = mode;
        packet.send();
    }

    // Response to above trackpad mode packet
    this.TrackpadModeResponse = function(packet,delta) {
        field = packet.getField("hid","status");
        if (field==undefined) {
            HIDDebug("Error parsing field status from packet");
            return;
        }
        if (field.value==1) {
            HIDDebug("Trackpad mode successfully set");
        } else {
            HIDDebug("Trackpad mode change failed");
        }
    }

    // Generic unsigned short to -1..0..1 range scaling
    this.plusMinus1Scaler = function(group,name,value) {
        if (value<32768)
            return value/32768-1;
        else
            return (value-32768)/32768;
    }

    // Volume slider scaling for 0..1..5 scaling
    this,volumeScaler = function(group,name,value) {
        return script.absoluteNonLin(value, 0, 1, 5, 0, 65536);
    }

    // EQ scaling function for 0..1..4 scaling
    this.eqScaler = function(group,name,value) {
        return script.absoluteNonLin(value, 0, 1, 4, 0, 65536);
    }

    // Mandatory call from init() to initialize hardware
    this.initializeHIDController = function() {
        this.registerInputPackets();
        this.registerOutputPackets();
    }

    this.shutdownHardware = function() {
        this.setLEDControlMode(2);
        this.setTrackpadMode(1);
    }

}

EksOtus = new EKSOtusController();

// Initialize device state, send request for firmware. Otus is not
// usable before we receive a valid firmware version response.
EksOtus.init = function (id) {
    EksOtus.id = id;

    EksOtus.LEDUpdateInterval = 250;
    // Valid values: 1 for mouse mode, 0 for xy-pad mode
    EksOtus.trackpadMode = 0;

    EksOtus.deckSwitchClicked = false;

    // Wheel absolute position value
    EksOtus.wheelPosition = undefined;

    // Wheel spin animation details
    EksOtus.activeTrackDuration = undefined;
    // Group registered to update spinning platter details
    EksOtus.activeSpinningPlatterGroup = undefined;
    // Virtual record spin time, 1.8 for 33 1/3 RPM, 1.33 for 45 RPM
    EksOtus.revTime = 1.8;
    // Wheel LED index, range 1-60
    EksOtus.activeSpinningPlatterLED = undefined;

    // Call the HID packet parser initializers
    EksOtus.initializeHIDController();
    var controller = EksOtus.controller;
    // Set callbacks for packets here to avoid issues in callback handling
    controller.setPacketCallback("firmware_version",EksOtus.FirmwareVersionWrapper);
    controller.setPacketCallback("trackpad_mode",EksOtus.TrackpadModeWrapper);

    controller.ignoredControlChanges = [
        "mask","timestamp","packet_number","deck_status", "wheel_speed",
        // These return the Otus slider position scaled by the 'slider scale'
        "slider_pos_1","slider_pos_2", "slider_value"
    ];

    // Scratch parameters
    controller.scratchintervalsPerRev = 1024;
    controller.scratchAlpha = 1.0/8;
    controller.rampedScratchEnable = true;

    EksOtus.setTrackpadMode(this.trackpadMode);
    // Note: Otus is not considered initialized before we get
    // response to this packet
    EksOtus.requestFirmwareVersion();
    // Link controls and register callbacks
    EksOtus.registerCallbacks();

    engine.softTakeover("[Master]","headVolume",true);
    engine.softTakeover("[Master]","headMix",true);
    for (var deck in controller.deckOutputColors) {
        engine.softTakeover("[Channel"+deck+"]","pregain",true);
        engine.softTakeover("[Channel"+deck+"]","volume",true);
    }

    if (EksOtus.LEDUpdateInterval!=undefined) {
        controller.timers["led_update"] = engine.beginTimer(
            EksOtus.LEDUpdateInterval,
            () => EksOtus.updateLEDs(true)
        );
    }

}

EksOtus.outputCallback = function(value,group,key) {
    var controller = EksOtus.controller;
    if (group=="deck") {
        if (controller.activeDeck==undefined)
            return;
        group = controller.resolveGroup("deck");
    }
    if (value==1)
        EksOtus.controller.setOutput("deck",key,
            controller.LEDColors[controller.deckOutputColors[controller.activeDeck]],
            true
        );
    else
        EksOtus.controller.setOutput("deck",key,controller.LEDColors.off,true);
}

EksOtus.updateLEDs = function(from_timer) {
    var controller = EksOtus.controller;
    controller.getOutputPacket("button_leds").send();
    controller.getOutputPacket("slider_leds").send();
    controller.getOutputPacket("led_wheel_left").send();
    controller.getOutputPacket("led_wheel_right").send();
}

// Device cleanup function
EksOtus.shutdown = function() {
    engine.softTakeover("[Master]","headVolume",false);
    engine.softTakeover("[Master]","headMix",false);
    for (var deck in controller.deckOutputColors) {
        engine.softTakeover("[Channel"+deck+"]","pregain",false);
        engine.softTakeover("[Channel"+deck+"]","volume",false);
    }
    EksOtus.shutdownHardware(2);
    HIDDebug("EKS "+EksOtus.id+" shut down");
}

// Mandatory default handler for incoming packets
EksOtus.incomingData = function(data,length) {
    EksOtus.controller.parsePacket(data,length);
}

EksOtus.FirmwareVersionWrapper = function(packet,data) {
    return EksOtus.FirmwareVersionResponse(packet,data);
}

EksOtus.TrackpadModeWrapper = function(packet,data) {
    return EksOtus.TrackpadModeResponse(packet,data);
}

// Callback to set current loaded track's duration for wheel led animation
EksOtus.loadedTrackDuration = function(value) {
    EksOtus.activeTrackDuration = value;
}

// Link virtual HID naming of input and LED controls to mixxx
// Note: HID specification has more fields than we map here.
EksOtus.registerCallbacks = function() {
    var controller = EksOtus.controller;

    controller.linkModifier("hid","eject_right","shift");
    controller.linkModifier("hid","touch_slider","pitch");

    controller.linkControl("hid","play","deck","play");
    controller.linkControl("hid","cue","deck","cue_default");
    controller.linkControl("hid","reverse","deck","reverse");
    controller.linkControl("hid","eject_left","deck","pfl");
    controller.linkControl("hid","jog_touch","deck","jog_touch");
    controller.linkControl("hid","wheel_position","deck","jog_wheel");

    controller.linkControl("hid","jog_se_button","deck","LoadSelectedTrack");
    controller.linkControl("hid","jog_se","[Playlist]","SelectTrackKnob");

    controller.linkControl("hid","crossfader","[Master]","crossfader");
    controller.linkControl("hid","gain_1","deck1","pregain");
    controller.linkControl("hid","gain_2","deck2","pregain");
    controller.linkControl("hid","eq_high_1","deck1","filterHigh");
    controller.linkControl("hid","eq_high_2","deck2","filterHigh");
    controller.linkControl("hid","eq_mid_1","deck1","filterMid");
    controller.linkControl("hid","eq_mid_2","deck2","filterMid");
    controller.linkControl("hid","eq_low_1","deck1","filterLow");
    controller.linkControl("hid","eq_low_2","deck2","filterLow");

    controller.setScaler("jog",EksOtus.jogScaler);
    controller.setScaler("jog_scratch",EksOtus.wheelScaler);
    controller.setScaler("crossfader",EksOtus.plusMinus1Scaler);
    controller.setScaler("pregain",EksOtus.eqScaler);
    controller.setScaler("filterHigh",EksOtus.eqScaler);
    controller.setScaler("filterMid",EksOtus.eqScaler);
    controller.setScaler("filterLow",EksOtus.eqScaler);

    controller.setCallback("control","hid","hotcue_1",EksOtus.hotcue);
    controller.setCallback("control","hid","hotcue_2",EksOtus.hotcue);
    controller.setCallback("control","hid","hotcue_3",EksOtus.hotcue);
    controller.setCallback("control","hid","hotcue_4",EksOtus.hotcue);
    controller.setCallback("control","hid","hotcue_5",EksOtus.hotcue);
    controller.setCallback("control","hid","hotcue_6",EksOtus.hotcue);

    controller.setCallback("control","hid","beatloop_1",EksOtus.beatloop);
    controller.setCallback("control","hid","beatloop_2",EksOtus.beatloop);
    controller.setCallback("control","hid","beatloop_4",EksOtus.beatloop);
    controller.setCallback("control","hid","beatloop_8",EksOtus.beatloop);
    controller.linkControl("hid","loop_in","deck","loop_in");
    controller.linkControl("hid","loop_out","deck","loop_out");
    controller.linkControl("hid","reloop_exit","deck","reloop_exit");

    controller.setCallback("control","hid","deck_switch",EksOtus.deckSwitch);

    //controller.linkControl("hid","headphones","[Master]","headphones");
    controller.setCallback("control","hid","headphones",EksOtus.headphones);

    controller.setCallback("control","hid","slider_scale",EksOtus.pitchSlider);
    controller.setCallback("control","hid","slider_value",EksOtus.pitchSlider);
    controller.setCallback("control","hid","slider_position",EksOtus.pitchSlider);
    controller.setCallback("control","hid","slider_pos_1",EksOtus.pitchSlider);
    controller.setCallback("control","hid","slider_pos_2",EksOtus.pitchSlider);

    controller.linkOutput("hid","beatloop_8","deck","beatloop_8_enabled",EksOtus.outputCallback);
    controller.linkOutput("hid","beatloop_4","deck","beatloop_4_enabled",EksOtus.outputCallback);
    controller.linkOutput("hid","beatloop_2","deck","beatloop_2_enabled",EksOtus.outputCallback);
    controller.linkOutput("hid","beatloop_1","deck","beatloop_1_enabled",EksOtus.outputCallback);
    controller.linkOutput("hid","loop_in","deck","loop_in",EksOtus.outputCallback);
    controller.linkOutput("hid","loop_out","deck","loop_out",EksOtus.outputCallback);
    controller.linkOutput("hid","reloop_exit","deck","reloop_exit",EksOtus.outputCallback);
    controller.linkOutput("hid","eject_left","deck","pfl",EksOtus.outputCallback);
    controller.linkOutput("hid","play","deck","play",EksOtus.outputCallback);
    controller.linkOutput("hid","reverse","deck","reverse",EksOtus.outputCallback);
    controller.linkOutput("hid","cue","deck","cue_default",EksOtus.outputCallback);

}

// Default scaler for jog values
EksOtus.wheelScaler = function(group,name,value) {
    if (EksOtus.wheelPosition==undefined) {
        EksOtus.wheelPosition = value;
        return 0;
    }
    var delta = EksOtus.wheelPosition - value;
    if (delta>32768)
        return 0;
    EksOtus.wheelPosition = value;
    if (delta>-8 && delta<8)
        return -delta/4;
    return -delta/16;
}

EksOtus.jogScaler = function(group,name,value) {
    if (EksOtus.wheelPosition==undefined) {
        EksOtus.wheelPosition = value;
        return 0;
    }
    var delta = EksOtus.wheelPosition - value;
    if (delta>32768)
        return 0;
    EksOtus.wheelPosition = value;
    return -delta/64;
}

// Deck rate adjustment with top corner wheels
EksOtus.rate_wheel = function(field) {
    var controller = EksOtus.controller;
    if (controller.activeDeck==undefined)
        return;
    var active_group = controller.resolveGroup(field.group);
    var current = engine.getValue(active_group,"rate");
    if (field.delta<0)
        engine.setValue(active_group,"rate",current+0.003);
    else
        engine.setValue(active_group,"rate",current-0.003);
}

// Reset all wheel LEDs to given color. If color is undefined,
// use 'off'
EksOtus.resetWheelLEDs = function (color) {
    var controller = EksOtus.controller;
    if (color==undefined || !(color in controller.LEDColors))
        color = "off";
    for (i=1;i<=EksOtus.wheelLEDCount;i++)
        controller.setOutput("jog","wheel_"+i,color,false);
    EksOtus.updateLEDs(true);
}

// Rotation of the Otus 'corner' wheels.
// Note right bottom wheel is library browser encoder and not handled here
EksOtus.corner_wheel = function(field) {
    // TODO - attach some functionality these corner wheels!
    print("CORNER " + field.name + " delta " + field.delta);
}

// Hotcues activated with normal press, cleared with shift
EksOtus.hotcue = function (field) {
    var controller = EksOtus.controller;
    var command;
    if (controller.activeDeck==undefined ||
        field.value==controller.buttonStates.released)
        return;
    var active_group = controller.resolveDeckGroup(controller.activeDeck);
    if (controller.modifiers.get("shift"))
        command = field.name + "_clear";
    else
        command = field.name + "_activate";
    engine.setValue(active_group,command,true);
}

// Beatloops activated with normal presses to beatloop_1 - beatloop_8
EksOtus.beatloop = function (field) {
    var controller = EksOtus.controller;
    var command;
    if (controller.activeDeck==undefined ||
        field.value==controller.buttonStates.released)
        return;
    var active_group = controller.resolveDeckGroup(controller.activeDeck);
    command = field.name + "_activate";
    engine.setValue(active_group,command,true);
}

EksOtus.beat_align = function (field) {
    var controller = EksOtus.controller;
    if (controller.activeDeck==undefined)
        return;
    var active_group = controller.resolveGroup(field.group);
    if (controller.modifiers.get("shift")) {
        // if (field.value==controller.buttonStates.released) return;
        engine.setValue(active_group,"beats_translate_curpos",field.value);
    } else {
        if (field.value==controller.buttonStates.released)
            return;
        if (!engine.getValue(active_group,"quantize"))
            engine.setValue(active_group,"quantize",true);
        else
            engine.setValue(active_group,"quantize",false);
    }
}

// Pitch slider modifies track speed directly
EksOtus.pitchSlider = function (field) {
    var controller = EksOtus.controller;
    if (controller.activeDeck==undefined)
        return;
    if (controller.modifiers.get("pitch")) {
        var active_group = controller.resolveDeckGroup(controller.activeDeck);
        if (field.name=="slider_position") {
            if (field.value==0)
                return;
            var value = EksOtus.plusMinus1Scaler(
                active_group,field.name,field.value
            );
            engine.setValue(active_group,"rate",value);
        }
    }
}

// Set pregain, if modifier shift is active, deck volume otherwise
EksOtus.volume_pregain = function (field) {
    var controller = EksOtus.controller;
    if (controller.activeDeck==undefined)
        return;
    var active_group = controller.resolveGroup(field.group);
    if (controller.modifiers.get("shift")) {
        value = script.absoluteNonLin(field.value, 0, 1, 5, 0, 65536);
        engine.setValue(active_group,"pregain",value);
    } else {
        value = field.value / 65536;
        engine.setValue(active_group,"volume",value);
    }
}

// Set headphones volume, if modifier shift is active, pre/main mix otherwise
EksOtus.headphones = function (field) {
    var controller = EksOtus.controller;
    if (controller.modifiers.get("shift")) {
        value = script.absoluteNonLin(field.value, 0, 1, 5, 0, 65536);
        engine.setValue("[Master]","headVolume",value);
    } else {
        value = EksOtus.plusMinus1Scaler(field.group,field.name,field.value);
        engine.setValue("[Master]","headMix",value);
    }
}

// Control effects or something with XY pad
EksOtus.xypad = function(field) {
    var controller = EksOtus.controller;
    if (controller.activeDeck==undefined)
        return;
    print ("XYPAD group " + field.group +
        " name " + field.name + " value " + field.value
    );
}

// Function called when the special 'Deck Switch' button is pressed
// TODO - add code for 'hold deck_switch and press hot_cue[1-4]
// to select deck 1-4
EksOtus.deckSwitch = function(field) {
    var controller = EksOtus.controller;
    if (EksOtus.initialized==false)
        return;
    if (field.value == controller.buttonStates.released) {
        if (EksOtus.deckSwitchClicked==false) {
            EksOtus.deckSwitchClicked=true;
            controller.timers["deck_switch"] = engine.beginTimer(
                250, EksOtus.deckSwitchClickedClear
            );
        } else {
            EksOtus.deckSwitchDoubleClick();
        }
    }
}

// Timer to clear the double click status for deck switch
EksOtus.deckSwitchClickedClear = function() {
    EksOtus.deckSwitchClicked = false;
    var controller = EksOtus.controller;
    if (controller.timers["deck_switch"]!=undefined) {
        engine.stopTimer(controller.timers["deck_switch"]);
        delete controller.timers["deck_switch"];
    }
}

// Function to handle case when 'deck_switch' button was double clicked
EksOtus.deckSwitchDoubleClick = function() {
    var controller = EksOtus.controller;
    EksOtus.deckSwitchClicked = false;
    if (controller.timers["deck_switch"]!=undefined) {
        engine.stopTimer(controller.timers["deck_switch"]);
        delete controller.timers["deck_switch"];
    }
    controller.switchDeck();
    controller.setOutput("hid","deck_switch", controller.LEDColors[controller.deckOutputColors[controller.activeDeck]]);
    EksOtus.updateLEDs();
    HIDDebug("Active EKS Otus deck now " + controller.activeDeck);
}

// Switch the visual LED feedback on platter LEDs to active deck
// NOTE this is now disabled, it causes HID input errors in firmware!
EksOtus.activateSpinningPlatterLEDs = function() {
    var controller = EksOtus.controller;
    var active_group = controller.resolveDeckGroup(controller.activeDeck);
    if (active_group==undefined)
        return;
    if (!(controller.activeDeck in controller.deckOutputColors)) {
        HIDDebug("LED color not mapped to deck " % controller.activeDeck);
        return;
    }
    if (active_group==undefined) {
        EksOtus.disableSpinningPlatterLEDs();
        return;
    }
    if (EksOtus.activeSpinningPlatterGroup !=undefined) {
        EksOtus.disableSpinningPlatterLEDs();
    }
    EksOtus.activeSpinningPlatterGroup = active_group;
    EksOtus.loadedTrackDuration(engine.getValue(active_group,"duration"));
    EksOtus.enableSpinningPlatterLEDs();
}

// Enable spinning platter LED functionality for active virtual deck
EksOtus.enableSpinningPlatterLEDs = function() {
    if (EksOtus.activeSpinningPlatterGroup==undefined)
        return;
    engine.connectControl(
        EksOtus.activeSpinningPlatterGroup,
        "visual_playposition",
        "EksOtus.circleLEDs"
    );
    engine.connectControl(
        EksOtus.activeSpinningPlatterGroup,
        "duration",
        "EksOtus.loadedTrackDuration"
    )
    EksOtus.resetWheelLEDs("off",false);
}

// Disable spinning platter LED functionality for active virtual deck
EksOtus.disableSpinningPlatterLEDs = function() {
    if (EksOtus.activeSpinningPlatterGroup==undefined)
        return;
    engine.connectControl(
        EksOtus.activeSpinningPlatterGroup,
        "visual_playposition",
        "EksOtus.circleLEDs",
        true
    );
    engine.connectControl(
        EksOtus.activeSpinningPlatterGroup,
        "duration",
        "EksOtus.loadedTrackDuration",
        true
    )
    EksOtus.resetWheelLEDs("off",false);
}

// Callback from engine to set every third LED in circling pattern according to
// the track position. Careful not to enable sending all 60 positions, it may
// cause too much HID traffic!
EksOtus.circleLEDs = function(position) {
    var controller = EksOtus.controller;
    if (position<0 || position>1) {
        EksOtus.resetWheelLEDs("off",false);
        return;
    }
    // Only update every third LED to save HID packet bandwidth
    var wheelLEDSplit = 3;
    var wheelLEDGroups = EksOtus.wheelLEDCount/wheelLEDSplit;
    var timeRemaining = ((1-position)*EksOtus.activeTrackDuration) | 0;
    var track_pos = position * EksOtus.activeTrackDuration;
    var revolutions = track_pos / EksOtus.revTime;
    var led_index = (((revolutions-(revolutions|0))*wheelLEDGroups)|0)*wheelLEDSplit;
    led_index++;
    if (led_index==EksOtus.activeSpinningPlatterLED)
        return;
    EksOtus.activeSpinningPlatterLED = led_index;
    EksOtus.resetWheelLEDs("off",false);
    var led_color = controller.deckOutputColors[controller.activeDeck];
    controller.setOutput("jog","wheel_"+(led_index),led_color);
    EksOtus.updateLEDs();
}
