├── .editorconfig ├── .gitignore ├── README.md ├── doc ├── example.png ├── example.tiff └── nuimo_mqtt.png ├── package.json └── src ├── app ├── app.ts ├── appManager.ts ├── automapperConfig.ts ├── ledMatrices.ts ├── nuimoApp.ts ├── nuimoEventConverter.ts ├── nuimoManager.ts └── nuimoMqttMessages.ts ├── test └── nuimoMqttMessages.test.ts ├── tsconfig.json ├── tslint.json └── typings.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # VS Code 37 | .vscode 38 | 39 | # Build files 40 | build 41 | 42 | # typings 43 | src/typings -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | This application is meant as a proof of concept to let multiple Nuimos work with multiple apps running on multiple devices. 3 | It removes the limitation of not being able to control the music on your phone while your computer is connected to it. 4 | 5 | # Demonstration 6 | A demo video can be seen at https://vimeo.com/162261040. 7 | 8 | # Multiple Nuimos 9 | This is not worked out in the application yet since I had no devices to test with, but the protocol is prepared for it. 10 | 11 | # Use cases 12 | *Note: these are ideas, but not all use cases are implemented* 13 | 14 | - Peter has Photoshop on his computer and a music player on his phone. He would like to control both applications with Nuimo without disconnect Nuimo from one of the devices. 15 | - Sandra has two Nuimos: one in her living room and one in her bedroom. She wants to control Photoshop, the light and music with her Nuimo in the living room, but the one in her bedroom should be simple: she wants to control only the light with that one. 16 | - Kwint has Photoshop and After Effects on his computer. When he uses Alt-Tab to switch between applications, he wants to have the controls on his Nuimo for the application that he switched to. 17 | - Tessa has two applications on her computer. When she selects another application on her Nuimo, she wants the selected application to get focus on her computer. 18 | - Jeff has a programming language that does not work well with Bluetooth Low Energy. He still wants his application to work with Nuimo. 19 | 20 | # Technical solution 21 | - Aggregate list of available app controls (not worked out) 22 | - App controls built into the manager app 23 | - Additional app controls dynamically added from other apps 24 | - List of Nuimos (not worked out) 25 | - List is filled by discovering Nuimos 26 | - Disconnected Nuimos stay in the list unless the user deletes them 27 | - Nuimos can have different states 28 | - App controls can be assigned to Nuimos that are not connected 29 | - Apps communicate over MQTT 30 | - Apps register and unregister themselves via MQTT on a general MQTT management channel (nuimo) 31 | - Apps receive their commands on their own MQTT channel (nuimo/\/\) 32 | - Apps can publish icons on their own MQTT channel (nuimo/\/\) 33 | - Apps can request their controls to be active on a certain Nuimo via MQTT 34 | 35 | # User interface 36 | A proposal for a user interface is shown below: 37 | 38 | ![User interface proposal](https://raw.githubusercontent.com/wind-rider/nuimo-mqtt-manager/master/doc/example.png) 39 | 40 | * Aggregate list of all available app controls 41 | * Built-in app controls that are built into the manager app 42 | * Additional app controls dynamically added from other apps 43 | * App controls can be unregistered by clicking the minus button 44 | * List of Nuimos 45 | * Nuimos enter the list automatically by discovery 46 | * Users can overwrite the Nuimo's default ID with a userfriendly name 47 | * Nuimos are not automatically removed when they disconnect or are out of sight 48 | * Nuimos can be forgotten by the user by clicking the minus button 49 | * Assigning of app controls to Nuimos 50 | * Available app controls can be added to a Nuimo by selecting a Nuimo and dragging the app control to the list showing the app controls for that Nuimo 51 | 52 | # System diagram 53 | A system overview is shown below. 54 | * The Nuimos connect over BLE to a device running the manager application 55 | * The client applications connect via a MQTT broker (message bus) to the manager application. This MQTT broker can be hosted in your home or in the cloud. 56 | 57 | ![Nuimo-MQTT system overview](https://raw.githubusercontent.com/wind-rider/nuimo-mqtt-manager/master/doc/nuimo_mqtt.png) 58 | 59 | 60 | # Protocol 61 | There are three types of channels (MQTT topics): 62 | 63 | * **nuimo** - this is a general channel where apps register and unregister themselves. Also the central app can show the current state here 64 | * **nuimo/log** - this is a logging channel for debugging purposes 65 | * **nuimo/\/\** - these are the channels where apps receive the messages from their nuimo if they are active, and where they post their icons 66 | 67 | ## MQTT topic: `nuimo` 68 | ### register 69 | Command sent by an app to let the nuimo-mqtt daemon 70 | * add the app to the available apps list 71 | * switch to the registered app and show its icon. 72 | 73 | ``` 74 | { 75 | "command": "register", 76 | "id": "idOftheApp", 77 | "name": "Display name of the app", 78 | "icon": "string of 81 characters representing an icon" 79 | } 80 | ``` 81 | 82 | ### unregister 83 | Command sent by an app to let the nuimo-mqtt daemon 84 | * remove the app from the available apps list 85 | * switch to the next app on the Nuimo(s) where the app was available. 86 | 87 | Format: 88 | ``` 89 | { 90 | "command": "unregister", 91 | "id": "idOfTheAppThatWantsToBeUnregistered" 92 | } 93 | ``` 94 | 95 | ## MQTT topic: `nuimo//` 96 | 97 | Nuimo events are only sent to the app that is currently 'active' on a Nuimo so that there will not be unintended input to other apps. Apps can still request to become active by publishing a `listenPlease` command on their channel. 98 | 99 | ### listenPlease 100 | The listenPlease message is to tell an app that the user selected it so that the app is expected to start listening. The app's icon will be shown shortly. 101 | 102 | (Not implemented yet) 103 | 104 | ### showIcon 105 | Command to shortly show an icon, for example to respond to user input or to show some notification. 106 | 107 | * `icon` should consist of 81 elements in total (string of 81 characters, or a string[] or number[] of 81 elements in total). The allowed values are 0 or 1. 108 | * `brightness` is a value between 0 and 1; optional 109 | * `duration` is a number in seconds; optional 110 | 111 | Format: 112 | ``` 113 | { 114 | "command": "showIcon", 115 | "icon": "string, array of strings or array of numbers representing an icon", 116 | "brightness": 0.3, 117 | "duration": 0.3 118 | } 119 | ``` 120 | 121 | ### showNamedIcon 122 | Command to shortly show an icon, for example to respond to user input or to show some notification. 123 | 124 | * `iconName` should be a name of one of the predefined icons from ledMatrices.ts 125 | * `brightness` is a value between 0 and 1; optional 126 | * `duration` is a number in seconds; optional 127 | 128 | Format: 129 | ``` 130 | { 131 | "command": "showNamedIcon", 132 | "iconName": "iconName", 133 | "brightness": 0.3, 134 | "duration": 0.3 135 | } 136 | ``` 137 | 138 | ### showProgressBarIcon 139 | Command to shortly show a progress bar icon, for example to respond to user input or to show some notification. 140 | 141 | * `value` is a number between 0 and 1 142 | * `style` should be either "VerticalBar" or "VolumeBar" 143 | * `brightness` is a value between 0 and 1; optional 144 | * `duration` is a number in seconds; optional 145 | 146 | Format: 147 | ``` 148 | { 149 | "command": "showProgressBarIcon", 150 | "value":0.77, 151 | "style": "VerticalBar", 152 | "brightness": 0.3, 153 | "duration": 0.3 154 | } 155 | ``` 156 | 157 | ### nuimoEvent 158 | Gesture event from Nuimo. The apps should listen to this event. 159 | 160 | Nuimo events are only sent to the app that is currently 'active' on a Nuimo so that there will not be unintended input to other apps. 161 | 162 | * `gesture` is an enum defined in `nuimoMqttMessages.ts`. 163 | * `value` is a number used for the FlyUpdate (for its speed) and for TurnUpdate (for its offset) 164 | 165 | Format: 166 | ``` 167 | { 168 | "command": "nuimoEvent", 169 | "gesture": "RotateRight", //one of the gesture types like ButtonPress etc 170 | "value": 24 //depends on gesture type 171 | } 172 | ``` 173 | -------------------------------------------------------------------------------- /doc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansmbakker/nuimo-mqtt-manager/8fdbc05c40b111771cd69e95bd9be0340cb9f0db/doc/example.png -------------------------------------------------------------------------------- /doc/example.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansmbakker/nuimo-mqtt-manager/8fdbc05c40b111771cd69e95bd9be0340cb9f0db/doc/example.tiff -------------------------------------------------------------------------------- /doc/nuimo_mqtt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hansmbakker/nuimo-mqtt-manager/8fdbc05c40b111771cd69e95bd9be0340cb9f0db/doc/nuimo_mqtt.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuimo-mqtt-manager", 3 | "version": "1.0.0", 4 | "description": "Daemon to make Senic Nuimo events available through apps over MQTT", 5 | "main": "build/app/app.js", 6 | "scripts": { 7 | "fix-nuimo-client": "cd node_modules/nuimo-client && npm install", 8 | "install-typings": "cd src && typings install", 9 | "postinstall": "npm run fix-nuimo-client && npm run install-typings", 10 | "prebuild": "rimraf build", 11 | "build": "tsc -p src/ || true", 12 | "test": "mocha build/test --debug-brk --require source-map-support/register || true", 13 | "clean": "rimraf build" 14 | }, 15 | "keywords": [ 16 | "Senic", 17 | "Nuimo", 18 | "MQTT" 19 | ], 20 | "author": "Hans Bakker", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "rimraf": "^2.5.2", 24 | "sinon": "^1.17.3", 25 | "typescript": "^1.8.9", 26 | "typings": "^0.7.9", 27 | "assert": "^1.3.0", 28 | "mocha": "^2.4.5", 29 | "tslint": "^3.6.0" 30 | }, 31 | "dependencies": { 32 | "@types/node": "^6.0.72", 33 | "automapper-ts": "^1.6.3", 34 | "events": "^1.1.1", 35 | "mqtt": "^1.7.4", 36 | "nuimo-client": "git+https://github.com/brendonparker/nuimo-client.js.git", 37 | "nuimo-client-ts": "https://github.com/wind-rider/nuimo-client.ts/releases/download/v0.4.0/nuimo-client-ts-0.4.0.tgz", 38 | "source-map-support": "^0.4.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/app.ts: -------------------------------------------------------------------------------- 1 | import "source-map-support/register"; 2 | 3 | import MqttJs = require("mqtt"); 4 | import { Update, SwipeUpdate } from "nuimo-client-ts"; 5 | 6 | import { AppManager } from "./appManager"; 7 | import { NuimoApp } from "./nuimoApp"; 8 | 9 | import { NuimoManager, NuimoDelegate } from "./nuimoManager"; 10 | import { NuimoMqttMessage, NuimoMqttRegisterCommand, NuimoMqttUnregisterCommand, NuimoShowIconCommand, NuimoShowNamedIconCommand, NuimoEventMessage, NuimoGesture } from "./nuimoMqttMessages"; 11 | import { default as LedMatrices, IconDictionary } from "./ledMatrices"; 12 | import { default as createNuimoEventMessage } from "./nuimoEventConverter"; 13 | 14 | class NuimoMqttManager implements NuimoDelegate { 15 | 16 | mqttClient: MqttJs.Client; 17 | nuimoManager: NuimoManager; 18 | appManager = new AppManager(); 19 | 20 | appTopicRegex = (appId: string): RegExp => { 21 | var regex = new RegExp('^nuimo\/(.{8}-?.{4}-?.{4}-?.{4}-?.{12})\/' + appId + '$'); 22 | return regex; 23 | }; 24 | 25 | genericAppTopicRegex = /^nuimo\/(.{8}-?.{4}-?.{4}-?.{4}-?.{12})\/(.*)$/; 26 | mainTopic = "nuimo"; 27 | logTopic = "nuimo/log"; 28 | appTopic = (appId: string): string => { 29 | if(this.nuimoManager && this.nuimoManager.nuimo && appId) { 30 | return "nuimo/" + this.nuimoManager.nuimo.uuid + '/' + appId; 31 | } 32 | return null; 33 | }; 34 | currentAppTopic = (): string => { 35 | if (this.appManager) { 36 | let currentApp = this.appManager.currentApp(); 37 | if(currentApp) { 38 | return this.appTopic(currentApp.id); 39 | } 40 | } 41 | return null; 42 | }; 43 | 44 | receiveUpdate = (update: Update) => { 45 | if (update instanceof SwipeUpdate) { 46 | let swipeUpdate = update; 47 | if (swipeUpdate.direction == "d") { 48 | let newApp = this.appManager.nextApp(); 49 | this.switchToApp(newApp); 50 | return; 51 | } 52 | } 53 | 54 | let currentAppTopic = this.currentAppTopic(); 55 | if (currentAppTopic) { 56 | 57 | let nuimoEventMessage = createNuimoEventMessage(update); 58 | 59 | this.mqttClient.publish(currentAppTopic, JSON.stringify(nuimoEventMessage)); 60 | } 61 | } 62 | 63 | start = async () => { 64 | await this.setupMqtt(); 65 | await this.setupNuimo(); 66 | } 67 | 68 | setupMqtt = async () => { 69 | return new Promise((resolve, reject) => { 70 | this.mqttClient = MqttJs.connect("ws://broker.hivemq.com:8000"); 71 | 72 | this.mqttClient.on("connect", () => { 73 | this.mqttClient.subscribe(this.mainTopic); 74 | this.logMqtt("NuimoMqttManager online"); 75 | resolve(); 76 | }); 77 | 78 | 79 | this.mqttClient.on("message", (topic: string, message: Buffer) => { 80 | 81 | try { 82 | // message is Buffer 83 | let jsonObject = JSON.parse(message.toString()); 84 | let mqttMessage: NuimoMqttMessage = jsonObject; 85 | 86 | this.handleMqttMessage(topic, mqttMessage); 87 | } 88 | catch (ex) { 89 | this.logMqtt("not supported: " + ex); 90 | } 91 | }); 92 | }); 93 | } 94 | 95 | setupNuimo = async () => { 96 | this.logMqtt("Connecting to Nuimo"); 97 | 98 | this.nuimoManager = new NuimoManager(); 99 | await this.nuimoManager.connect(); 100 | this.logMqtt("Nuimo connected"); 101 | this.nuimoManager.setDelegate(this); 102 | this.logMqtt("NuimoMqttManager will now receive Nuimo events"); 103 | 104 | return; 105 | } 106 | 107 | private handleMqttMessage(topic: string, mqttMessage: NuimoMqttMessage) { 108 | let currentAppId = this.appManager.currentApp() && this.appManager.currentApp().id; 109 | 110 | if (topic === this.mainTopic) { 111 | switch (mqttMessage.command) { 112 | case "register": 113 | this.registerApp(mqttMessage); 114 | break; 115 | case "unregister": 116 | this.unregisterApp(mqttMessage); 117 | break; 118 | } 119 | } 120 | else if (this.appTopicRegex(currentAppId).test(topic)) { 121 | switch (mqttMessage.command) { 122 | case "showIcon": 123 | this.showIcon(mqttMessage); 124 | break; 125 | case "showNamedIcon": 126 | this.showNamedIcon(mqttMessage); 127 | break; 128 | case "showProgressBarIcon": 129 | this.notImplementedYet(mqttMessage); 130 | break; 131 | case "nuimoEvent": 132 | // this event is meant to be handled by clients 133 | // so don't handle it here 134 | break; 135 | default: 136 | this.notImplementedYet(mqttMessage); 137 | break; 138 | 139 | } 140 | } 141 | else if (this.genericAppTopicRegex.test(topic)) { 142 | switch (mqttMessage.command) { 143 | case "listenPlease": 144 | this.notImplementedYet(mqttMessage); 145 | break; 146 | } 147 | } 148 | else { 149 | this.notImplementedYet(mqttMessage); 150 | } 151 | } 152 | 153 | private switchToApp(app: NuimoApp) { 154 | if(app) { 155 | this.nuimoManager.showIcon(app.icon, 1, 1); 156 | } 157 | } 158 | 159 | registerApp(messageFromMqtt: NuimoMqttMessage) { 160 | let registerMessage = messageFromMqtt; 161 | let app = new NuimoApp(registerMessage.id, registerMessage.name, registerMessage.icon) 162 | this.appManager.addApp(app); 163 | this.mqttClient.subscribe(this.appTopic(app.id)); 164 | this.switchToApp(app); 165 | } 166 | 167 | unregisterApp(messageFromMqtt: NuimoMqttMessage) { 168 | let unregisterMessage = messageFromMqtt; 169 | this.appManager.removeApp(unregisterMessage.id); 170 | this.mqttClient.subscribe(this.appTopic(unregisterMessage.id)); 171 | let newApp = this.appManager.currentApp(); 172 | this.switchToApp(newApp); 173 | } 174 | 175 | showIcon(messageFromMqtt: NuimoMqttMessage) { 176 | let iconMessage = messageFromMqtt; 177 | this.nuimoManager.showIcon(iconMessage.icon, iconMessage.brightness, iconMessage.duration) 178 | } 179 | 180 | showNamedIcon(messageFromMqtt: NuimoMqttMessage) { 181 | let iconMessage = messageFromMqtt; 182 | let iconName = iconMessage.iconName; 183 | let icon = LedMatrices[iconName]; 184 | if(icon) { 185 | this.nuimoManager.showIcon(icon, iconMessage.brightness, iconMessage.duration) 186 | } 187 | else { 188 | this.logMqtt("icon " + iconName + " not defined!"); 189 | } 190 | } 191 | 192 | notImplementedYet(messageFromMqtt: NuimoMqttMessage) { 193 | this.logMqtt("not implemented yet: " + messageFromMqtt.toString()); 194 | } 195 | 196 | logMqtt(message: any) { 197 | this.mqttClient.publish(this.logTopic, message.toString()); 198 | } 199 | } 200 | 201 | var manager = new NuimoMqttManager(); 202 | manager.start(); 203 | -------------------------------------------------------------------------------- /src/app/appManager.ts: -------------------------------------------------------------------------------- 1 | import { NuimoApp } from "./nuimoApp"; 2 | 3 | export class AppManager { 4 | constructor() { 5 | this.currentAppIndex = -1; 6 | } 7 | 8 | private apps: Array = []; 9 | 10 | addApp(app: NuimoApp): void { 11 | if (!this.appIsRegistered(app.id)) { 12 | let newLength = this.apps.push(app); 13 | this.currentAppIndex = newLength - 1; 14 | } 15 | } 16 | 17 | removeApp(id: string): void { 18 | if (this.appIsRegistered(id)) { 19 | let index = this.apps.findIndex(app => app.id == id); 20 | this.apps.splice(index, 1); 21 | } 22 | } 23 | 24 | private appIsRegistered(id: string): boolean { 25 | let index = this.apps.findIndex(app => app.id == id); 26 | return index !== -1; 27 | } 28 | 29 | currentApp(): NuimoApp { 30 | if (this.currentAppIndex >= 0 && this.currentAppIndex < this.apps.length) { 31 | return this.apps[this.currentAppIndex]; 32 | } 33 | return null; 34 | } 35 | 36 | nextApp(): NuimoApp { 37 | if (this.apps.length <= 0) { 38 | this.currentAppIndex = -1; 39 | return null; 40 | } 41 | 42 | this.currentAppIndex += 1; 43 | 44 | if (this.currentAppIndex >= this.apps.length) { 45 | this.currentAppIndex = -1; 46 | return null; 47 | } 48 | 49 | return this.currentApp(); 50 | } 51 | 52 | private currentAppIndex: number; 53 | } -------------------------------------------------------------------------------- /src/app/automapperConfig.ts: -------------------------------------------------------------------------------- 1 | // 'use strict'; 2 | 3 | // //import automapper = require('automapper'); 4 | // //import * as automapper from "automapper"; 5 | // import {automapper, AutoMapperJs} from "automapper"; 6 | 7 | // export class Base { 8 | // public apiJsonResult: any; 9 | // } 10 | 11 | // export class Person extends Base { 12 | 13 | // } 14 | 15 | // class MappingProfile implements AutoMapperJs.IProfile { 16 | // public sourceMemberNamingConvention = new AutoMapperJs.PascalCaseNamingConvention(); 17 | // public destinationMemberNamingConvention = new AutoMapperJs.CamelCaseNamingConvention(); 18 | 19 | // public profileName = 'PascalCaseToCamelCase'; 20 | 21 | // public configure(): void { 22 | // this.sourceMemberNamingConvention = new AutoMapperJs.PascalCaseNamingConvention(); 23 | // this.destinationMemberNamingConvention = new AutoMapperJs.CamelCaseNamingConvention(); 24 | // } 25 | // } 26 | 27 | // export class InitializeSamples { 28 | // public static initialize(): any { 29 | // automapper.initialize((cfg: IConfiguration) => { 30 | // cfg.addProfile(new MappingProfile()); 31 | // }); 32 | 33 | // const sourceKey = 'initialize'; 34 | // const destinationKey = '{}'; 35 | 36 | // const sourceObject = { FullName: 'John Doe' }; 37 | 38 | // automapper 39 | // .createMap(sourceKey, destinationKey) 40 | // .withProfile('PascalCaseToCamelCase'); 41 | 42 | // var result = automapper.map(sourceKey, destinationKey, sourceObject); 43 | 44 | // return result; 45 | // } 46 | // } 47 | 48 | // export class ForMemberSamples { 49 | // public static simpleMapFrom(): any { 50 | // const sourceKey = 'simpleMapFrom'; 51 | // const destinationKey = '{}'; 52 | 53 | // const sourceObject = { fullName: 'John Doe' }; 54 | 55 | // automapper 56 | // .createMap(sourceKey, destinationKey) 57 | // .forMember('name', (opts: AutoMapperJs.IMemberConfigurationOptions) => opts.mapFrom('fullName')); 58 | 59 | // var result = automapper.map(sourceKey, destinationKey, sourceObject); 60 | 61 | // return result; 62 | // } 63 | 64 | // public static stackedForMemberCalls(): any { 65 | // const sourceKey = 'stackedForMemberCalls'; 66 | // const destinationKey = 'Person'; 67 | 68 | // const sourceObject = { birthdayString: '2000-01-01T00:00:00.000Z' }; 69 | 70 | // automapper 71 | // .createMap(sourceKey, destinationKey) 72 | // .forMember('birthday', (opts: IMemberConfigurationOptions) => opts.mapFrom('birthdayString')) 73 | // .forMember('birthday', (opts: IMemberConfigurationOptions) => new Date(opts.sourceObject[opts.sourcePropertyName])); 74 | 75 | // var result = automapper.map(sourceKey, destinationKey, sourceObject); 76 | 77 | // return result; 78 | // } 79 | // } 80 | -------------------------------------------------------------------------------- /src/app/ledMatrices.ts: -------------------------------------------------------------------------------- 1 | export interface IconDictionary { 2 | [iconName: string]: string | string[] | number[] 3 | } 4 | 5 | let ledMatrices: IconDictionary = {}; 6 | 7 | ledMatrices['empty'] = [ 8 | "000000000", 9 | "000000000", 10 | "000000000", 11 | "000000000", 12 | "000000000", 13 | "000000000", 14 | "000000000", 15 | "000000000", 16 | "000000000"]; 17 | 18 | ledMatrices['musicNote'] = [ 19 | "000000000", 20 | "001111100", 21 | "001111100", 22 | "001000100", 23 | "001000100", 24 | "001000100", 25 | "011001100", 26 | "111011100", 27 | "010001000"]; 28 | 29 | ledMatrices['lightBulb'] = [ 30 | "000000000", 31 | "000111000", 32 | "001000100", 33 | "001000100", 34 | "001000100", 35 | "000111000", 36 | "000111000", 37 | "000111000", 38 | "000010000"]; 39 | 40 | ledMatrices['powerOn'] = [ 41 | "000000000", 42 | "000000000", 43 | "000111000", 44 | "001111100", 45 | "001111100", 46 | "001111100", 47 | "000111000", 48 | "000000000", 49 | "000000000"]; 50 | 51 | ledMatrices['powerOff'] = [ 52 | "000000000", 53 | "000000000", 54 | "000111000", 55 | "001000100", 56 | "001000100", 57 | "001000100", 58 | "000111000", 59 | "000000000", 60 | "000000000"]; 61 | 62 | ledMatrices['shuffle'] = [ 63 | "000000000", 64 | "000000000", 65 | "011000110", 66 | "000101000", 67 | "000010000", 68 | "000101000", 69 | "011000110", 70 | "000000000", 71 | "000000000"]; 72 | 73 | ledMatrices['letterB'] = [ 74 | "000000000", 75 | "000111000", 76 | "000100100", 77 | "000100100", 78 | "000111000", 79 | "000100100", 80 | "000100100", 81 | "000111000", 82 | "000000000"]; 83 | 84 | ledMatrices['letterO'] = [ 85 | "000000000", 86 | "000111000", 87 | "001000100", 88 | "001000100", 89 | "001000100", 90 | "001000100", 91 | "001000100", 92 | "000111000", 93 | "000000000"]; 94 | 95 | ledMatrices['letterG'] = [ 96 | "000000000", 97 | "000111000", 98 | "001000100", 99 | "001000000", 100 | "001011100", 101 | "001000100", 102 | "001000100", 103 | "000111000", 104 | "000000000"]; 105 | 106 | ledMatrices['letterW'] = [ 107 | "000000000", 108 | "010000010", 109 | "010000010", 110 | "010000010", 111 | "010000010", 112 | "010010010", 113 | "010010010", 114 | "001101100", 115 | "000000000"]; 116 | 117 | ledMatrices['letterY'] = [ 118 | "000000000", 119 | "001000100", 120 | "001000100", 121 | "000101000", 122 | "000010000", 123 | "000010000", 124 | "000010000", 125 | "000010000", 126 | "000000000"]; 127 | 128 | ledMatrices['play'] = [ 129 | "000000000", 130 | "000100000", 131 | "000110000", 132 | "000111000", 133 | "000111100", 134 | "000111000", 135 | "000110000", 136 | "000100000", 137 | "000000000"]; 138 | 139 | ledMatrices['pause'] = [ 140 | "000000000", 141 | "001101100", 142 | "001101100", 143 | "001101100", 144 | "001101100", 145 | "001101100", 146 | "001101100", 147 | "001101100", 148 | "000000000"]; 149 | 150 | ledMatrices['next'] = [ 151 | "000000000", 152 | "000000000", 153 | "000100100", 154 | "000110100", 155 | "000111100", 156 | "000110100", 157 | "000100100", 158 | "000000000", 159 | "000000000"]; 160 | 161 | ledMatrices['previous'] = [ 162 | "000000000", 163 | "000000000", 164 | "001001000", 165 | "001011000", 166 | "001111000", 167 | "001011000", 168 | "001001000", 169 | "000000000", 170 | "000000000"]; 171 | 172 | ledMatrices['questionMark'] = [ 173 | "000111000", 174 | "001000100", 175 | "010000010", 176 | "000000100", 177 | "000001000", 178 | "000010000", 179 | "000010000", 180 | "000000000", 181 | "000010000"]; 182 | 183 | ledMatrices['bluetooth'] = [ 184 | "000010000", 185 | "000011000", 186 | "001010100", 187 | "000111000", 188 | "000010000", 189 | "000111000", 190 | "001010100", 191 | "000011000", 192 | "000010000"]; 193 | 194 | ledMatrices['speaker'] = [ 195 | "000000000", 196 | "000100010", 197 | "001101001", 198 | "111100101", 199 | "111100101", 200 | "111100101", 201 | "001101001", 202 | "000100010", 203 | "000000000"]; 204 | 205 | 206 | ledMatrices['mutedSpeaker'] = [ 207 | "100000001", 208 | "010100010", 209 | "001101101", 210 | "111101101", 211 | "111110101", 212 | "111101101", 213 | "001101101", 214 | "010100010", 215 | "100000001"]; 216 | 217 | export default ledMatrices; -------------------------------------------------------------------------------- /src/app/nuimoApp.ts: -------------------------------------------------------------------------------- 1 | export class NuimoApp { 2 | constructor(id: string, name: string, icon: string | Array | Array) { 3 | this.id = id; 4 | this.name = name; 5 | this.icon = icon; 6 | } 7 | 8 | id: string; 9 | name: string; 10 | icon: string | Array | Array 11 | } -------------------------------------------------------------------------------- /src/app/nuimoEventConverter.ts: -------------------------------------------------------------------------------- 1 | import { NuimoEventMessage, NuimoGesture, NuimoGestureEvent } from "./nuimoMqttMessages"; 2 | import { Update, ClickUpdate, FlyUpdate, SwipeUpdate, TurnUpdate } from "nuimo-client-ts"; 3 | 4 | export default function createNuimoEventMessage(update: Update): NuimoEventMessage { 5 | let message: NuimoEventMessage = null; 6 | 7 | if(update instanceof ClickUpdate) { 8 | message = handleClickUpdate(update); 9 | } 10 | else if(update instanceof FlyUpdate) { 11 | message = handleFlyUpdate(update); 12 | } 13 | else if(update instanceof SwipeUpdate) { 14 | message = handleSwipeUpdate(update); 15 | } 16 | else if(update instanceof TurnUpdate) { 17 | message = handleTurnUpdate(update); 18 | } 19 | 20 | return message; 21 | }; 22 | 23 | function handleClickUpdate(update: ClickUpdate): NuimoEventMessage { 24 | let gesture: NuimoGesture = update.down ? NuimoGesture.ButtonPress : NuimoGesture.ButtonRelease; 25 | let message = new NuimoEventMessage(gesture); 26 | return message; 27 | } 28 | 29 | function handleFlyUpdate(update: FlyUpdate): NuimoEventMessage { 30 | let gesture: NuimoGesture; 31 | switch(update.direction) { 32 | case "l": gesture = NuimoGesture.FlyLeft; break; 33 | case "r": gesture = NuimoGesture.FlyRight; break; 34 | case "b": gesture = NuimoGesture.FlyBackwards; break; 35 | case "t": gesture = NuimoGesture.FlyTowards; break; 36 | case "u": gesture = NuimoGesture.FlyUp; break; 37 | case "d": gesture = NuimoGesture.FlyDown; break; 38 | } 39 | let speed = update.speed; 40 | let message = new NuimoEventMessage(gesture, speed); 41 | return message; 42 | } 43 | 44 | function handleSwipeUpdate(update: SwipeUpdate): NuimoEventMessage { 45 | let gesture: NuimoGesture; 46 | switch(update.direction) { 47 | case "l": gesture = NuimoGesture.SwipeLeft; break; 48 | case "r": gesture = NuimoGesture.SwipeRight; break; 49 | case "u": gesture = NuimoGesture.SwipeUp; break; 50 | case "d": gesture = NuimoGesture.SwipeDown; break; 51 | } 52 | let message = new NuimoEventMessage(gesture); 53 | return message; 54 | } 55 | 56 | function handleTurnUpdate(update: TurnUpdate): NuimoEventMessage { 57 | let gesture: NuimoGesture = (update.offset < 0) ? NuimoGesture.RotateLeft : NuimoGesture.RotateRight; 58 | let offset = update.offset; 59 | let message = new NuimoEventMessage(gesture, offset); 60 | return message; 61 | } -------------------------------------------------------------------------------- /src/app/nuimoManager.ts: -------------------------------------------------------------------------------- 1 | import "source-map-support/register"; 2 | 3 | import { NuimoClient, withNuimo, Update } from "nuimo-client-ts"; 4 | 5 | export class NuimoManager { 6 | nuimo: NuimoClient = null; 7 | 8 | nuimoDelegate: NuimoDelegate = null; 9 | 10 | connect = async (): Promise => { 11 | console.log("awaiting nuimo"); 12 | this.nuimo = await withNuimo(); 13 | console.log("connected. starting to listen"); 14 | this.nuimo.listen(this.processEvent); 15 | console.log("listening is set up"); 16 | return; 17 | } 18 | 19 | showIcon = async (icon: NuimoIcon, brightness: number, duration: number): Promise => { 20 | if (this.nuimo) { 21 | let fixedIcon = fixIcon(icon); 22 | let ledMatrixBuffer = this.nuimo.createLEDMatrixBuffer(fixedIcon); 23 | await this.nuimo.writeLEDS(ledMatrixBuffer, brightness, duration); 24 | } 25 | return; 26 | } 27 | 28 | processEvent = (update: Update): void => { 29 | if (this.nuimoDelegate !== null) { 30 | this.nuimoDelegate.receiveUpdate(update); 31 | } 32 | } 33 | 34 | setDelegate = (delegate: NuimoDelegate): void => { 35 | this.nuimoDelegate = delegate; 36 | } 37 | } 38 | 39 | function fixIcon(icon: NuimoIcon): string[] { 40 | let unfixedIconString = ""; 41 | if (icon instanceof Array) { 42 | unfixedIconString = icon.join(""); 43 | } else { 44 | unfixedIconString = icon; 45 | } 46 | 47 | let tempArr = unfixedIconString.split(""); 48 | if (tempArr.length !== 81) { 49 | throw "data must be 81 bits"; 50 | } 51 | 52 | let fixedArray = tempArr.map(x => { 53 | switch (x) { 54 | case " ": return "0"; 55 | case "0": return "0"; 56 | case "1": return "1"; 57 | default: return "1"; 58 | } 59 | }) 60 | return fixedArray; 61 | } 62 | 63 | export type NuimoIcon = string | Array | Array 64 | 65 | export interface NuimoDelegate { 66 | receiveUpdate(nuimoEvent: Update): void; 67 | } -------------------------------------------------------------------------------- /src/app/nuimoMqttMessages.ts: -------------------------------------------------------------------------------- 1 | export class NuimoMqttMessage implements Serializable { 2 | constructor(command: string) { 3 | this.command = command; 4 | } 5 | 6 | command: string; 7 | 8 | deserialize(input: any): NuimoMqttMessage { 9 | this.command = input.command; 10 | return this; 11 | }; 12 | } 13 | 14 | export class NuimoMqttRegisterCommand extends NuimoMqttMessage { 15 | constructor() 16 | constructor(id: string, name: string, icon: NuimoIcon) 17 | constructor(id?: any, name?: any, icon?: any) { 18 | super("register"); 19 | 20 | if (id !== undefined) { 21 | this.id = id; 22 | this.name = name; 23 | this.icon = icon; 24 | } 25 | } 26 | 27 | id: string; 28 | name: string; 29 | icon: NuimoIcon; 30 | 31 | deserialize(input: any): NuimoMqttRegisterCommand { 32 | this.id = input.id; 33 | this.name = input.name; 34 | this.icon = input.icon; 35 | return this; 36 | } 37 | } 38 | 39 | export class NuimoMqttUnregisterCommand extends NuimoMqttMessage { 40 | constructor() 41 | constructor(id: string) 42 | constructor(id?: any) { 43 | super("unregister"); 44 | 45 | if (id !== undefined) { 46 | this.id = id; 47 | } 48 | } 49 | 50 | id: string; 51 | 52 | deserialize(input: any): NuimoMqttUnregisterCommand { 53 | this.id = input.id; 54 | return this; 55 | } 56 | } 57 | 58 | export class NuimoListenPleaseCommand extends NuimoMqttMessage { 59 | constructor() 60 | constructor(id: string) 61 | constructor(id?: any) { 62 | super("listenPlease"); 63 | 64 | if (id !== undefined) { 65 | this.id = id; 66 | } 67 | } 68 | 69 | id: string; 70 | 71 | deserialize(input: any): NuimoListenPleaseCommand { 72 | this.id = input.id; 73 | return this; 74 | } 75 | } 76 | 77 | export class NuimoShowIconCommand extends NuimoMqttMessage { 78 | constructor() 79 | constructor(icon: NuimoIcon, brightness: number, duration: number) 80 | constructor(icon?: any, brightness?: any, duration?: any) { 81 | super("showIcon"); 82 | 83 | if (icon !== undefined) { 84 | this.icon = icon; 85 | this.brightness = brightness; 86 | this.duration = duration; 87 | } 88 | } 89 | icon: NuimoIcon; 90 | brightness: number; 91 | duration: number; 92 | 93 | deserialize(input: any): NuimoShowIconCommand { 94 | this.icon = input.icon; 95 | this.brightness = input.brightness; 96 | this.duration = input.duration; 97 | return this; 98 | } 99 | } 100 | 101 | export class NuimoShowNamedIconCommand extends NuimoMqttMessage { 102 | constructor() 103 | constructor(iconName: string, brightness: number, duration: number) 104 | constructor(iconName?: any, brightness?: any, duration?: any) { 105 | super("showNamedIcon"); 106 | 107 | if (iconName !== undefined) { 108 | this.iconName = iconName; 109 | this.brightness = brightness; 110 | this.duration = duration; 111 | } 112 | } 113 | 114 | iconName: string; 115 | brightness: number; 116 | duration: number; 117 | 118 | deserialize(input: any): NuimoShowNamedIconCommand { 119 | this.iconName = input.iconName; 120 | this.brightness = input.brightness; 121 | this.duration = input.duration; 122 | return this; 123 | } 124 | } 125 | 126 | export class NuimoShowProgressBarIconCommand extends NuimoMqttMessage { 127 | constructor() 128 | constructor(value: number, style: NuimoProgressBarStyle, brightness: number, duration: number) 129 | constructor(value?: any, style?: any, brightness?: any, duration?: any) { 130 | super("showProgressBarIcon"); 131 | if (value !== undefined) { 132 | this.value = value; 133 | this.style = style; 134 | this.brightness = brightness; 135 | this.duration = duration; 136 | } 137 | } 138 | 139 | value: number; 140 | style: NuimoProgressBarStyle; 141 | brightness: number; 142 | duration: number; 143 | 144 | deserialize(input: any): NuimoShowProgressBarIconCommand { 145 | this.value = input.value; 146 | this.style = input.style; 147 | this.brightness = input.brightness; 148 | this.duration = input.duration; 149 | return this; 150 | } 151 | } 152 | 153 | export class NuimoEventMessage extends NuimoMqttMessage implements NuimoGestureEvent { 154 | constructor() 155 | constructor(gesture: NuimoGesture, value?: number) 156 | constructor(gesture?: any, value?: any) { 157 | super("nuimoEvent"); 158 | 159 | if (gesture !== undefined) { 160 | this.gesture = gesture; 161 | this.value = value; 162 | } 163 | } 164 | 165 | gesture: NuimoGesture; 166 | value: number; 167 | 168 | deserialize(input: any): NuimoEventMessage { 169 | this.gesture = input.gesture; 170 | this.value = input.value; 171 | return this; 172 | } 173 | } 174 | 175 | export interface Serializable { 176 | deserialize(input: any): T; 177 | } 178 | 179 | export type NuimoIcon = string | Array | Array 180 | 181 | export interface NuimoGestureEvent { 182 | gesture: NuimoGesture; 183 | value: number; 184 | } 185 | 186 | export enum NuimoProgressBarStyle { 187 | VerticalBar = "VerticalBar", 188 | VolumeBar = "VolumeBar" 189 | } 190 | 191 | export enum NuimoGesture { 192 | Undefined = "Undefined", // TODO: Do we really need this enum value? We don't need to handle an "undefined" gesture 193 | ButtonPress = "ButtonPress", 194 | ButtonDoublePress = "ButtonDoublePress", 195 | ButtonRelease = "ButtonRelease", 196 | RotateLeft = "RotateLeft", 197 | RotateRight = "RotateRight", 198 | TouchLeftDown = "TouchLeftDown", 199 | TouchLeftRelease = "TouchLeftRelease", 200 | TouchRightDown = "TouchRightDown", 201 | TouchRightRelease = "TouchRightRelease", 202 | TouchTopDown = "TouchTopDown", 203 | TouchTopRelease = "TouchTopRelease", 204 | TouchBottomDown = "TouchBottomDown", 205 | TouchBottomRelease = "TouchBottomRelease", 206 | SwipeLeft = "SwipeLeft", 207 | SwipeRight = "SwipeRight", 208 | SwipeUp = "SwipeUp", 209 | SwipeDown = "SwipeDown", 210 | FlyLeft = "FlyLeft", 211 | FlyRight = "FlyRight", 212 | FlyBackwards = "FlyBackwards", 213 | FlyTowards = "FlyTowards", 214 | FlyUp = "FlyUp", 215 | FlyDown = "FlyDown" 216 | } -------------------------------------------------------------------------------- /src/test/nuimoMqttMessages.test.ts: -------------------------------------------------------------------------------- 1 | import { NuimoEventMessage, NuimoGesture, NuimoShowProgressBarIconCommand, NuimoProgressBarStyle } from "../app/nuimoMqttMessages"; 2 | import assert = require("assert"); 3 | import mocha = require("mocha") 4 | 5 | describe('NuimoShowProgressBarIconCommand', function() { 6 | it('should parse json well', function() { 7 | 8 | var testObject = new NuimoShowProgressBarIconCommand(0.77, NuimoProgressBarStyle.VolumeBar, 0.9, 0.3); 9 | var parsedJson = roundtripJson(testObject); 10 | var deserializedObject = new NuimoShowProgressBarIconCommand().deserialize(parsedJson); 11 | 12 | assert.deepEqual(testObject, deserializedObject, "deserializedObject is equal to testObject"); 13 | }); 14 | }); 15 | 16 | describe('NuimoEventMessage', function() { 17 | it('should parse json well', function() { 18 | 19 | var testObject = new NuimoEventMessage(NuimoGesture.ButtonPress); 20 | var parsedJson = roundtripJson(testObject); 21 | var deserializedObject = new NuimoEventMessage().deserialize(parsedJson); 22 | 23 | assert.deepEqual(testObject, deserializedObject, "deserializedObject is equal to testObject"); 24 | }); 25 | }); 26 | 27 | 28 | 29 | function roundtripJson(input: any): any{ 30 | var jsonString = JSON.stringify(input); 31 | var parsedJson = JSON.parse(jsonString); 32 | 33 | return parsedJson; 34 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmitOnError": true, 8 | "noImplicitAny": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | //"sourceRoot": "src", 12 | "outDir": "../build/", 13 | "declaration": true, 14 | "removeComments": true 15 | }, 16 | "exclude": [ 17 | "typings/browser.d.ts", 18 | "typings/browser" 19 | ] 20 | } -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "curly": true, 5 | "eofline": true, 6 | "forin": true, 7 | "indent": [ 8 | false 9 | ], 10 | "label-position": true, 11 | "label-undefined": true, 12 | "max-line-length": [ 13 | true, 14 | 140 15 | ], 16 | "no-arg": true, 17 | "no-bitwise": true, 18 | "no-console": [ 19 | true, 20 | "debug", 21 | "info", 22 | "time", 23 | "timeEnd", 24 | "trace" 25 | ], 26 | "no-construct": true, 27 | "no-debugger": true, 28 | "no-duplicate-key": true, 29 | "no-duplicate-variable": true, 30 | "no-empty": true, 31 | "no-eval": true, 32 | "no-string-literal": true, 33 | "no-switch-case-fall-through": true, 34 | "no-trailing-comma": true, 35 | "no-trailing-whitespace": true, 36 | "no-unused-expression": true, 37 | "no-unused-variable": false, 38 | "no-unreachable": true, 39 | "no-use-before-declare": true, 40 | "one-line": [ 41 | true, 42 | "check-open-brace", 43 | "check-catch", 44 | "check-else", 45 | "check-whitespace" 46 | ], 47 | "quotemark": [ 48 | true, 49 | "double" 50 | ], 51 | "radix": true, 52 | "semicolon": true, 53 | "triple-equals": [ 54 | true, 55 | "allow-null-check" 56 | ], 57 | "variable-name": false, 58 | "whitespace": [ 59 | true, 60 | "check-branch", 61 | "check-decl", 62 | "check-operator", 63 | "check-separator", 64 | "check-type" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "mqtt": "registry:dt/mqtt#0.0.0+20160317120654", 4 | "noble": "registry:dt/noble#0.0.0+20160406120732", 5 | "node": "registry:dt/node#4.0.0+20160330064709" 6 | }, 7 | "ambientDevDependencies": { 8 | "assert": "registry:dt/assert#0.0.0+20160317120654", 9 | "mocha": "registry:dt/mocha#2.2.5+20160317120654" 10 | }, 11 | "devDependencies": { 12 | "sinon": "registry:npm/sinon#1.16.0+20160309002336" 13 | } 14 | } 15 | --------------------------------------------------------------------------------