server.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. const express = require('express');
  2. const dgram = require('dgram');
  3. const oscParser = require('./osc-parser');
  4. const bodyParser = require('body-parser');
  5. const { Bundle, Client } = require('node-osc');
  6. const path = require('path');
  7. const config = require('../frontend/assets/config.json');
  8. const app = express();
  9. app.use(bodyParser.json());
  10. app.use(bodyParser.urlencoded({extended:true}));
  11. app.use(express.static('./frontend/assets'));
  12. app.get('/', function (req, res) {
  13. res.sendFile('./frontend/index.html', {
  14. root: path.resolve(__dirname + '/..')
  15. });
  16. });
  17. app.get('/test', function (req, res) {
  18. res.sendFile('./frontend/test.html', {
  19. root: path.resolve(__dirname + '/..')
  20. });
  21. });
  22. const httpServer = require('http').Server(app);
  23. /* --------
  24. * RECEIVER
  25. * --------
  26. */
  27. const receiverIo = require('socket.io')(httpServer, {
  28. transports: ['websocket']
  29. });
  30. const onSocketListening = function() {
  31. const address = receiverUdpSocket.address();
  32. console.log('Serveur TUIO en écoute sur : ' + address.address + ':' + address.port);
  33. };
  34. const onSocketConnection = function(socket) {
  35. receiverUdpSocket.on('message', function(msg) {
  36. socket.emit('osc', oscParser.decode(msg));
  37. });
  38. };
  39. const receiverUdpSocket = dgram.createSocket('udp4');
  40. receiverUdpSocket.on('listening', onSocketListening);
  41. receiverUdpSocket.bind(config.app.oscUdpPort, '127.0.0.1');
  42. app.get('/receiver/json', function (req, res) {
  43. res.status(200).send();
  44. });
  45. receiverIo.sockets.on('connection', (socket) =>{
  46. console.log(`Connecté au client ${socket.id}`);
  47. const dgramCallback = function (buf) {
  48. if (config.app.debug && config.app.debugLog.backend.receiver.oscDatagram) {
  49. console.log(oscParser.decode(buf));
  50. }
  51. socket.emit('osc', oscParser.decode(buf));
  52. };
  53. // forward UDP packets via socket.io
  54. receiverUdpSocket.on('message', dgramCallback);
  55. // prevent memory leak on disconnect
  56. socket.on('disconnect', function (socket) {
  57. receiverUdpSocket.removeListener('message', dgramCallback);
  58. });
  59. });
  60. /* -------
  61. * EMITTER
  62. * -------
  63. */
  64. const emitterOscClient = new Client('127.0.0.1', config.app.oscUdpPort);
  65. let alive = [];
  66. let fseq;
  67. let objectsAlive = [] ;
  68. let lastDots = [];
  69. function getHypotenuse(touch1, touch2) {
  70. const x = Math.abs(touch1.x - touch2.x);
  71. const y = Math.abs(touch1.y - touch2.y);
  72. return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  73. }
  74. function getTop(dotTrio) {
  75. const dist01 = getHypotenuse(dotTrio[0], dotTrio[1]);
  76. const dist02 = getHypotenuse(dotTrio[0], dotTrio[2]);
  77. const dist12 = getHypotenuse(dotTrio[1], dotTrio[2]);
  78. const diff01m02 = Math.abs(dist01 - dist02);
  79. const diff01m12 = Math.abs(dist01 - dist12);
  80. const diff02m12 = Math.abs(dist02 - dist12);
  81. if (diff01m02 < diff02m12 && diff01m02 < diff01m12) {
  82. return 0;
  83. }
  84. else if (diff01m12 < diff01m02 && diff01m12 < diff02m12) {
  85. return 1;
  86. }
  87. else if (diff02m12 < diff01m02 && diff02m12 < diff01m12) {
  88. return 2;
  89. }
  90. }
  91. function getAngleApex(dotTrio, topIndex) {
  92. let dotA;
  93. let dotB;
  94. let dotC;
  95. dotB = dotTrio[topIndex];
  96. if (topIndex == 0) {
  97. dotA = dotTrio[1];
  98. dotC = dotTrio[2];
  99. }
  100. else if (topIndex == 1) {
  101. dotA = dotTrio[0];
  102. dotC = dotTrio[2];
  103. }
  104. else if (topIndex == 2) {
  105. dotA = dotTrio[0];
  106. dotC = dotTrio[1];
  107. }
  108. const AB = [dotB.x - dotA.x, dotB.y - dotA.y];
  109. const CB = [dotB.x - dotC.x, dotB.y - dotC.y];
  110. const dotProd = (AB[0] * CB[0] + AB[1] * CB[1]);
  111. const crossProd = (AB[0] * CB[1] - AB[1] * CB[0]);
  112. const alpha = Math.atan2(crossProd, dotProd);
  113. //return alpha ;
  114. return Math.abs(Math.floor(alpha * 180. / Math.PI + 0.5)) ;
  115. }
  116. function getOrientation(dotTrio, topIndex) {
  117. let dotA;
  118. let dotB;
  119. let dotC;
  120. dotB = dotTrio[topIndex];
  121. if (topIndex == 0) {
  122. dotA = dotTrio[1];
  123. dotC = dotTrio[2];
  124. }
  125. else if (topIndex == 1) {
  126. dotA = dotTrio[0];
  127. dotC = dotTrio[2];
  128. }
  129. else if (topIndex == 2) {
  130. dotA = dotTrio[0];
  131. dotC = dotTrio[1];
  132. }
  133. const middlePt = [(dotA.x + dotC.x) / 2, (dotA.y + dotC.y) / 2 ] ;
  134. let diff = [dotB.x - middlePt[0], dotB.y - middlePt[1]] ;
  135. //const length = Math.sqrt(Math.pow(diff[0], 2) + Math.pow(diff[1], 2) ) ;
  136. //normalize diff
  137. //diff = [diff[0] / length, diff[1] / length];
  138. const rad = Math.atan2(diff[1], diff[0]) ;
  139. return Math.floor(rad * 180 / Math.PI) ;
  140. //return length ;
  141. }
  142. function getSizeOfOpposedSegment(dots, apex){
  143. let res = 0;
  144. if(apex == 0){
  145. res = getHypotenuse(dots[1], dots[2]);
  146. } else if(apex == 1) {
  147. res = getHypotenuse(dots[0], dots[2]);
  148. } else if(apex == 2) {
  149. res = getHypotenuse(dots[0], dots[1]);
  150. }
  151. return res;
  152. }
  153. function objectGarbageCollector(){
  154. //si un point dans last dots est detecté dans un des triangle alors on ne réduit pas sa duration
  155. for(const triangle of objectsAlive){
  156. if(triangle.dots.some(dot => lastDots.some(lastDot => lastDot.target == dot.target))){
  157. } else {
  158. triangle.remainingDuration -= 1;
  159. }
  160. };
  161. objectsAlive = objectsAlive.filter(triangle => triangle.remainingDuration > 0);
  162. if (config.app.debug && config.app.debugLog.backend.emitter.garbageCollection) {
  163. console.log("--garbageCollection--", lastDots);
  164. objectsAlive.forEach(tr => console.log(tr));
  165. }
  166. createBundle();
  167. }
  168. setInterval(objectGarbageCollector, config.app.garbageCollectorInterval);
  169. let currentOscBundle = null;
  170. let hasPending = false;
  171. function createBundle(){
  172. currentOscBundle = new Bundle ;
  173. currentOscBundle.append([ '/tuio/2Dobj', 'alive'].concat(objectsAlive.map(t => t.matchingObject.apexAngle) ));
  174. for(const triangle of objectsAlive){
  175. currentOscBundle.append([
  176. '/tuio/2Dobj',
  177. 'set',
  178. triangle.matchingObject.apexAngle,
  179. triangle.matchingObject.apexAngle,
  180. triangle.target, //we use x as target identifier
  181. triangle.center[1],
  182. triangle.orientation,
  183. 0.0,
  184. 0.0,
  185. 0.0,
  186. 0.0,
  187. 0.0
  188. ]);
  189. }
  190. currentOscBundle.append(['/tuio/2Dobj', 'fseq', fseq]);
  191. hasPending = true;
  192. }
  193. function listDots(touches){
  194. const dots = [];
  195. for(const touch of touches){
  196. dots.push({
  197. id: touch.identifier,
  198. x: touch.clientX,
  199. y: touch.clientY,
  200. target: touch.target
  201. });
  202. };
  203. if (config.app.debug && config.app.debugLog.backend.emitter.dots) {
  204. console.log('-- dots --', dots);
  205. }
  206. return dots
  207. }
  208. function listSegments(dots){
  209. const segments = [];
  210. if (dots.length > 2) {
  211. for (var i = 0; i < dots.length; i++) {
  212. for (var j = 0; j < dots.length; j++) {
  213. if (j !== i) {
  214. /* on vérifie que le segment n'est pas déjà listé */
  215. const alreadyExists = segments.find(segment => {
  216. return segment.identifiers.includes(i) && segment.identifiers.includes(j);
  217. });
  218. /* on calcule la taille du segment (l'hypoténuse) */
  219. var hyp = getHypotenuse(dots[i], dots[j]);
  220. /* on garde uniquement les segments inférieurs à 750px (valeur par défaut)
  221. * cette valeur est la variable de configuration "maxDistanceBetweenPoints" */
  222. if (!alreadyExists && hyp <= config.app.maxDistanceBetweenPoints) {
  223. segments.push({
  224. identifiers: [i, j],
  225. x1: dots[i].x,
  226. x2: dots[j].x,
  227. y1: dots[i].y,
  228. y2: dots[j].y,
  229. target: dots[i].target,
  230. hyp
  231. });
  232. }
  233. }
  234. }
  235. }
  236. }
  237. if (config.app.debug && config.app.debugLog.backend.emitter.segments) {
  238. console.log('-- segments --', segments);
  239. }
  240. return segments;
  241. }
  242. function listTriangles(segments){
  243. const triangles = [];
  244. /* on boucle sur les segments */
  245. for(const segment of segments) {
  246. const dot1 = segment.identifiers[0];
  247. const dot2 = segment.identifiers[1];
  248. /* on vérifie que le triangle n'est pas déjà listé */
  249. const alreadyExists = triangles.find(triangle => {
  250. return triangle.includes(dot1) && triangle.includes(dot2);
  251. });
  252. if (!alreadyExists) {
  253. /* on cherche les segments qui contiennent un des 2 points du segment actuel
  254. * ex: si le segment actuel est AB, on cherche un segment contenant A (pour AC) et un autre contenant B (pour BC) */
  255. const found1 = segments.findIndex(seg => {
  256. return (seg.identifiers.includes(dot1) && !seg.identifiers.includes(dot2));
  257. });
  258. const found2 = segments.findIndex(seg => {
  259. return (seg.identifiers.includes(dot2) && !seg.identifiers.includes(dot1));
  260. });
  261. /* si on trouve bien les 2 segments (AC et BC), on peut créer un triangle */
  262. if (found1 !== -1 && found2 !== -1) {
  263. /* on devine quel est le 3ème point du triangle par rapport au segment actuel (le point C par rapport au segment AB) */
  264. const dot3 = segments[found1].identifiers.find(identifier => {
  265. return identifier !== dot1 && identifier !== dot2;
  266. });
  267. triangles.push([dot1, dot2, dot3]);
  268. }
  269. }
  270. };
  271. if (config.app.debug && config.app.debugLog.backend.emitter.triangles) {
  272. console.log('-- triangles --', triangles);
  273. }
  274. return triangles
  275. }
  276. function filterTriangles(dots, triangles){
  277. const filteredTriangles = [];
  278. /* Définition de l'apex, de la position du centre et de l'orientation */
  279. for(const triangle of triangles){
  280. const objTriangle = {} ;
  281. objTriangle.dots = [];
  282. objTriangle.dots[0] = dots[triangle[0]];
  283. objTriangle.dots[1] = dots[triangle[1]];
  284. objTriangle.dots[2] = dots[triangle[2]];
  285. objTriangle.target = objTriangle.dots[0].target
  286. objTriangle.apex = getTop(objTriangle.dots);
  287. objTriangle.center = [
  288. (objTriangle.dots[0].x + objTriangle.dots[1].x + objTriangle.dots[2].x) / 3,
  289. (objTriangle.dots[0].y + objTriangle.dots[1].y + objTriangle.dots[2].y) / 3
  290. ];
  291. objTriangle.angleApex = getAngleApex(objTriangle.dots, objTriangle.apex);
  292. objTriangle.orientation = getOrientation(objTriangle.dots, objTriangle.apex);
  293. objTriangle.sizeOfOpposedSegment = getSizeOfOpposedSegment(objTriangle.dots, objTriangle.apex);
  294. if (config.app.debug && config.app.debugLog.backend.emitter.apex) {
  295. console.log('-- apex --', objTriangle.apex);
  296. console.log('angle: ', objTriangle.angleApex);
  297. console.log('centerPos: ' + objTriangle.center);
  298. console.log('orientation: ' + objTriangle.orientation);
  299. }
  300. //verify if triangle has a corresponding triangle in config
  301. const matchingObject = config.objects.find(item => {
  302. return objTriangle.angleApex > item.apexAngle - config.app.matchingTolerance &&
  303. objTriangle.angleApex < item.apexAngle + config.app.matchingTolerance &&
  304. objTriangle.sizeOfOpposedSegment > item.sizeOfOpposedSegment - config.app.matchingSizeTolerance &&
  305. objTriangle.sizeOfOpposedSegment < item.sizeOfOpposedSegment + config.app.matchingSizeTolerance;
  306. });
  307. if (matchingObject) {
  308. objTriangle.matchingObject = matchingObject;
  309. filteredTriangles.push(objTriangle);
  310. if (config.app.debug && config.app.debugLog.backend.emitter.matchingObject) {
  311. console.log('-- matchingObject --', objTriangle);
  312. }
  313. }
  314. };
  315. return filteredTriangles;
  316. }
  317. function updateAliveTriangles(filteredTriangles) {
  318. for (const triangle of filteredTriangles) {
  319. let idx = objectsAlive.findIndex(item => {
  320. return triangle.matchingObject.name == item.matchingObject.name;
  321. });
  322. if (idx == -1) {
  323. triangle.remainingDuration = config.app.remainingDuration;
  324. if(objectsAlive.some(obj => obj.target == triangle.target)){
  325. console.log("already a triangle in this box");
  326. } else {
  327. objectsAlive.push(triangle);
  328. }
  329. } else {
  330. triangle.remainingDuration = config.app.remainingDuration;
  331. objectsAlive[idx] = triangle;
  332. }
  333. }
  334. if (config.app.debug && config.app.debugLog.backend.emitter.aliveTriangles) {
  335. console.log('-- aliveTriangles --', objectsAlive);
  336. }
  337. }
  338. const sendBundle = () => {
  339. if (hasPending) {
  340. emitterOscClient.send(currentOscBundle, () => {
  341. hasPending = false;
  342. });
  343. }
  344. setTimeout(() => {
  345. sendBundle();
  346. }, config.app.timerRefresh);
  347. };
  348. sendBundle();
  349. app.post('/emitter/json', function (req, res) {
  350. if (config.app.debug && config.app.debugLog.backend.emitter.httpRequest) {
  351. console.log('### Emitter POST request ###');
  352. }
  353. let oscBundle;
  354. // if (req.body.type === 'touchend') {
  355. // fseq = fseq ? fseq + 1 : 1;
  356. // const aliveMessage = [ '/tuio/2Dcur', 'alive' ].concat(alive);
  357. // currentOscBundle = new Bundle(
  358. // [ '/tuio/2Dcur', 'source', `tangibles${req.body.section.toString()}@127.0.0.1` ],
  359. // aliveMessage,
  360. // [ '/tuio/2Dcur', 'fseq', fseq ],
  361. // [
  362. // '/tuio/2Dcur',
  363. // 'del',
  364. // req.body.touches[0].identifier
  365. // ]
  366. // );
  367. // emitterOscClient.send(currentOscBundle, () => {
  368. // const index = alive.indexOf(req.body.touches[0].identifier);
  369. // alive.splice(index, 1);
  370. // if (alive.length === 0) {
  371. // currentOscBundle = new Bundle(
  372. // [ '/tuio/2Dcur', 'source', `tangibles${req.body.section.toString()}@127.0.0.1` ],
  373. // [ '/tuio/2Dcur', 'alive' ],
  374. // [ '/tuio/2Dcur', 'fseq', fseq ]
  375. // );
  376. // emitterOscClient.send(currentOscBundle, () => {
  377. // res.status(200).send();
  378. // fseq = 0;
  379. // hasPending = false;
  380. // });
  381. // } else {
  382. // res.status(200).send();
  383. // }
  384. // });
  385. // } else {
  386. if (req.body.touches && req.body.touches.length && req.body.touches.length > 0) {
  387. fseq = fseq ? fseq + 1 : 1;
  388. const touches = Object.keys(req.body.touches);
  389. // const aliveMessage = [ '/tuio/2Dcur', 'alive' ].concat(alive);
  390. // touches.forEach(touch => {
  391. // const id = req.body.touches[touch].identifier;
  392. // if (!alive.includes(id)) {
  393. // alive.push(id);
  394. // aliveMessage.push(id);
  395. // }
  396. // });
  397. /* Listage de tous les points */
  398. const dots = listDots(req.body.touches);
  399. lastDots = dots;
  400. const segments = listSegments(dots);
  401. const triangles = listTriangles(segments);
  402. const filteredTriangles = filterTriangles(dots, triangles);
  403. updateAliveTriangles(filteredTriangles);
  404. createBundle();
  405. res.status(200).send();
  406. } else {
  407. lastDots = [];
  408. res.status(200).send();
  409. }
  410. });
  411. httpServer.listen(config.app.httpPort, function () {
  412. console.log(`Votre app est disponible sur http://localhost:${config.app.httpPort} !`)
  413. });