diff --git a/packages/uhk-common/src/models/device-connection-state.ts b/packages/uhk-common/src/models/device-connection-state.ts index 7ecf29650ff..48f65f395c9 100644 --- a/packages/uhk-common/src/models/device-connection-state.ts +++ b/packages/uhk-common/src/models/device-connection-state.ts @@ -24,9 +24,8 @@ export interface DeviceConnectionState { halvesInfo: HalvesInfo; hardwareModules?: HardwareModules; /** - * Each element describe the hostConnection is the user-config are paired to the keyboard or not. - * If the value is 1 then paired. + * The BLE addresses of the devices that paired with the keyboard but not is the hostConnections of the user configuration. */ - pairedDevices: number[]; + newPairedDevices: string[]; udevRulesInfo: UdevRulesInfo; } diff --git a/packages/uhk-usb/src/uhk-hid-device.ts b/packages/uhk-usb/src/uhk-hid-device.ts index 94cb247fc4c..ad1f2e7c677 100644 --- a/packages/uhk-usb/src/uhk-hid-device.ts +++ b/packages/uhk-usb/src/uhk-hid-device.ts @@ -9,10 +9,10 @@ import { BLE_ADDRESS_LENGTH, Buffer, CommandLineArgs, + convertBleAddressArrayToString, DeviceConnectionState, FIRMWARE_UPGRADE_METHODS, HalvesInfo, - HOST_CONNECTION_COUNT_MAX, isBitSet, isEqualArray, LeftSlotModules, @@ -84,6 +84,8 @@ interface UsvDeviceConnectionState { } export const UHK_HID_DEVICE_NOT_CONNECTED = '[UhkHidDevice] Device is not connected'; + +const MAX_BLE_ADDRESSES_IN_NEW_PAIRING_RESPONSE = 10; /** * HID API wrapper to support unified logging and async write */ @@ -339,7 +341,7 @@ export class UhkHidDevice { isMacroStatusDirty: false, leftHalfDetected: false, multiDevice: await getNumberOfConnectedDevices(this.options) > 1, - pairedDevices: [], + newPairedDevices: [], udevRulesInfo: await this.getUdevInfoAsync(), }; @@ -419,7 +421,7 @@ export class UhkHidDevice { result.isMacroStatusDirty = deviceState.isMacroStatusDirty; if (deviceState.newPairedDevice) { - result.pairedDevices = await this.getPairedDevices(); + result.newPairedDevices = await this.getPairedDevices(); } } else if (!result.connectedDevice) { this._device = undefined; @@ -681,13 +683,39 @@ export class UhkHidDevice { } } - async getPairedDevices(): Promise { - this.logService.usb('[UhkHidDevice] USB[T]: Read paired devices'); - const command = Buffer.from([UsbCommand.GetProperty, DevicePropertyIds.NewPairings]); - const buffer = await this.write(command); - const pairedDevices = convertBufferToIntArray(buffer); + async getPairedDevices(): Promise { + this.logService.misc('[UhkHidDevice] Read paired devices'); + let iteration = 0; + const result: string[] = []; + + while (true) { + this.logService.usb('[UhkHidDevice] USB[T]: Read paired devices'); + const command = Buffer.from([UsbCommand.GetProperty, DevicePropertyIds.NewPairings, iteration]); + const buffer = await this.write(command); + const uhkBuffer = UhkBuffer.fromArray(convertBufferToIntArray(buffer)); + // skip the first byte + uhkBuffer.readUInt8(); + + const remainingNewConnections = uhkBuffer.readUInt8(); + + const count = Math.min(remainingNewConnections, MAX_BLE_ADDRESSES_IN_NEW_PAIRING_RESPONSE); + + for (let i = 0; i < count; i++) { + const address = []; + for (let i = 0; i < BLE_ADDRESS_LENGTH; i++) { + address.push(uhkBuffer.readUInt8()); + } + result.push(convertBleAddressArrayToString(address)); + } + + if (remainingNewConnections <= MAX_BLE_ADDRESSES_IN_NEW_PAIRING_RESPONSE) { + break; + } - return pairedDevices.slice(0, HOST_CONNECTION_COUNT_MAX + 1); + iteration += 1; + } + + return result; } async getProtocolVersions(): Promise { diff --git a/packages/uhk-web/src/app/app.component.html b/packages/uhk-web/src/app/app.component.html index fc5334b0c8d..647072f483d 100644 --- a/packages/uhk-web/src/app/app.component.html +++ b/packages/uhk-web/src/app/app.component.html @@ -5,6 +5,11 @@ (updateApp)="updateApp()" (doNotUpdateApp)="doNotUpdateApp()"> + ; @@ -108,6 +111,7 @@ export class MainAppComponent implements OnDestroy { }; statusBuffer: string; private donglePairingStateSubscription: Subscription; + private newPairedDevicesStateSubscription: Subscription; private errorPanelHeightSubscription: Subscription; private keypressCapturing: boolean; private saveToKeyboardStateSubscription: Subscription; @@ -128,6 +132,11 @@ export class MainAppComponent implements OnDestroy { this.donglePairingState = data; this.cdRef.markForCheck(); }); + this.newPairedDevicesStateSubscription = store.select(getNewPairedDevicesState) + .subscribe(data => { + this.newPairedDevicesState = data; + this.cdRef.markForCheck(); + }); this.errorPanelHeightSubscription = store.select(getErrorPanelHeight) .subscribe(height => { this.splitSizes = { @@ -192,6 +201,7 @@ export class MainAppComponent implements OnDestroy { ngOnDestroy(): void { this.donglePairingStateSubscription.unsubscribe(); + this.newPairedDevicesStateSubscription.unsubscribe(); this.errorPanelHeightSubscription.unsubscribe(); this.saveToKeyboardStateSubscription.unsubscribe(); this.keypressCapturingSubscription.unsubscribe(); @@ -234,6 +244,10 @@ export class MainAppComponent implements OnDestroy { this.store.dispatch(new KeyUpAction(event)); } + addPairedDevicesToHostConnections() { + this.store.dispatch(new AddNewPairedDevicesToHostConnectionsAction()); + } + updateApp() { this.store.dispatch(new UpdateAppAction()); } @@ -251,7 +265,10 @@ export class MainAppComponent implements OnDestroy { } isTopNotificationPanelVisible(): boolean { - return this.showFirmwareUpgradePanel || this.showUpdateAvailable || this.donglePairingState?.showDonglePairingPanel; + return this.showFirmwareUpgradePanel + || this.showUpdateAvailable + || this.donglePairingState?.showDonglePairingPanel + || this.newPairedDevicesState?.showNewPairedDevicesPanel; } updateFirmware(): void { diff --git a/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.html b/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.html new file mode 100644 index 00000000000..124ede2634f --- /dev/null +++ b/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.html @@ -0,0 +1,23 @@ + diff --git a/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.scss b/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.scss new file mode 100644 index 00000000000..de4c52c8ded --- /dev/null +++ b/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.scss @@ -0,0 +1,21 @@ +@import '../../../../styles/variables'; + +.ble-pairing-panel-wrapper { + display: flex; + justify-content: center; + align-items: center; + height: $main-content-top-margin-on-update; + margin: 0; + padding-top: 5px; + padding-bottom: 5px; + + background-color: var(--color-firmware-upgrade-panel-bg); + color: var(--color-firmware-upgrade-panel-text); + border-radius: 0; + border-width: 0; + + a { + color: var(--color-firmware-upgrade-panel-text); + text-decoration: underline; + } +} diff --git a/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.ts b/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.ts new file mode 100644 index 00000000000..6a524c04185 --- /dev/null +++ b/packages/uhk-web/src/app/components/device/ble-pairing-panel/ble-pairing-panel.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { HOST_CONNECTION_COUNT_MAX } from 'uhk-common'; + +import { BleAddingState, BleAddingStates } from '../../../models'; + +@Component({ + selector: 'ble-pairing-panel', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './ble-pairing-panel.component.html', + styleUrls: ['./ble-pairing-panel.component.scss'] +}) +export class BlePairingPanelComponent { + @Input() state: BleAddingState; + + @Output() addNewPairedDevices = new EventEmitter(); + + protected readonly BlePairingStates = BleAddingStates; + protected readonly faSpinner = faSpinner; + protected readonly hostConnectionsMaxCount = HOST_CONNECTION_COUNT_MAX; +} diff --git a/packages/uhk-web/src/app/models/ble-adding-state.ts b/packages/uhk-web/src/app/models/ble-adding-state.ts new file mode 100644 index 00000000000..edb8ca0b1f4 --- /dev/null +++ b/packages/uhk-web/src/app/models/ble-adding-state.ts @@ -0,0 +1,14 @@ +export enum BleAddingStates { + Idle = 'Idle', + Adding = 'Adding', + AddingSuccess = 'AddingSuccess', + SavingToKeyboard = 'SavingToKeyboard', + TooMuchHostConnections = 'TooMuchHostConnections', +} + +export interface BleAddingState { + showNewPairedDevicesPanel: boolean; + nrOfNewBleAddresses: number; + nrOfHostConnections: number; + state: BleAddingStates; +} diff --git a/packages/uhk-web/src/app/models/index.ts b/packages/uhk-web/src/app/models/index.ts index e2ee536e10a..7c0c7400a68 100644 --- a/packages/uhk-web/src/app/models/index.ts +++ b/packages/uhk-web/src/app/models/index.ts @@ -1,5 +1,6 @@ export * from './apply-user-configuration-from-file-payload'; export * from './backlighting-option'; +export * from './ble-adding-state'; export * from './config-size-state'; export * from './delete-host-connection-payload'; export * from './device-ui-states'; diff --git a/packages/uhk-web/src/app/shared.module.ts b/packages/uhk-web/src/app/shared.module.ts index 2c999e62287..e2189db761b 100644 --- a/packages/uhk-web/src/app/shared.module.ts +++ b/packages/uhk-web/src/app/shared.module.ts @@ -21,6 +21,7 @@ import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; import { AddOnComponent } from './components/add-on'; import { BackToComponent } from './components/back-to/back-to.component'; import CircleTooltipComponent from './components/circle-tooltip/circle-tooltip.component'; +import { BlePairingPanelComponent } from './components/device/ble-pairing-panel/ble-pairing-panel.component'; import { FadeTimeoutSliderComponent } from './components/device/led-settings/fade-timeout-slider.component'; import { DonglePairingPanelComponent } from './components/device/dongle-pairing-panel/dongle-pairing-panel.component'; import { KeyboardSliderComponent } from './components/keyboard/slider'; @@ -172,6 +173,7 @@ import appInitFactory from './services/app-init-factory'; AdvancedSettingsPageComponent, AsHexColorPipe, BackToComponent, + BlePairingPanelComponent, HostConnectionTypeLabelPipePipe, NewLineToBrPipe, SafeHtmlPipe, diff --git a/packages/uhk-web/src/app/store/actions/user-config.ts b/packages/uhk-web/src/app/store/actions/user-config.ts index 1a2374400e1..669b196a919 100644 --- a/packages/uhk-web/src/app/store/actions/user-config.ts +++ b/packages/uhk-web/src/app/store/actions/user-config.ts @@ -14,6 +14,7 @@ import { NavigateToModuleSettingsPayload } from '../../models/navigate-to-module export enum ActionTypes { AddColorToBacklightingColorPalette = '[user-config] Add color to the backlighting color palette', + AddNewPairedDevicesToHostConnections = '[user-config] Add new paired devices to host connections', DeleteColorFromBacklightingColorPalette = '[user-config] delete color from the backlighting color palette', LoadUserConfig = '[user-config] Load User Config', LoadConfigFromDevice = '[user-config] Load User Config from Device', @@ -47,6 +48,10 @@ export class AddColorToBacklightingColorPaletteAction implements Action { } } +export class AddNewPairedDevicesToHostConnectionsAction implements Action { + type = ActionTypes.AddNewPairedDevicesToHostConnections; +} + export class DeleteColorFromBacklightingColorPaletteAction implements Action { type = ActionTypes.DeleteColorFromBacklightingColorPalette; } @@ -203,6 +208,7 @@ export class RecoverLEDSpacesAction implements Action { export type Actions = AddColorToBacklightingColorPaletteAction + | AddNewPairedDevicesToHostConnectionsAction | DeleteColorFromBacklightingColorPaletteAction | LoadUserConfigAction | LoadUserConfigSuccessAction diff --git a/packages/uhk-web/src/app/store/effects/device.ts b/packages/uhk-web/src/app/store/effects/device.ts index b22314c1953..933b444ae9e 100644 --- a/packages/uhk-web/src/app/store/effects/device.ts +++ b/packages/uhk-web/src/app/store/effects/device.ts @@ -10,6 +10,7 @@ import { FirmwareUpgradeIpcResponse, getHardwareConfigFromDeviceResponse, HardwareConfiguration, + HOST_CONNECTION_COUNT_MAX, IpcResponse, NotificationType, shouldUpgradeAgent, @@ -224,6 +225,10 @@ export class DeviceEffects { if (shouldUpgradeFirmware) return this.router.navigate(['/update-firmware']); + if (state.userConfiguration.userConfiguration.hostConnections.length > HOST_CONNECTION_COUNT_MAX) { + return; + } + setTimeout(() => this.sendUserConfigToKeyboard( state.userConfiguration.userConfiguration, state.app.hardwareConfig, diff --git a/packages/uhk-web/src/app/store/effects/user-config.ts b/packages/uhk-web/src/app/store/effects/user-config.ts index 0df2e8444d3..93a21ea068d 100644 --- a/packages/uhk-web/src/app/store/effects/user-config.ts +++ b/packages/uhk-web/src/app/store/effects/user-config.ts @@ -21,6 +21,7 @@ import { } from 'uhk-common'; import { EmptyAction } from '../actions/app'; +import { SaveConfigurationAction } from '../actions/device'; import { ActionTypes, ApplyUserConfigurationFromFileAction, @@ -71,6 +72,12 @@ export class UserConfigEffects { ) ); + addNewPairedDevives$ = createEffect(() => this.actions$ + .pipe( + ofType(ActionTypes.AddNewPairedDevicesToHostConnections), + map(() => new SaveConfigurationAction(true)) + )); + saveUserConfig$ = createEffect(() => this.actions$ .pipe( ofType( @@ -84,6 +91,7 @@ export class UserConfigEffects { ActionTypes.RenameUserConfiguration, ActionTypes.SetUserConfigurationValue, ActionTypes.SetUserConfigurationRgbValue, ActionTypes.RecoverLEDSpaces, ActionTypes.SetModuleConfigurationValue, ActionTypes.ReorderHostConnections, ActionTypes.RenameHostConnection, ActionTypes.SetHostConnectionSwitchover, + ActionTypes.AddNewPairedDevicesToHostConnections, ), withLatestFrom(this.store.select(getUserConfiguration), this.store.select(getPrevUserConfiguration)), mergeMap(([action, config, prevUserConfiguration]) => { diff --git a/packages/uhk-web/src/app/store/index.ts b/packages/uhk-web/src/app/store/index.ts index 36126c02e1c..7ee9a9b4c1b 100644 --- a/packages/uhk-web/src/app/store/index.ts +++ b/packages/uhk-web/src/app/store/index.ts @@ -121,6 +121,7 @@ export const isI2cDebuggingRingBellEnabled = createSelector(advanceSettingsState export const userConfigState = (state: AppState) => state.userConfiguration; export const getRouterState = (state: AppState) => state.router; +export const getNewPairedDevicesState = createSelector(userConfigState, fromUserConfig.getNewPairedDevicesState); export const getUserConfiguration = createSelector(userConfigState, fromUserConfig.getUserConfiguration); export const getKeymaps = createSelector(userConfigState, fromUserConfig.getKeymaps); export const getHostConnections = createSelector(userConfigState, fromUserConfig.getHostConnections); diff --git a/packages/uhk-web/src/app/store/reducers/user-configuration.ts b/packages/uhk-web/src/app/store/reducers/user-configuration.ts index 5a8b7c9ed8f..904e60cdc47 100644 --- a/packages/uhk-web/src/app/store/reducers/user-configuration.ts +++ b/packages/uhk-web/src/app/store/reducers/user-configuration.ts @@ -1,3 +1,4 @@ +import { HOST_CONNECTION_COUNT_MAX } from 'uhk-common'; import { BacklightingMode, Constants, @@ -29,7 +30,8 @@ import { SwitchLayerAction, UserConfiguration } from 'uhk-common'; - +import { BleAddingStates } from '../../models'; +import { BleAddingState } from '../../models'; import { BacklightingOption, defaultLastEditKey, @@ -70,6 +72,8 @@ export interface State { lastEditedKey: LastEditedKey; layerOptions: Map; halvesInfo: HalvesInfo; + newPairedDevices: string[]; + newPairedDevicesAdding: boolean; selectedLayerOption: LayerOption; theme: string; } @@ -82,6 +86,8 @@ export const initialState: State = { lastEditedKey: defaultLastEditKey(), layerOptions: initLayerOptions(), halvesInfo: getDefaultHalvesInfo(), + newPairedDevices: [], + newPairedDevicesAdding: false, selectedLayerOption: getBaseLayerOption(), theme: '' }; @@ -115,6 +121,25 @@ export function reducer( }; } + case UserConfig.ActionTypes.AddNewPairedDevicesToHostConnections: { + const userConfiguration: UserConfiguration = Object.assign(new UserConfiguration(), state.userConfiguration); + userConfiguration.hostConnections = state.userConfiguration.hostConnections.filter(hostConnection => hostConnection.type !== HostConnections.Empty); + for (const bleAddress of state.newPairedDevices) { + const hostConnection = new HostConnection(); + hostConnection.type = HostConnections.BLE; + hostConnection.address = bleAddress; + hostConnection.name = 'Bluetooth device'; + + userConfiguration.hostConnections.push(hostConnection); + } + + return { + ...state, + userConfiguration, + newPairedDevicesAdding: true, + }; + } + case UserConfig.ActionTypes.PreviewUserConfiguration: case UserConfig.ActionTypes.LoadResetUserConfiguration: case UserConfig.ActionTypes.LoadUserConfigSuccess: { @@ -187,9 +212,12 @@ export function reducer( } case Device.ActionTypes.ConnectionStateChanged: { + const payload = (action as Device.ConnectionStateChangedAction).payload; + const newState: State = { ...state, - halvesInfo: (action as DeviceActions.ConnectionStateChangedAction).payload.halvesInfo + halvesInfo: payload.halvesInfo, + newPairedDevices: payload.newPairedDevices, }; return assignUserConfiguration(newState, state.userConfiguration); @@ -219,6 +247,14 @@ export function reducer( ) }; + case DeviceActions.ActionTypes.SaveToKeyboardFailed: + case DeviceActions.ActionTypes.SaveToKeyboardSuccess: { + return { + ...state, + newPairedDevicesAdding: false, + }; + } + case KeymapActions.ActionTypes.Add: case KeymapActions.ActionTypes.Duplicate: { const newKeymap: Keymap = new Keymap((action as KeymapActions.AddKeymapAction).payload); @@ -1026,13 +1062,9 @@ export function reducer( case DonglePairing.ActionTypes.DeleteHostConnectionSuccess: { const {index} = (action as DonglePairing.DeleteHostConnectionSuccessAction).payload; const userConfiguration: UserConfiguration = Object.assign(new UserConfiguration(), state.userConfiguration); - userConfiguration.hostConnections = state.userConfiguration.hostConnections.map((hostConnection, idx) => { - if (idx === index) { - return emptyHostConnection(); - } - - return hostConnection; - }); + userConfiguration.hostConnections = [...state.userConfiguration.hostConnections]; + userConfiguration.hostConnections.splice(index, 1); + userConfiguration.hostConnections.push(emptyHostConnection()); return { ...state, @@ -1078,6 +1110,22 @@ export function reducer( } } +export const getNewPairedDevicesState = (state: State): BleAddingState => { + let addingState = BleAddingStates.Idle; + if (state.newPairedDevicesAdding) { + addingState = BleAddingStates.Adding; + } + else if (state.userConfiguration.hostConnections.length > HOST_CONNECTION_COUNT_MAX) { + addingState = BleAddingStates.TooMuchHostConnections; + } + + return { + state: addingState, + nrOfNewBleAddresses: state.newPairedDevices.length, + nrOfHostConnections: state.userConfiguration.hostConnections.length, + showNewPairedDevicesPanel: state.newPairedDevices.length > 0 || state.userConfiguration.hostConnections.length > HOST_CONNECTION_COUNT_MAX || state.newPairedDevicesAdding + }; +}; export const getUserConfiguration = (state: State): UserConfiguration => state.userConfiguration; export const getKeymaps = (state: State): Keymap[] => state.userConfiguration.keymaps; export const getDefaultKeymap = (state: State): Keymap => state.userConfiguration.keymaps.find(keymap => keymap.isDefault);