Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/uhk-agent/src/electron-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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']) {
Expand Down
59 changes: 59 additions & 0 deletions packages/uhk-agent/src/util/capture-oled.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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 <oledData.length; i++) {
const greyScale = oledData.readUInt8(i)

pixelData[pixelIndex] = greyScale; // R
pixelData[pixelIndex + 1] = greyScale; // G
pixelData[pixelIndex + 2] = greyScale; // B

pixelIndex += 3;
}

const pngBuffer = createPNG(OLED_DISPLAY_WIDTH, OLED_DISPLAY_HEIGHT, pixelData);
const outputPath = options.commandLineArgs['capture-oled'];
await fs.writeFile(outputPath, pngBuffer);
options.logger.misc(`[captureOled] capturing finished successfully.`);
process.exit(0);
}
catch (error) {
options.logger.error(error.message);
process.exit(-1);
}
}
6 changes: 6 additions & 0 deletions packages/uhk-agent/src/util/command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CommandLineArgs } from 'uhk-common';
import { assertCommandLineOptions } from 'uhk-usb';

const optionDefinitions: commandLineArgs.OptionDefinition[] = [
{ name: 'capture-oled', type: String },
{ name: 'devtools', type: Boolean },
{ name: 'disable-agent-update-protection', type: Boolean },
{ name: 'error-simulation', type: String },
Expand Down Expand Up @@ -38,6 +39,11 @@ const sections: commandLineUsage.Section[] = [
{
header: 'Options',
optionList: [
{
name: 'capture-oled',
description: 'Capture UHK 80 OLED content into the given path as png',
type: String
},
{
name: 'devtools',
description: 'Allow the Developer Tools menu.',
Expand Down
59 changes: 59 additions & 0 deletions packages/uhk-agent/src/util/create-png.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import zlib from 'node:zlib';

export function createPNG(width: number, height: number, pixelData: Buffer<ArrayBuffer>): 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
}
2 changes: 2 additions & 0 deletions packages/uhk-agent/src/util/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
5 changes: 5 additions & 0 deletions packages/uhk-common/src/models/command-line-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/uhk-common/src/models/uhk-products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/uhk-usb/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export enum UsbCommand {
IsPaired = 0x1b,
EnterPairingMode = 0x1c,
EraseBleSettings = 0x1d,
ExecShellCommand = 0x1e,
ReadOled = 0x1f,
}

export enum EepromOperation {
Expand Down
24 changes: 24 additions & 0 deletions packages/uhk-usb/src/uhk-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
LogService,
ModuleSlotToId,
ModuleVersionInfo,
OLED_DISPLAY_HEIGHT,
OLED_DISPLAY_WIDTH,
UhkBuffer,
UhkDeviceProduct,
UhkModule,
Expand Down Expand Up @@ -73,6 +75,28 @@ export class UhkOperations {
private device: UhkHidDevice) {
}

async readOled(): Promise<Buffer> {
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<void> {
this.logService.usbOps('[UhkHidDevice] USB[T]: Erase BLE settings.');
const transfer = Buffer.from([UsbCommand.EraseBleSettings]);
Expand Down
Loading