diff --git a/.travis.yml b/.travis.yml
index 94804bdb..a2bf69b7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,6 +28,9 @@ addons:
- libgnome-keyring-dev
- libsecret-1-dev
- icnsutils
+ - libudev-dev
+ - build-essential
+ - libusb-1.0-0-dev
install:
- nvm install
@@ -45,9 +48,3 @@ script:
cache:
directories:
- node_modules
-
-addons:
- apt:
- packages:
- - build-essential
- - libudev-dev
diff --git a/electron/src/app.module.ts b/electron/src/app.module.ts
index f80c8d2d..87e8f5ff 100644
--- a/electron/src/app.module.ts
+++ b/electron/src/app.module.ts
@@ -1,4 +1,4 @@
-import { NgModule } from '@angular/core';
+import { ErrorHandler, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@@ -75,7 +75,9 @@ import { SafeStylePipe } from './shared/pipes';
import { CaptureService } from './shared/services/capture.service';
import { MapperService } from './shared/services/mapper.service';
import { SvgModuleProviderService } from './shared/services/svg-module-provider.service';
-import { UhkDeviceService } from './services/uhk-device.service';
+import { UhkLibUsbApiService } from './services/uhk-lib-usb-api.service';
+import { UhkHidApiService } from './services/uhk-hid-api.service';
+import { uhkDeviceProvider } from './services/uhk-device-provider';
import { AutoUpdateSettingsEffects, KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects';
import { ApplicationEffect, AppUpdateEffect } from './store/effects';
@@ -90,6 +92,9 @@ import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service';
+import { ElectronLogService } from './services/electron-log.service';
+import { LOG_SERVICE } from '../../shared/src/services/logger.service';
+import { ElectronErrorHandlerService } from './services/electron-error-handler.service';
import { AppUpdateRendererService } from './services/app-update-renderer.service';
import { reducer } from './store';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
@@ -185,10 +190,15 @@ import { AutoUpdateSettings } from './shared/components/auto-update-settings/aut
KeymapEditGuard,
MacroNotFoundGuard,
CaptureService,
- UhkDeviceService,
{ provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService },
DefaultUserConfigurationService,
- AppUpdateRendererService
+ { provide: LOG_SERVICE, useClass: ElectronLogService },
+ { provide: ErrorHandler, useClass: ElectronErrorHandlerService },
+ AppUpdateRendererService,
+ UhkHidApiService,
+ UhkLibUsbApiService,
+ uhkDeviceProvider()
+
],
bootstrap: [AppComponent]
})
diff --git a/electron/src/components/missing-device/missing-device.component.ts b/electron/src/components/missing-device/missing-device.component.ts
index 0b153e21..d2643bb5 100644
--- a/electron/src/components/missing-device/missing-device.component.ts
+++ b/electron/src/components/missing-device/missing-device.component.ts
@@ -1,12 +1,11 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
-import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/takeWhile';
-import { UhkDeviceService } from './../../services/uhk-device.service';
+import { UhkDeviceService } from '../../services/uhk-device.service';
@Component({
selector: 'missing-device',
diff --git a/electron/src/components/privilege-checker/privilege-checker.component.ts b/electron/src/components/privilege-checker/privilege-checker.component.ts
index b16aba51..3ca98aee 100644
--- a/electron/src/components/privilege-checker/privilege-checker.component.ts
+++ b/electron/src/components/privilege-checker/privilege-checker.component.ts
@@ -1,7 +1,7 @@
///
-import { Component } from '@angular/core';
+import { Component, Inject } from '@angular/core';
import { Router } from '@angular/router';
-
+import * as isDev from 'electron-is-dev';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/observable/of';
@@ -15,6 +15,7 @@ import * as path from 'path';
import * as sudo from 'sudo-prompt';
import { UhkDeviceService } from '../../services/uhk-device.service';
+import { ILogService, LOG_SERVICE } from '../../../../shared/src/services/logger.service';
@Component({
selector: 'privilege-checker',
@@ -23,7 +24,18 @@ import { UhkDeviceService } from '../../services/uhk-device.service';
})
export class PrivilegeCheckerComponent {
- constructor(private router: Router, private uhkDevice: UhkDeviceService) {
+ private rootDir: string;
+
+ constructor(private router: Router,
+ private uhkDevice: UhkDeviceService,
+ @Inject(LOG_SERVICE) private logService: ILogService) {
+ if (isDev) {
+ this.rootDir = path.resolve(path.join(remote.process.cwd(), remote.process.argv[1]), '..');
+ } else {
+ this.rootDir = path.dirname(remote.app.getAppPath());
+ }
+ this.logService.info('App root dir: ', this.rootDir);
+
uhkDevice.isConnected()
.distinctUntilChanged()
.takeWhile(connected => connected)
@@ -50,14 +62,21 @@ export class PrivilegeCheckerComponent {
case 'linux':
permissionSetter = this.setUpPermissionsOnLinux();
break;
+ // HID API shouldn't need privilege escalation on Windows
+ // TODO: If all HID API test success then delete this branch and setUpPermissionsOnWin() method
+ // case 'win32':
+ // permissionSetter = this.setUpPermissionsOnWin();
+ // break;
default:
permissionSetter = Observable.throw('Permissions couldn\'t be set. Invalid platform: ' + process.platform);
break;
}
permissionSetter.subscribe({
- error: e => console.error(e),
+ error: e => {
+ console.log(e);
+ },
complete: () => {
- console.log('Permissions has been successfully set');
+ this.logService.info('Permissions has been successfully set');
this.uhkDevice.initialize();
this.router.navigate(['/']);
}
@@ -66,12 +85,13 @@ export class PrivilegeCheckerComponent {
private setUpPermissionsOnLinux(): Observable {
const subject = new ReplaySubject();
- const rootDir = path.resolve(path.join(remote.process.cwd(), remote.process.argv[1]), '..');
- const scriptPath = path.resolve(rootDir, 'rules/setup-rules.sh');
+ const scriptPath = path.resolve(this.rootDir, 'rules/setup-rules.sh');
const options = {
name: 'Setting UHK access rules'
};
- sudo.exec(`sh ${scriptPath}`, options, (error: any) => {
+ const command = `sh ${scriptPath}`;
+ console.log(command);
+ sudo.exec(command, options, (error: any) => {
if (error) {
subject.error(error);
} else {
@@ -82,4 +102,34 @@ export class PrivilegeCheckerComponent {
return subject.asObservable();
}
+ // TODO: If all HID API test success then delete this method.
+ // and remove zadic-${process.arch}.exe files from windows installer resources
+ // private setUpPermissionsOnWin(): Observable {
+ // const subject = new ReplaySubject();
+ //
+ // // source code: https://github.com/pbatard/libwdi
+ // const scriptPath = path.resolve(this.rootDir, `rules/zadic-${process.arch}.exe`);
+ // const options = {
+ // name: 'Setting UHK access rules'
+ // };
+ // const params = [
+ // `--vid $\{Constants.VENDOR_ID}`,
+ // `--pid $\{Constants.PRODUCT_ID}`,
+ // '--iface 0', // interface ID
+ // '--usealldevices', // if the device has installed USB driver than overwrite it
+ // '--noprompt' // return at the end of the installation and not waiting for any user command
+ // ];
+ // const paramsString = params.join(' ');
+ // const command = `"${scriptPath}" ${paramsString}`;
+ //
+ // sudo.exec(command, options, (error: any) => {
+ // if (error) {
+ // subject.error(error);
+ // } else {
+ // subject.complete();
+ // }
+ // });
+ //
+ // return subject.asObservable();
+ // }
}
diff --git a/electron/src/electron-main.ts b/electron/src/electron-main.ts
index 2bcc8bf2..16fedf67 100644
--- a/electron/src/electron-main.ts
+++ b/electron/src/electron-main.ts
@@ -13,7 +13,7 @@ import { IpcEvents } from './shared/util';
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
// import './dev-extension';
-// require('electron-debug')({ showDevTools: true, enabled: true });
+require('electron-debug')({ showDevTools: false, enabled: true });
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
diff --git a/electron/src/index.html b/electron/src/index.html
index e9fa1032..512f3c75 100644
--- a/electron/src/index.html
+++ b/electron/src/index.html
@@ -11,6 +11,7 @@
diff --git a/electron/src/models/sender-message.ts b/electron/src/models/sender-message.ts
new file mode 100644
index 00000000..3ff62a06
--- /dev/null
+++ b/electron/src/models/sender-message.ts
@@ -0,0 +1,6 @@
+import { Observer } from 'rxjs/Observer';
+
+export interface SenderMessage {
+ buffer: Buffer;
+ observer: Observer;
+}
diff --git a/electron/src/package.json b/electron/src/package.json
index 2df162ad..0befc731 100644
--- a/electron/src/package.json
+++ b/electron/src/package.json
@@ -14,6 +14,7 @@
"npm": ">=3.10.7 <4.0.0"
},
"dependencies": {
+ "node-hid": "0.5.4",
"usb": "git+https://github.com/aktary/node-usb.git"
}
}
diff --git a/electron/src/services/electron-error-handler.service.ts b/electron/src/services/electron-error-handler.service.ts
new file mode 100644
index 00000000..4be0533a
--- /dev/null
+++ b/electron/src/services/electron-error-handler.service.ts
@@ -0,0 +1,10 @@
+import { ErrorHandler, Inject } from '@angular/core';
+import { ILogService, LOG_SERVICE } from '../../../shared/src/services/logger.service';
+
+export class ElectronErrorHandlerService implements ErrorHandler {
+ constructor(@Inject(LOG_SERVICE)private logService: ILogService) {}
+
+ handleError(error: any) {
+ this.logService.error(error);
+ }
+}
diff --git a/electron/src/services/electron-log.service.ts b/electron/src/services/electron-log.service.ts
new file mode 100644
index 00000000..75a8461f
--- /dev/null
+++ b/electron/src/services/electron-log.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import * as log from 'electron-log';
+import * as util from 'util';
+
+import { ILogService } from '../../../shared/src/services/logger.service';
+
+/**
+ * This service use the electron-log package to write log in file.
+ * The logger usable in main and renderer process.
+ * The location of the log files:
+ * - on Linux: ~/.config//log.log
+ * - on OS X: ~/Library/Logs//log.log
+ * - on Windows: %USERPROFILE%\AppData\Roaming\\log.log
+ * The app name: UHK Agent. The up to date value in the scripts/release.js file.
+ */
+@Injectable()
+export class ElectronLogService implements ILogService {
+ private static getErrorText(args: any) {
+ return util.inspect(args);
+ }
+
+ error(...args: any[]): void {
+ log.error(ElectronLogService.getErrorText(args));
+ }
+
+ info(...args: any[]): void {
+ log.info(ElectronLogService.getErrorText(args));
+ }
+}
diff --git a/electron/src/services/uhk-device-provider.ts b/electron/src/services/uhk-device-provider.ts
new file mode 100644
index 00000000..2e2e3b9b
--- /dev/null
+++ b/electron/src/services/uhk-device-provider.ts
@@ -0,0 +1,18 @@
+import { Provider } from '@angular/core';
+
+import { UhkDeviceService } from './uhk-device.service';
+import { UhkHidApiService } from './uhk-hid-api.service';
+
+export function uhkDeviceProvider(): Provider {
+ // HID API officially support MAC, WIN and linux x64 platform
+ // https://github.com/node-hid/node-hid#platform-support
+ if (process.platform === 'darwin' ||
+ process.platform === 'win32' ||
+ (process.platform === 'linux' && process.arch === 'x64')) {
+ return { provide: UhkDeviceService, useClass: UhkHidApiService };
+ }
+
+ // On other platform use libUsb, but we try to test on all platform
+ // return { provide: UhkDeviceService, useClass: UhkLibUsbApiService };
+ return { provide: UhkDeviceService, useClass: UhkHidApiService };
+}
diff --git a/electron/src/services/uhk-device.service.ts b/electron/src/services/uhk-device.service.ts
index 8e9e8d79..9c1d4f95 100644
--- a/electron/src/services/uhk-device.service.ts
+++ b/electron/src/services/uhk-device.service.ts
@@ -1,195 +1,49 @@
-import { Injectable, OnDestroy, NgZone } from '@angular/core';
-
+import { Inject } from '@angular/core';
import { Observable } from 'rxjs/Observable';
-import { BehaviorSubject } from 'rxjs/BehaviorSubject';
-import { Observer } from 'rxjs/Observer';
-import { ConnectableObservable } from 'rxjs/observable/ConnectableObservable';
import { Subject } from 'rxjs/Subject';
+import { Observer } from 'rxjs/Observer';
import { Subscriber } from 'rxjs/Subscriber';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
-import 'rxjs/add/observable/empty';
-import 'rxjs/add/observable/from';
-import 'rxjs/add/observable/of';
-import 'rxjs/add/observable/timer';
-import 'rxjs/add/operator/catch';
-import 'rxjs/add/operator/concat';
-import 'rxjs/add/operator/combineLatest';
-import 'rxjs/add/operator/concatMap';
-import 'rxjs/add/operator/first';
-import 'rxjs/add/operator/mergeMap';
-import 'rxjs/add/operator/publish';
-import 'rxjs/add/operator/switchMap';
-
-import { Device, Interface, InEndpoint, OutEndpoint, findByIds, on } from 'usb';
-
-import { Layer } from '../shared/config-serializer/config-items/layer';
-import { UhkBuffer } from '../shared/config-serializer/uhk-buffer';
-
-const vendorId = 0x1d50;
-const productId = 0x6122;
-const MAX_PAYLOAD_SIZE = 64;
+import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
+import { SenderMessage } from '../models/sender-message';
+import { Constants } from '../shared/util/constants';
enum Command {
UploadConfig = 8,
ApplyConfig = 9
}
-interface SenderMessage {
- buffer: Buffer;
- observer: Observer;
-}
+export abstract class UhkDeviceService {
+ protected connected$: BehaviorSubject;
+ protected initialized$: BehaviorSubject;
+ protected deviceOpened$: BehaviorSubject;
+ protected outSubscription: Subscription;
-@Injectable()
-export class UhkDeviceService implements OnDestroy {
+ protected messageIn$: Observable;
+ protected messageOut$: Subject;
- private device: Device;
- private deviceOpened$: BehaviorSubject;
- private connected$: BehaviorSubject;
- private initizalized$: BehaviorSubject;
-
- private messageIn$: Observable;
- private messageOut$: Subject;
-
- private outSubscription: Subscription;
-
- constructor(zone: NgZone) {
+ constructor(@Inject(LOG_SERVICE) protected logService: ILogService) {
this.messageOut$ = new Subject();
- this.initizalized$ = new BehaviorSubject(false);
+ this.initialized$ = new BehaviorSubject(false);
this.connected$ = new BehaviorSubject(false);
this.deviceOpened$ = new BehaviorSubject(false);
this.outSubscription = Subscription.EMPTY;
-
- this.initialize();
-
- // The change detection doesn't work properly if the callbacks are called outside Angular Zone
- on('attach', (device: Device) => zone.run(() => this.onDeviceAttach(device)));
- on('detach', (device: Device) => zone.run(() => this.onDeviceDetach(device)));
}
ngOnDestroy() {
this.disconnect();
- this.initizalized$.unsubscribe();
+ this.initialized$.unsubscribe();
this.connected$.unsubscribe();
this.deviceOpened$.unsubscribe();
}
- initialize(): void {
- if (this.initizalized$.getValue()) {
- return;
- }
- this.device = findByIds(vendorId, productId);
- this.connected$.next(!!this.device);
- if (!this.device) {
- return;
- }
- try {
- this.device.open();
- this.deviceOpened$.next(true);
- } catch (error) {
- console.log(error);
- return;
- }
-
- const usbInterface: Interface = this.device.interface(0);
- // https://github.com/tessel/node-usb/issues/147
- // The function 'isKernelDriverActive' is not available on Windows and not even needed.
- if (process.platform !== 'win32' && usbInterface.isKernelDriverActive()) {
- usbInterface.detachKernelDriver();
- }
-
- // https://github.com/tessel/node-usb/issues/30
- // Mac is not allow excusive right to use USB
- if (process.platform !== 'darwin') {
- usbInterface.claim();
- }
-
- this.messageIn$ = Observable.create((subscriber: Subscriber) => {
- const inEndPoint: InEndpoint = usbInterface.endpoints[0];
- console.log('Try to read');
- inEndPoint.transfer(MAX_PAYLOAD_SIZE, (error: string, receivedBuffer: Buffer) => {
- if (error) {
- console.error('reading error', error);
- subscriber.error(error);
- } else {
- console.log('read data', receivedBuffer);
- subscriber.next(receivedBuffer);
- subscriber.complete();
- }
- });
- });
-
- const outEndPoint: OutEndpoint = usbInterface.endpoints[1];
- const outSending = this.messageOut$.concatMap(senderPackage => {
- return (>Observable.create((subscriber: Subscriber) => {
- console.log('transfering', senderPackage.buffer);
- outEndPoint.transfer(senderPackage.buffer, error => {
- if (error) {
- console.error('transfering errored', error);
- subscriber.error(error);
- } else {
- console.log('transfering finished');
- subscriber.complete();
- }
- });
- })).concat(this.messageIn$)
- .do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
- .catch((error: string) => {
- senderPackage.observer.error(error);
- return Observable.empty();
- });
- }).publish();
- this.outSubscription = outSending.connect();
-
- this.initizalized$.next(true);
- }
-
- disconnect() {
- this.outSubscription.unsubscribe();
- this.messageIn$ = undefined;
- this.initizalized$.next(false);
- this.deviceOpened$.next(false);
- this.connected$.next(false);
- }
-
- isInitialized(): Observable {
- return this.initizalized$.asObservable();
- }
-
- isConnected(): Observable {
- return this.connected$.asObservable();
- }
-
- hasPermissions(): Observable {
- return this.isConnected()
- .combineLatest(this.deviceOpened$)
- .map((latest: boolean[]) => {
- const connected = latest[0];
- const opened = latest[1];
- if (!connected) {
- return false;
- } else if (opened) {
- return true;
- }
- try {
- this.device.open();
- } catch (error) {
- return false;
- }
- this.device.close();
- return true;
- });
- }
-
- isOpened(): Observable {
- return this.deviceOpened$.asObservable();
- }
-
sendConfig(configBuffer: Buffer): Observable {
return Observable.create((subscriber: Subscriber) => {
- console.log('Sending...', configBuffer);
+ this.logService.info('Sending...', configBuffer);
const fragments: Buffer[] = [];
- const MAX_SENDING_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE - 4;
+ const MAX_SENDING_PAYLOAD_SIZE = Constants.MAX_PAYLOAD_SIZE - 4;
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
? MAX_SENDING_PAYLOAD_SIZE
@@ -206,7 +60,7 @@ export class UhkDeviceService implements OnDestroy {
if (buffers.length === fragments.length) {
subscriber.next(Buffer.concat(buffers));
subscriber.complete();
- console.log('Sending finished');
+ this.logService.info('Sending finished');
}
}
};
@@ -219,7 +73,7 @@ export class UhkDeviceService implements OnDestroy {
applyConfig(): Observable {
return Observable.create((subscriber: Subscriber) => {
- console.log('Applying configuration');
+ this.logService.info('Applying configuration');
this.messageOut$.next({
buffer: new Buffer([Command.ApplyConfig]),
observer: subscriber
@@ -227,21 +81,27 @@ export class UhkDeviceService implements OnDestroy {
});
}
- onDeviceAttach(device: Device) {
- if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) {
- return;
- }
- // Ugly hack: device is not openable (on Windows) right after the attach
- Observable.timer(100)
- .first()
- .subscribe(() => this.initialize());
+ isInitialized(): Observable {
+ return this.initialized$.asObservable();
}
- onDeviceDetach(device: Device) {
- if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) {
- return;
- }
- this.disconnect();
+ isConnected(): Observable {
+ return this.connected$.asObservable();
}
+ isOpened(): Observable {
+ return this.deviceOpened$.asObservable();
+ }
+
+ disconnect() {
+ this.outSubscription.unsubscribe();
+ this.messageIn$ = undefined;
+ this.initialized$.next(false);
+ this.deviceOpened$.next(false);
+ this.connected$.next(false);
+ }
+
+ abstract initialize(): void;
+
+ abstract hasPermissions(): Observable;
}
diff --git a/electron/src/services/uhk-hid-api.service.ts b/electron/src/services/uhk-hid-api.service.ts
new file mode 100644
index 00000000..e3f31668
--- /dev/null
+++ b/electron/src/services/uhk-hid-api.service.ts
@@ -0,0 +1,164 @@
+import { Inject, Injectable, OnDestroy } from '@angular/core';
+
+import { Observable } from 'rxjs/Observable';
+import { Subscriber } from 'rxjs/Subscriber';
+import { Subscription } from 'rxjs/Subscription';
+import { Device, devices, HID } from 'node-hid';
+
+import 'rxjs/add/observable/empty';
+import 'rxjs/add/observable/timer';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/concat';
+import 'rxjs/add/operator/combineLatest';
+import 'rxjs/add/operator/concatMap';
+import 'rxjs/add/operator/publish';
+import 'rxjs/add/operator/do';
+
+import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
+import { Constants } from '../shared/util';
+import { UhkDeviceService } from './uhk-device.service';
+
+@Injectable()
+export class UhkHidApiService extends UhkDeviceService implements OnDestroy {
+ private device: HID;
+
+ private pollTimer$: Subscription;
+
+ constructor(@Inject(LOG_SERVICE) protected logService: ILogService) {
+ super(logService);
+
+ this.pollUhkDevice();
+ }
+
+ ngOnDestroy() {
+ super.ngOnDestroy();
+ this.pollTimer$.unsubscribe();
+ }
+
+ initialize(): void {
+ if (this.initialized$.getValue()) {
+ return;
+ }
+
+ this.device = this.getDevice();
+ if (!this.device) {
+ return;
+ }
+
+ this.deviceOpened$.next(true);
+
+ this.messageIn$ = Observable.create((subscriber: Subscriber) => {
+ this.logService.info('Try to read');
+ this.device.read((error: any, data: any = []) => {
+ if (error) {
+ this.logService.error('reading error', error);
+ subscriber.error(error);
+ } else {
+ this.logService.info('read data', data);
+ subscriber.next(data);
+ subscriber.complete();
+ }
+ });
+ });
+
+ const outSending = this.messageOut$.concatMap(senderPackage => {
+ return (>Observable.create((subscriber: Subscriber) => {
+ this.logService.info('transfering', senderPackage.buffer);
+ const data = Array.prototype.slice.call(senderPackage.buffer, 0);
+ // if data start with 0 need to add additional leading zero because HID API remove it.
+ // https://github.com/node-hid/node-hid/issues/187
+ if (data.length > 0 && data[0] === 0) {
+ data.unshift(0);
+ }
+
+ // From HID API documentation:
+ // http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af
+ // The first byte of data[] must contain the Report ID.
+ // For devices which only support a single report, this must be set to 0x0.
+ data.unshift(0);
+
+ try {
+ this.device.write(data);
+ this.logService.info('transfering finished');
+ subscriber.complete();
+ }
+ catch (error) {
+ this.logService.error('transfering errored', error);
+ subscriber.error(error);
+ }
+ })).concat(this.messageIn$)
+ .do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
+ .catch((error: string) => {
+ senderPackage.observer.error(error);
+ return Observable.empty();
+ });
+ }).publish();
+ this.outSubscription = outSending.connect();
+
+ this.initialized$.next(true);
+ }
+
+ hasPermissions(): Observable {
+ return this.isConnected()
+ .combineLatest(this.deviceOpened$)
+ .map((latest: boolean[]) => {
+ const connected = latest[0];
+ const opened = latest[1];
+ if (!connected) {
+ return false;
+ } else if (opened) {
+ return true;
+ }
+ return true;
+ });
+ }
+
+ /**
+ * Return the 0 interface of the keyboard.
+ * @returns {HID}
+ */
+ getDevice(): HID {
+ try {
+ const devs = devices();
+ this.logService.info('Available devices:', devs);
+
+ const dev = devs.find((x: Device) =>
+ x.vendorId === Constants.VENDOR_ID &&
+ x.productId === Constants.PRODUCT_ID &&
+ ((x.usagePage === 128 && x.usage === 129) || x.interface === 0));
+
+ const device = new HID(dev.path);
+ this.logService.info('Used device:', dev);
+ return device;
+ }
+ catch (err) {
+ this.logService.error('Can not create device:', err);
+ }
+
+ return null;
+ }
+
+ /**
+ * 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.
+ */
+ private pollUhkDevice() {
+ this.pollTimer$ = Observable.timer(0, 1000)
+ .map(() => {
+ return devices().some((dev: Device) => dev.vendorId === Constants.VENDOR_ID &&
+ dev.productId === Constants.PRODUCT_ID);
+ })
+ .distinctUntilChanged()
+ .do((connected: boolean) => {
+ this.connected$.next(connected);
+ if (connected) {
+ this.initialize();
+ } else {
+ this.disconnect();
+ }
+
+ })
+ .subscribe();
+ }
+}
diff --git a/electron/src/services/uhk-lib-usb-api.service.ts b/electron/src/services/uhk-lib-usb-api.service.ts
new file mode 100644
index 00000000..43408ac6
--- /dev/null
+++ b/electron/src/services/uhk-lib-usb-api.service.ts
@@ -0,0 +1,142 @@
+import { Inject, Injectable, NgZone, OnDestroy } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Subscriber } from 'rxjs/Subscriber';
+
+import 'rxjs/add/observable/empty';
+import 'rxjs/add/observable/timer';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/concat';
+import 'rxjs/add/operator/combineLatest';
+import 'rxjs/add/operator/concatMap';
+import 'rxjs/add/operator/first';
+import 'rxjs/add/operator/publish';
+import 'rxjs/add/operator/do';
+
+import { Device, findByIds, InEndpoint, Interface, on, OutEndpoint } from 'usb';
+
+import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
+import { Constants } from '../shared/util';
+import { UhkDeviceService } from './uhk-device.service';
+
+@Injectable()
+export class UhkLibUsbApiService extends UhkDeviceService implements OnDestroy {
+ private device: Device;
+
+ static isUhkDevice(device: Device) {
+ return device.deviceDescriptor.idVendor === Constants.VENDOR_ID &&
+ device.deviceDescriptor.idProduct === Constants.PRODUCT_ID;
+ }
+
+ constructor(zone: NgZone,
+ @Inject(LOG_SERVICE) protected logService: ILogService) {
+ super(logService);
+
+ this.initialize();
+
+ // The change detection doesn't work properly if the callbacks are called outside Angular Zone
+ on('attach', (device: Device) => zone.run(() => this.onDeviceAttach(device)));
+ on('detach', (device: Device) => zone.run(() => this.onDeviceDetach(device)));
+ }
+
+ initialize(): void {
+ if (this.initialized$.getValue()) {
+ return;
+ }
+ this.device = findByIds(Constants.VENDOR_ID, Constants.PRODUCT_ID);
+ this.connected$.next(!!this.device);
+ if (!this.device) {
+ return;
+ }
+ try {
+ this.device.open();
+ this.deviceOpened$.next(true);
+ } catch (error) {
+ this.logService.error(error);
+ return;
+ }
+
+ const usbInterface: Interface = this.device.interface(0);
+ // https://github.com/tessel/node-usb/issues/147
+ // The function 'isKernelDriverActive' is not available on Windows and not even needed.
+ if (usbInterface.isKernelDriverActive()) {
+ usbInterface.detachKernelDriver();
+ }
+
+ this.messageIn$ = Observable.create((subscriber: Subscriber) => {
+ const inEndPoint: InEndpoint = usbInterface.endpoints[0];
+ this.logService.info('Try to read');
+ inEndPoint.transfer(Constants.MAX_PAYLOAD_SIZE, (error: string, receivedBuffer: Buffer) => {
+ if (error) {
+ this.logService.error('reading error', error);
+ subscriber.error(error);
+ } else {
+ this.logService.info('read data', receivedBuffer);
+ subscriber.next(receivedBuffer);
+ subscriber.complete();
+ }
+ });
+ });
+
+ const outEndPoint: OutEndpoint = usbInterface.endpoints[1];
+ const outSending = this.messageOut$.concatMap(senderPackage => {
+ return (>Observable.create((subscriber: Subscriber) => {
+ this.logService.info('transfering', senderPackage.buffer);
+ outEndPoint.transfer(senderPackage.buffer, error => {
+ if (error) {
+ this.logService.error('transfering errored', error);
+ subscriber.error(error);
+ } else {
+ this.logService.info('transfering finished');
+ subscriber.complete();
+ }
+ });
+ })).concat(this.messageIn$)
+ .do(buffer => senderPackage.observer.next(buffer) && senderPackage.observer.complete())
+ .catch((error: string) => {
+ senderPackage.observer.error(error);
+ return Observable.empty();
+ });
+ }).publish();
+ this.outSubscription = outSending.connect();
+
+ this.initialized$.next(true);
+ }
+
+ hasPermissions(): Observable {
+ return this.isConnected()
+ .combineLatest(this.deviceOpened$)
+ .map((latest: boolean[]) => {
+ const connected = latest[0];
+ const opened = latest[1];
+ if (!connected) {
+ return false;
+ } else if (opened) {
+ return true;
+ }
+ try {
+ this.device.open();
+ } catch (error) {
+ return false;
+ }
+ this.device.close();
+ return true;
+ });
+ }
+
+ onDeviceAttach(device: Device) {
+ if (!UhkLibUsbApiService.isUhkDevice(device)) {
+ return;
+ }
+ // Ugly hack: device is not openable (on Windows) right after the attach
+ Observable.timer(100)
+ .first()
+ .subscribe(() => this.initialize());
+ }
+
+ onDeviceDetach(device: Device) {
+ if (!UhkLibUsbApiService.isUhkDevice(device)) {
+ return;
+ }
+ this.disconnect();
+ }
+}
diff --git a/electron/src/webpack.config.js b/electron/src/webpack.config.js
index ed8de2e3..34ab4a2a 100644
--- a/electron/src/webpack.config.js
+++ b/electron/src/webpack.config.js
@@ -18,7 +18,8 @@ module.exports = {
},
target: 'electron-renderer',
externals: {
- usb: 'usb'
+ usb: 'usb',
+ 'node-hid':'nodeHid'
},
devtool: 'source-map',
resolve: {
@@ -40,7 +41,8 @@ module.exports = {
use: ['raw-loader', 'sass-loader']
},
{ test: /jquery/, loader: 'expose-loader?$!expose-loader?jQuery' },
- { test: require.resolve("usb"), loader: "expose-loader?usb" }
+ { test: require.resolve("usb"), loader: "expose-loader?usb" },
+ { test: require.resolve("node-hid"), loader: "expose-loader?node-hid" }
]
},
plugins: [
diff --git a/package.json b/package.json
index 3ab78bf1..559015f0 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
{
"name": "uhk-agent",
+ "author": "Ultimate Gadget Laboratories",
"main": "electron/dist/electron-main.js",
"version": "1.0.0",
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
@@ -21,6 +22,7 @@
"@types/file-saver": "0.0.1",
"@types/jquery": "3.2.1",
"@types/node": "^6.0.78",
+ "@types/node-hid": "^0.5.2",
"@types/usb": "^1.1.3",
"angular2-template-loader": "0.6.2",
"copy-webpack-plugin": "^4.0.1",
@@ -37,6 +39,7 @@
"npm-run-all": "4.0.2",
"path": "^0.12.7",
"raw-loader": "^0.5.1",
+ "rimraf": "^2.6.1",
"sass-loader": "^6.0.3",
"standard-version": "^4.0.0",
"stylelint": "^7.10.1",
@@ -75,6 +78,7 @@
"ng2-dragula": "1.5.0",
"ng2-select2": "1.0.0-beta.10",
"ngrx-store-freeze": "^0.1.9",
+ "node-hid": "0.5.4",
"reselect": "3.0.1",
"rxjs": "^5.4.1",
"select2": "^4.0.3",
@@ -85,17 +89,17 @@
"zone.js": "0.8.12"
},
"scripts": {
- "postinstall": "run-p build:usb \"symlink -- -i\" ",
+ "postinstall": "run-p \"symlink -- -i\" ",
"test": "cd ./test-serializer && node ./test-serializer.js",
"lint": "run-s -scn lint:ts lint:style",
"lint:ts": "tslint \"electron/src/**/*.ts\" \"web/src/**/*.ts\" \"shared/**/*.ts\" \"test-serializer/**/*.ts\"",
"lint:style": "stylelint \"electron/**/*.scss\" \"web/**/*.scss\" \"shared/**/*.scss\" --syntax scss",
"build": "run-p build:web build:electron",
"build:web": "webpack --config \"web/src/webpack.config.js\"",
- "build:electron": "run-s -scn build:electron:main build:electron:app",
+ "build:electron": "run-s -sn build:electron:main build:electron:app install:build-deps",
"build:electron:main": "webpack --config \"electron/src/webpack.config.electron-main.js\"",
"build:electron:app": "webpack --config \"electron/src/webpack.config.js\"",
- "build:usb": "electron-rebuild -w usb -p",
+ "build:usb": "electron-rebuild -w usb,node-hid -p -m electron/dist",
"build:test": "webpack --config \"test-serializer/webpack.config.js\"",
"server:web": "webpack-dev-server --config \"web/src/webpack.config.js\" --content-base \"./web/dist\"",
"server:electron": "webpack --config \"electron/src/webpack.config.js\" --watch",
@@ -103,7 +107,8 @@
"symlink": "node ./tools/symlinker",
"standard-version": "standard-version",
"pack": "node ./scripts/release.js",
- "install:build-deps": "cd electron/dist && npm i",
- "release": "npm run install:build-deps && node ./scripts/release.js"
+ "install:build-deps": "cd electron/dist && npm i && cd .. && npm run build:usb",
+ "release": "node ./scripts/release.js",
+ "clean": "rimraf ./node_modules ./electron/dist"
}
}
diff --git a/rules/zadic-ia32.exe b/rules/zadic-ia32.exe
new file mode 100644
index 00000000..8c96a9a8
Binary files /dev/null and b/rules/zadic-ia32.exe differ
diff --git a/rules/zadic-x64.exe b/rules/zadic-x64.exe
new file mode 100644
index 00000000..31178dc2
Binary files /dev/null and b/rules/zadic-x64.exe differ
diff --git a/scripts/release.js b/scripts/release.js
index 3219ea21..dc48c2cf 100644
--- a/scripts/release.js
+++ b/scripts/release.js
@@ -1,7 +1,11 @@
'use strict';
const jsonfile = require('jsonfile');
-const TEST_BUILD = false; // set true if you would like to test on your local machince
+const TEST_BUILD = process.env.TEST_BUILD; // set true if you would like to test on your local machince
+
+// electron-builder security override.
+// Need if wanna create test release build from PR
+process.env.PUBLISH_FOR_PULL_REQUEST = TEST_BUILD;
if (!process.env.CI && !TEST_BUILD) {
console.error('Create release only on CI server');
@@ -25,7 +29,7 @@ if (process.env.TRAVIS) {
repoName = process.env.APPVEYOR_REPO_NAME;
}
-console.log({branchName, pullRequestNr, gitTag, repoName});
+console.log({ branchName, pullRequestNr, gitTag, repoName });
const isReleaseCommit = TEST_BUILD || branchName === gitTag && repoName === 'UltimateHackingKeyboard/agent';
@@ -51,16 +55,23 @@ if (process.env.TRAVIS) {
let target = '';
let artifactName = 'UHK.Agent-${version}-${os}';
+let extraResources = [];
if (process.platform === 'darwin') {
target = Platform.MAC.createTarget();
artifactName += '.${ext}';
} else if (process.platform === 'win32') {
target = Platform.WINDOWS.createTarget();
+ // TODO: If all HID API test success then remove zadic extra resources
+ extraResources.push(`rules/zadic-ia32.exe`);
+ extraResources.push(`rules/zadic-x64.exe`);
artifactName += '-${arch}.${ext}';
+ extraResources.push(`rules/zadic-${process.arch}.exe`);
} else if (process.platform === 'linux') {
target = Platform.LINUX.createTarget();
artifactName += '.${ext}';
+ extraResources.push('rules/setup-rules.sh');
+ extraResources.push('rules/50-uhk60.rules');
} else {
console.error(`I dunno how to publish a release for ${process.platform} :(`);
process.exit(1);
@@ -78,7 +89,7 @@ if (TEST_BUILD || gitTag) {
updateVersionNumberIn2rndPackageJson(jsonVersion);
builder.build({
- dir: true,
+ dir: TEST_BUILD,
targets: target,
appMetadata: {
main: 'electron-main.js',
@@ -97,6 +108,12 @@ if (TEST_BUILD || gitTag) {
mac: {
category: 'public.app-category.utilities'
},
+ win: {
+ extraResources
+ },
+ linux: {
+ extraResources
+ },
publish: 'github',
artifactName,
files: [
diff --git a/shared/src/services/logger.service.ts b/shared/src/services/logger.service.ts
new file mode 100644
index 00000000..468d6898
--- /dev/null
+++ b/shared/src/services/logger.service.ts
@@ -0,0 +1,20 @@
+import {Injectable, InjectionToken} from '@angular/core';
+
+export interface ILogService {
+
+ error(...args: any[]): void;
+ info(...args: any[]): void;
+}
+
+export let LOG_SERVICE = new InjectionToken('logger-service');
+
+@Injectable()
+export class ConsoleLogService implements ILogService {
+ error(...args: any[]): void {
+ console.error(args);
+ }
+
+ info(...args: any[]): void {
+ console.info(args);
+ }
+}
diff --git a/shared/src/util/constants.ts b/shared/src/util/constants.ts
new file mode 100644
index 00000000..0b3e32e3
--- /dev/null
+++ b/shared/src/util/constants.ts
@@ -0,0 +1,5 @@
+export namespace Constants {
+ export const VENDOR_ID = 0x1D50;
+ export const PRODUCT_ID = 0x6122;
+ export const MAX_PAYLOAD_SIZE = 64;
+}
diff --git a/shared/src/util/index.ts b/shared/src/util/index.ts
index b25fbecc..71e8a2a5 100644
--- a/shared/src/util/index.ts
+++ b/shared/src/util/index.ts
@@ -1,3 +1,7 @@
+import { Constants } from './constants';
+
+export { Constants };
+
// Source: http://stackoverflow.com/questions/13720256/javascript-regex-camelcase-to-sentence
export function camelCaseToSentence(camelCasedText: string): string {
return camelCasedText.replace(/^[a-z]|[A-Z]/g, function (v, i) {
diff --git a/web/src/app.module.ts b/web/src/app.module.ts
index 6db46ee0..bd5b28cd 100644
--- a/web/src/app.module.ts
+++ b/web/src/app.module.ts
@@ -76,7 +76,8 @@ import { MacroNotFoundGuard } from './shared/components/macro/not-found';
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
import { LocalDataStorageRepositoryService } from './shared/services/local-datastorage-repository.service';
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.service';
-import { reducer } from './shared/store/reducers/index';
+import { reducer } from '../../shared/src/store/reducers/index';
+import { ConsoleLogService, LOG_SERVICE } from '../../shared/src/services/logger.service';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
@NgModule({
@@ -159,10 +160,11 @@ import { AutoUpdateSettings } from './shared/components/auto-update-settings/aut
KeymapEditGuard,
MacroNotFoundGuard,
CaptureService,
- { provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService },
+ {provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService},
+ DefaultUserConfigurationService,
+ { provide: LOG_SERVICE, useClass: ConsoleLogService },
DefaultUserConfigurationService
],
bootstrap: [MainAppComponent]
})
-export class AppModule {
-}
+export class AppModule { }