UNPKG

@logitech-mx-creative-console/core

Version:

An npm module for interfacing with the Logitech MX Creative Console

184 lines (181 loc) 8.6 kB
export 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 = []; const blackBuffer = new Uint8Array(this.#deviceProperties.PANEL_SIZE.width * this.#deviceProperties.PANEL_SIZE.height * 4); // TODO - cache this? const byteBuffer = await this.#imagePacker.convertPixelBuffer(blackBuffer, { format: 'rgba', offset: 0, stride: this.#deviceProperties.PANEL_SIZE.width * 4 }, this.#deviceProperties.PANEL_SIZE); const packets = this.#imageWriter.generateFillImageWrites({ pixelSize: this.#deviceProperties.PANEL_SIZE, pixelPosition: { x: 0, y: 0 } }, byteBuffer); ps.push(this.#device.sendReports(packets)); /* for (const control of this.#deviceProperties.CONTROLS) { if (control.type !== 'button') continue switch (control.feedbackType) { case 'lcd': { // Handled by PANEL_SIZE 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`); const pixels = new Uint8Array(control.pixelSize.width * control.pixelSize.height * 3); 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`); // 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 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`); 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}`); } await this.fillImageRangeControl(control, imageBuffer, { format: sourceFormat, offset: 0, stride: control.pixelSize.width * sourceFormat.length, }); } async fillPanelBuffer(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.fillImageRangeControl(control, imageBuffer, { format: sourceFormat, offset: rowOffset + colOffset, stride, })); } await Promise.all(ps); } async fillImageRangeControl(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); const packets = this.#imageWriter.generateFillImageWrites({ pixelSize: buttonControl.pixelSize, pixelPosition: buttonControl.pixelPosition }, byteBuffer); await this.#device.sendReports(packets); } 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}"`); } } } } //# sourceMappingURL=default.js.map