Files
agent/packages/uhk-usb/src/uhk-hid-device.ts
Róbert Kiss cbccaba1c5 feat: Display/hide left keyboard half and merged/unmerged state (#987)
* feat: Display/hide left keyboard half and merged/unmerged state

* feat: improve the animation

* feat: decrease left fade animation time
2019-07-25 23:41:20 +02:00

375 lines
13 KiB
TypeScript

import { Device, devices, HID } from 'node-hid';
import { pathExists } from 'fs-extra';
import * as path from 'path';
import { platform } from 'os';
import { CommandLineArgs, DeviceConnectionState, HalvesInfo, isEqualArray, LogService, UdevRulesInfo } from 'uhk-common';
import {
ConfigBufferId,
Constants,
EepromOperation,
enumerationModeIdToProductId,
EnumerationModes,
KbootCommands,
ModuleSlotToI2cAddress,
ModuleSlotToId,
UsbCommand,
UsbVariables
} from './constants';
import { bufferToString, getFileContentAsync, getTransferData, isUhkDevice, isUhkZeroInterface, retry, snooze } from './util';
export const BOOTLOADER_TIMEOUT_MS = 5000;
/**
* HID API wrapper to support unified logging and async write
*/
export class UhkHidDevice {
/**
* Internal variable that represent the USB UHK device
* @private
*/
private _prevDevices = [];
private _device: HID;
private _hasPermission = false;
private _udevRulesInfo = UdevRulesInfo.Unknown;
constructor(private logService: LogService,
private options: CommandLineArgs,
private rootDir: string) {
}
/**
* Return true if the app has right to communicate over the USB.
* Need only on linux.
* If return false need to run {project-root}/rules/setup-rules.sh or
* the Agent will ask permission to run at the first time.
* @returns {boolean}
*/
public hasPermission(): boolean {
if (this.options.spe) {
return false;
}
try {
if (this._hasPermission) {
return true;
}
this.logService.debug('[UhkHidDevice] Devices before check permission:');
const devs = devices();
this.logDevices(devs);
const dev = devs.find((x: Device) => isUhkZeroInterface(x) || x.productId === Constants.BOOTLOADER_ID);
if (!dev) {
return true;
}
const device = new HID(dev.path);
device.close();
this._hasPermission = true;
return this._hasPermission;
} catch (err) {
this.logService.error('[UhkHidDevice] hasPermission', err);
}
return false;
}
/**
* Return with the USB device communication sate.
* @returns {DeviceConnectionState}
*/
public async getDeviceConnectionStateAsync(): Promise<DeviceConnectionState> {
const devs = devices();
const result: DeviceConnectionState = {
bootloaderActive: false,
connected: false,
zeroInterfaceAvailable: false,
hasPermission: this.hasPermission(),
udevRulesInfo: await this.getUdevInfoAsync(),
halvesInfo: { areHalvesMerged: true, isLeftHalfConnected: true }
};
for (const dev of devs) {
if (isUhkDevice(dev)) {
result.connected = true;
}
if (isUhkZeroInterface(dev)) {
result.zeroInterfaceAvailable = true;
} else if (dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.BOOTLOADER_ID) {
result.bootloaderActive = true;
}
}
if (result.connected && result.hasPermission && result.zeroInterfaceAvailable) {
result.halvesInfo = await this.getHalvesStates();
} else if (!result.connected) {
this._device = undefined;
}
return result;
}
/**
* Send data to the UHK device and wait for the response.
* Throw an error when 1st byte of the response is not 0
* @param {Buffer} buffer
* @returns {Promise<Buffer>}
*/
public async write(buffer: Buffer): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const device = this.getDevice();
if (!device) {
return reject(new Error('[UhkHidDevice] Device is not connected'));
}
device.read((err: any, receivedData: Array<number>) => {
if (err) {
this.logService.error('[UhkHidDevice] Transfer error: ', err);
this.close();
return reject(err);
}
const logString = bufferToString(receivedData);
this.logService.debug('[UhkHidDevice] USB[R]:', logString);
if (receivedData[0] !== 0) {
return reject(new Error(`Communications error with UHK. Response code: ${receivedData[0]}`));
}
return resolve(Buffer.from(receivedData));
});
const sendData = getTransferData(buffer);
this.logService.debug('[UhkHidDevice] USB[W]:', bufferToString(sendData).substr(3));
device.write(sendData);
});
}
public async writeConfigToEeprom(configBufferId: ConfigBufferId): Promise<void> {
await this.write(Buffer.from([UsbCommand.LaunchEepromTransfer, EepromOperation.write, configBufferId]));
await this.waitUntilKeyboardBusy();
}
public async enableUsbStackTest(): Promise<void> {
await this.write(Buffer.from([UsbCommand.SetVariable, UsbVariables.testUsbStack, 1]));
await this.waitUntilKeyboardBusy();
}
/**
* Close the communication chanel with UHK Device
*/
public close(): void {
this.logService.debug('[UhkHidDevice] Device communication closing.');
if (!this._device) {
return;
}
this._device.close();
this._device = null;
this.logService.debug('[UhkHidDevice] Device communication closed.');
}
public async waitUntilKeyboardBusy(): Promise<void> {
while (true) {
const buffer = await this.write(Buffer.from([UsbCommand.GetDeviceState]));
if (buffer[1] === 0) {
break;
}
this.logService.debug('Keyboard is busy, wait...');
await snooze(200);
}
}
public resetDeviceCache(): void {
this._prevDevices = [];
}
async reenumerate(enumerationMode: EnumerationModes): Promise<void> {
const reenumMode = EnumerationModes[enumerationMode].toString();
this.logService.debug(`[UhkHidDevice] Start reenumeration, mode: ${reenumMode}`);
const message = Buffer.from([
UsbCommand.Reenumerate,
enumerationMode,
BOOTLOADER_TIMEOUT_MS & 0xff,
(BOOTLOADER_TIMEOUT_MS & 0xff << 8) >> 8,
(BOOTLOADER_TIMEOUT_MS & 0xff << 16) >> 16,
(BOOTLOADER_TIMEOUT_MS & 0xff << 24) >> 24
]);
const enumeratedProductId = enumerationModeIdToProductId[enumerationMode.toString()];
const startTime = new Date();
let jumped = false;
while (new Date().getTime() - startTime.getTime() < 20000) {
const devs = devices();
this.logService.silly('[UhkHidDevice] reenumeration devices', devs);
const inBootloaderMode = devs.some((x: Device) =>
x.vendorId === Constants.VENDOR_ID &&
x.productId === enumeratedProductId);
if (inBootloaderMode) {
this.logService.debug(`[UhkHidDevice] reenumeration devices up`);
return;
}
this.logService.silly(`[UhkHidDevice] Could not find reenumerated device: ${reenumMode}. Waiting...`);
await snooze(100);
if (!jumped) {
const device = this.getDevice();
if (device) {
const data = getTransferData(message);
this.logService.debug(`[UhkHidDevice] USB[T]: Enumerate device. Mode: ${reenumMode}`);
this.logService.debug('[UhkHidDevice] USB[W]:', bufferToString(data).substr(3));
device.write(data);
device.close();
jumped = true;
} else {
this.logService.silly(`[UhkHidDevice] USB[T]: Enumerate device is not ready yet}`);
}
}
}
this.logService.error(`[UhkHidDevice] Could not find reenumerated device: ${reenumMode}. Timeout`);
throw new Error(`Could not reenumerate as ${reenumMode}`);
}
async sendKbootCommandToModule(module: ModuleSlotToI2cAddress, command: KbootCommands, maxTry = 1): Promise<any> {
let transfer;
const moduleName = kbootCommandName(module);
this.logService.debug(`[UhkHidDevice] USB[T]: Send KbootCommand ${moduleName} ${KbootCommands[command].toString()}`);
if (command === KbootCommands.idle) {
transfer = Buffer.from([UsbCommand.SendKbootCommandToModule, command]);
} else {
transfer = Buffer.from([UsbCommand.SendKbootCommandToModule, command, module]);
}
await retry(async () => await this.write(transfer), maxTry, this.logService);
}
async jumpToBootloaderModule(module: ModuleSlotToId): Promise<any> {
this.logService.debug(`[UhkHidDevice] USB[T]: Jump to bootloader. Module: ${ModuleSlotToId[module].toString()}`);
const transfer = Buffer.from([UsbCommand.JumpToModuleBootloader, module]);
await this.write(transfer);
}
async getHalvesStates(): Promise<HalvesInfo> {
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}
* @private
*/
private getDevice() {
if (!this._device) {
this._device = this.connectToDevice();
}
return this._device;
}
/**
* Initialize new UHK HID device.
* @returns {HID}
*/
private connectToDevice(): HID {
try {
const devs = devices();
let compareDevices = devs as any;
if (platform() === 'linux') {
compareDevices = devs.map(x => ({
productId: x.productId,
vendorId: x.vendorId,
interface: x.interface
}));
}
if (!isEqualArray(this._prevDevices, compareDevices)) {
this.logService.debug('[UhkHidDevice] Available devices:');
this.logDevices(devs);
this._prevDevices = compareDevices;
} else {
this.logService.debug('[UhkHidDevice] Available devices unchanged');
}
const dev = devs.find(isUhkZeroInterface);
if (!dev) {
this.logService.debug('[UhkHidDevice] UHK Device not found:');
return null;
}
const device = new HID(dev.path);
this.logService.debug('[UhkHidDevice] Used device:', JSON.stringify(dev));
return device;
} catch (err) {
this.logService.error('[UhkHidDevice] Can not create device:', err);
}
return null;
}
private logDevices(devs: Array<Device>): void {
for (const logDevice of devs) {
this.logService.debug(JSON.stringify(logDevice));
}
}
private async getUdevInfoAsync(): Promise<UdevRulesInfo> {
if (this._udevRulesInfo === UdevRulesInfo.Ok) {
return UdevRulesInfo.Ok;
}
if (process.platform === 'win32' || process.platform === 'darwin') {
this._udevRulesInfo = UdevRulesInfo.Ok;
return UdevRulesInfo.Ok;
}
if (!(await pathExists('/etc/udev/rules.d/50-uhk60.rules'))) {
return UdevRulesInfo.NeedToSetup;
}
const expectedUdevSettings = await getFileContentAsync(path.join(this.rootDir, 'rules/50-uhk60.rules'));
const v2UdevSettings = await getFileContentAsync(path.join(this.rootDir, 'rules/50-uhk60_v2.rules'));
const currentUdevSettings = await getFileContentAsync('/etc/udev/rules.d/50-uhk60.rules');
if (isEqualArray(expectedUdevSettings, currentUdevSettings) ||
isEqualArray(v2UdevSettings, currentUdevSettings)) {
this._udevRulesInfo = UdevRulesInfo.Ok;
return UdevRulesInfo.Ok;
}
return UdevRulesInfo.Different;
}
}
function kbootCommandName(module: ModuleSlotToI2cAddress): string {
switch (module) {
case ModuleSlotToI2cAddress.leftHalf:
return 'leftHalf';
case ModuleSlotToI2cAddress.leftModule:
return 'leftModule';
case ModuleSlotToI2cAddress.rightModule:
return 'rightModule';
default:
return 'Unknown';
}
}