@logitech-mx-creative-console/core
Version:
An npm module for interfacing with the Logitech MX Creative Console
184 lines (181 loc) • 8.6 kB
JavaScript
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