From 517aed1b1c35633b2c20c009ffb7a6eaeb247a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Farkas=20J=C3=B3zsef?= Date: Thu, 19 Jan 2017 19:03:02 +0100 Subject: [PATCH] Transfer the layer/keymap/config with node-usb on shortcuts --- .travis.yml | 21 ++- package.json | 4 + src/app.module.ts | 4 +- .../keymap/edit/keymap-edit.component.ts | 67 +++++++- .../svg/wrap/svg-keyboard-wrap.component.ts | 4 + src/index.html | 4 + src/main-app/main-app.component.ts | 62 ++++++- src/main.ts | 4 +- src/polyfills.ts | 1 + src/services/uhk-device.service.ts | 159 ++++++++++++++++++ src/tsconfig.json | 3 +- 11 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 src/services/uhk-device.service.ts diff --git a/.travis.yml b/.travis.yml index b27f131c..91b9facc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,16 @@ language: node_js -sudo: false +sudo: required node_js: - - '5.10.0' + - '6.9.4' + +before_install: + - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + - sudo apt-get update -q + - sudo apt-get install g++-4.8 -y install: + - npm install -g npm@3.10.7 - npm install before_script: @@ -18,4 +24,13 @@ script: cache: directories: - - node_modules \ No newline at end of file + - node_modules + +env: + - CXX=g++-4.8 + +addons: + apt: + packages: + - build-essential + - libudev-dev diff --git a/package.json b/package.json index 821aae80..31ccd65b 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "@types/electron": "^1.4.29", "@types/jquery": "2.0.39", "@types/node": "6.0.52", + "@types/usb": "^1.1.2", "copy-webpack-plugin": "^4.0.1", "electron": "1.4.15", + "electron-rebuild": "^1.5.5", "expose-loader": "^0.7.1", "html-loader": "0.4.4", "node-sass": "^4.3.0", @@ -54,10 +56,12 @@ "rxjs": "5.0.3", "select2": "^4.0.3", "typescript": "2.1.5", + "usb": "git+https://github.com/aktary/node-usb.git", "xml-loader": "^1.1.0", "zone.js": "0.7.6" }, "scripts": { + "postinstall": "electron-rebuild -w usb -p", "tslint": "tslint \"src/**/*.ts\" \"test-serializer/**/*.ts\"", "stylelint": "stylelint \"src/**/*.scss\" --syntax scss", "lint": "run-s -scn tslint stylelint", diff --git a/src/app.module.ts b/src/app.module.ts index 242f0e93..57b8d676 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -64,6 +64,7 @@ import { CancelableDirective } from './directives'; import { CaptureService } from './services/capture.service'; import { MapperService } from './services/mapper.service'; +import { UhkDeviceService } from './services/uhk-device.service'; import { KeymapEffects, MacroEffects } from './store/effects'; import { keymapReducer, macroReducer, presetReducer } from './store/reducers'; @@ -156,7 +157,8 @@ const storeConfig = { appRoutingProviders, KeymapEditGuard, MacroNotFoundGuard, - CaptureService + CaptureService, + UhkDeviceService ], bootstrap: [MainAppComponent] }) diff --git a/src/components/keymap/edit/keymap-edit.component.ts b/src/components/keymap/edit/keymap-edit.component.ts index 7dff2339..c559522e 100644 --- a/src/components/keymap/edit/keymap-edit.component.ts +++ b/src/components/keymap/edit/keymap-edit.component.ts @@ -1,17 +1,22 @@ -import { Component } from '@angular/core'; +import { Component, HostListener, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import '@ngrx/core/add/operator/select'; import { Store } from '@ngrx/store'; +import 'rxjs/add/operator/first'; import 'rxjs/add/operator/let'; import 'rxjs/add/operator/publishReplay'; import 'rxjs/add/operator/switchMap'; import { Observable } from 'rxjs/Observable'; import { Keymap } from '../../../config-serializer/config-items/Keymap'; +import { UhkBuffer } from '../../../config-serializer/UhkBuffer'; import { AppState } from '../../../store'; import { getKeymap, getKeymapEntities } from '../../../store/reducers/keymap'; +import { SvgKeyboardWrapComponent } from '../../svg/wrap'; + +import { UhkDeviceService } from '../../../services/uhk-device.service'; @Component({ selector: 'keymap-edit', @@ -22,12 +27,16 @@ import { getKeymap, getKeymapEntities } from '../../../store/reducers/keymap'; } }) export class KeymapEditComponent { + + @ViewChild(SvgKeyboardWrapComponent) wrap: SvgKeyboardWrapComponent; + private keymap$: Observable; private deletable$: Observable; constructor( private store: Store, - private route: ActivatedRoute + private route: ActivatedRoute, + private uhkDevice: UhkDeviceService ) { this.keymap$ = route .params @@ -39,4 +48,58 @@ export class KeymapEditComponent { this.deletable$ = store.let(getKeymapEntities()) .map((keymaps: Keymap[]) => keymaps.length > 1); } + + @HostListener('window:keydown.control.u', ['$event']) + onCtrlU(event: KeyboardEvent): void { + console.log('ctrl + u pressed'); + event.preventDefault(); + event.stopPropagation(); + this.sendLayer(); + } + + @HostListener('window:keydown.control.i', ['$event']) + onCtrlI(event: KeyboardEvent): void { + console.log('ctrl + i pressed'); + event.preventDefault(); + event.stopPropagation(); + this.sendKeymap(); + } + + private sendLayer(): void { + const currentLayer: number = this.wrap.getSelectedLayer(); + this.keymap$ + .first() + .map(keymap => keymap.layers[currentLayer]) + .map(layer => { + const uhkBuffer = new UhkBuffer(); + layer.toBinary(uhkBuffer); + return uhkBuffer.getBufferContent(); + }) + .switchMap((buffer: Buffer) => this.uhkDevice.sendConfig(buffer)) + .do(response => console.log('Sending layer finished', response)) + .switchMap(() => this.uhkDevice.applyConfig()) + .subscribe( + (response) => console.log('Applying layer finished', response), + error => console.error('Error during uploading layer', error), + () => console.log('Layer has been sucessfully uploaded') + ); + } + + private sendKeymap(): void { + this.keymap$ + .first() + .map(keymap => { + const uhkBuffer = new UhkBuffer(); + keymap.toBinary(uhkBuffer); + return uhkBuffer.getBufferContent(); + }) + .switchMap((buffer: Buffer) => this.uhkDevice.sendConfig(buffer)) + .do(response => console.log('Sending keymap finished', response)) + .switchMap(() => this.uhkDevice.applyConfig()) + .subscribe( + (response) => console.log('Applying keymap finished', response), + error => console.error('Error during uploading keymap', error), + () => console.log('Keymap has been sucessfully uploaded') + ); + } } diff --git a/src/components/svg/wrap/svg-keyboard-wrap.component.ts b/src/components/svg/wrap/svg-keyboard-wrap.component.ts index 95944635..ffcfad4a 100644 --- a/src/components/svg/wrap/svg-keyboard-wrap.component.ts +++ b/src/components/svg/wrap/svg-keyboard-wrap.component.ts @@ -227,6 +227,10 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges { this.currentLayer = index; } + getSelectedLayer(): number { + return this.currentLayer; + } + private getKeyActionContent(keyAction: KeyAction): Observable { if (keyAction instanceof KeystrokeAction) { const keystrokeAction: KeystrokeAction = keyAction; diff --git a/src/index.html b/src/index.html index 265a9d2e..9196967c 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,10 @@ + + diff --git a/src/main-app/main-app.component.ts b/src/main-app/main-app.component.ts index 9db4848f..6111d433 100644 --- a/src/main-app/main-app.component.ts +++ b/src/main-app/main-app.component.ts @@ -1,4 +1,16 @@ -import { Component, ViewEncapsulation } from '@angular/core'; +import { Component, ViewEncapsulation, HostListener } from '@angular/core'; + +// import { Observable } from 'rxjs/Observable'; + +// import { Store } from '@ngrx/store'; +// import { AppState } from '../store'; +// import { DataStorage } from '../store/storage'; +// import { getKeymapEntities, getMacroEntities } from '../store/reducers'; + +// import { UhkBuffer } from '../config-serializer/UhkBuffer'; +// import { UserConfiguration } from '../config-serializer/config-items/UserConfiguration'; + +// import { UhkDeviceService } from '../services/uhk-device.service'; @Component({ selector: 'main-app', @@ -6,4 +18,50 @@ import { Component, ViewEncapsulation } from '@angular/core'; styles: [require('./main-app.component.scss')], encapsulation: ViewEncapsulation.None }) -export class MainAppComponent { } +export class MainAppComponent { + + // private configuration$: Observable; + + constructor( + // private uhkDevice: UhkDeviceService, + // store: Store, + // dataStorage: DataStorage + ) { + // this.configuration$ = store.let(getKeymapEntities()) + // .combineLatest(store.let(getMacroEntities())) + // .map((pair) => { + // const config = new UserConfiguration(); + // Object.assign(config, dataStorage.getConfiguration()); + // config.keymaps = pair[0]; + // config.macros = pair[1]; + // return config; + // }); + } + + @HostListener('window:keydown.control.o', ['$event']) + onCtrlO(event: KeyboardEvent): void { + console.log('ctrl + o pressed'); + event.preventDefault(); + event.stopPropagation(); + this.sendConfiguration(); + } + + private sendConfiguration(): void { + // this.configuration$ + // .first() + // .map(configuration => { + // const uhkBuffer = new UhkBuffer(); + // configuration.toBinary(uhkBuffer); + // return uhkBuffer.getBufferContent(); + // }) + // .switchMap((buffer: Buffer) => this.uhkDevice.sendConfig(buffer)) + // .do(response => console.log('Sending config finished', response)) + // .switchMap(() => this.uhkDevice.applyConfig()) + // .subscribe( + // (response) => console.log('Applying config finished', response), + // error => console.error('Error during uploading config', error), + // () => console.log('Config has been sucessfully uploaded') + // ); + } + +} diff --git a/src/main.ts b/src/main.ts index 1bf928da..d0f8e749 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; -process.stdout = require('browser-stdout')(); +if (!process.stdout) { + process.stdout = require('browser-stdout')(); +} platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/src/polyfills.ts b/src/polyfills.ts index a386ad53..212b227b 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,2 +1,3 @@ +import 'core-js/es7/object'; import 'core-js/es7/reflect'; import 'zone.js/dist/zone'; diff --git a/src/services/uhk-device.service.ts b/src/services/uhk-device.service.ts new file mode 100644 index 00000000..b6cd9bc1 --- /dev/null +++ b/src/services/uhk-device.service.ts @@ -0,0 +1,159 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; +import { ConnectableObservable } from 'rxjs/observable/ConnectableObservable'; +import { Subject } from 'rxjs/Subject'; +import { Subscriber } from 'rxjs/Subscriber'; +import { Subscription } from 'rxjs/Subscription'; + +import 'rxjs/add/observable/empty'; +import 'rxjs/add/observable/from'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/concat'; +import 'rxjs/add/operator/concatMap'; +import 'rxjs/add/operator/mergeMap'; +import 'rxjs/add/operator/publish'; + +import { Device, Interface, InEndpoint, OutEndpoint, findByIds } from 'usb'; + +import { Layer } from '../config-serializer/config-items/Layer'; +import { UhkBuffer } from '../config-serializer/UhkBuffer'; + +const vendorId = 0x16d3; +const productId = 0x05ea; +const MAX_PAYLOAD_SIZE = 64; + +enum Command { + UploadConfig = 8, + ApplyConfig = 9 +}; + +interface SenderMessage { + buffer: Buffer; + observer: Observer; +}; + +@Injectable() +export class UhkDeviceService { + + private device: Device; + private connected: boolean; + + private messageIn$: Observable; + private messageOut$: Subject; + + private outSubscription: Subscription; + + constructor() { + this.messageOut$ = new Subject(); + this.connect(); + } + + connect(): void { + if (this.connected) { + return; + } + this.device = findByIds(vendorId, productId); + this.device.open(); + + 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(); + } + 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.connected = true; + } + + disconnect() { + if (!this.connected) { + return; + } + this.outSubscription.unsubscribe(); + this.messageIn$ = undefined; + this.device.interface(0).release(); + this.device.close(); + this.connected = false; + } + + sendConfig(configBuffer: Buffer): Observable { + return Observable.create((subscriber: Subscriber) => { + console.log('Sending...', configBuffer); + const fragments: Buffer[] = []; + const MAX_SENDING_PAYLOAD_SIZE = 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 + : configBuffer.length - offset; + const header = new Buffer([Command.UploadConfig, length, offset & 0xFF, offset >> 8]); + fragments.push(Buffer.concat([header, configBuffer.slice(offset, offset + length)])); + } + + const buffers: Buffer[] = []; + const observer: Observer = { + next: (buffer: Buffer) => buffers.push(buffer), + error: error => subscriber.error(error), + complete: () => { + if (buffers.length === fragments.length) { + subscriber.next(Buffer.concat(buffers)); + subscriber.complete(); + console.log('Sending finished'); + } + } + }; + + fragments + .map(fragment => ({ buffer: fragment, observer })) + .forEach(senderPackage => this.messageOut$.next(senderPackage)); + }); + } + + applyConfig(): Observable { + return Observable.create((subscriber: Subscriber) => { + console.log('Applying configuration'); + this.messageOut$.next({ + buffer: new Buffer([Command.ApplyConfig]), + observer: subscriber + }); + }); + } + +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 7be8dc9d..5a04149d 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -17,7 +17,8 @@ "jquery", "core-js", "select2", - "electron" + "electron", + "usb" ] }, "exclude": [