Skip to content

Commit b1287ce

Browse files
committed
feat: capture OLED as png
1 parent d99d0f6 commit b1287ce

9 files changed

Lines changed: 170 additions & 0 deletions

File tree

packages/uhk-agent/src/electron-main.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import isDev from 'electron-is-dev';
2525
import { setMenu } from './electron-menu';
2626
import { loadWindowState, saveWindowState } from './util/window';
2727
import {
28+
captureOled,
2829
getWindowBackgroundColor,
2930
options,
3031
cliUsage,
@@ -171,6 +172,12 @@ async function createWindow() {
171172

172173
if (isSecondInstance) {
173174
app.quit();
175+
} else if (options['capture-oled']) {
176+
captureOled({
177+
logger,
178+
commandLineArgs: options,
179+
uhkOperations,
180+
})
174181
} else if (options['print-hardware-configuration']) {
175182
printHardwareConfiguration({ logger, uhkOperations })
176183
} else if (options['print-status-buffer']) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import process from 'node:process';
2+
import fs from 'node:fs/promises';
3+
4+
import {
5+
CommandLineArgs,
6+
OLED_DISPLAY_HEIGHT,
7+
OLED_DISPLAY_WIDTH,
8+
UHK_80_DEVICE,
9+
} from 'uhk-common';
10+
import { getCurrentUhkDeviceProduct } from 'uhk-usb';
11+
import { UhkOperations } from 'uhk-usb';
12+
13+
import { ElectronLogService } from '../services/logger.service';
14+
import { createPNG } from './create-png';
15+
16+
export interface CaptureOledOptions {
17+
logger: ElectronLogService;
18+
uhkOperations: UhkOperations;
19+
commandLineArgs: CommandLineArgs;
20+
}
21+
22+
export async function captureOled(options: CaptureOledOptions): Promise<void> {
23+
try {
24+
const device = await getCurrentUhkDeviceProduct(options.commandLineArgs);
25+
26+
if (!device) {
27+
options.logger.error('Cannot detect UHK device');
28+
process.exit(-1);
29+
}
30+
else if (device.id !== UHK_80_DEVICE.id) {
31+
options.logger.error(`${device.name} does not have OLED panel.`);
32+
process.exit(-1);
33+
}
34+
35+
const oledData = await options.uhkOperations.readOled()
36+
console.log('OledLength ', oledData.length)
37+
console.log('dimension ', OLED_DISPLAY_WIDTH * OLED_DISPLAY_HEIGHT)
38+
39+
const pixelData = Buffer.alloc(OLED_DISPLAY_WIDTH * OLED_DISPLAY_HEIGHT * 3);
40+
41+
let pixelIndex = 0;
42+
for (let i = 0; i <oledData.length; i++) {
43+
const greyScale = oledData.readUInt8(i)
44+
45+
pixelData[pixelIndex] = greyScale; // R
46+
pixelData[pixelIndex + 1] = greyScale; // G
47+
pixelData[pixelIndex + 2] = greyScale; // B
48+
49+
pixelIndex += 3;
50+
}
51+
52+
const pngBuffer = createPNG(OLED_DISPLAY_WIDTH, OLED_DISPLAY_HEIGHT, pixelData);
53+
const outputPath = options.commandLineArgs['capture-oled'];
54+
await fs.writeFile(outputPath, pngBuffer);
55+
options.logger.misc(`[captureOled] capturing finished successfully.`);
56+
process.exit(0);
57+
}
58+
catch (error) {
59+
options.logger.error(error.message);
60+
process.exit(-1);
61+
}
62+
}

packages/uhk-agent/src/util/command-line.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CommandLineArgs } from 'uhk-common';
44
import { assertCommandLineOptions } from 'uhk-usb';
55

66
const optionDefinitions: commandLineArgs.OptionDefinition[] = [
7+
{ name: 'capture-oled', type: String },
78
{ name: 'devtools', type: Boolean },
89
{ name: 'disable-agent-update-protection', type: Boolean },
910
{ name: 'error-simulation', type: String },
@@ -38,6 +39,11 @@ const sections: commandLineUsage.Section[] = [
3839
{
3940
header: 'Options',
4041
optionList: [
42+
{
43+
name: 'capture-oled',
44+
description: 'Capture UHK 80 OLED content into the given path as png',
45+
type: String
46+
},
4147
{
4248
name: 'devtools',
4349
description: 'Allow the Developer Tools menu.',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import zlib from 'node:zlib';
2+
3+
export function createPNG(width: number, height: number, pixelData: Buffer<ArrayBuffer>): Buffer {
4+
// PNG signature
5+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
6+
7+
// IHDR chunk
8+
const ihdr = Buffer.alloc(25);
9+
ihdr.writeUInt32BE(13, 0); // Length
10+
ihdr.write('IHDR', 4);
11+
ihdr.writeUInt32BE(width, 8);
12+
ihdr.writeUInt32BE(height, 12);
13+
ihdr.writeUInt8(8, 16); // Bit depth
14+
ihdr.writeUInt8(2, 17); // Color type (RGB)
15+
ihdr.writeUInt8(0, 18); // Compression
16+
ihdr.writeUInt8(0, 19); // Filter
17+
ihdr.writeUInt8(0, 20); // Interlace
18+
19+
// Calculate CRC for IHDR
20+
const ihdrCrc = crc32(ihdr.slice(4, 21));
21+
ihdr.writeUInt32BE(ihdrCrc, 21);
22+
23+
// Prepare image data with filter bytes
24+
const scanlineLength = width * 3 + 1; // 3 bytes per pixel + 1 filter byte
25+
const imageData = Buffer.alloc(height * scanlineLength);
26+
27+
for (let y = 0; y < height; y++) {
28+
const offset = y * scanlineLength;
29+
imageData[offset] = 0; // Filter type: None
30+
31+
for (let x = 0; x < width; x++) {
32+
const pixelOffset = offset + 1 + x * 3;
33+
const dataOffset = (y * width + x) * 3;
34+
35+
imageData[pixelOffset] = pixelData[dataOffset]; // R
36+
imageData[pixelOffset + 1] = pixelData[dataOffset + 1]; // G
37+
imageData[pixelOffset + 2] = pixelData[dataOffset + 2]; // B
38+
}
39+
}
40+
41+
const compressed = zlib.deflateSync(imageData);
42+
43+
// IDAT chunk
44+
const idat = Buffer.alloc(compressed.length + 12);
45+
idat.writeUInt32BE(compressed.length, 0);
46+
idat.write('IDAT', 4);
47+
compressed.copy(idat, 8);
48+
const idatCrc = crc32(idat.slice(4, 8 + compressed.length));
49+
idat.writeUInt32BE(idatCrc, 8 + compressed.length);
50+
51+
// IEND chunk
52+
const iend = Buffer.from([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]);
53+
54+
return Buffer.concat([signature, ihdr, idat, iend]);
55+
}
56+
57+
function crc32(data: any): number {
58+
return zlib.crc32(data) >>> 0; // Convert to unsigned 32-bit
59+
}

packages/uhk-agent/src/util/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export * from './backup-user-configuration';
2+
export * from './capture-oled';
23
export * from './command-line';
34
export * from './copy-smart-macro-doc-to-webserver';
45
export * from './copy-smart-macro-loading-html';
6+
export * from './create-png';
57
export * from './delete-user-config-history';
68
export * from './get-default-firmware-path';
79
export * from './get-smart-macro-doc-root-path';

packages/uhk-common/src/models/command-line-args.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export interface DeviceIdentifier {
2121
}
2222

2323
export interface CommandLineArgs extends DeviceIdentifier {
24+
/**
25+
* Capture UHK 80 OLED state as png
26+
*/
27+
'capture-oled'?: string;
28+
2429
/**
2530
* Allow Developer Tools menu
2631
*/

packages/uhk-common/src/models/uhk-products.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { ModuleSlotToId } from './module-slot-id.js';
44
import { UHK_DEVICE_IDS, UHK_DEVICE_IDS_TYPE } from './uhk-device-ids.js';
55
import { UHK_MODULE_IDS, UHK_MODULE_IDS_TYPE } from './uhk-module-ids.js';
66

7+
export const OLED_DISPLAY_HEIGHT = 64;
8+
export const OLED_DISPLAY_WIDTH = 256;
9+
710
export const UHK_VENDOR_ID_OLD = 0x1D50; // decimal 7504
811
export const UHK_VENDOR_ID = 0x37A8; // decimal 14248
912
export const UHK_BLE_MIN_PRODUCT_iD = 0x8000; // decimal 32768

packages/uhk-usb/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export enum UsbCommand {
3434
IsPaired = 0x1b,
3535
EnterPairingMode = 0x1c,
3636
EraseBleSettings = 0x1d,
37+
ExecShellCommand = 0x1e,
38+
ReadOled = 0x1f,
3739
}
3840

3941
export enum EepromOperation {

packages/uhk-usb/src/uhk-operations.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
LogService,
1919
ModuleSlotToId,
2020
ModuleVersionInfo,
21+
OLED_DISPLAY_HEIGHT,
22+
OLED_DISPLAY_WIDTH,
2123
UhkBuffer,
2224
UhkDeviceProduct,
2325
UhkModule,
@@ -73,6 +75,28 @@ export class UhkOperations {
7375
private device: UhkHidDevice) {
7476
}
7577

78+
async readOled(): Promise<Buffer> {
79+
this.logService.usbOps('[UhkHidDevice] USB[T]: Capture oled.');
80+
let offset = 0
81+
let oledData = Buffer.alloc(0);
82+
83+
while (true) {
84+
const transfer = Buffer.from([UsbCommand.ReadOled, offset & 0xff, offset >> 8]);
85+
const readBuffer = await this.device.write(transfer);
86+
const dataLength = readBuffer.readUInt8(1)
87+
88+
if (dataLength === 0) {
89+
break;
90+
}
91+
92+
oledData = Buffer.concat([oledData, Buffer.from(readBuffer.slice(2, dataLength + 2))]);
93+
94+
offset += dataLength;
95+
}
96+
97+
return oledData
98+
}
99+
76100
public async eraseBleSettings(): Promise<void> {
77101
this.logService.usbOps('[UhkHidDevice] USB[T]: Erase BLE settings.');
78102
const transfer = Buffer.from([UsbCommand.EraseBleSettings]);

0 commit comments

Comments
 (0)