Skip to content
Draft
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
35 changes: 33 additions & 2 deletions packages/uhk-agent/src/services/device.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ipcMain } from 'electron';
import { cloneDeep, isEqual } from 'lodash';
import { rm } from 'node:fs/promises';
import os from 'node:os';
import { UhkDeviceProduct } from 'uhk-common';
import {
ALL_UHK_DEVICES,
AreBleAddressesPairedIpcResponse,
Expand All @@ -14,6 +13,7 @@ import {
convertBleStringToNumberArray,
CurrentlyUpdatingModuleInfo,
DeviceConnectionState,
escapeZephyrControlChars,
findUhkModuleById,
FIRMWARE_UPGRADE_METHODS,
FirmwareUpgradeIpcResponse,
Expand Down Expand Up @@ -47,6 +47,7 @@ import {
UHK_DONGLE,
UHK_MODULE_IDS,
UHK_MODULES,
UhkDeviceProduct,
UpdateFirmwareData,
UploadFileData,
VERSIONS,
Expand Down Expand Up @@ -125,6 +126,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,
Expand All @@ -138,6 +140,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,
Expand Down Expand Up @@ -195,6 +198,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) => {
Expand Down Expand Up @@ -973,6 +985,25 @@ export class DeviceService {
event.sender.send(IpcEvents.device.eraseBleSettingsReply, response);
}

public async execShellCommand(_: Electron.IpcMainEvent, [command]): Promise<void> {
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');
// give some time for the command to complete
await snooze(5);
await this.readZephyrLog();
}
catch(error) {
this.logService.error('[DeviceService] execute shell command failed', error);
}
finally {
this.startPollUhkDevice();
}
}

public async startDonglePairing(event: Electron.IpcMainEvent): Promise<void> {
this.logService.misc('[DeviceService] start Dongle pairing');
try {
Expand Down Expand Up @@ -1422,7 +1453,7 @@ export class DeviceService {
try {
const uhkDeviceProduct = await getCurrentUhkDeviceProduct(this.options);
const log = await this.operations.getVariable(UsbVariables.ShellBuffer)
this.logService.misc(`[DeviceService] Right half zephyr log: ${log}`);
this.logService.misc(`[DeviceService] Right half zephyr log (escaped): ⟦${escapeZephyrControlChars(log as string)}⟧`);
const logEntry: ZephyrLogEntry = {
log: log as string,
level: 'info',
Expand Down
77 changes: 67 additions & 10 deletions packages/uhk-agent/src/services/zephyr-log.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ipcMain } from 'electron';
import pLimit from 'p-limit';
import { CommandLineArgs, IpcEvents, LogService, UhkDeviceProduct, ZephyrLogEntry } from 'uhk-common'
import {
CommandLineArgs,
escapeZephyrControlChars,
IpcEvents,
LogService,
UhkDeviceProduct,
ZephyrLogEntry,
} from 'uhk-common'
import { UsbVariables } from 'uhk-usb';
import { getCurrentUhkDongleHID, getCurrenUhk80LeftHID, snooze, UhkHidDevice, UhkOperations, } from 'uhk-usb'

Expand All @@ -11,6 +18,7 @@ export interface ZephyrLogServiceOptions {
currentDeviceFn: typeof getCurrenUhk80LeftHID | typeof getCurrentUhkDongleHID;
logService: LogService;
ipcEvents: {
execShellCommand: string;
isZephyrLoggingEnabled: string;
isZephyrLoggingEnabledReply: string;
toggleZephyrLogging: string;
Expand All @@ -31,6 +39,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,
Expand Down Expand Up @@ -81,6 +98,31 @@ export class ZephyrLogService {
this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Disabled`);
}

private async execShellCommand(_: Electron.IpcMainEvent, [command]): Promise<void> {
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);
this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] execute shell command success`);
// give some time for the command to complete
await snooze(5);
await this.readZephyrLog(operations);
}
finally {
await this.resumeLogging();
}
}

private async getOperations(logEarlierInited = true): Promise<UhkOperations> {
if (logEarlierInited) {
this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] getOperations`);
Expand Down Expand Up @@ -161,6 +203,28 @@ export class ZephyrLogService {
this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] paused logging`);
}

private async readZephyrLog(operations: UhkOperations): Promise<void> {
try {
const log = (await operations.getVariable(UsbVariables.ShellBuffer)) as string;
this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Zephyr log (escaped): ${escapeZephyrControlChars(log)}`);
const logEntry: ZephyrLogEntry = {
log: log as string,
level: 'info',
device: this.options.uhkDeviceProduct.logName,
}
this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry)
}
catch (error) {
this.options.logService.error(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] can't read zephyr log`, error);
const logEntry: ZephyrLogEntry = {
log: error.message as string,
level: 'error',
device: this.options.uhkDeviceProduct.logName,
}
this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry)
}
}

private async resumeLogging(): Promise<void> {
if (!this.isPaused) {
return;
Expand Down Expand Up @@ -188,18 +252,11 @@ export class ZephyrLogService {
const deviceState = await this.uhkHidDevice.getDeviceState();

if (deviceState.isZephyrLogAvailable) {
const log = await operations.getVariable(UsbVariables.ShellBuffer)
this.options.logService.misc(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Zephyr log: ${log}`);
const logEntry: ZephyrLogEntry = {
log: log as string,
level: 'info',
device: this.options.uhkDeviceProduct.logName,
}
this.options.win.webContents.send(IpcEvents.device.zephyrLog, logEntry)
await this.readZephyrLog(operations);
}
}
catch (error) {
this.options.logService.error(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Can't read log`, error);
this.options.logService.error(`[ZephyrLogService | ${this.options.uhkDeviceProduct.logName}] Can't poll log`, error);
const logEntry: ZephyrLogEntry = {
log: error.message,
level: 'error',
Expand Down
24 changes: 24 additions & 0 deletions packages/uhk-common/src/log/escape-zephyr-control-chars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Render a chunk so every control/whitespace byte is visible in the log. Printable chars pass
* through; common controls get named escapes (\r \n \t \e), everything else control-range
* becomes \xNN, and the chunk is wrapped in ⟦…⟧ so leading/trailing spaces are obvious.
*/
export function escapeZephyrControlChars(input: string): string {
let out = '';
for (const ch of input) {
const code = ch.charCodeAt(0);
switch (ch) {
case '\x1b': out += '\\e'; break;
case '\r': out += '\\r'; break;
case '\n': out += '\\n'; break;
case '\t': out += '\\t'; break;
case '\\': out += '\\\\'; break;
default:
out += code < 0x20 || code === 0x7f
? '\\x' + code.toString(16).padStart(2, '0')
: ch;
}
}

return out;
}
1 change: 1 addition & 0 deletions packages/uhk-common/src/log/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './default-log-options.js';
export * from './escape-zephyr-control-chars.js';
export * from './get-log-options.js';
export * from './log-reg-exps.js';
export * from './log-user-config-helper.js';
Expand Down
3 changes: 3 additions & 0 deletions packages/uhk-common/src/util/ipcEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
19 changes: 18 additions & 1 deletion packages/uhk-usb/src/uhk-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,10 @@ export class UhkOperations {
message += await this.getVariable(variableId, iteration + 1);
}

if (iteration === 0) {
// 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) {
message = normalizeStatusBuffer(message);
}

Expand Down Expand Up @@ -936,4 +939,18 @@ export class UhkOperations {

await this.device.write(buffer);
}

public async execShellCommand(cmd: string): Promise<void> {
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);
}
}
2 changes: 2 additions & 0 deletions packages/uhk-web/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
],
"styles": [
"node_modules/nouislider/dist/nouislider.min.css",
"node_modules/@xterm/xterm/css/xterm.css",
"node_modules/@perfectmemory/ngx-contextmenu/src/assets/stylesheets/base.scss",
{
"input": "src/styles.scss",
Expand Down Expand Up @@ -136,6 +137,7 @@
],
"styles": [
"node_modules/nouislider/dist/nouislider.min.css",
"node_modules/@xterm/xterm/css/xterm.css",
"node_modules/@perfectmemory/ngx-contextmenu/src/assets/stylesheets/base.scss",
{
"input": "src/styles.scss",
Expand Down
Loading
Loading