├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── docker-compose.yml ├── dockerfile ├── main.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // https://github.com/FredericHeem/react-node-starterkit/blob/master/.jshintrc 3 | // JSHint Default Configuration File (as on JSHint website) 4 | // See http://jshint.com/docs/ for more details 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // Unused variables: 30 | // true : all variables, last function parameter 31 | // "vars" : all variables only 32 | // "strict" : all variables, all function parameters 33 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 34 | "maxparams" : false, // {int} Max number of formal params allowed per function 35 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 36 | "maxstatements" : false, // {int} Max number statements per function 37 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 38 | "maxlen" : false, // {int} Max number of characters per line 39 | 40 | // Relaxing 41 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 42 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 43 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 44 | "eqnull" : false, // true: Tolerate use of `== null` 45 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 46 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 47 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 48 | // (ex: `for each`, multiple try/catch, function expression…) 49 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 50 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 51 | "funcscope" : false, // true: Tolerate defining variables inside control statements 52 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 53 | "iterator" : false, // true: Tolerate using the `__iterator__` property 54 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 55 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 56 | "laxcomma" : false, // true: Tolerate comma-first style coding 57 | "loopfunc" : false, // true: Tolerate functions being defined in loops 58 | "multistr" : false, // true: Tolerate multi-line strings 59 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 60 | "notypeof" : false, // true: Tolerate invalid typeof operator values 61 | "proto" : false, // true: Tolerate using the `__proto__` property 62 | "scripturl" : false, // true: Tolerate script-targeted URLs 63 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 64 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 65 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 66 | "validthis" : false, // true: Tolerate using this in a non-constructor function 67 | 68 | // Environments 69 | "browser" : false, // Web Browser (window, document, etc) 70 | "browserify" : false, // Browserify (node.js code in the browser) 71 | "couch" : false, // CouchDB 72 | "devel" : true, // Development/debugging (alert, confirm, etc) 73 | "dojo" : false, // Dojo Toolkit 74 | "jasmine" : false, // Jasmine 75 | "jquery" : false, // jQuery 76 | "mocha" : true, // Mocha 77 | "mootools" : false, // MooTools 78 | "node" : true, // Node.js 79 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 80 | "phantom" : false, // PhantomJS 81 | "prototypejs" : false, // Prototype and Scriptaculous 82 | "qunit" : false, // QUnit 83 | "rhino" : false, // Rhino 84 | "shelljs" : false, // ShellJS 85 | "typed" : false, // Globals for typed array constructions 86 | "worker" : false, // Web Workers 87 | "wsh" : false, // Windows Scripting Host 88 | "yui" : false, // Yahoo User Interface 89 | 90 | // Custom Globals 91 | "globals" : {} // additional predefined global variables 92 | } 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Tod E. Kurt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-blink1-server 2 | HTTP REST API server in Node for blink(1) devices 3 | 4 | Supports plug and unplug of blink(1) while server is running. 5 | 6 | Uses new `node-hid@0.5.1` so works with Node 4.x. 7 | 8 | ### Installation 9 | 10 | Install globally and use on the commandline: 11 | ``` 12 | npm install -g node-blink1-server 13 | blink1-server 8754 # starts server on port 8754 14 | ``` 15 | 16 | Or check out and use via npm: 17 | ``` 18 | git clone https://github.com/todbot/node-blink1-server.git 19 | cd node-blink1-server 20 | npm install 21 | npm start 8754 22 | ``` 23 | 24 | ### Supported URIs: 25 | - `/blink1` -- status info about connected blink1s, lastColor, etc. 26 | - `/blink1/fadeToRGB` -- fade blink(1) to a color. query args: 27 | - `rgb` -- hex color code (e.g. "#ff00ff") [required] 28 | - `time` -- fade time in seconds (default: 0.1) 29 | - `ledn` -- LED to control (0=both, 1=top, 2=bottom; default: 0) 30 | - `/blink1/blink` -- blink a color, query args: 31 | - `rgb` -- hex color code (e.g. "`#ff00ff`") [required] 32 | - `time` -- fade & blink time in seconds (default: 0.1) 33 | - `ledn` -- LED to control (0=both, 1=top, 2=bottom; default: 0) 34 | - `repeats` -- number of times to blink (default: 3) 35 | - `/blink1/pattern` -- blink a pattern of colors, query args: 36 | - `rgb` -- hex color codes separated by a comma (,) (e.g. "`#ff00ff`") [required] 37 | - `time` -- time in seconds between colors (default: 0.1) 38 | - `repeats` -- number of times to blink pattern (default: 3) 39 | - `/blink1/on` -- turn blink(1) on to full-on white (#FFFFFF) 40 | - `/blink1/red` -- turn blink(1) red (#FF0000) 41 | - `/blink1/green` -- turn blink(1) green (#00FF00) 42 | - `/blink1/blue` -- turn blink(1) blue (#000000) 43 | - `/blink1/yellow` -- turn blink(1) yellow (#FFFF00) 44 | - `/blink1/cyan` -- turn blink(1) cyan (#00FFFF) 45 | - `/blink1/magenta` -- turn blink(1) magenta (#FF00FF) 46 | - `/blink1/off` -- turn blink(1) off (#000000) 47 | 48 | ### Examples: 49 | ``` 50 | $ blink1-server 8754 & 51 | $ curl 'http://localhost:8754/blink1' 52 | { 53 | "blink1Connected": true, 54 | "blink1Serials": [ 55 | "AB0026C1" 56 | ], 57 | "lastColor": "#FF0000", 58 | "lastTime": 1.5, 59 | "lastLedn": 0, 60 | "lastRepeats": 0, 61 | "cmd": "info", 62 | "status": "success" 63 | } 64 | $ curl 'http://localhost:8754/blink1/fadeToRGB?rgb=%230000ff&time=2.5&ledn=2' 65 | { 66 | "blink1Connected": true, 67 | "blink1Serials": [ 68 | "200026C1" 69 | ], 70 | "lastColor": "#0000ff", 71 | "lastTime": 2.5, 72 | "lastLedn": 2, 73 | "lastRepeats": 0, 74 | "cmd": "fadeToRGB", 75 | "status": "success" 76 | } 77 | 78 | $ curl 'http://localhost:8754/blink1/pattern?rgb=%23ff0000,%23ffffff,%230000ff&time=.2&repeats=8' 79 | { 80 | "blink1Connected": true, 81 | "blink1Serials": [ 82 | "200026C1" 83 | ], 84 | "time": 0.2, 85 | "colors": [ 86 | "#ff0000", 87 | "#ffffff", 88 | "#0000ff" 89 | ], 90 | "repeats": 8, 91 | "cmd": "pattern", 92 | "status": "success" 93 | } 94 | ``` 95 | 96 | 97 | 98 | ### debugging 99 | ``` 100 | % DEBUG=Blink1Service node main.js 101 | ``` 102 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | blink1-node: 4 | build: . 5 | container_name: blink1-node 6 | network_mode: bridge 7 | ports: 8 | - "8081:8080" 9 | devices: 10 | - /dev/hidraw0 11 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM arm32v7/node:8.16.1-alpine 2 | 3 | # add necessary usb libraries 4 | RUN apk add --update --quiet libusb libusb-dev eudev-dev git 5 | # add build environment (will be deleted later) 6 | RUN apk add --no-cache --virtual .gyp python2 make g++ linux-headers 7 | 8 | # install blink1-server npm version 9 | RUN npm config set user root 10 | RUN npm install --silent -g node-blink1-server 11 | 12 | # git repo version 13 | #RUN git clone https://github.com/todbot/node-blink1-server.git 14 | #RUN cd node-blink1-server && npm install 15 | 16 | # cleanup 17 | RUN apk del --quiet .gyp git && rm -rf /var/cache/apk 18 | 19 | EXPOSE 8080 20 | 21 | # for git repo version 22 | #ENTRYPOINT ["npm", "--prefix", "/node-blink1-server", "start", "8080"] 23 | 24 | # for npm version 25 | ENTRYPOINT ["blink1-server", "8080"] 26 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * node-blink1-server 4 | * 5 | * 6 | * @author Tod E. Kurt, http://todbot.com/blog 7 | * 8 | */ 9 | 10 | "use strict"; 11 | 12 | var debug = require('debug')('http'); 13 | var Blink1 = require('node-blink1'); 14 | var parsecolor = require('parse-color'); 15 | var express = require('express'); 16 | 17 | var app = express(); 18 | app.set('json spaces', 4); 19 | 20 | var port = 8080; // default, can be set as an argument 21 | 22 | var devices = Blink1.devices(); // returns array of serial numbers 23 | var blink1 = null; 24 | if( devices.length ) { 25 | blink1 = new Blink1(); // gets first device found 26 | } 27 | 28 | var lastColor = '#000000'; 29 | var lastTime = 0; 30 | var lastLedn = 0; 31 | var lastRepeats = 0; 32 | 33 | // rescan if we know we have no blink1 34 | var blink1TryConnect = function() { 35 | if( blink1 ) { return false; } 36 | devices = Blink1.devices(); 37 | if( devices.length ) { 38 | blink1 = new Blink1(); 39 | } 40 | return true; 41 | }; 42 | 43 | // Call blink1.setRGB while dealing with disconnect / reconnect of blink1 44 | var blink1Set = function( r, g, b ){ 45 | blink1TryConnect(); 46 | if( !blink1 ) { return "no blink1"; } 47 | try { 48 | blink1.setRGB( r, g, b ); 49 | } catch(err) { 50 | blink1 = null; 51 | return ""+err; 52 | } 53 | return "success"; 54 | }; 55 | 56 | // Call blink1.fadeToRGB while dealing with disconnect / reconnect of blink1 57 | var blink1Fade = function( millis, r, g, b, ledn ){ 58 | blink1TryConnect(); 59 | if( !blink1 ) { return "no blink1"; } 60 | try { 61 | blink1.fadeToRGB( millis, r, g, b, ledn ); 62 | } catch(err) { 63 | blink1 = null; 64 | return ""+err; 65 | } 66 | return "success"; 67 | }; 68 | 69 | var blink1Blink = function( onoff, repeats, millis, r, g, b, ledn ) { 70 | // console.log("blink1Blink:", onoff, repeats, millis, r, g, b, ledn ); 71 | if( onoff ) { 72 | blink1Fade( millis/2, r, g, b, ledn ); 73 | } else { 74 | blink1Fade( millis/2, 0, 0, 0, ledn ); 75 | repeats--; 76 | } 77 | onoff = !onoff; 78 | if( repeats ) { 79 | setTimeout( function() { 80 | blink1Blink(onoff, repeats, millis, r, g, b, ledn); 81 | }, millis ); 82 | } 83 | }; 84 | 85 | var blink1Pattern = function(time, rgb, position) { 86 | blink1.writePatternLine(time * 1000, rgb[0], rgb[1], rgb[2], position); 87 | }; 88 | 89 | // parse the standard args into a data struct 90 | var parseQueryArgs = function(query) { 91 | var args = {}; 92 | args.color = parsecolor(query.rgb); 93 | args.time = Number(query.time) || 0.1; 94 | args.ledn = Number(query.ledn) || 0; 95 | args.repeats = Number(query.repeats) || 3; 96 | args.blink1_id = query.blink1_id; 97 | return args; 98 | }; 99 | 100 | app.get('/blink1', function(req, res) { 101 | blink1TryConnect(); 102 | var response = { 103 | blink1Connected: blink1 !== null, 104 | blink1Serials: devices, 105 | currentColor: '#000000', 106 | lastColor: lastColor, 107 | lastTime: lastTime, 108 | lastLedn: lastLedn, 109 | lastRepeats: lastRepeats, 110 | cmd: "info", 111 | status: "success" 112 | }; 113 | if( blink1 == null ) { // in case no blink1 plugged in 114 | res.json(response); 115 | return; 116 | } 117 | try { 118 | blink1.rgb(function(r, g, b) { 119 | var color = parsecolor("rgb("+r+","+g+","+b+")"); 120 | response.blink1Connected = true; 121 | response.currentColor = color.hex; 122 | res.json( response ); 123 | }); 124 | } catch(err) { 125 | blink1 = null; 126 | res.json( response ); 127 | } 128 | }); 129 | 130 | app.get('/blink1/:type(fadeToRGB|on|off|red|green|blue|yellow|cyan|magenta)', function(req, res) { 131 | if( req.params.type == 'on' ) { req.query.rgb = '#FFFFFF'; } 132 | else if( req.params.type == 'off' ) { req.query.rgb = '#000000'; } 133 | else if( req.params.type == 'red' ) { req.query.rgb = '#FF0000'; } 134 | else if( req.params.type == 'green' ) { req.query.rgb = '#00FF00'; } 135 | else if( req.params.type == 'blue' ) { req.query.rgb = '#0000FF'; } 136 | else if( req.params.type == 'yellow' ) { req.query.rgb = '#FFFF00'; } 137 | else if( req.params.type == 'cyan' ) { req.query.rgb = '#00FFFF'; } 138 | else if( req.params.type == 'magenta') { req.query.rgb = '#FF00FF'; } 139 | var args = parseQueryArgs(req.query); 140 | var status = req.params.type; 141 | 142 | if( typeof(args.color.rgb) != 'undefined' ) { 143 | lastColor = args.color.hex; 144 | lastTime = args.time; 145 | lastLedn = args.ledn; 146 | var rgb = args.color.rgb; 147 | status = blink1Fade( args.time*1000, rgb[0], rgb[1], rgb[2], args.ledn ); 148 | } 149 | else { 150 | status = "bad hex color specified '" + req.query.rgb + "'"; 151 | } 152 | 153 | var response = { 154 | blink1Connected: blink1 !== null, 155 | blink1Serials: devices, 156 | currentColor: lastColor, 157 | lastColor: lastColor, 158 | lastTime: lastTime, 159 | lastLedn: lastLedn, 160 | lastRepeats: lastRepeats, 161 | cmd: "fadeToRGB", 162 | status: status 163 | }; 164 | res.json( response ); 165 | }); 166 | 167 | app.get('/blink1/setRGB', function(req, res) { 168 | var color = parsecolor(req.query.rgb); 169 | var time = Number(req.query.time) || 0.1; 170 | var ledn = Number(req.query.ledn) || 0; 171 | var status = "success"; 172 | var rgb = color.rgb; 173 | 174 | if( rgb ) { 175 | lastColor = color.hex; 176 | lastTime = time; 177 | lastLedn = ledn; 178 | status = blink1Set( rgb[0], rgb[1], rgb[2] ); 179 | } 180 | else { 181 | status = "bad hex color specified " + req.query.rgb; 182 | } 183 | var response = { 184 | blink1Connected: blink1 !== null, 185 | blink1Serials: devices, 186 | currentColor: lastColor, 187 | lastColor: lastColor, 188 | lastTime: lastTime, 189 | lastLedn: lastLedn, 190 | lastRepeats: lastRepeats, 191 | cmd: "setRGB", 192 | status: status 193 | }; 194 | res.json( response ); 195 | }); 196 | 197 | app.get('/blink1/blink', function(req, res) { 198 | var color = parsecolor(req.query.rgb); 199 | var time = Number(req.query.time) || 0.1; 200 | var ledn = Number(req.query.ledn) || 0; 201 | var repeats = Number(req.query.repeats) || Number(req.query.count) || 3; 202 | var status = "success"; 203 | var rgb = color.rgb; 204 | if( rgb ) { 205 | lastColor = color.hex; 206 | lastTime = time; 207 | lastLedn = ledn; 208 | lastRepeats = repeats; 209 | blink1Blink( true, repeats, time*1000, rgb[0], rgb[1], rgb[2], ledn ); 210 | } 211 | else { 212 | status = "bad hex color specified " + req.query.rgb; 213 | } 214 | var response = { 215 | blink1Connected: blink1 !== null, 216 | blink1Serials: devices, 217 | currentColor: lastColor, 218 | lastColor: lastColor, 219 | lastTime: lastTime, 220 | lastLedn: lastLedn, 221 | lastRepeats: lastRepeats, 222 | cmd: "blink1", 223 | status: status 224 | }; 225 | res.json( response ); 226 | }); 227 | 228 | app.get('/blink1/pattern', function(req, res) { 229 | var colors = req.query.rgb.split(','); 230 | var time = Number(req.query.time) || 0.1; 231 | // var repeats = Number(req.query.repeats) || Number(req.query.count) || 3; 232 | var repeats = parseInt( req.query.repeats || req.query.count ); 233 | repeats = (repeats == NaN ) ? 3 : repeats; 234 | var status = "success"; 235 | 236 | blink1TryConnect(); 237 | if( blink1 ) { 238 | for (var i=0, len=colors.length; i < len; i++) { 239 | var rgb = parsecolor(colors[i]).rgb; 240 | blink1Pattern(time, rgb, i); 241 | } 242 | 243 | blink1.playLoop(0, colors.length, repeats); 244 | 245 | if (colors.length > 16) { 246 | status = "can only display first 16 colors. " + colors.length + " colors specified" 247 | } 248 | } 249 | else { 250 | status = "no blink1 connected"; 251 | } 252 | 253 | var response = { 254 | blink1Connected: blink1 !== null, 255 | blink1Serials: devices, 256 | time: time, 257 | colors: colors, 258 | repeats: repeats, 259 | cmd: "pattern", 260 | status: status 261 | }; 262 | 263 | res.json( response ); 264 | }); 265 | 266 | // respond with "Hello World!" on the homepage 267 | app.get('/', function(req, res) { 268 | res.send("" + 269 | "

