UNPKG

@elgato-stream-deck/core

Version:

An npm module for interfacing with the Elgato Stream Deck

241 lines 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DefaultButtonsLcdService = void 0; const preparedBuffer_js_1 = require("../../preparedBuffer.js"); class DefaultButtonsLcdService { #imageWriter; #imagePacker; #device; #deviceProperties; constructor(imageWriter, imagePacker, device, deviceProperties) { this.#imageWriter = imageWriter; this.#imagePacker = imagePacker; this.#device = device; this.#deviceProperties = deviceProperties; } getLcdButtonControls() { return this.#deviceProperties.CONTROLS.filter((control) => control.type === 'button' && control.feedbackType === 'lcd'); } calculateLcdGridSpan(buttonsLcd) { if (buttonsLcd.length === 0) return null; const allRowValues = buttonsLcd.map((button) => button.row); const allColumnValues = buttonsLcd.map((button) => button.column); return { minRow: Math.min(...allRowValues), maxRow: Math.max(...allRowValues), minCol: Math.min(...allColumnValues), maxCol: Math.max(...allColumnValues), }; } calculateDimensionsFromGridSpan(gridSpan, buttonPixelSize, withPadding) { if (withPadding) { // TODO: Implement padding throw new Error('Not implemented'); } else { const rowCount = gridSpan.maxRow - gridSpan.minRow + 1; const columnCount = gridSpan.maxCol - gridSpan.minCol + 1; // TODO: Consider that different rows/columns could have different dimensions return { width: columnCount * buttonPixelSize.width, height: rowCount * buttonPixelSize.height, }; } } calculateFillPanelDimensions(options) { const buttonLcdControls = this.getLcdButtonControls(); const gridSpan = this.calculateLcdGridSpan(buttonLcdControls); if (!gridSpan || buttonLcdControls.length === 0) return null; return this.calculateDimensionsFromGridSpan(gridSpan, buttonLcdControls[0].pixelSize, options?.withPadding); } async clearPanel() { const ps = []; if (this.#deviceProperties.FULLSCREEN_PANELS > 0) { // TODO - should this be a separate property? for (let screenIndex = 0; screenIndex < this.#deviceProperties.FULLSCREEN_PANELS; screenIndex++) { ps.push(this.#device.sendFeatureReport(new Uint8Array([0x03, 0x05, screenIndex, 0, 0, 0]))); } // TODO - clear rgb? } else { for (const control of this.#deviceProperties.CONTROLS) { if (control.type !== 'button') continue; switch (control.feedbackType) { case 'rgb': ps.push(this.sendKeyRgb(control.hidIndex, 0, 0, 0)); break; case 'lcd': if (this.#deviceProperties.SUPPORTS_RGB_KEY_FILL) { ps.push(this.sendKeyRgb(control.hidIndex, 0, 0, 0)); } else { const pixels = new Uint8Array(control.pixelSize.width * control.pixelSize.height * 3); // TODO - caching? ps.push(this.fillImageRangeControl(control, pixels, { format: 'rgb', offset: 0, stride: control.pixelSize.width * 3, })); } break; case 'none': // Do nothing break; } } } await Promise.all(ps); } async clearKey(keyIndex) { const control = this.#deviceProperties.CONTROLS.find((control) => control.type === 'button' && control.index === keyIndex); if (!control || control.feedbackType === 'none') throw new TypeError(`Expected a valid keyIndex`); if (this.#deviceProperties.SUPPORTS_RGB_KEY_FILL || control.feedbackType === 'rgb') { await this.sendKeyRgb(keyIndex, 0, 0, 0); } else { const pixels = new Uint8Array(control.pixelSize.width * control.pixelSize.height * 3); // TODO - caching? await this.fillImageRangeControl(control, pixels, { format: 'rgb', offset: 0, stride: control.pixelSize.width * 3, }); } } async fillKeyColor(keyIndex, r, g, b) { this.checkRGBValue(r); this.checkRGBValue(g); this.checkRGBValue(b); const control = this.#deviceProperties.CONTROLS.find((control) => control.type === 'button' && control.index === keyIndex); if (!control || control.feedbackType === 'none') throw new TypeError(`Expected a valid keyIndex`); if (this.#deviceProperties.SUPPORTS_RGB_KEY_FILL || control.feedbackType === 'rgb') { await this.sendKeyRgb(keyIndex, r, g, b); } else { // rgba is excessive here, but it makes the fill easier as it can be done in a 32bit uint const pixelCount = control.pixelSize.width * control.pixelSize.height; const pixels = new Uint8Array(pixelCount * 4); const view = new DataView(pixels.buffer, pixels.byteOffset, pixels.byteLength); // write first pixel view.setUint8(0, r); view.setUint8(1, g); view.setUint8(2, b); view.setUint8(3, 255); // read computed pixel const sample = view.getUint32(0); // fill with computed pixel for (let i = 1; i < pixelCount; i++) { view.setUint32(i * 4, sample); } await this.fillImageRangeControl(control, pixels, { format: 'rgba', offset: 0, stride: control.pixelSize.width * 4, }); } } async fillKeyBuffer(keyIndex, imageBuffer, options) { const packets = await this.prepareFillKeyBufferInner(keyIndex, imageBuffer, options); await this.#device.sendReports(packets); } async prepareFillKeyBufferInner(keyIndex, imageBuffer, options) { const sourceFormat = options?.format ?? 'rgb'; this.checkSourceFormat(sourceFormat); const control = this.#deviceProperties.CONTROLS.find((control) => control.type === 'button' && control.index === keyIndex); if (!control || control.feedbackType === 'none') throw new TypeError(`Expected a valid keyIndex`); if (control.feedbackType !== 'lcd') throw new TypeError(`keyIndex ${control.index} does not support lcd feedback`); const imageSize = control.pixelSize.width * control.pixelSize.height * sourceFormat.length; if (imageBuffer.length !== imageSize) { throw new RangeError(`Expected image buffer of length ${imageSize}, got length ${imageBuffer.length}`); } return this.prepareFillImageRangeControl(control, imageBuffer, { format: sourceFormat, offset: 0, stride: control.pixelSize.width * sourceFormat.length, }); } async prepareFillKeyBuffer(keyIndex, imageBuffer, options, jsonSafe) { const packets = await this.prepareFillKeyBufferInner(keyIndex, imageBuffer, options); return (0, preparedBuffer_js_1.wrapBufferToPreparedBuffer)(this.#deviceProperties.MODEL, 'fill-key', packets, jsonSafe ?? false); } async fillPanelBuffer(imageBuffer, options) { const packets = await this.prepareFillPanelBufferInner(imageBuffer, options); await this.#device.sendReports(packets); } async prepareFillPanelBufferInner(imageBuffer, options) { const sourceFormat = options?.format ?? 'rgb'; this.checkSourceFormat(sourceFormat); const buttonLcdControls = this.getLcdButtonControls(); const panelGridSpan = this.calculateLcdGridSpan(buttonLcdControls); if (!panelGridSpan || buttonLcdControls.length === 0) { throw new Error(`Panel does not support being filled`); } const panelDimensions = this.calculateDimensionsFromGridSpan(panelGridSpan, buttonLcdControls[0].pixelSize, options?.withPadding); const expectedByteCount = sourceFormat.length * panelDimensions.width * panelDimensions.height; if (imageBuffer.length !== expectedByteCount) { throw new RangeError(`Expected image buffer of length ${expectedByteCount}, got length ${imageBuffer.length}`); } const stride = panelDimensions.width * sourceFormat.length; const ps = []; for (const control of buttonLcdControls) { const controlRow = control.row - panelGridSpan.minRow; const controlCol = control.column - panelGridSpan.minCol; // TODO: Consider that different rows/columns could have different dimensions const iconSize = control.pixelSize.width * sourceFormat.length; const rowOffset = stride * controlRow * control.pixelSize.height; const colOffset = controlCol * iconSize; // TODO: Implement padding ps.push(this.prepareFillImageRangeControl(control, imageBuffer, { format: sourceFormat, offset: rowOffset + colOffset, stride, })); } const packets = await Promise.all(ps); return packets.flat(); } async prepareFillPanelBuffer(imageBuffer, options, jsonSafe) { const packets = await this.prepareFillPanelBufferInner(imageBuffer, options); return (0, preparedBuffer_js_1.wrapBufferToPreparedBuffer)(this.#deviceProperties.MODEL, 'fill-panel', packets, jsonSafe ?? false); } async sendKeyRgb(keyIndex, red, green, blue) { await this.#device.sendFeatureReport(new Uint8Array([0x03, 0x06, keyIndex, red, green, blue])); } async fillImageRangeControl(buttonControl, imageBuffer, sourceOptions) { const packets = await this.prepareFillImageRangeControl(buttonControl, imageBuffer, sourceOptions); await this.#device.sendReports(packets); } async prepareFillImageRangeControl(buttonControl, imageBuffer, sourceOptions) { if (buttonControl.feedbackType !== 'lcd') throw new TypeError(`keyIndex ${buttonControl.index} does not support lcd feedback`); const byteBuffer = await this.#imagePacker.convertPixelBuffer(imageBuffer, sourceOptions, buttonControl.pixelSize); return this.#imageWriter.generateFillImageWrites({ keyIndex: buttonControl.hidIndex }, byteBuffer); } checkRGBValue(value) { if (value < 0 || value > 255) { throw new TypeError('Expected a valid color RGB value 0 - 255'); } } checkSourceFormat(format) { switch (format) { case 'rgb': case 'rgba': case 'bgr': case 'bgra': break; default: { const fmt = format; throw new TypeError(`Expected a known color format not "${fmt}"`); } } } } exports.DefaultButtonsLcdService = DefaultButtonsLcdService; //# sourceMappingURL=default.js.map