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
54 changes: 53 additions & 1 deletion packages/uhk-agent/src/services/device.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -973,6 +984,22 @@ 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');
}
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 @@ -1418,11 +1445,36 @@ export class DeviceService {
}
}

/**
* 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.
*/
private escapeControlChars(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;
}

private async readZephyrLog(): Promise<void> {
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): ⟦${this.escapeControlChars(log as string)}⟧`);
const logEntry: ZephyrLogEntry = {
log: log as string,
level: 'info',
Expand Down
31 changes: 31 additions & 0 deletions packages/uhk-agent/src/services/zephyr-log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ZephyrLogServiceOptions {
currentDeviceFn: typeof getCurrenUhk80LeftHID | typeof getCurrentUhkDongleHID;
logService: LogService;
ipcEvents: {
execShellCommand: string;
isZephyrLoggingEnabled: string;
isZephyrLoggingEnabledReply: string;
toggleZephyrLogging: string;
Expand All @@ -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,
Expand Down Expand Up @@ -81,6 +91,27 @@ 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);
}
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
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