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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Firmware: 18.0.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/
- Accesibility: improve click-only controls behavior.
- Make host connection management slot-focused instead of host-focused.
- Group macros in the sidebar
- Fix: refuse importing a user configuration newer than Agent supports, and refuse saving one newer than the firmware supports.

## [10.1.0] - 2026-06-23

Expand Down
8 changes: 2 additions & 6 deletions packages/uhk-common/src/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Buffer } from '../buffer.js';

import { HardwareConfiguration, UhkBuffer, UserConfiguration } from '../config-serializer/index.js';
import { UHK_EEPROM_SIZE } from './constants.js';
import { readUserConfigurationVersionFromBinary } from './read-user-configuration-version.js';
import { shouldUpgradeAgent } from './should-upgrade-agent.js';

export const getHardwareConfigFromDeviceResponse = (json: string): HardwareConfiguration => {
Expand Down Expand Up @@ -34,12 +35,7 @@ export const getUserConfigFromDeviceResponse = (json: string): ParsedUserConfigu
try {
const data: number[] = JSON.parse(json);
const uhkBuffer = UhkBuffer.fromArray(data)
const userConfigMajorVersion = uhkBuffer.readUInt16();
const userConfigMinorVersion = uhkBuffer.readUInt16();
const userConfigPatchVersion = uhkBuffer.readUInt16();
uhkBuffer.offset = 0;

const userConfigurationVersion = `${userConfigMajorVersion}.${userConfigMinorVersion}.${userConfigPatchVersion}`
const userConfigurationVersion = readUserConfigurationVersionFromBinary(uhkBuffer);
if (shouldUpgradeAgent(userConfigurationVersion, false)) {
return {
result: 'newer',
Expand Down
2 changes: 2 additions & 0 deletions packages/uhk-common/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ export * from './is-device-protocol-support-status-error.js';
export * from './is-equal-array.js';
export * from './is-official-uhk-firmware.js';
export * from './is-same-firmware.js';
export * from './is-user-config-version-higher-than-firmware.js';
export * from './map-i2c-address-to-module-name.js';
export * from './map-i2c-address-to-slot-id.js';
export * from './read-user-configuration-version.js';
export * from './should-upgrade-agent.js';
export * from './should-upgrade-firmware.js';
export * from './simulate-invalid-user-config-error.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, it } from 'node:test';

import { isUserConfigVersionHigherThanFirmware } from './is-user-config-version-higher-than-firmware.js';

describe('isUserConfigVersionHigherThanFirmware', () => {
it('should return false when versions are equal', ({ assert }) => {
assert.equal(isUserConfigVersionHigherThanFirmware('12.0.1', '12.0.1'), false);
});

it('should return false when user config is older than firmware', ({ assert }) => {
assert.equal(isUserConfigVersionHigherThanFirmware('12.0.0', '12.0.1'), false);
});

it('should return true when user config minor is newer than firmware', ({ assert }) => {
assert.equal(isUserConfigVersionHigherThanFirmware('12.1.0', '12.0.1'), true);
});

it('should return true when user config patch is newer than firmware', ({ assert }) => {
assert.equal(isUserConfigVersionHigherThanFirmware('12.0.2', '12.0.1'), true);
});

it('should return false when user config version is missing', ({ assert }) => {
assert.equal(isUserConfigVersionHigherThanFirmware('', '12.0.1'), false);
});

it('should return false when firmware user config version is missing', ({ assert }) => {
assert.equal(isUserConfigVersionHigherThanFirmware('12.1.0', ''), false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { isVersionGt } from './version-helpers.js';

/**
* Returns true when the user configuration version is higher than what the firmware supports
* (firmwareBuiltUserconfig / rightHalf.userConfigVersion).
*/
export function isUserConfigVersionHigherThanFirmware(
userConfigVersion: string,
firmwareUserConfigVersion: string | undefined,
): boolean {
return Boolean(
userConfigVersion
&& firmwareUserConfigVersion
&& isVersionGt(userConfigVersion, firmwareUserConfigVersion),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it } from 'node:test';

import { UhkBuffer } from '../config-serializer/uhk-buffer.js';
import {
readUserConfigurationVersionFromBinary,
readUserConfigurationVersionFromJsonObject,
} from './read-user-configuration-version.js';

describe('readUserConfigurationVersionFromBinary', () => {
it('should read the version and restore the buffer offset', ({ assert }) => {
const buffer = new UhkBuffer();
buffer.writeUInt16(15);
buffer.writeUInt16(1);
buffer.writeUInt16(2);
buffer.writeUInt16(99);
buffer.offset = 0;

assert.equal(readUserConfigurationVersionFromBinary(buffer), '15.1.2');
assert.equal(buffer.offset, 0);
assert.equal(buffer.readUInt16(), 15);
});
});

describe('readUserConfigurationVersionFromJsonObject', () => {
it('should read the version from a JSON object', ({ assert }) => {
assert.equal(readUserConfigurationVersionFromJsonObject({
userConfigMajorVersion: 15,
userConfigMinorVersion: 1,
userConfigPatchVersion: 0,
}), '15.1.0');
});

it('should return undefined when version fields are missing', ({ assert }) => {
assert.equal(readUserConfigurationVersionFromJsonObject({}), undefined);
assert.equal(readUserConfigurationVersionFromJsonObject(null), undefined);
});
});
28 changes: 28 additions & 0 deletions packages/uhk-common/src/util/read-user-configuration-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { UhkBuffer } from '../config-serializer/uhk-buffer.js';

/**
* Reads user-config version from a binary buffer without advancing the caller's offset permanently.
*/
export function readUserConfigurationVersionFromBinary(buffer: UhkBuffer): string {
const offset = buffer.offset;
const userConfigMajorVersion = buffer.readUInt16();
const userConfigMinorVersion = buffer.readUInt16();
const userConfigPatchVersion = buffer.readUInt16();
buffer.offset = offset;

return `${userConfigMajorVersion}.${userConfigMinorVersion}.${userConfigPatchVersion}`;
}

/**
* Reads user-config version from a JSON object without fully deserializing it.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function readUserConfigurationVersionFromJsonObject(jsonObject: any): string | undefined {
if (jsonObject?.userConfigMajorVersion == null
|| jsonObject?.userConfigMinorVersion == null
|| jsonObject?.userConfigPatchVersion == null) {
return undefined;
}

return `${jsonObject.userConfigMajorVersion}.${jsonObject.userConfigMinorVersion}.${jsonObject.userConfigPatchVersion}`;
}
29 changes: 21 additions & 8 deletions packages/uhk-web/src/app/store/effects/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
HardwareConfiguration,
HOST_CONNECTION_COUNT_MAX,
IpcResponse,
isUserConfigVersionHigherThanFirmware,
NotificationType,
shouldUpgradeFirmware,
UdevRulesInfo,
Expand Down Expand Up @@ -346,26 +347,38 @@ export class DeviceEffects {
.pipe(
ofType<SaveConfigurationAction>(ActionTypes.SaveConfiguration),
withLatestFrom(this.store, this.store.select(getShowFirmwareUpgradePanel)),
tap(([action, state, shouldUpgradeFirmware]) => {
mergeMap(([action, state, shouldUpgradeFirmware]) => {
if (shouldUpgradeFirmware) {
this.router.navigate(['/update-firmware']);
return;
return EMPTY;
}

if (state.userConfiguration.userConfiguration.hostConnections.length > HOST_CONNECTION_COUNT_MAX) {
this.router.navigate(['/host-connections']);
return;
return EMPTY;
}

const userConfig = state.userConfiguration.userConfiguration;
const firmwareUserConfigVersion = state.device.modules.rightModuleInfo?.userConfigVersion;
if (isUserConfigVersionHigherThanFirmware(userConfig.getSemanticVersion(), firmwareUserConfigVersion)) {
return [
new ShowNotificationAction({
type: NotificationType.Error,
message: `The user configuration version (${userConfig.getSemanticVersion()}) is too high for this firmware (supports up to ${firmwareUserConfigVersion}). Please update the firmware or use a compatible configuration.`
}),
new SaveToKeyboardSuccessFailed()
];
}

setTimeout(() => this.sendUserConfigToKeyboard(
state.userConfiguration.userConfiguration,
userConfig,
state.app.hardwareConfig,
action.payload),
100);
}),
switchMap(() => EMPTY)
),
{ dispatch: false }

return EMPTY;
})
)
);

saveConfigurationReply$ = createEffect(() => this.actions$
Expand Down
61 changes: 53 additions & 8 deletions packages/uhk-web/src/app/store/effects/user-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import {
getHardwareConfigFromDeviceResponse,
getUserConfigFromDeviceResponse,
ConfigurationReply,
isUserConfigVersionHigherThanFirmware,
LogService,
NotificationType,
readUserConfigurationVersionFromBinary,
readUserConfigurationVersionFromJsonObject,
RightModuleInfo,
shouldUpgradeAgent,
UHK_60_DEVICE,
UhkBuffer,
UhkDeviceProduct,
Expand Down Expand Up @@ -72,7 +76,6 @@ import {
} from '../actions/device';
import { DeviceRendererService } from '../../services/device-renderer.service';
import { UndoUserConfigData } from '../../models/undo-user-config-data';
import { LoadUserConfigurationFromFilePayload } from '../../models';
import { RouterState } from '../router-util.js';

@Injectable()
Expand Down Expand Up @@ -328,19 +331,52 @@ export class UserConfigEffects {
loadUserConfigurationFromFile$ = createEffect(() => this.actions$
.pipe(
ofType<LoadUserConfigurationFromFileAction>(ActionTypes.LoadUserConfigurationFromFile),
withLatestFrom(this.store.select(getUserConfiguration)),
map(([action, currentUserConfiguration]): [LoadUserConfigurationFromFilePayload, string] =>
[action.payload, currentUserConfiguration.deviceName]),
map(([payload, deviceName]: [LoadUserConfigurationFromFilePayload, string]) => {
withLatestFrom(
this.store.select(getUserConfiguration),
this.store.select(disableUpdateAgentProtection),
this.store.select(getHardwareModules),
),
map(([action, currentUserConfiguration, disableUpdateAgentProtection, hardwareModules]) => {
const payload = action.payload;
const deviceName = currentUserConfiguration.deviceName;
const firmwareUserConfigVersion = hardwareModules.rightModuleInfo?.userConfigVersion;
const userConfigTooHighForAgentNotification = (importedVersion: string) => new ShowNotificationAction({
type: NotificationType.Error,
message: `The imported user configuration version (${importedVersion}) is too high for this Agent (supports up to ${VERSIONS.userConfigVersion}). Please update Agent.`
});
const userConfigTooHighForFirmwareNotification = (importedVersion: string) => new ShowNotificationAction({
type: NotificationType.Error,
message: `The imported user configuration version (${importedVersion}) is too high for this firmware (supports up to ${firmwareUserConfigVersion}). Please update the firmware or use a compatible configuration.`
});

let importedUserConfigVersion: string | undefined;

try {
let userConfig = new UserConfiguration();

if (payload.uploadFileData.filename.endsWith('.bin')) {
userConfig.fromBinary(UhkBuffer.fromArray(payload.uploadFileData.data));
const uhkBuffer = UhkBuffer.fromArray(payload.uploadFileData.data);
importedUserConfigVersion = readUserConfigurationVersionFromBinary(uhkBuffer);
if (shouldUpgradeAgent(importedUserConfigVersion, disableUpdateAgentProtection)) {
return userConfigTooHighForAgentNotification(importedUserConfigVersion);
}
if (isUserConfigVersionHigherThanFirmware(importedUserConfigVersion, firmwareUserConfigVersion)) {
return userConfigTooHighForFirmwareNotification(importedUserConfigVersion);
}
userConfig.fromBinary(uhkBuffer);
} else {
const buffer = Buffer.from(payload.uploadFileData.data);
const json = buffer.toString();
userConfig.fromJsonObject(JSON.parse(json));
const json = JSON.parse(buffer.toString());
importedUserConfigVersion = readUserConfigurationVersionFromJsonObject(json);
if (importedUserConfigVersion
&& shouldUpgradeAgent(importedUserConfigVersion, disableUpdateAgentProtection)) {
return userConfigTooHighForAgentNotification(importedUserConfigVersion);
}
if (importedUserConfigVersion
&& isUserConfigVersionHigherThanFirmware(importedUserConfigVersion, firmwareUserConfigVersion)) {
return userConfigTooHighForFirmwareNotification(importedUserConfigVersion);
}
userConfig.fromJsonObject(json);
}

if (userConfig.userConfigMajorVersion) {
Expand All @@ -367,6 +403,15 @@ export class UserConfigEffects {
message: 'Invalid configuration specified.'
});
} catch (err) {
if (importedUserConfigVersion
&& shouldUpgradeAgent(importedUserConfigVersion, false)) {
return userConfigTooHighForAgentNotification(importedUserConfigVersion);
}
if (importedUserConfigVersion
&& isUserConfigVersionHigherThanFirmware(importedUserConfigVersion, firmwareUserConfigVersion)) {
return userConfigTooHighForFirmwareNotification(importedUserConfigVersion);
}

return new ShowNotificationAction({
type: NotificationType.Error,
message: 'Invalid configuration specified.'
Expand Down