Welcome to blink1-server

\n" + 270 | "

" + 271 | "Supported URIs:

\n" + 291 | "When starting server, argument specified is port to run on, e.g.:" + 292 | " blink1-server 8080 \n" + 293 | ""); 294 | }); 295 | 296 | 297 | // if we have args 298 | if( process.argv.length > 2 ) { 299 | var p = Number(process.argv[2]); 300 | port = (p) ? p : port; 301 | } 302 | 303 | var server = app.listen(port, function() { 304 | var host = server.address().address; 305 | var port = server.address().port; 306 | host = (host === '::' ) ? "localhost" : host; 307 | 308 | console.log('blink1-server listening at http://%s:%s/', host, port); 309 | }); 310 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-blink1-server", 3 | "version": "0.5.0", 4 | "description": "HTTP REST API server in Node for blink(1) devices", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/todbot/node-blink1-server.git" 8 | }, 9 | "main": "main.js", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "node main.js", 13 | "debug": "DEBUG=* node main.js" 14 | }, 15 | "bin": { 16 | "blink1-server": "./main.js" 17 | }, 18 | "author": "Tod E. Kurt", 19 | "license": "ISC", 20 | "keywords": [ 21 | "blink(1)", 22 | "blink1" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/todbot/node-blink1-server/issues" 26 | }, 27 | "dependencies": { 28 | "express": "^4.17.2", 29 | "node-blink1": "^0.5.1", 30 | "parse-color": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "debug": "^4.3.3" 34 | } 35 | } 36 | --------------------------------------------------------------------------------