From 226cc1e94b6300073d7317127d8e9555de547579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Sun, 22 Mar 2026 19:06:05 +0100 Subject: [PATCH 1/2] feat: command prompt for zephyr log --- .../uhk-agent/src/services/device.service.ts | 27 ++++++++++++++ .../src/services/zephyr-log.service.ts | 31 ++++++++++++++++ packages/uhk-common/src/util/ipcEvents.ts | 3 ++ packages/uhk-usb/src/uhk-operations.ts | 14 ++++++++ .../advanced-settings.page.component.html | 36 +++++++++++++++++-- .../advanced-settings.page.component.ts | 33 ++++++++++++++--- .../app/components/xterm/xterm.component.html | 2 +- .../app/components/xterm/xterm.component.scss | 8 +++-- .../app/services/device-renderer.service.ts | 12 +++++++ .../uhk-web/src/app/store/actions/device.ts | 24 +++++++++++++ .../uhk-web/src/app/store/effects/device.ts | 33 +++++++++++++++++ 11 files changed, 213 insertions(+), 10 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index b2e69809e57..d4596092139 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -125,6 +125,7 @@ export class DeviceService { currentDeviceFn: getCurrentUhkDongleHID, logService: this.logService, ipcEvents: { + execShellCommand: IpcEvents.device.execShellCommandOnDongle, isZephyrLoggingEnabled: IpcEvents.device.isDongleZephyrLoggingEnabled, isZephyrLoggingEnabledReply: IpcEvents.device.isDongleZephyrLoggingEnabledReply, toggleZephyrLogging: IpcEvents.device.toggleDongleZephyrLogging, @@ -138,6 +139,7 @@ export class DeviceService { currentDeviceFn: getCurrenUhk80LeftHID, logService: this.logService, ipcEvents: { + execShellCommand: IpcEvents.device.execShellCommandOnLeftHalf, isZephyrLoggingEnabled: IpcEvents.device.isLeftHalfZephyrLoggingEnabled, isZephyrLoggingEnabledReply: IpcEvents.device.isLeftHalfZephyrLoggingEnabledReply, toggleZephyrLogging: IpcEvents.device.toggleLeftHalfZephyrLogging, @@ -195,6 +197,15 @@ export class DeviceService { }); }); + ipcMain.on(IpcEvents.device.execShellCommandOnRightHalf, (...args) => { + this.queueManager.add({ + method: this.execShellCommand, + bind: this, + params: args, + asynchronous: true + }); + }); + ipcMain.on(IpcEvents.device.toggleI2cDebugging, this.toggleI2cDebugging.bind(this)); ipcMain.on(IpcEvents.device.isRightHalfZephyrLoggingEnabled, (...args) => { @@ -973,6 +984,22 @@ export class DeviceService { event.sender.send(IpcEvents.device.eraseBleSettingsReply, response); } + public async execShellCommand(_: Electron.IpcMainEvent, [command]): Promise { + this.logService.misc(`[DeviceService] execute shell command: ${command}`); + + try { + await this.stopPollUhkDevice(); + await this.operations.execShellCommand(command); + this.logService.misc('[DeviceService] execute shell command success'); + } + catch(error) { + this.logService.error('[DeviceService] execute shell command failed', error); + } + finally { + this.startPollUhkDevice(); + } + } + public async startDonglePairing(event: Electron.IpcMainEvent): Promise { this.logService.misc('[DeviceService] start Dongle pairing'); try { diff --git a/packages/uhk-agent/src/services/zephyr-log.service.ts b/packages/uhk-agent/src/services/zephyr-log.service.ts index 1172f5ac0d1..4a56eed7a12 100644 --- a/packages/uhk-agent/src/services/zephyr-log.service.ts +++ b/packages/uhk-agent/src/services/zephyr-log.service.ts @@ -11,6 +11,7 @@ export interface ZephyrLogServiceOptions { currentDeviceFn: typeof getCurrenUhk80LeftHID | typeof getCurrentUhkDongleHID; logService: LogService; ipcEvents: { + execShellCommand: string; isZephyrLoggingEnabled: string; isZephyrLoggingEnabledReply: string; toggleZephyrLogging: string; @@ -31,6 +32,15 @@ export class ZephyrLogService { private operationLimiter = pLimit(1); constructor(private options: ZephyrLogServiceOptions) { + ipcMain.on(options.ipcEvents.execShellCommand, (...args) => { + this.queueManager.add({ + method: this.execShellCommand, + bind: this, + params: args, + asynchronous: true + }); + }); + ipcMain.on(options.ipcEvents.isZephyrLoggingEnabled, (...args) => { this.queueManager.add({ method: this.isZephyrLoggingEnabled, @@ -81,6 +91,27 @@ export class ZephyrLogService { this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Disabled`); } + private async execShellCommand(_: Electron.IpcMainEvent, [command]): Promise { + try { + await this.pauseLogging(); + + const operations = await this.getOperations(); + if (!operations) { + const logEntry: ZephyrLogEntry = { + log: "Device is not connected. Can't execute shell command", + level: 'error', + device: this.options.uhkDeviceProduct.logName, + } + this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry) + return; + } + await operations.execShellCommand(command); + } + finally { + await this.resumeLogging(); + } + } + private async getOperations(logEarlierInited = true): Promise { if (logEarlierInited) { this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] getOperations`); diff --git a/packages/uhk-common/src/util/ipcEvents.ts b/packages/uhk-common/src/util/ipcEvents.ts index 386a2cd303e..61916764982 100644 --- a/packages/uhk-common/src/util/ipcEvents.ts +++ b/packages/uhk-common/src/util/ipcEvents.ts @@ -31,6 +31,9 @@ export class Device { public static readonly dongleVersionInfoLoaded = 'device-dongle-version-info-loaded'; public static readonly eraseBleSettings = 'device-erase-ble-settings'; public static readonly eraseBleSettingsReply = 'device-erase-ble-settings-reply'; + public static readonly execShellCommandOnDongle = 'device-exec-shell-command-on-dongle'; + public static readonly execShellCommandOnLeftHalf = 'device-exec-shell-command-on-left-half'; + public static readonly execShellCommandOnRightHalf = 'device-exec-shell-command-on-right-half'; public static readonly hardwareModulesLoaded = 'device-hardware-modules-loaded'; public static readonly isDongleZephyrLoggingEnabled = 'device-is-dongle-zephyr-logging-enabled'; public static readonly isDongleZephyrLoggingEnabledReply = 'device-is-dongle-zephyr-logging-enabled-reply'; diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 190b1e163f8..0d42e2a1bf3 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -936,4 +936,18 @@ export class UhkOperations { await this.device.write(buffer); } + + public async execShellCommand(cmd: string): Promise { + this.logService.usbOps('[DeviceOperation] USB[T]: Execute Shell Command'); + const b1 = Buffer.from([UsbCommand.ExecShellCommand]); + const b2 = Buffer.from(cmd); + const b0 = Buffer.from([0x00]); + const buffer = Buffer.concat([b1, b2, b0]); + + if (buffer.length > MAX_USB_PAYLOAD_SIZE) { + throw new Error('Shel command is too long. At most 61 characters are supported.') + } + + await this.device.write(buffer); + } } diff --git a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html index e713b44383d..f5cd51252b2 100644 --- a/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html +++ b/packages/uhk-web/src/app/components/device/advanced-settings/advanced-settings.page.component.html @@ -79,7 +79,7 @@

-
- + +
diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html new file mode 100644 index 00000000000..c74d5a40087 --- /dev/null +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.html @@ -0,0 +1 @@ +
diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss new file mode 100644 index 00000000000..4cfd8c1958a --- /dev/null +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.scss @@ -0,0 +1,15 @@ +:host { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + height: 100%; +} + +.zephyr-terminal { + flex: 1; + min-height: 0; + background-color: var(--color-xterm-bg); + border: 1px solid var(--color-xterm-border); + padding: 4px; +} diff --git a/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts new file mode 100644 index 00000000000..8c63071cf37 --- /dev/null +++ b/packages/uhk-web/src/app/components/zephyr-terminal/zephyr-terminal.component.ts @@ -0,0 +1,87 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + OnDestroy, + ViewChild, +} from '@angular/core'; +import { Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { FitAddon } from '@xterm/addon-fit'; +import { Terminal } from '@xterm/xterm'; +import { Subscription } from 'rxjs'; +import { UHK_80_DEVICE } from 'uhk-common'; + +import { AppState } from '../../store'; +import { ActionTypes as AdvancedSettingsActionTypes, ZephyrLogAction } from '../../store/actions/advance-settings.action'; +import { ExecShellCommandOnRightHalfAction } from '../../store/actions/device'; + +/** + * Proof-of-concept VT100 terminal for the UHK 80 right half. + * + * Output: the raw shell buffer chunks emitted by the right-half poller (device === UHK 80 right) + * are written verbatim to xterm.js, so colors and cursor control render. + * + * Input: every keystroke (incl. ESC sequences for arrows, Tab, Ctrl-C, Enter) is forwarded to the + * device through the ExecShellCommand byte channel. Echo and history are produced by the firmware's + * shell, so they only light up once the firmware injects these bytes into the interactive shell + * input. NUL (0x00) is the only byte the transport can't carry, and VT100 treats NUL as ignorable + * fill, so nothing meaningful is lost. + */ +@Component({ + selector: 'zephyr-terminal', + standalone: false, + templateUrl: './zephyr-terminal.component.html', + styleUrls: ['./zephyr-terminal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ZephyrTerminalComponent implements AfterViewInit, OnDestroy { + @ViewChild('terminal', { static: true }) terminalElement: ElementRef; + + private terminal: Terminal; + private fitAddon: FitAddon; + private logSubscription: Subscription; + private resizeObserver: ResizeObserver; + + constructor( + private store: Store, + private actions$: Actions, + ) {} + + ngAfterViewInit(): void { + this.terminal = new Terminal({ + convertEol: false, + fontFamily: 'monospace', + cursorBlink: true, + scrollback: 10000, + }); + this.fitAddon = new FitAddon(); + this.terminal.loadAddon(this.fitAddon); + this.terminal.open(this.terminalElement.nativeElement); + this.fitAddon.fit(); + + // Forward every keystroke (raw bytes, incl. ESC sequences) to the right half. + this.terminal.onData((data: string) => { + this.store.dispatch(new ExecShellCommandOnRightHalfAction(data)); + }); + + // Write raw shell output coming from the right half straight into the terminal. + this.logSubscription = this.actions$ + .pipe(ofType(AdvancedSettingsActionTypes.zephyrLog)) + .subscribe((action: ZephyrLogAction) => { + if (action.payload.device === UHK_80_DEVICE.logName) { + this.terminal.write(action.payload.log); + } + }); + + this.resizeObserver = new ResizeObserver(() => this.fitAddon.fit()); + this.resizeObserver.observe(this.terminalElement.nativeElement); + } + + ngOnDestroy(): void { + this.logSubscription?.unsubscribe(); + this.resizeObserver?.disconnect(); + this.terminal?.dispose(); + } +} diff --git a/packages/uhk-web/src/app/shared.module.ts b/packages/uhk-web/src/app/shared.module.ts index a4fbbe3bf1f..e2c7d547446 100644 --- a/packages/uhk-web/src/app/shared.module.ts +++ b/packages/uhk-web/src/app/shared.module.ts @@ -147,6 +147,7 @@ import { UpdateAgentPageComponent } from './pages/update-agent.page'; import { UpdateFirmwarePageComponent } from './pages/update-firmware.page'; import { UhkDeviceLoadingGuard } from './services/uhk-device-loading.guard'; import { XtermComponent } from './components/xterm/xterm.component'; +import { ZephyrTerminalComponent } from './components/zephyr-terminal/zephyr-terminal.component'; import { SliderWrapperComponent } from './components/slider-wrapper/slider-wrapper.component'; import { EditableTextComponent } from './components/editable-text/editable-text.component'; import { Autofocus } from './directives/autofocus/autofocus.directive'; @@ -271,6 +272,7 @@ import appInitFactory from './services/app-init-factory'; UpdateAgentPageComponent, UpdateFirmwarePageComponent, XtermComponent, + ZephyrTerminalComponent, SliderWrapperComponent, EditableTextComponent, Autofocus,