diff --git a/Corsair_Virtuoso_XT_Headset.js b/Corsair_Virtuoso_XT_Headset.js new file mode 100644 index 0000000..55a5346 --- /dev/null +++ b/Corsair_Virtuoso_XT_Headset.js @@ -0,0 +1,254 @@ +export function Name() { return "Corsair Virtuoso XT Headset"; } +export function VendorId() { return 0x1b1c; } +export function ProductId() { return 0x0A64; } +export function Publisher() { return "WhirlwindFX"; } +export function Documentation(){ return "troubleshooting/corsair"; } +export function Size() { return [1, 1]; } +export function DefaultPosition(){return [145, 85];} +export function DefaultScale(){return 10.0;} +/* global +LightingMode:readonly +forcedColor:readonly +idleTimeout:readonly +idleTimeoutLength:readonly +SidetoneAmount:readonly +*/ +export function ControllableParameters() { + return [ + {"property":"LightingMode", "group":"lighting", "label":"Lighting Mode", "type":"combobox", "values":["Canvas", "Forced"], "default":"Canvas"}, + {"property":"forcedColor", "group":"lighting", "label":"Forced Color", "min":"0", "max":"360", "type":"color", "default":"#009bde"}, + {"property":"idleTimeout", "group":"", "label":"Enable Device Sleep", "type":"boolean", "default":"true"}, + {"property":"idleTimeoutLength", "group":"", "label":"Device Sleep Timeout (Minutes)", "type":"combobox", "values":["1","2","3","4","5","10","15","20","25","30"], "default":"10"}, + {"property":"SidetoneAmount", "group":"", "label":"Sidetone", "step":"1", "type":"number", "min":"0", "max":"100", "default":"0"}, + ]; +} + +let headsetMode; +let device_status; +let previousBatteryLevel = -1; +let batteryPercentage = -1; + +const vLeds = [ 0, 1 ] + +const vLedNames = [ + "Logo", "Mic" +]; + +const vLedPositions = [ + [0, 0], [0, 1] +]; + +export function LedNames() { + return vLedNames; +} + +export function LedPositions() { + return vLedPositions; +} + +export function Initialize() { + // Wired uses 0x08 and wireless device uses 0x09 + headsetMode = (ProductId() == 0x0a6b || ProductId() == 0x0a64) ? 0x09 : 0x08; + device.addFeature("battery"); + setSoftwareMode(); + setMicOn() + setSidetone(); + setIdleTimeout(idleTimeoutLength); +} + +export function Render() { + readDevice(); // Micstatus why seperate stream?! + sendColors(); +} + +export function Shutdown(SystemSuspending) { + if(SystemSuspending){ + // Go Dark on System Sleep/Shutdown + sendColors("#000000"); + }else{ + device.write([0x02, headsetMode, 0x01, 0x03, 0x00, 0x01], 64); // Hardware mode + } +} + +export function onSidetoneAmountChanged() { + setSidetone(); +} + +export function onidleTimeoutLengthChanged() { + if (idleTimeout){ + console.log ("Set Idle Timeout to: " + idleTimeoutLength + "Minutes") + setIdleTimeout(idleTimeoutLength) + } +} + +function sendColors(overrideColor) { + + const packet = []; + const RGBData = []; + + packet[0] = 0x02; + packet[1] = headsetMode; + packet[2] = 0x06; + packet[4] = 0x09; + + for (let idx = 0; idx < vLedPositions.length; idx++) { + const iPxX = vLedPositions[idx][0]; + const iPxY = vLedPositions[idx][1]; + let color; + + if(overrideColor){ + color = hexToRgb(overrideColor); + }else if (LightingMode === "Forced") { + color = hexToRgb(forcedColor); + }else{ + color = device.color(iPxX, iPxY); + } + + RGBData[(vLeds[idx]*3)] = color[0]; + RGBData[(vLeds[idx]*3)+1] = color[1]; + RGBData[(vLeds[idx]*3)+2] = color[2]; + } + + device.set_endpoint(3, 0x0001, 0xFF42); + device.write(packet, 64); +} + +function setSidetone() { + device.set_endpoint(3, 0x0001, 0xFF42); + const packet = []; + // Header + packet[0] = 0x02; + packet[1] = headsetMode; + packet[2] = 0x01; + packet[3] = 0x47; + + let sidetoneValue = Math.round((SidetoneAmount / 100) * 1000); + + packet[0x05] = sidetoneValue & 0xFF; + packet[0x06] = (sidetoneValue >> 8) & 0xFF; + device.write(packet, 64); +} + +export function onidleTimeoutChanged() { + if (idleTimeout){ + device.set_endpoint(3, 0x0001, 0xFF42); + device.write([0x02, headsetMode, 0x01, 0x0d, 0x01], 128); + device.pause(1); + setIdleTimeout(idleTimeoutLength) + }else{ + device.set_endpoint(3, 0x0001, 0xFF42); + device.write([0x02, headsetMode, 0x01, 0x0d], 128); + device.log("Set IdleTimeout to: disabled"); + } +} + +function setSoftwareMode() { + device.set_endpoint(3, 0x0001, 0xFF42); + + device.write([0x02, headsetMode, 0x01, 0x03, 0x00, 0x02], 64); // Enable Software Mode + device.write([0x02, headsetMode, 0x0D, 0x00, 0x01], 64); //Open lighting endpoint + device.pause(100); + device.write([0x02, headsetMode, 0x01, 0x02, 0x00, 0xe8, 0x03], 64); //Hardware Brightness 100% + +} + +function readDevice() { + device.set_endpoint(3, 0x0002, 0xFF42); + const packet = device.read([0x00], 16, 15); + + if(packet[0] == 0x03 && packet[1] == 0x01 && packet[2] == 0x01 && packet[3] == 0x10 && packet[4] == 0x00 && packet[5] == 0x02) { + setSoftwareMode(); + } + + if(packet[0] == 0x03 && packet[2] == 0x01 && packet[3] == 0x46 && packet[4] == 0x00 && packet[5] == 0x00) { + console.log("Microphone: ON") + } + + if(packet[0] == 0x03 && packet[2] == 0x01 && packet[3] == 0x46 && packet[4] == 0x00 && packet[5] == 0x01) { + console.log("Microphone: OFF") + } + if(packet[0] == 0x03 && packet[2] == 0x01 && packet[3] == 0x10 && packet[4] == 0x00 && packet[5] == 0x01) { + battery.setBatteryState(2) + console.log("Headset is charging") + } + if(packet[0] == 0x03 && packet[2] == 0x01 && packet[3] == 0x10 && packet[4] == 0x00 && packet[5] == 0x02) { + battery.setBatteryState(1) + console.log("Headset is draining") + } + if (packet[0] == 0x03 && packet[1] == 0x01 && packet[2] == 0x01 && packet[3] == 0x0f && packet[4] == 0x00) { + + let batteryValue = (packet[5] & 0xFF) | ((packet[6] & 0xFF) << 8); + let maxBatteryValue = 1023; // 0xff03 in Dezimal + + batteryPercentage = Math.round((batteryValue / maxBatteryValue) * 100); + + console.log("Battery Level: " + batteryPercentage + "%"); + console.log("Battery Level hex: " + packet[5].toString(16) + " " + packet[6].toString(16)); + + battery.setBatteryLevel(batteryPercentage); + + if (typeof previousBatteryLevel !== 'undefined') { + if (batteryPercentage < previousBatteryLevel) { + battery.setBatteryState(1); + } else if (batteryPercentage > previousBatteryLevel) { + battery.setBatteryState(2); + } + } + previousBatteryLevel = batteryPercentage; + } +} + +function setMicOn() { + device.set_endpoint(3, 0x0001, 0xFF42); + device.write([0x02, headsetMode, 0x01, 0x8e, 0x00, 0x00], 128); //Mic Status + device.write([0x02, headsetMode, 0x01, 0x46, 0x00, 0x00], 128); //commit +} + +function setIdleTimeout(idleTimeoutLength) { + applyPreConfig() + applyIdleTimeout(idleTimeoutLength) +} + +function applyIdleTimeout(idleTimeoutLength) { + device.set_endpoint(3, 0x0001, 0xFF42); + + const packet = []; + packet[0] = 0x02; + packet[1] = headsetMode; + packet[2] = 0x01; + packet[3] = 0x0e; + + const timeoutValue = idleTimeoutLength * 60000; + const hexValue = timeoutValue.toString(16).padStart(6, '0'); + const littleEndianHex = hexValue.match(/../g).reverse(); + + packet[5] = parseInt(littleEndianHex[0], 16); + packet[6] = parseInt(littleEndianHex[1], 16); + packet[7] = parseInt(littleEndianHex[2], 16); + console.log ("Idle Timeout: " + idleTimeoutLength + "Min") + device.write(packet, 128); +} + +function applyPreConfig() { + device.set_endpoint(3, 0x0001, 0xFF42); + device.write([0x02, headsetMode, 0x01, 0x0d, 0x00, 0x01], 128); + device.pause(1); +} + +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + const colors = []; + colors[0] = parseInt(result[1], 16); + colors[1] = parseInt(result[2], 16); + colors[2] = parseInt(result[3], 16); + + return colors; +} + +export function Validate(endpoint) { + return endpoint.interface === 3 && (endpoint.usage === 0x0001 || endpoint.usage === 0x0002) && endpoint.usage_page === 0xFF42; +} + +export function ImageUrl() { + return "https://assets.signalrgb.com/devices/brands/corsair/audio/virtuoso-xt.png"; +}