From 0beefad5a1254bf319d95229172c7eeb695d72e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Mon, 3 Aug 2026 22:09:21 +0200 Subject: [PATCH 1/3] fix: stop status buffer from splitting and dropping binding lines Drain the full firmware status buffer instead of stopping after 20 USB transfers, avoid re-reading on every connection-state flicker, and keep binding-site lines in the error panel (#3025). Co-authored-by: Cursor --- .../uhk-agent/src/services/device.service.ts | 10 ++++- packages/uhk-usb/src/uhk-operations.ts | 38 ++++++++++++++----- .../src/app/util/status-buffer-parser.ts | 7 +++- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index ea145994fbc..7790b6bc7c2 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -1237,6 +1237,9 @@ export class DeviceService { const state = await this.device.getDeviceConnectionStateAsync(); if (!isEqual(state, this.savedState)) { const newState = cloneDeep(state); + const becameAvailable = state.hasPermission + && state.communicationInterfaceAvailable + && !(this.savedState?.hasPermission && this.savedState?.communicationInterfaceAvailable); if (state.hasPermission && state.communicationInterfaceAvailable) { state.hardwareModules = await this.getHardwareModules(false); @@ -1279,7 +1282,12 @@ export class DeviceService { await this.dongleZephyrLogService.disable(); } - this._checkStatusBuffer = true; + // Only pull the status buffer when the device newly becomes available. + // Re-reading on every connection-state field change drains leftovers from + // a previous partial read (or an empty buffer) and overwrites the UI (#3025). + if (becameAvailable) { + this._checkStatusBuffer = true; + } } else { deviceProtocolVersion = undefined; state.hardwareModules = { diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 76315f2f894..5f4f451ad4f 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -812,28 +812,46 @@ export class UhkOperations { return convertSlaveI2cErrorBuffer(responseBuffer, slaveId); } - public async getVariable(variableId: UsbVariables, iteration: number = 0): Promise { - this.logService.usbOps(`[DeviceOperation] USB[T]: get variable: ${UsbVariables[variableId]}. Iteration: ${iteration}`); - const buffer = Buffer.from([UsbCommand.GetVariable, variableId]); - const responseBuffer = await this.device.write(buffer); - + public async getVariable(variableId: UsbVariables): Promise { if (variableId === UsbVariables.statusBuffer || variableId === UsbVariables.ShellBuffer) { - let message = readUhkResponseAs0EndString(UhkBuffer.fromArray(convertBufferToIntArray(responseBuffer))); - this.logService.misc(`[DeviceOperation] status buffer segment: ${message}`); - if (message.length === responseBuffer.length - 1 && iteration < 20) { - message += await this.getVariable(variableId, iteration + 1); + // Firmware status buffer is STATUS_BUFFER_MAX_LENGTH (3000); shell buffer is 2048. + // Each USB transfer returns at most (report length - 1) payload bytes (~62). + // The previous hard cap of 20 iterations (~1260 bytes) left unread data in the device, + // which the next poll then showed alone and overwrote the UI (#3025). + const maxIterations = 100; + let message = ''; + + for (let iteration = 0; iteration < maxIterations; iteration++) { + this.logService.usbOps(`[DeviceOperation] USB[T]: get variable: ${UsbVariables[variableId]}. Iteration: ${iteration}`); + const buffer = Buffer.from([UsbCommand.GetVariable, variableId]); + const responseBuffer = await this.device.write(buffer); + const segment = readUhkResponseAs0EndString(UhkBuffer.fromArray(convertBufferToIntArray(responseBuffer))); + this.logService.misc(`[DeviceOperation] status buffer segment: ${segment}`); + message += segment; + + if (segment.length !== responseBuffer.length - 1) { + break; + } + + if (iteration === maxIterations - 1) { + this.logService.error(`[DeviceOperation] ${UsbVariables[variableId]} truncated after ${maxIterations} USB transfers`); + } } // The shell buffer carries a raw VT100 stream (colors, cursor control) that must be // forwarded verbatim to the terminal emulator. Only the macro status buffer gets the // dedup/reorder normalization. - if (iteration === 0 && variableId === UsbVariables.statusBuffer) { + if (variableId === UsbVariables.statusBuffer) { message = normalizeStatusBuffer(message); } return message; } + this.logService.usbOps(`[DeviceOperation] USB[T]: get variable: ${UsbVariables[variableId]}`); + const buffer = Buffer.from([UsbCommand.GetVariable, variableId]); + const responseBuffer = await this.device.write(buffer); + return responseBuffer[1]; } diff --git a/packages/uhk-web/src/app/util/status-buffer-parser.ts b/packages/uhk-web/src/app/util/status-buffer-parser.ts index 58f4edd10f1..9b0dc4d3cca 100644 --- a/packages/uhk-web/src/app/util/status-buffer-parser.ts +++ b/packages/uhk-web/src/app/util/status-buffer-parser.ts @@ -68,6 +68,11 @@ function transformToErrorBlock(macros: Macro[], block: string): string { const url = `#/macro/${macro.id}?actionIndex=${macroActionIndex}&lineNr=${lineNr}&columnNr=${columnNr}&inlineEdit=true`; const newLine2 = `${escapeHtml(line1Result[1])}${escapeHtml(line1Result[2])}`; + // Keep binding-site lines and nested location-stack lines (firmware may emit more than 3). + const extraLines = lines + .slice(3) + .map(line => escapeHtml(line)) + .join('\n'); - return `${escapeHtml(lines[0])}\n${newLine2}\n${escapeHtml(lines[2])}\n`; + return `${escapeHtml(lines[0])}\n${newLine2}\n${escapeHtml(lines[2])}\n${extraLines}`; } From f3ed789ec47d65d6031100de8a468489fdd0d0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Wed, 5 Aug 2026 13:56:08 +0200 Subject: [PATCH 2/3] refactor: split the UhkOperations.getVariable for better readability --- packages/uhk-usb/src/uhk-operations.ts | 70 ++++++++++++++------------ 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 5f4f451ad4f..a06d544db0e 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -814,38 +814,7 @@ export class UhkOperations { public async getVariable(variableId: UsbVariables): Promise { if (variableId === UsbVariables.statusBuffer || variableId === UsbVariables.ShellBuffer) { - // Firmware status buffer is STATUS_BUFFER_MAX_LENGTH (3000); shell buffer is 2048. - // Each USB transfer returns at most (report length - 1) payload bytes (~62). - // The previous hard cap of 20 iterations (~1260 bytes) left unread data in the device, - // which the next poll then showed alone and overwrote the UI (#3025). - const maxIterations = 100; - let message = ''; - - for (let iteration = 0; iteration < maxIterations; iteration++) { - this.logService.usbOps(`[DeviceOperation] USB[T]: get variable: ${UsbVariables[variableId]}. Iteration: ${iteration}`); - const buffer = Buffer.from([UsbCommand.GetVariable, variableId]); - const responseBuffer = await this.device.write(buffer); - const segment = readUhkResponseAs0EndString(UhkBuffer.fromArray(convertBufferToIntArray(responseBuffer))); - this.logService.misc(`[DeviceOperation] status buffer segment: ${segment}`); - message += segment; - - if (segment.length !== responseBuffer.length - 1) { - break; - } - - if (iteration === maxIterations - 1) { - this.logService.error(`[DeviceOperation] ${UsbVariables[variableId]} truncated after ${maxIterations} USB transfers`); - } - } - - // The shell buffer carries a raw VT100 stream (colors, cursor control) that must be - // forwarded verbatim to the terminal emulator. Only the macro status buffer gets the - // dedup/reorder normalization. - if (variableId === UsbVariables.statusBuffer) { - message = normalizeStatusBuffer(message); - } - - return message; + return this.getVariableWithIteration(variableId); } this.logService.usbOps(`[DeviceOperation] USB[T]: get variable: ${UsbVariables[variableId]}`); @@ -855,6 +824,43 @@ export class UhkOperations { return responseBuffer[1]; } + private async getVariableWithIteration(variableId: UsbVariables): Promise { + // Firmware status buffer is STATUS_BUFFER_MAX_LENGTH (3000); shell buffer is 2048. + // Each USB transfer returns at most (report length - 1) payload bytes (~62). + // The buffers are NUL terminated strings. + // The maxIterations is a safeguard against infinite loops. + const maxIterations = 100; + let message = ''; + + for (let iteration = 0; iteration < maxIterations; iteration++) { + this.logService.usbOps(`[DeviceOperation] USB[T]: get variable: ${UsbVariables[variableId]}. Iteration: ${iteration}`); + const buffer = Buffer.from([UsbCommand.GetVariable, variableId]); + const responseBuffer = await this.device.write(buffer); + const segment = readUhkResponseAs0EndString(UhkBuffer.fromArray(convertBufferToIntArray(responseBuffer))); + this.logService.misc(`[DeviceOperation] status buffer segment: ${segment}`); + message += segment; + + // The content of the variable is a NUL terminated string. + // When the segment length is not equal to the buffer length - 1, the buffer is complete. + if (segment.length !== responseBuffer.length - 1) { + break; + } + + if (iteration === maxIterations) { + this.logService.error(`[DeviceOperation] ${UsbVariables[variableId]} truncated after ${maxIterations} USB transfers`); + } + } + + // The shell buffer carries a raw VT100 stream (colors, cursor control) that must be + // forwarded verbatim to the terminal emulator. Only the macro status buffer gets the + // dedup/reorder normalization. + if (variableId === UsbVariables.statusBuffer) { + message = normalizeStatusBuffer(message); + } + + return message; + } + public async pairToDongle(dongle: UhkHidDevice) : Promise { const deviceBleAddress = await this.device.getBleAddress(); this.logService.misc('[DeviceOperation] Device BLE address: ', convertBleAddressArrayToString(deviceBleAddress)); From 7c05f962f17f29fd97b24b36268c5809677f260f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Wed, 5 Aug 2026 15:13:39 +0200 Subject: [PATCH 3/3] fix: revert becameAvailable logic --- packages/uhk-agent/src/services/device.service.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index 7790b6bc7c2..ea145994fbc 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -1237,9 +1237,6 @@ export class DeviceService { const state = await this.device.getDeviceConnectionStateAsync(); if (!isEqual(state, this.savedState)) { const newState = cloneDeep(state); - const becameAvailable = state.hasPermission - && state.communicationInterfaceAvailable - && !(this.savedState?.hasPermission && this.savedState?.communicationInterfaceAvailable); if (state.hasPermission && state.communicationInterfaceAvailable) { state.hardwareModules = await this.getHardwareModules(false); @@ -1282,12 +1279,7 @@ export class DeviceService { await this.dongleZephyrLogService.disable(); } - // Only pull the status buffer when the device newly becomes available. - // Re-reading on every connection-state field change drains leftovers from - // a previous partial read (or an empty buffer) and overwrites the UI (#3025). - if (becameAvailable) { - this._checkStatusBuffer = true; - } + this._checkStatusBuffer = true; } else { deviceProtocolVersion = undefined; state.hardwareModules = {