diff --git a/packages/uhk-agent/src/electron-main.ts b/packages/uhk-agent/src/electron-main.ts index 772faa70f3d..60a7053842a 100644 --- a/packages/uhk-agent/src/electron-main.ts +++ b/packages/uhk-agent/src/electron-main.ts @@ -25,6 +25,7 @@ import isDev from 'electron-is-dev'; import { setMenu } from './electron-menu'; import { loadWindowState, saveWindowState } from './util/window'; import { + captureOled, getWindowBackgroundColor, options, cliUsage, @@ -171,6 +172,12 @@ async function createWindow() { if (isSecondInstance) { app.quit(); +} else if (options['capture-oled']) { + captureOled({ + logger, + commandLineArgs: options, + uhkOperations, + }) } else if (options['print-hardware-configuration']) { printHardwareConfiguration({ logger, uhkOperations }) } else if (options['print-status-buffer']) { diff --git a/packages/uhk-agent/src/util/capture-oled.ts b/packages/uhk-agent/src/util/capture-oled.ts new file mode 100644 index 00000000000..7adc0ba27f9 --- /dev/null +++ b/packages/uhk-agent/src/util/capture-oled.ts @@ -0,0 +1,59 @@ +import process from 'node:process'; +import fs from 'node:fs/promises'; + +import { + CommandLineArgs, + OLED_DISPLAY_HEIGHT, + OLED_DISPLAY_WIDTH, + UHK_80_DEVICE, +} from 'uhk-common'; +import { getCurrentUhkDeviceProduct } from 'uhk-usb'; +import { UhkOperations } from 'uhk-usb'; + +import { ElectronLogService } from '../services/logger.service'; +import { createPNG } from './create-png'; + +export interface CaptureOledOptions { + logger: ElectronLogService; + uhkOperations: UhkOperations; + commandLineArgs: CommandLineArgs; +} + +export async function captureOled(options: CaptureOledOptions): Promise { + try { + const device = await getCurrentUhkDeviceProduct(options.commandLineArgs); + + if (!device) { + options.logger.error('Cannot detect UHK device'); + process.exit(-1); + } + else if (device.id !== UHK_80_DEVICE.id) { + options.logger.error(`${device.name} does not have OLED panel.`); + process.exit(-1); + } + + const oledData = await options.uhkOperations.readOled() + const pixelData = Buffer.alloc(OLED_DISPLAY_WIDTH * OLED_DISPLAY_HEIGHT * 3); + + let pixelIndex = 0; + for (let i = 0; i ): Buffer { + // PNG signature + const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + + // IHDR chunk + const ihdr = Buffer.alloc(25); + ihdr.writeUInt32BE(13, 0); // Length + ihdr.write('IHDR', 4); + ihdr.writeUInt32BE(width, 8); + ihdr.writeUInt32BE(height, 12); + ihdr.writeUInt8(8, 16); // Bit depth + ihdr.writeUInt8(2, 17); // Color type (RGB) + ihdr.writeUInt8(0, 18); // Compression + ihdr.writeUInt8(0, 19); // Filter + ihdr.writeUInt8(0, 20); // Interlace + + // Calculate CRC for IHDR + const ihdrCrc = crc32(ihdr.slice(4, 21)); + ihdr.writeUInt32BE(ihdrCrc, 21); + + // Prepare image data with filter bytes + const scanlineLength = width * 3 + 1; // 3 bytes per pixel + 1 filter byte + const imageData = Buffer.alloc(height * scanlineLength); + + for (let y = 0; y < height; y++) { + const offset = y * scanlineLength; + imageData[offset] = 0; // Filter type: None + + for (let x = 0; x < width; x++) { + const pixelOffset = offset + 1 + x * 3; + const dataOffset = (y * width + x) * 3; + + imageData[pixelOffset] = pixelData[dataOffset]; // R + imageData[pixelOffset + 1] = pixelData[dataOffset + 1]; // G + imageData[pixelOffset + 2] = pixelData[dataOffset + 2]; // B + } + } + + const compressed = zlib.deflateSync(imageData); + + // IDAT chunk + const idat = Buffer.alloc(compressed.length + 12); + idat.writeUInt32BE(compressed.length, 0); + idat.write('IDAT', 4); + compressed.copy(idat, 8); + const idatCrc = crc32(idat.slice(4, 8 + compressed.length)); + idat.writeUInt32BE(idatCrc, 8 + compressed.length); + + // IEND chunk + const iend = Buffer.from([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]); + + return Buffer.concat([signature, ihdr, idat, iend]); +} + +function crc32(data: any): number { + return zlib.crc32(data) >>> 0; // Convert to unsigned 32-bit +} diff --git a/packages/uhk-agent/src/util/index.ts b/packages/uhk-agent/src/util/index.ts index 63ed643287f..f61360a9881 100644 --- a/packages/uhk-agent/src/util/index.ts +++ b/packages/uhk-agent/src/util/index.ts @@ -1,7 +1,9 @@ export * from './backup-user-configuration'; +export * from './capture-oled'; export * from './command-line'; export * from './copy-smart-macro-doc-to-webserver'; export * from './copy-smart-macro-loading-html'; +export * from './create-png'; export * from './delete-user-config-history'; export * from './get-default-firmware-path'; export * from './get-smart-macro-doc-root-path'; diff --git a/packages/uhk-common/src/models/command-line-args.ts b/packages/uhk-common/src/models/command-line-args.ts index ae84476ee47..c7f364d22dc 100644 --- a/packages/uhk-common/src/models/command-line-args.ts +++ b/packages/uhk-common/src/models/command-line-args.ts @@ -21,6 +21,11 @@ export interface DeviceIdentifier { } export interface CommandLineArgs extends DeviceIdentifier { + /** + * Capture UHK 80 OLED state as png + */ + 'capture-oled'?: string; + /** * Allow Developer Tools menu */ diff --git a/packages/uhk-common/src/models/uhk-products.ts b/packages/uhk-common/src/models/uhk-products.ts index 6b253d1ff8d..803b7947169 100644 --- a/packages/uhk-common/src/models/uhk-products.ts +++ b/packages/uhk-common/src/models/uhk-products.ts @@ -4,6 +4,9 @@ import { ModuleSlotToId } from './module-slot-id.js'; import { UHK_DEVICE_IDS, UHK_DEVICE_IDS_TYPE } from './uhk-device-ids.js'; import { UHK_MODULE_IDS, UHK_MODULE_IDS_TYPE } from './uhk-module-ids.js'; +export const OLED_DISPLAY_HEIGHT = 64; +export const OLED_DISPLAY_WIDTH = 256; + export const UHK_VENDOR_ID_OLD = 0x1D50; // decimal 7504 export const UHK_VENDOR_ID = 0x37A8; // decimal 14248 export const UHK_BLE_MIN_PRODUCT_iD = 0x8000; // decimal 32768 diff --git a/packages/uhk-usb/src/constants.ts b/packages/uhk-usb/src/constants.ts index adb22f1613c..0dc38694d0d 100644 --- a/packages/uhk-usb/src/constants.ts +++ b/packages/uhk-usb/src/constants.ts @@ -34,6 +34,8 @@ export enum UsbCommand { IsPaired = 0x1b, EnterPairingMode = 0x1c, EraseBleSettings = 0x1d, + ExecShellCommand = 0x1e, + ReadOled = 0x1f, } export enum EepromOperation { diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index f02d7798fd7..b0c2bb8492c 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -18,6 +18,8 @@ import { LogService, ModuleSlotToId, ModuleVersionInfo, + OLED_DISPLAY_HEIGHT, + OLED_DISPLAY_WIDTH, UhkBuffer, UhkDeviceProduct, UhkModule, @@ -73,6 +75,28 @@ export class UhkOperations { private device: UhkHidDevice) { } + async readOled(): Promise { + this.logService.usbOps('[UhkHidDevice] USB[T]: Capture oled.'); + let offset = 0 + let oledData = Buffer.alloc(0); + + while (true) { + const transfer = Buffer.from([UsbCommand.ReadOled, offset & 0xff, offset >> 8]); + const readBuffer = await this.device.write(transfer); + const dataLength = readBuffer.readUInt8(1) + + if (dataLength === 0) { + break; + } + + oledData = Buffer.concat([oledData, Buffer.from(readBuffer.slice(2, dataLength + 2))]); + + offset += dataLength; + } + + return oledData + } + public async eraseBleSettings(): Promise { this.logService.usbOps('[UhkHidDevice] USB[T]: Erase BLE settings.'); const transfer = Buffer.from([UsbCommand.EraseBleSettings]);