-
Notifications
You must be signed in to change notification settings - Fork 1
/
server.js
414 lines (335 loc) · 10.9 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
/************************************************************
* imports
*/
var fs = require('fs');
var path = require("path");
var express = require('express');
var sockjs = require('sockjs');
var microtime = require('microtime');
var nanotimer = require('nanotimer');
var pngparse = require("pngparse");
var gm = require("gm");
require ("gm-buffer");
var myLedStripe = require('ledstripe');
var Gpio = require('onoff').Gpio;
/***********************************************************
* settings
*/
var imgDir = __dirname + '/img';
var mySPI = '/dev/spidev0.0';
var numLEDs = 32;
var bytePerPixel = 3; //RGB
var httpPort = 80; //the port the server will listen on, use 3000 if 80 will not work
var myFlashlightColor = "#303030";
/*************************************************************
* init variables
*/
var rowResetTime = 1000; // number of us CLK has to be pulled low (=no writes) for frame reset
// manual of WS2801 says 500 is enough, however we need at least 1000
var rowDelay = 10000; //in ns - 100 FPS
var rowsDropped = 0; //count dropped Frames
var app = express();
var deviceReady = false; // without an image buffer we are not ready
var myOutputSettings = {
RPS : 100, //rows per second
walkingSpeed : 120, // cm per second
brightness : 100 //manipulate brightness of image
};
var btnGoLast = 1; //last value of hardware go button
var btnLightLast = 1; //last value of hardware light button
var btnGo = new Gpio(2, 'in', 'both');
var btnLight = new Gpio(1, 'in', 'both');
var myImage = {
filename : path.join(imgDir, "rainbowsparkle.png"),
size : {
width : 1,
hight : 1
},
ratio : 1.0, //width:height ratio of image
imgBuffer : null
};
// just a black buffer
var blackBuffer = new Buffer(numLEDs*bytePerPixel);
for (var i=0; i<blackBuffer.length; i++){
blackBuffer[i]=0;
};
/*************************************************************
* connect to LED stripe
*/
myLedStripe.connect(numLEDs, 'WS2801', mySPI);
// signal startup with blue light
myLedStripe.fill(0x00, 0x00, 0x20);
/*************************************************************
* startup webserver
*/
app.use(express.logger('dev')); // log requests
app.use(express.static(__dirname + '/static'));
app.use('/img', express.static(__dirname + '/img'));
app.use(app.router);
var server = app.listen(httpPort);
console.log('listening on port' + httpPort);
/*************************************************************
* websockets setup
*/
var ws = sockjs.createServer();
var myWsConns = {};
function wsSend(o) {
for(var id in myWsConns){
try {
myWsConns[id].send(o);
} catch (e) {
console.error("Error sending to client " + id, e);
}
}
} // end wsSend
ws.on('connection', function(conn) {
function send(o) {
conn.write(JSON.stringify(o));
};
var id, n = 10000;
do {
id = Math.floor(n * Math.random());
n *= 10;
} while (myWsConns.hasOwnProperty(id));
myWsConns[id] = { conn: conn, send: send };
console.log("new client, setting id to " + id);
if (imageList!==null)
send({'updateImgList' : Object.keys(imageList)});
conn.on('data', function(message) {
var o;
// parsing message from client
try {
o = JSON.parse(message);
} catch (e) {
console.error("Invalid JSON on SockJS:", message);
}
if (!o)
return;
if (o.go) {
doLightpainting();
} else if (o.imageSelected){
myOutputSettings = (o.imageSelected.outputSettings);
setMyImage(o.imageSelected.imageName);
console.log(o);
} else if (o.colorFill){
colorFill(o.colorFill);
myFlashlightColor = (o.colorFill == '#000000') ? myFlashlightColor : o.colorFill;
}
}); // end conn.on('data')
conn.on('close', function() {
delete myWsConns[id];
console.log('Client ' + id + 'closed connection.');
});
});
ws.installHandlers(server, {prefix:'/sockjs'});
var imageList = null;
function parseImageDir(){
// http://nodeexamples.com/2012/09/28/getting-a-directory-listing-using-the-fs-module-in-node-js/
fs.readdir(imgDir, function (err, filenames) {
if (err) {
throw err;
}
imageList = new Array();
filenames.filter(function(filename){
var regEx = /.*.\.(jpg|jpeg|png)$/i;
return (regEx.test(filename) && fs.statSync(path.join(imgDir, filename)).isFile());
}).forEach(function (filename) {
imageList[filename] = {'aspectRatio' : null};
});
console.log(imageList);
console.log(Object.keys(imageList));
wsSend({'updateImgList' : Object.keys(imageList)});
});
}
parseImageDir();
function setMyImage(imagename, callback){
myImage.filename = path.join(imgDir, imagename);
gm(myImage.filename).size(function (err, size) {
if (err) {
console.log ("Error reading image " + myImage.filename);
throw err;
} else {
//console.log(size);
//console.log(myImage);
myImage.size = size;
myImage.ratio = myImage.size.width / myImage.size.height;
myImage.buffer = null;
var imageParms = {
widthInMeters : myImage.ratio * 1, // LightScyte is 1m high currently ToDo: parametrize this
};
wsSend({ 'imageSet' : {
'imageParms' : imageParms
}
});
prepareImageBuffer(function(){
if (callback)
callback();
});
} //end else if err
});
}
//var fd = fs.openSync(spiDevice, 'w');
//var isBusy = false;
//var lastWriteTime = microtime.now()-rowResetTime-1;
/*function isReady(){
return microtime.now() > (lastWriteTime + rowResetTime);
}
*/
function colorFill(color){ // hexstring #RRGGBB
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
try {
var r = parseInt(result[1], 16),
g = parseInt(result[2], 16),
b = parseInt(result[3], 16);
myLedStripe.fill(r,g,b);
console.log("Filling with color "+color);
} catch (e) {
console.error("Problem setting color",e);
}
};
/*
* write a row with RGB values to the strip
*/
// function writeRow(row, buffer){
// if (isReady()){
// fs.writeSync(fd, buffer, row*numLEDs*bytePerPixel, numLEDs*bytePerPixel, null);
// lastWriteTime = microtime.now();
// return true;
// }
// console.log('LED strip not ready, frame dropped: '+row);
// return false;
// }
// function writeFrame(buffer,frameDelay, callback){
// var row = 0;
// var framesDropped = 0;
// var rows = buffer.length/(numLEDs*bytePerPixel);
// var myTimer = new nanotimer();
// var tstart = microtime.now();
// myTimer.setInterval(function(){
// if (row>=rows){
// myTimer.clearInterval();
// var frametime = microtime.now()-tstart;
// if (callback)
// callback({'frametime' : frametime,
// 'rows' : rows,
// 'rowsPerSecond' : Math.round(rows*100000000/frametime,2)/100,
// 'framesDropped' : framesDropped
// });
// } else {
// //console.log("write row "+row);
// framesDropped += !writeRow(row,buffer);
// row++;
// }
// }, frameDelay, function(err) {
// if(err) {
// //error
// }
// });
// } //end writeFrame
/* PARSE DIRECTLY FROM resized and aligned png file
pngparse.parseFile(myImage.filename, function(err, data) {
if(err)
throw err
console.log(data);
myImage.imgBuffer = Buffer.concat([data.data, blackBuffer]);
//append 1 black row
writeFrame(myImage.imgBuffer,'10m',function(result){
console.log(result.rows+" rows in "+result.frametime+" us = "+result.rowsPerSecond+" rows/s with "+result.framesDropped+" dropped frames");
});
});
*/
function prepareImageBuffer(callback){
var imageheight = 1; // 1 Meter
var outputtime = 100 * myImage.ratio / myOutputSettings.walkingSpeed; // t = s/v ( v is in cm/sec !)
var imageWidthPx = Math.round(myOutputSettings.RPS * outputtime);
deviceReady = false;
gm(myImage.filename)
.resize(imageWidthPx,numLEDs,"!")
.rotate('black',90)
.modulate(myOutputSettings.brightness)
.setFormat('PNG')
.buffer(function(err, buf) {
pngparse.parse(buf, function(err, data) {
if(err) {
wsSend({'logmessage' : "error processing image"});
throw err
} else {
myImage.imgBuffer = Buffer.concat([data.data, blackBuffer]);
wsSend({'logmessage' : "image buffer ready"});
deviceReady = true;
wsSend({'deviceReady': deviceReady});
if (callback)
callback();
} //end if err
}); // pngparse
}); //bufer
};
function doLightpainting(){
console.log("Go for gold!");
var delay = Math.round(1000000/myOutputSettings.RPS) +"u"; //row delay in microseconds
if (myImage.imgBuffer !== null && deviceReady){
deviceReady = false;
//stop btn poll timer
clearInterval(myBtnPollTimer);
wsSend({'deviceReady':deviceReady});
myLedStripe.animate(myImage.imgBuffer,delay,function(result){
//what to do when finished
var message = "TODO: Animation stats";
//var message = result.rows+" rows in "+result.frametime+" us = "+result.rowsPerSecond+" rows/s with "+result.framesDropped+" dropped frames";
console.log(message);
wsSend({'logmessage':message});
deviceReady = true;
wsSend({'deviceReady':deviceReady});
//re-enablebutton poll timer
myBtnPollTimer = setInterval(btnPoll, 10);
});
} // end if imgBuffer !== null
}
function btnPoll(){
//poll for changes of hardware buttons, emit events
btnGo.read(function(err, value) { // Asynchronous read.
if (err) console.error("error reading go button", err);
if (value !== btnGoLast){
btnGoLast = value;
if (btnGoLast == 0)
doLightpainting();
}
});
btnLight.read(function(err, value) { // Asynchronous read.
if (err) console.error("error reading light button", err);
if (value !== btnLightLast){
btnLightLast = value;
if (btnLightLast == 0) {
colorFill(myFlashlightColor);
} else {
colorFill("#000000");
wsSend({'toggleBlank' : true});
}
}
});
}
// graceful exit (not necessary but we will play nice)
function gracefulExit() {
console.log( "Exiting gracefully on SIGINT or SIGTERM" )
// TODO close websocket connections
// TODO shutdown sockjs server
// shutdown express
server.close();
// switching all leds off
myLedStripe.fill(0x00, 0x00, 0x00);
// close conection to SPI
myLedStripe.disconnect();
process.exit( )
}
// shutdown on CTRL-C and SIGTERM
process.on('SIGINT', gracefulExit).on('SIGTERM', gracefulExit)
// polling buttons every 10ms
var myBtnPollTimer = setInterval(btnPoll, 10);
/*
* STARTUP ANIMATION
*/
setMyImage("rainbowsparkle.png", function(){
myLedStripe.animate(myImage.imgBuffer,'8m', function(){
colorFill("#200000");
});
});