diff --git a/packages/uhk-agent/src/electron-main.ts b/packages/uhk-agent/src/electron-main.ts index 093b9c92..d5035166 100644 --- a/packages/uhk-agent/src/electron-main.ts +++ b/packages/uhk-agent/src/electron-main.ts @@ -118,13 +118,13 @@ function createWindow() { }); // Emitted when the window is closed. - win.on('closed', () => { + win.on('closed', async () => { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. logger.info('[Electron Main] win closed'); win = null; - deviceService.close(); + await deviceService.close(); deviceService = null; appUpdateService = null; appService = null; diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index 1cfc2e68..55825d10 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -14,8 +14,6 @@ import { UpdateFirmwareData } from 'uhk-common'; import { snooze, UhkHidDevice, UhkOperations } from 'uhk-usb'; -import { Subscription, interval, from } from 'rxjs'; -import { distinctUntilChanged, startWith, switchMap, tap } from 'rxjs/operators'; import { emptyDir } from 'fs-extra'; import * as path from 'path'; @@ -35,7 +33,8 @@ import { * - Read UserConfiguration from the UHK Device */ export class DeviceService { - private pollTimer$: Subscription; + private _pollerAllowed: boolean; + private _uhkDevicePolling: boolean; private queueManager = new QueueManager(); constructor(private logService: LogService, @@ -43,7 +42,11 @@ export class DeviceService { private device: UhkHidDevice, private operations: UhkOperations, private rootDir: string) { - this.pollUhkDevice(); + this.startPollUhkDevice(); + this.uhkDevicePoller() + .catch(error => { + this.logService.error('[DeviceService] UHK Device poller error', error); + }); ipcMain.on(IpcEvents.device.saveUserConfiguration, (...args: any[]) => { this.queueManager.add({ @@ -72,7 +75,7 @@ export class DeviceService { }); }); - ipcMain.on(IpcEvents.device.startConnectionPoller, this.pollUhkDevice.bind(this)); + ipcMain.on(IpcEvents.device.startConnectionPoller, this.startPollUhkDevice.bind(this)); ipcMain.on(IpcEvents.device.recoveryDevice, (...args: any[]) => { this.queueManager.add({ @@ -103,6 +106,8 @@ export class DeviceService { let response: ConfigurationReply; try { + await this.stopPollUhkDevice(); + await this.device.waitUntilKeyboardBusy(); const result = await this.operations.loadConfigurations(); const modules: HardwareModules = await this.getHardwareModules(false); @@ -123,6 +128,7 @@ export class DeviceService { }; } finally { this.device.close(); + this.startPollUhkDevice(); } event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response)); @@ -136,8 +142,7 @@ export class DeviceService { leftModuleInfo: await this.operations.getLeftModuleVersionInfo(), rightModuleInfo: await this.operations.getRightModuleVersionInfo() }; - } - catch (err) { + } catch (err) { if (!catchError) { return err; } @@ -151,8 +156,8 @@ export class DeviceService { } } - public close(): void { - this.stopPollTimer(); + public async close(): Promise { + await this.stopPollUhkDevice(); this.logService.info('[DeviceService] Device connection checker stopped.'); } @@ -168,7 +173,7 @@ export class DeviceService { this.logService.debug('Device right firmware version:', hardwareModules.rightModuleInfo.firmwareVersion); this.logService.debug('Device left firmware version:', hardwareModules.leftModuleInfo.firmwareVersion); - this.stopPollTimer(); + await this.stopPollUhkDevice(); this.device.resetDeviceCache(); if (data.firmware) { @@ -179,8 +184,7 @@ export class DeviceService { await this.operations.updateRightFirmware(firmwarePathData.rightFirmwarePath); await this.operations.updateLeftModule(firmwarePathData.leftFirmwarePath); - } - else { + } else { const packageJsonPath = path.join(this.rootDir, 'packages/firmware/package.json'); const packageJson = await getPackageJsonFromPathAsync(packageJsonPath); this.logService.debug('New firmware version:', packageJson.firmwareVersion); @@ -192,7 +196,7 @@ export class DeviceService { response.success = true; response.modules = await this.getHardwareModules(false); } catch (error) { - const err = {message: error.message, stack: error.stack}; + const err = { message: error.message, stack: error.stack }; this.logService.error('[DeviceService] updateFirmware error', err); response.modules = await this.getHardwareModules(true); @@ -205,7 +209,7 @@ export class DeviceService { await snooze(500); - this.pollUhkDevice(); + this.startPollUhkDevice(); event.sender.send(IpcEvents.device.updateFirmwareReply, response); } @@ -214,14 +218,14 @@ export class DeviceService { const response = new FirmwareUpgradeIpcResponse(); try { - this.stopPollTimer(); + await this.stopPollUhkDevice(); await this.operations.updateRightFirmware(); response.modules = await this.getHardwareModules(false); response.success = true; } catch (error) { - const err = {message: error.message, stack: error.stack}; + const err = { message: error.message, stack: error.stack }; this.logService.error('[DeviceService] updateFirmware error', err); response.modules = await this.getHardwareModules(true); @@ -236,28 +240,51 @@ export class DeviceService { await this.device.enableUsbStackTest(); } + private startPollUhkDevice(): void { + this._pollerAllowed = true; + } + + private async stopPollUhkDevice(): Promise { + return new Promise(async resolve => { + this._pollerAllowed = false; + + while (true) { + if (!this._uhkDevicePolling) { + return resolve(); + } + + await snooze(100); + } + }); + } + /** * HID API not support device attached and detached event. * This method check the keyboard is attached to the computer or not. - * Every second check the HID device list. + * The halves are connected and merged or not. + * Every 250ms check the HID device list. * @private */ - private pollUhkDevice(): void { - if (this.pollTimer$) { - return; - } + private async uhkDevicePoller(): Promise { + let savedState: DeviceConnectionState; - this.pollTimer$ = interval(1000) - .pipe( - startWith(0), - switchMap(() => from(this.device.getDeviceConnectionStateAsync())), - distinctUntilChanged(isEqual), - tap((state: DeviceConnectionState) => { + while (true) { + if (this._pollerAllowed) { + + this._uhkDevicePolling = true; + + const state = await this.device.getDeviceConnectionStateAsync(); + if (!isEqual(state, savedState)) { + savedState = state; this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state); this.logService.info('[DeviceService] Device connection state changed to:', state); - }) - ) - .subscribe(); + } + + this._uhkDevicePolling = false; + } + + await snooze(250); + } } private async saveUserConfiguration(event: Electron.Event, args: Array): Promise { @@ -265,32 +292,23 @@ export class DeviceService { const data: SaveUserConfigurationData = JSON.parse(args[0]); try { + await this.stopPollUhkDevice(); await backupUserConfiguration(data); const buffer = mapObjectToUserConfigBinaryBuffer(data.configuration); await this.operations.saveUserConfiguration(buffer); response.success = true; - } - catch (error) { + } catch (error) { this.logService.error('[DeviceService] Transferring error', error); - response.error = {message: error.message}; + response.error = { message: error.message }; } finally { this.device.close(); + this.startPollUhkDevice(); } event.sender.send(IpcEvents.device.saveUserConfigurationReply, response); return Promise.resolve(); } - - private stopPollTimer(): void { - if (!this.pollTimer$) { - return; - } - - this.pollTimer$.unsubscribe(); - this.pollTimer$ = null; - - } } diff --git a/packages/uhk-common/src/models/device-connection-state.ts b/packages/uhk-common/src/models/device-connection-state.ts index 1cdb6063..1c627a15 100644 --- a/packages/uhk-common/src/models/device-connection-state.ts +++ b/packages/uhk-common/src/models/device-connection-state.ts @@ -1,4 +1,5 @@ import { UdevRulesInfo } from './udev-rules-info'; +import { HalvesInfo } from './halves-info'; export interface DeviceConnectionState { connected: boolean; @@ -6,4 +7,5 @@ export interface DeviceConnectionState { bootloaderActive: boolean; zeroInterfaceAvailable: boolean; udevRulesInfo: UdevRulesInfo; + halvesInfo: HalvesInfo; } diff --git a/packages/uhk-common/src/models/halves-info.ts b/packages/uhk-common/src/models/halves-info.ts new file mode 100644 index 00000000..e73f61da --- /dev/null +++ b/packages/uhk-common/src/models/halves-info.ts @@ -0,0 +1,4 @@ +export interface HalvesInfo { + areHalvesMerged: boolean; + isLeftHalfConnected: boolean; +} diff --git a/packages/uhk-common/src/models/index.ts b/packages/uhk-common/src/models/index.ts index d9d4eabe..8248fe95 100644 --- a/packages/uhk-common/src/models/index.ts +++ b/packages/uhk-common/src/models/index.ts @@ -11,3 +11,4 @@ export * from './hardware-module-info'; export * from './save-user-configuration-data'; export * from './udev-rules-info'; export * from './update-firmware-data'; +export * from './halves-info'; diff --git a/packages/uhk-usb/src/uhk-hid-device.ts b/packages/uhk-usb/src/uhk-hid-device.ts index bf24a248..c7e493c7 100644 --- a/packages/uhk-usb/src/uhk-hid-device.ts +++ b/packages/uhk-usb/src/uhk-hid-device.ts @@ -2,7 +2,7 @@ import { Device, devices, HID } from 'node-hid'; import { pathExists } from 'fs-extra'; import * as path from 'path'; import { platform } from 'os'; -import { CommandLineArgs, DeviceConnectionState, isEqualArray, LogService, UdevRulesInfo } from 'uhk-common'; +import { CommandLineArgs, DeviceConnectionState, HalvesInfo, isEqualArray, LogService, UdevRulesInfo } from 'uhk-common'; import { ConfigBufferId, @@ -89,7 +89,8 @@ export class UhkHidDevice { connected: false, zeroInterfaceAvailable: false, hasPermission: this.hasPermission(), - udevRulesInfo: await this.getUdevInfoAsync() + udevRulesInfo: await this.getUdevInfoAsync(), + halvesInfo: { areHalvesMerged: true, isLeftHalfConnected: true } }; for (const dev of devs) { @@ -105,6 +106,12 @@ export class UhkHidDevice { } } + if (result.connected && result.hasPermission && result.zeroInterfaceAvailable) { + result.halvesInfo = await this.getHalvesStates(); + } else if (!result.connected) { + this._device = undefined; + } + return result; } @@ -253,6 +260,15 @@ export class UhkHidDevice { await this.write(transfer); } + async getHalvesStates(): Promise { + const buffer = await this.write(Buffer.from([UsbCommand.GetDeviceState])); + + return { + areHalvesMerged: buffer[2] !== 0, + isLeftHalfConnected: buffer[3] !== 0 + }; + } + /** * Return the stored version of HID device. If not exist try to initialize. * @returns {HID} diff --git a/packages/uhk-web/src/app/components/keyboard/slider/keyboard-slider.component.html b/packages/uhk-web/src/app/components/keyboard/slider/keyboard-slider.component.html index 7ff7f5d0..693e2235 100644 --- a/packages/uhk-web/src/app/components/keyboard/slider/keyboard-slider.component.html +++ b/packages/uhk-web/src/app/components/keyboard/slider/keyboard-slider.component.html @@ -1,7 +1,7 @@ ; keymap$: Observable; keyboardLayout$: Observable; allowLayerDoubleTap$: Observable; lastEditedKey$: Observable; + halvesInfo$: Observable; keymap: Keymap; private routeSubscription: Subscription; @@ -67,6 +67,7 @@ export class KeymapEditComponent implements OnDestroy { this.keyboardLayout$ = store.select(getKeyboardLayout); this.allowLayerDoubleTap$ = store.select(layerDoubleTapSupported); this.lastEditedKey$ = store.select(lastEditedKey); + this.halvesInfo$ = store.select(getHalvesInfo); } ngOnDestroy(): void { @@ -94,11 +95,6 @@ export class KeymapEditComponent implements OnDestroy { }); } - @HostListener('window:keydown.alt.s', ['$event']) - toggleKeyboardSplit() { - this.keyboardSplit = !this.keyboardSplit; - } - descriptionChanged(event: ChangeKeymapDescription): void { this.store.dispatch(new EditDescriptionAction(event)); } diff --git a/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.html b/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.html index e74f8497..ee3c6ad2 100644 --- a/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.html +++ b/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.html @@ -11,6 +11,7 @@ [keyActions]="moduleConfig[i].keyActions" [selectedKey]="selectedKey" [@split]="moduleAnimationStates[i]" + [@fadeKeyboard]="moduleVisibilityAnimationStates[i]" [selected]="selectedKey?.moduleId === i" [lastEdited]="lastEditedKey?.moduleId === i" [lastEditedKeyId]="lastEditedKey?.key" diff --git a/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.ts b/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.ts index 1bae72dd..784b3975 100644 --- a/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.ts +++ b/packages/uhk-web/src/app/components/svg/keyboard/svg-keyboard.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, Output, SimpleChanges, ChangeDetectionStrategy } from '@angular/core'; import { animate, state, trigger, style, transition } from '@angular/animations'; import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; -import { Module } from 'uhk-common'; +import { HalvesInfo, Module } from 'uhk-common'; import { SvgModule } from '../module'; import { SvgModuleProviderService } from '../../../services/svg-module-provider.service'; @@ -23,13 +23,23 @@ import { LastEditedKey } from '../../../models'; animations: [ trigger('split', [ state('rotateLeft', style({ - transform: 'translate(-3%, 15%) rotate(4deg) scale(0.92, 0.92)' + transform: 'translate(2%, 30%) rotate(10.8deg) scale(0.80, 0.80)' })), state('rotateRight', style({ - transform: 'translate(3%, 15%) rotate(-4deg) scale(0.92, 0.92)' + transform: 'translate(-2%, 30.7%) rotate(-10deg) scale(0.80, 0.80)' })), transition('* <=> *', animate(500)) ]), + trigger('fadeKeyboard', [ + state('visible', style({ + opacity: 1 + })), + state('invisible', style({ + opacity: 0 + })), + transition('visible => invisible', animate(500)), + transition('invisible => visible', animate(500)) + ]), trigger('fadeSeparator', [ state('visible', style({ opacity: 1 @@ -37,8 +47,8 @@ import { LastEditedKey } from '../../../models'; state('invisible', style({ opacity: 0 })), - transition('visible => invisible', animate(500)), - transition('invisible => visible', animate(1500)) + transition('visible => invisible', animate('200ms')), + transition('invisible => visible', animate('200ms 500ms')) ]) ] }) @@ -47,7 +57,7 @@ export class SvgKeyboardComponent { @Input() capturingEnabled: boolean; @Input() selectedKey: { layerId: number, moduleId: number, keyId: number }; @Input() selected: boolean; - @Input() halvesSplit: boolean; + @Input() halvesInfo: HalvesInfo; @Input() keyboardLayout = KeyboardLayout.ANSI; @Input() description: string; @Input() showDescription = false; @@ -60,6 +70,7 @@ export class SvgKeyboardComponent { modules: SvgModule[]; viewBox: string; moduleAnimationStates: string[]; + moduleVisibilityAnimationStates: string[]; separator: SvgSeparator; separatorStyle: SafeStyle; separatorAnimation = 'visible'; @@ -68,7 +79,6 @@ export class SvgKeyboardComponent { private sanitizer: DomSanitizer) { this.modules = []; this.viewBox = '-520 582 1100 470'; - this.halvesSplit = false; this.moduleAnimationStates = []; } @@ -77,7 +87,7 @@ export class SvgKeyboardComponent { } ngOnChanges(changes: SimpleChanges) { - if (changes.halvesSplit) { + if (changes.halvesInfo) { this.updateModuleAnimationStates(); } @@ -110,12 +120,19 @@ export class SvgKeyboardComponent { } private updateModuleAnimationStates() { - if (this.halvesSplit) { - this.moduleAnimationStates = ['rotateRight', 'rotateLeft']; - this.separatorAnimation = 'invisible'; - } else { + if (this.halvesInfo.areHalvesMerged) { this.moduleAnimationStates = []; this.separatorAnimation = 'visible'; + } else { + this.moduleAnimationStates = ['rotateRight', 'rotateLeft']; + this.separatorAnimation = 'invisible'; + } + + if (this.halvesInfo.isLeftHalfConnected) { + this.moduleVisibilityAnimationStates = ['visible', 'visible']; + } else { + this.moduleVisibilityAnimationStates = ['visible', 'invisible']; + } } diff --git a/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html b/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html index 95824550..a322e868 100644 --- a/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html +++ b/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.html @@ -4,7 +4,7 @@ [currentLayer]="currentLayer" [capturingEnabled]="popoverEnabled" [selectedKey]="selectedKey" - [halvesSplit]="halvesSplit" + [halvesInfo]="halvesInfo" [keyboardLayout]="keyboardLayout" [description]="keymap.description" [lastEditedKey]="lastEditedKey" diff --git a/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.ts b/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.ts index 3186cedb..b0527638 100644 --- a/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.ts +++ b/packages/uhk-web/src/app/components/svg/wrap/svg-keyboard-wrap.component.ts @@ -20,6 +20,7 @@ import { Store } from '@ngrx/store'; import { camelCaseToSentence, capitalizeFirstLetter, + HalvesInfo, KeyAction, Keymap, KeystrokeAction, @@ -65,7 +66,7 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges { @Input() keymap: Keymap; @Input() popoverEnabled: boolean = true; @Input() tooltipEnabled: boolean = false; - @Input() halvesSplit: boolean; + @Input() halvesInfo: HalvesInfo; @Input() keyboardLayout: KeyboardLayout.ANSI; @Input() allowLayerDoubleTap: boolean; @Input() lastEditedKey: LastEditedKey; diff --git a/packages/uhk-web/src/app/store/effects/device.ts b/packages/uhk-web/src/app/store/effects/device.ts index 8cc38729..3fc618d0 100644 --- a/packages/uhk-web/src/app/store/effects/device.ts +++ b/packages/uhk-web/src/app/store/effects/device.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { Action, Store } from '@ngrx/store'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { EMPTY, Observable, of, timer } from 'rxjs'; -import { map, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, map, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { FirmwareUpgradeIpcResponse, @@ -73,6 +73,14 @@ export class DeviceEffects { return this.router.navigate(['/detection']); }), + distinctUntilChanged(( + [prevAction, prevRoute, prevConnected], + [currAction, currRoute, currConnected]) => { + + return prevConnected === currConnected && + prevAction.payload.hasPermission === currAction.payload.hasPermission && + prevAction.payload.zeroInterfaceAvailable === currAction.payload.zeroInterfaceAvailable; + }), switchMap(([action, route, connected]) => { const payload = action.payload; diff --git a/packages/uhk-web/src/app/store/index.ts b/packages/uhk-web/src/app/store/index.ts index fc8ed8a6..d0704c9e 100644 --- a/packages/uhk-web/src/app/store/index.ts +++ b/packages/uhk-web/src/app/store/index.ts @@ -110,6 +110,7 @@ export const bootloaderActive = createSelector(deviceState, fromDevice.bootloade export const firmwareUpgradeFailed = createSelector(deviceState, fromDevice.firmwareUpgradeFailed); export const firmwareUpgradeSuccess = createSelector(deviceState, fromDevice.firmwareUpgradeSuccess); export const getUpdateUdevRules = createSelector(deviceState, fromDevice.updateUdevRules); +export const getHalvesInfo = createSelector(deviceState, fromDevice.halvesInfo); export const getPrivilegePageState = createSelector(appState, getUpdateUdevRules, (app, updateUdevRules): PrivilagePageSate => { const permissionSetupFailed = !!app.permissionError; diff --git a/packages/uhk-web/src/app/store/reducers/device.ts b/packages/uhk-web/src/app/store/reducers/device.ts index ca92d42b..d7994363 100644 --- a/packages/uhk-web/src/app/store/reducers/device.ts +++ b/packages/uhk-web/src/app/store/reducers/device.ts @@ -1,5 +1,5 @@ import { Action } from '@ngrx/store'; -import { HardwareModules, UdevRulesInfo } from 'uhk-common'; +import { HardwareModules, UdevRulesInfo, HalvesInfo } from 'uhk-common'; import * as Device from '../actions/device'; import * as App from '../actions/app'; @@ -24,6 +24,7 @@ export interface State { log: Array; restoringUserConfiguration: boolean; hasBackupUserConfiguration: boolean; + halvesInfo: HalvesInfo; } export const initialState: State = { @@ -47,7 +48,8 @@ export const initialState: State = { }, log: [{ message: '', cssClass: XtermCssClass.standard }], restoringUserConfiguration: false, - hasBackupUserConfiguration: false + hasBackupUserConfiguration: false, + halvesInfo: { isLeftHalfConnected: true, areHalvesMerged: true } }; export function reducer(state = initialState, action: Action): State { @@ -60,7 +62,8 @@ export function reducer(state = initialState, action: Action): State { hasPermission: data.hasPermission, zeroInterfaceAvailable: data.zeroInterfaceAvailable, bootloaderActive: data.bootloaderActive, - udevRuleInfo: data.udevRulesInfo + udevRuleInfo: data.udevRulesInfo, + halvesInfo: data.halvesInfo }; } @@ -249,3 +252,4 @@ export const bootloaderActive = (state: State) => state.bootloaderActive; export const firmwareUpgradeFailed = (state: State) => state.firmwareUpdateFailed; export const firmwareUpgradeSuccess = (state: State) => state.firmwareUpdateSuccess; export const updateUdevRules = (state: State) => state.udevRuleInfo === UdevRulesInfo.Different; +export const halvesInfo = (state: State) => state.halvesInfo;