diff --git a/.eslintrc.json b/.eslintrc.json index 84911dce953..784a383b209 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["eslint:recommended", "plugin:jsdoc/recommended"], + "extends": [ "eslint:recommended", "plugin:jsdoc/recommended" ], "parserOptions": { "ecmaVersion": 7, "sourceType": "script" @@ -7,30 +7,49 @@ "env": { "es6": true }, - "plugins": ["jsdoc"], + "plugins": [ "jsdoc" ], + "settings": { + "jsdoc": { + "preferredTypes": { + "object": "Object" + } + } + }, "rules": { - "array-bracket-spacing" : "warn", + "array-bracket-spacing": "warn", "block-spacing": "warn", - "brace-style": ["warn", "1tbs", { + "brace-style": [ + "warn", + "1tbs", + { "allowSingleLine": true - }], + } + ], "curly": "warn", "camelcase": "warn", "comma-spacing": "warn", - "computed-property-spacing" : ["warn", "never", { + "computed-property-spacing": [ + "warn", + "never", + { "enforceForClassMembers": true - }], - "dot-location": ["warn", "property"], + } + ], + "dot-location": [ "warn", "property" ], "dot-notation": "warn", - "eqeqeq": ["error", "always"], + "eqeqeq": [ "error", "always" ], "func-call-spacing": "warn", - "func-style": ["error", "expression", { + "func-style": [ + "error", + "expression", + { "allowArrowFunctions": true - }], - "indent": ["warn", 4], + } + ], + "indent": [ "warn", 4 ], "key-spacing": "warn", "keyword-spacing": "warn", - "linebreak-style": ["warn", "unix"], + "linebreak-style": [ "warn", "unix" ], "newline-per-chained-call": "warn", "no-constructor-return": "warn", "no-extra-bind": "warn", @@ -38,43 +57,62 @@ "no-useless-call": "warn", "no-useless-return": "warn", "no-trailing-spaces": "warn", - "no-unneeded-ternary": ["warn", { + "no-unneeded-ternary": [ + "warn", + { "defaultAssignment": false - }], - "no-unused-vars": ["error", { + } + ], + "no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_" - }], + } + ], "no-var": "warn", - "object-curly-newline" : ["warn", { + "object-curly-newline": [ + "warn", + { "consistent": true, "multiline": true - }], - "object-curly-spacing" : "warn", + } + ], + "object-curly-spacing": "warn", "prefer-const": "warn", "prefer-regex-literals": "warn", - "quotes": ["warn", "double"], + "quotes": [ "warn", "double" ], "require-atomic-updates": "error", "semi": "warn", "semi-spacing": "warn", - "space-before-blocks": ["warn", "always"], - "space-before-function-paren": ["warn", "never"], + "space-before-blocks": [ "warn", "always" ], + "space-before-function-paren": [ "warn", "never" ], "space-in-parens": "warn", "yoda": "warn" }, "globals": { - "console": "readable" + "console": "readonly" }, "overrides": [ { - "files": ["**/*.mjs"], + "files": [ "**/*.mjs" ], "parserOptions": { "sourceType": "module" } }, { - "files": ["res/controllers/*.js"], + "files": [ "res/controllers/common-hid-packet-parser.js" ], + "globals": { + "console": "readonly" + }, + "rules": { + "camelcase": "off" + } + }, + { + "files": [ "res/controllers/*.js" ], + "excludedFiles": "res/controllers/common-hid-packet-parser.js", "globals": { - "console": "readable", + "console": "readonly", "svg": "writable", "HIDController": "writable", "HIDDebug": "writable", diff --git a/res/controllers/common-hid-packet-parser.js b/res/controllers/common-hid-packet-parser.js index 6a47697decc..c62addda783 100644 --- a/res/controllers/common-hid-packet-parser.js +++ b/res/controllers/common-hid-packet-parser.js @@ -1,1549 +1,2304 @@ /* global controller */ -/** Common HID script debugging function. Just to get logging with 'HID' prefix. */ -var HIDDebug = function(message) { - print("HID " + message); +/** + * Common HID script debugging function. Just to get logging with 'HID' prefix. + * + * @deprecated Use console.log instead + * @param {any} message Message to be printed on controller debug console output + */ +this.HIDDebug = function(message) { + console.log(`HID ${message}`); }; -/** HID Bit Vector Class +/** + * Callback function to call when, the packet represents an HID InputReport, and new data for this + * InputReport are received. If a packet callback is defined and the data for the InputReport are + * received, the complete report data are sent to the callback function after field values are + * parsed, without calling any packet field parsing functions. * - * Collection of bits in one parsed packet field. These objects are - * created by HIDPacket addControl and addOutput and should not be - * created manually. */ -var HIDBitVector = function() { - this.size = 0; - this.bits = {}; -}; + * @callback packetCallback + * @param {HIDPacket} packet The packet that represents the InputReport + * @param {Object.} changed_data The data received from the device + */ +/** + * Callback function to call when, the value of a modifier control changed + * + * @callback modifierCallback + * @param {boolean} Value of the modifier control + */ +/** + * Callback function to call when, data for specified filed in the packet is updated. + * + * @callback fieldChangeCallback + * @param {packetField|bitObject} Object that describes a field/bit inside of a packet, which can often + * mapped to a Mixxx control. + */ -/** Return bit offset based on bitmask */ -HIDBitVector.prototype.getOffset = function(bitmask) { - for (var i = 0; i < 32; i++) - if ((1 & bitmask >> i) !== 0) - return i; - return 0; -}; +/** + * Callback function, which will be called every time, the value of the connected control changes. + * + * @callback controlCallback + * @param {number} value New value of the control + * @param {string} group Mixxx control group name e.g. "[Channel1]" + * @param {string} name Mixxx control name "pregain" + * @returns {any} Value + */ -/** Add a control bitmask to the HIDBitVector */ -HIDBitVector.prototype.addBitMask = function(group, name, bitmask) { - var bit = {}; - bit.type = "button"; - bit.packet = undefined; - bit.id = group + "." + name; - bit.group = group; - bit.name = name; - bit.mapped_group = undefined; - bit.mapped_name = undefined; - bit.bitmask = bitmask; - bit.bitmask = bitmask; - bit.bit_offset = this.getOffset(bitmask); - bit.callback = undefined; - bit.value = undefined; - bit.auto_repeat = undefined; - bit.auto_repeat_interval = undefined; - this.bits[bit.id] = bit; -}; +/** + * In almost every case, a HID controller sends data values with input fields which are not directly + * suitable for Mixxx control values. To solve this issue, HIDController contains function to scale + * the input value to suitable range automatically before calling any field processing functions. + * Scalers can be registered with HIDController.setScaler. + * + * The ScallingCallback function can also have a boolean property .useSetParameter, if: + * - 'false' or 'undefined', engine.setValue is used + * - 'true' engine.setParameter is used + * + * @callback scalingCallback + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "pregain" + * @param {number} value Value to be scaled + * @returns {number} Scaled value + */ -/** Add a Output control bitmask to the HIDBitVector */ -HIDBitVector.prototype.addOutputMask = function(group, name, bitmask) { - var bit = {}; - bit.type = "output"; - bit.packet = undefined; - bit.id = group + "." + name; - bit.group = group; - bit.name = name; - bit.mapped_group = undefined; - bit.mapped_name = undefined; - bit.bitmask = bitmask; - bit.bit_offset = this.getOffset(bitmask); - bit.callback = undefined; - bit.value = undefined; - bit.toggle = undefined; - this.bits[bit.id] = bit; -}; +/** + * Callback function to call when, jog wheel scratching got enabled or disabled by + * the button with the special name 'jog_touch' + * + * @callback scratchingCallback + * @param {boolean} isScratchEnabled True, when button 'jog_touch' is active + */ + +/** + * @typedef packetField + * @type {Object} + * @property {HIDPacket} packet + * @property {string} id Group and control name separated by a dot + * @property {string} group + * @property {string} name + * @property {string} mapped_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]" + * @property {string} mapped_name Name of mapped control, must be a valid Mixxx control name "VuMeter" + * @property {controlCallback} mapped_callback + * @property {string} pack Control packing format for unpack(), one of b/B, h/H, i/I + * @property {number} offset Position of the first byte in the packet in bytes (first byte is 0) + * @property {number} end_offset Position of the last byte in the packet in bytes ({@link packetField.offset} + packet size) + * @property {number} bitmask + * @property {boolean} isEncoder + * @property {fieldChangeCallback} callback + * @property {boolean} soft_takeover + * @property {boolean} ignored + * @property {fieldChangeCallback} auto_repeat + * @property {number} auto_repeat_interval + * @property {number} min + * @property {number} max + * @property {('bitvector'|'control'|'output')} type Must be either: + * - 'bitvector' If value is of type HIDBitVector + * - 'control' If value is a number + * - 'output' + * @property {HIDBitVector|boolean|number} value + * @property {number} delta + * @property {number} mindelta + * @property {number} toggle + */ + +/** + * @typedef bitObject + * @type {Object} + * @property {HIDPacket} packet + * @property {string} id Group and control name separated by a dot + * @property {string} group + * @property {string} name + * @property {string} mapped_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]" + * @property {string} mapped_name Name of mapped control, must be a valid Mixxx control name "cue_indicator" + * @property {controlCallback} mapped_callback + * @property {number} bitmask + * @property {number} bit_offset + * @property {fieldChangeCallback} callback + * @property {fieldChangeCallback} auto_repeat + * @property {number} auto_repeat_interval + * @property {('button'|'output')} type Must be either: + * - 'button' + * - 'output' + * @property {number} value + * @property {number} toggle + */ -/** HID Modifiers object +/** + * HID Bit Vector Class * - * Wraps all defined modifiers to one object with uniform API. - * Don't call directly, this is available as HIDController.modifiers */ -var HIDModifierList = function() { - this.modifiers = Object(); - this.callbacks = Object(); -}; + * Collection of bits in one parsed packet field. These objects are + * created by HIDPacket addControl and addOutput and should not be + * created manually. + */ +// @ts-ignore Same identifier for class and instance needed for backward compatibility +class HIDBitVector { + constructor() { + /** + * Number of bitObjects in bits array + * + * @type {number} + */ + this.size = 0; + /** + * Object of bitObjects, referred by a string of group and control name separated by a dot + * + * @type {Object.} + */ + this.bits = {}; + } + /** + * Get the index of the least significant bit that is 1 in `bitmask` + * + * @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are + * considered. + * @returns {number} Index of the least significant bit that is 1 in `bitmask` + */ + getOffset(bitmask) { + bitmask >>>= 0; // ensures coercion to Uint32 + // The previous implementation should have returned 32 for an empty bitmask, instead it + // returned 0 + if (bitmask === 0) { + return 0; + } // skipping this step would make it return -1 + bitmask &= -bitmask; // equivalent to `bitmask = bitmask & (~bitmask + 1)` + return 31 - Math.clz32(bitmask); + } + /** + * Add a control bitmask to the HIDBitVector + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + * @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are + * considered. + */ + addBitMask(group, name, bitmask) { + /** @type {bitObject} */ + const bit = {}; + bit.type = "button"; + bit.packet = undefined; + bit.id = `${group}.${name}`; + bit.group = group; + bit.name = name; + bit.mapped_group = undefined; + bit.mapped_name = undefined; + bit.bitmask = bitmask; + bit.bit_offset = this.getOffset(bitmask); + bit.callback = undefined; + bit.value = undefined; + bit.auto_repeat = undefined; + bit.auto_repeat_interval = undefined; + this.bits[bit.id] = bit; + } + /** + * Add an output control bitmask to the HIDBitVector + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + * @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are + * considered. + */ + addOutputMask(group, name, bitmask) { + /** @type {bitObject} */ + const bit = {}; + bit.type = "output"; + bit.packet = undefined; + bit.id = `${group}.${name}`; + bit.group = group; + bit.name = name; + bit.mapped_group = undefined; + bit.mapped_name = undefined; + bit.bitmask = bitmask; + bit.bit_offset = this.getOffset(bitmask); + bit.callback = undefined; + bit.value = undefined; + bit.toggle = undefined; + this.bits[bit.id] = bit; + } +} +// Add class HIDBitVector to the Global JavaScript object +// @ts-ignore Same identifier for class and instance needed for backward compatibility +this.HIDBitVector = HIDBitVector; -/** Add a new modifier to controller. */ -HIDModifierList.prototype.add = function(name) { - if (name in this.modifiers) { - HIDDebug("Modifier already defined: " + name); - return; - } - this.modifiers[name] = undefined; -}; -/** Set modifier value */ -HIDModifierList.prototype.set = function(name, value) { - if (!(name in this.modifiers)) { - HIDDebug("Unknown modifier: " + name); - return; - } - this.modifiers[name] = value; - if (name in this.callbacks) { - var callback = this.callbacks[name]; - callback(value); +/** + * HID Modifiers object + * + * e.g. a shift button can be defined as modifier for the behavior of other controls. + * + * Wraps all defined modifiers to one object with uniform API. + * Don't call directly, this is available as HIDController.modifiers + */ +// @ts-ignore Same identifier for class and instance needed for backward compatibility +class HIDModifierList { + constructor() { + /** + * Actual value of the modifier + * + * @type {Object.} + */ + this.modifiers = Object(); + + /** + * Function to be called after modifier value changes + * + * @type {Object.} + */ + this.callbacks = Object(); + } + /** + * Add a new modifier to controller. + * + * @param {string} name Name of modifier + */ + add(name) { + if (name in this.modifiers) { + console.warn(`HIDModifierList.add - Modifier already defined: ${name}`); + return; + } + this.modifiers[name] = undefined; + } + /** + * Set modifier value + * + * @param {string} name Name of modifier + * @param {boolean} value Value to be set + */ + set(name, value) { + if (!(name in this.modifiers)) { + console.error(`HIDModifierList.set - Unknown modifier: ${name}`); + return; + } + this.modifiers[name] = value; + if (name in this.callbacks) { + const callback = this.callbacks[name]; + callback(value); + } } -}; - -/** Get modifier value */ -HIDModifierList.prototype.get = function(name) { - if (!(name in this.modifiers)) { - HIDDebug("Unknown modifier: " + name); - return false; + /** + * Get modifier value + * + * @param {string} name Name of modifier + * @returns {boolean} Value of modifier + */ + get(name) { + if (!(name in this.modifiers)) { + console.error(`HIDModifierList.get - Unknown modifier: ${name}`); + return false; + } + return this.modifiers[name]; + } + /** + * Set modifier callback function + * + * @param {string} name Name of reference in HIDModifierList + * @param {modifierCallback} callback Function to be called after modifier value changes + */ + setCallback(name, callback) { + if (!(name in this.modifiers)) { + console.error(`HIDModifierList.setCallback - Unknown modifier: ${name}`); + return; + } + this.callbacks[name] = callback; } - return this.modifiers[name]; -}; +} +// Add class HIDModifierList to the Global JavaScript object +// @ts-ignore Same identifier for class and instance needed for backward compatibility +this.HIDModifierList = HIDModifierList; -/** Set modifier callback (update function after modifier state changes) */ -HIDModifierList.prototype.setCallback = function(name, callback) { - if (!(name in this.modifiers)) { - HIDDebug("Unknown modifier: " + name); - return; - } - this.callbacks[name] = callback; -}; /** * HID Packet object * - * One HID input/output packet to register to HIDController - * @param name name of packet - * @param reportId report ID of the packet. If the device only uses - * one report type, this must be 0. - * @param callback function to call when this packet type is input - * and is received. If packet callback is set, the - * packet is not parsed by delta functions. - * callback is not meaningful for output packets - * @param header (optional) list of bytes to match from beginning - * of packet. Do NOT put the report ID in this; use - * the reportId parameter instead. */ -var HIDPacket = function(name, reportId, callback, header) { - this.name = name; - this.header = header; - this.callback = callback; - - this.reportId = 0; - if (reportId !== undefined) { + * An HIDPacket represents one HID report of type InputReport or OutputReport (FeatureReports are + * currently not supported) + * + * Each HIDPacket must be registered to HIDController. + */ +// @ts-ignore Same identifier for class and instance needed for backward compatibility +class HIDPacket { + /** + * @param {string} name Name of packet (it makes sense to refer the HID report type and HID + * ReportID here e.g. 'InputReport_0x02' or 'OutputReport_0x81') + * @param {number} reportId ReportID of the packet. If the device does not use ReportIDs this + * must be 0. [default = 0] + * @param {packetCallback} callback function to call when the packet type represents an InputReport, + * and a new report is received. If packet callback is set, the packet is not parsed by delta + * functions. Note, that a callback is not meaningful for output packets. + * @param {number[]} header (optional) List of bytes to match from beginning of packet. + * Do NOT put the report ID in this - use the reportId parameter instead. + */ + constructor(name, reportId = 0, callback = undefined, header = []) { + /** + * Name of packet + * + * @type {string} + */ + this.name = name; + + /** + * ReportID of the packet. If the device does not use ReportIDs this must be 0. + * + * @type {number} + */ this.reportId = reportId; - } - - this.groups = {}; - // Size of various 'pack' values in bytes - this.packSizes = {b: 1, B: 1, h: 2, H: 2, i: 4, I: 4}; - this.signedPackFormats = ["b", "h", "i"]; -}; + /** + * Function to call when the packet type represents an InputReport, and a new report is received. + * + * @type {packetCallback} + */ + this.callback = callback; + + /** + * List of bytes to match from beginning of packet + * + * @type {number[]} + */ + this.header = header; + + /** + * Object of groups, referred by the group string + * + * @type {Object.>} + */ + this.groups = {}; -/** Pack a field value to the packet. - * Can only pack bits and byte values, patches welcome. */ -HIDPacket.prototype.pack = function(data, field) { - var value; - if (!(field.pack in this.packSizes)) { - HIDDebug("ERROR parsing packed value: invalid pack format " + field.pack); - return; - } - var bytes = this.packSizes[field.pack]; - var signed = false; - if (this.signedPackFormats.indexOf(field.pack) !== -1) - signed = true; - - if (field.type === "bitvector") { - // TODO - fix multi byte bit vector outputs - if (bytes > 1) { - HIDDebug("ERROR: packing multibyte bit vectors not yet supported"); + /** + * Length of packet in bytes + * + * @type {number} + */ + this.length = this.header.length; + + /** + * Size of the 'pack' types in bytes + * + * @type {Object.} + */ + this.packSizes = {b: 1, B: 1, h: 2, H: 2, i: 4, I: 4}; + this.signedPackFormats = ["b", "h", "i"]; + } + + /** + * Pack a field value to the packet. + * Can only pack bits and byte values, patches welcome. + * + * @todo Implement multi byte bit vector outputs + * @param {Uint8Array} data Data received as InputReport from the device + * @param {packetField} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + */ + pack(data, field) { + if (!(field.pack in this.packSizes)) { + console.error(`HIDPacket.pack - Parsing packed value: invalid pack format ${field.pack}`); return; } - for (var bit_id in field.value.bits) { - var bit = field.value.bits[bit_id]; - data[field.offset] = data[field.offset] | bit.value; + const bytes = this.packSizes[field.pack]; + const signed = this.signedPackFormats.includes(field.pack); + if (field.type === "bitvector") { + const bitVector = /** @type {HIDBitVector} */ (field.value); + if (bytes > 1) { + console.error("HIDPacket.pack - Packing multibyte bit vectors not yet supported"); + return; + } + for (const bit_id in bitVector.bits) { + const bit = bitVector.bits[bit_id]; + data[field.offset] = data[field.offset] | bit.value; + } + return; } - return; - } - value = (field.value !== undefined) ? field.value : 0; + const value = Number((field.value !== undefined) ? field.value : 0); - if (value < field.min || value > field.max) { - HIDDebug("ERROR " + field.id + " packed value out of range: " + value); - return; - } + if (value < field.min || value > field.max) { + console.error(`HIDPacket.pack - ${field.id} packed value out of range: ${value}`); + return; + } - for (var byte_index = 0; byte_index < bytes; byte_index++) { - var index = field.offset + byte_index; - if (signed) { - if (value >= 0) { - data[index] = (value >> (byte_index * 8)) & 255; + for (let byte_index = 0; byte_index < bytes; byte_index++) { + const index = field.offset + byte_index; + if (signed) { + if (value >= 0) { + data[index] = (value >> (byte_index * 8)) & 255; + } else { + data[index] = 255 - ((-(value + 1) >> (byte_index * 8)) & 255); + } } else { - data[index] = 255 - ((-(value + 1) >> (byte_index * 8)) & 255); + data[index] = (value >> (byte_index * 8)) & 255; } - } else { - data[index] = (value >> (byte_index * 8)) & 255; } } + /** + * Parse and return field value matching the 'pack' field from field attributes. + * Valid field packing types are: + * - b signed byte + * - B unsigned byte + * - h signed short + * - H unsigned short + * - i signed integer + * - I unsigned integer + * + * @param {Uint8Array} data Data received as InputReport from the device + * @param {packetField} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + * @returns {number} Value for the field in data, represented according the fields packing type + */ + unpack(data, field) { + + if (!(field.pack in this.packSizes)) { + console.error(`HIDPacket.unpack - Parsing packed value: invalid pack format ${field.pack}`); + return undefined; + } + const bytes = this.packSizes[field.pack]; + const signed = this.signedPackFormats.includes(field.pack); -}; - -/** Parse and return field value matching the 'pack' field from field attributes. - * Valid values are: - * b signed byte - * B unsigned byte - * h signed short - * H unsigned short - * i signed integer - * I unsigned integer */ -HIDPacket.prototype.unpack = function(data, field) { - var value = 0; - - if (!(field.pack in this.packSizes)) { - HIDDebug("ERROR parsing packed value: invalid pack format " + field.pack); - return; - } - var bytes = this.packSizes[field.pack]; - var signed = false; - if (this.signedPackFormats.indexOf(field.pack) !== -1) - signed = true; - - for (var field_byte = 0; field_byte < bytes; field_byte++) { - if (data[field.offset + field_byte] === 255 && field_byte === 4) - value += 0; - else - value += data[field.offset + field_byte] * Math.pow(2, (field_byte * 8)); - } - if (signed) { - var max_value = Math.pow(2, bytes * 8); - var split = max_value / 2 - 1; - if (value > split) - value = value - max_value; - } - return value; -}; - -/** Find HID packet group matching name. - * Create group if create is true */ -HIDPacket.prototype.getGroup = function(name, create) { - if (this.groups === undefined) - this.groups = {}; - if (name in this.groups) + let value = 0; + for (let field_byte = 0; field_byte < bytes; field_byte++) { + if (data[field.offset + field_byte] === 255 && field_byte === 4) { + value += 0; + } else { + value += data[field.offset + field_byte] * Math.pow(2, (field_byte * 8)); + } + } + if (signed) { + const max_value = Math.pow(2, bytes * 8); + const split = max_value / 2 - 1; + if (value > split) { + value = value - max_value; + } + } + return value; + } + /** + * Find HID packet group matching name. + * Create group if create is true + * + * @param {string} name Name of the group + * @param {boolean} [create=false] If true, group will be created + @returns {any} Group Returns group or undefined, when group is not existing and create is set + to false + */ + getGroup(name, create) { + if (this.groups === undefined) { + this.groups = {}; + } + if (name in this.groups) { + return this.groups[name]; + } + if (!create) { + return undefined; + } + this.groups[name] = {}; return this.groups[name]; - if (!create) - return undefined; - this.groups[name] = {}; - return this.groups[name]; -}; - -/** Lookup HID packet field matching given offset and pack type - * Returns undefined if no patching field can be found. */ -HIDPacket.prototype.getFieldByOffset = function(offset, pack) { - if (!(pack in this.packSizes)) { - HIDDebug("Unknown pack string " + pack); - return undefined; } - var end_offset = offset + this.packSizes[pack]; - var group; - var field; - for (var group_name in this.groups) { - group = this.groups[group_name]; - for (var field_id in group) { - field = group[field_id]; - // Same field offset - if (field.offset === offset) - return field; - // 7-8 8-9 - // Offset for smaller packet inside multibyte field - if (field.offset < offset && field.end_offset >= end_offset) - return field; - // Packet offset starts inside field, may overflow - if (field.offset < offset && field.end_offset > offset) - return field; - // Packet start before field, ends or overflows field - if (field.offset > offset && field.offset < end_offset) - return field; + /** + * Lookup HID packet field matching given offset and pack type + * + * @param {number} offset The field's offset from the start of the packet in bytes: + * - For HID devices which don't use ReportIDs, the data bytes starts at + * position 0 + * - For HID devices which use ReportIDs to enumerate the reports, the + * data bytes starts at position 1 + * @param {string} pack Is one of the field packing types: + * - b signed byte (Int8) + * - B unsigned byte (Uint8) + * - h signed short (Int16 Little-Endian) + * - H unsigned short (Uint16 Little-Endian) + * - i signed integer (Int32 Little-Endian) + * - I unsigned integer (Uint32 Little-Endian) + * @returns {packetField} Returns matching field or undefined if no matching field can be found. + */ + getFieldByOffset(offset, pack) { + if (!(pack in this.packSizes)) { + console.error(`HIDPacket.getFieldByOffset - Unknown pack string ${pack}`); + return undefined; + } + const end_offset = offset + this.packSizes[pack]; + + for (const group_name in this.groups) { + const group = this.groups[group_name]; + for (const field_id in group) { + /** @type {packetField} */ + const field = group[field_id]; + // Same field offset + if (field.offset === offset) { + return field; + } + // 7-8 8-9 + // Offset for smaller packet inside multibyte field + if (field.offset < offset && field.end_offset >= end_offset) { + return field; + } + // Packet offset starts inside field, may overflow + if (field.offset < offset && field.end_offset > offset) { + return field; + } + // Packet start before field, ends or overflows field + if (field.offset > offset && field.offset < end_offset) { + return field; + } + } } - } - return undefined; -}; - -/** Return a field by group and name from the packet, - * Returns undefined if field could not be found */ -HIDPacket.prototype.getField = function(group, name) { - var field_id = group + "." + name; - if (!(group in this.groups)) { - HIDDebug("PACKET " + this.name + " group not found " + group); return undefined; } + /** + * Return a field by group and name from the packet, + * Returns undefined if field could not be found + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + * @returns {packetField} Field + */ + getField(group, name) { + const field_id = `${group}.${name}`; + if (!(group in this.groups)) { + console.error(`HIDPacket.getField - Packet ${this.name} group not found ${group}`); + return undefined; + } - var control_group = this.groups[group]; - if (field_id in control_group) - return control_group[field_id]; + const control_group1 = this.groups[group]; + if (field_id in control_group1) { + return control_group1[field_id]; + } - // Lookup for bit fields in bitvector matching field name - for (var group_name in this.groups) { - control_group = this.groups[group_name]; - for (var field_name in control_group) { - var field = control_group[field_name]; - if (field === undefined || field.type !== "bitvector") - continue; - for (var bit_name in field.value.bits) { - var bit = field.value.bits[bit_name]; - if (bit.id === field_id) { - return field; + // Lookup for bit fields in bitvector matching field name + for (const group_name in this.groups) { + const control_group2 = this.groups[group_name]; + for (const field_name in control_group2) { + const field = control_group2[field_name]; + if (field === undefined || field.type !== "bitvector") { + continue; + } + for (const bit_name in field.value.bits) { + const bit = field.value.bits[bit_name]; + if (bit.id === field_id) { + return field; + } } } } - } - // Field not found - return undefined; -}; - -/** Return reference to a bit in a bitvector field */ -HIDPacket.prototype.lookupBit = function(group, name) { - var field = this.getField(group, name); - if (field === undefined) { - HIDDebug("Bitvector match not found: " + group + "." + name); + // Field not found return undefined; } - var bit_id = group + "." + name; - for (var bit_name in field.value.bits) { - var bit = field.value.bits[bit_name]; - if (bit.id === bit_id) - return bit; + /** + * Return reference to a bit in a bitvector field + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + * @returns {bitObject} Reference to a bit in a bitvector field + */ + lookupBit(group, name) { + const field = this.getField(group, name); + if (field === undefined) { + console.error(`HIDPacket.lookupBit - Bitvector match not found: ${group}.${name}`); + return undefined; + } + if (field.type !== "bitvector") { + console.error(`HIDPacket.lookupBit - Control doesn't refer a field of type bitvector: ${group}.${name}`); + return undefined; + } + const bitVector = /** @type {HIDBitVector} */ (field.value); + const bit_id = `${group}.${name}`; + for (const bit_name in bitVector.bits) { + const bit = bitVector.bits[bit_name]; + if (bit.id === bit_id) { + return bit; + } + } + console.error("HIDPacket.lookupBit - BUG: bit not found after successful field lookup"); + return undefined; } - HIDDebug("BUG: bit not found after successful field lookup"); - return undefined; -}; + /** + * Remove a control registered. Normally not needed + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + */ + removeControl(group, name) { + const control_group = this.getGroup(group); + if (!(name in control_group)) { + console.warn(`HIDPacket.removeControl - Field not in control group ${group}: ${name}`); + return; + } + delete control_group[name]; + } + /** + * Register a numeric value to parse from input packet + * + * 'group' and 'name' form the ID of the field, if it matches a valid Mixxx control name, + * the system attempts to attach it directly to the correct field. + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + * @param {number} offset The field's offset from the start of the packet in bytes: + * - For HID devices which don't use ReportIDs, the data bytes starts at + * position 0 + * - For HID devices which use ReportIDs to enumerate the reports, the + * data bytes starts at position 1 + * @param {string} pack Is one of the field packing types: + * - b signed byte (Int8) + * - B unsigned byte (Uint8) + * - h signed short (Int16 Little-Endian) + * - H unsigned short (Uint16 Little-Endian) + * - i signed integer (Int32 Little-Endian) + * - I unsigned integer (Uint32 Little-Endian) + * @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are + * considered. + * Note: For controls that use full bytes (8bit, 16bit, ...), you can set this to + * undefined NOTE: Parsing bitmask with multiple bits is not supported yet. + * @param {boolean} isEncoder indicates if this is an encoder which should be wrapped and delta + * reported + * @param {fieldChangeCallback} callback Callback function for the control + */ + addControl(group, name, offset, pack, bitmask, isEncoder, callback) { + const control_group = this.getGroup(group, true); + if (control_group === undefined) { + console.error(`HIDPacket.addControl - Creating HID packet group ${group}`); + return; + } + if (!(pack in this.packSizes)) { + console.error(`HIDPacket.addControl - Unknown pack value ${pack}`); + return; + } -/** Remove a control registered. Normally not needed */ -HIDPacket.prototype.removeControl = function(group, name) { - var control_group = this.getGroup(group); - if (!(name in control_group)) { - HIDDebug("Field not in control group " + group + ": " + name); - return; - } - delete control_group[name]; -}; + const fieldByOffset = this.getFieldByOffset(offset, pack); + if (fieldByOffset !== undefined) { + if (bitmask === undefined) { + console.error(`HIDPacket.addControl - Registering offset ${offset} pack ${pack}`); + console.error(`HIDPacket.addControl - Trying to overwrite non-bitmask control ${group} ${name}`); + return; + } + if (fieldByOffset.type !== "bitvector") { + console.error(`HIDPacket.addControl - Field is not of type bitvector: ${group}.${name}`); + return; + } else { + const bitVector = /** @type {HIDBitVector} */ (fieldByOffset.value); + bitVector.addBitMask(group, name, bitmask); + if (callback !== undefined) { + if (typeof callback !== "function") { + console.error( + `HIDPacket.addControl - Callback provided for ${group}.${name} is not a function.`); + return; + } + this.setCallback(group, name, callback); + } + return; + } + } -/** Register a numeric value to parse from input packet - * - * @param group control group name - * @param name name of the field - * @param offset field offset inside packet (bytes) - * @param pack control packing format for unpack(), one of b/B, h/H, i/I - * @param bitmask bitmask size, undefined for byte(s) controls - * NOTE: Parsing bitmask with multiple bits is not supported yet. - * @param isEncoder indicates if this is an encoder which should be wrapped and delta reported - * @param callback callback function for the control - */ -HIDPacket.prototype.addControl = function(group, name, offset, pack, bitmask, isEncoder, callback) { - var control_group = this.getGroup(group, true); - var bitvector = undefined; - if (control_group === undefined) { - HIDDebug("ERROR creating HID packet group " + group); - return; - } - if (!(pack in this.packSizes)) { - HIDDebug("Unknown pack value " + pack); - return; - } + /** @type {packetField} */ + const field = {}; + field.packet = undefined; + field.id = `${group}.${name}`; + field.group = group; + field.name = name; + field.mapped_group = undefined; + field.mapped_name = undefined; + field.pack = pack; + field.offset = offset; + field.end_offset = offset + this.packSizes[field.pack]; + field.bitmask = bitmask; + field.isEncoder = isEncoder; + field.callback = undefined; + field.soft_takeover = false; + field.ignored = false; + field.auto_repeat = undefined; + field.auto_repeat_interval = undefined; + + const packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8); + const signed = this.signedPackFormats.includes(field.pack); + if (signed) { + field.min = 0 - (packet_max_value / 2) + 1; + field.max = (packet_max_value / 2) - 1; + } else { + field.min = 0; + field.max = packet_max_value - 1; + } - var field = this.getFieldByOffset(offset, pack); - if (field !== undefined) { - if (bitmask === undefined) { - HIDDebug("ERROR registering offset " + offset + " pack " + pack); - HIDDebug( - "ERROR trying to overwrite non-bitmask control " + group + " " + name - ); - return; + if (bitmask === undefined || bitmask === packet_max_value) { + field.type = "control"; + field.value = undefined; + field.delta = 0; + field.mindelta = 0; + } else { + // bitmask is only defined for fields which are not expected to handle all bits in the + // control field. For fields with bitmasks, you can define same offset and pack multiple + // times with different bitmask values to get for example all 8 bits of a buttons state + // byte to different control fields in addControl input packet command. Masking multiple + // bits should work but has not been as widely tested. + const signed = this.signedPackFormats.includes(field.pack); + if (signed) { + console.error("HIDPacket.addControl - Registering bitvector: signed fields not supported"); + return; + } + // Create a new bitvector field and add the bit to that + // TODO - accept controls with bitmask < packet_max_value + const field_name = `bitvector_${offset}`; + field.type = "bitvector"; + field.name = field_name; + field.id = `${group}.${field_name}`; + const bitvector = new HIDBitVector(); + bitvector.size = field.max; + bitvector.addBitMask(group, name, bitmask); + field.value = bitvector; + field.delta = undefined; + field.soft_takeover = undefined; + field.mindelta = undefined; } - bitvector = field.value; - bitvector.addBitMask(group, name, bitmask); + + // Add the new field to the packet + control_group[field.id] = field; + if (callback !== undefined) { if (typeof callback !== "function") { - HIDDebug("ERROR callback provided for " + group + "." + name + " is not a function."); + console.error( + `HIDPacket.addControl - Callback provided for ${group}.${name} is not a function.`); return; } this.setCallback(group, name, callback); } - return; - } - - field = {}; - field.packet = undefined; - field.id = group + "." + name; - field.group = group; - field.name = name; - field.mapped_group = undefined; - field.mapped_name = undefined; - field.pack = pack; - field.offset = offset; - field.end_offset = offset + this.packSizes[field.pack]; - field.bitmask = bitmask; - field.isEncoder = isEncoder; - field.callback = undefined; - field.soft_takeover = false; - field.ignored = false; - field.auto_repeat = undefined; - field.auto_repeat_interval = undefined; - - var packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8); - if (this.signedPackFormats.indexOf(pack) !== -1) { - field.min = 0 - (packet_max_value / 2) + 1; - field.max = (packet_max_value / 2) - 1; - } else { - field.min = 0; - field.max = packet_max_value - 1; } - - if (bitmask === undefined || bitmask === packet_max_value) { - field.type = "control"; - field.value = undefined; - field.delta = 0; - field.mindelta = 0; - } else { - if (this.signedPackFormats.indexOf(pack) !== -1) { - HIDDebug("ERROR registering bitvector: signed fields not supported"); + /** + * Register a Output control field or Output control bit to output packet + * Output control field: + * Output field with no bitmask, controls Output with multiple values + * Output control bit: + * Output with with bitmask, controls Output with a single bit + * + * It is recommended to define callbacks after packet creation with + * setCallback instead of adding it directly here. But you can do it. + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "VuMeter" + * @param {number} offset The field's offset from the start of the packet in bytes: + * - For HID devices which don't use ReportIDs, the data bytes starts at + * position 0 + * - For HID devices which use ReportIDs to enumerate the reports, the + * data bytes starts at position 1 + * @param {string} pack Is one of the field packing types: + * - b signed byte (Int8) + * - B unsigned byte (Uint8) + * - h signed short (Int16 Little-Endian) + * - H unsigned short (Uint16 Little-Endian) + * - i signed integer (Int32 Little-Endian) + * - I unsigned integer (Uint32 Little-Endian) + * @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are + * considered. + * @param {fieldChangeCallback} [callback=undefined] Callback function for the control + */ + addOutput(group, name, offset, pack, bitmask, callback) { + const control_group = this.getGroup(group, true); + const field_id = `${group}.${name}`; + + if (control_group === undefined) { return; } - // Create a new bitvector field and add the bit to that - // TODO - accept controls with bitmask < packet_max_value - var field_name = "bitvector_" + offset; - field.type = "bitvector"; - field.name = field_name; - field.id = group + "." + field_name; - bitvector = new HIDBitVector(field.max); - bitvector.size = field.max; - bitvector.addBitMask(group, name, bitmask); - field.value = bitvector; - field.delta = undefined; - field.soft_takeover = undefined; - field.mindelta = undefined; - } - - // Add the new field to the packet - control_group[field.id] = field; - - if (callback !== undefined) { - if (typeof callback !== "function") { - HIDDebug("ERROR callback provided for " + group + "." + name + " is not a function."); + if (!(pack in this.packSizes)) { + console.error(`HIDPacket.addOutput - Unknown Output control pack value ${pack}`); return; } - this.setCallback(group, name, callback); - } -}; - -/** Register a Output control field or Output control bit to output packet - * Output control field: - * Output field with no bitmask, controls Output with multiple values - * Output control bit: - * Output with with bitmask, controls Output with a single bit - * - * It is recommended to define callbacks after packet creation with - * setCallback instead of adding it directly here. But you can do it. - * - * @param offset the offset of the byte(s) to use, starting at 1 - * (0 is automatically populated with the reportId) - * @param pack control packing format for pack(), one of b/B, h/H, i/I */ -HIDPacket.prototype.addOutput = function(group, name, offset, pack, bitmask, callback) { - var control_group = this.getGroup(group, true); - var field; - var bitvector = undefined; - var field_id = group + "." + name; - - if (control_group === undefined) { - return; - } - if (!(pack in this.packSizes)) { - HIDDebug("ERROR: unknown Output control pack value " + pack); - return; - } - // Adjust offset by 1 because the reportId was previously considered part of the payload - // but isn't anymore and we can't be bothered to adjust every single script manually - offset -= 1; + // Adjust offset by 1 because the reportId was previously considered part of the payload + // but isn't anymore and we can't be bothered to adjust every single script manually + offset -= 1; - // Check if we are adding a Output bit to existing bitvector - field = this.getFieldByOffset(offset, pack); - if (field !== undefined) { - if (bitmask === undefined) { - HIDDebug( - "ERROR: overwrite non-bitmask control " + group + "." + name - ); + // Check if we are adding a Output bit to existing bitvector + const fieldByOffset = this.getFieldByOffset(offset, pack); + if (fieldByOffset !== undefined) { + if (bitmask === undefined) { + console.error(`HIDPacket.addOutput - Overwrite non-bitmask control ${group}.${name}`); + return; + } + if (fieldByOffset.type !== "bitvector") { + console.error(`HIDPacket.addOutput - Field is not of type bitvector: ${group}.${name}`); + return; + } + const bitVector = /** @type {HIDBitVector} */ (fieldByOffset.value); + bitVector.addOutputMask(group, name, bitmask); + if (this.length < offset) { this.length = offset; } return; } - bitvector = field.value; - bitvector.addOutputMask(group, name, bitmask); - return; - } - field = {}; - field.id = field_id; - field.group = group; - field.name = name; - field.mapped_group = undefined; - field.mapped_name = undefined; - field.pack = pack; - field.offset = offset; - field.end_offset = offset + this.packSizes[field.pack]; - field.bitmask = bitmask; - field.callback = callback; - field.toggle = undefined; - - var packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8); - if (this.signedPackFormats.indexOf(pack) !== -1) { - field.min = 0 - (packet_max_value / 2) + 1; - field.max = (packet_max_value / 2) - 1; - } else { - field.min = 0; - field.max = packet_max_value - 1; - } - if (bitmask === undefined || bitmask === packet_max_value) { - field.type = "output"; - field.value = undefined; - field.delta = undefined; - field.mindelta = undefined; - } else { - // Create new Output bitvector control field, add bit to it - // rewrite name to use bitvector instead - var field_name = "bitvector_" + offset; - field.type = "bitvector"; - field.id = group + "." + field_name; - field.name = field_name; - bitvector = new HIDBitVector(); - bitvector.size = field.max; - bitvector.addOutputMask(group, name, bitmask); - field.value = bitvector; - field.delta = undefined; - field.mindelta = undefined; - } + /** @type {packetField} */ + const field = {}; + field.id = field_id; + field.group = group; + field.name = name; + field.mapped_group = undefined; + field.mapped_name = undefined; + field.pack = pack; + field.offset = offset; + field.end_offset = offset + this.packSizes[field.pack]; + field.bitmask = bitmask; + field.callback = callback; + field.toggle = undefined; - // Add Output to HID packet - control_group[field.id] = field; -}; + const packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8); + const signed = this.signedPackFormats.includes(field.pack); + if (signed) { + field.min = 0 - (packet_max_value / 2) + 1; + field.max = (packet_max_value / 2) - 1; + } else { + field.min = 0; + field.max = packet_max_value - 1; + } + if (bitmask === undefined || bitmask === packet_max_value) { + field.type = "output"; + field.value = undefined; + field.delta = undefined; + field.mindelta = undefined; + } else { + // Create new Output bitvector control field, add bit to it + // rewrite name to use bitvector instead + const field_name = `bitvector_${offset}`; + field.type = "bitvector"; + field.id = `${group}.${field_name}`; + field.name = field_name; + const bitvector = new HIDBitVector(); + bitvector.size = field.max; + bitvector.addOutputMask(group, name, bitmask); + field.value = bitvector; + field.delta = undefined; + field.mindelta = undefined; + } -/** Register a callback to field or a bit vector bit. - * Does not make sense for Output fields but you can do that. */ -HIDPacket.prototype.setCallback = function(group, name, callback) { - var field = this.getField(group, name); - var field_id = group + "." + name; - if (callback === undefined) { - HIDDebug("Callback to add was undefined for " + field_id); - return; - } - if (field === undefined) { - HIDDebug("setCallback: field for " + field_id + " not found" - ); - return; - } - if (field.type === "bitvector") { - for (var bit_id in field.value.bits) { - var bit = field.value.bits[bit_id]; - if (bit_id !== field_id) - continue; - bit.callback = callback; + // Add Output to HID packet + if (this.length < field.end_offset) { this.length = field.end_offset; } + control_group[field.id] = field; + } + /** + * Register a callback to field or a bit vector bit. + * Does not make sense for Output fields but you can do that. + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name e.g. "play" + * @param {fieldChangeCallback} callback Callback function for the control + */ + setCallback(group, name, callback) { + const field = this.getField(group, name); + const field_id = `${group}.${name}`; + if (callback === undefined) { + console.error(`HIDPacket.setCallback - Callback to add was undefined for ${field_id}`); return; } - HIDDebug("ERROR: BIT NOT FOUND " + field_id); - } else { - field.callback = callback; - } -}; - -/** Set 'ignored' flag for field to given value (true or false) - * If field is ignored, it is not reported in 'delta' objects. */ -HIDPacket.prototype.setIgnored = function(group, name, ignored) { - var field = this.getField(group, name); - if (field === undefined) { - HIDDebug("ERROR setting ignored flag for " + group + " " + name); - return; - } - field.ignored = ignored; -}; - -/** Adjust field's minimum delta value. - * Input value changes smaller than this are not reported in delta */ -HIDPacket.prototype.setMinDelta = function(group, name, mindelta) { - var field = this.getField(group, name); - if (field === undefined) { - HIDDebug("ERROR adjusting mindelta for " + group + " " + name); - return; - } - if (field.type === "bitvector") { - HIDDebug("ERROR setting mindelta for bitvector packet does not make sense"); - return; - } - field.mindelta = mindelta; -}; - -/** Parse bitvector field values, returning object with the named bits set. - * Value must be a valid unsigned byte to parse, with enough bits. - * Returns list of modified bits (delta) */ -HIDPacket.prototype.parseBitVector = function(field, value) { - var bits = {}; - var bit; - var new_value; - for (var bit_id in field.value.bits) { - bit = field.value.bits[bit_id]; - new_value = (bit.bitmask & value) >> bit.bit_offset; - if (bit.value !== undefined && bit.value !== new_value) - bits[bit_id] = bit; - bit.value = new_value; - } - return bits; -}; - -/** Parse input packet fields from data. - * Data is expected to be a Packet() received from HID device. - * Returns list of changed fields with new value. - * BitVectors are returned as bits you can iterate separately. */ -HIDPacket.prototype.parse = function(data) { - var field_changes = {}; - var group; - var group_name; - var field; - var field_id; - - for (group_name in this.groups) { - group = this.groups[group_name]; - for (field_id in group) { - field = group[field_id]; - if (field === undefined) - continue; - - var value = this.unpack(data, field); - if (value === undefined) { - HIDDebug("Error parsing packet field value for " + field_id); + if (field === undefined) { + console.error(`HIDPacket.setCallback - Field for ${field_id} not found`); + return; + } + if (field.type === "bitvector") { + const bitVector = /** @type {HIDBitVector} */ (field.value); + for (const bit_id in bitVector.bits) { + const bit = bitVector.bits[bit_id]; + if (bit_id !== field_id) { + continue; + } + bit.callback = callback; return; } - - if (field.type === "bitvector") { - // Bitvector deltas are checked in parseBitVector - var changed_bits = this.parseBitVector(field, value); - for (var bit_name in changed_bits) - field_changes[bit_name] = changed_bits[bit_name]; - - } else if (field.type === "control") { - if (field.value === value && field.mindelta !== undefined) - continue; - if (field.ignored || field.value === undefined) { - field.value = value; + console.error(`HIDPacket.setCallback - Bit not found ${field_id}`); + } else { + field.callback = callback; + } + } + /** + * This function can be set in script code to ignore a field you don't want to be processed but + * still wanted to define, to make packet format complete from specifications. If field is + * ignored, it is not reported in 'delta' objects. + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "pregain" + * @param {boolean} ignored 'ignored' flag for field to given value (true or false) + */ + setIgnored(group, name, ignored) { + const field = this.getField(group, name); + if (field === undefined) { + console.error(`HIDPacket.setIgnored - Setting ignored flag for ${group} ${name}`); + return; + } + field.ignored = ignored; + } + /** + * Adjust field's minimum delta value. + * Input value changes smaller than this are not reported in delta + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "pregain" + * @param {number} mindelta Minimum delta value. + */ + setMinDelta(group, name, mindelta) { + const field = this.getField(group, name); + if (field === undefined) { + console.error(`HIDPacket.setMinDelta - Adjusting mindelta for ${group} ${name}`); + return; + } + if (field.type === "bitvector") { + console.error("HIDPacket.setMinDelta - Setting mindelta for bitvector packet does not make sense"); + return; + } + field.mindelta = mindelta; + } + /** + * Parse bitvector field values, returning object with the named bits set. + * + * @param {packetField} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + * @param {number} value Value must be a valid unsigned byte to parse, with enough bits. + * @returns {Object.} List of modified bits (delta), + * referred by a string of group and control name separated by a dot + */ + parseBitVector(field, value) { + /** + * Object of bitObjects, referred by a string of group and control name separated by a dot + * + * @type {Object.} + */ + const bits = {}; + + if (field.type !== "bitvector") { + console.error("HIDPacket.parseBitVector - Field isn't of type bitvector"); + return undefined; + } + const bitVector = /** @type {HIDBitVector} */ (field.value); + for (const bit_id in bitVector.bits) { + const bit = bitVector.bits[bit_id]; + const new_value = (bit.bitmask & value) >> bit.bit_offset; + if (bit.value !== undefined && bit.value !== new_value) { + bits[bit_id] = bit; + } + bit.value = new_value; + } + return bits; + } + /** + * Parse input packet fields from data. + * Data is expected to be a Packet() received from HID device. + * BitVectors are returned as bits you can iterate separately. + * + * @param {Uint8Array} data Data received as InputReport from the device + * @returns {Object.} List of changed fields with new value. + */ + parse(data) { + /** + * Object of packetField or bitObjects, referred by a string of group and control name separated by a dot + * + * @type {Object.} + */ + const field_changes = {}; + + for (const group_name in this.groups) { + const group = this.groups[group_name]; + for (const field_id in group) { + const field = group[field_id]; + if (field === undefined) { continue; } - var change; - if (field.isEncoder) { - if (field.value === field.max && value === field.min) { - change = 1; - field.delta = 1; - } else if (value === field.max && field.value === field.min) { - change = 1; - field.delta = -1; + + const value = this.unpack(data, field); + if (value === undefined) { + console.error(`HIDPacket.parse - Parsing packet field value for ${field_id}`); + return; + } + + if (field.type === "bitvector") { + // Bitvector deltas are checked in parseBitVector + const changed_bits = this.parseBitVector(field, value); + for (const bit_name in changed_bits) { + field_changes[bit_name] = changed_bits[bit_name]; + } + + } else if (field.type === "control") { + if (field.value === value && field.mindelta !== undefined) { + continue; + } + if (field.ignored || field.value === undefined) { + field.value = value; + continue; + } + let change = 0; + if (field.isEncoder) { + if (field.value === field.max && value === field.min) { + change = 1; + field.delta = 1; + } else if (value === field.max && field.value === field.min) { + change = 1; + field.delta = -1; + } else { + change = 1; + field.delta = value - field.value; + } + field.value = value; } else { - change = 1; + change = Math.abs(value - field.value); field.delta = value - field.value; } - field.value = value; - } else { - change = Math.abs(value - field.value); - field.delta = value - field.value; - } - if (field.mindelta === undefined || change > field.mindelta) { - field_changes[field.id] = field; - field.value = value; + if (field.mindelta === undefined || change > field.mindelta) { + field_changes[field.id] = field; + field.value = value; + } } } } - } - return field_changes; -}; + return field_changes; + } + /** + * Send this HID packet to device. + * First the header bytes are copied to beginning of packet, then + * field object values are packed to the HID packet according to the + * field type. + * + * @param {boolean} [debug=false] Enables debug output to console + */ + send(debug) { + const data = new Uint8Array(this.length); + + if (this.header !== undefined) { + for (let header_byte = 0; header_byte < this.header.length; header_byte++) { + data[header_byte] = this.header[header_byte]; + } + } -/** Send this HID packet to device. - * First the header bytes are copied to beginning of packet, then - * field object values are packed to the HID packet according to the - * field type. */ -HIDPacket.prototype.send = function(debug) { - var data = []; - - if (this.header !== undefined) { - for (var header_byte = 0; header_byte < this.header.length; header_byte++) { - data[header_byte] = this.header[header_byte]; - } - } - - for (var group_name in this.groups) { - var group = this.groups[group_name]; - for (var field_name in group) { - this.pack(data, group[field_name]); + for (const group_name in this.groups) { + const group = this.groups[group_name]; + for (const field_name in group) { + this.pack(data, group[field_name]); + } } - } - if (debug) { - var packet_string = ""; - for (var d in data) { - if (data[d] < 0x10) { - // Add padding for bytes smaller than 10 - packet_string += "0"; + if (debug) { + let packet_string = ""; + for (const d in data) { + if (data[d] < 0x10) { + // Add padding for bytes smaller than 10 + packet_string += "0"; + } + packet_string += data[d].toString(16) + " "; } - packet_string += data[d].toString(16) + " "; + console.log(`Sending packet with Report ID ${this.reportId}: ${packet_string}`); } - HIDDebug("Sending packet with Report ID " + this.reportId + ": " + packet_string); + controller.sendOutputReport(this.reportId, data.buffer); } - controller.send(data, data.length, this.reportId); -}; +} +// Add class HIDPacket to the Global JavaScript object +// @ts-ignore Same identifier for class and instance needed for backward compatibility +this.HIDPacket = HIDPacket; + /** - * HID Controller Class - * - * HID Controller with packet parser - * Global attributes include: - * - * initialized by default false, you should set this to true when - * controller is found and everything is OK - * activeDeck by default undefined, used to map the virtual deck - * names 'deck','deck1' and 'deck2' to actual [ChannelX] - * isScratchEnabled set to true, when button 'jog_touch' is active - * buttonStates valid state values for buttons, should contain fields - * released (default 0) and pressed (default 1) - * LEDColors possible Output colors named, must contain 'off' value - * deckOutputColors Which colors to use for each deck. Default 'on' for first - * four decks. Values are like {1: 'red', 2: 'green' } - * and must reference valid OutputColors fields. - * OutputUpdateInterval By default undefined. If set, it's a value for timer - * executed every n ms to update Outputs with updateOutputs() - * modifiers Reference to HIDModifierList object - * toggleButtons List of button names you wish to act as 'toggle', i.e. - * pressing the button and releasing toggles state of the - * control and does not set it off again when released. - * - * Scratch variables (initialized with 'common' defaults, you can override): - * scratchintervalsPerRev Intervals value for scratch_enable - * scratchRPM RPM value for scratch_enable - * scratchAlpha Alpha value for scratch_enable - * scratchBeta Beta value for scratch_enable - * scratchRampOnEnable If 'ramp' is used when enabling scratch - * scratchRampOnDisable If 'ramp' is used when disabling scratch + * HID Controller Class with packet parser */ -var HIDController = function() { - this.initialized = false; - this.activeDeck = undefined; - - this.InputPackets = {}; - this.OutputPackets = {}; - // Default input control packet name: can be modified for controllers - // which can swap modes (wiimote for example) - this.defaultPacket = "control"; - - // Callback functions called by deck switching. Undefined by default - this.disconnectDeck = undefined; - this.connectDeck = undefined; - - // Scratch parameter defaults for this.scratchEnable function - // override for custom control - this.isScratchEnabled = false; - this.scratchintervalsPerRev = 128; - this.scratchRPM = 33 + 1 / 3; - this.scratchAlpha = 1.0 / 8; - this.scratchBeta = this.scratchAlpha / 32; - this.scratchRampOnEnable = false; - this.scratchRampOnDisable = false; - - // Button states available - this.buttonStates = {released: 0, pressed: 1}; - // Output color values to send - this.LEDColors = {off: 0x0, on: 0x7f}; - // Toggle buttons - this.toggleButtons = ["play", "pfl", "keylock", "quantize", "reverse", "slip_enabled", - "group_[Channel1]_enable", "group_[Channel2]_enable", - "group_[Channel3]_enable", "group_[Channel4]_enable"]; - - // Override to set specific colors for multicolor button Output per deck - this.deckOutputColors = {1: "on", 2: "on", 3: "on", 4: "on"}; - // Mapping of automatic deck switching with deckSwitch function - this.virtualDecks = ["deck", "deck1", "deck2", "deck3", "deck4"]; - this.deckSwitchMap = {1: 2, 2: 1, 3: 4, 4: 3, undefined: 1}; - - // Standard target groups available in mixxx. This is used by - // HID packet parser to recognize group parameters we should - // try sending to mixxx. - this.valid_groups = [ - "[Channel1]", "[Channel2]", "[Channel3]", "[Channel4]", - "[Sampler1]", "[Sampler2]", "[Sampler3]", "[Sampler4]", - "[Sampler5]", "[Sampler6]", "[Sampler7]", "[Sampler8]", - "[Master]", "[PreviewDeck1]", "[Effects]", "[Playlist]", "[Flanger]", - "[Microphone]", "[EffectRack1_EffectUnit1]", "[EffectRack1_EffectUnit2]", - "[EffectRack1_EffectUnit3]", "[EffectRack1_EffectUnit4]", - "[InternalClock]"]; - - // Set to value in ms to update Outputs periodically - this.OutputUpdateInterval = undefined; - - this.modifiers = new HIDModifierList(); - this.scalers = {}; - this.timers = {}; - - this.auto_repeat_interval = 100; -}; - -/** Function to close the controller object cleanly */ -HIDController.prototype.close = function() { - for (var name in this.timers) { - var timer = this.timers[name]; - if (timer === undefined) - continue; - engine.stopTimer(timer); - this.timers[name] = undefined; - } -}; - -/** Initialize our packet data and callbacks. This does not seem to - * work when executed from here, but we keep a stub just in case. */ -HIDController.prototype.initializePacketData = function() { -}; - -/** Return deck number from deck name. Deck name can't be virtual deck name - * in this function call. */ -HIDController.prototype.resolveDeck = function(group) { - if (group === undefined) - return undefined; - var result = group.match(/\[Channel[0-9]+\]/); - if (!result) - return undefined; - var str = group.replace(/\[Channel/, ""); - return str.substring(0, str.length - 1); -}; - -/** Return the group name from given deck number. */ -HIDController.prototype.resolveDeckGroup = function(deck) { - if (deck === undefined) - return undefined; - return "[Channel" + deck + "]"; -}; +// @ts-ignore Same identifier for class and instance needed for backward compatibility +class HIDController { + constructor() { + + /** + * - By default 'false' + * - Should be set 'true', when controller is found and everything is OK + * + * @type {boolean} + */ + this.initialized = false; + + /** + * @type {number} + */ + this.activeDeck = undefined; + + /** + * HIDPackets representing HID InputReports, by packet name + * + * @type {Object.} + */ + this.InputPackets = {}; + + /** + * HIDPackets representing HID OutputReports, by packet name + * + * @type {Object.} + */ + this.OutputPackets = {}; + + /** + * Default input packet name: can be modified for controllers + * which can swap modes (wiimote for example) + * + * @type {string} + */ + this.defaultPacket = "control"; + + // Callback functions called by deck switching. Undefined by default + this.disconnectDeck = undefined; + this.connectDeck = undefined; + + // Scratch parameter defaults for this.scratchEnable function + // override for custom control + /** + * Set to true, when button 'jog_touch' is active + * + * @type {boolean} + */ + this.isScratchEnabled = false; -/** Map virtual deck names to real deck group. If group is already - * a real mixxx group value, just return it as it without mapping. */ -HIDController.prototype.resolveGroup = function(group) { - var channel_name = /\[Channel[0-9]+\]/; - if (group !== undefined && group.match(channel_name)) - return group; - if (this.valid_groups.indexOf(group) !== -1) { - return group; + /** + * The resolution of the jogwheel HID control (in intervals per revolution) + * - Default is 128 + * + * @type {number} + */ + this.scratchintervalsPerRev = 128; + + /** + * The speed of the imaginary record at 0% pitch - in revolutions per minute (RPM) + * - Default 33+1/3 - adjust for comfort + * + * @type {number} + */ + this.scratchRPM = 33 + 1 / 3; + + /** + * The alpha coefficient of the filter + * - Default is 1/8 (0.125) - start tune from there + * + * @type {number} + */ + this.scratchAlpha = 1.0 / 8; + + /** + * The beta coefficient of the filter + * - Default is scratchAlpha/32 - start tune from there + * + * @type {number} + */ + this.scratchBeta = this.scratchAlpha / 32; + + /** + * - Set 'true' to ramp the deck speed down. + * - Set 'false' to stop instantly (default) + * + * @type {boolean} + */ + this.scratchRampOnEnable = false; + + /** + * - Set 'true' to ramp the deck speed up. + * - Set 'false' to jump to normal play speed instantly (default) + * + * @type {boolean} + */ + this.scratchRampOnDisable = false; + + /** + * Callback function to call when, jog wheel scratching got enabled or disabled + * + * @type {scratchingCallback} + */ + this.enableScratchCallback = undefined; + + /** + * List of valid state values for buttons, should contain fields: + * - 'released' (default 0) + * - 'pressed' (default 1) + */ + this.buttonStates = {released: 0, pressed: 1}; + + /** + * List of named output colors to send + * - must contain 'off' value + */ + this.LEDColors = {off: 0x0, on: 0x7f}; + + /** + * List of button names you wish to act as 'toggle' + * + * i.e. pressing the button and releasing toggles state of the control and doesn't set it off again when released. + */ + this.toggleButtons = [ + "play", "pfl", "keylock", "quantize", "reverse", "slip_enabled", + "group_[Channel1]_enable", "group_[Channel2]_enable", + "group_[Channel3]_enable", "group_[Channel4]_enable" + ]; + + /** + * List of colors to use for each deck + * - Default is 'on' for first four decks. + * + * Override to set specific colors for multicolor button output per deck: + * - Values are like {1: 'red', 2: 'green' } and must reference valid OutputColors fields. + */ + this.deckOutputColors = {1: "on", 2: "on", 3: "on", 4: "on"}; + + // + /** + * Used to map the virtual deck names 'deck', 'deck1' or 'deck2' to actual [ChannelX] + * + * @type {string[]} + */ + this.virtualDecks = ["deck", "deck1", "deck2", "deck3", "deck4"]; + + /** + * Mapping of automatic deck switching with switchDeck function + */ + this.deckSwitchMap = {1: 2, 2: 1, 3: 4, 4: 3, undefined: 1}; + + /** + * Standard target groups available in mixxx. + * + * This is used by HID packet parser to recognize group parameters we should try sending to mixxx. + * + * @type {string[]} + */ + this.valid_groups = [ + "[Channel1]", + "[Channel2]", + "[Channel3]", + "[Channel4]", + "[Sampler1]", + "[Sampler2]", + "[Sampler3]", + "[Sampler4]", + "[Sampler5]", + "[Sampler6]", + "[Sampler7]", + "[Sampler8]", + "[Master]", + "[PreviewDeck1]", + "[Effects]", + "[Playlist]", + "[Flanger]", + "[Microphone]", + "[EffectRack1_EffectUnit1]", + "[EffectRack1_EffectUnit2]", + "[EffectRack1_EffectUnit3]", + "[EffectRack1_EffectUnit4]", + "[InternalClock]" + ]; + + // + /** + * Set to value in ms to update Outputs periodically + * - By default undefined. + * - If set, it's a value for timer executed every n ms to update Outputs with updateOutputs() + * + * @deprecated This is unused and updateOutputs() doesn't exist - Remove? + * @type {number} + */ + this.OutputUpdateInterval = undefined; + + /** + * Reference to HIDModifierList object + * + * @type {HIDModifierList} + */ + this.modifiers = new HIDModifierList(); + + /** + * Object of scaling function callbacks by name + * + * @type {Object.} + */ + this.scalers = {}; + + /** + * Object of engine timer IDs of running auto repeat timers + * Key is a user specified timer_id. + * Used only in the controller.startAutoRepeatTimer code stubs of Sony-SixxAxis.js and Nintendo-Wiimote.js. + * + * @type {Object.} + */ + this.timers = {}; + + /** + * Auto repeat interval default for fields, where not specified individual (in milliseconds) + * + * @default 100 + * @type {number} + */ + this.auto_repeat_interval = 100; + + /** + * @deprecated Use {@link postProcessDelta} instead + * (not used in any official mapping) + * @type {packetCallback} + */ + this.processDelta = undefined; + + /** + * Callback that is executed after parsing incoming packet + * (see Traktor-Kontrol-F1-scripts.js for an example) + * + * @type {packetCallback} + */ + this.postProcessDelta = undefined; + + } + /** Function to close the controller object cleanly */ + close() { + for (const name in this.timers) { + const timer = this.timers[name]; + if (timer === undefined) { + continue; + } + engine.stopTimer(timer); + this.timers[name] = undefined; + } } - if (group === "deck" || group === undefined) { - if (this.activeDeck === undefined) + /** + * Return deck number from deck name. Deck name can't be virtual deck name + * in this function call. + * + * @param {string} group Control group name e.g. "[Channel1]" + * @returns {number} Number of deck + */ + resolveDeck(group) { + if (group === undefined) { return undefined; - return "[Channel" + this.activeDeck + "]"; - } - if (this.activeDeck === 1 || this.activeDeck === 2) { - if (group === "deck1") return "[Channel1]"; - if (group === "deck2") return "[Channel2]"; - } - if (this.activeDeck === 3 || this.activeDeck === 4) { - if (group === "deck1") return "[Channel3]"; - if (group === "deck2") return "[Channel4]"; + } + const result = group.match(/\[Channel[0-9]+\]/); + if (!result) { + return undefined; + } + const str = group.replace(/\[Channel/, ""); + return Number(str.substring(0, str.length - 1)); + } + /** + * Return the group name from given deck number. + * + * @param {number} deck Number of deck + * @returns {string} Group name of the deck (e.g. Channel2 for deck number 2) + */ + resolveDeckGroup(deck) { + if (deck === undefined) { + return undefined; + } + return `[Channel${deck}]`; + } + /** + * Map virtual deck names to real deck group. If group is already + * a real mixxx group value, just return it as it without mapping. + * + * @param {string} group Control group name e.g. "[Channel1]" + * @returns {string} Channel + */ + resolveGroup(group) { + const channel_name = /\[Channel[0-9]+\]/; + if (group !== undefined && group.match(channel_name)) { + return group; + } + if (this.valid_groups.indexOf(group) !== -1) { + return group; + } + if (group === "deck" || group === undefined) { + if (this.activeDeck === undefined) { + return undefined; + } + return `[Channel${this.activeDeck}]`; + } + if (this.activeDeck === 1 || this.activeDeck === 2) { + if (group === "deck1") { return "[Channel1]"; } + if (group === "deck2") { return "[Channel2]"; } + } + if (this.activeDeck === 3 || this.activeDeck === 4) { + if (group === "deck1") { return "[Channel3]"; } + if (group === "deck2") { return "[Channel4]"; } + } + return undefined; } - return undefined; -}; - -/** Find Output control matching give group and name - * Returns undefined if output field can't be found. */ -HIDController.prototype.getOutputField = function(m_group, m_name) { - for (var packet_name in this.OutputPackets) { - var packet = this.OutputPackets[packet_name]; - for (var group_name in packet.groups) { - var group = packet.groups[group_name]; - for (var field_name in group) { - var field = group[field_name]; - if (field.type === "bitvector") { - for (var bit_id in field.value.bits) { - var bit = field.value.bits[bit_id]; - if (bit.mapped_group === m_group && bit.mapped_name === m_name) - return bit; - if (bit.group === m_group && bit.name === m_name) - return bit; + /** + * Find Output control matching give group and name + * + * @todo The current implementation of this often called function is very slow and does not + * scale, due to several nested loops. + * @param {string} m_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]" + * @param {string} m_name Name of mapped control, must be a valid Mixxx control name "VuMeter" + * @returns {bitObject|packetField} Bit or bytewise field - Returns undefined if output field + * can't be found. + */ + getOutputField(m_group, m_name) { + for (const packet_name in this.OutputPackets) { + const packet = this.OutputPackets[packet_name]; + for (const group_name in packet.groups) { + const group = packet.groups[group_name]; + for (const field_name in group) { + const field = group[field_name]; + if (field.type === "bitvector") { + for (const bit_id in field.value.bits) { + const bit = field.value.bits[bit_id]; + if (bit.mapped_group === m_group && bit.mapped_name === m_name) { + return bit; + } + if (bit.group === m_group && bit.name === m_name) { + return bit; + } + } + continue; + } + if (field.mapped_group === m_group && field.mapped_name === m_name) { + return field; + } + if (field.group === m_group && field.name === m_name) { + return field; } - continue; } - if (field.mapped_group === m_group && field.mapped_name === m_name) - return field; - if (field.group === m_group && field.name === m_name) - return field; } } - } - return undefined; -}; - -/** Find input packet matching given name. - * Returns undefined if input packet name is not registered. */ -HIDController.prototype.getInputPacket = function(name) { - if (!(name in this.InputPackets)) - return undefined; - return this.InputPackets[name]; -}; - -/** Find output packet matching given name - * Returns undefined if output packet name is not registered. */ -HIDController.prototype.getOutputPacket = function(name) { - if (!(name in this.OutputPackets)) return undefined; - return this.OutputPackets[name]; -}; - -/** Set input packet callback afterwards */ -HIDController.prototype.setPacketCallback = function(packet, callback) { - var input_packet = this.getInputPacket(packet); - input_packet.callback = callback; -}; - -/** Register packet field's callback. - * If packet has callback, it is still parsed but no field processing is done, - * callback is called directly after unpacking fields from packet. */ -HIDController.prototype.setCallback = function(packet, group, name, callback) { - var input_packet = this.getInputPacket(packet); - if (input_packet === undefined) { - HIDDebug("Input packet not found " + packet); - return; - } - input_packet.setCallback(group, name, callback); -}; - -/** Register scaling function for a control name - * This does not check if given control name is valid */ -HIDController.prototype.setScaler = function(name, callback) { - if (name in this.scalers) - return; - this.scalers[name] = callback; -}; - -/** Lookup scaling function for control - * Returns undefined if function is not registered. */ -HIDController.prototype.getScaler = function(name, _callback) { - if (!(name in this.scalers)) - return undefined; - return this.scalers[name]; -}; - -/** Change type of a previously defined field to modifier and register it */ -HIDController.prototype.linkModifier = function(group, name, modifier) { - var packet = this.getInputPacket(this.defaultPacket); - if (packet === undefined) { - HIDDebug( - "ERROR creating modifier: input packet " + this.defaultPacket + " not found" - ); - return; - } - var bit_id = group + "." + name; - var field = packet.lookupBit(group, name); - if (field === undefined) { - HIDDebug("BIT field not found: " + bit_id); - return; - } - field.group = "modifiers"; - field.name = modifier; - this.modifiers.set(modifier); -}; - -/** TODO - implement unlinking of modifiers */ -HIDController.prototype.unlinkModifier = function(_group, _name, _modifier) { - HIDDebug("Unlinking of modifiers not yet implemented"); -}; - -/** Link a previously declared HID control to actual mixxx control */ -HIDController.prototype.linkControl = function(group, name, m_group, m_name, callback) { - var field; - var packet = this.getInputPacket(this.defaultPacket); - if (packet === undefined) { - HIDDebug("ERROR creating modifier: input packet " + this.defaultPacket + " not found"); - return; } - field = packet.getField(group, name); - if (field === undefined) { - HIDDebug("Field not found: " + group + "." + name); - return; - } - if (field.type === "bitvector") { - field = packet.lookupBit(group, name); + /** + * Find input packet matching given name. + * Returns undefined if input packet name is not registered. + * + * @param {string} name Name of packet (it makes sense to refer the HID report type and HID + * Report-ID here e.g. 'InputReport_0x02') + * @returns {HIDPacket} The input packet + */ + getInputPacket(name) { + if (!(name in this.InputPackets)) { + return undefined; + } + return this.InputPackets[name]; + } + /** + * Find output packet matching given name + * Returns undefined if output packet name is not registered. + * + * @param {string} name Name of packet (it makes sense to refer the HID report type and HID + * Report-ID here e.g. 'OutputReport_0x81') + * @returns {HIDPacket} The output packet + */ + getOutputPacket(name) { + if (!(name in this.OutputPackets)) { + return undefined; + } + return this.OutputPackets[name]; + } + /** + * Set input packet callback afterwards + * + * @param {string} packet The name of the input packet e.g. 'InputReport_0x02' + * @param {packetCallback} callback Callback function for the control + */ + setPacketCallback(packet, callback) { + const input_packet = this.getInputPacket(packet); + input_packet.callback = callback; + } + /** + * Register packet field's callback. + * If packet has callback, it is still parsed but no field processing is done, + * callback is called directly after unpacking fields from packet. + * + * @param {string} packet The name of the input packet e.g. 'InputReport_0x02' + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "pregain" + * @param {fieldChangeCallback} callback Callback function for the control + */ + setCallback(packet, group, name, callback) { + const input_packet = this.getInputPacket(packet); + if (input_packet === undefined) { + console.error(`HIDController.setCallback - Input packet not found ${packet}`); + return; + } + input_packet.setCallback(group, name, callback); + } + /** + * Register scaling function for a control name + * This does not check if given control name is valid + * + * @param {string} name Reference of the scaling function in scalers list of HIDController + * @param {scalingCallback} callback Scaling function + */ + setScaler(name, callback) { + if (name in this.scalers) { + return; + } + this.scalers[name] = callback; + } + /** + * Lookup scaling function for control + * + * @param {string} name Reference of the scaling function in scalers list of HIDController + * @param {any} _callback Unused + * @returns {scalingCallback} Scaling function. Returns undefined if function is not + * registered. + */ + getScaler(name, _callback = undefined) { + if (!(name in this.scalers)) { + return undefined; + } + return this.scalers[name]; + } + /** + * Change type of a previously defined field to modifier and register it + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name + * @param {string} modifier Name of the modifier e.g. 'shiftbutton' + */ + linkModifier(group, name, modifier) { + const packet = this.getInputPacket(this.defaultPacket); + if (packet === undefined) { + console.error(`HIDController.linkModifier - Creating modifier: input packet ${this.defaultPacket} not found`); + return; + } + const bit_id = `${group}.${name}`; + const bitField = packet.lookupBit(group, name); + if (bitField === undefined) { + console.error(`HIDController.linkModifier - Bit field not found: ${bit_id}`); + return; + } + bitField.group = "modifiers"; + bitField.name = modifier; + this.modifiers.set(modifier, Boolean(bitField.value)); + } + + /** + * @todo Implement unlinking of modifiers + * @param {string} _group Unused + * @param {string} _name Unused + * @param {string} _modifier Unused + */ + unlinkModifier(_group, _name, _modifier) { + console.warn("HIDController.unlinkModifier - Unlinking of modifiers not yet implemented"); + } + + /** + * Link a previously declared HID control to actual mixxx control + * + * @param {string} group Control group name + * @param {string} name Control name + * @param {string} m_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]" + * @param {string} m_name Name of mapped control, must be a valid Mixxx control name "pregain" + * @param {fieldChangeCallback} callback Callback function for the control + */ + linkControl(group, name, m_group, m_name, callback) { + const packet = this.getInputPacket(this.defaultPacket); + if (packet === undefined) { + console.error(`HIDController.linkControl - Creating modifier: input packet ${this.defaultPacket} not found`); + return; + } + const field = packet.getField(group, name); if (field === undefined) { - HIDDebug("bit not found: " + group + "." + name); + console.error(`HIDController.linkControl - Field not found: ${group}.${name}`); return; } + if (field.type !== "bitvector") { + field.mapped_group = m_group; + field.mapped_name = m_name; + if (callback !== undefined) { + field.callback = callback; + } + } else { + const bitField = packet.lookupBit(group, name); + if (field === undefined) { + console.error(`HIDController.linkControl - Bit not found: ${group}.${name}`); + return; + } + bitField.mapped_group = m_group; + bitField.mapped_name = m_name; + if (callback !== undefined) { + bitField.callback = callback; + } + } } - field.mapped_group = m_group; - field.mapped_name = m_name; - if (callback !== undefined) - field.callback = callback; -}; - -/** TODO - implement unlinking of controls */ -HIDController.prototype.unlinkControl = function(_group, _name) { - -}; - -/** Register HID input packet type to controller. - * Input packets can be responses from device to queries, or control - * data details. The default control data packet must be named in - * variable this.defaultPacket to allow automatic processing. */ -HIDController.prototype.registerInputPacket = function(packet) { - // Find modifiers and other special cases from packet fields - for (var group_name in packet.groups) { - var group = packet.groups[group_name]; - for (var field_name in group) { - var field = group[field_name]; - field.packet = packet; - if (field.type === "bitvector") { - for (var bit_id in field.value.bits) { - var bit = field.value.bits[bit_id]; - bit.packet = packet; - if (bit.group === "modifiers") - this.modifiers.add(bit.name); + /** + * @todo Implement unlinking of controls + * @param {string} _group Mixxx control group name e.g. "[Channel1]" + * @param {string} _name Mixxx control name "pregain" + */ + unlinkControl(_group, _name) {} + /** + * Register HID input packet type to controller. + * Input packets can be responses from device to queries, or control + * data details. The default control data packet must be named in + * variable this.defaultPacket to allow automatic processing. + * + * @param {HIDPacket} packet The input packet to register + */ + registerInputPacket(packet) { + // Find modifiers and other special cases from packet fields + for (const group_name in packet.groups) { + const group = packet.groups[group_name]; + for (const field_name in group) { + const field = group[field_name]; + field.packet = packet; + if (field.type === "bitvector") { + for (const bit_id in field.value.bits) { + const bit = field.value.bits[bit_id]; + bit.packet = packet; + if (bit.group === "modifiers") { + this.modifiers.add(bit.name); + } + } + } else { + if (field.group === "modifiers") { + this.modifiers.add(field.name); + } } - } else { - if (field.group === "modifiers") - this.modifiers.add(field.name); } } - } - this.InputPackets[packet.name] = packet; -}; - -/** Register HID output packet type to controller - * There are no special Output control output packets, just register Outputs to any - * valid packet and we detect them here. - * This module only supports sending bitvector values and byte fields to device. - * If you need other data structures, patches are welcome, or you can just do it - * manually in your script without registering the packet. */ -HIDController.prototype.registerOutputPacket = function(packet) { - this.OutputPackets[packet.name] = packet; - // Link packet to all fields - for (var group_name in packet.groups) { - var group = packet.groups[group_name]; - for (var field_name in group) { - var field = group[field_name]; - field.packet = packet; - if (field.type === "bitvector") { - for (var bit_id in field.value.bits) { - var bit = field.value.bits[bit_id]; - bit.packet = packet; + this.InputPackets[packet.name] = packet; + } + /** + * Register HID output packet type to controller + * There are no special Output control output packets, just register Outputs to any + * valid packet and we detect them here. + * This module only supports sending bitvector values and byte fields to device. + * If you need other data structures, patches are welcome, or you can just do it + * manually in your script without registering the packet. + * + * @param {HIDPacket} packet The output packet to register + */ + registerOutputPacket(packet) { + this.OutputPackets[packet.name] = packet; + // Link packet to all fields + for (const group_name in packet.groups) { + const group = packet.groups[group_name]; + for (const field_name in group) { + const field = group[field_name]; + field.packet = packet; + if (field.type === "bitvector") { + for (const bit_id in field.value.bits) { + const bit = field.value.bits[bit_id]; + bit.packet = packet; + } } } } } -}; + /** + * Parse a packet representing an HID InputReport, and processes each field with "unpack": + * - Calls packet callback and returns, if packet callback was defined + * - Calls processIncomingPacket and processes automated events there. + * - If defined, calls postProcessDelta for results after processing automated fields + * + * @param {Uint8Array} data The data received from an HID InputReport. + * In case of HID devices, which use ReportIDs to enumerate the reports, + * the ReportID is stored in the first byte and the data start at the second byte + * @param {number} length Length of the data array in bytes + */ + parsePacket(data, length) { + if (this.InputPackets === undefined) { + return; + } + for (const name in this.InputPackets) { + /** @type {HIDPacket} */ + let packet = this.InputPackets[name]; + + // When the device uses ReportIDs to enumerate the reports, hidapi + // prepends the report ID to the data sent to Mixxx. If the device + // only has a single report type, the HIDPacket constructor sets the + // reportId as 0. In this case, hidapi only sends the data of the + // report to Mixxx without a report ID. + if (packet.reportId !== 0 && packet.reportId !== data[0]) { + continue; + } -/** Parse a received input packet fields with "unpack" calls to fields - * Calls packet callback and returns, if packet callback was defined - * Calls processIncomingPacket and processes automated events there. - * If defined, calls processDelta for results after processing automated fields */ -HIDController.prototype.parsePacket = function(data, length) { - var packet; - var changed_data; - if (this.InputPackets === undefined) { - return; - } - for (var name in this.InputPackets) { - packet = this.InputPackets[name]; - - // When the device uses multiple report types with report IDs, hidapi - // prepends the report ID to the data sent to Mixxx. If the device - // only has a single report type, the HIDPacket constructor sets the - // reportId as 0. In this case, hidapi only sends the data of the - // report to Mixxx without a report ID. - if (packet.reportId !== 0 && packet.reportId !== data[0]) { - continue; - } - - if (packet.header !== undefined) { - for (var header_byte = 0; header_byte < packet.header.length; header_byte++) { - if (packet.header[header_byte] !== data[header_byte]) { - packet = undefined; - break; + if (packet.header !== undefined) { + for (let header_byte = 0; header_byte < packet.header.length; header_byte++) { + if (packet.header[header_byte] !== data[header_byte]) { + packet = undefined; + break; + } + } + if (packet === undefined) { + continue; } } - if (packet === undefined) - continue; - } - changed_data = packet.parse(data); - if (packet.callback !== undefined) { - packet.callback(packet, changed_data); + const changed_data = packet.parse(data); + if (packet.callback !== undefined) { + packet.callback(packet, changed_data); + return; + } + // Process named group controls + if (packet.name === this.defaultPacket) { + this.processIncomingPacket(packet, changed_data); + } + // Process generic changed_data packet, if callback is defined + if (this.processDelta !== undefined) { + this.processDelta(packet, changed_data); + } + if (this.postProcessDelta !== undefined) { + this.postProcessDelta(packet, changed_data); + } return; } - // Process named group controls - if (packet.name === this.defaultPacket) - this.processIncomingPacket(packet, changed_data); - // Process generic changed_data packet, if callback is defined - if (this.processDelta !== undefined) - this.processDelta(packet, changed_data); - if (this.postProcessDelta !== undefined) - this.postProcessDelta(packet, changed_data); - return; - } - HIDDebug("Received unknown packet of " + length + " bytes"); - for (var i in data) HIDDebug("BYTE " + data[i]); -}; - -/** Process the modified field values (delta) from input packet fields for - * input control packet, if packet name is in this.defaultPacket. - * - * Button field processing: - * - Sets modifiers from buttons - * - Calls button callbacks, if defined - * - Finally tries to run matching engine.setValue() function for buttons - * in default mixxx groups, honoring toggleButtons and other button - * details. Not done if a callback was defined for button. - * - * Control field processing - * - Calls scaling functions for control fields, if defined for field. - * Scaling function for encoders (isEncoder attribute is true) scales - * field delta instead of raw value. - * - Calls callback functions for control fields, if defined for field - * - Finally tries run matching engine.setValue() function for control - * fields in default mixxx groups. Not done if a callback was defined. */ -HIDController.prototype.processIncomingPacket = function(packet, delta) { - var field; - for (var name in delta) { - if (this.ignoredControlChanges !== undefined - && this.ignoredControlChanges.indexOf(name) !== -1) - continue; - field = delta[name]; - if (field.type === "button") - this.processButton(field); - else if (field.type === "control") - this.processControl(field); - else - HIDDebug("Unknown field " + field.name + " type " + field.type); - } -}; - -/** Get active group for this field */ -HIDController.prototype.getActiveFieldGroup = function(field) { - if (field.mapped_group !== undefined) { - return this.resolveGroup(field.mapped_group); - } - var group = field.group; - if (group === undefined) { - if (this.activeDeck !== undefined) - return "[Channel" + this.activeDeck + "]"; - } - if (this.valid_groups.indexOf(group) !== -1) { - //HIDDebug("Resolving group " + group); - return this.resolveGroup(group); + console.warn(`HIDController.parsePacket - Received unknown packet of ${length} bytes`); + for (const i in data) { + console.log(`BYTE ${data[i]}`); + } } - return group; -}; - -/** Get active control name from field */ -HIDController.prototype.getActiveFieldControl = function(field) { - if (field.mapped_name !== undefined) - return field.mapped_name; - return field.name; -}; - -/** Process given button field, triggering events */ -HIDController.prototype.processButton = function(field) { - var group = this.getActiveFieldGroup(field); - var control = this.getActiveFieldControl(field); - - if (group === undefined) { - HIDDebug("processButton: Could not resolve group from " - + field.group + " " + field.mapped_group + " " - + field.name + " " + field.mapped_name - ); - return; + /** + * Process the modified field values (delta) from input packet fields for + * input control packet, if packet name is in this.defaultPacket. + * + * Button (Boolean value) field processing: + * - Sets modifiers from buttons + * - Calls button callbacks, if defined + * - Finally tries to run matching engine.setValue() function for buttons + * in default mixxx groups, honoring toggleButtons and other button + * details. Not done if a callback was defined for button. + * + * Control (Numeric value) field processing + * - Calls scaling functions for control fields, if defined for field. + * Scaling function for encoders (isEncoder attribute is true) scales + * field delta instead of raw value. + * - Calls callback functions for control fields, if defined for field + * - Finally tries run matching engine.setValue() function for control + * fields in default mixxx groups. Not done if a callback was defined. + * + * @param {any} packet Unused + * @param {Object.} delta + */ + processIncomingPacket(packet, delta) { + /** @type {packetField} */ + for (const name in delta) { + // @ts-ignore ignoredControlChanges should be defined in the users mapping + // see EKS-Otus.js for an example + if (this.ignoredControlChanges !== undefined && + // @ts-ignore + this.ignoredControlChanges.indexOf(name) !== -1) { + continue; + } + const field = delta[name]; + if (field.type === "button") { + // Button/Boolean field + this.processButton(field); + } else if (field.type === "control") { + // Numeric value field + this.processControl(field); + } else { + console.warn(`HIDController.processIncomingPacket - Unknown field ${field.name} type ${field.type}`); + } + } } - - if (group === "modifiers") { - if (field.value !== 0) - this.modifiers.set(control, true); - else - this.modifiers.set(control, false); - return; + /** + * Get active group for this field + * + * @param {packetField|bitObject} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + * @returns {string} Group + */ + getActiveFieldGroup(field) { + if (field.mapped_group !== undefined) { + return this.resolveGroup(field.mapped_group); + } + const group = field.group; + if (group === undefined) { + if (this.activeDeck !== undefined) { + return `[Channel${this.activeDeck}]`; + } + } + if (this.valid_groups.indexOf(group) !== -1) { + // console.log(`Resolving group ${group}`); + return this.resolveGroup(group); + } + return group; } - if (field.auto_repeat) { - var timer_id = "auto_repeat_" + field.id; - if (field.value) { - this.startAutoRepeatTimer(timer_id, field.auto_repeat_interval); + /** + * Get active control name from field + * + * @param {packetField|bitObject} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + * @returns {string} Name of field + */ + getActiveFieldControl(field) { + if (field.mapped_name !== undefined) { + return field.mapped_name; } else { - this.stopAutoRepeatTimer(timer_id); + return field.name; } } - if (field.callback !== undefined) { - field.callback(field); - return; - } - if (control === "jog_touch") { - if (group !== undefined) { - if (field.value === this.buttonStates.pressed) - this.enableScratch(group, true); - else - this.enableScratch(group, false); - } - return; - } - if (this.toggleButtons.indexOf(control) !== -1) { - if (field.value === this.buttonStates.released) + /** + * Process given button field, triggering events + * + * @param {bitObject} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + */ + processButton(field) { + const group = this.getActiveFieldGroup(field); + const control = this.getActiveFieldControl(field); + + if (group === undefined) { + console.warn( + `HIDController.processButton - Could not resolve group from ${field.group} ${field.mapped_group} ${field.name} ${field.mapped_name}`); return; - if (engine.getValue(group, control)) { - if (control === "play") - engine.setValue(group, "stop", true); - else - engine.setValue(group, control, false); - } else { - engine.setValue(group, control, true); } - return; - } - if (field.auto_repeat && field.value === this.buttonStates.pressed) { - HIDDebug("Callback for " + field.group); - engine.setValue(group, control, field.auto_repeat(field)); - } else if (engine.getValue(group, control) === false) { - engine.setValue(group, control, true); - } else { - engine.setValue(group, control, false); - } -}; -/** Process given control field, triggering events */ -HIDController.prototype.processControl = function(field) { - var value; - var group = this.getActiveFieldGroup(field); - var control = this.getActiveFieldControl(field); - - if (group === undefined) { - HIDDebug("processControl: Could not resolve group from " - + field.group + " " + field.mapped_group + " " - + field.name + " " + field.mapped_name - ); - return; - } - - if (field.callback !== undefined) { - value = field.callback(field); - return; - } - if (group === "modifiers") { - this.modifiers.set(control, field.value); - return; - } - if (control === "jog_wheel") { - // Handle jog wheel scratching transparently - this.jog_wheel(field); - return; - } - // Call value scaler if defined and send mixxx signal - value = field.value; - var scaler = this.getScaler(control); - if (field.isEncoder) { - var field_delta = field.delta; - if (scaler !== undefined) - field_delta = scaler(group, control, field_delta); - engine.setValue(group, control, field_delta); - } else { - if (scaler !== undefined) { - value = scaler(group, control, value); - // See the Traktor S4 script for how to use this. If the scaler function has this - // parameter set to true, we use the effects-engine setParameter call instead of - // setValue. - if (scaler.useSetParameter) { - engine.setParameter(group, control, value); + if (group === "modifiers") { + if (field.value !== 0) { + this.modifiers.set(control, true); + } else { + this.modifiers.set(control, false); + } + return; + } + if (field.auto_repeat) { + const timer_id = `auto_repeat_${field.id}`; + if (field.value) { + // @ts-ignore startAutoRepeatTimer needs to be implemented in the users mapping + this.startAutoRepeatTimer(timer_id, field.auto_repeat_interval); + } else { + this.stopAutoRepeatTimer(timer_id); + } + } + if (field.callback !== undefined) { + field.callback(field); + return; + } + if (control === "jog_touch") { + if (group !== undefined) { + if (field.value === this.buttonStates.pressed) { + this.enableScratch(group, true); + } else { + this.enableScratch(group, false); + } + } + return; + } + if (this.toggleButtons.indexOf(control) !== -1) { + if (field.value === this.buttonStates.released) { return; } + if (engine.getValue(group, control)) { + if (control === "play") { + engine.setValue(group, "stop", 1); + } else { + engine.setValue(group, control, Number(false)); + } + } else { + engine.setValue(group, control, Number(true)); + } + return; + } + if (field.auto_repeat && field.value === this.buttonStates.pressed) { + console.log(`Callback for ${field.group}`); + engine.setValue(group, control, field.auto_repeat(field)); + } else if (engine.getValue(group, control) === 0) { + engine.setValue(group, control, Number(true)); + } else { + engine.setValue(group, control, Number(false)); } - engine.setValue(group, control, value); - } -}; - -/** Toggle control state from toggle button */ -HIDController.prototype.toggle = function(group, control, value) { - if (value === this.buttonStates.released) - return; - var status = engine.getValue(group, control) !== true; - engine.setValue(group, control, status); -}; - -/** Toggle play/pause state */ -HIDController.prototype.togglePlay = function(group, field) { - if (field.value === this.buttonStates.released) - return; - var status = !(engine.getValue(group, "play")); - if (!status) - engine.setValue(group, "stop", true); - else - engine.setValue(group, "play", true); -}; - -/** Processing of the 'jog_touch' special button name, which is used to detect - * when scratching should be enabled. - * Deck is resolved from group with 'resolveDeck' - * - * Enabling scratching (press 'jog_touch' button) - * Sets the internal 'isScratchEnabled' attribute to true, and calls scratchEnable - * with the scratch attributes (see class definition) - * - * Disabling scratching (release 'jog_touch' button) - * Sets the internal 'isScratchEnabled attribute to false, and calls scratchDisable - * to end scratching mode */ -HIDController.prototype.enableScratch = function(group, status) { - var deck = this.resolveDeck(group); - if (status) { - this.isScratchEnabled = true; - engine.scratchEnable(deck, - this.scratchintervalsPerRev, - this.scratchRPM, - this.scratchAlpha, - this.scratchBeta, - this.rampedScratchEnable - ); - if (this.enableScratchCallback !== undefined) this.enableScratchCallback(true); - } else { - this.isScratchEnabled = false; - engine.scratchDisable(deck, this.rampedScratchDisable); - if (this.enableScratchCallback !== undefined) this.enableScratchCallback(false); } -}; + /** + * Process given control field, triggering events + * + * @param {packetField} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + */ + processControl(field) { + const group = this.getActiveFieldGroup(field); + const control = this.getActiveFieldControl(field); + + if (group === undefined) { + console.error( + `HIDController.processControl - Could not resolve group from ${field.group} ${field.mapped_group} ${field.name} ${field.mapped_name}`); + return; + } + if (field.type === "bitvector") { + console.error(`HIDController.processControl - Control refers a field of type bitvector: ${group}.${control}`); + return; + } -/** Default jog scratching function. Used to handle jog move events from special - * input control field called 'jog_wheel'. Handles both 'scratch' and 'jog' mixxx - * functions, depending on isScratchEnabled value above (see enableScratch()) - * - * Since most controllers require value scaling for jog and scratch functions, - * you are warned if following scaling function names are not registered: - * - * jog - * Scaling function from 'jog_wheel' for rate bend events with mixxx 'jog' - * function. Should return value range suitable for 'jog', whatever you - * wish it to do. - * jog_scratch - * Scaling function from 'jog_wheel' for scratch movements with mixxx - * 'scratchTick' function. Should return -1,0,1 or small ranges of integers - * both negative and positive values. - */ -HIDController.prototype.jog_wheel = function(field) { - var scaler = undefined; - var active_group = this.getActiveFieldGroup(field); - var value = undefined; - if (field.isEncoder) - value = field.delta; - else - value = field.value; - if (this.isScratchEnabled) { - var deck = this.resolveDeck(active_group); - if (deck === undefined) + if (field.callback !== undefined) { + /*value =*/field.callback(field); + return; + } + if (group === "modifiers") { + this.modifiers.set(control, Boolean(field.value)); return; - scaler = this.getScaler("jog_scratch"); - if (scaler !== undefined) - value = scaler(active_group, "jog_scratch", value); - else - HIDDebug("WARNING non jog_scratch scaler, you likely want one"); - engine.scratchTick(deck, value); - } else { - if (active_group === undefined) + } + if (control === "jog_wheel") { + // Handle jog wheel scratching transparently + this.jog_wheel(field); return; - scaler = this.getScaler("jog"); - if (scaler !== undefined) - value = scaler(active_group, "jog", value); - else - HIDDebug("WARNING non jog scaler, you likely want one"); - engine.setValue(active_group, "jog", value); + } + // Call value scaler if defined and send mixxx signal + let value = field.value; + const scaler = this.getScaler(control); + if (field.isEncoder) { + let field_delta = field.delta; + if (scaler !== undefined) { + field_delta = scaler(group, control, field_delta); + } + engine.setValue(group, control, field_delta); + } else { + if (scaler !== undefined) { + value = scaler(group, control, Number(value)); + // See the Traktor S4 script for how to use this. If the scaler function has this + // parameter set to true, we use the effects-engine setParameter call instead of + // setValue. + if (scaler.useSetParameter) { + engine.setParameter(group, control, value); + return; + } + } + engine.setValue(group, control, Number(value)); + } } -}; - -HIDController.prototype.stopAutoRepeatTimer = function(timer_id) { - if (this.timers[timer_id]) { - engine.stopTimer(this.timers[timer_id]); - delete this.timers[timer_id]; - } else { - //HIDDebug("No such autorepeat timer: " + timer_id); + /** + * Toggle control state from toggle button + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} control Name of the control (button) + * @param {number} value Value defined in this.buttonStates + */ + toggle(group, control, value) { + if (value === this.buttonStates.released) { + return; + } + const status = Boolean(engine.getValue(group, control)) !== true; + engine.setValue(group, control, Number(status)); + } + /** + * Toggle play/pause state + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {packetField} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + */ + togglePlay(group, field) { + if (field.value === this.buttonStates.released) { + return; + } + const status = !(engine.getValue(group, "play")); + if (!status) { + engine.setValue(group, "stop", Number(true)); + } else { + engine.setValue(group, "play", Number(true)); + } } -}; - -/** Toggle field autorepeat on or off */ -HIDController.prototype.setAutoRepeat = function(group, name, callback, interval) { - var packet = this.getInputPacket(this.defaultPacket); - var field = packet.getField(group, name); - if (field === undefined) { - HIDDebug("setAutoRepeat: field not found " + group + "." + name); - return; + /** + * Processing of the 'jog_touch' special button name, which is used to detect + * when scratching should be enabled. + * Deck is resolved from group with 'resolveDeck' + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {boolean} status Enable or Disable scratching: + * - true enables scratching (press 'jog_touch' button) + * Sets the internal 'isScratchEnabled' attribute to true, and calls scratchEnable + * with the scratch attributes (see class definition) + * + * - false disables scratching (release 'jog_touch' button) + * Sets the internal 'isScratchEnabled attribute to false, and calls scratchDisable + * to end scratching mode + */ + enableScratch(group, status) { + const deck = this.resolveDeck(group); + if (status) { + this.isScratchEnabled = true; + engine.scratchEnable(deck, + this.scratchintervalsPerRev, + this.scratchRPM, + this.scratchAlpha, + this.scratchBeta, + this.scratchRampOnEnable + ); + if (this.enableScratchCallback !== undefined) { + this.enableScratchCallback(true); + } + } else { + this.isScratchEnabled = false; + engine.scratchDisable(deck, this.scratchRampOnDisable); + if (this.enableScratchCallback !== undefined) { + this.enableScratchCallback(false); + } + } } - field.auto_repeat = callback; - if (interval) - field.auto_repeat_interval = interval; - else - field.auto_repeat_interval = controller.auto_repeat_interval; - if (callback) - callback(field); -}; - -/** Callback for auto repeat timer to send again the values for - * buttons and controls marked as 'auto_repeat' - * Timer must be defined from actual controller side, because of - * callback call namespaces and 'this' reference */ -HIDController.prototype.autorepeatTimer = function() { - var group_name; - var group; - var field; - var field_name; - var bit_name; - var bit; - var packet = this.InputPackets[this.defaultPacket]; - for (group_name in packet.groups) { - group = packet.groups[group_name]; - for (field_name in group) { - field = group[field_name]; - if (field.type !== "bitvector") { - if (field.auto_repeat) - this.processControl(field); - continue; + /** + * Default jog scratching function. Used to handle jog move events from special + * input control field called 'jog_wheel'. Handles both 'scratch' and 'jog' mixxx + * functions, depending on isScratchEnabled value above (see enableScratch()) + * + * Since most controllers require value scaling for jog and scratch functions, + * you are warned if following scaling function names are not registered: + * + * jog + * Scaling function from 'jog_wheel' for rate bend events with mixxx 'jog' + * function. Should return value range suitable for 'jog', whatever you + * wish it to do. + * jog_scratch + * Scaling function from 'jog_wheel' for scratch movements with mixxx + * 'scratchTick' function. Should return -1,0,1 or small ranges of integers + * both negative and positive values. + * + * @param {packetField} field Object that describes a field inside of a packet, which can often + * mapped to a Mixxx control. + */ + jog_wheel(field) { + const active_group = this.getActiveFieldGroup(field); + if (field.type === "bitvector") { + console.error("HIDPacket.jog_wheel - Setting field.value of type for bitvector packet does not make sense"); + return; + } + let value; + if (field.isEncoder) { + value = Number(field.delta); + } else { + value = Number(field.value); + } + if (this.isScratchEnabled) { + const deck = this.resolveDeck(active_group); + if (deck === undefined) { + return; } - for (bit_name in field.value.bits) { - bit = field.value.bits[bit_name]; - if (bit.auto_repeat) - this.processButton(bit); + const jogScratchScaler = this.getScaler("jog_scratch"); + if (jogScratchScaler !== undefined) { + value = jogScratchScaler(active_group, "jog_scratch", value); + } else { + console.warn("HIDController.jog_wheel - Non jog_scratch scaler, you likely want one"); } + engine.scratchTick(deck, value); + } else { + if (active_group === undefined) { + return; + } + const jogScaler = this.getScaler("jog"); + if (jogScaler !== undefined) { + value = jogScaler(active_group, "jog", value); + } else { + console.warn("HIDController.jog_wheel - Non jog scaler, you likely want one"); + } + engine.setValue(active_group, "jog", value); } } -}; - -/** Toggle active deck and update virtual output field control mappings */ -HIDController.prototype.switchDeck = function(deck) { - var packet; - var field; - var controlgroup; - if (deck === undefined) { - if (this.activeDeck === undefined) { - deck = 1; + /** + * Stops the specified auto repeat timer + * + * @param {string} timer_id Reference of the timer to stop + */ + stopAutoRepeatTimer(timer_id) { + if (this.timers[timer_id]) { + engine.stopTimer(this.autorepeat[timer_id]); + delete this.timers[timer_id]; } else { - // This is unusable: num_decks has always minimum 4 decks - // var totalDecks = engine.getValue("[Master]","num_decks"); - // deck = (this.activeDeck+1) % totalDecks; - deck = this.deckSwitchMap[this.activeDeck]; - if (deck === undefined) - deck = 1; + // console.warn(`HIDController.stopAutoRepeatTimer - No such autorepeat timer: ${timer_id}`); } } - var new_group = this.resolveDeckGroup(deck); - HIDDebug("Switching to deck " + deck + " group " + new_group); - if (this.disconnectDeck !== undefined) - this.disconnectDeck(); - for (var packet_name in this.OutputPackets) { - packet = this.OutputPackets[packet_name]; - for (var group_name in packet.groups) { - var group = packet.groups[group_name]; - for (var field_name in group) { - field = group[field_name]; - if (field.type === "bitvector") { - for (var bit_id in field.value.bits) { - var bit = field.value.bits[bit_id]; - if (this.virtualDecks.indexOf(bit.mapped_group) === -1) - continue; - controlgroup = this.resolveGroup(bit.mapped_group); - engine.connectControl(controlgroup, bit.mapped_name, bit.mapped_callback, true); - engine.connectControl(new_group, bit.mapped_name, bit.mapped_callback); - var value = engine.getValue(new_group, bit.mapped_name); - HIDDebug("BIT " + bit.group + "." + bit.name + " value " + value); - if (value) - this.setOutput( - bit.group, bit.name, - this.LEDColors[this.deckOutputColors[deck]] - ); - else - this.setOutput( - bit.group, bit.name, - this.LEDColors.off - ); + /** + * Toggle field autorepeat on or off + * + * @param {string} group + * @param {string} name + * @param {fieldChangeCallback} callback Callback function for the control + * @param {number} interval + */ + setAutoRepeat(group, name, callback, interval) { + const packet = this.getInputPacket(this.defaultPacket); + const field = packet.getField(group, name); + if (field === undefined) { + console.error(`HIDController.setAutoRepeat - Field not found ${group}.${name}`); + return; + } + field.auto_repeat = callback; + if (interval) { + field.auto_repeat_interval = interval; + } else { + field.auto_repeat_interval = this.auto_repeat_interval; + } + if (callback) { + callback(field); + } + } + /** + * Callback for auto repeat timer to send again the values for + * buttons and controls marked as 'auto_repeat' + * Timer must be defined from actual controller side, because of + * callback call namespaces and 'this' reference + */ + autorepeatTimer() { + const packet = this.InputPackets[this.defaultPacket]; + for (const group_name in packet.groups) { + const group = packet.groups[group_name]; + for (const field_name in group) { + const field = group[field_name]; + if (field.type !== "bitvector") { + if (field.auto_repeat) { + this.processControl(field); } continue; } - // Only move outputs of virtual decks - if (this.virtualDecks.indexOf(field.mapped_group) === -1) - continue; - controlgroup = this.resolveGroup(field.mapped_group); - engine.connectControl(controlgroup, field.mapped_name, field.mapped_callback, true); - engine.connectControl(new_group, field.mapped_name, field.mapped_callback); - value = engine.getValue(new_group, field.mapped_name); - if (value) - this.setOutput( - field.group, field.name, - this.LEDColors[this.deckOutputColors[deck]] - ); - else - this.setOutput( - field.group, field.name, - this.LEDColors.off - ); + for (const bit_name in field.value.bits) { + const bit = field.value.bits[bit_name]; + if (bit.auto_repeat) { + this.processButton(bit); + } + } } } } - this.activeDeck = deck; - if (this.connectDeck !== undefined) - this.connectDeck(); -}; - -/** Link a virtual HID Output to mixxx control */ -HIDController.prototype.linkOutput = function(group, name, m_group, m_name, callback) { - var controlgroup; - var field = this.getOutputField(group, name); - if (field === undefined) { - HIDDebug("Linked output not found: " + group + "." + name); - return; - } - if (field.mapped_group !== undefined) { - HIDDebug("Output already linked: " + field.mapped_group); - return; - } - controlgroup = this.resolveGroup(m_group); - field.mapped_group = m_group; - field.mapped_name = m_name; - field.mapped_callback = callback; - engine.connectControl(controlgroup, m_name, callback); - if (engine.getValue(controlgroup, m_name)) - this.setOutput(m_group, m_name, "on"); - else - this.setOutput(m_group, m_name, "off"); -}; - -/** Unlink a virtual HID Output from mixxx control */ -HIDController.prototype.unlinkOutput = function(group, name, callback) { - var field = this.getOutputField(group, name); - var controlgroup; - if (field === undefined) { - HIDDebug("Unlinked output not found: " + group + "." + name); - return; + /** + * Toggle active deck and update virtual output field control mappings + * + * @param {number} deck Number of deck + */ + switchDeck(deck) { + if (deck === undefined) { + if (this.activeDeck === undefined) { + deck = 1; + } else { + // This is unusable: num_decks has always minimum 4 decks + // var totalDecks = engine.getValue("[Master]","num_decks"); + // deck = (this.activeDeck+1) % totalDecks; + deck = this.deckSwitchMap[this.activeDeck]; + if (deck === undefined) { + deck = 1; + } + } + } + const new_group = this.resolveDeckGroup(deck); + console.log(`Switching to deck ${deck} group ${new_group}`); + if (this.disconnectDeck !== undefined) { + this.disconnectDeck(); + } + for (const packet_name in this.OutputPackets) { + const packet = this.OutputPackets[packet_name]; + for (const group_name in packet.groups) { + const group = packet.groups[group_name]; + for (const field_name in group) { + const field = group[field_name]; + if (field.type === "bitvector") { + for (const bit_id in field.value.bits) { + const bit = field.value.bits[bit_id]; + if (this.virtualDecks.indexOf(bit.mapped_group) === -1) { + continue; + } + const bitControlGroup = this.resolveGroup(bit.mapped_group); + engine.connectControl( + bitControlGroup, bit.mapped_name, bit.mapped_callback, true); + engine.connectControl(new_group, bit.mapped_name, bit.mapped_callback); + const value = engine.getValue(new_group, bit.mapped_name); + console.log(`Bit ${bit.group}.${bit.name} value ${value}`); + if (value) { + this.setOutput( + bit.group, bit.name, + this.LEDColors[this.deckOutputColors[deck]]); + } else { + this.setOutput( + bit.group, bit.name, + this.LEDColors.off + ); + } + } + continue; + } + // Only move outputs of virtual decks + if (this.virtualDecks.indexOf(field.mapped_group) === -1) { + continue; + } + const fieldControlGroup = this.resolveGroup(field.mapped_group); + engine.connectControl( + fieldControlGroup, field.mapped_name, field.mapped_callback, true); + engine.connectControl(new_group, field.mapped_name, field.mapped_callback); + const value = engine.getValue(new_group, field.mapped_name); + if (value) { + this.setOutput( + field.group, field.name, + this.LEDColors[this.deckOutputColors[deck]] + ); + } else { + this.setOutput( + field.group, field.name, + this.LEDColors.off + ); + } + } + } + } + this.activeDeck = deck; + if (this.connectDeck !== undefined) { + this.connectDeck(); + } } - if (field.mapped_group === undefined || field.mapped_name === undefined) { - HIDDebug("Unlinked output not mapped: " + group + "." + name); - return; + /** + * Link a virtual HID Output to mixxx control + * + * @param {string} group Control group name + * @param {string} name Control name + * @param {string} m_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]" + * @param {string} m_name Name of mapped control, must be a valid Mixxx control name "VuMeter" + * @param {controlCallback} callback Callback function for the control + */ + linkOutput(group, name, m_group, m_name, callback) { + const field = this.getOutputField(group, name); + if (field === undefined) { + console.error(`HIDController.linkOutput - Linked output not found: ${group}.${name}`); + return; + } + if (field.mapped_group !== undefined) { + console.warn(`HIDController.linkOutput - Output already linked: ${field.mapped_group}`); + return; + } + const controlgroup = this.resolveGroup(m_group); + field.mapped_group = m_group; + field.mapped_name = m_name; + field.mapped_callback = callback; + engine.connectControl(controlgroup, m_name, callback); + if (engine.getValue(controlgroup, m_name)) { + this.setOutput(m_group, m_name, this.LEDColors.on); + } else { + this.setOutput(m_group, m_name, this.LEDColors.off); + } } - controlgroup = this.resolveGroup(field.mapped_group); - engine.connectControl(controlgroup, field.mapped_name, callback, true); - field.mapped_group = undefined; - field.mapped_name = undefined; - field.mapped_callback = undefined; -}; - -/** Set output state to given value */ -HIDController.prototype.setOutput = function(group, name, value, send_packet) { - var field = this.getOutputField(group, name); - if (field === undefined) { - HIDDebug("setOutput: unknown field: " + group + "." + name); - return; + /** + * Unlink a virtual HID Output from mixxx control + * + * @param {string} group Mixxx control group name e.g. "[Channel1]" + * @param {string} name Mixxx control name "VuMeter" + * @param {controlCallback} callback Callback function for the control + */ + unlinkOutput(group, name, callback) { + const field = this.getOutputField(group, name); + if (field === undefined) { + console.warn(`HIDController.unlinkOutput - Output to be unlinked not found: ${group}.${name}`); + return; + } + if (field.mapped_group === undefined || field.mapped_name === undefined) { + console.warn(`HIDController.unlinkOutput - Output to be unlinked not mapped: ${group}.${name}`); + return; + } + const controlgroup = this.resolveGroup(field.mapped_group); + engine.connectControl(controlgroup, field.mapped_name, callback, true); + field.mapped_group = undefined; + field.mapped_name = undefined; + field.mapped_callback = undefined; + } + /** + * Set output state to given value + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "cue_indicator" + * @param {number|boolean} value Value to set as new output state of the control + * @param {boolean} [send_packet=false] If true, the packet (an HID OutputReport) is send + * immediately + */ + setOutput(group, name, value, send_packet) { + const field = this.getOutputField(group, name); + if (field === undefined) { + console.error(`HIDController.setOutput - Unknown field: ${group}.${name}`); + return; + } + field.value = Number(value) << field.bit_offset; + field.toggle = Number(value) << field.bit_offset; + if (send_packet) { + field.packet.send(); + } } - field.value = value << field.bit_offset; - field.toggle = value << field.bit_offset; - if (send_packet) + /** + * Set Output to toggle between two values. Reset with setOutput(name,'off') + * + * @param {string} group Control group name e.g. "[Channel1]" + * @param {string} name Control name "cue_indicator" + * @param toggle_value + */ + setOutputToggle(group, name, toggle_value) { + const field = this.getOutputField(group, name); + if (field === undefined) { + console.error(`HIDController.setOutputToggle - Unknown field ${group}.${name}`); + return; + } + field.value = toggle_value << field.bit_offset; + field.toggle = toggle_value << field.bit_offset; field.packet.send(); -}; - -/** Set Output to toggle between two values. Reset with setOutput(name,'off') */ -HIDController.prototype.setOutputToggle = function(group, name, toggle_value) { - var field = this.getOutputField(group, name); - if (field === undefined) { - HIDDebug("setOutputToggle: unknown field " + group + "." + name); - return; } - field.value = toggle_value << field.bit_offset; - field.toggle = toggle_value << field.bit_offset; - field.packet.send(); -}; +} +// Add class HIDController to the Global JavaScript object +// @ts-ignore Same identifier for class and instance needed for backward compatibility +this.HIDController = HIDController;