Skip to content
Merged
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
36 changes: 35 additions & 1 deletion packages/uhk-agent/src/services/device.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ import {
import { QueueManager } from './queue-manager';
import { ZephyrLogService } from './zephyr-log.service';

// the poller iterates at most every 250ms, so it is at least 10 seconds
const DONGLE_BOND_POLL_ITERATIONS = 40;

/**
* IpcMain pair of the UHK Communication
* Functionality:
Expand Down Expand Up @@ -1231,6 +1234,8 @@ export class DeviceService {
private async uhkDevicePoller(): Promise<void> {
let deviceProtocolVersion: string;
let iterationCount = 0;
// set only while a dongle that can be queried is connected
let dongleBond: { deviceBleAddress: number[], isBondBroken: boolean };

while (true) {
if (this._pollerAllowed) {
Expand All @@ -1241,6 +1246,7 @@ export class DeviceService {
const state = await this.device.getDeviceConnectionStateAsync();
if (!isEqual(state, this.savedState)) {
const newState = cloneDeep(state);
dongleBond = undefined;

if (state.hasPermission && state.communicationInterfaceAvailable) {
state.hardwareModules = await this.getHardwareModules(false);
Expand All @@ -1264,7 +1270,10 @@ export class DeviceService {
dongleUhkDevice = new UhkHidDevice(this.logService, this.options, this.rootDir, dongle);
const dongleBleAddress = await dongleUhkDevice.getBleAddress();
state.dongle.bleAddress = convertBleAddressArrayToString(dongleBleAddress);
state.dongle.isPairedWithKeyboard = await dongleUhkDevice.isPairedWith(deviceBleAddress);
const dongleBondState = await dongleUhkDevice.getBondState(deviceBleAddress);
state.dongle.isPairedWithKeyboard = dongleBondState.isPaired;
state.dongle.isBondBroken = dongleBondState.isBondBroken;
dongleBond = { deviceBleAddress, isBondBroken: dongleBondState.isBondBroken };
state.isPairedWithDongle = await this.device.isPairedWith(dongleBleAddress);
const dongleOperations = new UhkOperations(this.logService, dongleUhkDevice);
state.dongle.versionInfo = await dongleOperations.getDeviceVersionInfo();
Expand Down Expand Up @@ -1299,6 +1308,13 @@ export class DeviceService {

this.logService.misc('[DeviceService] Device connection state changed to:', JSON.stringify(state, null, 2));
}
else if (dongleBond && iterationCount % DONGLE_BOND_POLL_ITERATIONS === 0) {
// The dongle is queried only when the connection state changes, but its bond can break at any time.
const isBondBroken = await this.isDongleBondBroken(dongleBond.deviceBleAddress);
if (isBondBroken !== dongleBond.isBondBroken) {
this.savedState = undefined;
}
}

if (state.isMacroStatusDirty) {
this._checkStatusBuffer = true;
Expand Down Expand Up @@ -1327,6 +1343,24 @@ export class DeviceService {
}
}

private async isDongleBondBroken(deviceBleAddress: number[]): Promise<boolean> {
await this.dongleZephyrLogService.disable();
let dongleUhkDevice: UhkHidDevice;
try {
const dongle = await getCurrentUhkDongleHID();
dongleUhkDevice = new UhkHidDevice(this.logService, this.options, this.rootDir, dongle);
const bondState = await dongleUhkDevice.getBondState(deviceBleAddress);

return bondState.isBondBroken;
}
finally {
if (dongleUhkDevice) {
await dongleUhkDevice.close();
}
await this.dongleZephyrLogService.enable();
}
}

private async pollDebugInfo(iterationCount: number): Promise<void> {
if(!this.isI2cDebuggingEnabled) {
return;
Expand Down
4 changes: 4 additions & 0 deletions packages/uhk-common/src/models/device-connection-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface DeviceConnectionState {
dongle: Dongle;
leftHalfBootloaderActive: boolean;
hasPermission: boolean;
/**
* The left half rejected the bond of the right half (GetDeviceState byte 2 bit 4).
*/
isHalvesBondBroken: boolean;
bootloaderActive: boolean;
isMacroStatusDirty: boolean;
isZephyrLogAvailable: boolean;
Expand Down
5 changes: 5 additions & 0 deletions packages/uhk-common/src/models/dongle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ export interface Dongle {

bootloaderActive: boolean;

/**
* The keyboard rejected the bond of the dongle.
*/
isBondBroken?: boolean;

isPairedWithKeyboard?: boolean;
/**
* True if more than 1 UHK dongle connected.
Expand Down
7 changes: 7 additions & 0 deletions packages/uhk-usb/src/models/bond-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface BondState {
isPaired: boolean;
/**
* The peer rejected the bond (IsPaired response byte 2).
*/
isBondBroken: boolean;
}
1 change: 1 addition & 0 deletions packages/uhk-usb/src/models/device-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface DeviceState {
isEepromBusy: boolean;
isMacroStatusDirty: boolean;
isPeerBondBroken: boolean;
isZephyrLogAvailable: boolean;
areHalvesMerged: boolean;
isLeftHalfConnected: boolean;
Expand Down
1 change: 1 addition & 0 deletions packages/uhk-usb/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './bond-state.js';
export * from './debug-info.js';
export * from './device-state.js';
export * from './duration.js';
Expand Down
19 changes: 16 additions & 3 deletions packages/uhk-usb/src/uhk-hid-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
UsbCommand
} from './constants.js';
import {
BondState,
DeviceState,
PairingInfo,
ReenumerateOption,
Expand Down Expand Up @@ -229,6 +230,12 @@ export class UhkHidDevice {
}

public async isPairedWith(address: number[]): Promise<boolean> {
const bondState = await this.getBondState(address);

return bondState.isPaired;
}

public async getBondState(address: number[]): Promise<BondState> {
await this.assertDeviceSupportWirelessUSBCommands();

this.logService.usbOps('[UhkHidDevice] USB[T]: is paired with');
Expand All @@ -237,10 +244,13 @@ export class UhkHidDevice {
...address,
]);
const responseBuffer = await this.write(buffer);
// 1st byte is the status code we skip it
const response = responseBuffer.readUInt8(1);

return response === 1;
// 1st byte is the status code we skip it
return {
isPaired: responseBuffer.readUInt8(1) === 1,
// older firmwares always send 0
isBondBroken: responseBuffer.readUInt8(2) === 1,
};
}

public async pairCentral(): Promise<void> {
Expand Down Expand Up @@ -328,6 +338,7 @@ export class UhkHidDevice {
rightModuleSlot: RightSlotModules.NoModule
},
hardwareModules: {},
isHalvesBondBroken: false,
isMacroStatusDirty: false,
isZephyrLogAvailable: false,
leftHalfDetected: false,
Expand Down Expand Up @@ -410,6 +421,7 @@ export class UhkHidDevice {
const deviceState = await this.getDeviceState();
result.activeKeymapIndex = deviceState.activeKeymapIndex;
result.halvesInfo = calculateHalvesState(deviceState);
result.isHalvesBondBroken = deviceState.isPeerBondBroken;
result.isMacroStatusDirty = deviceState.isMacroStatusDirty;
result.isZephyrLogAvailable = deviceState.isZephyrLogAvailable;

Expand Down Expand Up @@ -635,6 +647,7 @@ export class UhkHidDevice {
leftModuleSlot: MODULE_ID_TO_STRING[buffer[4]],
newPairedDevice: isBitSet(buffer[2], 2),
isZephyrLogAvailable: isBitSet(buffer[2], 3),
isPeerBondBroken: isBitSet(buffer[2], 4),
rightModuleSlot: MODULE_ID_TO_STRING[buffer[5]]
};
}
Expand Down
6 changes: 6 additions & 0 deletions packages/uhk-web/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
<dongle-pairing-panel *ngIf="donglePairingState?.showDonglePairingPanel"
[@topNotificationPanelVisible]
[state]="donglePairingState.state"
[isBondBroken]="donglePairingState.isBondBroken"
(pairDongle)="startDonglePairing()"
/>
<halves-pairing-panel *ngIf="halvesPairingState?.showHalvesPairingPanel"
[@topNotificationPanelVisible]
[isPairing]="halvesPairingState.isPairing"
(pairHalves)="startHalvesPairing()"
/>
<firmware-upgrade-panel *ngIf="showFirmwareUpgradePanel"
[@topNotificationPanelVisible]
(updateFirmware)="updateFirmware()"
Expand Down
1 change: 1 addition & 0 deletions packages/uhk-web/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

ble-pairing-panel,
dongle-pairing-panel,
halves-pairing-panel,
firmware-upgrade-panel,
app-update-available,
shared-config-change-panel {
Expand Down
17 changes: 16 additions & 1 deletion packages/uhk-web/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { EnableUsbStackTestAction, UpdateFirmwareAction } from './store/actions/
import {
AppState,
getDonglePairingState,
getHalvesPairingState,
getNewPairedDevicesState,
getErrorPanelHeight,
getShowAppUpdateAvailable,
Expand All @@ -32,6 +33,7 @@ import {
getShowFirmwareUpgradePanel,
getSharedConfigChange
} from './store';
import { StartLeftHalfPairingAction } from './store/actions/advance-settings.action';
import { StartDonglePairingAction } from './store/actions/dongle-pairing.action';
import { AddNewPairedDevicesToHostConnectionsAction } from './store/actions/user-config';
import { ProgressButtonState } from './store/reducers/progress-button-state';
Expand All @@ -45,7 +47,7 @@ import {
ApplySharedConfigChangeAction,
DismissSharedConfigChangeAction,
} from './store/actions/app';
import { BleAddingState, DonglePairingState, OutOfSpaceWarningData } from './models';
import { BleAddingState, DonglePairingState, HalvesPairingState, OutOfSpaceWarningData } from './models';
import { filter } from 'rxjs/operators';
import { SecondSideMenuContainerComponent } from './components/side-menu';

Expand Down Expand Up @@ -114,6 +116,7 @@ export class MainAppComponent implements OnDestroy {
@ViewChild('manuallyUpdateNotification', { static: true }) manuallyUpdateNotificationTmpl;

donglePairingState: DonglePairingState;
halvesPairingState: HalvesPairingState;
newPairedDevicesState: BleAddingState;
showFirmwareUpgradePanel: boolean;
showUpdateAvailable: boolean;
Expand All @@ -135,6 +138,7 @@ export class MainAppComponent implements OnDestroy {
private actionsSubscription: Subscription;
private readonly cdRef = inject(ChangeDetectorRef);
private donglePairingStateSubscription: Subscription;
private halvesPairingStateSubscription: Subscription;
private newPairedDevicesStateSubscription: Subscription;
private errorPanelHeightSubscription: Subscription;
private readonly keyboardSvgExportService = inject(KeyboardSvgExportService);
Expand Down Expand Up @@ -172,6 +176,11 @@ export class MainAppComponent implements OnDestroy {
this.donglePairingState = data;
this.cdRef.markForCheck();
});
this.halvesPairingStateSubscription = this.store.select(getHalvesPairingState)
.subscribe(data => {
this.halvesPairingState = data;
this.cdRef.markForCheck();
});
this.newPairedDevicesStateSubscription = this.store.select(getNewPairedDevicesState)
.subscribe(data => {
this.newPairedDevicesState = data;
Expand Down Expand Up @@ -248,6 +257,7 @@ export class MainAppComponent implements OnDestroy {
ngOnDestroy(): void {
this.actionsSubscription.unsubscribe();
this.donglePairingStateSubscription.unsubscribe();
this.halvesPairingStateSubscription.unsubscribe();
this.newPairedDevicesStateSubscription.unsubscribe();
this.errorPanelHeightSubscription.unsubscribe();
this.saveToKeyboardStateSubscription.unsubscribe();
Expand Down Expand Up @@ -338,6 +348,7 @@ export class MainAppComponent implements OnDestroy {
return this.showFirmwareUpgradePanel
|| this.showUpdateAvailable
|| this.donglePairingState?.showDonglePairingPanel
|| this.halvesPairingState?.showHalvesPairingPanel
|| this.newPairedDevicesState?.showNewPairedDevicesPanel
|| !!this.sharedConfigChange;
}
Expand All @@ -356,6 +367,10 @@ export class MainAppComponent implements OnDestroy {
this.store.dispatch(new UpdateFirmwareAction(false));
}

startHalvesPairing(): void {
this.store.dispatch(new StartLeftHalfPairingAction());
}

startDonglePairing(): void {
this.store.dispatch(new StartDonglePairingAction());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<div class="dongle-pairing-panel-wrapper" role="alert">
<span *ngIf="state === DonglePairingStates.Idle">
<span *ngIf="state === DonglePairingStates.Idle && isBondBroken">
The pairing of the dongle is broken. <a routerLink="/device/host-connections" (click)="pairDongle.emit()">Re-pair it</a> with this keyboard.
</span>

<span *ngIf="state === DonglePairingStates.Idle && !isBondBroken">
A dongle is detected. <a routerLink="/device/host-connections" (click)="pairDongle.emit()">Pair it</a> with this keyboard.
</span>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DonglePairingStates } from '../../../models';
})
export class DonglePairingPanelComponent {
@Input() state: DonglePairingStates;
@Input() isBondBroken: boolean;

@Output() pairDongle = new EventEmitter<void>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="halves-pairing-panel-wrapper" role="alert">
<span *ngIf="!isPairing">
The pairing of the keyboard halves is broken. Connect both halves via USB and <a href="#" (click)="$event.preventDefault(); pairHalves.emit()">re-pair them</a>.
</span>

<span *ngIf="isPairing">
Pairing keyboard halves... <fa-icon [icon]="faSpinner" animation="spin"></fa-icon>
</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import '../../../../styles/variables';

.halves-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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';

@Component({
selector: 'halves-pairing-panel',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
templateUrl: './halves-pairing-panel.component.html',
styleUrls: ['./halves-pairing-panel.component.scss']
})
export class HalvesPairingPanelComponent {
@Input() isPairing: boolean;

@Output() pairHalves = new EventEmitter<void>();

protected readonly faSpinner = faSpinner;
}
1 change: 1 addition & 0 deletions packages/uhk-web/src/app/models/dongle-pairing-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum DongleOperations {

export interface DonglePairingState {
showDonglePairingPanel: boolean;
isBondBroken: boolean;
operation: DongleOperations;
state: DonglePairingStates;
}
4 changes: 4 additions & 0 deletions packages/uhk-web/src/app/models/halves-pairing-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface HalvesPairingState {
showHalvesPairingPanel: boolean;
isPairing: boolean;
}
1 change: 1 addition & 0 deletions packages/uhk-web/src/app/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './copied-layer-origin';
export * from './delete-host-connection-payload';
export * from './device-ui-states';
export * from './dongle-pairing-state';
export * from './halves-pairing-state';
export * from './duplicate-macro-action-payload';
export * from './erase-ble-settings-button-state';
export * from './exchange-keys-action.model';
Expand Down
2 changes: 2 additions & 0 deletions packages/uhk-web/src/app/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import CircleTooltipComponent from './components/circle-tooltip/circle-tooltip.c
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 { HalvesPairingPanelComponent } from './components/device/halves-pairing-panel/halves-pairing-panel.component';
import { SharedConfigChangePanelComponent } from './components/shared-config-change-panel/shared-config-change-panel.component';
import { KeyboardSliderComponent } from './components/keyboard/slider';
import {
Expand Down Expand Up @@ -204,6 +205,7 @@ import appInitFactory from './services/app-init-factory';
KeymapHeaderComponent,
NotificationComponent,
DonglePairingPanelComponent,
HalvesPairingPanelComponent,
SharedConfigChangePanelComponent,
SvgIconTextKeyComponent,
SvgKeyboardKeyComponent,
Expand Down
Loading
Loading