* style(privilege): Fix typo 'excusive' -> 'exclusive' * style(privilege): remove unused imports * style(privilege): Fix typo 'initizalized$' -> 'initialized$' * feat(log): Add application wide logger and error handler It is help to debug electron install app on different device * feat(privilege): Add windows USB driver installation * build: I need the windows installer to test the app on windows * fix(privilege): change wdi-simpler installer to zadic * feat(log): change log level to debug in renderer process * chore: Add author in package.json * feat(privilege): Add privilege setter file as extraResource * fix(log): Allowed transport level change only in main process * fix(privilege): Fix app path calculation * fix(privilege): Take the scriptPath between double quote * build: revert the appveyor settings * refactor(privilege): Extract vendor ID, product ID and MAX_PAYLOAD_SIZE into constants file * refactor(privilege): Add both 32 and 64 bit zadics to extraResource of the installer * feat(device): Add HID API communication protocol * build: Fix npm install process * build: Fix npm install process v2 * ci: Add libudev-dev as travis apt dependencies * ci: Merge travis apt packages * ci: remove node-hid from build:usb * ci: try to fix linux build * ci: node-hid use git repo * ci: Add libusb-1.0-0-dev to travis apt dependency * feat(device): Use logging service when communicate with the device * build: create test build * build: PUBLISH_FOR_PULL_REQUEST override * build: revert TEST_BUILD to false * build: node-hid use package version instead of git repo * refactor: remove unused device store files from PR * ci: Manage test build from environment variable * fix(privilege): Set rules files dir base on dev or prod mode * fix(log): Extract nested properties of the logged object * feat(log): use util.inspect in logger service * build: upgrade @types/node-hid -> 0.5.2 * fix(device): Add extra logging when try to open device. * fix(device): log device description and not the device * fix(device): add win specific write * fix(device): add report id as first byte * style(privilege): Reformat else and comment in privilege-checker component * fix(privilege): Comment out windows branch
This commit is contained in:
committed by
László Monda
parent
84b13d3219
commit
2df8f2ea54
@@ -28,6 +28,9 @@ addons:
|
|||||||
- libgnome-keyring-dev
|
- libgnome-keyring-dev
|
||||||
- libsecret-1-dev
|
- libsecret-1-dev
|
||||||
- icnsutils
|
- icnsutils
|
||||||
|
- libudev-dev
|
||||||
|
- build-essential
|
||||||
|
- libusb-1.0-0-dev
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- nvm install
|
- nvm install
|
||||||
@@ -45,9 +48,3 @@ script:
|
|||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- node_modules
|
- node_modules
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- build-essential
|
|
||||||
- libudev-dev
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { ErrorHandler, NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
@@ -75,7 +75,9 @@ import { SafeStylePipe } from './shared/pipes';
|
|||||||
import { CaptureService } from './shared/services/capture.service';
|
import { CaptureService } from './shared/services/capture.service';
|
||||||
import { MapperService } from './shared/services/mapper.service';
|
import { MapperService } from './shared/services/mapper.service';
|
||||||
import { SvgModuleProviderService } from './shared/services/svg-module-provider.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 { AutoUpdateSettingsEffects, KeymapEffects, MacroEffects, UserConfigEffects } from './shared/store/effects';
|
||||||
import { ApplicationEffect, AppUpdateEffect } from './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 { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
|
||||||
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
||||||
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.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 { AppUpdateRendererService } from './services/app-update-renderer.service';
|
||||||
import { reducer } from './store';
|
import { reducer } from './store';
|
||||||
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
|
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,
|
KeymapEditGuard,
|
||||||
MacroNotFoundGuard,
|
MacroNotFoundGuard,
|
||||||
CaptureService,
|
CaptureService,
|
||||||
UhkDeviceService,
|
|
||||||
{ provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService },
|
{ provide: DATA_STORAGE_REPOSITORY, useClass: ElectronDataStorageRepositoryService },
|
||||||
DefaultUserConfigurationService,
|
DefaultUserConfigurationService,
|
||||||
AppUpdateRendererService
|
{ provide: LOG_SERVICE, useClass: ElectronLogService },
|
||||||
|
{ provide: ErrorHandler, useClass: ElectronErrorHandlerService },
|
||||||
|
AppUpdateRendererService,
|
||||||
|
UhkHidApiService,
|
||||||
|
UhkLibUsbApiService,
|
||||||
|
uhkDeviceProvider()
|
||||||
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import 'rxjs/add/operator/distinctUntilChanged';
|
import 'rxjs/add/operator/distinctUntilChanged';
|
||||||
import 'rxjs/add/operator/ignoreElements';
|
import 'rxjs/add/operator/ignoreElements';
|
||||||
import 'rxjs/add/operator/takeWhile';
|
import 'rxjs/add/operator/takeWhile';
|
||||||
|
|
||||||
import { UhkDeviceService } from './../../services/uhk-device.service';
|
import { UhkDeviceService } from '../../services/uhk-device.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'missing-device',
|
selector: 'missing-device',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference path="../../custom_types/sudo-prompt.d.ts"/>
|
/// <reference path="../../custom_types/sudo-prompt.d.ts"/>
|
||||||
import { Component } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import * as isDev from 'electron-is-dev';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||||
import 'rxjs/add/observable/of';
|
import 'rxjs/add/observable/of';
|
||||||
@@ -15,6 +15,7 @@ import * as path from 'path';
|
|||||||
import * as sudo from 'sudo-prompt';
|
import * as sudo from 'sudo-prompt';
|
||||||
|
|
||||||
import { UhkDeviceService } from '../../services/uhk-device.service';
|
import { UhkDeviceService } from '../../services/uhk-device.service';
|
||||||
|
import { ILogService, LOG_SERVICE } from '../../../../shared/src/services/logger.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'privilege-checker',
|
selector: 'privilege-checker',
|
||||||
@@ -23,7 +24,18 @@ import { UhkDeviceService } from '../../services/uhk-device.service';
|
|||||||
})
|
})
|
||||||
export class PrivilegeCheckerComponent {
|
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()
|
uhkDevice.isConnected()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.takeWhile(connected => connected)
|
.takeWhile(connected => connected)
|
||||||
@@ -50,14 +62,21 @@ export class PrivilegeCheckerComponent {
|
|||||||
case 'linux':
|
case 'linux':
|
||||||
permissionSetter = this.setUpPermissionsOnLinux();
|
permissionSetter = this.setUpPermissionsOnLinux();
|
||||||
break;
|
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:
|
default:
|
||||||
permissionSetter = Observable.throw('Permissions couldn\'t be set. Invalid platform: ' + process.platform);
|
permissionSetter = Observable.throw('Permissions couldn\'t be set. Invalid platform: ' + process.platform);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
permissionSetter.subscribe({
|
permissionSetter.subscribe({
|
||||||
error: e => console.error(e),
|
error: e => {
|
||||||
|
console.log(e);
|
||||||
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
console.log('Permissions has been successfully set');
|
this.logService.info('Permissions has been successfully set');
|
||||||
this.uhkDevice.initialize();
|
this.uhkDevice.initialize();
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
@@ -66,12 +85,13 @@ export class PrivilegeCheckerComponent {
|
|||||||
|
|
||||||
private setUpPermissionsOnLinux(): Observable<void> {
|
private setUpPermissionsOnLinux(): Observable<void> {
|
||||||
const subject = new ReplaySubject<void>();
|
const subject = new ReplaySubject<void>();
|
||||||
const rootDir = path.resolve(path.join(remote.process.cwd(), remote.process.argv[1]), '..');
|
const scriptPath = path.resolve(this.rootDir, 'rules/setup-rules.sh');
|
||||||
const scriptPath = path.resolve(rootDir, 'rules/setup-rules.sh');
|
|
||||||
const options = {
|
const options = {
|
||||||
name: 'Setting UHK access rules'
|
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) {
|
if (error) {
|
||||||
subject.error(error);
|
subject.error(error);
|
||||||
} else {
|
} else {
|
||||||
@@ -82,4 +102,34 @@ export class PrivilegeCheckerComponent {
|
|||||||
return subject.asObservable();
|
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<void> {
|
||||||
|
// const subject = new ReplaySubject<void>();
|
||||||
|
//
|
||||||
|
// // 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();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { IpcEvents } from './shared/util';
|
|||||||
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
||||||
|
|
||||||
// import './dev-extension';
|
// 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
|
// 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.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<!--<script src="vendor/usb/usb.js"></script>-->
|
<!--<script src="vendor/usb/usb.js"></script>-->
|
||||||
<script>
|
<script>
|
||||||
usb = require('usb');
|
usb = require('usb');
|
||||||
|
nodeHid = require('node-hid');
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
6
electron/src/models/sender-message.ts
Normal file
6
electron/src/models/sender-message.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Observer } from 'rxjs/Observer';
|
||||||
|
|
||||||
|
export interface SenderMessage {
|
||||||
|
buffer: Buffer;
|
||||||
|
observer: Observer<any>;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"npm": ">=3.10.7 <4.0.0"
|
"npm": ">=3.10.7 <4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"node-hid": "0.5.4",
|
||||||
"usb": "git+https://github.com/aktary/node-usb.git"
|
"usb": "git+https://github.com/aktary/node-usb.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
electron/src/services/electron-error-handler.service.ts
Normal file
10
electron/src/services/electron-error-handler.service.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
electron/src/services/electron-log.service.ts
Normal file
29
electron/src/services/electron-log.service.ts
Normal file
@@ -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/<app name>/log.log
|
||||||
|
* - on OS X: ~/Library/Logs/<app name>/log.log
|
||||||
|
* - on Windows: %USERPROFILE%\AppData\Roaming\<app name>\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));
|
||||||
|
}
|
||||||
|
}
|
||||||
18
electron/src/services/uhk-device-provider.ts
Normal file
18
electron/src/services/uhk-device-provider.ts
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
@@ -1,195 +1,49 @@
|
|||||||
import { Injectable, OnDestroy, NgZone } from '@angular/core';
|
import { Inject } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
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 { Subject } from 'rxjs/Subject';
|
||||||
|
import { Observer } from 'rxjs/Observer';
|
||||||
import { Subscriber } from 'rxjs/Subscriber';
|
import { Subscriber } from 'rxjs/Subscriber';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
|
||||||
import 'rxjs/add/observable/empty';
|
import { ILogService, LOG_SERVICE } from '../shared/services/logger.service';
|
||||||
import 'rxjs/add/observable/from';
|
import { SenderMessage } from '../models/sender-message';
|
||||||
import 'rxjs/add/observable/of';
|
import { Constants } from '../shared/util/constants';
|
||||||
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;
|
|
||||||
|
|
||||||
enum Command {
|
enum Command {
|
||||||
UploadConfig = 8,
|
UploadConfig = 8,
|
||||||
ApplyConfig = 9
|
ApplyConfig = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SenderMessage {
|
export abstract class UhkDeviceService {
|
||||||
buffer: Buffer;
|
protected connected$: BehaviorSubject<boolean>;
|
||||||
observer: Observer<any>;
|
protected initialized$: BehaviorSubject<boolean>;
|
||||||
}
|
protected deviceOpened$: BehaviorSubject<boolean>;
|
||||||
|
protected outSubscription: Subscription;
|
||||||
|
|
||||||
@Injectable()
|
protected messageIn$: Observable<Buffer>;
|
||||||
export class UhkDeviceService implements OnDestroy {
|
protected messageOut$: Subject<SenderMessage>;
|
||||||
|
|
||||||
private device: Device;
|
constructor(@Inject(LOG_SERVICE) protected logService: ILogService) {
|
||||||
private deviceOpened$: BehaviorSubject<boolean>;
|
|
||||||
private connected$: BehaviorSubject<boolean>;
|
|
||||||
private initizalized$: BehaviorSubject<boolean>;
|
|
||||||
|
|
||||||
private messageIn$: Observable<Buffer>;
|
|
||||||
private messageOut$: Subject<SenderMessage>;
|
|
||||||
|
|
||||||
private outSubscription: Subscription;
|
|
||||||
|
|
||||||
constructor(zone: NgZone) {
|
|
||||||
this.messageOut$ = new Subject<SenderMessage>();
|
this.messageOut$ = new Subject<SenderMessage>();
|
||||||
this.initizalized$ = new BehaviorSubject(false);
|
this.initialized$ = new BehaviorSubject(false);
|
||||||
this.connected$ = new BehaviorSubject(false);
|
this.connected$ = new BehaviorSubject(false);
|
||||||
this.deviceOpened$ = new BehaviorSubject(false);
|
this.deviceOpened$ = new BehaviorSubject(false);
|
||||||
this.outSubscription = Subscription.EMPTY;
|
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() {
|
ngOnDestroy() {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.initizalized$.unsubscribe();
|
this.initialized$.unsubscribe();
|
||||||
this.connected$.unsubscribe();
|
this.connected$.unsubscribe();
|
||||||
this.deviceOpened$.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<Buffer>) => {
|
|
||||||
const inEndPoint: 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 = <OutEndpoint>usbInterface.endpoints[1];
|
|
||||||
const outSending = this.messageOut$.concatMap(senderPackage => {
|
|
||||||
return (<Observable<void>>Observable.create((subscriber: Subscriber<void>) => {
|
|
||||||
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<void>();
|
|
||||||
});
|
|
||||||
}).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<boolean> {
|
|
||||||
return this.initizalized$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected(): Observable<boolean> {
|
|
||||||
return this.connected$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPermissions(): Observable<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
return this.deviceOpened$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendConfig(configBuffer: Buffer): Observable<Buffer> {
|
sendConfig(configBuffer: Buffer): Observable<Buffer> {
|
||||||
return Observable.create((subscriber: Subscriber<Buffer>) => {
|
return Observable.create((subscriber: Subscriber<Buffer>) => {
|
||||||
console.log('Sending...', configBuffer);
|
this.logService.info('Sending...', configBuffer);
|
||||||
const fragments: Buffer[] = [];
|
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) {
|
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
|
||||||
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
|
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
|
||||||
? MAX_SENDING_PAYLOAD_SIZE
|
? MAX_SENDING_PAYLOAD_SIZE
|
||||||
@@ -206,7 +60,7 @@ export class UhkDeviceService implements OnDestroy {
|
|||||||
if (buffers.length === fragments.length) {
|
if (buffers.length === fragments.length) {
|
||||||
subscriber.next(Buffer.concat(buffers));
|
subscriber.next(Buffer.concat(buffers));
|
||||||
subscriber.complete();
|
subscriber.complete();
|
||||||
console.log('Sending finished');
|
this.logService.info('Sending finished');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -219,7 +73,7 @@ export class UhkDeviceService implements OnDestroy {
|
|||||||
|
|
||||||
applyConfig(): Observable<Buffer> {
|
applyConfig(): Observable<Buffer> {
|
||||||
return Observable.create((subscriber: Subscriber<Buffer>) => {
|
return Observable.create((subscriber: Subscriber<Buffer>) => {
|
||||||
console.log('Applying configuration');
|
this.logService.info('Applying configuration');
|
||||||
this.messageOut$.next({
|
this.messageOut$.next({
|
||||||
buffer: new Buffer([Command.ApplyConfig]),
|
buffer: new Buffer([Command.ApplyConfig]),
|
||||||
observer: subscriber
|
observer: subscriber
|
||||||
@@ -227,21 +81,27 @@ export class UhkDeviceService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeviceAttach(device: Device) {
|
isInitialized(): Observable<boolean> {
|
||||||
if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) {
|
return this.initialized$.asObservable();
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Ugly hack: device is not openable (on Windows) right after the attach
|
|
||||||
Observable.timer(100)
|
|
||||||
.first()
|
|
||||||
.subscribe(() => this.initialize());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeviceDetach(device: Device) {
|
isConnected(): Observable<boolean> {
|
||||||
if (device.deviceDescriptor.idVendor !== vendorId || device.deviceDescriptor.idProduct !== productId) {
|
return this.connected$.asObservable();
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOpened(): Observable<boolean> {
|
||||||
|
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<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
164
electron/src/services/uhk-hid-api.service.ts
Normal file
164
electron/src/services/uhk-hid-api.service.ts
Normal file
@@ -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<Buffer>) => {
|
||||||
|
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<void>>Observable.create((subscriber: Subscriber<void>) => {
|
||||||
|
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<void>();
|
||||||
|
});
|
||||||
|
}).publish();
|
||||||
|
this.outSubscription = outSending.connect();
|
||||||
|
|
||||||
|
this.initialized$.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermissions(): Observable<boolean> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
142
electron/src/services/uhk-lib-usb-api.service.ts
Normal file
142
electron/src/services/uhk-lib-usb-api.service.ts
Normal file
@@ -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<Buffer>) => {
|
||||||
|
const inEndPoint: 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 = <OutEndpoint>usbInterface.endpoints[1];
|
||||||
|
const outSending = this.messageOut$.concatMap(senderPackage => {
|
||||||
|
return (<Observable<void>>Observable.create((subscriber: Subscriber<void>) => {
|
||||||
|
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<void>();
|
||||||
|
});
|
||||||
|
}).publish();
|
||||||
|
this.outSubscription = outSending.connect();
|
||||||
|
|
||||||
|
this.initialized$.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermissions(): Observable<boolean> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
target: 'electron-renderer',
|
target: 'electron-renderer',
|
||||||
externals: {
|
externals: {
|
||||||
usb: 'usb'
|
usb: 'usb',
|
||||||
|
'node-hid':'nodeHid'
|
||||||
},
|
},
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -40,7 +41,8 @@ module.exports = {
|
|||||||
use: ['raw-loader', 'sass-loader']
|
use: ['raw-loader', 'sass-loader']
|
||||||
},
|
},
|
||||||
{ test: /jquery/, loader: 'expose-loader?$!expose-loader?jQuery' },
|
{ 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: [
|
plugins: [
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uhk-agent",
|
"name": "uhk-agent",
|
||||||
|
"author": "Ultimate Gadget Laboratories",
|
||||||
"main": "electron/dist/electron-main.js",
|
"main": "electron/dist/electron-main.js",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
|
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"@types/file-saver": "0.0.1",
|
"@types/file-saver": "0.0.1",
|
||||||
"@types/jquery": "3.2.1",
|
"@types/jquery": "3.2.1",
|
||||||
"@types/node": "^6.0.78",
|
"@types/node": "^6.0.78",
|
||||||
|
"@types/node-hid": "^0.5.2",
|
||||||
"@types/usb": "^1.1.3",
|
"@types/usb": "^1.1.3",
|
||||||
"angular2-template-loader": "0.6.2",
|
"angular2-template-loader": "0.6.2",
|
||||||
"copy-webpack-plugin": "^4.0.1",
|
"copy-webpack-plugin": "^4.0.1",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"npm-run-all": "4.0.2",
|
"npm-run-all": "4.0.2",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
|
"rimraf": "^2.6.1",
|
||||||
"sass-loader": "^6.0.3",
|
"sass-loader": "^6.0.3",
|
||||||
"standard-version": "^4.0.0",
|
"standard-version": "^4.0.0",
|
||||||
"stylelint": "^7.10.1",
|
"stylelint": "^7.10.1",
|
||||||
@@ -75,6 +78,7 @@
|
|||||||
"ng2-dragula": "1.5.0",
|
"ng2-dragula": "1.5.0",
|
||||||
"ng2-select2": "1.0.0-beta.10",
|
"ng2-select2": "1.0.0-beta.10",
|
||||||
"ngrx-store-freeze": "^0.1.9",
|
"ngrx-store-freeze": "^0.1.9",
|
||||||
|
"node-hid": "0.5.4",
|
||||||
"reselect": "3.0.1",
|
"reselect": "3.0.1",
|
||||||
"rxjs": "^5.4.1",
|
"rxjs": "^5.4.1",
|
||||||
"select2": "^4.0.3",
|
"select2": "^4.0.3",
|
||||||
@@ -85,17 +89,17 @@
|
|||||||
"zone.js": "0.8.12"
|
"zone.js": "0.8.12"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "run-p build:usb \"symlink -- -i\" ",
|
"postinstall": "run-p \"symlink -- -i\" ",
|
||||||
"test": "cd ./test-serializer && node ./test-serializer.js",
|
"test": "cd ./test-serializer && node ./test-serializer.js",
|
||||||
"lint": "run-s -scn lint:ts lint:style",
|
"lint": "run-s -scn lint:ts lint:style",
|
||||||
"lint:ts": "tslint \"electron/src/**/*.ts\" \"web/src/**/*.ts\" \"shared/**/*.ts\" \"test-serializer/**/*.ts\"",
|
"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",
|
"lint:style": "stylelint \"electron/**/*.scss\" \"web/**/*.scss\" \"shared/**/*.scss\" --syntax scss",
|
||||||
"build": "run-p build:web build:electron",
|
"build": "run-p build:web build:electron",
|
||||||
"build:web": "webpack --config \"web/src/webpack.config.js\"",
|
"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:main": "webpack --config \"electron/src/webpack.config.electron-main.js\"",
|
||||||
"build:electron:app": "webpack --config \"electron/src/webpack.config.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\"",
|
"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:web": "webpack-dev-server --config \"web/src/webpack.config.js\" --content-base \"./web/dist\"",
|
||||||
"server:electron": "webpack --config \"electron/src/webpack.config.js\" --watch",
|
"server:electron": "webpack --config \"electron/src/webpack.config.js\" --watch",
|
||||||
@@ -103,7 +107,8 @@
|
|||||||
"symlink": "node ./tools/symlinker",
|
"symlink": "node ./tools/symlinker",
|
||||||
"standard-version": "standard-version",
|
"standard-version": "standard-version",
|
||||||
"pack": "node ./scripts/release.js",
|
"pack": "node ./scripts/release.js",
|
||||||
"install:build-deps": "cd electron/dist && npm i",
|
"install:build-deps": "cd electron/dist && npm i && cd .. && npm run build:usb",
|
||||||
"release": "npm run install:build-deps && node ./scripts/release.js"
|
"release": "node ./scripts/release.js",
|
||||||
|
"clean": "rimraf ./node_modules ./electron/dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
rules/zadic-ia32.exe
Normal file
BIN
rules/zadic-ia32.exe
Normal file
Binary file not shown.
BIN
rules/zadic-x64.exe
Normal file
BIN
rules/zadic-x64.exe
Normal file
Binary file not shown.
@@ -1,7 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const jsonfile = require('jsonfile');
|
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) {
|
if (!process.env.CI && !TEST_BUILD) {
|
||||||
console.error('Create release only on CI server');
|
console.error('Create release only on CI server');
|
||||||
@@ -25,7 +29,7 @@ if (process.env.TRAVIS) {
|
|||||||
repoName = process.env.APPVEYOR_REPO_NAME;
|
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';
|
const isReleaseCommit = TEST_BUILD || branchName === gitTag && repoName === 'UltimateHackingKeyboard/agent';
|
||||||
|
|
||||||
@@ -51,16 +55,23 @@ if (process.env.TRAVIS) {
|
|||||||
|
|
||||||
let target = '';
|
let target = '';
|
||||||
let artifactName = 'UHK.Agent-${version}-${os}';
|
let artifactName = 'UHK.Agent-${version}-${os}';
|
||||||
|
let extraResources = [];
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
target = Platform.MAC.createTarget();
|
target = Platform.MAC.createTarget();
|
||||||
artifactName += '.${ext}';
|
artifactName += '.${ext}';
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
target = Platform.WINDOWS.createTarget();
|
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}';
|
artifactName += '-${arch}.${ext}';
|
||||||
|
extraResources.push(`rules/zadic-${process.arch}.exe`);
|
||||||
} else if (process.platform === 'linux') {
|
} else if (process.platform === 'linux') {
|
||||||
target = Platform.LINUX.createTarget();
|
target = Platform.LINUX.createTarget();
|
||||||
artifactName += '.${ext}';
|
artifactName += '.${ext}';
|
||||||
|
extraResources.push('rules/setup-rules.sh');
|
||||||
|
extraResources.push('rules/50-uhk60.rules');
|
||||||
} else {
|
} else {
|
||||||
console.error(`I dunno how to publish a release for ${process.platform} :(`);
|
console.error(`I dunno how to publish a release for ${process.platform} :(`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -78,7 +89,7 @@ if (TEST_BUILD || gitTag) {
|
|||||||
updateVersionNumberIn2rndPackageJson(jsonVersion);
|
updateVersionNumberIn2rndPackageJson(jsonVersion);
|
||||||
|
|
||||||
builder.build({
|
builder.build({
|
||||||
dir: true,
|
dir: TEST_BUILD,
|
||||||
targets: target,
|
targets: target,
|
||||||
appMetadata: {
|
appMetadata: {
|
||||||
main: 'electron-main.js',
|
main: 'electron-main.js',
|
||||||
@@ -97,6 +108,12 @@ if (TEST_BUILD || gitTag) {
|
|||||||
mac: {
|
mac: {
|
||||||
category: 'public.app-category.utilities'
|
category: 'public.app-category.utilities'
|
||||||
},
|
},
|
||||||
|
win: {
|
||||||
|
extraResources
|
||||||
|
},
|
||||||
|
linux: {
|
||||||
|
extraResources
|
||||||
|
},
|
||||||
publish: 'github',
|
publish: 'github',
|
||||||
artifactName,
|
artifactName,
|
||||||
files: [
|
files: [
|
||||||
|
|||||||
20
shared/src/services/logger.service.ts
Normal file
20
shared/src/services/logger.service.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
shared/src/util/constants.ts
Normal file
5
shared/src/util/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export namespace Constants {
|
||||||
|
export const VENDOR_ID = 0x1D50;
|
||||||
|
export const PRODUCT_ID = 0x6122;
|
||||||
|
export const MAX_PAYLOAD_SIZE = 64;
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { Constants } from './constants';
|
||||||
|
|
||||||
|
export { Constants };
|
||||||
|
|
||||||
// Source: http://stackoverflow.com/questions/13720256/javascript-regex-camelcase-to-sentence
|
// Source: http://stackoverflow.com/questions/13720256/javascript-regex-camelcase-to-sentence
|
||||||
export function camelCaseToSentence(camelCasedText: string): string {
|
export function camelCaseToSentence(camelCasedText: string): string {
|
||||||
return camelCasedText.replace(/^[a-z]|[A-Z]/g, function (v, i) {
|
return camelCasedText.replace(/^[a-z]|[A-Z]/g, function (v, i) {
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ import { MacroNotFoundGuard } from './shared/components/macro/not-found';
|
|||||||
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
|
import { DATA_STORAGE_REPOSITORY } from './shared/services/datastorage-repository.service';
|
||||||
import { LocalDataStorageRepositoryService } from './shared/services/local-datastorage-repository.service';
|
import { LocalDataStorageRepositoryService } from './shared/services/local-datastorage-repository.service';
|
||||||
import { DefaultUserConfigurationService } from './shared/services/default-user-configuration.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';
|
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -159,10 +160,11 @@ import { AutoUpdateSettings } from './shared/components/auto-update-settings/aut
|
|||||||
KeymapEditGuard,
|
KeymapEditGuard,
|
||||||
MacroNotFoundGuard,
|
MacroNotFoundGuard,
|
||||||
CaptureService,
|
CaptureService,
|
||||||
{ provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService },
|
{provide: DATA_STORAGE_REPOSITORY, useClass: LocalDataStorageRepositoryService},
|
||||||
|
DefaultUserConfigurationService,
|
||||||
|
{ provide: LOG_SERVICE, useClass: ConsoleLogService },
|
||||||
DefaultUserConfigurationService
|
DefaultUserConfigurationService
|
||||||
],
|
],
|
||||||
bootstrap: [MainAppComponent]
|
bootstrap: [MainAppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule { }
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user