@elgato-stream-deck/core
Version:
An npm module for interfacing with the Elgato Stream Deck
241 lines • 11.5 kB
JavaScript
;
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